Aetheria/Assets/Scripts/Gameplay/EntityInstance.cs
Meta Vens 97d94ecfae Present landed impacts: shield ripple, hull reaction and camera kick through one ImpactPresenter
ImpactPresenter (bound on every EntityInstance by SetEntity through the one CapabilityPresentationBinder) renders
feedback-1's absorbs: Feedback.Cue decides intensity and shake, DrawAhead.Carried the drawn point; a hull absorb
spawns ImpactFeedback's pooled effect under the instance, and a cue with shake raises a Cinemachine impulse.
ShieldManager ripples on a Shield-layer absorb. MainMenu gains a Screen Shake slider that persists on Back.
Editor: WireImpactFeedback (scene wiring, scene diff in the next commit) and ImpactPresenterSmoke.
Unity batchmode compile 0 error CS; ImpactPresenterSmoke exit 0. The scene is not yet wired in this commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 21:10:04 +02:00

441 lines
19 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UniRx;
using UnityEngine;
using CultMath;
using CultMath.UnityBridge;
using static CultMath.math;
using float2 = CultMath.float2;
using Random = UnityEngine.Random;
public class EntityInstance : MonoBehaviour
{
public MeshRenderer MapIcon;
public Transform InfluencePrefab;
public Transform PingPrefab;
public Material InvisibleMaterial;
public static Transform EffectManagerParent;
public ShieldManager Shield;
public HullCollider[] HullColliders;
public RadiatorHardpoint[] RadiatorHardpoints;
public ThrusterHardpoint[] ThrusterHardpoints;
public WeaponHardpoint[] WeaponHardpoints;
public ArticulationPoint[] ArticulationPoints;
public GameObject DestroyEffect;
public event Action OnFadedOut;
public event Action OnFadedIn;
private List<Material> _fadeMaterials = new List<Material>();
private Dictionary<MeshRenderer, Material[]> _meshes = new Dictionary<MeshRenderer, Material[]>();
private List<(MeshRenderer mesh, int submesh, Material material)> _nonFadedSubmeshes = new List<(MeshRenderer mesh, int submesh, Material material)>();
private List<IDisposable> _subscriptions = new List<IDisposable>();
private float _fade = 0;
private bool _fading;
private bool _fadingIn;
private bool _fadedElementsVisible = false;
private bool _unfadedElementsVisible = false;
private float _fadeTime;
private bool _destroyed;
private (Transform transform, MeshRenderer meshRenderer) _currentPing;
private float _pingBrightness;
private Sensor _sensor;
private Transform _influenceInstance;
//private List<(GameObject source, EquippedItem item)> _audioSources = new List<(GameObject source, EquippedItem item)>();
// private (Reactor reactor, GameObject sfxSource) _reactor;
// private Dictionary<Radiator, GameObject> _radiatorSfx = new Dictionary<Radiator, GameObject>();
private static Dictionary<InstantWeaponData, InstantWeaponEffectManager> _instantWeaponManagers = new Dictionary<InstantWeaponData, InstantWeaponEffectManager>();
private static Dictionary<ConstantWeaponData, ConstantWeaponEffectManager> _constantWeaponManagers = new Dictionary<ConstantWeaponData, ConstantWeaponEffectManager>();
public static void ClearWeaponManagers()
{
_instantWeaponManagers.Clear();
_constantWeaponManagers.Clear();
}
public CompassIcon CompassIcon { get; set; }
public Dictionary<HardpointData, Transform[]> Barrels { get; private set; }
public Dictionary<HardpointData, int> BarrelIndices { get; private set; }
public Dictionary<Radiator, MeshRenderer> RadiatorMeshes { get; private set; }
public Transform LookAtPoint { get; private set; }
private Dictionary<int, Transform> _groupAims;
public Entity Entity { get; private set; }
public ZoneRenderer ZoneRenderer { get; private set; }
public Transform LocalSpace { get; private set; }
public bool Visible
{
get => _fade > 0.01f;
}
private void Awake()
{
LocalSpace = new GameObject().transform;
var meshes = gameObject.GetComponentsInChildren<MeshRenderer>();
var materials = new List<(Material material, List<(MeshRenderer renderer, int index)> submeshes)>();
foreach (var mesh in meshes)
{
_meshes.Add(mesh, mesh.sharedMaterials);
for (var i = 0; i < mesh.sharedMaterials.Length; i++)
{
var material = mesh.sharedMaterials[i];
if (material.shader.FindPropertyIndex("_Fade") >= 0)
{
var match = materials.FirstOrDefault(lm => lm.material == material);
if(match.material==null || material.shader.FindPropertyIndex("_EmissionFresnel") >= 0)
{
match = (material, new List<(MeshRenderer renderer, int index)>());
materials.Add(match);
}
match.submeshes.Add((mesh, i));
}
else
{
_nonFadedSubmeshes.Add((mesh, i, material));
_meshes[mesh][i] = InvisibleMaterial;
}
}
}
foreach (var (material, submeshes) in materials)
{
var materialInstance = Instantiate(material);
_fadeMaterials.Add(materialInstance);
foreach (var (mesh, index) in submeshes)
{
_meshes[mesh][index] = materialInstance;
}
materialInstance.SetFloat("_Fade", 0);
}
foreach (var mesh in _meshes.Keys) mesh.enabled = false;
foreach (var mesh in _meshes) mesh.Key.sharedMaterials = mesh.Value;
}
protected virtual void ShowUnfadedElements()
{
_unfadedElementsVisible = true;
foreach (var (mesh, submesh, material) in _nonFadedSubmeshes) _meshes[mesh][submesh] = material;
foreach (var mesh in _meshes) mesh.Key.sharedMaterials = mesh.Value;
}
protected virtual void HideUnfadedElements()
{
_unfadedElementsVisible = false;
foreach (var (mesh, submesh, material) in _nonFadedSubmeshes) _meshes[mesh][submesh] = InvisibleMaterial;
foreach (var mesh in _meshes) mesh.Key.sharedMaterials = mesh.Value;
}
public void FadeIn(float time)
{
if (_fade > .99f)
{
_fading = false;
return;
}
_fadeTime = time;
_fading = true;
_fadingIn = true;
}
public void FadeOut(float time)
{
if (_fade < .01f)
{
_fading = false;
return;
}
_fadeTime = time;
_fading = true;
_fadingIn = false;
}
public virtual void SetEntity(ZoneRenderer zoneRenderer, Entity entity)
{
gameObject.name = entity.Name;
LocalSpace.gameObject.name = $"{entity.Name} Sim Space";
LocalSpace.SetParent(transform.parent);
Entity = entity;
ZoneRenderer = zoneRenderer;
var hullData = entity.ItemManager.GetData(entity.Hull) as HullData;
if(Shield)
Shield.Entity = entity;
// Landed impacts reach the screen through the entity's one binding: the shield ripple (ShieldManager) and the
// hull reaction and camera kick (ImpactPresenter) subscribe to the Absorb stream the simulation publishes.
var impacts = gameObject.GetComponent<ImpactPresenter>() ?? gameObject.AddComponent<ImpactPresenter>();
impacts.Entity = entity;
(gameObject.GetComponent<CapabilityPresentationBinder>() ?? gameObject.AddComponent<CapabilityPresentationBinder>())
.Bind(entity.Capabilities);
foreach (var hullCollider in HullColliders) hullCollider.Entity = entity;
foreach (var item in entity.Equipment)
{
foreach (var behavior in item.Behaviors)
{
if (behavior is Sensor sensor)
{
_sensor = sensor;
sensor.OnPingStart += () =>
{
var pingInstance = Instantiate(PingPrefab);
var pingMesh = pingInstance.GetComponent<MeshRenderer>();
pingInstance.position = entity.Position.ToUnity();
_pingBrightness = pingMesh.material.GetFloat("_Depth");
_currentPing = (pingInstance, pingMesh);
};
sensor.OnPingEnd += OnSensorPingEnd;
}
// A mine layer's presentation is the mine it lays (MineInstance, drawn by ZoneRenderer from Zone.Mines).
if (behavior is InstantWeapon instantWeapon && !FireControl.IsMineLayer(instantWeapon))
{
var data = (InstantWeaponData) instantWeapon.Data;
if (!_instantWeaponManagers.ContainsKey(data))
{
var managerPrefab = EngineAssets.Load<InstantWeaponEffectManager>(data.EffectPrefab);
if(managerPrefab)
{
_instantWeaponManagers.Add(data, Instantiate(managerPrefab, EffectManagerParent));
}
else Debug.LogError($"No InstantWeaponEffectManager prefab found for GUID {data.EffectPrefab}");
}
// Cut 3 (docs/fire-control-cut.md): carries the ShotId FireControl.Fire assigned so the
// effect manager (and whatever it spawns) can bind its presentation to Zone.ShotCommitted /
// ShotResolved instead of deciding or applying anything itself.
// The target handed to the effect is the shot's own, not the shooter's selection: a fused round
// whose selected target was invalid, or out of arc, flies at none (FireControl.Fire) and the
// effect must not chase one.
instantWeapon.OnFire += shotId =>
_instantWeaponManagers[data].Fire(instantWeapon, item, this, ShotTarget(shotId), shotId);
if (behavior is ChargedWeapon chargedWeapon)
{
var chargeManager = _instantWeaponManagers[data].GetComponent<ChargeEffectManager>();
if (chargeManager)
{
chargedWeapon.OnStartCharging += () => chargeManager.StartCharging(chargedWeapon, item, this);
chargedWeapon.OnStopCharging += () => chargeManager.StopCharging(chargedWeapon);
chargedWeapon.OnCharged += () => chargeManager.Charged(chargedWeapon);
chargedWeapon.OnFailed += () => chargeManager.Failed(chargedWeapon);
}
}
}
if (behavior is ConstantWeapon constantWeapon)
{
var data = (ConstantWeaponData) constantWeapon.Data;
if (!_constantWeaponManagers.ContainsKey(data))
{
var managerPrefab = EngineAssets.Load<ConstantWeaponEffectManager>(data.EffectPrefab);
if(managerPrefab)
{
_constantWeaponManagers.Add(data, Instantiate(managerPrefab, EffectManagerParent));
}
else Debug.LogError($"No ConstantWeaponEffectManager prefab found for GUID {data.EffectPrefab}");
}
constantWeapon.OnStartFiring += () =>
_constantWeaponManagers[data].StartFiring(data, item, this, entity.Target.Value.Entity != null ? ZoneRenderer.EntityInstances[entity.Target.Value.Entity] : null);
constantWeapon.OnStopFiring += () =>
_constantWeaponManagers[data].StopFiring(item);
}
}
}
RadiatorMeshes = new Dictionary<Radiator, MeshRenderer>();
Barrels = new Dictionary<HardpointData, Transform[]>();
BarrelIndices = new Dictionary<HardpointData, int>();
foreach (var radiator in entity.GetBehaviors<Radiator>())
{
var hp = radiator.Item.Hardpoint;
if (hp != null && hp.Type == HardpointType.Radiator)
{
var mesh = RadiatorHardpoints.FirstOrDefault(x => x.name == hp.Transform);
if (mesh)
{
RadiatorMeshes.Add(radiator, mesh.Mesh);
}
}
}
foreach (var hp in hullData.Hardpoints)
{
if(hp.Type == HardpointType.Ballistic || hp.Type == HardpointType.Energy || hp.Type == HardpointType.Launcher)
{
var whp = WeaponHardpoints.FirstOrDefault(x => x.name == hp.Transform);
if (whp)
{
Barrels.Add(hp, whp.FiringPoint);
BarrelIndices.Add(hp, 0);
}
}
}
// Cut 3 (docs/fire-control-cut.md): DamageSchematic and both HullCollider subscriptions (blast, Hit)
// are deleted -- FireControl decides hits now and applies them through Entity.ArmorAbsorb/ItemAbsorb/
// DamageHull (ServerShared, Cut 12.3/12.4(b); DamageSchematic, the old per-cell single-lane absorb path
// and ApplyHit are all gone), the single owner in place of what used to exist once per Unity effect.
// HullColliders no longer publish Hit or a blast event at all (R8).
LookAtPoint = new GameObject($"{entity.Name} Look Point").transform;
// One aim point per articulation group, placed each LateUpdate where the group's first weapon aims.
_groupAims = new Dictionary<int, Transform>();
foreach (var articulationPoint in ArticulationPoints)
{
if (!_groupAims.TryGetValue(articulationPoint.Group, out var aim))
_groupAims[articulationPoint.Group] = aim = new GameObject($"{entity.Name} Aim {articulationPoint.Group}").transform;
articulationPoint.Target = aim;
}
_subscriptions.Add(Entity.HullDamage.Subscribe(_ =>
{
if (Entity.Hull.IsDestroyed)
{
if (!this) return;
_destroyed = true;
foreach (var gear in Entity.Equipment)
{
if (gear != Entity.EquippedHull && Random.value < ZoneRenderer.Settings.LootDropProbability)
{
ZoneRenderer.DropItem(
Entity.Position.ToUnity(),
Random.onUnitSphere * ZoneRenderer.Settings.LootDropVelocity,
gear.EquippableItem);
}
}
foreach (var cargo in Entity.CargoBays)
{
foreach (var item in cargo.Cargo.Keys)
{
ZoneRenderer.DropItem(
Entity.Position.ToUnity(),
Random.onUnitSphere * ZoneRenderer.Settings.LootDropVelocity,
item);
}
}
if (DestroyEffect != null)
{
var t = Instantiate(DestroyEffect).transform;
t.position = transform.position;
}
// Cut 5, 5.6 (docs/fire-control-cut.md, Soul finding 7): Zone now removes the entity itself
// (subscribed to Entity.Death when the entity joined) -- this subscription's own business is
// loot and the destroy effect, presentation both, not removal from the simulation's own
// collection.
}
}));
if (entity is OrbitalEntity orbital && orbital.IsSecureArea)
{
_influenceInstance = Instantiate(InfluencePrefab);
_influenceInstance.transform.localScale = Vector3.one * orbital.SecurityRadius;
}
}
private EntityInstance ShotTarget(int shotId) =>
Entity.Zone.TryGetShot(shotId, out var shot) && shot.Target != null &&
ZoneRenderer.EntityInstances.TryGetValue(shot.Target, out var instance) ? instance : null;
private void OnSensorPingEnd()
{
Destroy(_currentPing.transform.gameObject);
}
public Transform GetBarrel(HardpointData hardpoint)
{
if (Barrels.ContainsKey(hardpoint))
{
var barrel = Barrels[hardpoint][BarrelIndices[hardpoint]];
BarrelIndices[hardpoint] = (BarrelIndices[hardpoint] + 1) % Barrels[hardpoint].Length;
return barrel;
}
return transform;
}
public virtual void Update()
{
if (_currentPing.transform)
{
_currentPing.transform.localScale = _sensor.PingRadius * Vector3.one;
_currentPing.meshRenderer.material.SetFloat("_Depth", _pingBrightness * _sensor.PingBrightness);
}
if (_fading)
{
if (_fadingIn)
{
_fade += Time.deltaTime / _fadeTime;
if (!_fadedElementsVisible && _fade > .01f)
{
_fadedElementsVisible = true;
foreach (var mesh in _meshes.Keys) mesh.enabled = true;
}
if (_fade > 1)
{
_fade = 1;
_fading = false;
OnFadedIn?.Invoke();
OnFadedIn = null;
ShowUnfadedElements();
}
foreach(var material in _fadeMaterials) material.SetFloat("_Fade", _fade);
}
else
{
_fade -= Time.deltaTime / _fadeTime;
if(_unfadedElementsVisible && _fade < .99f) HideUnfadedElements();
if (_fade < 0)
{
_fadedElementsVisible = false;
foreach (var mesh in _meshes.Keys) mesh.enabled = false;
_fade = 0;
_fading = false;
OnFadedOut?.Invoke();
OnFadedOut = null;
}
foreach(var material in _fadeMaterials) material.SetFloat("_Fade", _fade);
}
}
foreach (var x in RadiatorMeshes)
{
x.Value.material.SetFloat("_Emission", Entity.ItemManager.GameplaySettings.TemperatureEmissionCurve.Evaluate(x.Key.RadiatorTemperature));
}
var aimDistance = !Entity.Target.Value.IsNone
? max(Entity.TargetRange, Entity.ItemManager.GameplaySettings.ConvergenceMinimumDistance)
: 10000;
LookAtPoint.position = transform.position + Entity.Aim.ToUnity() * aimDistance;
foreach (var group in _groupAims)
{
var weapon = group.Key >= 0 && group.Key < Entity.WeaponGroups.Length ? Entity.WeaponGroups[group.Key].weapons.FirstOrDefault() : null;
if (weapon == null) { group.Value.position = LookAtPoint.position; continue; }
var barrel = Barrels.TryGetValue(weapon.Item.Hardpoint, out var barrels) ? barrels[0].position : transform.position;
var direction = FireControl.Solution(weapon, Entity, Entity.Target.Value).Direction;
group.Value.position = barrel + new Vector3(direction.x, 0, direction.y) * aimDistance;
}
var drawn = DrawAhead.Position(Entity, ActionGameManager.Clock.Lead);
LocalSpace.localPosition = transform.position = drawn.ToUnity();
if (_influenceInstance)
_influenceInstance.position = new Vector3(drawn.x, 0, drawn.z);
}
public virtual void OnDestroy()
{
if(_sensor!=null)
_sensor.OnPingEnd -= OnSensorPingEnd;
if (_influenceInstance) Destroy(_influenceInstance.gameObject);
Destroy(LocalSpace.gameObject);
foreach(var x in _subscriptions)
x.Dispose();
}
}