Lower thermal presentation generically
This commit is contained in:
parent
290b384703
commit
580af5328d
5 changed files with 285 additions and 0 deletions
|
|
@ -88,6 +88,9 @@ namespace GameCult.Eve.UnityScene
|
|||
var feedbackEffects = GetComponent<EveUnityFeedbackEffectRenderer>();
|
||||
if (feedbackEffects == null) feedbackEffects = gameObject.AddComponent<EveUnityFeedbackEffectRenderer>();
|
||||
feedbackEffects.Bind(this, Runtime.GameObjectAssetProvider);
|
||||
var thermal = GetComponent<EveUnityThermalPresenter>();
|
||||
if (thermal == null) thermal = gameObject.AddComponent<EveUnityThermalPresenter>();
|
||||
thermal.Bind(this);
|
||||
|
||||
return Runtime.Connect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,192 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Eve.UnityScene
|
||||
{
|
||||
public interface IEveUnityThermalPresentationSink
|
||||
{
|
||||
void ApplyThermalPresentation(EveUnityThermalPresentationFrame frame);
|
||||
}
|
||||
|
||||
public readonly struct EveUnityThermalPresentationFrame
|
||||
{
|
||||
public EveUnityThermalPresentationFrame(string entityId, float cockpitTemperature,
|
||||
float heatstroke, float hypothermia, bool heatstrokeRisk, bool hypothermiaRisk,
|
||||
float heatstrokeWeight, float severeHeatstrokeWeight, float hypothermiaWeight,
|
||||
float severeHypothermiaWeight, string deathCause, float deathWeight)
|
||||
{
|
||||
EntityId = entityId ?? "";
|
||||
CockpitTemperature = cockpitTemperature;
|
||||
Heatstroke = heatstroke;
|
||||
Hypothermia = hypothermia;
|
||||
HeatstrokeRisk = heatstrokeRisk;
|
||||
HypothermiaRisk = hypothermiaRisk;
|
||||
HeatstrokeWeight = heatstrokeWeight;
|
||||
SevereHeatstrokeWeight = severeHeatstrokeWeight;
|
||||
HypothermiaWeight = hypothermiaWeight;
|
||||
SevereHypothermiaWeight = severeHypothermiaWeight;
|
||||
DeathCause = deathCause ?? "";
|
||||
DeathWeight = deathWeight;
|
||||
}
|
||||
|
||||
public string EntityId { get; }
|
||||
public float CockpitTemperature { get; }
|
||||
public float Heatstroke { get; }
|
||||
public float Hypothermia { get; }
|
||||
public bool HeatstrokeRisk { get; }
|
||||
public bool HypothermiaRisk { get; }
|
||||
public float HeatstrokeWeight { get; }
|
||||
public float SevereHeatstrokeWeight { get; }
|
||||
public float HypothermiaWeight { get; }
|
||||
public float SevereHypothermiaWeight { get; }
|
||||
public string DeathCause { get; }
|
||||
public float DeathWeight { get; }
|
||||
}
|
||||
|
||||
public sealed class EveUnityThermalPresentationState
|
||||
{
|
||||
private string _deathEventId = "";
|
||||
private string _deathCause = "";
|
||||
private float _deathStartedAt = float.NegativeInfinity;
|
||||
|
||||
public EveUnityThermalPresentationFrame Apply(
|
||||
EveUnityPlayableWorldProjection? world,
|
||||
EveUnitySceneProjection? projection,
|
||||
float timeSeconds)
|
||||
{
|
||||
var entity = ResolvePlayer(world);
|
||||
var props = entity?.Props ?? EmptyProps;
|
||||
var death = LatestThermalDeath(projection);
|
||||
if (death.HasValue && !string.Equals(_deathEventId, death.Value.EventId, StringComparison.Ordinal))
|
||||
{
|
||||
_deathEventId = death.Value.EventId;
|
||||
_deathCause = death.Value.Cause;
|
||||
_deathStartedAt = death.Value.FrameId >= death.Value.CurrentFrameId
|
||||
? timeSeconds
|
||||
: float.NegativeInfinity;
|
||||
}
|
||||
else if (!death.HasValue && entity != null)
|
||||
{
|
||||
_deathEventId = "";
|
||||
_deathCause = "";
|
||||
_deathStartedAt = float.NegativeInfinity;
|
||||
}
|
||||
|
||||
var duration = Positive(Float(props, "deathTransitionSeconds", 1f), 1f);
|
||||
var deathWeight = string.IsNullOrWhiteSpace(_deathCause) ? 0f :
|
||||
float.IsNegativeInfinity(_deathStartedAt) ? 1f :
|
||||
Mathf.Clamp01((timeSeconds - _deathStartedAt) / duration);
|
||||
var heatstroke = Mathf.Clamp01(Float(props, "heatstroke", 0f));
|
||||
var hypothermia = Mathf.Clamp01(Float(props, "hypothermia", 0f));
|
||||
var heatWeight = Mathf.Clamp01(Float(props, "heatstrokePostWeight", 0f));
|
||||
var severeNormalized = Mathf.Clamp01(Float(props, "severeHeatstrokeWeight", 0f));
|
||||
var phaseFloor = Float(props, "heatstrokePhasingFloor", 0f);
|
||||
var phaseFrequency = Float(props, "heatstrokePhasingFrequency", 5f);
|
||||
var severeWeight = Mathf.Clamp01(severeNormalized + severeNormalized * (1f - severeNormalized) *
|
||||
Mathf.Max(phaseFloor, Mathf.Sin(timeSeconds * phaseFrequency)));
|
||||
var thermalFade = 1f - deathWeight;
|
||||
if (entity == null && string.Equals(_deathCause, "heatstroke", StringComparison.Ordinal))
|
||||
{
|
||||
heatWeight = thermalFade;
|
||||
severeWeight = thermalFade;
|
||||
}
|
||||
var coldWeight = entity == null && string.Equals(_deathCause, "hypothermia", StringComparison.Ordinal)
|
||||
? thermalFade : 0f;
|
||||
|
||||
return new EveUnityThermalPresentationFrame(
|
||||
entity?.EntityId ?? "",
|
||||
Float(props, "cockpitTemperature", 0f), heatstroke, hypothermia,
|
||||
Bool(props, "heatstrokeRisk"), Bool(props, "hypothermiaRisk"),
|
||||
heatWeight, severeWeight, coldWeight, coldWeight, _deathCause, deathWeight);
|
||||
}
|
||||
|
||||
private static EveUnityPlayableWorldEntity? ResolvePlayer(EveUnityPlayableWorldProjection? world)
|
||||
{
|
||||
if (world == null) return null;
|
||||
return world.Entities.FirstOrDefault(value =>
|
||||
string.Equals(value.EntityId, world.PlayerEntityId, StringComparison.Ordinal)) ??
|
||||
world.Entities.FirstOrDefault(value => value.Controllable);
|
||||
}
|
||||
|
||||
private static ThermalDeath? LatestThermalDeath(EveUnitySceneProjection? projection)
|
||||
{
|
||||
if (projection == null) return null;
|
||||
ThermalDeath? latest = null;
|
||||
Visit(projection.Root);
|
||||
return latest;
|
||||
|
||||
void Visit(EveUnitySceneNode node)
|
||||
{
|
||||
if (string.Equals(node.ComponentKind, "feedback.event", StringComparison.Ordinal) &&
|
||||
string.Equals(Read(node.Props, "eventKind"), "entity.destroyed", StringComparison.Ordinal))
|
||||
{
|
||||
var cause = Read(node.Props, "reason");
|
||||
if (string.Equals(cause, "heatstroke", StringComparison.Ordinal) ||
|
||||
string.Equals(cause, "hypothermia", StringComparison.Ordinal))
|
||||
{
|
||||
var candidate = new ThermalDeath(Read(node.Props, "eventId"), cause,
|
||||
Long(node.Props, "frameId", -1), Long(node.Props, "currentFrameId", -1));
|
||||
if (!latest.HasValue || candidate.FrameId > latest.Value.FrameId ||
|
||||
candidate.FrameId == latest.Value.FrameId && string.CompareOrdinal(
|
||||
candidate.EventId, latest.Value.EventId) > 0)
|
||||
latest = candidate;
|
||||
}
|
||||
}
|
||||
foreach (var child in node.Children) Visit(child);
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly IReadOnlyDictionary<string, string> EmptyProps =
|
||||
new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
private static string Read(IReadOnlyDictionary<string, string> props, string key) =>
|
||||
props.TryGetValue(key, out var value) ? value ?? "" : "";
|
||||
private static float Float(IReadOnlyDictionary<string, string> props, string key, float fallback) =>
|
||||
float.TryParse(Read(props, key), NumberStyles.Float, CultureInfo.InvariantCulture, out var value)
|
||||
? value : fallback;
|
||||
private static long Long(IReadOnlyDictionary<string, string> props, string key, long fallback) =>
|
||||
long.TryParse(Read(props, key), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)
|
||||
? value : fallback;
|
||||
private static bool Bool(IReadOnlyDictionary<string, string> props, string key) =>
|
||||
bool.TryParse(Read(props, key), out var value) && value;
|
||||
private static float Positive(float value, float fallback) => value > 0f ? value : fallback;
|
||||
|
||||
private readonly struct ThermalDeath
|
||||
{
|
||||
public ThermalDeath(string eventId, string cause, long frameId, long currentFrameId)
|
||||
{ EventId = eventId ?? ""; Cause = cause ?? ""; FrameId = frameId; CurrentFrameId = currentFrameId; }
|
||||
public string EventId { get; }
|
||||
public string Cause { get; }
|
||||
public long FrameId { get; }
|
||||
public long CurrentFrameId { get; }
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class EveUnityThermalPresenter : MonoBehaviour
|
||||
{
|
||||
private readonly EveUnityThermalPresentationState _state = new EveUnityThermalPresentationState();
|
||||
private EveUnityPlayableWorldClientHost? _host;
|
||||
private IEveUnityThermalPresentationSink[] _sinks = Array.Empty<IEveUnityThermalPresentationSink>();
|
||||
|
||||
public EveUnityThermalPresentationFrame LastFrame { get; private set; }
|
||||
|
||||
public void Bind(EveUnityPlayableWorldClientHost host)
|
||||
{
|
||||
_host = host != null ? host : throw new ArgumentNullException(nameof(host));
|
||||
_sinks = GetComponents<MonoBehaviour>().OfType<IEveUnityThermalPresentationSink>().ToArray();
|
||||
ApplyAt(Time.unscaledTime);
|
||||
}
|
||||
|
||||
public void ApplyAt(float timeSeconds)
|
||||
{
|
||||
LastFrame = _state.Apply(_host?.ActiveWorld, _host?.ActiveProjection, timeSeconds);
|
||||
foreach (var sink in _sinks) sink.ApplyThermalPresentation(LastFrame);
|
||||
}
|
||||
|
||||
private void Update() => ApplyAt(Time.unscaledTime);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 434d12dfc31947e9b2c2a91038491b12
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace GameCult.Eve.UnityScene.Tests
|
||||
{
|
||||
public sealed class EveUnityThermalPresentationTests
|
||||
{
|
||||
[Test]
|
||||
public void AlivePilotUsesProviderThermalFactsAndOriginalSeverePulse()
|
||||
{
|
||||
var state = new EveUnityThermalPresentationState();
|
||||
var entity = Entity(new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["cockpitTemperature"] = "340",
|
||||
["heatstroke"] = "0.5",
|
||||
["hypothermia"] = "0.1",
|
||||
["heatstrokeRisk"] = "true",
|
||||
["hypothermiaRisk"] = "false",
|
||||
["heatstrokePostWeight"] = "1",
|
||||
["severeHeatstrokeWeight"] = "0.5",
|
||||
["heatstrokePhasingFloor"] = "0",
|
||||
["heatstrokePhasingFrequency"] = "5"
|
||||
});
|
||||
|
||||
var frame = state.Apply(World(entity), Projection(World(entity), Node("root", "surface", Empty())),
|
||||
(float)(Math.PI / 10));
|
||||
|
||||
Assert.That(frame.EntityId, Is.EqualTo("pilot"));
|
||||
Assert.That(frame.CockpitTemperature, Is.EqualTo(340).Within(0.0001));
|
||||
Assert.That(frame.HeatstrokeRisk, Is.True);
|
||||
Assert.That(frame.HeatstrokeWeight, Is.EqualTo(1).Within(0.0001));
|
||||
Assert.That(frame.SevereHeatstrokeWeight, Is.EqualTo(0.75).Within(0.0001));
|
||||
Assert.That(frame.DeathWeight, Is.Zero);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CurrentThermalDeathCrossfadesAndRetainedDeathSettlesOnReconnect()
|
||||
{
|
||||
var current = Projection(World(), Node("root", "surface", Empty(),
|
||||
Node("death", "feedback.event", new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["eventId"] = "death-1", ["eventKind"] = "entity.destroyed",
|
||||
["reason"] = "heatstroke", ["frameId"] = "20", ["currentFrameId"] = "20"
|
||||
})));
|
||||
var live = new EveUnityThermalPresentationState();
|
||||
var start = live.Apply(World(), current, 10f);
|
||||
var middle = live.Apply(World(), current, 10.5f);
|
||||
Assert.That(start.DeathWeight, Is.Zero);
|
||||
Assert.That(start.HeatstrokeWeight, Is.EqualTo(1).Within(0.0001));
|
||||
Assert.That(middle.DeathWeight, Is.EqualTo(0.5).Within(0.0001));
|
||||
Assert.That(middle.HeatstrokeWeight, Is.EqualTo(0.5).Within(0.0001));
|
||||
|
||||
var retained = Projection(World(), Node("root", "surface", Empty(),
|
||||
Node("death", "feedback.event", new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["eventId"] = "death-1", ["eventKind"] = "entity.destroyed",
|
||||
["reason"] = "heatstroke", ["frameId"] = "20", ["currentFrameId"] = "25"
|
||||
})));
|
||||
var reconnect = new EveUnityThermalPresentationState().Apply(World(), retained, 99f);
|
||||
Assert.That(reconnect.DeathCause, Is.EqualTo("heatstroke"));
|
||||
Assert.That(reconnect.DeathWeight, Is.EqualTo(1).Within(0.0001));
|
||||
Assert.That(reconnect.HeatstrokeWeight, Is.Zero);
|
||||
}
|
||||
|
||||
private static EveUnityPlayableWorldProjection World(params EveUnityPlayableWorldEntity[] entities) =>
|
||||
new EveUnityPlayableWorldProjection("world", "state", "assets", "input", "camera", "pilot",
|
||||
"pilot", "move", "focus", "target", "action", entities);
|
||||
|
||||
private static EveUnityPlayableWorldEntity Entity(IReadOnlyDictionary<string, string> props) =>
|
||||
new EveUnityPlayableWorldEntity("node", "pilot", "ship", "Pilot", "player", "ship", 0, 0, 0,
|
||||
0, 1, true, true, "focus", "move", "target", "action", props);
|
||||
|
||||
private static EveUnitySceneProjection Projection(EveUnityPlayableWorldProjection world, EveUnitySceneNode root) =>
|
||||
new EveUnitySceneProjection("provider", "surface", "scene", "commands", "receipts", "provider",
|
||||
world, root);
|
||||
|
||||
private static EveUnitySceneNode Node(string id, string kind, IReadOnlyDictionary<string, string> props,
|
||||
params EveUnitySceneNode[] children) =>
|
||||
new EveUnitySceneNode(id, kind, "scene-node", props, 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: 0b0147ca55ca4c2d9f811dddc889219a
|
||||
Reference in a new issue