STARGAZER · DAY 7
StarGazer is a single-player 3D space combat game where you command massive capital warships through procedural roguelike runs — broadsides, voxel hull damage, deterministic simulation, built solo with AI assistants.
Started May 21, 2026 · last activity today

Activity & Beats
Composite of commits, decisions, and posts per day. Click a bar to filter the timeline below.
Tighten 10 skills with mandatory proof artifacts or ask-user fallbacks
Audit found weakness clustered in "look at it and confirm" steps that all relied on the LLM honestly reporting what it sees. The 2026-05-25 Schema Phase 1 silent rollback and the bright-pink default-run regression are load-bearing precedents, both shipped because no machine-readable gate caught the failure. User's directive: "AUTOMATE, AUTOMATE, AUTOMATE." When no automation exists for a step, ASK the user for the proof recipe and encode it, never assume.
Split `Game1.cs` (1689 LOC god-class) into `StarGazerGame` partial-class file map
`Game1.cs` at 1689 LOC was a god-class, `LoadContent` interleaved 4 jobs in 522 lines, `Update` interleaved 5 concerns in ~190 lines, MCP terminal block buried 160 LOC under `#if STARGAZER_MCP`. The AI-access angle was load-bearing: an agent fixing a beam tint could spend the whole file paging `Game1.cs` before realizing the answer lives in `Render/`. Names like `StarGazerGame.Commands.cs` and `StarGazerGame.Draw.cs` self-reject that mis-step on grep alone.
Inside-Out Spaceships, Caught by a Side-by-Side Sweep
A rigorous parity sweep between the in-game render and the reference shipyard preview cracked open a see-through hull bug, and the real fix lived all the way back in the exporter.
Skills migrated from `.agents/skills/` to `.claude/skills/`; lint script enforces the path
Claude Code's skill loader auto-discovers from `.claude/skills/` (project) and `~/.claude/skills/` (user) only. Skills under `.agents/skills/` worked when an agent read AGENTS.md and followed links manually, but they did NOT appear in the auto-discovered tool list for fresh sessions, defeating the entire point of having them as Skills. Verified in this session by inspecting the system-prompt skill list and confirming zero StarGazer skills present. The `.agents/` naming was project-creation intuition that pre-dated awareness of Claude Code's discovery path. Once observed, the fix is mechanical (move) but the value is permanent (every future session auto-loads `stargazer-mcp-usage`, `stargazer-visual-eval`, the deploy skills, etc. as named invocable Skills).
An Operating Console for the Running Game: Six Weeks of MCP
StarGazer now hosts a Model Context Protocol server inside the game process. Twenty tools across read, mutate, capture, commit. Every screenshot in this post was captured by the MCP driving itself.
Diverged-main sync policy: rebase the trivial cases, surface the messy ones
The push-rejection path in commit-push was silent, no analysis, no proposed move, just "ask the user", putting triage cost on the user every time, even when the answer is obvious. At the same time, autonomously resolving a real conflict or rebasing across a merge commit is the exact failure mode that destroys work. Split: agent does the work where the answer is mechanical, user decides anything involving history-shape judgment. Force-push on `main` stays banned regardless, the release-branch exception in deploy skills does NOT extend here.
Add stargazer-verify-determinism skill + .gitattributes LF-pin for hash-bound fixtures + defer stargazer-schema-contract
Determinism is load-bearing per CHARTER §2.1, and [`game.yml:44-48`](../.github/workflows/game.yml) skips `--voxel-test-suite` on Windows ("headless path TBD"). Solo-dev on Windows + pre-push hook capped at 30s (CHARTER §5.2 forbids expanding) = zero local determinism gate with CI catching regressions N-commits-deep; the skill closes that gap, running ~3.5s locally. The fixture-hash crash on Windows traced to git autocrlf eating LF bytes the sidecar hash was computed against, hence (1). Schema-contract deferral waits for referents to exist; writing it now would calcify placeholder layouts that PLAN phases 4–5 will likely reshape.
Ship pipeline phases L→F→G: shipyard cooks voxels, game stops cooking at load
Two long-standing problems converged, voxel-cook drifted across OS (Mac `0x516953CC401E7EB7` vs Windows `2D1009DC12BA4008` from the same source made `.last-hash` only meaningful on one platform) AND every C# build paid ~250 ms `CookGlb` at startup + every R-key reset despite the GLB never changing. Both root-cause to the cook living inside the runtime where FP depends on JIT+libm+CPU; moving it into the digest-pinned container makes byte output a property of the container, not the host. Three-file packs load 250 ms faster AND eliminate cross-platform drift; the 10-check validator is defense-in-depth (structural 1-6 fail-fast, self_hash 7 catches in-transit corruption, cross-refs 8-9 catch torn writes, check 10 closes splice attacks), each with a distinct error message for log forensics.
Ship pipeline Phase I followup: multi-stage shipyard.Dockerfile + digest-pinned cook-base + cross-platform byte-determinism verified
The pin + multi-stage is the load-bearing piece of "Python becomes canonical", digest pin blocks Docker Hub retags swapping in new libc6, `apt-mark hold` blocks `apt-get upgrade` in stage 2 from swapping libc6 out from under stage 1, `--require-hashes` blocks transitive numpy/pygltflib upgrades silently changing cook output. Two-stage design means production cook and CI canonical cook literally share the same Python interpreter and libm, they cannot drift, because `shipyard-service` IS `cook-base` + Node + web frontend. The cross-platform verification step is the receipt the whole plan was after: pure-Python `float` is deterministic on paper, but the proof is Mac+Linux Docker producing identical `stable_hash` byte-for-byte. Subtle gotcha during verification: an earlier 83-voxel Mac-vs-Linux delta traced to CLI-vs-pipeline voxel_size truncation, not FP drift, once both runs got the full-precision input, both produced identical output.
Close out 3 deferred ship-pipeline items: schema.yml canonical-cook CI gate + oracle threshold calibration + `.last-hash` Mac bump
Without the CI gate, contributors can edit `voxel_cooker.py` or bump a transitive numpy in `requirements.lock` and silently change canonical cook output, the `HASH_BUMPS.md` entry is the grep-able artifact proving drift was intentional. The threshold lowering reflects that runtime LoadFromBin uses the Python-cooked core stored in the binary header, so the C# cooker's independent core-pick can't affect runtime; the strict gate failed on a no-production-consequence delta. The `.last-hash` bump closes the Phase L row deferred at original ship time ("Mac sees different hash than Windows"), plan §L.1 required it in the same commit as the schema lockdowns. Windows-side bump will be a same-commit baseline update by whoever next runs on Windows.
Skill overhaul: fix daily-blog, add data/asset/combat verification skills, add `--dump-fleet`/`--dump-pack`, extend determinism gate to physics
A full skill audit (4 parallel reviewer agents on the existing 9-skill set) found that 5 of 13 verification surfaces in the codebase were dark, the C# game emits rich data lines (PERF_TICK, FLEET_LOAD, VOXEL_LOAD_FALLBACK_COOK, BENCH_PHYSICS hashes, asset validator output) that no skill was consuming. The "screenshot proves the pixels but not the code path" past-sin (default `dotnet run` falling back to runtime cook, rendering bright pink while static screenshots looked fine) was specifically what the log-scan addition guards against; auto-firing data-eval on every voxel/sim/pack edit closes the loop. The four-persona daily-blog swarm was load-bearing design intent (4 independent voices stay distinct in a way 1 voice merging them cannot) but the implementation never worked, phantom subagent APIs. Physics determinism wasn't gated despite BEPU being half the sim, so a floating-point regression there could sail through CI. YAGNI reviewer pushed back hard on `verify-combat` (5 scenario sweeps per beam tweak is too expensive); user resolved by gating it behind explicit invocation, keeping the auto-trigger budget cheap. `--dump-fleet`/`--dump-pack` exist as headless read-only CLI flags because the alternative (parsing `.run/stargazer.log` after a full launch) costs ~10s per commit suggestion vs <1s for the dump.
Unified capital-ship reactor core (Singularity Forge)
The prior quantum_sphere focal was undersized (~0.6m on a 72m ship) and the four-style split meant no ship felt iconic, plus the connectivity-slide pass buried it inside the hull. Replacement size lands 1.05m on an 18m gunship and 4m on a 72m dreadnought; the spine yoke gives the shell guaranteed OBB-overlap with rib frames so connectivity is satisfied without sliding. The anchor leaves ~78% of the surface exposed above hull silhouette, supporting "the core is your exposed kill target, defend it." Validator passes 8/8 randomized ships at 82–100% exposure (was failing-by-burial on most).
Singularity Forge v2: midline-centred, 20% cap, hex+multi-band
v1 floated above the hull instead of sitting in the gap, had no diameter ceiling (8m orbs on 72m dreadnoughts), and read as a saturated white emissive ball with no surface structure. v2 inverts the contrast strategy, bright shell, dark cladding, so the hex pattern and bands read as silhouette structure at every zoom. Multi-agent parallel review across 30 renders (6 ship families × 5 angles) confirms v2-iter3 reads as "a contained hex-panelled reactor cell on the centreline" on every family.
Singularity Forge: The Reactor Core Your Capital Ship Is Built Around
Every capital ship now carries a hex-paneled, multi-banded containment cell at its centreline, a deliberately exposed kill-target the rest of the hull defends.
Phase 5b kinematic chain: 3-phase render-side aim path
Each phase is its own commit with a clear regression surface. Phase 1 alone gives rest-pose-equivalent visuals, Phase 2 alone shows kinematic parts at rest, Phase 3 alone rotates turrets; bundling would make blame impossible if math or API shape proves wrong. Render-side aim placement is correct because aim VISUAL is a render concern while weapon FIRE today is sim-owned via `WeaponBeamActive`/`FindEnemyTargetForShip`; computing render-only angles from the same sim target preserves the determinism tenet (no writes back to sim). When weapon fire is rewired to consume aim angles, the state migrates sim-side and `KinematicAimSystem` moves to `Sim/`.
StarUI Zero-Allocation Responsive UI Framework
Zero-allocation avoids GC sweeps in critical update/draw frames, ensuring locked 60+ FPS on Steam Deck. Single-pass batch rendering coalesces draw sessions, minimizing CPU/GPU driver command overhead.
Wound clusterer: O(n²) all-pairs → O(n) cell-indexed BFS
Prior implementation rescanned all unvisited records on every BFS dequeue, for a beam producing ~125 destroyed voxels on one face axis, that's ~15.6k comparisons per face per chunk, summed over 6 faces × dirty chunks × ships dominating the per-frame damage path during sustained combat. New cost is O(n) for the index build + O(k · 9) per cluster of size k, no allocation per occupied cell (linked list lives in a single `int[]` keyed by record index). The 9-cell neighborhood preserves the original Chebyshev-radius-1 semantics (prior comment claimed "4-neighbor" but the `|du|≤1 ∧ |dv|≤1` test was always Chebyshev, corrected in comment too).
DamageVisualHistory skips clean chunks instead of full 21k-voxel walk
Prior cheap chunk-hash early-out skipped the whole pass when nothing changed, but as soon as ANY chunk version incremented (every frame in combat) the inner loop visited all `asset.VoxelCount` (~21k) voxels and did 6 neighbor-lookup + IsAlive + key-hash-Add per live voxel, ~750k face-checks/frame at 60Hz across 6 combatants, dominant render-tick cost. With per-chunk skipping, a single-beam-impact frame typically touches only the 1–2 chunks the beam hit (~5% of voxels), so the inner work drops ~20×. Sentinel `-1` initialization preserves the "first call always walks everything" contract from the prior `lastStateChunkVersionsHash = -1` start.
KinematicAimSystem.BuildDeltaLookup reuses caller-owned Dictionary
Old signature allocated a fresh `Dictionary<string, (Vector3, float)>` once per ship per draw frame (~120 dicts/sec at the default 2-ship scene, growing linearly with ship count), directly violating the [StarUI zero-alloc render-loop discipline](#2026-05-24--starui-zero-allocation-responsive-ui-framework) that explicitly cited per-frame Dictionary allocation as the bottleneck dropping framerate to ~23fps in the prior HUD alternative. Reusing one Dictionary across all ships and frames removes the alloc entirely; `.Clear()` keeps capacity, so steady-state cost is one rehash-free walk per call.
Gameplay loop phase 2: 9-slot hotbar UI + scenario keys move to F1/F2/F3
Phase 2 of the gameplay-loop goal (see [`DOC/GAMEPLAY-LOOP-DESIGN.md`](../DOC/GAMEPLAY-LOOP-DESIGN.md)), the hotbar is the player-facing surface for the ability system. Landing the UI + key reshuffle before the sim-side routing keeps each commit reviewable in isolation: phase 2 ships "visible, no behavior", phase 3 ships "behavior wired through the existing UI". Self-contained `HotbarPanel` (no StarUI child elements) instead of mirroring `HudButtonPanel`'s UIElement-per-button pattern because the 9 slots sit at fixed offsets, the layout engine would burn allocations recomputing them every frame for no win. Voxel test suite PASS (hash `0x2A7028BB38A011F0`).
Enforce voxel core placement zone and exposure constraints
Complies with core gameplay requirements that each hull contains a visible and easily targeted core. Ensured 100% validation pass rates on procedural designs.
StarUI Responsive HUD Button Panel and Command Routing
Decoupled, addressable UI trees make all UI functionality reachable visually AND programmatically. This meets the architectural tenet that external AI agents can query UI state via JSON dumps and execute path-addressable button triggers (e.g., `/hud/buttonpanel/reset-battle`), keeping test automation fast and direct.
Majestic Dawn Logo Color Swap
User requested a swap to gold background with navy lines and details for a high-contrast, vibrant appearance.
Update Native Game Icons with Refined Logo
User requested the refined Majestic Dawn logo in the game shell too, updating raw SVG and compiled binaries ensures the executable shows gold-background/navy-lines branding on Windows and macOS windows, dock, and file explorer.
Require core visibility and longitudinal zoning on ship hulls
Guarantees every ship has a core that is visible, accessible, and shootable, directly supporting space-combat tactical gameplay where targeting cores is a primary mechanic.
Monorepo: four-stack + standard library, one repo
Solo dev, polyrepo discipline costs without buying anything. Shared schema/ subdir + CI cross-validation prevents the silent contract drift that motivated the integration plan in the first place.
Turrets in Motion: Kinematic Aiming and Zero-Allocation StarUI
Bringing capital ship turrets to life with render-side kinematics and introducing StarUI, a zero-allocation HUD.
Share interactive combat launch defaults
The normal playable launch drifted away from the laser-duel proof, and loiter steering moved the enemy out of arc before the player could reliably see engagement.
Default Program camera to Orbit
The app runner and plain `dotnet run` share `Program`; putting the default there keeps the interactive launch, screenshot proof, and future agent runs from drifting.
Default Program voxel view to mesh damage
Combat was applying voxel damage, but default rendering stayed on the static GLB path; once visible, the older larger impact cluster deleted too much hull too quickly.
Keep default render systems enabled
The default live run is the showcase surface, so compiled visual systems should not be silently left null unless they are broken or explicitly disabled.
Tune beam damage as direct hits plus splash
The mesh-damage renderer exposed that repeated 1000-damage clusters detached nearly the whole GLB from the core after a few hits, making ships disappear.
Run live apphost as one-shot LaunchAgent
`launchctl submit` keeps failed programs alive, while plain background/nohup launches can be reaped by the Codex shell; a non-keepalive LaunchAgent survives launch but does not respawn after close.
Keep broadside orbit steering in AiPilotSystem
Orbit combat is simulation behavior, not render/gameplay glue; keeping it in the AI system makes it findable and keeps coordinate math consuming ship-pose basis vectors.
Default voxel debug to translucent overlay
The default visual proof needs damageable GLB triangles, voxel debug surfaces, and beam width to agree without making voxel-only debug captures transparent.
Plan damage rendering as derived render meshes
The simulation must stay deterministic and voxel-authoritative while the renderer becomes visually rich; the reviewed implementation checklist is [`DOC/SHIP-DAMAGE-RENDERING-PLAN.md`](SHIP-DAMAGE-RENDERING-PLAN.md).
Add retained wound history to damage plan
Live voxel state alone cannot reconstruct dramatic wounds after destroyed voxels detach or disappear; render history preserves visuals without becoming gameplay authority.
Build first retained damage renderer slice
The first damage-render slice needs to look like layered ship wreckage while keeping voxel state as gameplay truth and leaving triangle deletion as `debug-cull`.
Aggregate damage wound across chunks, cull interior-poking GLB
The first slice had per-voxel hot-edge dashes that read as a Minecraft grid, and the ship's own GLB tail-fin triangles (owner voxel alive but vertices in destroyed space) painted a pink "lightning bolt" through the wound regardless of how dark the cap was. Aggregating per-axis gives one authored-looking breach; vertex-level culling stops the GLB bleed-through; the cap stays per-voxel/dirty-region so simple beam hits still rebuild cheaply.
Remove energy shields from the universe
A shield bubble in front of the hull hides exactly the damage geometry the `Render/Damage/*` slice exists to show; the combat fiction is grounded in physical damage, not energy fields.
Gate full structural wound on cluster size; tiny hits scorch only
A 1-voxel beam hit generates a wound record on all 5–6 live face-neighbors of the destroyed voxel, so the unconstrained generator was painting 5–6 voxel-sized dark cavity backdrops around every tiny hit, they read as floating black "planes" instead of damage. Front-breach demo still looks the same because big wounds always have many records per cluster.
Default launch is an AI-vs-AI RTS battle with damage on the selected ship
User wants the default run to be the showcase. AI vs AI from above so battles unfold and damage is visible. Per-ship damage rendering for every combatant scaled badly on FNA/Metal (chunk rebuilds + buffer uploads × ships); selected-ship-only matches the original Render/Damage LOD plan and keeps the frame rate playable.
In-game HUD buttons for reset + runtime scenario switching
User wants to try different scenarios and visual states without restarting with new CLI flags every time. The architectural tenet keeps keyboard shortcuts as the primary entry (so headless scenarios and screenshot automation still work); buttons are a mouse-friendly mirror.
Voxel state is the truth for visual feedback, not the physics OBB
The user noticed beam glow floating multiple voxels past the actual hull, root cause was the visual endpoint reading the OBB sweep while the damage path correctly used the voxel raycast. Lining both up on voxel hit eliminates the gap and codifies the rule for every future "thing that visually touches a ship", missile splatters, decal positions, particle spawn origins.
Ship contact normal + point routed through voxel raycast
Audit of the physics layer found `PhysicsWorld.TryGetPairContactData` was setting contact normal to `(positionB - positionA).Normalize()` and contact position to the body midpoint, both broadphase placeholders that pretended every collision was sphere-to-sphere. Physics broadphase cannot know about voxel state; the simulation layer is the right place to refine. Same pattern as the beam-endpoint fix: physics for broadphase only, voxels for "what actually touched."
Coalesce shell triangle draws into one per-ship buffer
With the half-voxel-size default (~0.067u edge, 120k voxels/ship), the prior per-chunk `DynamicVertexBuffer` layout issued ~1,900 `SetVertexBuffer + DrawPrimitives` calls per ship per frame. FNA per-call CPU overhead summed to ~30ms/frame at 7 ships, dropping live frame rate from 60 to ~23 fps. Coalescing keeps the dirty-region rebuild benefit (only the affected slice re-uploads on damage) but stops paying for ~13,300 redundant draw calls every frame. Confirmed restore: live capture at tick 60 with 7 ships shows 60.9 fps, matching the pre-fine-voxel baseline.
Defer voxel topology BFS to end-of-batch
A beam at `BeamImpactRadius=2` queues ~125 destroys per hit. Under per-voxel BFS those flowed into `ShipVoxelStateStore.Apply` as a single ship-run, then ran ~125 full-ship BFS over the ~30k-voxel hull = ~3.75M neighbor visits per beam = ~107ms UPDATE_TICK spike per beam hit. FNA fixed-timestep catchup swallowed every Update, starving Draw to 1–2 fps during combat. Batch-end BFS cuts the per-beam cost to one ~30ms pass; live measurement holds 60 fps with 24% of a ship destroyed across 6 combatants.
Honor glTF PBR materials on ship draw
External authors (ninja-blender) need per-part color, metallic distribution, and emissive engine glows to survive the round-trip into the in-game render. Prior `GlbLoader` discarded materials entirely ("first mesh, first primitive", `Vector2.Zero` UV, no material read), and `RenderPipeline` overrode `DiffuseColor` with a hardcoded faction tint (cool steel for player, warm copper for AI). Per-primitive `GlbMaterialParams` + a per-part bind on BasicEffect is the minimum work delivering "the same ship reads as the same ship" without a custom shader pipeline. Approach picked after surfacing the toolchain tradeoff with the user as a v1/v2 split in DESIGN-LOG.md.
Read KHR_materials_emissive_strength from raw GLB JSON chunk
The user's original brief identified the strength multiplier as `material.extras["strength"]`, but the correction is that it's the standard glTF 2.0 extension `KHR_materials_emissive_strength`. Reading from the documented standard location is the right contract: any PBR-aware authoring tool (Blender, Three.js, Unity, Unreal) writes strength there. The earlier extras path would have silently returned 1.0 for every ninja-blender ship, ignoring authored emissive intensity.
Revert emissive strength to `material.extras["strength"]`
The standards-correct lookup is `extensions.KHR_materials_emissive_strength.emissiveStrength`, but ninja-blender dual-writes the same value into BOTH the standard extension AND `material.extras["strength"]`, exactly so projects on libraries that haven't exposed the extension publicly can read it without re-exporting. SharpGLTF 1.0.6 marks `MaterialEmissiveStrength` as `internal`; the prior raw-JSON workaround was correct but unnecessary given the dual-write contract. Reading extras directly is fewer lines and switches to the standard accessor with a one-line change when SharpGLTF exposes the extension.
DamageableGlbMeshAsset multi-primitive, retains node identity
Prior `DamageableGlbMeshAsset.Load` was locked to `LogicalMeshes[0].Primitives[0]`. Every incoming ninja-blender ship is multi-node + multi-primitive (one node per `module.*` part, one primitive per material), so under the old loader a new ship would render as a single sliver, whichever primitive happened to land first. The cold-launch showcase has to keep working when the new ship packs replace `Content/models/spaceship.glb`, and this is the load-bearing path for that.
GlbVoxelTriangle carries NodeName
Per-voxel component-kind assignment is currently a geometry heuristic; the sidecar promotes it to authored intent. The mapping table needs string-prefix matching on the originating node name for each triangle that voted for a voxel cell, so the data must be on the triangle when it reaches the builder. Adding the field now is one structural change; adding it later requires rewriting every triangle-construction site after the sidecar consumer is in flight.
Automated frame sequence capture and GIF compiler pipeline
Enables the AI agent swarm to automatically record and compile short, high-fidelity gameplay clips (voxel detaches, combat maneuvers, explosion patterns) for dev diaries without manual screen-recording.
Default launch is single-ship Dreadnought-style + hull binds GLB material
Three corrections layered into one commit: (1) StarGazer is Dreadnought-style single-player-ship combat, not RTS, the prior 6-AI overhead default mis-framed the game and pushed the player ship off-frame; (2) hardcoded faction tint destroyed the PBR work by overriding `DiffuseColor` to a saturated cyan/copper that the warm key light multiplied into a bright pink hull, masking every authored material; (3) PBR binding was wired only into the static draw path, not the damage-cook path the default scenario actually uses, so even after removing faction tint, GLB color wouldn't have read. Fixing all three together is what makes "the new stuff is visible when you `run`" actually true.
ShipPackSidecarReader implements the ninja-blender ship-pack v1.0 contract
Pre-positions the consumer side for multi-node ship packs the ninja-blender exporter now produces. The schema doc is the single source of truth across both repos, implementing it line-for-line (including the placeholder-substitution hash trick) keeps both sides in lockstep without re-deriving anything. Hash-chain hard-fails are the contract that catches partial writes, hand-edits, and pack files from different SceneIR generations before they manifest as voxel-mapping or material-rendering bugs further down the pipeline.
Migrate default ship path to Content/ships/spaceship/
Last step of the cross-repo migration the user laid out in the ninja-blender pipeline-update brief. The legacy `Content/models/spaceship.glb` was a single-material collapse of the 897-object SceneIR, keeping it as default would mean the PBR + multi-primitive + sidecar pipeline ships in the codebase but never actually fires in the default scene (the gate-features-behind-flags anti-pattern from `feedback_default_run_is_the_showcase`). Falling back to the legacy path means dev checkouts that haven't synced the new asset still launch cleanly.
Start the default run in combat
The current playable slice is proving voxel/GLB combat, so the first window should show enemy engagement without requiring hidden scenario flags or perfect timing.
Make run launch orbit combat
The user "run" loop should land directly in the playable test surface, not require remembering hidden flags or pressing Tab before camera input works.
Show voxel damage through the GLB by default
Voxels are intentionally invisible gameplay truth, but the normal playtest view must show their effects or combat looks like lasers hitting an indestructible model.
Treat default run as the showcase
The user run loop is the product review surface; hiding effects behind flags makes features look broken or absent.
Make lasers scar before they sever
Capital ship damage should read as progressive hull punishment; disappearing after a few hits feels like a render bug even when voxel topology is technically obeying core ownership.
Make enemy broadsides orbit the player
Moving the firing origin spreads laser hits across the voxel/GLB damage surface and makes the encounter feel like capital ships maneuvering around each other.
Show voxel truth over the GLB in default combat
Playtesting the voxel damage cornerstone needs the authored ship and the underlying damage grid visible together; oversized lasers made hits look wider than the damage they applied.
Replace triangle deletion with authored wound rendering
Capital-ship damage needs to expose structure and battle scars, not look like missing GLB triangles; full reviewed plan is [`DOC/SHIP-DAMAGE-RENDERING-PLAN.md`](SHIP-DAMAGE-RENDERING-PLAN.md).
Treat breaches as capital-ship interiors
A capital ship breach should read as a huge layered vessel being torn open, not a GLB shell with missing faces or flat caps.
Use layered wreckage as the damage bar
The damage cornerstone needs to feel like a huge ship being gutted, not a debug mesh disappearing; working art bible is [`DOC/DAMAGE-VFX-ART-BIBLE.md`](DAMAGE-VFX-ART-BIBLE.md).
Honor glTF PBR materials by mapping onto BasicEffect (v1, no custom shader)
External authors (ninja-blender) need to author per-part colors, metallic distribution, roughness, and emissive engine glows and have the in-game ship match the source preview color identity. The renderer was throwing every material away and re-tinting per faction (cool steel player / warm copper AI), so two visually-distinct authored ships read the same in-game. The "color identity matches" + "engines glow" goals only require per-part color, per-part specular character, and per-part emissive, none of which need a custom shader.
Tonemap target: ACES filmic at exposure 1.0
Ninja-blender web preview uses `THREE.ACESFilmicToneMapping` with exposure 1.0; matching tonemap end-to-end means per-part baseColor and emissive values authored in Blender will read the **same shade** in StarGazer as in the source preview. Without it, a red-orange engine glow can read pink-white because the shoulder curve differs. Tonemap choice has more visible impact on "does this ship look like its preview" than BRDF choice does, picking the matching one closes the largest source-of-truth gap.
Tonemap target: AGX (supersedes ACES choice)
AGX preserves bright chromatic edges around stars and thrusters better than ACES, important for a space-combat game where every visible HDR source (engines, beams, suns) sits over a dark background and the tonemap shoulder decides whether they "bloom and bleed" or "clip to a flat hot ring." This was the original analysis in the render architecture entry; the brief deviation toward ACES was driven by ninja-blender preview using ACES, but matching the planned architecture beats matching one external preview.
v1 IBL stand-in: asymptotic raise-the-blacks emissive lift
Bare PBR-via-BasicEffect renders the literal product of `baseColor * directLighting`, and for a baseColor authored expecting environment-map bounce (every modern ship tool, including ninja-blender's Three.js studio HDRI), the missing bounce leaves dark hulls reading pure black. The first `dotnet run` reaction was "why is the ship so dark?", the math confirmed it is authored that dark on purpose, expecting IBL to lift it. The `(1 - baseColor) * k` curve is the "raise the blacks" component of any HDR/tonemap workflow, max lift to surfaces with minimum direct-light response, zero lift to already-bright ones. This matches the qualitative behavior of an IBL irradiance map without requiring the cubemap or shader pipeline.
Trust contrast: tiny IBL floor + brighter key lights (supersedes the k=0.25 lift)
The k=0.25 lift was the wrong knob, for ninja-blender baseColor (linear ~0.014), a 0.25 emissive add is a ~17× boost that paints near-grey over near-black and erases the "dark hull" identity. The right principle per user feedback is "accept dark materials render dark in shadow, raise key-light intensity, trust contrast." With the bright key, lit faces read ~30-35% sRGB (dark but structured), shadow faces ~5% (truly dark, matches SceneIR), and metallic spec hot spots punch ~60% because they reflect the bright key/fill not the dark base.
Configure interactive combat launch defaults and broadside AI behavior
Grouping combat parameters under a central defaults registry guarantees consistent, predictable scenario initialization; the broadside-hold pilot keeps weapon alignment for high-fidelity combat feedback.
Guts and Glow: Voxel Damage & Broadside AI
We watch exposed decks, ribs, and dark interior structural wounds take shape, tune laser carving, and face orbiting broadside enemies.
Retained Damage Rendering & Zero-Alloc Billboards
Under the Hood: Technical deconstruction of procedural wound mesh generation and optimization of FNA billboard structures.
Canonicalize agent skills and split solo-main from worktree-branch commits
Codex agents need one visible skill home, and parallel work needs branch isolation without weakening the small-commit solo workflow.
Add world grid and scripted thrust for movement captures
The camera follows the ship, so motion needs a fixed reference frame and a non-manual way to drive movement during `--screenshot` runs.
Camera is lightweight render-side state with CLI modes
Agents need a fast, scriptable way to frame screenshots without polluting deterministic simulation or adopting a heavy camera/scene library.
Orbit camera remains render-side and scriptable
Orbiting is a viewing tool, not simulation state; keeping it render-side preserves determinism while letting agents and humans inspect ships from exact angles.
Camera default far clip keeps 10x space headroom
Space scenes need more draw-distance headroom than the first ship-framing camera; making the default deeper avoids clipping future world/grid/ship content during orbit inspection.
Engine systems use findable ownership boundaries
AI agents need obvious search targets and small ownership boundaries to extend camera, input, rendering, and sim systems without scattering behavior through the game loop.
Ship visual forward is world +Z
The model nose faces `+Z` and its visual right side faces `-X`, while the first mappings used XNA-style `-Z` and then world `+X`, making controls feel backward.
Ship motion starts as assisted Newtonian velocity
This gives space-like momentum without jumping straight to orbital mechanics or full rigid-body simulation.
Make thrust follow ship yaw
The early physics prototype needs momentum to respect where the bow points without jumping to a full six-degree rigid body simulation.
Keep vertical thrust on Shift
`Space` reads as a deliberate stop/brake command in this prototype, and `Shift` is easier to hold while combining vertical thrust with WASD.
Add target dummy instrumentation before weapons
Target/range/bearing feedback lets movement feel and approach geometry be tested before committing to weapon rules.
Kill stale game instances before launching
Hung FNA/dotnet windows accumulate invisibly and make testing confusing; one fresh process keeps verification and human playtests honest.
Roll joins ship-local orientation
Roll needs to affect thrust/debug vectors and broadside posture, not just visually tilt the model.
Ship physics has a named sim system
Physics will get tuned often, so agents need one obvious ownership file instead of editing the general sim loop.
Roll basis matches rendered ship roll
A render/sim sign mismatch made broadside arcs roll opposite the model, which breaks visual trust while testing weapons.
Coordinate conventions get a project skill
Recent roll and firing-arc fixes showed that ad hoc inverse/sign patches drift fast unless future agents identify coordinate spaces and ownership first.
Keep AGENTS as the instruction source
Multiple agents will touch StarGazer, and mirrored instruction files would drift as skills and workflow rules change.
Plan BEPU-backed body physics
Large numbers of colliding space objects need real broadphase/collision while gameplay systems still need clear ownership, coordinate boundaries, and agent-friendly extension points.
Extend physics plan for modules and constraints
Capital ships need child hit zones, detachable pieces, and possible articulated/docking/tether behavior without leaking BEPU handles or weakening PC/Mac/Linux portability.
Headless sim scenarios start physics migration
The BEPU/ECS migration needs a fast, windowless baseline for thrust, yaw, roll, brake, and fire behavior so architecture work does not silently change ship feel.
Benchmark body iteration before BEPU
The physics migration needs a cheap baseline for body iteration, event buffers, churn, and snapshot publication before final ECS handles or BEPU integration lock in structure.
Ship pose snapshots become the read model
Render, debug, and scenario code need to migrate to body snapshots incrementally so BEPU/ECS work can land without rewriting every consumer at once.
Validate physics assets before runtime bodies
Collider, mass, and response rules need a diffable contract before gameplay systems depend on them.
Keep BEPU behind StarGazer physics handles
The first runtime spike must prove body creation and missile sweep hits without letting backend types leak into the rest of the game.
Start collision slice headless
Ordered body events and debug records need to be stable before rendering them, or visual overlay work will paper over event ordering bugs.
Render physics debug from collision slice data
Visual physics debugging should consume the tested snapshot/event path, not a second renderer-only interpretation of physics state.
Prove compound detach before articulated joints
Detach and hit-zone semantics need stable headless outputs before BEPU constraints make failures harder to inspect.
Set first physics performance gates
The physics migration needs measurable platform gates before more BEPU, compound, and debug-overlay complexity lands.
Harden physics runtime API
Future agents need reusable physics primitives and lifecycle behavior before adding many object types, not spike-only helpers capped at 64 bodies.
Add a BEPU runtime benchmark gate
Scaling decisions need runtime collision-world numbers before adding pooling, multithreaded stepping, or more body systems.
Support static physics bodies behind BodyRef
Non-ship physics objects include immobile obstacles, stations, beacons, and environmental colliders; exposing BEPU static handles would split the agent-facing API.
Name physics runtime velocity units
Seconds-based BEPU integration and tick-based ship tuning coexist during migration, so the unit boundary must be visible in API names.
Keep BEPU shape ownership inside PhysicsWorld
Spawn/despawn churn should release backend shape bookkeeping while preserving the StarGazer `BodyRef` API boundary.
Serialize dotnet commands per worktree
MSBuild apphost and intermediate DLL writes race inside one `obj/` tree, producing false failures that waste agent time.
Route runtime queries through collision channels
Runtime collision behavior needs to use the same channel contract as authored physics assets, otherwise agents would reimplement filters per feature.
Make runtime query hits self-describing
Gameplay needs enough metadata to resolve hits without doing backend lookups or guessing which response path produced the hit.
Make playable physics consume PhysicsWorld
The BEPU migration needed to stop living only in CLI proofs so gameplay, debug overlays, weapons, and benchmarks exercise the same runtime body/event/query path agents will extend.
Wake controlled physics bodies on motion
BEPU can put idle bodies to sleep; without an explicit wake, ship rotation stayed responsive while translation appeared dead after waiting at launch.
Separate chase and orbit camera ownership
Tying the view target to a rotated model bounds center made real translation look like rotation-only movement, and shared arrow keys made camera/ship ownership ambiguous.
Harden live physics review gaps
The first gameplay migration exposed agent-review gaps that would scale poorly: next-tick query stamps, O(n^2) contact diffs, invalid values poisoning BEPU, and debug overlays that could silently truncate or allocate every frame.
Use raw runtime icon assets
FNA/SDL discovers the window icon by title-named PNG beside the executable, while raw icon derivatives keep the asset pipeline simple and cross-platform.
Deterministic AI pilot emits movement intent only
The first AI ship needs to exercise the same scalable sim path as the player while keeping deterministic replay and no-combat separation clean.
Promote ships into a dense control store
Scaling from one player body to many ships needs one agent-findable path that preserves BEPU collision response and avoids singleton fields or per-feature control code.
Add real-ship scale and determinism gates
Fleet gameplay needs evidence from the actual ship/control/AI/physics path before scaling visual debug or combat behavior. Multi-instance threading is the first safe threading gate because same-world parallel mutation belongs behind a deliberate `PhysicsWorld` worker-dispatch design.
Measure live frame metrics in the render HUD
Fixed sim ticks prove deterministic step cadence, but live playtesting needs a quick render-loop signal while AI/physics scale changes land.
Draw selected ship vectors from sim snapshots
Debug vectors must use the same sim-owned basis conventions as gameplay, not render-side matrix reconstruction. Keeping selection in the game layer avoids adding UI-only state to deterministic simulation.
Split ship scale profiles and benchmark phases
Old benchmark spawn layout mixed normal fleet scale with worst-case contact load, making it unclear whether cost came from AI/control, BEPU stepping, or contact publication. Sparse and dense profiles let agents compare normal space spread against deliberate contact stress.
Plan voxel ships as deterministic state over coarse physics bodies
One-body-per-voxel would not scale to hundreds/thousands of ships and would leak backend physics details into damage rules. Dense deterministic arrays give stable IDs, replayable damage ordering, cheap core-connectivity checks, and a clean path to future system tags without rebuilding colliders in the first slice.
Harden voxel plan around chunked and pooled performance paths
Earlier plan was directionally right but still allowed expensive fallback implementations: full-ship remeshes, full BFS for leaf damage, binary-search/hash lookup during DDA, per-ship heap islands, runtime GLB voxelization. Those would pass first screenshot while failing the "hundreds or thousands of ships" goal.
Define voxel construction, destruction, debris, and performance test gates
Voxel system can fail subtly in ways screenshots will not catch: floating source pieces, diagonal-only contact, impossible bounds, sliver-triangle cook explosions, bridge cuts leaving ownership bugs, split debris still responding to player/AI systems. Capturing these as CLI scenarios + benchmarks makes the cornerstone damage system testable before it gets too large.
Add synthetic voxel validation, split scenarios, and benchmark gates
Before GLB voxel cooking lands, topology and ownership rules need exact synthetic proofs. These tests catch disconnected construction, diagonal-only contact, core errors, leaf-vs-bridge topology behavior, and detached pieces accidentally remaining live/player/AI-owned.
Add CPU GLB voxel cook slice
Synthetic topology tests prove rules exactly, but the cornerstone feature also needs an early real-model proof that GLB traversal, coordinate conversion, density selection, validation, and cache boundaries work without touching render GPU buffers.
Add aggregate voxel test suite gate
Individual voxel probes are useful during development, but agents need one obvious command that catches construction, split/debris, DDA, performance smoke, and real-model cook regressions before committing later voxel work.
Record core-death hull as voxel debris
A killed ship must stop exposing live controllable/targetable voxels even when the final hit destroys only the core cell.
Hash voxel suite results by stable evidence
Performance output should still show elapsed/p95/p99 metrics, but those values cannot participate in repeatable scenario hashes.
Harden voxel headless gates before runtime integration
Runtime voxel wiring needs trustworthy automated gates; false determinism claims, zero-default cook options, or unbounded pre-validation allocations would hide the next real scaling bugs.
Attach voxel state to runtime ships
Voxel damage has to exercise the same sim-owned ship lifecycle as movement, AI, weapons, and hashes before render overlays or combat hit mapping can be trusted.
Add selected-ship voxel debug overlay
Voxel topology and core death need visible proof in the running game before combat hit mapping and chunked mesh rendering build on top.
Plan FNA-native HDR forward rendering pipeline
FNA Effect API caps at SM3 (no compute/geometry/tessellation), so the renderer must be built from pixel/vertex passes only; targeting Steam Deck (1.6 TFLOPS RDNA2, 1280×800) forces tight per-pass budgets and rules out TAA, FSR 2/3, and Forward+. Cheapest path to a modern look that survives all four supported backends (D3D11 + SDL_GPU → D3D12/Vulkan/Metal).
Fold Git LFS upload into the committed pre-push hook
`core.hooksPath=scripts/hooks` replaces Git LFS default `.git/hooks/pre-push` hook. That let Git commits containing screenshot pointer files reach GitHub while the binary LFS objects stayed only in the local checkout, causing fresh clones to fail during smudge with 404 missing-object errors.
Render voxel debug cells at cooked GLB density
Previous runtime overlay used the synthetic 27-cell cube and a radius-derived display scale, making the voxel system look much coarser than the GLB cook actually supports. Explicit debug cooking keeps normal startup free of GLB voxelization while giving honest density proof.
Route beam hits through ship-local voxel DDA
Voxel damage has to come from actual weapon rays, not hand-entered voxel ids. Keeping BEPU at the coarse-body layer preserves the one-body-per-ship scaling rule while DDA gives deterministic, allocation-free per-voxel targeting.
Prove sparse voxel DDA crosses empty cells without allocation
Capital ships will be sparse and beams can traverse long empty spans before hitting damage cells. The hot path must stay bounded and allocation-free before weapon volume increases.
Add aggregate voxel state slots with promotion on demand
The game needs hundreds/thousands of physics objects, but only selected/combat-near ships should carry detailed voxel state. Gives sim an explicit ownership boundary for fleet LOD without changing default gameplay, which still uses detailed state unless a cap/scenario opts in.
Add detailed voxel state scale ladder
Fleet LOD proves far ships can stay aggregate, but combat tuning also needs a clean headless ladder for fully detailed ships before broadside/explosion command volumes rise.
Report detailed voxel caps in sim scenarios
Agents need a direct headless signal that fleet LOD caps are active in the real simulation, not only in synthetic state runners and benchmark counters.
Apply voxel damage by sorted command runs
Broadside and explosion damage should scale with command runs, not `shipCount * commandCount`. The sorted command buffer already provides target grouping; the store should preserve that shape.
Resolve same-tick voxel detaches inside command runs
The damage transaction path must match gameplay ownership rules: once an island detaches from the core, later commands in that ordered run cannot keep damaging it as live ship hull.
Report real voxel topology and LOD benchmark metrics
Voxel splitting performance needs real per-tick topology and LOD signals before the pending-topology queue exists, or scale regressions can hide behind aggregate totals and synthetic counters.
Guard aggregate voxel ships against invalid promotion
Fleet-scale LOD only works if debug, benchmark, and combat misses do not accidentally promote far ships into detailed HP/alive/BFS arrays.
Read aggregate voxel core ids without promotion
Debug and scripted damage need asset metadata, but metadata reads should not defeat aggregate fleet LOD before the damage transaction actually lands.
Test killed voxel ships through runtime control systems
Debris/kill ownership rules need coverage through runtime input and AI systems, not only metadata on synthetic debris records.
Extract render pipeline scaffold from Game1.Draw
Pre-Phase-0 Draw method had become a 162-line dump mixing scene, HUD, and physics-debug rendering; subsequent visual phases (HDR fp16 RT, AGX tonemap, bloom, lens flare, FSR1, PBR, CSM, WBOIT) need a single seam to layer onto without re-disturbing `Game1`.
Mark detached voxel debris without drawing dead hull
Debug visuals must reflect live ownership: removed islands are no longer part of the ship, but agents still need a visible marker proving where the split happened.
Use apphost launcher for live game runs
Backgrounded `dotnet run` leaves a parent/child process pair and plain `nohup` launches can die when the agent shell session ends; launchd owns the app after command exit, but must be pointed back at the repo root so relative dev paths like `obj/voxel-cache` do not resolve under `/`.
Add live voxel damage demo
Voxel correctness screenshots were useful, but the working loop needs an obvious live demo for watching damage progress over time.
Render voxel debug cells as filled blocks
Wire cubes proved topology but were unreadable as gameplay visuals; exposed faces remove the fake gaps and make voxel damage and splits legible in the live demo.
Cache voxel debug rendering by dirty chunks
Full per-draw voxel surface rebuilds were fine for proof but wrong for the cornerstone damage system; chunk versions give render a cheap dirty boundary while keeping coordinate conversion owned by `ModelToShipVoxelSpace`.
Drive GLB mesh damage from voxel state
Voxels should remain the gameplay truth, but the visible ship must preserve the authored GLB silhouette and lose mesh regions instead of becoming visible blocks.
Add programmatic GLB breach demo
Agents need a one-flag live/screenshot state for GLB mesh damage instead of brittle hand-written lists of voxel damage commands.
Share GLB breach damage patterns with tests
Programmatic visual states need headless guards so agents can tune camera/rendering without silently drifting the underlying damage scenario.
Route ship lasers through voxel damage
Beam rendering and voxel hit mapping already existed, but gameplay needed one owner that could connect ship targeting, damage transactions, GLB mesh removal, and HUD life values without render-side state.
First weapon is a broadside beam gate
Capital ships should reward bearing and positioning before twitch aim; the beam gives immediate feedback while weapon art/damage rules are still fluid.
Make prototype thrust visibly readable
The physics-backed ship was technically moving but too subtly against the follow camera and large grid for live playtesting to trust the controls.
Default flight view is ship-local chase
Translational motion needs to stay readable during live playtesting, a visual-center follow camera can hide movement on a capital ship because rotation changes the camera target.
First AI gameplay slice is no-combat flight
This proves ownership, control, collision, camera framing, and debug surfaces before layering fighting, targeting, damage, or fleet behaviors on top.
Compact runtime HUD around performance first
The previous debug readout exposed useful values but behaved like an unstructured dump, performance needs to be visible while playtesting scale, and the rest should be scannable without covering the playfield.
Ship selection is keyboard and CLI addressable
Fleet-scale debugging needs a stable way to inspect one ship at a time without mouse-only picking; keeping chase player-owned preserves the flying camera while orbit becomes the inspection camera.
Capital ships become core-rooted damageable voxel structures
Damageable capital ships are a cornerstone fantasy, positioning and weapon impacts should matter spatially, not just subtract from a single health bar. A core-rooted graph gives readable rules, supports future "real ship systems," and keeps disconnected wreckage behavior deterministic for the first slice.
Detached voxel islands become debris, not ship
Capital-ship damage needs to feel physical without letting severed pieces keep acting like the player ship, and this gives the sim a crisp ownership boundary for future salvage, collision debris, visual wreckage, and system detachment.
Core death sheds remaining hull ownership
Core death is the same ownership boundary as bridge severing, once the root is gone, no surviving hull piece should keep behaving like the ship.
Adopt cinematic space-opera aesthetic
The "modern beautiful space game" target needs a single aesthetic compass so per-effect technique choices stop being relitigated. Cinematic space-opera fits capital-ship combat's slow tactical pacing, gives every system biome a strong identity, and survives the FNA SM3 rendering ceiling without compute-required modern looks.
Tune flight around capital-ship commitment
Capital ships should feel massive because repositioning is a decision with momentum, not because top speed is tiny.
Put primary turning on A/D
For capital-ship handling, left/right on the home cluster should command heading commitment first, strafe is secondary maneuvering thrust.
Use voxels as invisible damage truth
Capital ships need authored silhouettes and readable art direction while voxels give spatial damage and scaling, showing raw blocks as the main ship undercuts the fantasy.
Make lasers the first live voxel combat proof
This proves the cornerstone loop in play, weapons, ship ownership, voxel damage, life, debris, and GLB mesh removal, without waiting for turrets, reactors, armor types, or full combat AI.
Newtonian Flight and Weapon Gating
We integrate assisted Newtonian momentum, implement roll controls, and implement broadside weapon firing arcs.
Newtonian Basis Vectors & BEPUphysics Integration
Under the Hood: Deconstruction of ship-local thrust transformations and details of BEPUphysics v2 integration with dense scaling benchmarks.
Vendor FNA instead of MonoGame NuGet
User wants full ownership of the runtime, returning to indie roots from XNA days, ability to patch FNA source directly without coordinating upstream. The MIT / MS-PL licenses are preserved under `FNA/licenses/` and `FNA/lib/*/LICENSE` for Steam release.
Target .NET 8
Current LTS, fully cross-platform, native ARM64 on Apple Silicon, no Mono dependency. FNA `FNA.Core.csproj` already targets net8.0 so we inherit cleanly.
SDL3, not SDL2, as the FNA platform layer
This is what current `fnalibs-dailies` ships (no SDL2 binary in 2026 builds). FNA auto-detects the available platform at runtime and prefers SDL3. SDL3 has better controller, HiDPI, and Wayland support, all relevant for SteamDeck and Mac.
Native libs laid out as runtimes/<rid>/native/
Idiomatic .NET layout. Honors `dotnet publish -r <rid>` for cross-platform publishing while falling back to the host RID for `dotnet run`. The D3D12 Agility SDK folder structure is preserved on Windows because FNA3D refuses to load it otherwise.
Use SharpGLTF.Core via NuGet for GLB loading
Canonical .NET glTF library, actively maintained, pure managed code (no native deps). Small utility scope. NuGet is appropriate here. The vendor-source rule is strongest for framework-level deps like FNA, not for utilities.
Main-only git workflow with auto commit + push
Solo developer, no team to coordinate. Multi-machine work (Windows + Mac travel) needs frequent pushes to avoid stranded uncommitted state. Always-green main + small commits gives natural rollback points.
Adopt DOC/ decision-log discipline
Vibe-coded projects lose decision reasoning between sessions because the user did not physically type the code. The log is the durable memory the future-us reads first when reopening the repo.
Defer text rendering library choice; "Hello World" via window title only
`FontStashSharp.XNA` 1.5.5 only targets .NET Framework 4.8 and dragged in original Microsoft XNA reference assemblies, producing CS0433 type-conflict errors against vendored FNA. `FNA.NET.FontStashSharp` would conflict in reverse (pulls competing FNA NuGet). The clean path exists (`PlatformAgnostic` + custom adapter) but is a focused workstream, not scaffold-verification work. Choosing a font library before knowing kerning/bitmap/rich-text/localization needs would lock in the wrong tool.
Adopt FontStashSharp.PlatformAgnostic + custom FNA renderer for text
PlatformAgnostic has no XNA/MonoGame transitive dep, so it composes cleanly with vendored FNA. The ~70-line custom renderer is the price of vendored FNA, small, owned, no drift. FontStashSharp does runtime TTF rasterization so we swap typefaces without a content build step.
In-process screenshot via RenderTarget2D + `--screenshot` CLI flag
Implements the screenshot half of the [programmatic-control tenet](DESIGN-LOG.md), game is its own screenshot source, no external tools, no OS permission gates, identical Win/Mac/Linux behavior. Render-to-target is also the foundation for post-processing (bloom, grading, tonemap, UI compositing); paying that cost now is cheaper than retrofitting later.
First ship renders: GLB loader extended for normals + BasicEffect render path
Loader existed but nothing rendered, lighting needed normals, which forced upgrading vertex format from `VertexPositionColor` to `VertexPositionNormalTexture`. `BasicEffect` is FNA's built-in fixed-function effect: zero HLSL, zero asset pipeline, "looks 3D" free. Bounding-sphere framing makes `--model <path>` auto-frame any GLB without per-model camera tuning. The `--model` flag applies the programmatic-control tenet.
Cull clockwise, not counter-clockwise, for glTF + FNA right-handed projection
glTF winds front-facing triangles counter-clockwise from outside. FNA's `CreateLookAt` + `CreatePerspectiveFieldOfView` are right-handed and preserve CCW into NDC. `CullCounterClockwise` therefore culls the *front* faces, `CullClockwise` is the correct setting.
Roguelike structure (procedural runs, not scripted campaign)
Solo indie scope: replay value scales with procedural systems while content authoring stays manageable, and the roguelike loop fits "one more run" sessions which is how the user actually plays games while traveling.
3D space combat centered on capital ships
Stated game concept from project inception; capital ships afford strategic-feel combat (positioning, broadside arcs, subsystem targeting) over twitch reflexes, aligning with the roguelike "tactical decision" loop more than a flight-sim arcade loop.
GLB as the ship model format
Modern open format, widely supported in DCC tools (Blender, etc.); avoids the dead XNA-era `.fbx` → `.xnb` Content Pipeline; runtime loading enables data-driven ship variants without rebuild.
Target PC, Mac, Linux (incl. SteamDeck)
SteamDeck is a strategic platform for indie space games, controller-friendly, growing audience, lower competition; Mac coverage enables continued work on a Mac during travel; Linux is essentially free with the FNA+SDL3 stack.
Programmatic control of every game state is a core tenet
Four compounding reasons: AI-driven verification needs programmatic state access for automated screenshots and scenario runs; dev velocity collapses if reproducing a bug takes 90 seconds of menu-clicking instead of a `--scenario` flag; the same affordances let players write mods/replay/speedrun tools later; and forcing programmatic reach makes the sim explicitly expose its state transitions (easier to test, more predictable). Retrofitting onto a finished UI is a multi-week refactor, menu state machines, animation skippers, save/load headless paths all have to be threaded through every screen, but designing it in from feature one is essentially free.
Determinism in the simulation layer is a core tenet
Replay unlocks bug-repro-by-file (user sends a 200-byte replay, you reproduce locally), automated scenario regression (replay → diff every Nth frame), AI-agent verification loops, and modding/speedrun infrastructure, and these compound forever atop a deterministic sim. Retrofit cost is brutal (every system touching randomness/time/IO has to be audited and rewritten), so adopting the discipline now while sim code is empty is essentially free. The cost gap between "do this from day 1" and "add this in year 2" is multiple orders of magnitude.
Parallelizable by design, multi-agent, multi-instance from day 1
The user wants multiple Claude Code agents running in parallel (e.g., one in worktree A, another in worktree B, screenshot diff comparing). That only works if the codebase has no hidden contention surface, no shared file someone forgot about, no global cache that gets corrupted, no log file two agents fight over. Building this in from day 1 costs nothing; bolting it on after the first global-state bug is brutal, you find them one race at a time over months.
Asset pipeline: raw files at runtime, no packing until shipped game needs it
Projected asset footprint is modest, small fleet of ship GLBs, modest textures, conventional SFX/music, low hundreds of MB at ship. In that size class, the iteration-speed benefits of raw files (no build latency, trivial future hot-reload, every agent can write any asset in any worktree without tooling, dev environment matches ship) outweigh distribution-size and cold-start benefits of packing. Stating "no packing" as principle now prevents drift into "just add a small manifest" creep that compounds into a pipeline rewrite a year in.
Determinism tenet refinements after expert review
Determinism specialist review (2026-05-21) named these as the 30% the original 7 rules missed, the parts that actually break in shipped lockstep games. Cheap to add to the tenet text now while sim code is still empty.
Run shape: short FTL-style runs, hard perma-death, procedural fleets + sectors, no meta-progression
Short runs + hard perma-death gives the "one more run" loop that fits travel-on-Mac sessions, matches solo-dev scope (no meta-DB), and lets each run carry real stakes. Procedural-fleet-on-fixed-roster keeps content authoring tractable.
First Transmissions: Vendored FNA Scaffolding
We vendor FNA, construct native runtime paths, and set up our development environment to render our first capital ship model.
FNA Native Scaffolding & Dynamic Font Rendering
Under the Hood: Details of the RID-aware native deployment build pipelines and implementing runtime TTF vector font generation via FontStashSharp.