SimClock (engine-free) turns real frame seconds into whole fixed steps; ActionGameManager.Update feeds it outside the pause gate and Zone.Update(Time.deltaTime) is gone, with IntroCutscene (dead, Unity-driven pose writes). Entity.TurnRate is the sum of the rotation actuators' applied turn per sim second, zeroed each Update; DrawAhead reads it so EntityInstance and ShipInstance draw the latest sim state ahead by Lead. Unverified at this commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
434 lines
18 KiB
C#
434 lines
18 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;
|
|
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.Durability < .01f)
|
|
{
|
|
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();
|
|
}
|
|
}
|