Lower provider combat feedback in EveUnity
This commit is contained in:
parent
edf8f4fe62
commit
739cd9862f
15 changed files with 493 additions and 0 deletions
|
|
@ -15,6 +15,13 @@ selection filters; they are not product knowledge required by the client.
|
|||
|
||||
The package never imports provider product assemblies.
|
||||
|
||||
Retained provider feedback is lowered without becoming Unity authority.
|
||||
`EveUnityFeedbackPresenter` emits each new `feedback.event` identity once, and
|
||||
`EveUnityShotReceiptPresenter` does the same for `shot.receipt` trajectories.
|
||||
The generic `EveUnityShotTrajectoryRenderer` renders provider-authored origin,
|
||||
endpoint, duration, and hit outcome, then expires the visual locally. It never
|
||||
uses Unity collision to infer damage or report a hit back to the provider.
|
||||
|
||||
`fields.surface` projection consumes the plugin-owned
|
||||
`org.gamecult.eve.plugin-fields` contracts. `EveFieldsSplatRasterizer` owns the
|
||||
Unity structured-buffer and RenderTexture lowering without importing provider
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Eve.UnityScene
|
||||
{
|
||||
public sealed class EveUnityFeedbackEvent
|
||||
{
|
||||
public EveUnityFeedbackEvent(EveUnitySceneNode node)
|
||||
{
|
||||
EventId = Get(node, "eventId"); Kind = Get(node, "eventKind"); SubjectKey = Get(node, "subjectKey");
|
||||
ItemKey = Get(node, "itemKey"); Position = Get(node, "position");
|
||||
FrameId = Long(node, "frameId", -1); ZoneIndex = Int(node, "zoneIndex", -1);
|
||||
SourceEntityIndex = Int(node, "sourceEntityIndex", -1); TargetEntityIndex = Int(node, "targetEntityIndex", -1);
|
||||
PickupIndex = Int(node, "pickupIndex", -1); ScalarValue = Double(node, "scalarValue", 0);
|
||||
}
|
||||
public string EventId { get; }
|
||||
public string Kind { get; }
|
||||
public long FrameId { get; }
|
||||
public int ZoneIndex { get; }
|
||||
public int SourceEntityIndex { get; }
|
||||
public int TargetEntityIndex { get; }
|
||||
public int PickupIndex { get; }
|
||||
public string SubjectKey { get; }
|
||||
public string ItemKey { get; }
|
||||
public string Position { get; }
|
||||
public double ScalarValue { get; }
|
||||
private static string Get(EveUnitySceneNode node, string key) => node.Props.TryGetValue(key, out var value) ? value ?? "" : "";
|
||||
private static int Int(EveUnitySceneNode node, string key, int fallback) => int.TryParse(Get(node, key), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) ? value : fallback;
|
||||
private static long Long(EveUnitySceneNode node, string key, long fallback) => long.TryParse(Get(node, key), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) ? value : fallback;
|
||||
private static double Double(EveUnitySceneNode node, string key, double fallback) => double.TryParse(Get(node, key), NumberStyles.Float, CultureInfo.InvariantCulture, out var value) ? value : fallback;
|
||||
}
|
||||
|
||||
public sealed class EveUnityFeedbackPresenter
|
||||
{
|
||||
private readonly HashSet<string> _seen = new HashSet<string>(StringComparer.Ordinal);
|
||||
private bool _primed;
|
||||
public event Action<EveUnityFeedbackEvent>? FeedbackAvailable;
|
||||
|
||||
public int Apply(EveUnitySceneProjection projection)
|
||||
{
|
||||
if (projection == null) throw new ArgumentNullException(nameof(projection));
|
||||
var pending = new List<EveUnityFeedbackEvent>();
|
||||
Collect(projection.Root, pending);
|
||||
if (!_primed)
|
||||
{
|
||||
foreach (var value in pending) if (!string.IsNullOrWhiteSpace(value.EventId)) _seen.Add(value.EventId);
|
||||
_primed = true;
|
||||
return 0;
|
||||
}
|
||||
var emitted = 0;
|
||||
foreach (var value in pending)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value.EventId) || !_seen.Add(value.EventId)) continue;
|
||||
FeedbackAvailable?.Invoke(value); emitted++;
|
||||
}
|
||||
return emitted;
|
||||
}
|
||||
|
||||
public void Reset() { _seen.Clear(); _primed = false; }
|
||||
|
||||
private static void Collect(EveUnitySceneNode node, List<EveUnityFeedbackEvent> values)
|
||||
{
|
||||
if (string.Equals(node.ComponentKind, "feedback.event", StringComparison.Ordinal)) values.Add(new EveUnityFeedbackEvent(node));
|
||||
foreach (var child in node.Children) Collect(child, values);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 28dc73d4047dbbf4f926612650a72592
|
||||
|
|
@ -18,6 +18,7 @@ namespace GameCult.Eve.UnityScene
|
|||
[SerializeField] private bool connectOnEnable;
|
||||
[SerializeField] private bool refreshInUpdate = true;
|
||||
[SerializeField] private float refreshIntervalSeconds = 0.1f;
|
||||
[SerializeField] private bool renderShotTrajectories = true;
|
||||
|
||||
private float _nextRefreshAt;
|
||||
|
||||
|
|
@ -33,6 +34,9 @@ namespace GameCult.Eve.UnityScene
|
|||
|
||||
public long ActiveVersion => Runtime?.ActiveVersion ?? 0;
|
||||
|
||||
public event Action<EveUnityFeedbackEvent>? FeedbackAvailable;
|
||||
public event Action<EveUnityShotReceipt>? ShotAvailable;
|
||||
|
||||
public Transform SceneRoot => sceneRoot == null ? transform : sceneRoot;
|
||||
|
||||
public bool ConnectOnEnable
|
||||
|
|
@ -73,6 +77,14 @@ namespace GameCult.Eve.UnityScene
|
|||
Optional<IEveUnityPlayableWorldAssetManifestDocumentSource>(assetManifestDocuments),
|
||||
Optional<IEveUnitySceneCommandReceiptSource>(receiptSource),
|
||||
Optional<IEveUnityGameObjectAssetProvider>(fallbackAssetProvider));
|
||||
Runtime.FeedbackAvailable += OnFeedbackAvailable;
|
||||
Runtime.ShotAvailable += OnShotAvailable;
|
||||
if (renderShotTrajectories)
|
||||
{
|
||||
var trajectories = GetComponent<EveUnityShotTrajectoryRenderer>();
|
||||
if (trajectories == null) trajectories = gameObject.AddComponent<EveUnityShotTrajectoryRenderer>();
|
||||
trajectories.Bind(this);
|
||||
}
|
||||
|
||||
return Runtime.Connect();
|
||||
}
|
||||
|
|
@ -131,10 +143,18 @@ namespace GameCult.Eve.UnityScene
|
|||
|
||||
public void Disconnect()
|
||||
{
|
||||
if (Runtime != null)
|
||||
{
|
||||
Runtime.FeedbackAvailable -= OnFeedbackAvailable;
|
||||
Runtime.ShotAvailable -= OnShotAvailable;
|
||||
}
|
||||
Runtime?.Dispose();
|
||||
Runtime = null;
|
||||
}
|
||||
|
||||
private void OnFeedbackAvailable(EveUnityFeedbackEvent value) => FeedbackAvailable?.Invoke(value);
|
||||
private void OnShotAvailable(EveUnityShotReceipt value) => ShotAvailable?.Invoke(value);
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (connectOnEnable)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ namespace GameCult.Eve.UnityScene
|
|||
private readonly EveUnityPlayableWorldPresenter _presenter;
|
||||
private readonly IEveUnitySceneCommandReceiptSource? _receiptSource;
|
||||
private readonly bool _refreshOnTerminalReceipt;
|
||||
private readonly EveUnityFeedbackPresenter _feedback = new EveUnityFeedbackPresenter();
|
||||
private readonly EveUnityShotReceiptPresenter _shots = new EveUnityShotReceiptPresenter();
|
||||
private readonly List<EveUnitySceneCommandReceipt> _pendingReceipts =
|
||||
new List<EveUnitySceneCommandReceipt>();
|
||||
private bool _connected;
|
||||
|
|
@ -42,6 +44,17 @@ namespace GameCult.Eve.UnityScene
|
|||
public string SourcePointer => _connection.SourcePointer;
|
||||
|
||||
public event Action<EveUnitySceneCommandReceipt>? ReceiptAvailable;
|
||||
public event Action<EveUnityFeedbackEvent>? FeedbackAvailable
|
||||
{
|
||||
add { _feedback.FeedbackAvailable += value; }
|
||||
remove { _feedback.FeedbackAvailable -= value; }
|
||||
}
|
||||
|
||||
public event Action<EveUnityShotReceipt>? ShotAvailable
|
||||
{
|
||||
add { _shots.ShotAvailable += value; }
|
||||
remove { _shots.ShotAvailable -= value; }
|
||||
}
|
||||
|
||||
public EveUnityPlayableWorldPresentation Connect()
|
||||
{
|
||||
|
|
@ -116,6 +129,8 @@ namespace GameCult.Eve.UnityScene
|
|||
}
|
||||
|
||||
_connection.Disconnect();
|
||||
_feedback.Reset();
|
||||
_shots.Reset();
|
||||
_pendingReceipts.Clear();
|
||||
}
|
||||
|
||||
|
|
@ -146,6 +161,8 @@ namespace GameCult.Eve.UnityScene
|
|||
private void OnProjectionUpdated(EveUnitySceneProjection projection)
|
||||
{
|
||||
LastPresentation = _presenter.Apply(projection);
|
||||
_feedback.Apply(projection);
|
||||
_shots.Apply(projection);
|
||||
PublishReceiptsWhoseStateIsVisible();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -59,6 +59,18 @@ namespace GameCult.Eve.UnityScene
|
|||
remove { _client.ReceiptAvailable -= value; }
|
||||
}
|
||||
|
||||
public event Action<EveUnityFeedbackEvent>? FeedbackAvailable
|
||||
{
|
||||
add { _client.FeedbackAvailable += value; }
|
||||
remove { _client.FeedbackAvailable -= value; }
|
||||
}
|
||||
|
||||
public event Action<EveUnityShotReceipt>? ShotAvailable
|
||||
{
|
||||
add { _client.ShotAvailable += value; }
|
||||
remove { _client.ShotAvailable -= value; }
|
||||
}
|
||||
|
||||
public static EveUnityPlayableWorldRuntime CreateForGameObjectScene(
|
||||
Transform root,
|
||||
IEveUnitySceneProviderSurfaceDocumentSource surfaceDocuments,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,120 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Eve.UnityScene
|
||||
{
|
||||
public readonly struct EveUnityShotVector3
|
||||
{
|
||||
public EveUnityShotVector3(double x, double y, double z)
|
||||
{
|
||||
X = x;
|
||||
Y = y;
|
||||
Z = z;
|
||||
}
|
||||
|
||||
public double X { get; }
|
||||
public double Y { get; }
|
||||
public double Z { get; }
|
||||
}
|
||||
|
||||
public sealed class EveUnityShotReceipt
|
||||
{
|
||||
public EveUnityShotReceipt(EveUnitySceneNode node)
|
||||
{
|
||||
ShotId = Get(node, "shotId");
|
||||
FrameId = Long(node, "frameId", -1);
|
||||
ZoneIndex = Int(node, "zoneIndex", -1);
|
||||
SourceEntityIndex = Int(node, "sourceEntityIndex", -1);
|
||||
TargetEntityIndex = Int(node, "targetEntityIndex", -1);
|
||||
Hit = Bool(node, "hit");
|
||||
Outcome = Get(node, "outcome");
|
||||
Origin = Vector(node, "origin");
|
||||
Endpoint = Vector(node, "endpoint");
|
||||
DurationSeconds = Math.Max(0, Double(node, "presentationDuration", 0));
|
||||
PresentationKind = Get(node, "presentationKind");
|
||||
}
|
||||
|
||||
public string ShotId { get; }
|
||||
public long FrameId { get; }
|
||||
public int ZoneIndex { get; }
|
||||
public int SourceEntityIndex { get; }
|
||||
public int TargetEntityIndex { get; }
|
||||
public bool Hit { get; }
|
||||
public string Outcome { get; }
|
||||
public EveUnityShotVector3 Origin { get; }
|
||||
public EveUnityShotVector3 Endpoint { get; }
|
||||
public double DurationSeconds { get; }
|
||||
public string PresentationKind { get; }
|
||||
|
||||
private static string Get(EveUnitySceneNode node, string key) =>
|
||||
node.Props.TryGetValue(key, out var value) ? value ?? "" : "";
|
||||
|
||||
private static int Int(EveUnitySceneNode node, string key, int fallback) =>
|
||||
int.TryParse(Get(node, key), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) ? value : fallback;
|
||||
|
||||
private static long Long(EveUnitySceneNode node, string key, long fallback) =>
|
||||
long.TryParse(Get(node, key), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) ? value : fallback;
|
||||
|
||||
private static double Double(EveUnitySceneNode node, string key, double fallback) =>
|
||||
double.TryParse(Get(node, key), NumberStyles.Float, CultureInfo.InvariantCulture, out var value) ? value : fallback;
|
||||
|
||||
private static bool Bool(EveUnitySceneNode node, string key) =>
|
||||
bool.TryParse(Get(node, key), out var value) && value;
|
||||
|
||||
private static EveUnityShotVector3 Vector(EveUnitySceneNode node, string key)
|
||||
{
|
||||
var parts = Get(node, key).Split(',');
|
||||
if (parts.Length < 2) return new EveUnityShotVector3(0, 0, 0);
|
||||
var x = double.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var parsedX) ? parsedX : 0;
|
||||
var z = double.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var parsedZ) ? parsedZ : 0;
|
||||
return new EveUnityShotVector3(x, 0, z);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class EveUnityShotReceiptPresenter
|
||||
{
|
||||
private readonly HashSet<string> _seen = new HashSet<string>(StringComparer.Ordinal);
|
||||
private bool _primed;
|
||||
|
||||
public event Action<EveUnityShotReceipt>? ShotAvailable;
|
||||
|
||||
public int Apply(EveUnitySceneProjection projection)
|
||||
{
|
||||
if (projection == null) throw new ArgumentNullException(nameof(projection));
|
||||
var receipts = new List<EveUnityShotReceipt>();
|
||||
Collect(projection.Root, receipts);
|
||||
if (!_primed)
|
||||
{
|
||||
foreach (var receipt in receipts)
|
||||
if (!string.IsNullOrWhiteSpace(receipt.ShotId)) _seen.Add(receipt.ShotId);
|
||||
_primed = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
var emitted = 0;
|
||||
foreach (var receipt in receipts)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(receipt.ShotId) || !_seen.Add(receipt.ShotId)) continue;
|
||||
ShotAvailable?.Invoke(receipt);
|
||||
emitted++;
|
||||
}
|
||||
return emitted;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_seen.Clear();
|
||||
_primed = false;
|
||||
}
|
||||
|
||||
private static void Collect(EveUnitySceneNode node, ICollection<EveUnityShotReceipt> receipts)
|
||||
{
|
||||
if (string.Equals(node.ComponentKind, "shot.receipt", StringComparison.Ordinal))
|
||||
receipts.Add(new EveUnityShotReceipt(node));
|
||||
foreach (var child in node.Children) Collect(child, receipts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7af61307fcb149fc96d9f5d67f5aa1f3
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Eve.UnityScene
|
||||
{
|
||||
public sealed class EveUnityShotTrajectoryRenderer : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private float width = 0.2f;
|
||||
[SerializeField] private float minimumDurationSeconds = 0.03f;
|
||||
[SerializeField] private float lingerSeconds = 0.08f;
|
||||
[SerializeField] private Color hitColor = new Color(1f, 0.35f, 0.12f, 1f);
|
||||
[SerializeField] private Color missColor = new Color(0.35f, 0.8f, 1f, 0.8f);
|
||||
|
||||
private readonly List<ActiveTrajectory> _active = new List<ActiveTrajectory>();
|
||||
private EveUnityPlayableWorldClientHost? _host;
|
||||
private Material? _material;
|
||||
|
||||
public int ActiveTrajectoryCount => _active.Count;
|
||||
|
||||
public void Bind(EveUnityPlayableWorldClientHost host)
|
||||
{
|
||||
if (host == null) throw new ArgumentNullException(nameof(host));
|
||||
if (ReferenceEquals(_host, host)) return;
|
||||
Unbind();
|
||||
_host = host;
|
||||
_host.ShotAvailable += Present;
|
||||
}
|
||||
|
||||
public void Unbind()
|
||||
{
|
||||
if (_host != null) _host.ShotAvailable -= Present;
|
||||
_host = null;
|
||||
}
|
||||
|
||||
public void Present(EveUnityShotReceipt receipt)
|
||||
{
|
||||
if (receipt == null) throw new ArgumentNullException(nameof(receipt));
|
||||
var shot = new GameObject("Eve shot " + receipt.ShotId);
|
||||
shot.transform.SetParent(transform, false);
|
||||
var line = shot.AddComponent<LineRenderer>();
|
||||
line.useWorldSpace = true;
|
||||
line.positionCount = 2;
|
||||
line.startWidth = Math.Max(0.01f, width);
|
||||
line.endWidth = Math.Max(0.01f, width * 0.5f);
|
||||
line.material = SharedMaterial();
|
||||
line.startColor = line.endColor = receipt.Hit ? hitColor : missColor;
|
||||
|
||||
var origin = Vector(receipt.Origin);
|
||||
line.SetPosition(0, origin);
|
||||
line.SetPosition(1, origin);
|
||||
_active.Add(new ActiveTrajectory(
|
||||
shot,
|
||||
line,
|
||||
origin,
|
||||
Vector(receipt.Endpoint),
|
||||
Time.unscaledTime,
|
||||
Math.Max(minimumDurationSeconds, (float)receipt.DurationSeconds)));
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
var now = Time.unscaledTime;
|
||||
for (var index = _active.Count - 1; index >= 0; index--)
|
||||
{
|
||||
var trajectory = _active[index];
|
||||
var progress = Mathf.Clamp01((now - trajectory.StartedAt) / trajectory.Duration);
|
||||
trajectory.Line.SetPosition(1, Vector3.Lerp(trajectory.Origin, trajectory.Endpoint, progress));
|
||||
if (progress < 1 || now < trajectory.StartedAt + trajectory.Duration + lingerSeconds) continue;
|
||||
Destroy(trajectory.Root);
|
||||
_active.RemoveAt(index);
|
||||
}
|
||||
}
|
||||
|
||||
private Material SharedMaterial()
|
||||
{
|
||||
if (_material != null) return _material;
|
||||
var shader = Shader.Find("Sprites/Default") ?? Shader.Find("Universal Render Pipeline/Unlit");
|
||||
if (shader == null) throw new InvalidOperationException("EveUnity requires an unlit shader for shot trajectories.");
|
||||
_material = new Material(shader) { name = "EveUnity shot trajectory" };
|
||||
return _material;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
Unbind();
|
||||
foreach (var trajectory in _active)
|
||||
if (trajectory.Root != null) Destroy(trajectory.Root);
|
||||
_active.Clear();
|
||||
if (_material != null) Destroy(_material);
|
||||
}
|
||||
|
||||
private static Vector3 Vector(EveUnityShotVector3 value) =>
|
||||
new Vector3((float)value.X, (float)value.Y, (float)value.Z);
|
||||
|
||||
private sealed class ActiveTrajectory
|
||||
{
|
||||
public ActiveTrajectory(GameObject root, LineRenderer line, Vector3 origin, Vector3 endpoint, float startedAt, float duration)
|
||||
{
|
||||
Root = root;
|
||||
Line = line;
|
||||
Origin = origin;
|
||||
Endpoint = endpoint;
|
||||
StartedAt = startedAt;
|
||||
Duration = duration;
|
||||
}
|
||||
|
||||
public GameObject Root { get; }
|
||||
public LineRenderer Line { get; }
|
||||
public Vector3 Origin { get; }
|
||||
public Vector3 Endpoint { get; }
|
||||
public float StartedAt { get; }
|
||||
public float Duration { get; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 96ad4b7918444a82bc1ba2fd37ac2e39
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Eve.UnityScene.Tests
|
||||
{
|
||||
public sealed class EveUnityFeedbackPresenterTests
|
||||
{
|
||||
[Test]
|
||||
public void PresenterPrimesRetainedEventsAndEmitsEachNewIdentityOnce()
|
||||
{
|
||||
var presenter = new EveUnityFeedbackPresenter();
|
||||
var emitted = new List<EveUnityFeedbackEvent>();
|
||||
presenter.FeedbackAvailable += emitted.Add;
|
||||
|
||||
Assert.That(presenter.Apply(Projection(Event("old", "projectile.impact", 4))), Is.Zero);
|
||||
Assert.That(emitted, Is.Empty, "initial retained events must not replay on connect");
|
||||
|
||||
var next = Projection(Event("old", "projectile.impact", 4), Event("new", "entity.damaged", 5));
|
||||
Assert.That(presenter.Apply(next), Is.EqualTo(1));
|
||||
Assert.That(emitted.Count, Is.EqualTo(1));
|
||||
Assert.That(emitted[0].EventId, Is.EqualTo("new"));
|
||||
Assert.That(emitted[0].Kind, Is.EqualTo("entity.damaged"));
|
||||
Assert.That(emitted[0].FrameId, Is.EqualTo(5));
|
||||
|
||||
Assert.That(presenter.Apply(next), Is.Zero, "surface refresh must not replay an event identity");
|
||||
Assert.That(emitted.Count, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
private static EveUnitySceneProjection Projection(params EveUnitySceneNode[] events) =>
|
||||
new EveUnitySceneProjection("provider", "surface", "world", "commands", "receipt", "provider", null,
|
||||
Node("root", "surface", Node("feedback", "feedback.stream", events)));
|
||||
|
||||
private static EveUnitySceneNode Event(string id, string kind, long frame) =>
|
||||
new EveUnitySceneNode(id, "feedback.event", "scene-node",
|
||||
new Dictionary<string, string>(StringComparer.Ordinal) { ["eventId"] = id, ["eventKind"] = kind, ["frameId"] = frame.ToString() },
|
||||
Empty(), Empty(), 0, 0, Array.Empty<EveUnitySceneEmbeddedDocumentSlot>(), null, Array.Empty<EveUnitySceneNode>());
|
||||
|
||||
private static EveUnitySceneNode Node(string id, string kind, params EveUnitySceneNode[] children) =>
|
||||
new EveUnitySceneNode(id, kind, "scene-node", Empty(), Empty(), Empty(), 0, 0,
|
||||
Array.Empty<EveUnitySceneEmbeddedDocumentSlot>(), null, children);
|
||||
|
||||
private static IReadOnlyDictionary<string, string> Empty() => new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 6c518b1a24658e947bfac8a10a853b8e
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Eve.UnityScene.Tests
|
||||
{
|
||||
public sealed class EveUnityShotReceiptPresenterTests
|
||||
{
|
||||
[Test]
|
||||
public void PresenterPrimesRetainedShotsAndEmitsNewTrajectoryOnce()
|
||||
{
|
||||
var presenter = new EveUnityShotReceiptPresenter();
|
||||
var emitted = new List<EveUnityShotReceipt>();
|
||||
presenter.ShotAvailable += emitted.Add;
|
||||
|
||||
Assert.That(presenter.Apply(Projection(Shot("old", "0,0", "10,5", false))), Is.Zero);
|
||||
Assert.That(emitted, Is.Empty);
|
||||
|
||||
var next = Projection(
|
||||
Shot("old", "0,0", "10,5", false),
|
||||
Shot("new", "2,3", "20,30", true));
|
||||
Assert.That(presenter.Apply(next), Is.EqualTo(1));
|
||||
Assert.That(emitted, Has.Count.EqualTo(1));
|
||||
Assert.That(emitted[0].ShotId, Is.EqualTo("new"));
|
||||
Assert.That(emitted[0].Hit, Is.True);
|
||||
Assert.That(emitted[0].Origin.X, Is.EqualTo(2));
|
||||
Assert.That(emitted[0].Origin.Z, Is.EqualTo(3));
|
||||
Assert.That(emitted[0].Endpoint.X, Is.EqualTo(20));
|
||||
Assert.That(emitted[0].Endpoint.Z, Is.EqualTo(30));
|
||||
Assert.That(emitted[0].DurationSeconds, Is.EqualTo(0.25));
|
||||
Assert.That(emitted[0].PresentationKind, Is.EqualTo("test-laser"));
|
||||
Assert.That(presenter.Apply(next), Is.Zero);
|
||||
}
|
||||
|
||||
private static EveUnitySceneProjection Projection(params EveUnitySceneNode[] shots) =>
|
||||
new EveUnitySceneProjection("provider", "surface", "world", "commands", "receipt", "provider", null,
|
||||
Node("root", "surface", Node("shots", "shot.receipt-stream", shots)));
|
||||
|
||||
private static EveUnitySceneNode Shot(string id, string origin, string endpoint, bool hit) =>
|
||||
new EveUnitySceneNode(id, "shot.receipt", "scene-node",
|
||||
new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["shotId"] = id,
|
||||
["frameId"] = "12",
|
||||
["zoneIndex"] = "0",
|
||||
["sourceEntityIndex"] = "1",
|
||||
["targetEntityIndex"] = "2",
|
||||
["hit"] = hit.ToString(),
|
||||
["outcome"] = hit ? "hit" : "miss",
|
||||
["origin"] = origin,
|
||||
["endpoint"] = endpoint,
|
||||
["presentationDuration"] = "0.25",
|
||||
["presentationKind"] = "test-laser"
|
||||
}, Empty(), Empty(), 0, 0, Array.Empty<EveUnitySceneEmbeddedDocumentSlot>(), null,
|
||||
Array.Empty<EveUnitySceneNode>());
|
||||
|
||||
private static EveUnitySceneNode Node(string id, string kind, params EveUnitySceneNode[] children) =>
|
||||
new EveUnitySceneNode(id, kind, "scene-node", Empty(), Empty(), Empty(), 0, 0,
|
||||
Array.Empty<EveUnitySceneEmbeddedDocumentSlot>(), null, children);
|
||||
|
||||
private static IReadOnlyDictionary<string, string> Empty() =>
|
||||
new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 2fd315330f9c4acbac32249d6dcccb38
|
||||
|
|
@ -24,6 +24,9 @@
|
|||
"liveProviderTransportBridge",
|
||||
"livePlayableWorldClient",
|
||||
"providerCommandReceipts",
|
||||
"providerFeedbackEvents",
|
||||
"providerShotReceipts",
|
||||
"shotTrajectoryPresentation",
|
||||
"providerAssetManifestDocumentSource",
|
||||
"playableWorldClientHost",
|
||||
"playableWorldClientBootstrap",
|
||||
|
|
@ -108,6 +111,9 @@
|
|||
"packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientBootstrap.cs",
|
||||
"packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldInputDriver.cs",
|
||||
"packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs",
|
||||
"packages/org.gamecult.eve.unity-scene/Runtime/EveUnityFeedbackPresenter.cs",
|
||||
"packages/org.gamecult.eve.unity-scene/Runtime/EveUnityShotReceiptPresenter.cs",
|
||||
"packages/org.gamecult.eve.unity-scene/Runtime/EveUnityShotTrajectoryRenderer.cs",
|
||||
"packages/org.gamecult.eve.unity-scene/Runtime/EveUnityProviderRefreshSource.cs",
|
||||
"packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldPresenter.cs",
|
||||
"packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs",
|
||||
|
|
|
|||
Reference in a new issue