Resolve combat effects by presentation role

This commit is contained in:
Meta Vens 2026-07-12 01:24:45 +01:00
commit d1240f9fab
6 changed files with 67 additions and 5 deletions

View file

@ -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

View file

@ -83,7 +83,7 @@ namespace GameCult.Eve.UnityScene
{
var trajectories = GetComponent<EveUnityShotTrajectoryRenderer>();
if (trajectories == null) trajectories = gameObject.AddComponent<EveUnityShotTrajectoryRenderer>();
trajectories.Bind(this);
trajectories.Bind(this, Runtime.GameObjectAssetProvider);
}
return Runtime.Connect();

View file

@ -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;
}

View file

@ -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; }

View file

@ -16,16 +16,18 @@ namespace GameCult.Eve.UnityScene
private readonly List<ActiveTrajectory> _active = new List<ActiveTrajectory>();
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; }
}
}
}

View file

@ -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<EveUnitySceneEmbeddedDocumentSlot>(), null,