This repository has been archived on 2026-10-29. You can view files and clone it, but you cannot make any changes to its state, such as pushing and creating new issues, pull requests or comments.
EveUnity/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityThermalPresentation.cs
2026-07-12 06:25:10 +01:00

225 lines
11 KiB
C#

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 interface IEveUnityThermalPresentationAssetSink
{
void ConfigureThermalPresentationAssets(EveUnityThermalPresentationAssets assets);
}
public sealed class EveUnityThermalPresentationAssets
{
public EveUnityThermalPresentationAssets(UnityEngine.Object? heatstroke,
UnityEngine.Object? severeHeatstroke, UnityEngine.Object? hypothermia,
UnityEngine.Object? severeHypothermia, UnityEngine.Object? death)
{
Heatstroke = heatstroke; SevereHeatstroke = severeHeatstroke;
Hypothermia = hypothermia; SevereHypothermia = severeHypothermia; Death = death;
}
public UnityEngine.Object? Heatstroke { get; }
public UnityEngine.Object? SevereHeatstroke { get; }
public UnityEngine.Object? Hypothermia { get; }
public UnityEngine.Object? SevereHypothermia { get; }
public UnityEngine.Object? Death { get; }
}
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, IEveUnityNativeAssetProvider? assets = null)
{
_host = host != null ? host : throw new ArgumentNullException(nameof(host));
_sinks = GetComponents<MonoBehaviour>().OfType<IEveUnityThermalPresentationSink>().ToArray();
var resolved = new EveUnityThermalPresentationAssets(
Resolve(assets, "post.thermal.heatstroke"),
Resolve(assets, "post.thermal.severe-heatstroke"),
Resolve(assets, "post.thermal.hypothermia"),
Resolve(assets, "post.thermal.severe-hypothermia"),
Resolve(assets, "post.death"));
foreach (var sink in GetComponents<MonoBehaviour>().OfType<IEveUnityThermalPresentationAssetSink>())
sink.ConfigureThermalPresentationAssets(resolved);
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);
private static UnityEngine.Object? Resolve(IEveUnityNativeAssetProvider? assets, string role) =>
assets?.ResolveAsset(new EveUnityPlayableWorldAssetBinding("", role, "provider-asset-ref"),
typeof(UnityEngine.Object));
}
}