Soul's pass over the whole campaign confirmed eleven findings. Seven were code
defects of one shape -- a rule implemented on one path and left off the path
beside it -- and this is all seven.
5.1 Tracking falls back to GameplaySettings.UnaidedTracking (new, 10) instead of
zero, and Commit drops the zero branch that turned a Tracking-less shooter
into a hard wall. Unaided fire was meant to be really, really bad; it was
334 shots and no hits against a target drifting at 5 u/s.
5.2 A shot the shield cannot absorb now breaks it. The decision is frozen into
ShotOutcome.ShieldBroken at commit and performed in Apply, so a shield that
recharges mid-flight cannot retroactively survive. Splash already did this;
the discrete roll never did, which made the whole shield-break mechanic
unreachable for every weapon in the catalog.
5.3 ConstantWeapon gates on ArcAllowsFire alongside StanceAllowsFire. A
side-mounted beam was firing forward.
5.4 The point-blank bearing case moves out of Weapon.ArcAllowsFire and into
InArc, which used to normalize the zero vector into NaN. The trigger said
fire and the probability said impossible.
5.5 A destroyed item drops out of IsRevealed's ranking, so Precision stops
concentrating hits onto a dead subsystem's footprint.
5.6 Zone subscribes Entity.Death and removes the corpse itself. That removal
lived only in Unity's loot-drop subscription, so headless nothing ever left
the zone, shots landed on corpses and no fight could end.
5.7 PendingShot drops PredictedIntercept and FlightTime, written and read
nowhere.
208 tests pass (197 plus eleven new).
Known defect, recorded rather than hidden: the committed mutation harness is
unreliable. Across two runs it failed to restore FireControl.cs and Weapon.cs,
leaving live mutations in the working tree -- including its own 5.1 mutation,
which is how a green suite briefly reported two failures. Its verdicts are
therefore evidence only where re-run from a verified-clean tree. Two anchors
(Zone.cs's ObserveAdd line, Weapon.cs's InArc return) fail to match at all, and
5.3's ConstantWeaponObeysArc survives its own mutation, so that rule is written
but not pinned. Cut 5b owns the repair; nothing here is trusted on the harness's
word, and the suite result above was taken by hand from a hash-verified tree.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
420 lines
17 KiB
C#
420 lines
17 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 Transform[] EquipmentHardpoints;
|
|
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; }
|
|
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;
|
|
}
|
|
|
|
if (behavior is InstantWeapon 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.
|
|
instantWeapon.OnFire += shotId =>
|
|
_instantWeaponManagers[data].Fire(instantWeapon, item, this, entity.Target.Value != null && ZoneRenderer.EntityInstances.ContainsKey(entity.Target.Value) ? ZoneRenderer.EntityInstances[entity.Target.Value] : null, 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 != null ? ZoneRenderer.EntityInstances[entity.Target.Value] : null);
|
|
constantWeapon.OnStopFiring += () =>
|
|
_constantWeaponManagers[data].StopFiring(item);
|
|
}
|
|
}
|
|
|
|
var hp = Entity.Hardpoints[item.Position.x, item.Position.y];
|
|
if (hp != null && item.Data.SoundBank != 0)
|
|
{
|
|
var hardpointTransform = EquipmentHardpoints.FirstOrDefault(x => x.name == hp.Transform);
|
|
if (hardpointTransform != null)
|
|
{
|
|
var hardpointGameObject = hardpointTransform.gameObject;
|
|
}
|
|
}
|
|
}
|
|
RadiatorMeshes = new Dictionary<Radiator, MeshRenderer>();
|
|
Barrels = new Dictionary<HardpointData, Transform[]>();
|
|
BarrelIndices = new Dictionary<HardpointData, int>();
|
|
foreach (var radiator in entity.GetBehaviors<Radiator>())
|
|
{
|
|
var hp = Entity.Hardpoints[radiator.Item.Position.x, radiator.Item.Position.y];
|
|
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 (Splash, Hit)
|
|
// are deleted -- FireControl decides hits now and Entity.ApplyHit/DamageSchematic (ServerShared) apply
|
|
// them, the single owner in place of what used to exist once per Unity effect. HullColliders no longer
|
|
// publish Hit/Splash at all (R8).
|
|
|
|
LookAtPoint = new GameObject($"{entity.Name} Look Point").transform;
|
|
|
|
foreach (var articulationPoint in ArticulationPoints)
|
|
{
|
|
articulationPoint.Target = LookAtPoint;
|
|
}
|
|
|
|
_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 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));
|
|
}
|
|
|
|
LookAtPoint.position = transform.position + Entity.LookDirection.ToUnity() *
|
|
(Entity.Target.Value != null ? max(Entity.TargetRange,Entity.ItemManager.GameplaySettings.ConvergenceMinimumDistance) : 10000);
|
|
LocalSpace.localPosition = transform.position = Entity.Position.ToUnity();
|
|
if (_influenceInstance)
|
|
_influenceInstance.position = new Vector3(Entity.Position.x, 0, Entity.Position.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();
|
|
}
|
|
}
|