From 0e2fe6dc11585520f29676e35ab94d1ec0720d7b Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 18 Sep 2026 13:51:26 +0200 Subject: [PATCH] Add ShieldInterceptor: the interceptor presenter and panel pool (Cut 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit R2: the shield is an interceptor. ShieldInterceptor implements IAbsorbPresenter and converts each AbsorbEvent into a pose (intercept point projected onto the ShieldEnvelope, normal from SurfaceNormal, size from the item's PanelRadius) and a strike, deciding nothing else. It reuses Assets/Scripts/Prototype.cs rather than writing a second pool. Reuse: a second hit landing within PanelRadius*ReuseFraction of a live panel strikes that panel (Hit only, no ResetSim) instead of spawning a new one -- this is what makes ShieldSim.compute's temper erosion observable across multiple hits. Pool exhaustion: when MaxLivePanels is already live, the oldest live panel (by spawn order) is recycled in place -- reposed and re-armed for the new intercept point -- rather than dropping the strike or growing past the §2.3 frame budget. Co-Authored-By: Claude Opus 5 --- Assets/Scripts/Gameplay/ShieldInterceptor.cs | 179 ++++++++++++++++++ .../Gameplay/ShieldInterceptor.cs.meta | 2 + 2 files changed, 181 insertions(+) create mode 100644 Assets/Scripts/Gameplay/ShieldInterceptor.cs create mode 100644 Assets/Scripts/Gameplay/ShieldInterceptor.cs.meta diff --git a/Assets/Scripts/Gameplay/ShieldInterceptor.cs b/Assets/Scripts/Gameplay/ShieldInterceptor.cs new file mode 100644 index 000000000..409946d04 --- /dev/null +++ b/Assets/Scripts/Gameplay/ShieldInterceptor.cs @@ -0,0 +1,179 @@ +using System.Collections.Generic; +using UnityEngine; +using CultMath.UnityBridge; +using ShieldField; + +// Cut 3 of docs/shield-panel-cut.md (R2): the shield is an interceptor. It converts each absorbed +// hit into a hard-light panel posed on the ship's ShieldEnvelope -- it decides nothing about the +// rule (R3/R6: the hits it shows are cosmetic, fire control decides what actually happened). +// +// Adds no pool: Assets/Scripts/Prototype.cs already is one (Instantiate/ReturnToPool), and +// ShieldManager.ShowHit already uses it for exactly this short-lived per-hit shape. +public class ShieldInterceptor : MonoBehaviour, IAbsorbPresenter +{ + public ShieldEnvelope Envelope; + public Prototype PanelPrototype; // a prefab carrying one ShieldPanel (Cut 4 rigs it) + public int MaxLivePanels = 12; // §2.3: ~0.12 ms CPU issuance per live panel + public float PanelRadius = 2f; // the SHIELD ITEM's size, not the hull's (R2) + public float EnergyScale = 1f; + [Range(0f, 2f)] public float ReuseFraction = 1f; + [Tooltip("Margin added on top of growDuration + shardLife before a panel with no further " + + "hits is released back to the pool (§3: a fixed lifetime, not a GPU readback).")] + public float DeathMargin = 0.25f; + public FracturePattern[] PatternByDamageType; + + struct LivePanel + { + public ShieldPanel Panel; + public Prototype Proto; + public Vector3 Position; + public float SpawnedAt; + public float DieAt; + } + + readonly List _live = new(16); + + void Awake() + { + // Cut 4 rigs the scene; this is the one push Cut 3 owns so the prototype's tiling is built + // at the item's size (R2: size is the item's, not the hull's) rather than whatever radius + // happens to be authored on the prefab. It only lands correctly if the prototype's own + // ShieldPanel has not already run its first OnEnable/Rebuild -- see the Cut 4 recipe note + // on starting PanelPrototype's GameObject inactive in the scene. + if (PanelPrototype != null) + { + var proto = PanelPrototype.GetComponent(); + if (proto != null) proto.panelRadius = PanelRadius; + } + _live.Capacity = Mathf.Max(_live.Capacity, MaxLivePanels); + } + + void Update() + { + // Release timed-out panels. A GPU readback of "is anything still visible" would stall the + // pipeline every frame for every live panel (§3's cost note); a fixed lifetime, started at + // arm time, is the honest and cheap alternative -- same tradeoff the map makes for Death in + // ShieldPanel itself. + for (int i = _live.Count - 1; i >= 0; i--) + { + if (Time.time < _live[i].DieAt) continue; + _live[i].Proto.ReturnToPool(); + _live.RemoveAt(i); + } + } + + public int LiveCount => _live.Count; + + /// Cut 3 verification surface (Q7: no NUnit path for these files) -- lets the + /// batchmode probe step and read back a specific live panel's simulation without duplicating + /// the interceptor's own spawn/reuse/pose logic. + public ShieldPanel DebugPanelAt(int i) => _live[i].Panel; + + // IAbsorbPresenter. The event is enough (map §"The event is enough"): project the event's + // world-space position onto the envelope, pose a panel there with the envelope's true surface + // normal, strike whichever panel owns that point. + public void Absorb(AbsorbEvent e) + { + if (Envelope == null || PanelPrototype == null) return; + + Vector3 worldPos = e.Position.ToUnity(); + Vector3 intercept = Envelope.ProjectToSurface(worldPos); + Vector3 dir = e.Direction.ToUnity(); + float energy = e.Magnitude * EnergyScale; + var pattern = PatternFor(e.DamageType); + + int reuseIndex = FindReusable(intercept); + if (reuseIndex >= 0) + { + // Reuse: a second hit landing inside a living panel strikes THAT panel rather than + // spawning a new one -- this is the rule that makes temper erosion (ShieldSim.compute's + // multi-hit budget) visible at all. No ResetSim here: that would wipe the very state + // this rule exists to preserve. + _live[reuseIndex].Panel.Hit(intercept, dir, energy, pattern); + return; + } + + ShieldPanel panel; + Prototype panelProto; + if (_live.Count >= MaxLivePanels) + { + // Pool exhausted: recycle the oldest live panel in place rather than drop the strike + // (one event, one strike) or grow past the §2.3 frame budget. The oldest is the one + // furthest through its own fade. Reused directly (not routed through + // Prototype.ReturnToPool + Instantiate) because the interceptor already holds the + // exact instance to reassign; a round trip through the pool's free list would only add + // SetParent/SetActive churn for the same outcome. + int oldest = OldestIndex(); + panel = _live[oldest].Panel; + panelProto = _live[oldest].Proto; + _live.RemoveAt(oldest); + } + else + { + panelProto = PanelPrototype.Instantiate(); + panel = panelProto.GetComponent(); + } + + var normal = Envelope.SurfaceNormal(intercept); + var t = panel.transform; + t.position = intercept; + t.rotation = Quaternion.LookRotation(normal); + // Scale is never touched here: R2/Cut 3 require it stay 1, which is a property of the + // prototype's own authored transform and its parent chain (Cut 4's rig), not something + // this component enforces at pose time -- parenting under the envelope's non-uniform + // Shield-prefab scale would skew every shard (§ Cut 3 "Scale must be 1"). + + panel.ResetSim(); + panel.Hit(intercept, dir, energy, pattern); + + _live.Add(new LivePanel + { + Panel = panel, + Proto = panelProto, + Position = intercept, + SpawnedAt = Time.time, + DieAt = Time.time + panel.growDuration + panel.shardLife + DeathMargin + }); + } + + int FindReusable(Vector3 point) + { + float threshold = PanelRadius * ReuseFraction; + float threshold2 = threshold * threshold; + int best = -1; + float bestDist2 = float.MaxValue; + for (int i = 0; i < _live.Count; i++) + { + float d2 = (_live[i].Position - point).sqrMagnitude; + if (d2 < threshold2 && d2 < bestDist2) + { + bestDist2 = d2; + best = i; + } + } + return best; + } + + int OldestIndex() + { + int oldest = 0; + float oldestTime = float.MaxValue; + for (int i = 0; i < _live.Count; i++) + { + if (_live[i].SpawnedAt < oldestTime) + { + oldestTime = _live[i].SpawnedAt; + oldest = i; + } + } + return oldest; + } + + FracturePattern PatternFor(DamageType t) + { + int i = (int)t; + if (PatternByDamageType != null && i >= 0 && i < PatternByDamageType.Length) + return PatternByDamageType[i]; + return FracturePattern.Radial; + } +} diff --git a/Assets/Scripts/Gameplay/ShieldInterceptor.cs.meta b/Assets/Scripts/Gameplay/ShieldInterceptor.cs.meta new file mode 100644 index 000000000..661fc6598 --- /dev/null +++ b/Assets/Scripts/Gameplay/ShieldInterceptor.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: fdca6a832d764b6e89fc83fdcf7b42d7