From d1240f9fab99dbd1ef24d23b0dcf90cd06ce4c86 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 12 Jul 2026 01:24:45 +0100 Subject: [PATCH] Resolve combat effects by presentation role --- .../EveUnityPlayableWorldAssetManifest.cs | 6 +++ .../EveUnityPlayableWorldClientHost.cs | 2 +- .../Runtime/EveUnityPlayableWorldRuntime.cs | 3 ++ .../Runtime/EveUnityShotReceiptPresenter.cs | 2 + .../Runtime/EveUnityShotTrajectoryRenderer.cs | 46 +++++++++++++++++-- .../EveUnityShotReceiptPresenterTests.cs | 13 ++++++ 6 files changed, 67 insertions(+), 5 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldAssetManifest.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldAssetManifest.cs index d6dd81a..23653d4 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldAssetManifest.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldAssetManifest.cs @@ -67,6 +67,12 @@ namespace GameCult.Eve.UnityScene return entry; return null; } + + public EveUnityPlayableWorldAssetManifestEntry? FindByPresentationRole(string presentationRole) + { + if (string.IsNullOrWhiteSpace(presentationRole)) return null; + return _byEntityKind.TryGetValue(presentationRole, out var entry) ? entry : null; + } } public interface IEveUnityPlayableWorldAssetManifestSource diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs index f30a134..a8787dc 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs @@ -83,7 +83,7 @@ namespace GameCult.Eve.UnityScene { var trajectories = GetComponent(); if (trajectories == null) trajectories = gameObject.AddComponent(); - trajectories.Bind(this); + trajectories.Bind(this, Runtime.GameObjectAssetProvider); } return Runtime.Connect(); diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs index 4af8a43..34d41bc 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs @@ -41,6 +41,8 @@ namespace GameCult.Eve.UnityScene public EveUnityPlayableWorldAssetManifestCache AssetManifests { get; } + public IEveUnityGameObjectAssetProvider? GameObjectAssetProvider { get; private set; } + public EveUnityPlayableWorldProjection? ActiveWorld => _client.ActiveWorld; public EveUnitySceneProjection? ActiveProjection => _client.ActiveProjection; @@ -95,6 +97,7 @@ namespace GameCult.Eve.UnityScene receiptSource, new EveUnityAssetRefResolver(), assetManifests); + runtime.GameObjectAssetProvider = liveAssetProvider; return runtime; } diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityShotReceiptPresenter.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityShotReceiptPresenter.cs index a7527b0..5f853ce 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityShotReceiptPresenter.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityShotReceiptPresenter.cs @@ -35,6 +35,7 @@ namespace GameCult.Eve.UnityScene Endpoint = Vector(node, "endpoint"); DurationSeconds = Math.Max(0, Double(node, "presentationDuration", 0)); PresentationKind = Get(node, "presentationKind"); + ItemKey = Get(node, "itemKey"); ImpactKind = Get(node, "impactKind"); PresentationIntensity = Math.Max(0, Double(node, "presentationIntensity", 1)); } @@ -50,6 +51,7 @@ namespace GameCult.Eve.UnityScene public EveUnityShotVector3 Endpoint { get; } public double DurationSeconds { get; } public string PresentationKind { get; } + public string ItemKey { get; } public string ImpactKind { get; } public double PresentationIntensity { get; } diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityShotTrajectoryRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityShotTrajectoryRenderer.cs index 3404fea..183d78b 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityShotTrajectoryRenderer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityShotTrajectoryRenderer.cs @@ -16,16 +16,18 @@ namespace GameCult.Eve.UnityScene private readonly List _active = new List(); private EveUnityPlayableWorldClientHost? _host; + private IEveUnityGameObjectAssetProvider? _assetProvider; private Material? _material; public int ActiveTrajectoryCount => _active.Count; - public void Bind(EveUnityPlayableWorldClientHost host) + public void Bind(EveUnityPlayableWorldClientHost host, IEveUnityGameObjectAssetProvider? assetProvider = null) { if (host == null) throw new ArgumentNullException(nameof(host)); - if (ReferenceEquals(_host, host)) return; + if (ReferenceEquals(_host, host) && ReferenceEquals(_assetProvider, assetProvider)) return; Unbind(); _host = host; + _assetProvider = assetProvider; _host.ShotAvailable += Present; } @@ -33,6 +35,7 @@ namespace GameCult.Eve.UnityScene { if (_host != null) _host.ShotAvailable -= Present; _host = null; + _assetProvider = null; } public void Present(EveUnityShotReceipt receipt) @@ -52,6 +55,15 @@ namespace GameCult.Eve.UnityScene var origin = Vector(receipt.Origin); line.SetPosition(0, origin); var endpoint = Vector(receipt.Endpoint); + var effectPrefab = ResolveEffectPrefab(receipt); + if (effectPrefab != null) + { + var effect = Instantiate(effectPrefab, shot.transform); + effect.transform.position = origin; + var direction = endpoint - origin; + if (direction.sqrMagnitude > 0.0001f) effect.transform.rotation = Quaternion.LookRotation(direction.normalized); + } + var impactPrefab = ResolveRolePrefab("effect.impact." + receipt.ImpactKind); var travels = string.Equals(receipt.PresentationKind, "bolt", StringComparison.Ordinal) || string.Equals(receipt.PresentationKind, "guided", StringComparison.Ordinal); line.SetPosition(1, travels ? origin : endpoint); @@ -62,7 +74,8 @@ namespace GameCult.Eve.UnityScene endpoint, Time.unscaledTime, Math.Max(minimumDurationSeconds, (float)receipt.DurationSeconds), - travels)); + travels, + impactPrefab)); } private void Update() @@ -74,6 +87,15 @@ namespace GameCult.Eve.UnityScene var progress = Mathf.Clamp01((now - trajectory.StartedAt) / trajectory.Duration); if (trajectory.Travels) trajectory.Line.SetPosition(1, Vector3.Lerp(trajectory.Origin, trajectory.Endpoint, progress)); + if (progress >= 1 && !trajectory.ImpactPresented) + { + trajectory.ImpactPresented = true; + if (trajectory.ImpactPrefab != null) + { + var impact = Instantiate(trajectory.ImpactPrefab, transform); + impact.transform.position = trajectory.Endpoint; + } + } if (progress < 1 || now < trajectory.StartedAt + trajectory.Duration + lingerSeconds) continue; Destroy(trajectory.Root); _active.RemoveAt(index); @@ -89,6 +111,19 @@ namespace GameCult.Eve.UnityScene return _material; } + private GameObject? ResolveEffectPrefab(EveUnityShotReceipt receipt) + { + if (!string.IsNullOrWhiteSpace(receipt.ItemKey)) + { + var itemPrefab = ResolveRolePrefab("effect.shot.item." + receipt.ItemKey); + if (itemPrefab != null) return itemPrefab; + } + return ResolveRolePrefab("effect.shot." + receipt.PresentationKind); + } + + private GameObject? ResolveRolePrefab(string role) => _assetProvider?.ResolvePrefab( + new EveUnityPlayableWorldAssetBinding("", role, "provider-asset-ref")); + private void OnDestroy() { Unbind(); @@ -103,7 +138,7 @@ namespace GameCult.Eve.UnityScene private sealed class ActiveTrajectory { - public ActiveTrajectory(GameObject root, LineRenderer line, Vector3 origin, Vector3 endpoint, float startedAt, float duration, bool travels) + public ActiveTrajectory(GameObject root, LineRenderer line, Vector3 origin, Vector3 endpoint, float startedAt, float duration, bool travels, GameObject? impactPrefab) { Root = root; Line = line; @@ -112,6 +147,7 @@ namespace GameCult.Eve.UnityScene StartedAt = startedAt; Duration = duration; Travels = travels; + ImpactPrefab = impactPrefab; } public GameObject Root { get; } @@ -121,6 +157,8 @@ namespace GameCult.Eve.UnityScene public float StartedAt { get; } public float Duration { get; } public bool Travels { get; } + public GameObject? ImpactPrefab { get; } + public bool ImpactPresented { get; set; } } } } diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityShotReceiptPresenterTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityShotReceiptPresenterTests.cs index 7bf4472..98beaa5 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityShotReceiptPresenterTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityShotReceiptPresenterTests.cs @@ -31,11 +31,23 @@ namespace GameCult.Eve.UnityScene.Tests 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(emitted[0].ItemKey, Is.EqualTo("test-cannon")); Assert.That(emitted[0].ImpactKind, Is.EqualTo("shield")); Assert.That(emitted[0].PresentationIntensity, Is.EqualTo(9)); Assert.That(presenter.Apply(next), Is.Zero); } + [Test] + public void ManifestResolvesSemanticEffectPresentationRoles() + { + var bolt = new EveUnityPlayableWorldAssetManifestEntry( + "prefab.effect.shot.bolt", "effect.shot.bolt", "Prefabs/Lightning", "bolt"); + var manifest = new EveUnityPlayableWorldAssetManifest("manifest", new[] { bolt }); + + Assert.That(manifest.FindByPresentationRole("effect.shot.bolt"), Is.SameAs(bolt)); + Assert.That(manifest.FindByPresentationRole("effect.impact.shield"), Is.Null); + } + 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))); @@ -55,6 +67,7 @@ namespace GameCult.Eve.UnityScene.Tests ["endpoint"] = endpoint, ["presentationDuration"] = "0.25", ["presentationKind"] = "test-laser", + ["itemKey"] = "test-cannon", ["impactKind"] = "shield", ["presentationIntensity"] = "9" }, Empty(), Empty(), 0, 0, Array.Empty(), null,