Add Scarab and Urchin vessels with ecosystem and UI support - #799
Open
FenrysUnchained wants to merge 795 commits into
Open
Add Scarab and Urchin vessels with ecosystem and UI support#799FenrysUnchained wants to merge 795 commits into
FenrysUnchained wants to merge 795 commits into
Conversation
…rvable Reading the shipped expression turned up a real composition bug. The drift was added BEFORE the rotation, and the rotation is about the face centroid - so `rel` was the ~12 world units the shard had already travelled rather than the face's own ~1 unit of extent. The face swung on a wide arc instead of spinning in place, and at the angles this reaches (up to ~3 rad) the shard ended up travelling back toward the blow rather than away from it. Tumble now runs first, drift second. The tumble is also conditional (a degenerate normal, or an impulse straight down the face normal, has no axis to turn about) while the drift is not, so neither case returns early any more - a face struck dead-on is pushed, it just does not spin. The verifier's expectation for that case was transcribed from the code rather than from the intent, so it asserted the bug. Corrected: it now states the composition (spin in place, then translate) and independently checks that a dead-on strike still drifts. Also adds CSLogChannel.PrismShieldShatter and one guarded line per disengage: whether the batched overlay was accepted at all, and the impulse before and after the shield's speed clamp. "The shatter looks unchanged" has two very different causes - no overlay, or an overlay carrying no impulse, since zero velocity is the identity by design - and nothing on screen tells them apart. Off by default; FrogletTools > Toolbox > Logging turns it on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HECJksYQTkpbPwpFWzTiBp
…dais-dkzo2j feat(scarab): pay a struck switch with a scarab-wing dais
…pawned N times
The right thumb stick is REPLICATED, and ScarabJukeController never checked ownership.
InputStatus.RightNormalizedJoystickPosition is backed by a NetworkVariable with read
permission Everyone (InputStatus.n_rNorm), on the InputStatus NetworkBehaviour that
lives on Player.prefab - which carries a NetworkObject and is registered in
DefaultNetworkPrefabs.asset, so IsSpawned is true in every networked session and the
getter returns the owner's stick on EVERY machine.
ScarabJukeController.Update had no ownership test, and nothing above the stick read
stops a replica: _jukeArmed re-arms everywhere (jukeCooldownSeconds 0), InputStatus is
non-null on replicas, and AutoPilotEnabled is false on every peer for a HUMAN pilot
(Player.StartPlayer only calls ToggleAIPilot for IsInitializedAsAI, and returns early
on network clients). So the whole fire path ran once per peer:
- N cavitation plates per dash - each peer spawned its own AOECylindricalExplosion
and shredded its own local prism set;
- the Astro League ball took TWO kicks - the server's own replica-blast called
AstroLeagueBall.ApplyBlastServer directly while the owner's blast arrived again
through RequestBlastBall_ServerRpc (the double-credit shape BENDS.md records for a
replayed blast);
- replicas wrote ModifyVelocity for a vessel they do not own, fighting the
owner-authoritative NetworkTransform;
- and NotifyJukeFired_ServerRpc looked redundant, because the server was already
firing - which is the tell that nobody expected the replicated stick.
SCARAB.md section 3.4 already specified the correct shape ("owner poll -> execute
locally -> Juke_ServerRpc -> Juke_ClientRpc -> non-owner peers play the visual"); only
the ServerRpc half had been wired, and without the owner gate the "owner poll" was
every peer polling. This lands the rest of it:
1. The fire path is gated on `_status.Player is not { IsLocalPilot: true }`.
IsLocalPilot, NOT IsOwner: it is the same predicate InputController.Update uses to
decide who may CONSUME the stick, and the only one that also holds on the legacy
non-networked single-player spawn path where IsSpawned is false and IsOwner reports
false for a human. A response to local input belongs behind the same gate as the
input itself.
2. The 360 degree spin is now SENT rather than left to fall out of duplicated
simulation: NotifyJukeFired_ServerRpc carries the roll sign and fans out
BroadcastJukeRoll_ClientRpc (the host, owning the server copy, broadcasts directly
instead of round-tripping). Non-owners get RollRoutine with a NULL transformer, so a
replica writes only the visual child's local rotation - never the root bank, never
BlockRotationOverride, both of which belong to the owner.
This also makes SCARAB.md's "a client's cavitation explosion exists only on that
client" true; it described an intent the code did not have.
Also noted in AOECylindricalExplosion: it is spawned directly, not through
ExplosionHelper.CreateExplosion, which special-cases the cone.
Verified: Roslyn type-check of ScarabJukeController against a stub harness transcribed
from the real VesselTransformer/IVesselStatus/IPlayer/NetworkBehaviour signatures;
structural compile of every file the branch touches; the Netcode RPC suffix convention;
and the ownership matrix walked by hand for host-owned, client-owned and observer
peers (owner rolls locally and skips the ClientRpc; every other peer rolls cosmetically;
the server's strike window still opens via the ServerRpc).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WPQZ9ActM4dCrqL9GUW8UV
…zero its zero state Two defects found by reading the project's own settings and the blast's own exit paths. 1. THE CONTACT HOLD WAS SIZED AGAINST A TIMESTEP THIS PROJECT DOES NOT USE. contactHoldSeconds 0.05 was chosen against Unity's default 0.02 fixed timestep; ProjectSettings/TimeManager.asset runs 0.04 (25 Hz), so it guaranteed only 1.25 physics steps at the plate's full extent - and one step is the difference between "the punch reliably catches a ball at its rim" and "usually". The hold is now floored at 2 x Time.fixedDeltaTime, so the guarantee is expressed in the unit it actually depends on and survives someone retuning the timestep. At 0.04 that is 0.08s / 2.0 steps; the sweep itself is 5.25 steps. 2. AT DEPTH 0 THE TRIGGER WAS A 90x90 INVISIBLE PANE, AND A BLAST CAN FREEZE THERE. ShapeTriggerBox(0) produced 2R x 2R x epsilon - full width, no thickness - because only the axial term was driven by depth. The base spherical blast reaches the correct zero state for free (it zeroes transform.localScale in Initialize, which zeroes its collider with it); this one drives the collider directly on a scale-1 root and has to say so. It matters because AOEExplosion's cancellation contract deliberately leaves a cancelled explosion ALIVE at its current size so a visible blast does not pop out of the world - and the pre-visual early-out runs before the sweep has advanced at all, so an early cancel would have parked an invisible 90x90 vessel/ball hitbox in the arena for the rest of the match. Depth <= 0 is now a nothing-box on every axis, and that same pre-visual exit disables the trigger outright (nothing has rendered yet, so there is no continuity to preserve - only an inert husk holding a live hitbox). The geometry proof now asserts both: the zero state is a nothing-box, and the contact window is at least two physics steps at the shipped timestep. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WPQZ9ActM4dCrqL9GUW8UV
…t from typed-in numbers
Tools/Build/verify_scarab_cavitation_plate.py, in the shape this repo already uses for
generated constants (verify_gyroid_octagon_tables.py and friends) and for the same
reason: a geometry argument validated once in a session, against numbers typed into a
script, is not evidence about what ships. Between the argument and the asset there is a
TRANSCRIPTION, and that step is invisible to both the argument and code review.
It parses Scarab.prefab, AOEScarabCavitation.prefab and ProjectSettings/TimeManager.asset,
derives the plate the game will actually build, and asserts:
- the hull collider is centred on the vessel origin (or "radius" means something else)
- the Burst slabs tile [0, L] exactly at 10..240 fps - no gap, no reach past the tip
- the drawn cylinder IS the damaged volume, frame for frame
- the trigger box circumscribes and never under-reaches
- depth 0 is a nothing-box on every axis (a cancelled blast can freeze there)
- the contact hold covers >= 2 physics steps at the REAL fixed timestep, and is floored
off fixedDeltaTime rather than being a bare wall-clock number
- proportionalDebris is on and restitution x Inertia == 1, so debris leaves at the
blast's own velocity
- the plate child's authored rotation maps the built-in Cylinder's +Y onto the sweep
axis, and the mesh really is the Cylinder (10206), not the Sphere it was cloned from
Proven to FAIL, not just to pass - four negative controls, each reverted byte-identically
afterwards: reverting the plate mesh to a Sphere, retuning Inertia off the parity value,
and un-flooring the contact hold all fail correctly.
The fourth control taught the tool something. Asserting "an inscribed sphere would
under-reach" failed on a plate authored at L >= 2R - a LEGAL aspect where a sphere would
be adequate and the box is still correct, just no longer necessary. That is a validator
enforcing a window it measured at one aspect, which is how a gate starts lying, so the
sphere comparison is now printed as info and the assertion is the one that actually
matters: the trigger never under-reaches.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WPQZ9ActM4dCrqL9GUW8UV
An eight-dimension audit of the right-stick dash -> cylindrical blast chain. Two of its dimensions independently re-derived the replicated-stick defect already fixed in 5848011 (from the same evidence: InputStatus.n_rNorm, readPerm Everyone). These are the rest. 1. A STRAIGHT UP/DOWN JUKE AIMED THE BLAST AT WORLD +Z. The blast's rotation is Quaternion.LookRotation(dir, ship.up), and a vertical juke makes dir EXACTLY ship.up - the keyboard's right stick is digital, so it hands over exactly (0,1), and ProjectOnPlane leaves it untouched while flying straight. LookRotation cannot resolve a basis from parallel vectors and returns identity. That was harmless when this line was written, because the blast was a SPHERE; the plate reads its sweep axis straight off that rotation, so the punch swept and shoved mass along world +Z while the player watched their ship dash upward. The existing sqrMagnitude guard cannot catch it either - identity yields a perfectly unit forward. Now picks an up-hint that cannot be parallel; the cylinder is rotationally symmetric about its axis, so the hint only has to be usable (unlike the cone's gape axis, which IS gameplay). 2. THE CRYSTAL SWEEP REACHED 43 UNITS BEHIND THE PILOT. Prisms come off the exact Burst slab, but crystals came off the swept cylinder's BOUNDING SPHERE - and a sphere cannot bound a squat cylinder tightly: at 45 wide by 54 long it reaches 42.9 u behind the hull on the first frame. A crystal conversion is not a soft outcome, it SPENDS the crystal and forges a ball, so the blast built balls out of mass it visibly missed while its own prism half agreed it had touched nothing there. The sphere is now only the broadphase; SweepCrystals takes an optional SweptCylinder narrowphase running the same predicate AOECylinderSweepQueryJob runs on prisms. Verified against the reported case: a crystal 30u astern is inside the sphere and outside the cylinder; one genuinely inside the plate still resolves. 3. THE BLAST HAD NO TURN-END KILL SWITCH. AOEExplosion.gameData is [Inject] and is the only thing that wires OnMiniGameTurnEnd -> CancelExplosion and OnResetForReplay -> PerformResetCleanup, but ScarabCavitationBlast spawned the blast with a bare Instantiate and never injected - so this was the one AOE in the game that kept sweeping after a turn ended and survived a replay reset. Silent, because the null-guard that swallows it is written exactly as a good null-guard is (CLAUDE.md's anti-pattern on relying on [Inject] at a non-injecting spawn site). Now injected off the vessel impactor's container, before Initialize, the way ExplosionHelper does it. 4. PROSE THAT QUOTED DERIVED NUMBERS WENT STALE. The 5x/3x resize (3988d56) moved the geometry but not the rationale written by 91faccc, so SCARAB.md and the load-bearing comment in AOECylindricalExplosion.Initialize still asserted an 18 u reach, a 0.070 s duration and a 5.6 u dash displacement against the shipped 54 / 0.210 / 16.8. Re-anchored, and the general lesson recorded next to the rule that caused it: when a blast's reach becomes DERIVED, prose quoting the derived numbers goes stale the moment it is retuned - state the relationship and let the verifier print the arithmetic. 5. TWO UNCONDITIONAL LOGS PER DASH. CLAUDE.md requires finished-system bring-up telemetry to move to a CSLogChannel rather than shout forever. New CSLogChannel.ScarabDash, off by default, guarded by IsVerbose so the interpolation is not paid either. Also corrected two stale "BoxCollider" descriptions of the Scarab hull left over from the sphere swap (ScarabHullBuilder's class doc, SCARAB.md section 3.0). Reported but deliberately NOT changed, because each is a design call rather than a defect: DualMouseInputStrategy never writes the right stick (so the dash is unfirable there); holding the stick at the perimeter re-fires the dash every 0.5 s (the blast's own cooldown still paces the punch); the hull sphere over-covers the body vertically by ~2.9x, which is inherent to fitting one sphere to an elongated hull; and the Scarab's skimmer strictly contains its hull, so the hull's two vessel-crystal effects can never fire. Verified: Roslyn type-checks of both changed files against stub harnesses transcribed from the real signatures, structural compile of every changed file, the plate verifier, and a numeric proof of the narrowphase against the exact scenario the audit reported. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WPQZ9ActM4dCrqL9GUW8UV
…nert half-fix of my own The audit's critic re-read the branch at HEAD and checked eight areas none of the eight audit dimensions covered. Four were real. 1. MY OWN DI FIX (1bafccc) WAS INERT, and its comment documented behaviour that does not occur. Injecting at the spawn site only makes the subscription POSSIBLE: Instantiate runs Awake+OnEnable before any call site can inject, so gameData is null there, and the retry lives in AOEExplosion's PRIVATE SubscribeToGameEvents - called from the base Initialize, which AOECylindricalExplosion replaces wholesale and could not reach. So the blast was injected and still never hooked OnMiniGameTurnEnd / OnResetForReplay. The method is now protected, with a doc comment saying why a subclass needs it, and the override calls it. Both halves are required; neither works alone. AOEConicExplosion has the identical gap and is deliberately NOT touched - that is the Dolphin's play-tested blast in Bends and Rampage, and it deserves its own change and its own playtest. Recorded in SCARAB.md so it is not lost. 2. THE ABILITY MAP CONTRADICTED THE REQUIREMENT. Scarab.asset declared the Cavitation Blast with Input 11 = OnlyRightStickAction, which on gamepad is raised from the right TRIGGER (GamepadInputStrategy builds both stick events off rightTrigger.ReadValue() > deadzone, despite their names) - so the four-icon row would have taught the blast as a trigger ability the moment that row is authored. The enum has no member for "right thumbstick at the perimeter" (the juke polls the axis directly and raises nothing), so it is now 0, the same "no input-event binding" value the Space entry already uses for the ball mint. 3. THE SAME ASSET STILL DESCRIBED A SPHERE to the player - "every right-stick dash throws a compact spherical blast" - which is the exact geometry this branch replaced. The three doc-drift findings all named SCARAB.md and code comments; the shipped text a player reads was missed by every one of them. Rewritten to the swept plate, and to name the control. 4. THE DASH WAS UNFIRABLE ON DUAL MOUSE. DualMouseInputStrategy set only the EASED stick pair and never the normalized one, so RightNormalizedJoystickPosition was permanently (0,0) on that strategy - the Scarab's dash and blast could not fire, the Sparrow's gun aim never tracked, and the thumb-perimeter UI never moved. Its virtual stick is already ClampMagnitude'd to 1, so publishing it is the same contract the other three strategies ship; nothing is newly computed, it was simply never handed over. Also: the plate verifier now STATES the one thing it cannot prove - the hull collider's world lossyScale lives in SparrowModel1.fbx's binary, so R=45 assumes a unit-scaled FBX root. The blast sizes itself correctly either way and every assertion is about relationships that hold at any scale, but whether 4.5 is the intended WORLD radius needs one look in the editor, and the checklist now says exactly where to look. And four latent traps the critic surfaced are recorded in SCARAB.md: the trigger-named stick events, blast<->ball working only because the ball is layer 0 (Explosions does not collide with Crystals), the shared 16-element crystal overlap buffer, and the debuff effect asset shared with the Dolphin's cone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WPQZ9ActM4dCrqL9GUW8UV
…umption Missed by the previous commit, which staged Assets and Tools by path and left Docs behind — exactly the half-landed shape the tooling contract warns about, caught by the stop hook rather than by me. The plate verifier prints a caveat it cannot resolve: the hull collider's world scale lives in SparrowModel1.fbx's binary, so "r 4.5 -> a 45-unit plate" assumes the FBX instance root is unit-scaled. This is the matching in-editor step — select the hull GameObject, read its world scale — so the caveat has somewhere to be discharged instead of being a note nobody acts on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WPQZ9ActM4dCrqL9GUW8UV
…in-spotlight-prisms-cost-ldgl3n
…ll overload Three changes, all Scarab-side. 1. The skimmer forges from OMNI crystals only. An elemental crystal is the platform's element economy - it is how every vessel levels Charge/Mass/ Space/Time - so spending one on a ball meant a Scarab could never level an element it flew past. ScarabBallForgeBySkimmerCrystalEffectSO now returns on anything that is not an OmniCrystalImpactor, handing the crystal back to the HULL, whose four elemental branches collect it normally. TeamCrystalImpactor derives from Omni and is deliberately included. The BLAST forge was already omni-only by construction (ExplosionImpactor.SweepCrystals only picks up OmniCrystalImpactor), so this makes the two paths agree. 2. The Scarab's hull carries no omni-crystal effects (ScarabImpactorDataContainer.vesselCrystalEffects is now empty). The skimmer sphere strictly contains the hull, so whatever the skimmer converts the hull never sees; an omni effect there could only fire on a crystal the forge had already refused, and there is no such case. The four elemental branches keep their effects, because those are exactly the crystals the forge hands back. 3. A CELL holds at most 4 loose balls. When a further one enters, every loose ball in that cell detonates regardless of domain, the arriving one included - one networked event on every peer (TickCellMembershipServer -> DetonateAllLooseInCellServer -> CellOverload_ClientRpc), using the same per-ball DetonateWithRadiusServer the nucleus overload uses so the two read as one event. Embedded/hidden balls do not count: a ball studded in the nucleus is not in play, and knocking it loose is exactly the event the rule is about. This replaces Scarab Scramble's per-DOMAIN cap enforced at FORGE time (ScarabBallForge.ForgeGate, ballsPerPlayer x roster - both retired). A rule enforced at one producer can only ever see that producer: a ball enters play two ways, forged from a crystal and knocked loose out of the nucleus, so the forge-time gate was blind to half of them. It was also arbitrary - it fired at 2 balls for a solo pilot while the same court held 6. The limit lives on the ball prefab because it is a platform ball rule, not a mode policy, so it travels into freestyle and the menu with nothing to install. Two ordering hazards handled: the announcement is sent BEFORE the detonation loop (a forged ball is despawned by its own detonation, so the RPC would be sent from an object that no longer exists), and ServerFixedUpdate bails when the tick detonated this ball rather than continuing to write NetworkVariables on it. The rule is gated on IsSpawned && IsServer, since a no-network local session can neither detonate nor send the RPC. Docs: SCARAB.md 4.1/4.6, SCARABSCRAMBLE.md, UNITY_VERIFICATION_CHECKLIST.md (incl. an MPPM step for the synced overload and a check that both entry routes trigger it), GameToastSituation comment. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WPQZ9ActM4dCrqL9GUW8UV
…traps Session retrospective from the Echo Sight replication + AI orbit-break work. Technique 4.5d — offline simulation of a CONTROL LOOP: extract the pure math into a UnityEngine-free static class so the tested path is the shipped path, integrate a bounded-turn-rate plant, score over a randomized ensemble rather than a scenario, and measure candidate fixes against each other before shipping one. Two of this session's plausible fixes were rejected on those numbers, which is what forced the search to continue to the real cause. Traps: - a comparison mixing a squared quantity with a linear one is invisible to review and to every static check; its symptom is behaviour nobody attributes to arithmetic - a guard that belongs to a heuristic FALLBACK must not be inherited by an explicit provider (the named case is the one the guard rejects) - an AI's engagement range is authored independently of its weapon's reach - IsLocalPilot and IsOwner coincide for every human and diverge for every AI Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014uhgLqwZSu9Zini2pXcs68
…prisms-cost-ldgl3n Every pilot sees every Echo Sight, and AI stop orbiting their objectives
The Dolphin's `DefaultMinimumSpeed` was authored 10, so a released throttle left the vessel crawling at 10 u/s instead of stopping. Set it to 0 and re-derive the speed ladder in DOLPHIN_ENERGY_ECONOMY.md (cruise 78 -> 68, boost 357 -> 347). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DzR5V5ZcTBwbXs1eY89Hj3
…b-squirrel-colliders-4po5km # Conflicts: # Assets/_Scripts/Controller/Vessel/R_VesselActions/SCARAB.md # Assets/_Scripts/Utility/CSDebug.cs
- UNITY_VERIFICATION_CHECKLIST: new newest-first entry with the in-editor steps (throttle-off is now a real stop; drift-at-rest and AI are the risk cases), and a superseded marker on the earlier retune's knob table. - SPEED_TUNNEL fleet table: the Dolphin row still carried its pre-retune 60/210; now 68/347 with the squared boost multiplier noted. - ElementalAbilitySystem/BACKLOG item 20: retune figures re-derived off floor 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DzR5V5ZcTBwbXs1eY89Hj3
…eed-59q8ay fix(dolphin): minimum speed is zero
… steers it
Reported twice as "looks the same as always", and the reason is a design error
rather than a plumbing one: the rotation was made PROPORTIONAL to the breaking
impulse. A shatter with no impulse - or with one that never reaches the GPU -
therefore had no rotation at all and rendered as the pre-change fly-out, pixel
for pixel. That is indistinguishable on screen from "the feature is not wired",
which is exactly how it read.
The reporter's own description isolated it: the faces DO fly along their normals
and DO respect the shatter duration, so Normal, Direction, ShatterOffset,
StartTime and Duration all demonstrably reach the shader. Only the new float3
_ShieldMorphVelocity does not. (Ruled out along the way: the C# value is not
zero - SpawnablePrism.prefab serialises the shield component with an EMPTY field
block, yet the shipped 0.6s duration and 3-unit fly-out are exactly the C#
initialisers, which proves Unity applies initialisers for absent fields and so
shatterDriftSpeedCap is live at 20.)
So the headline motion no longer stands on the least-proven input in the chain:
* PRISM_SHIELD_SHATTER_TUMBLE (rad/s) is ALWAYS ON and depends on nothing but
the clock and the mesh's own face normal - both proven to arrive.
* PRISM_SHIELD_SHATTER_SPIN (rad per world unit travelled) rides on top, so a
hard hit still spins harder than a soft one.
* The axis is cross(v, n) - the explosion's own axis - when there is an impulse,
and the face's in-plane Duff-basis tangent when there is not. In-plane
matters: the face TIPS AWAY from where it was pointing instead of spinning
about its own normal like a plate on a stick, and since the object-space
normal is the face id on these hard-edged meshes, the eight faces never
tumble in lockstep.
General rule now recorded in 4.8.1: an effect's headline motion must not be gated
on the newest, least-proven input. Put the new input on the refinement.
Also: shatter durations 0.6 -> 1.0s (octahedron) and 0.7 -> 1.1s (stellation),
in the initialisers AND in the two prefabs that serialise a value, since "they
don't last long" was the other half of the report - a fly-out reads in a third of
a second, a rotation needs long enough to be seen turning.
The verifier's headline case is inverted to match: it now asserts that a shatter
with NO impulse still tumbles, and that the impulse-less tumble is pure rotation
about the centroid (no drift). Twelve checks, all passing on the shipped HLSL.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HECJksYQTkpbPwpFWzTiBp
…l overload The merge with bleeding-edge surfaced doc drift my own earlier commits left behind, plus one enum collision with a parallel branch. - CSLogChannel.ScarabDash moved 1 << 4 -> 1 << 5. The switch-logging branch claimed 1 << 4 first and merged, and two branches adding "the next bit" merge cleanly into an enum with duplicate members. The mask is a session-only static (no EditorPrefs), so renumbering costs nothing. - UNITY_VERIFICATION_CHECKLIST: step 3b still described the cavitation blast as "a small SPHERICAL explosion ~45u ahead (diameter 90)" and the tuning line still carried blastScale/forwardOffset/Inertia 1.8 — all replaced by the swept plate on this branch and never re-written. Step 4 still described the retired HULL forge (a ball appearing ahead of the nose carrying your speed); the skimmer converts at rest, so the check is now the opposite one, and step 8's "dash-into-crystal parity" is retired for the same reason. - CLAUDE.md: the ScarabScramble paragraph described the per-domain ForgeGate cap that this branch replaced with the per-CELL ball limit, and the AOE-impulse paragraph named only the Dolphin cone as proportionalDebris-on. Both updated, including why the Scarab's product is deliberately 1.0 (debris speed IS the sweep speed) and what a third AOE shape has to carry. - ScarabBallForge.ForgeGate's docstring claimed Scramble installs it. Nothing installs it now; the hook is kept for a mode that wants to gate FORGING, with a note not to reach for it to bound a population again. Recorded in SCARABSCRAMBLE.md's follow-ups alongside the poll-vs-event note. Session retrospective (skills): - asset-surgery: CS2001 on a path you can cat is a QUOTING bug — this repo's space-bearing directories shred a shell-expanded file list; use a response file unconditionally. Plus three networking ordering traps: announce a batch removal before performing it when the announcer is in the batch (a ClientRpc needs a live NetworkObject); a per-object tick that can destroy `this` must return a bool so its caller bails; a server-only rule must be gated on IsSpawned && IsServer, since the local no-network path runs the same tick. - ship: grep AssetDatabase.CreateAsset, never a bare CreateAsset — [CreateAssetMenu] matches the short form and reports gameplay SOs as tools. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WPQZ9ActM4dCrqL9GUW8UV
…lliders-4po5km feat(scarab): sphere hull, +50% skimmer, swept-plate dash blast, omni-only forge, per-cell ball overload
explosion's duration
Two corrections from playtest, both of them the base explosion material's own
method that the shatter was not following.
THE PIVOT. The rotation was applied to (MorphedPosition - FaceCentroid), which
pivots the face around its BAKED centroid - a point it has already flown up to
ShatterOffset units away from along its normal. The face therefore orbited that
point instead of spinning about its centre of mass, which reads as "not really
rotating". The shatter now splits the face the way the explosion material does:
faceCenter = FaceCentroid + offset*Normal // where the centre ends up
rel = faceScale * (Position - centroid) // the face, centred on ITSELF
p = faceCenter + R(rel) + drift
Translate the face centre to the origin, rotate, translate back. The split also
makes the drift immune to the tumble for free - it is added to the CENTRE, never
rotated with the geometry.
THE DURATION. The shatter ran 0.6-1.1s against the prism explosion's 7.5s and
looked truncated beside it. Both tiers now default shatterDuration to
PrismExplosion.DefaultDuration rather than a number of their own, so the two can
never drift again: a shield coming apart and a prism coming apart are the same
event class. Authored into the two prefabs that serialise a value; the main
SpawnablePrism.prefab serialises none and picks up the initialiser.
The tumble is a RATE (rad/second, like the explosion's own), so a longer shatter
spins further - PRISM_SHIELD_SHATTER_TUMBLE re-scaled 4.0 -> 1.2 rad/s for the
new life. Noted in the docs: the shatter's removal mechanism is the face
CONTRACTING to a point rather than the explosion's opacity fade, so the
contraction is now correspondingly slow and is the knob to watch if it ever
reads sluggish.
The verifier is rebuilt around the same decomposition and grows the regression
test for exactly the reported symptom: the rotation must preserve the distance
to the face's own centre, and must NOT preserve the distance to the baked
centroid. Mutation-tested - restoring the old pivot fails both. Thirteen checks,
all passing on the shipped HLSL.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HECJksYQTkpbPwpFWzTiBp
Every future tool (human- or AI-written) now writes against one normative checklist: placement/naming, mandatory [FrogletTool] metadata with calibrated importance, palette-only colours with the banner accent bound to the category colour, the standard window anatomy, config-in-ScriptableObject, the reader/writer declaration, console discipline, and the paperwork (tool + file index rows) that ships with a keeper tool. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSU6uKMdHPVw7QG9vqwZFh
…etector) Always-on editor crash watchdog. A BelowNormal background thread drains a concurrent queue of errors/exceptions (captured via Application.logMessageReceivedThreaded, so capture never depends on a responsive main thread) into an append-only journal under Logs/CrashDetector/, flushing after every drain, and heartbeats a session sentinel carrying the editor state plus a main-thread liveness stamp. A clean exit marks the sentinel clean; a domain reload re-adopts the session by pid. When the next launch finds a sentinel that is neither clean nor owned by a live Unity process, the previous session ended abnormally - Unity crash, PC fault, force-kill, hang-then-kill - and a Crash-*.log report is written from the stale sentinel, the captured error journal, and the tail of Unity's own Editor-prev.log, with a hang-vs-abrupt-death verdict derived from the liveness stamp. Reports are pruned to a cap; an error storm is rate-limited and the journal size-capped so the detector can never fill a disk. Reader tool per Docs/TOOLING.md: writes only machine-local files (Logs/, UserSettings/), never assets - no ledger, no ship panel. Settings are a ScriptableSingleton under UserSettings/; the window (banner, status pills, verdict card, report rows, settings) draws entirely through FrogletEditorPalette per the new authoring contract. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSU6uKMdHPVw7QG9vqwZFh
…tector New FrogletTools lane (FrogletToolCategory.Diagnostics, indigo accent, "Diagnostics & Health") per the TOOLING.md curation protocol - enum, palette map, LabelFor, registry keywords and the section table move together. The crash detector refiles under it: CrashDetector/ becomes Diagnostics/ and the window becomes DiagnosticsWindow (git renames, GUIDs stable), now tabbed Crash Detector + Bug Ledger with a board card for each. The Bug Ledger is the team's live bug list inside the editor. Every distinct error/exception/assert signature auto-files one issue into BugLedger/issues/ at the project root - one small JSON file per issue, committable and merge-friendly: ids derive from the normalized signature (digit/hex runs collapsed; machine-local path prefixes cut back to Assets/ in both mono-style " in " and unity-style "(at " frames), so the same bug lands in the SAME file on every machine and reviewers only ever see the issues a branch touched. Capture rides Application.logMessageReceivedThreaded into a background worker so the log path never blocks on file IO, and one occurrence write per issue per editor session keeps the committed files quiet. A fix is not believed until the game proves it: Mark Fixed -> VALIDATING, and the issue closes (file deleted) only after its clean-session quota - play runs for play-mode bugs, full editor sessions for edit-mode ones, each with a minimum length so an accidental play press is not evidence, and a bounded queue drain before the session verdict so an in-flight error cannot be credited as clean. A recurrence while validating reopens the issue as a regression, loudly. Per issue: pause validation, ignore (parks it and suppresses re-filing), resolve now, delete; custom bugs file by hand, and a crash report files into the ledger with one button. Serialization is hand-rolled both ways (worker-thread-safe, deterministic one-field-per-line layout); the parser tolerates unknown keys, missing keys and garbage files. Round-trip, parser tolerance and cross-machine signature determinism are proven by compiling and EXECUTING the shipped code offline - which caught the unity-style frame format keeping its absolute path, fixed before ship. Reader tool per Docs/TOOLING.md: no Assets/ writes, no ship panel; settings are a ScriptableSingleton under UserSettings/, and the committable store's contract lives in BugLedger/README.md. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSU6uKMdHPVw7QG9vqwZFh
The shatter removed its faces by scaling each one to a point, which reads as the
shield DEFLATING. Debris does not shrink - the exploding prism keeps its faces at
full size and PrismErosionFade sweeps one hard, jagged, UV0-anchored front across
each of them. The shield now does the same, on BlockGraph, where a shielded
prism's own material actually lives.
* faceScale stays 1 for a shatter (the bloom still scales out from the centroid)
* PrismShieldMorph gained an Opacity output - 1 unless shattering, then a LINEAR
1 - p, because the erosion's thresholds are CDF-fitted against a linear alpha
ramp rather than the eased t. Appended as slot 10, so the migration renumbers
no existing edge.
* the shield meshes grew UV0 (Octahedron/StellatedOctahedronMeshGenerator
.ErosionUVChannel). The erosion anchors to a MESH attribute by design and these
meshes carried only UV1. Each face maps to the same isoceles triangle in the
unit square; identical UVs do not make faces peel alike because the wipe's
direction and jag are hashed PER FACE from the centroid. Safe to add only
because BlockGraph reads UV0 nowhere else - the tool asserts that before it
splices.
BlockGraph MULTIPLIES rather than replaces: _Alpha x Survival -> BaseAlpha. A prism
that is not shattering has Opacity 1, PrismErosionFade returns Survival 1 outright
at BaseOpacity >= 1, and Alpha x 1 is Alpha - bit-for-bit the old chain, INCLUDING
the cloak family's authored near-zero alpha. Feeding the erosion the material's
alpha directly would have put a wipe on every cloaked prism.
wire_prism_shield_erosion.py owns this splice and deliberately does not share a
tool with wire_prism_explosion_erosion.py: same HLSL function, two graphs, two
drivers, disjoint asset sets, so neither can regress the other. It ships with the
acyclicity and property-node-type gates whose absence turned this branch magenta
earlier.
Also confirms, by assertion rather than by reading: the tumble axis IS
normalize(cross(Velocity, Normal)) when an impulse is present. The verifier states
the full expected position, so it fails if the cross product is dropped,
mis-ordered or left un-normalised. Seventeen checks, all passing on the shipped
HLSL, plus a new edit-mode gate that every face's UV0 is a real non-degenerate
frame (an empty channel would pop each face instead of eroding it).
Known gap, stated: a prism that is BOTH transparent and shielded renders its shards
on ExplodingBlockGraph, where the erosion is driven by the explosion clock - which
is unstamped for a shard, so TransparentPrismMaterial's resting _Opacity of 0 makes
those shards invisible. Predates this pass, unchanged by it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HECJksYQTkpbPwpFWzTiBp
…mic-Shore into bleeding-edge
…rity, doc links Four upgrades to the Diagnostics family plus its documentation and a doc-link system for the whole tool board. Resolved archive: closing an issue now stamps it (state, resolvedUtc, resolution) into BugLedger/resolved/ - pruned to a cap, toggleable - instead of discarding it outright; git history keeps everything either way, the archive makes "what got fixed lately" browsable. Shared signature core: the fingerprint pipeline moved to CosmicShore.Utility.BugSignature - runtime-safe on purpose, so the planned in-game reporter can hash device-side errors with the SAME code and write the same ids into UGS player data. Pinned by BugSignatureTests (edit mode), including the cross-OS frame case the first implementation failed. Tool findings: BugLedger.ReportFromTool / ReportToolFindings let auditors file failures as tracked issues, deduped by (tool, normalized title) so re-runs refresh rather than duplicate - and the TOOL is the validator: a full clean re-run auto-resolves its validating findings (quota 1; a deterministic re-run is stronger evidence than any number of play sessions). Tool issues carry scope "Tool" and are excluded from session-based validation by construction. VesselSkimmerAudit is the reference integration (findings offered via one dialog, clean runs credit silently); the crash detector's File Bug button now dedupes by report filename and files at blocker severity. Severity: blocker/major/minor on every issue - serialized (missing key reads as major, so pre-severity files stay valid), sorted within each state, click-to-cycle pill on rows, popup on the new-bug form. Doc links: [FrogletTool] grew DocPath (repo-relative path + optional #anchor). Documented cards on the Froglet Master Tool board carry a DOCS chip and an "Open documentation" context entry; FrogletDocLinks builds the GitHub URL from the checkout's own origin remote on bleeding-edge and falls back to the local file. DocPath authored on the board itself, Pending Tool Changes, Game Mode Prefab Kit, Validate Clock Wiring, the skimmer audit, and both Diagnostics cards; the Diagnostics window gained a Docs button. The authoring contract now requires DocPath on documented tools. Docs/DIAGNOSTICS.md is the proper documentation of both tools - mechanisms, store contract, signature identity, the full lifecycle with validation evidence rules, honest limits, the tool-author API, and a 5-minute in-editor verification script - indexed from CLAUDE.md and TOOLING.md; BugLedger/README.md updated to match. Verified offline: full-reference Roslyn type-check of the diagnostics family clean; 34 executed assertions over the shipped code (JSON round-trip incl. the new fields, parser tolerance, cross-machine error ids in both stack-frame formats, severity model, tool-id stability, remote-URL parsing); conditional-compilation gate, meta parity and ambiguity sweeps clean. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSU6uKMdHPVw7QG9vqwZFh
Both fixes target work that runs inside the window the editor spends in "Run managed callbacks", where the main thread is blocked and Windows marks the editor Not Responding. PlayModeSOProtector: the play-mode SO snapshot held ~11 MB of asset text across ~790 SessionState string keys for the whole play session, read every file back on exit to compare contents, and called ImportAsset with ForceUpdate one asset at a time (each triggering its own synchronous refresh). It now snapshots to Library/PlayModeSOSnapshot, gates restore on write time + length so an untouched asset costs one stat call instead of a full read-back, and batches the reimports inside StartAssetEditing/StopAssetEditing. A small SessionState bool keeps a crash-orphaned snapshot from being replayed into a later editor session. Assets created or deleted during play are still left alone. SceneBootstrapper: the OnValidate-noise auto-save ran EditorSceneManager.SaveScene on the active scene after every domain reload and every play-mode exit whenever Cinemachine/URP/NetworkManager OnValidate dirtied it -- Menu_Main is 4.1 MB. It is now opt-in and off by default, toggled from FrogletTools > Scene Setup > Testing Multiplayer. With it off the scene reads as dirty and saving is the developer's call. Docs: PERFORMANCE_OPTIMIZATION.md Task 10 records what the phase is, the five ranked contributors, the Editor.log "Domain Reload Profiling:" block as the measurement that ends the guessing, and the three deferred items with the reason each needs its own pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RsuPLRYp7naxnijvoxp2bs
…te audit Resolves Task 10's two actionable deferred items (the third, FMOD, stays capture-gated). Enter Play Mode Options is ON (m_EnterPlayModeOptionsEnabled: 1, both Disable bits kept), so a Play press no longer pays the domain reload that was the largest cost in the 'Run managed callbacks' stall. The audit that gated it classified every mutable static and static event in the 259 runtime files that carry one, and ships 52 new [RuntimeInitializeOnLoadMethod(SubsystemRegistration)] resets for the needs-reset bucket: OnApplicationQuit latches that fire on editor play exit (PrismEffectsManager._isQuitting killed half the VFX system from the second Play on), Time-stamp comparisons across the restarting clock (haptics rate limits, combat-hit latches, per-impactor explosion cooldowns), begin/end bracket counters whose finally a play exit skips (PrismTrailBuilder's arena gate, Prism bulk transport), benchmark overrides with no restore path, disk-cache Initialized latches, and the dormant PlayFab session statics. Two structural cases: a non-generic reset host for NetworkClientCache<T> (the attribute never fires on an open generic), and an instance teardown for TournamentController, whose SceneManager.sceneLoaded subscription outlives its pure-C# singleton. Obvious.Soap gets the project's second [Cosmic Shore patch]: ScriptableEventBase had no play-mode lifecycle at all, so an event's private _onRaised delegate survived every Play press and the un-unsubscribed constructor lambdas in AnalyticsServiceFacade (14), ApplicationStateMachine (3) and TournamentController (1) would have stacked one dead handler set per session. Events, lists and dictionaries now clear their C# delegates at both play-mode boundaries, ScriptableVariable also clears _onValueChanged, and all four families fix a reimport double-subscribe. Reflex 14.1.0, UniTask, Netcode 2.5.0, FMOD and DOTween were verified compatible from pinned source. The editor asmdef split is closed with data, not shipped: 79 of 168 editor files are tests that cannot move, only 26 of the remaining 89 (~17% of editor LOC) are free of gameplay-type references, and the split cannot touch the reload cost - only sub-second compile time. Docs: Task 10 records the resolution, the recurring leak shapes for review calibration, and the protocol that keeps the flip safe (second- Play misbehavior means a missed static; fix with a reset, not by turning the option off). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SNzk7VVDVWNsM6G9SvNoBw
…ce sets THE MISSING BAND. The gauge track is drawn after the ability plate and, at gaugeCellFraction 1, is exactly the same trapezoid - so at full width it painted over the plate's slant band and that card silently lost its edge. It only happens on a card that actually BINDS a meter, which is why it showed up on the Squirrel's Charge slot alone (and would have on Sparrow Time and Scarab Space). Everything laid on a plate now insets by PlateEdgeReach = slantEdgeThickness + slantEdgeAntialias, so the band FRAMES the meter, which is the better read anyway. The general shape is worth remembering: a decoration that exactly covers the thing beneath it is invisible until something else needs what is underneath. TIGHTER ROW. cardPitch 137.7 -> 116, authored as plateWidth + 2x cellGap so the space BETWEEN totems is exactly twice the space WITHIN one - the same relationship as inter-word against inter-letter spacing, and what keeps four cards reading as four objects rather than one strip. Margins 65.1 -> 40 right and 53 -> 44 bottom; the chip's own bottom margin goes 21px -> 14px. CONTROL CHIPS. chipGap 8 -> 6, matching cellGap: the chip is a third element in the same vertical stack, so it should clear the ability plate by the same distance the element plate does. And the reason they moved when the device set changed: Unity does not lay out inactive hierarchies, so placing a hidden set's glyphs measured a stale or zero parent rect and baked a wrong anchor into them - pad -> keyboard -> pad brought the pad glyphs back somewhere else. TryApplyAbilityPlacement now skips anything that is not activeInHierarchy, and ApplySet re-arms placement every time a set is shown, so each set is placed from live rects on the frame it appears. That also makes the placement self-healing against anything that moves the row later. Auditor and tests gained the three relationships this pass introduced: cards must sit further apart than a totem's own plates, chipGap must equal cellGap, and the gauge must be inset rather than flush with the plate outline. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YZnzo7ipUR6TSQ9FjMf7bt
Claude/qa backlog 7mvlsr
The missile crosses ~360 u of arena over three seconds and a hit is worth 50
points in Dog Fight, so everyone in the arena has a reason to see it coming and
know whose it is. It gets a TAIL — the long streak tuned for legibility at range
(Docs/VESSEL_TAIL_AND_JETS.md) — not a jet (a short plume on an engine node,
tuned for the pilot flying that engine) and not a trail (conserved prism mass;
this carries no collider, no state, and killing it destroys nothing).
A tail is defined by its PURPOSE, not its owner. The contract said "vessel"
because vessels were the only things that had one; this is the first non-vessel.
A jet stays vessel-only: it is a readout of an engine, and a missile is one.
The shared asset, not a copy. SkyBurstProjectile.prefab NESTS
Components/VesselTail.prefab, so a retune of the tail's look reaches the missile
with no second edit. On the instance, widthScale and the authored local position
are both inert — the code owns them, per flight, because the round's size is not
a constant.
Both missile-specific numbers are MEASURED, off the geometry the hit sphere is
already fitted to, so the tail and the hit volume cannot disagree about how big
the round is:
mount the model's measured rear face, scaled by growth (Projectile.TailMount)
— the exact mirror of the nose fit
width 0.4 x the round's own body diameter (Projectile.TailWidth) — 3.05 u at
resting Mass. Deriving it is not optional: the round swells 14x-38x with
MASS and a TrailRenderer's width is world-space, so a fixed ribbon would
be a thread behind a 63 u missile. 0.4 is the ratio the one hull the
fleet actually tuned a tail against already flies at (widthScale 2.5 on
a ~6.4 u hull), so the missile's tail reads as the same KIND of streak.
colour the firing pilot's live domain, re-read per flight, never snapshotted —
the same reason ApplySpikeAppearance is
Two things a pooled round needs and a vessel does not:
* ReclaimTail() clears the ribbon at launch. A TrailRenderer records
world-space points, so without it every reissue draws one straight line from
wherever the last missile detonated to this one's bay.
* ReleaseTailToFade() cuts the tail loose at retirement — the round is switched
off 0.025 s after it detonates, which would blink several hundred units of
live ribbon out in one frame. The blast covers the head of it; the rest is
nowhere near the explosion. Continuity of existence applies to a tail exactly
as it applies to a prism. It comes home when the fade ends or when the round
is re-fired, whichever is first: a 20-deep pool cycles faster than a 4 s
ribbon.
TailGradient (new) is now the ONE composition of a tail's colour gradient, shared
by VesselTailAndJets and Projectile — two transcriptions of one gradient drift
the first time either is retuned, and a tail that reads differently depending on
what wears it has stopped being one signal.
VesselTail.prefab loses six dead disabled particle systems (~690 KB -> ~4 KB).
They were free on a vessel and would not have been on a pooled projectile: the
skyburst pool is 20 deep per Sparrow, so a four-Sparrow match would have carried
~480 disabled ParticleSystems nothing could ever play. Proven safe rather than
assumed — every reference into that prefab anywhere in the project resolves to
one of the four surviving objects.
Also excludes the tail from CacheTransformRole: a TrailRenderer is a Renderer,
and counting it would tell a bare hit sphere (the turret's carried collider) that
it draws a model, silently switching its growth off the moment it got a tail.
TailMount/TailWidth are pure statics with five new cases in
SparrowRoundGrowthTests (rear-face fit, sign, width curve, the 0.4, monotonicity
in Mass).
Not editor-verified — /verify-unity could not run in this session. Roslyn parsed
every changed .cs clean, the shipped numbers re-derive to the prefab's collider
bytes exactly, both edited prefabs pass a YAML integrity pass, and
check_conditional_compilation.py is green. The look is unearned; the in-editor
steps are Prompt 17 in Docs/UNITY_VERIFICATION_CHECKLIST.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y7R8eSqTufRumLLyxLyabo
The glyph "moving" on a device switch was never a drift. It was a SIZE mismatch, and my previous fix addressed a cause that was not the cause. Measured off Squirrel.prefab: the pad icon strips are 269x11 px with glyphs spanning 50x50, while the PC text strip is 366x22 with glyphs at 106x22. Placement centred every hint on its card's 24px chip socket, so the two sets got DIFFERENT clearances - the pad glyph overhung the card by 7px while the keyboard one sat 7px clear. Going pad -> keyboard -> pad looked like the label had moved; it never moved, the two sets were never the same size. AdoptIntoSocket now reparents each hint INTO its card's ControlChip, stretched to fill it, with preserveAspect on the image. The lockup owns the chip's size as well as its position, exactly as it owns the ability icon's, and a hint supplies only artwork - so every device set renders at one size with one clearance, 6px below the card. Being a child also makes the position structurally undriftable: no rect fractions, no retry, no dependence on when a set was last laid out. Its other half is ApplyAdoptedVisibility. An adopted hint has left its icon-set root, so activating that root can no longer reach it and the switcher has to toggle the hints themselves; without it every device's glyphs would show at once. PlaceOnAbilityIcon survives as the legacy path for a HUD with no lockup, and its doc comment - which asserted the previous, wrong diagnosis as fact - is corrected rather than left in the tree. The general trap, recorded in CLAUDE.md and ABILITY_LOCKUP.md: two things that look interchangeable at one size are only interchangeable at that size, and anything centred on a fixed socket inherits the content's height as a variable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YZnzo7ipUR6TSQ9FjMf7bt
…ld HUDs REVERT THE ADOPTION. Re-homing the control hints into the chip socket made their position undriftable, but it took them out from under the icon-set roots InputDeviceIconSetSwitcher activates - so switching device sets stopped working and the keyboard set could no longer be reached. The rule it cost: a component that shows and hides things by toggling a parent OWNS that parenting, and anything that re-homes its children has to take over the showing and hiding too. Not worth it here, so the hints stay under their roots. The actual defect was never the parenting, it was the SIZE, and that part stands: PlaceOnAbilityIcon now takes a size alongside its target. With one supplied it collapses the anchors to a point and states the size outright, plus preserveAspect on the image, so every device set renders identically; with none it keeps the old span-preserving path for a HUD with no lockup. The size comes from the chip socket's own sizeDelta - point-anchored, so no layout pass - which is why SUPPLYING it is safe where READING the hint's rect is not: these glyphs are stretch rects with sizeDelta zero and read as zero before layout. RETIRE THE OLD UI. RetireLegacyChrome works per ability HOST, so it reaches nothing on a HUD with no hosts - leaving exactly the vessels whose row is all LOCKED cards (Rhino, Manta, Serpent) drawing their old boost meters and buttons next to the new row, where the older one looked like the real UI. A HUD that binds no icons now has its root-level content retired too, under two guards: only children that actually DRAW (a subtree with no Graphic is logic, not UI, and switching it off would stop behaviour rather than hide a picture), and the device-hint roots are EXEMPT - they are direct children of the HUD root, so a positional sweep would take every control hint with them, on the one un-populated vessel that has any. The auditor lists what each HUD will lose, since it happens at runtime and the prefab still shows it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YZnzo7ipUR6TSQ9FjMf7bt
…ow-missile-tail-a8pntf
bleeding-edge superseded Docs/UNITY_VERIFICATION_CHECKLIST.md while this branch was in flight: new unverified work now goes in the PR body's Verification status section, which /qa-backlog scans into Docs/QA/QA_BACKLOG.md. So the Prompt 17 entry this branch added is removed and its content moves to the PR body, and SPARROW_SKYBURST_BAY.md's verification section points at the current loop for the tail while keeping the existing checklist pointer for the bay + growth pass, whose entries are still live there. (The tail commit's own message cites 'Prompt 17 in UNITY_VERIFICATION_CHECKLIST.md'; that pointer is superseded by this commit and by the PR body.) Also keeps CacheTransformRole's docstring in step with its body — it now excludes two renderers, the charge shell and the tail, because neither draws the round. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y7R8eSqTufRumLLyxLyabo
It is the same marker, on the same shared prefab, that the skyburst missile now wears - so 'A vessel's TAIL' is the exact doc rot this branch created, and a reader of the component would conclude tails are vessel-only. Also corrects a pre-existing contradiction in the same block: it claimed a jet is 'seen by that pilot only, by default', which the shipped contract explicitly retired (VESSEL_TAIL_AND_JETS.md 2 - a per-viewer switch was added once and removed, because a rival's plumes are how you read their thrust up close). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y7R8eSqTufRumLLyxLyabo
Three additions, all from work this session did for the first time: - ADDING a nested prefab instance. The skill covered reading them thoroughly and writing them not at all. Three documents plus a list edit, and the third -- a stripped stub PER COMPONENT you need to reference from the host -- is the one that fails silently: a serialized field just reads None. - Deleting objects from a SHARED prefab, with the project-wide proof that makes it safe, including the regex detail that matters (Unity wraps the guid onto the next line at ~80 columns, so a single-line grep misses about half the real references). Plus the reason the deletion became worth doing at all: a 'costs nothing' claim about a dead asset is scoped to its current consumers, and pooling a new one voids it. - The TrailRenderer-on-a-pooled-object trap, in three parts: the world-space reuse streak, the vanish-on-return, and the fact that a TrailRenderer is a Renderer so any GetComponentsInChildren<Renderer>() semantic test changes answer the day somebody adds one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y7R8eSqTufRumLLyxLyabo
…il-a8pntf Sparrow: give the skyburst missile a TAIL
The Sparrow's control glyphs were stranded where the old ability row used to be. The cause is structural, not positional: SparrowHUDVariant, DolphinHUDVariant and ScarabHUDVariant each carry an XBOX_Icon_Root and a PS_Icon_Root with NO InputDeviceIconSetSwitcher to drive them - they are standalone prefabs rather than variants of VesselHUDPrefab, which is where the switcher lives. Those glyphs are never lit, never switched for the player's actual device, and never placed, so moving the row left them behind. RetireLegacyChrome only ever reached what sat around an ability icon, so it could not touch them - nor the orphaned boost rings, nor an Ammo Count nothing writes to. The lockup now also retires root-level content that no component on the HUD still REFERENCES. By then everything the lockup owns has moved into the row, so a root-level child that still draws is either actively driven or dead, and which one is a reference question - asked by reflecting over the HUD root's components plus the icon-set switcher, rather than guessed from names or hand -listed per vessel. Three guards. Only children that DRAW, because a subtree with no Graphic is logic and switching it off would stop behaviour rather than hide a picture. Reference sources limited to the HUD ROOT, because a component sitting inside a leftover branch would reference its own children and spare the branch it belongs to. And a reference from `highlights` does not count: that is the legacy press glow the card superseded, and the Rhino's entry points inside its old boost container - a reference from something the lockup retired is not evidence that anything still uses it. Measured from the assets: Sparrow and Scarab lose Boost Button/display, Ammo Count, the emptied ActionIconHolder and both glyph roots; the Dolphin loses a stray Image and both glyph roots; the Rhino loses BoostContainer. The Serpent and Squirrel keep their glyph roots, because their switcher references them. The real fix for those three HUDs is authoring a switcher so their hints work like the Squirrel's rather than being retired; until then they show no control hints, which is honest - static pad glyphs shown to a keyboard player were misinformation before they were also misplaced. Recorded as the open follow-up. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YZnzo7ipUR6TSQ9FjMf7bt
…e HUD root TWO SYMPTOMS, ONE GAP: the lockup owned the row but not the control hints, and not the frame the row is anchored to. CHIPS. The lockup now DRAWS each card's control chip from a fleet-wide ControlGlyphSetSO, so a vessel authors no glyphs at all. Each card derives its own artwork - ability -> its map entry's InputEvents -> InputHintBindingMap.BindingFor (a new inverse of the existing table) -> physical control -> sprite for pad, label for keyboard. Nothing is guessed and nothing is per-vessel, which makes a wrong label structurally impossible. That replaces per-vessel authored glyph roots, which is what stranded the Sparrow: SparrowHUDVariant, DolphinHUDVariant and ScarabHUDVariant each carried an XBOX_Icon_Root and a PS_Icon_Root with no switcher to drive them, so their glyphs were never lit, never device-matched and never placed. Reconstructing hints from that art was not an option - the Sparrow's pad sets do not even correspond between families (Xbox A/B/R1/R2 against PS circle/triangle/L2/ square), so honouring them would have produced labels that were confidently wrong. Pad families therefore share one sprite, which is what the only working hint set in the project (the Squirrel's L1/R1) already did. Blank stays honest: a passive ability has no button, and Button1/2/3 have no keyboard equivalent in InputHintBindingMap, so those chips draw nothing rather than borrowing another device's picture. Device DETECTION stays in InputDeviceIconSetSwitcher, which VesselHUDController now ENSURES the same way it ensures the lockup, and pushes to the view on every set change. THE FRAME. The row anchors to (1,0) of the HUD root with the style's margins, which assumes that root is the screen. Measured off the prefabs, it is not: Rhino/Scarab/Sparrow stretch their root to the full canvas while Dolphin and VesselHUDPrefab - and therefore Squirrel, Serpent and Manta - point-anchor theirs at the CENTRE at 100x100. So (1,0) resolved to the screen's corner on one group and to a point 50px from the canvas centre on the other: the margins meant two different things and the vessels visibly disagreed about where the row sits. NormaliseHudRoot stretches the root and flattens any per-vessel HUD scale, which is safe precisely because the sweep has already moved everything the lockup owns into the row and retired the rest. A container's rect is part of the contract for everything anchored inside it - standardising the contents while leaving the frame per-vessel standardises nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YZnzo7ipUR6TSQ9FjMf7bt
…last competing glyphs The card already DREW its control chip, but InputDeviceIconSetSwitcher was still toggling three authored glyph roots on the VesselHUDPrefab variants (Squirrel, Serpent, Manta) - a second glyph display beside the chip. It survived the lockup's retire sweep only because the sweep consulted the switcher's own references, and ApplySet re-activated the roots on every device change, so switching them off could never have held. The switcher gives up the display entirely (627 -> 138 lines). Deleted: the three icon-set root fields, the per-set HintVisual list, the ability-placement pass, SetHintActive, DriveHintVisuals and BindHintsToAbilities. Kept: Current, IsKeyboard, OnSetChanged - which device is the player holding, and nothing else. With no reference left to spare them, those branches now retire like any other leftover. A reference from something the lockup superseded is not evidence anything still uses it - including a reference held by the superseded component itself. Three bugs the authored display had masked: - OnSetChanged was declared, subscribed and NEVER RAISED, so a chip could not follow a device change at all. ApplySet now raises it. An event nobody raises looks identical to an event nobody needs. - KeyboardSet() fell back to Xbox whenever keyboardTextRoot was null, and no vessel wires one - so IsKeyboard was never true and the LSHIFT/RSHIFT labels could never appear anywhere. Together with the above, this is why pad -> keyboard -> pad could not reach the keyboard. A fallback that protected an authored display keeps firing after the display stops being authored. - padGlyphHeld/heldColor were authored (L1 Active / R1 Active) and read by nothing. The chip takes the held art off the card's existing press path, so the card lights the ability and the chip lights the button - one press. A one-shot PlayPressFlash is explicitly not a hold. VesselAbilityRowAuditor section 5 retargeted: it read the switcher's setVisuals by name, which now resolves to nothing, and would have reported "no control hint labels it" for every pressable ability on every vessel. It now checks what can still leave a card blank - an ability whose control has no pad sprite or keyboard label in Resources/ControlGlyphSet. VesselHUDPrefab.prefab drops the four dead switcher keys. Both stub harnesses compile, including a generated probe that compiles the auditor's new check verbatim (negative-controlled). check_conditional_compilation.py passes 1756 files. Not editor-verified - Docs/UNITY_VERIFICATION_CHECKLIST.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YZnzo7ipUR6TSQ9FjMf7bt
…ty-icon-design-system-whstrh # Conflicts: # CLAUDE.md # Docs/UNITY_VERIFICATION_CHECKLIST.md
…UD asset This doc claimed the retire sweep clears the Rhino's BoostContainer. It does not. The claim was read off RhinoHUDVariant.prefab, where RhinoVesselHUDView wires only `highlights` and BoostContainer therefore looks unreferenced. Rhino.prefab's instance overrides that view with six live readouts (lineIcon, debuffIcon, crystalIcon, debuffTimerText, skimmerSizeIcon, slowedCountText) and adds three root-level branches the HUD asset does not contain at all (LaserTargeting, Crystal, ForceField). Four of the Rhino's six root branches are genuinely driven - verified, the view writes every one of those fields - so the sweep spares them exactly as its third guard promises. The cluster is anchored at (0.93..0.98, 0.04..0.13), the bottom-right corner the lockup row now occupies, which is why it reads as the old UI coming back. A HUD prefab asset does not tell you what a vessel wires: the vessel's prefab instance can add GameObjects the asset has never heard of and populate fields the asset leaves empty. Same family as the Scarab name-override misread - a confident claim about assets derived from the wrong file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YZnzo7ipUR6TSQ9FjMf7bt
Reported as "the Rhino is showing its old UI again". It was not a regression: the Rhino's cluster is genuinely driven - RhinoVesselHUDView writes lineIcon, debuffIcon, crystalIcon, debuffTimerText, skimmerSizeIcon and slowedCountText - and the retire sweep was sparing it exactly as its third guard promises. It reads as the old UI because it is anchored at (0.93..0.98, 0.04..0.13), the bottom-right corner the lockup row now occupies. The reference guard asks "is this driven?" as a proxy for "is this still wanted?". On a vessel that has adopted the row that proxy is right; on one whose four cards all render LOCKED it answers the wrong question, and on the Rhino the honest answer to both is yes. RetireLegacyHudContent now splits on one fact - does this vessel bind any ability icon? Any icon bound (Squirrel, Sparrow, Dolphin, Scarab): guard unchanged. No icon bound (Rhino, Manta, Serpent, all 0/4): every drawing root-level child retires, because there is no designed row for anything at root to belong to. Accepted cost, by design call: the Rhino stops showing its debuff timer, slowed count, skimmer-size ring, laser and crystal indicators until they are re-homed. The view still writes to them, so a branch is switched off rather than unwired, and the rule reverses itself the day that vessel binds its first icon. Harness compiles; check_conditional_compilation.py passes 1771 files. Not editor-verified - Docs/UNITY_VERIFICATION_CHECKLIST.md carries the steps, including the regression to watch for (the four adopted vessels must be unaffected). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YZnzo7ipUR6TSQ9FjMf7bt
Deep-pass findings, all evidence-backed. 1. DolphinHUDVariant was the ONLY prefab carrying an authored AbilityLockupView component; the other seven ensure it at runtime. Nothing referenced it (abilityLockups is unwired, and ResolveAbilityLockups GetComponents it anyway), so it is removed: nobody authors what everybody ensures. EnsureAbilityLockup is idempotent - it resolves an existing component first and Build early-outs on _built - so behaviour is unchanged either way. 2. The docs claimed "pad families share one sprite - the only working hint set in the project already did". That is FALSE, and measuring it is cheap: at the merge base every HUD that authored glyph art authored FAMILY-SPECIFIC art (Sparrow and Scarab each carried PS L2/o/square/triangle against Xbox A/B/R1/R2), and SquirrelHUDVariant authored none of its own. Sharing one sprite is a deliberate regression, not a continuation. Two things make it acceptable and both are narrow: the shoulder pair CANNOT be authored per-family today (the project ships PS/L1 and XBOX/R1 and has no XBOX/L1 or PS/R1 - the asset's mixed sourcing is the only art that exists), and the face buttons are used by one vessel. Stated cost: a PlayStation player sees Xbox A/B letters on the Sparrow. Follow-up recorded. 3. The clear-slate rule's cost is now tabulated per vessel rather than described for the Rhino alone. Manta and Serpent also lose four inherited buttons - Boost Button/display, Exhause Barrage, ShootBullets, ShootMissiles - each carrying a live ResourceDisplay. Those are safe to switch off for a checked reason, not an assumed one: their Button components have zero onClick wiring, and both vessels' ability-map entries are open design slots with Input = 0, so nothing routes a press through them. Had that not held, clearing them would have deleted the on-screen ability control on every touch device. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YZnzo7ipUR6TSQ9FjMf7bt
…three traps Two prefab-reading traps the branch paid for (an asset does not tell you what its instance wires; a multi-document regex spans documents and returns a plausible wrong answer) and one harness trap (an explicit <Compile Include> list silently ignores a file you add, so 'Build succeeded' can mean nothing was checked - negative-control it). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YZnzo7ipUR6TSQ9FjMf7bt
…n-system-whstrh The ability lockup (TOTEM) — one fleet-wide system for ability icons + element indicators
…e boost ring The Squirrel's Time slot (Boost Ring / SquirrelTubeActionSO) was still wearing BloomIcon-PLACEHOLDER. Replace it with the thing the ability actually makes: the ring seen endwise, cut perpendicular to the tube axis. The icon is GENERATED from the authored asset rather than drawn, so it cannot drift from the ability. Tools/Build/author_squirrel_boost_ring_icon.py reads segments/radius/prismScale out of SquirrelTubeAction.asset and reproduces BoostRingBuilder.LayRing's own placement maths; --check fails until the icon is regenerated after a retune (one authored number per displayed quantity). At the shipped 8 / 8 / 4 that is eight 4x4 cube faces on a radius-8 circle. Each prism is posed with local +y along the radial, so the squares carry their own 45 degree step and the ring alternates four square-reading prisms with four diamond-reading ones - not styling, that is what gets built. The generator asserts the prisms do not overlap (gap 0.897 world units; at segments 12 on this radius they interpenetrate, which it refuses outright) and emits pure white with alpha only, since the HUD tints ability icons at runtime. The placeholder sprite is shared with AOEExplosionPlaceHolder.prefab, so it is left untouched - only the Squirrel HUD's Time icon is repointed (one line). The icon RectTransform's legacy 90 degree Z rotation is a no-op for this figure and is deliberately left alone, so nothing in the row moves. Also corrects the adjacent stale "Cooldown HUD" paragraph, which still described the bespoke radial reload that SquirrelVesselHUDView retired in favour of the ability lockup's fleet-wide cooldown veil. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DuQKibovn6ini452mGE26C
…ty icon The new cross-section icon did not render: Squirrel.prefab's nested HUD instance carried a prefab-instance override setting m_Sprite on fileID 8778855275387912087 (the Time ability icon) to GyroIcon-PLACEHOLDER, and an instance override always beats the prefab asset. SquirrelHUDVariant's own sprite had therefore been dead for as long as that override existed. It was the only m_Sprite override on the instance - the other three ability icons resolve from the variant - which reads as a stray edit made on the vessel instance rather than a per-vessel pattern. Delete it rather than repoint it, so the HUD variant is the single source of truth for all four icons. Swept the rest of the fleet: no other vessel prefab overrides an ability icon sprite (Serpent's remaining m_Sprite override is a control glyph). Records the trap in SQUIRREL_TUBE.md, including why it survived a first pass: the modification entry wraps `target:` across two lines, so a one-line regex over m_Modifications reports zero overrides and reads as a clean instance. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DuQKibovn6ini452mGE26C
Review-pass findings from the ship protocol: - author_squirrel_boost_ring_icon.py --check wrote a scratch PNG INSIDE Assets/ before comparing, which churns Unity's asset database and strands a stray file if the run dies before cleanup. Split encoding from writing (build_png/write_png) so --check compares in memory and never touches disk. Output is byte-identical. - Verified the doc's claim that the icon RectTransform's 90 degree Z rotation is a no-op for this figure: 0 of 21,904 pixels differ under the rotation, worst alpha delta 0/255. Exact, not approximate. Knowledge capture: - CLAUDE.md: record on the Squirrel fleet row that the Time icon was authored in the HUD variant AND overridden on the vessel prefab instance, so the variant's value was dead and editing it changed nothing. - asset-surgery skill: extend "a PREFAB ASSET does not tell you what its INSTANCE wires" with the third failure mode (an instance OVERRIDING a field the asset does author - worse, because the asset shows a plausible value that is never used) and the raw-line parse recipe, since m_Modifications entries wrap across lines and a one-line regex reports zero overrides on a 166-override instance. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DuQKibovn6ini452mGE26C
…-prisms-7n2ly3 feat(squirrel): Time ability icon is a true cross-section of the boost ring
Adds the data layer the in-game encyclopedia reads, and the FrogletTools window that authors it from the project's own assets. - CodexSO (Assets/Resources/Codex.asset) — one catalog the runtime UI loads with CodexSO.Load(), no per-scene wiring. An entry is a PAGE: one per crystal element family (5) and one per lifeform species (22), with the four elements / five heart levels folded in as variants. 27 pages over 88 lifeform configs. - CodexHarvester — scans the project and MERGES under a field-ownership contract, so Scan & Merge is safe to run at any time: harvested facts and wiring are re-derived, authored prose/ordering/discovery/pose are never touched, and an entry whose source asset vanished is flagged as an orphan rather than deleted. Species group by PREFAB, not asset name. - CodexImageBaker — renders a hero PNG per entry off the prefab ASSET (nothing is instantiated, so no gameplay component Awakes). Alpha is recovered from two opaque renders rather than trusted from the render target, which is pipeline-dependent; a shader that needs a running frame renders empty here, so coverage is measured and a lit silhouette is used instead of writing a blank PNG. - CodexWindow — add / edit / delete / reorder any entry, detach a harvested fact to keep an edit, re-pose and re-bake an image, and ship the asset output through FrogletToolShipPanel. A crystal's impactor class (elemental / omni / team) is deliberately not surfaced: it decides who may collect one, which the palette already says in-world, and is mechanics rather than encyclopedia content. Discovery is a hook only — every entry ships unlocked; UnlockedByDefault and DiscoveryKey exist so progression needs no schema change later. Verification status: C# verified syntax-only (Roslyn parse, zero CS1xxx; remaining errors are the expected missing Unity assemblies). NOT opened in the Unity Editor — no Editor is available in this session, so /verify-unity could not run. The tool has not been executed and Codex.asset does not exist yet; it is created on first open. Docs: Docs/CODEX.md, indexed in CLAUDE.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0152jjmZN9HXCq1dwQ2SYZLv
A flora prefab carries exactly ONE prism — the seed — because a plant is a growth RULE, not a model, so the first pass photographed a single box for all 16 species. The lava lamp's Lifeform bench already solved this (FloraIconBuilder); the baker now reaches the same answer through the same two calls rather than a second copy of it: - Flora.TryPreviewGrowth runs the species' rule in the abstract, and CellMiniatureBuilder.BuildFromLays turns the poses into one mesh, painted with a lit material in the domain's colour. - PhyllotacticFlora.TryPreviewGrowth added — the 8 Hesperides forms had no preview at all, so they were anonymous spheres on the bench too. It mirrors SeedTips / DecideStep / DecideWhorl with three substitutions and only three: a caller-seeded System.Random (the contract forbids touching UnityEngine.Random), a local claim list instead of PrismSpatialIndex, and a node that becomes a tip immediately because there are no frames. Prism SHAPES are not re-derived — StemPrismScale/LeafPrismScale are the live ones, so a preview cannot drift from the plant on taper, cross-section or jitter. Jitter() gained one shared implementation that swaps its randomness source. - Fauna are harvested normally: unlike flora they ARE authored in place (a shark's wings, belly and danger rods sit at real offsets). Non-body branches (trail/vfx/pip/explosion/particle) are now filtered, matching the bench. - A COLONY's body is its MEMBERS: the worm colony root carries no mesh and no nested instance, which is why it failed with "no visible meshes". The baker now lays a chain of its head/body/tail prefabs at the colony's own authored spacing and taper, found by serialized-property name so a rename drops the icon instead of breaking the build. Also: Validate & Push is removed. Validate stays as a report-only toolbar button; every file the tool writes is still recorded on the tool ledger, so FrogletTools > Build > Pending Tool Changes lists them and they are pushed by hand. The four "Wildlife Cell N Fauna Config Data has no prefab" warnings are correct and need no code change — those assets are empty stubs (FaunaPrefab unset, InitialSpawnCount 0, SpawnProbability 0) in the Wildlife Blitz cell configs. Recorded in Docs/CODEX.md as a known limitation. Verification status: C# verified syntax-only (Roslyn parse, zero CS1xxx; remaining errors are the expected missing Unity assemblies). NOT opened in the Unity Editor — no Editor is available in this session. The flora preview and the colony chain have not been rendered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0152jjmZN9HXCq1dwQ2SYZLv
Three things, all from playtest feedback on the first bake. Flora are painted NEUTRAL, not Jade. Colour means DOMAIN in this project — who owns it — and an encyclopedia page is nobody's. The lava lamp paints its bench icons in the player's domain for the opposite reason: there, you are about to release one. Images fill the frame. Framing was solved from bounds.extents.magnitude — the radius of the sphere the box fits inside, which for anything non-spherical is far larger than the box, so every icon sat small in a wide margin (worst for the long thin subjects this codex is mostly made of: a plant, a worm). It is now solved from the bounds' eight corners, so Padding 1 really does mean edge to edge, and the default drops 1.25 -> 1.05. A per-entry "Reset pose" button picks the new default up on entries authored before it. The window follows the house style. It was plain IMGUI; it now uses FrogletEditorPalette the way the Froglet Master Tool does: - Coloured section bars (tinted ground + accent stripe + accent label + count pill), collapsible, per kingdom and per detail section. - List rows as cards with hover and selection states, an accent stripe, a larger thumbnail and an at-a-glance state pill (ORPHAN / NO IMAGE / LOCKED). - The four primary actions as ColorButtons in their own strip instead of toolbar text, colour-coded by what they do. - A status card with a DONE/ISSUES pill and a dismiss button, and a footer reporting entry counts and how many are illustrated. - Search, kingdom filter, expand/collapse all and Select Asset in the toolbar. - Preview box 128 -> 168px. Verification status: C# verified syntax-only (Roslyn parse, zero CS1xxx) and every FrogletEditorPalette member used was cross-checked against the palette. NOT opened in the Unity Editor — no Editor is available in this session, so the layout has not been seen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0152jjmZN9HXCq1dwQ2SYZLv
…tream
The QA window's publish step takes a different path on a protected branch
(bleeding-edge): rather than checking anything out, it pushes the commit
straight to a qa/results-<session> ref. That push carried -u.
With a REFSPEC, -u sets the upstream of the SOURCE ref -- which is the
tester's current branch, not the ref being created. Reproduced in a sandbox:
git push -u origin HEAD:refs/heads/qa/results-demo
branch 'bleeding-edge' set up to track 'origin/qa/results-demo'
So a tester who pressed Submit on bleeding-edge had their local bleeding-edge
silently re-pointed at a QA results branch, and their next Pull in GitHub
Desktop would have pulled that branch into the build they were testing. The
tool would have reported success while doing it.
Dropping -u fixes it and still publishes the ref (verified in the same
sandbox: branch created, upstream left on origin/bleeding-edge). Nothing local
tracks the QA ref and the tester never checks it out, so it needs no upstream
at all.
This path had never run: all testing so far was on a working branch, which
takes the ordinary `push -u origin <branch>` path where -u is correct. It
became the DEFAULT path the moment this work merged to bleeding-edge, since a
tester now starts there.
Known and unchanged: the commit is still made on the local protected branch
before being pushed to the QA ref, so the tester's local bleeding-edge sits
one commit ahead of origin afterwards. Harmless (the data is on the QA branch,
and it resolves when that merges) but untidy; building the commit with
commit-tree so the local branch never moves is the follow-up.
C# real-compiled (Roslyn + stubs, exit 0); 65 python selftest checks green;
conditional-compilation gate clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsUBppM6FtPcfyiZSM9Eun
…ster owns
Follow-up to the -u fix. Publishing from a protected branch still made a LOCAL
commit before pushing it to the QA ref, which left the tester's bleeding-edge
one commit ahead of origin forever -- with a Push button in GitHub Desktop that
either errors or, if the branch is not protected server-side, lands a QA results
commit straight on bleeding-edge.
No local commit is made at all now. The commit object is assembled with plumbing
and pushed directly:
read-tree HEAD -> add PATHS -> write-tree -> commit-tree -> push
against a THROWAWAY index (GIT_INDEX_FILE), which is also what keeps it scoped --
whatever else the tester has dirty or staged is invisible to it. HEAD, the branch,
the real index and the working tree are all untouched.
refs/qa-published/<stem> records what was last published for this session and
parents the next publish, so a second submit FAST-FORWARDS the QA branch instead
of being rejected as unrelated histories. It also keeps that commit object
reachable against gc, and sits outside refs/heads so it never appears as a branch.
Prototyped against real git before any C# was written, and the prototype checks
the things that actually bite: HEAD unmoved, upstream still origin/bleeding-edge,
branch not ahead, an unrelated dirty file NOT swept into the pushed tree, and a
second submit fast-forwarding cleanly.
FrogletGit gains RunInWithEnv/RunWithEnv (env vars are the only way to reach
GIT_INDEX_FILE -- it has no command-line equivalent). Pure refactor: RunIn now
delegates with a null env, so every existing caller is byte-identical in
behaviour. Both FrogletGit.cs and QASessionWindow.cs were really compiled
(Roslyn, exit 0) -- the shared-infra file against the REAL
System.Diagnostics.Process ref assembly, so the process plumbing is type-checked
rather than stubbed.
65 python selftest checks green; conditional-compilation gate clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsUBppM6FtPcfyiZSM9Eun
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Introduces two new playable vessels (Scarab and Urchin) with full integration into the game ecosystem, UI systems, and level configurations. Includes new environmental assets, shader enhancements, and comprehensive cell/spawn profile updates.
Key Changes
New Vessels
Assets/_Prefabs/Spacevessels/Scarab.prefab): New vessel with HUD container, camera settings, and elemental ability mapGraphics & Shaders
ChargeCrystal.shader,EchoSightHalo.shader,ForcefieldCrackleCapsule.shader,PrismOcclusionDitherPreview.shaderChargeCrystal.hlsl,PrismDestructionSight.hlsl(Dolphin's Echo Sight GPU implementation)ExplodingBlockGraph.shadergraphandBlockGraph.shadergraphwith expanded functionalityEcosystem & Environment
UI & HUD
ScarabHUDVariant.prefabfor Scarab vesselConfiguration & Data
CrystalCaptureConfig,PrivacyConsentConfig,PrismSuperShieldJiggleConfig,ScarabNucleusFieldConfig,DisplayNameValidationConfig,SelfTrailContactConfigEndConditionOverrides,ElementalCrystalSet,PostHogConfig,DefaultNetworkPrefabsModels & Animations
SparrowModel4.fbx,Sparrow Missile.fbxwith metadataSparrowAnimatorController.controllerwith new blend tree statesInfrastructure
bleeding-edge-guard.ymlworkflow; updatedunity-ci.yml,sync-build-branches.yml,tag-internal-build.yml,build-branch-ci.yml.editorconfigfor formatting standardshttps://claude.ai/code/session_01PsUBppM6FtPcfyiZSM9Eun