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 _fadeMaterials = new List(); private Dictionary _meshes = new Dictionary(); private List<(MeshRenderer mesh, int submesh, Material material)> _nonFadedSubmeshes = new List<(MeshRenderer mesh, int submesh, Material material)>(); private List _subscriptions = new List(); 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 _radiatorSfx = new Dictionary(); private static Dictionary _instantWeaponManagers = new Dictionary(); private static Dictionary _constantWeaponManagers = new Dictionary(); public static void ClearWeaponManagers() { _instantWeaponManagers.Clear(); _constantWeaponManagers.Clear(); } public CompassIcon CompassIcon { get; set; } public Dictionary Barrels { get; private set; } public Dictionary BarrelIndices { get; private set; } public Dictionary RadiatorMeshes { get; private set; } public Transform LookAtPoint { get; private set; } private Dictionary _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(); 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() ?? gameObject.AddComponent(); impacts.Entity = entity; (gameObject.GetComponent() ?? gameObject.AddComponent()) .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(); 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(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(); 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(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(); Barrels = new Dictionary(); BarrelIndices = new Dictionary(); foreach (var radiator in entity.GetBehaviors()) { 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(); 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(); } }