From 67737f3ba870b13817bcbbeb0980f889dca0026b Mon Sep 17 00:00:00 2001 From: Frank Tovar Date: Tue, 21 Apr 2026 22:25:49 +0200 Subject: [PATCH] Add hero runtime exec plan --- SLICE4.md | 498 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 498 insertions(+) create mode 100644 SLICE4.md diff --git a/SLICE4.md b/SLICE4.md new file mode 100644 index 0000000..1614355 --- /dev/null +++ b/SLICE4.md @@ -0,0 +1,498 @@ +# Add Hero Runtime + +This ExecPlan is a living document. The sections `Progress`, `Surprises & Discoveries`, `Decision Log`, and `Outcomes & Retrospective` must be kept up to date as work proceeds. + +This plan follows `PLANS.md` in the repository root. It is intentionally self-contained so a developer with only this repository and this file can implement Slice 4 without reading prior chat history. + +## Purpose / Big Picture + +After this slice, the project will have the first real gameplay actor: a controllable hero with camera-relative movement bounds, shield charges, retries, death and rebirth rules, points, level-up thresholds, primary weapon slots, secondary weapon selection, special weapon ammo, and mission-persistent inventory state. A developer will be able to boot directly into a hero sandbox, move the hero inside visible bounds, press debug buttons or shortcuts to damage, heal, kill, rebirth, add points, change level, change retries, clear inventory, and see every state change immediately. + +The observable result is concrete: from `D:\Code\zfxaction26_1`, `dotnet test SideScrollerGame.sln` validates pure hero rules, `dotnet build SideScrollerGame.sln` succeeds, and `.\godot --headless --path godot -- --debug-boot=hero-sandbox --debug-script=hero-smoke --seed=444` boots a Godot hero sandbox, runs a scripted sequence of hero debug commands, prints the resulting hero state, prints `Hero sandbox smoke succeeded`, and exits with code 0. + +This slice does not implement weapon firing, collectibles as physical pickup actors, enemy collisions, squadron mate actors, shop flow, or the mission runner. It creates the hero state model, hero node, sandbox, and debug hooks those later slices will use. + +## Progress + +- [x] (2026-04-21 20:22Z) Read repository rules, `PLANS.md`, `DESIGN.md`, `CODE.md`, `SLICE1.MD`, `SLICE2.MD`, `SLICE3.MD`, current debug command files, current sample content, current root boot wiring, and current project file list. +- [x] (2026-04-21 20:22Z) Verified that Slice 3 has committed a debug command foundation, a debug sandbox boot mode, content validation tests, and a clean worktree. +- [x] (2026-04-21 20:22Z) Created this Slice 4 ExecPlan. +- [ ] Implement pure hero runtime state and rules. +- [ ] Add unit tests for hero movement-independent rules. +- [ ] Add Godot hero actor, hero HUD, and hero sandbox. +- [ ] Add hero sandbox debug commands, keyboard shortcuts, and headless smoke script. +- [ ] Wire `HeroSandbox` into debug boot and validate existing boot modes still work. +- [ ] Run formatting, tests, Godot smokes, update this plan, and commit the completed slice. + +## Surprises & Discoveries + +- Observation: The active Godot project is under `godot/`, not at the repository root. + Evidence: `godot/project.godot` is the configured project file, and all successful Godot commands use `.\godot --path godot ...` from `D:\Code\zfxaction26_1`. + +- Observation: The current debug command foundation already supports pause, frame step, time scale, difficulty, seed, fake actor spawning, marker jumps, reload, restart, and debug flags. + Evidence: `godot/scripts/debug/commands/DebugCommandId.cs` and `godot/scripts/debug/commands/DebugCommandService.cs` contain those commands. + +- Observation: Sample difficulty definitions already contain hero starting shield charges and retry count. + Evidence: `godot/scripts/content/definitions/DifficultyDefinition.cs` defines `HeroStartingShieldCharges` and `HeroRetryCount`; `godot/scripts/content/samples/SampleContent.cs` sets Normal to 3 shields and 3 retries. + +- Observation: There is no hero runtime code yet. + Evidence: `rg --files godot\scripts godot\scenes tests` lists bootstrap, content, debug, and menu files only; there are no `hero` scene or script folders. + +## Decision Log + +- Decision: Implement hero gameplay rules as plain C# before Godot node behavior. + Rationale: Shield, retry, death, rebirth, point threshold, inventory slot, and level-up behavior are brittle game rules. They can be tested quickly with `dotnet test` without launching Godot, then driven from the Godot hero actor and sandbox. + Date/Author: 2026-04-21 / Codex. + +- Decision: Use `DifficultyDefinition` as the source for starting shield charges and retry count. + Rationale: Difficulty definitions already include those fields and the design says difficulty should influence hero starting survival rules. This avoids duplicating difficulty constants in hero code. + Date/Author: 2026-04-21 / Codex. + +- Decision: Treat score points and hero level as run progress that survive death, while collectible-derived inventory resets on death. + Rationale: The design says the hero collects points and levels up from point thresholds, and also says the hero loses all collectibles on death. For this slice, points and level are not treated as collectible inventory, while extra weapon slots, secondary selection, special ammo, and squadron mates are reset by the death penalty. This keeps scoring and level progression stable while preserving the death penalty. + Date/Author: 2026-04-21 / Codex. + +- Decision: Rebirth consumes one retry and restores shield charges to the active difficulty starting value. + Rationale: The design gives initial shield charges and retry counts but does not define the exact rebirth shield value. Restoring difficulty starting shields makes the sandbox playable and keeps difficulty visible in hero survival behavior. + Date/Author: 2026-04-21 / Codex. + +- Decision: Add `HeroSandbox` as a dedicated boot mode instead of expanding `DebugSandbox`. + Rationale: The debug sandbox tests debug commands generically. The hero sandbox should be focused on movement bounds and hero state transitions so later weapon, collectible, and squadron systems can reuse it without mixing unrelated fake actors. + Date/Author: 2026-04-21 / Codex. + +## Outcomes & Retrospective + +No implementation has been performed yet. This plan defines the target result for Slice 4 and must be updated as code, tests, validation, and any course corrections happen. + +## Context and Orientation + +The repository root is `D:\Code\zfxaction26_1`. The Godot project root is `D:\Code\zfxaction26_1\godot`. The repo-local wrapper `D:\Code\zfxaction26_1\godot.cmd` launches Godot 4.5.1 .NET, so Godot commands are run from the repository root with `.\godot --path godot ...`. + +Current important files are: + +- `godot/project.godot`: Godot project configuration. It sets `run/main_scene="res://scenes/bootstrap/GameRoot.tscn"`, the C# assembly name, gameplay input actions, and debug input actions. +- `SideScrollerGame.sln`: Visual Studio solution. It contains the Godot C# project and the `tests/SideScrollerGame.Content.Tests` xUnit project. +- `godot/scripts/bootstrap/GameRoot.cs`: root scene script. It reads `DebugSettings`, seeds Godot randomness, initializes `DebugCommandNode`, and loads a scene based on `DebugBootMode`. +- `godot/scenes/bootstrap/GameRoot.tscn`: main scene. It wires the current debug boot scenes and owns `DebugCommandNode` and `DebugOverlay`. +- `godot/scripts/debug/DebugBootMode.cs`: debug boot enum. It currently contains `Menu`, `Smoke`, `ContentBrowser`, and `DebugSandbox`. +- `godot/scripts/debug/DebugCommandNode.cs`: Godot wrapper around `DebugCommandService`. It applies pause, time scale, frame stepping, scene reload, and seed effects. +- `godot/scripts/debug/commands/DebugCommandService.cs`: pure debug command service. It validates difficulty ids, actor ids, and mission marker ids, and exposes command events. +- `godot/scripts/debug/DebugOverlay.cs`: debug overlay label. It currently shows boot, scene, seed, difficulty, pause, time scale, marker, fake spawn count, and debug flags. +- `godot/scripts/content/samples/SampleContent.cs`: theme-neutral sample content. It defines `difficulty.easy`, `difficulty.normal`, `difficulty.hard`, `weapon.primary.basic`, `weapon.secondary.vertical`, `weapon.special.bomb`, and `squadron.orbit`. +- `tests/SideScrollerGame.Content.Tests/`: xUnit tests. Slice 4 should add hero rule tests here to keep the local test path simple. + +Definitions used in this plan: + +- The hero is the player-controlled ship or avatar. Its final visual theme is unknown, so this slice uses simple placeholder shapes and labels. +- Camera-relative movement bounds are a rectangle on screen, relative to the current camera view, that the hero may not leave. In the sandbox, this rectangle is drawn visibly and does not require a real mission camera path. +- Shield charges are hit protection points. A hit removes one shield charge when at least one charge exists. A hit with zero charges kills the hero unless invulnerability is active. +- Retry count is the number of rebirths still available after death. Rebirth consumes one retry. If the hero dies with no retry available, the state becomes game over. +- Rebirth means returning from a dead state into an alive state inside the same run. +- Mission-persistent inventory state is the hero state that should be carried into the next mission after victory. This slice stores the shape of that state but does not implement mission flow. +- A primary weapon slot is one entry in a fixed-size list. In this slice the hero has two primary slots. The first slot starts with `weapon.primary.basic`; the second starts empty. Later slices can tune the slot count through content. +- A sandbox is a development-only scene for exercising one system quickly. The hero sandbox is not final gameplay UI. +- Headless mode means Godot runs without opening a window. This plan uses headless mode to prove the hero sandbox from the command line. + +## Plan of Work + +First, add pure hero rule files under `godot/scripts/hero/rules/`. Add `HeroRunState`, a mutable state object or record that stores active difficulty id, level, points, shield charges, retry count, alive/dead/game-over status, primary weapon slots, selected primary weapon slot index, current secondary weapon id, current special weapon id, special ammo, squadron mate type id, squadron mate count, and last state change message. Add `HeroRuleConfig`, a plain object containing primary slot count, base primary weapon id, base secondary weapon id, default special weapon id, special ammo, max squadron mates, and point thresholds. Add `HeroRuleResult`, a small result object with `Succeeded`, `Message`, and optional changed state snapshot. Add `HeroRuntimeService`, a plain service that owns a `HeroRunState` and exposes methods for starting from a difficulty, applying hits, killing, rebirthing, adding points, setting level for debug, adding and removing shield charges, setting retries, selecting the current primary slot, applying primary weapon pickups, applying secondary weapon pickups, setting special weapon and ammo, adding special ammo, setting squadron mate type and count, clearing collectible-derived inventory, and creating a mission persistence snapshot. + +The default hero config for this slice should be conservative and theme-neutral: two primary weapon slots, `weapon.primary.basic` in slot 0, empty slot 1, `weapon.secondary.vertical` as the base secondary weapon, `weapon.special.bomb` as the default special weapon for the sandbox, max four squadron mates, and point thresholds of 500, 1500, and 3000 for levels 2, 3, and 4. Level-up should add one shield charge for each level gained. If one point grant crosses multiple thresholds, it should apply every level and every shield gain. + +The death and rebirth rules should be explicit. `ApplyHit(invulnerable: false)` reduces shield charges by one if the hero is alive and has shields. If the hero is alive and has zero shields, it sets the hero to dead when retries remain, or game over when no retries remain. `ApplyHit(invulnerable: true)` should produce a successful no-damage result. `Kill()` should force the same death path as a zero-shield hit. `Rebirth()` should only work from the dead state, consume one retry, set the hero alive, restore shield charges from the active difficulty, and clear collectible-derived inventory. If no retries remain, `Rebirth()` should fail and leave game over unchanged. + +Next, add tests in `tests/SideScrollerGame.Content.Tests/HeroRuntimeServiceTests.cs`. Use `SampleContent.CreateRegistry()` and `difficulty.normal` for most tests. Cover: start state uses difficulty shields and retries, movement-independent hit rules consume shields before death, death with retries enters dead state, rebirth consumes a retry and restores shields, death with zero retries enters game over, adding points levels up and grants shields, point grants can cross multiple thresholds, primary weapon pickup fills an empty slot before replacing the current selected slot, selected primary slot can be toggled, secondary pickup replaces the current secondary weapon, special ammo changes are capped at or above zero according to the config, squadron mate pickup changes type while preserving count up to four, clearing inventory resets collectible-derived state, and invulnerability prevents damage. + +Then add Godot-facing hero scripts and scenes. Create `godot/scripts/hero/HeroActor.cs` and `godot/scenes/hero/Hero.tscn`. `HeroActor` should extend `Node2D`, use existing input actions `move_up`, `move_down`, `move_left`, and `move_right`, move at an exported speed, clamp its global position to an exported `Rect2 PlayBounds`, and expose `SetRuntime(HeroRuntimeService runtime)` and `SetPlayBounds(Rect2 bounds)`. The scene should contain a simple placeholder body such as `Polygon2D` plus a collision or marker node only if useful for debugging. It should not implement weapon firing in this slice. + +Add `godot/scripts/hero/HeroStateHudController.cs`. This should be a small `Control` script that displays the active hero state: alive/dead/game over, level, points, next threshold, shields, retries, selected primary slot, primary slot contents, secondary weapon, special weapon ammo, squadron mate type and count, and last state change. Keep the HUD dense and readable. It is a debug tool, not final game UI. + +Add `godot/scripts/debug/HeroSandboxController.cs` and `godot/scenes/debug/HeroSandbox.tscn`. The sandbox should contain a visible playfield rectangle, a `HeroActor`, a `HeroStateHudController`, a log label, and a simple debug button panel. The controller should create `HeroRuntimeService` from `SampleContent.CreateRegistry()`, the active difficulty from `DebugCommandNode.Service.State.ActiveDifficultyId`, and the default hero config. It should pass the runtime into `HeroActor` and HUD. It should register or call hero debug commands through the shared `DebugCommandNode` so keyboard, buttons, and headless smoke use the same route. + +Extend the debug command layer for hero actions. Add these values to `godot/scripts/debug/commands/DebugCommandId.cs`: `DamageHero`, `HealHero`, `KillHero`, `RebirthHero`, `AddHeroPoints`, `SetHeroLevel`, `AddHeroShield`, `RemoveHeroShield`, `SetHeroRetries`, `TogglePrimaryWeaponSlot`, `ClearHeroInventory`, `GivePrimaryWeapon`, `GiveSecondaryWeapon`, `GiveSpecialAmmo`, and `GiveSquadronMate`. Add a small generic command handler mechanism to `DebugCommandService`, such as `RegisterCommandHandler(DebugCommandId commandId, Func handler)`, so scene-specific systems can handle hero commands without putting hero state directly into the shared debug service. Existing commands must keep their current behavior and tests. Hero sandbox command handlers should call `HeroRuntimeService`, refresh the HUD, log the result, and return `DebugCommandResult`. + +Upgrade `godot/scripts/debug/DebugOverlay.cs` only enough to show a concise hero summary when one is available. Do not make the overlay own hero state. Add a method such as `SetHeroSummary(string summary)` and have the hero sandbox update it after hero state changes. The existing debug overlay text for boot, scene, seed, difficulty, pause, time scale, marker, fake actor count, and flags must remain intact. + +Add a clickable hero debug panel in the hero sandbox. Buttons should call the same debug commands as keyboard shortcuts. Include buttons for damage, heal, kill, rebirth, add 100 points, add 500 points, set level 1, set level 4, add shield, remove shield, set retries to 0, set retries to 3, toggle primary slot, give primary `weapon.primary.basic`, give secondary `weapon.secondary.vertical`, give special ammo 3, give squadron mate `squadron.orbit`, clear inventory, toggle invulnerability, pause, frame step, restart sandbox, and reload scene. If authoring all controls in the `.tscn` file is too verbose, the controller may create them in `_Ready()` for this slice. + +Add keyboard shortcuts to `godot/project.godot` for the most common hero sandbox commands: `debug_damage_hero`, `debug_kill_hero`, `debug_rebirth_hero`, `debug_add_points`, `debug_add_shield`, `debug_remove_shield`, `debug_toggle_primary_slot`, and `debug_clear_hero_inventory`. Keep existing actions and add only missing ones. In `HeroSandboxController._UnhandledInput`, translate those actions to `DebugCommandNode.Execute(...)`. + +Add `HeroSandbox` boot wiring. Update `godot/scripts/debug/DebugBootMode.cs` to include `HeroSandbox`. Update `godot/scripts/bootstrap/GameRoot.cs` to export a `PackedScene? HeroSandboxScene` and load it when the boot mode is `HeroSandbox`. Update `godot/scenes/bootstrap/GameRoot.tscn` to reference `res://scenes/debug/HeroSandbox.tscn`. + +Add headless smoke support. If Godot is headless and the user command-line contains `--debug-script=hero-smoke`, `HeroSandboxController` should run a deterministic command sequence after one process frame: set difficulty to `difficulty.normal`, damage the hero once, add 500 points, toggle primary slot, give primary `weapon.primary.basic`, give special ammo 3, give squadron mate `squadron.orbit`, toggle invulnerability, damage the hero once, kill the hero, rebirth the hero, set retries to 0, kill the hero again, verify game over, print a compact state transcript, print `Hero sandbox smoke succeeded`, and quit with exit code 0. If any expected command fails or any expected state is wrong, print `Hero sandbox smoke failed` and quit with exit code 1. + +Finally, update this ExecPlan as work proceeds. Do not edit `DESIGN.md` or `CODE.md` as part of this slice unless the implementation reveals a mismatch that must be documented. If `SideScrollerGame.sln`, `godot/SideScrollerGame.Godot.csproj`, or shared debug files have unrelated user edits, preserve them and stage only Slice 4 changes. + +## Concrete Steps + +Run all commands from `D:\Code\zfxaction26_1`. + +1. Confirm the current state before editing: + + git status --short + dotnet test SideScrollerGame.sln + dotnet build SideScrollerGame.sln + .\godot --headless --path godot -- --debug-boot=debug-sandbox --debug-script=foundation-smoke --seed=333 + .\godot --headless --path godot -- --debug-boot=content-browser --content-validate-only + + Expected result: the worktree should be clean unless the user has made new edits. Tests should report 22 or more passing tests. The debug sandbox should print `Debug foundation smoke succeeded`, and the content browser should print `Content validation succeeded`. + +2. Create hero folders: + + godot/scripts/hero + godot/scripts/hero/rules + godot/scenes/hero + + Creating directories that already exist is safe. + +3. Add pure hero rule files under `godot/scripts/hero/rules/`: + + HeroLifeState.cs + HeroRuleConfig.cs + HeroRuleResult.cs + HeroRunState.cs + HeroRuntimeService.cs + HeroMissionSnapshot.cs + + Keep these files free of Godot node dependencies. They may use `System`, `System.Collections.Generic`, `System.Linq`, and content registry or definition types. + +4. Add tests in: + + tests/SideScrollerGame.Content.Tests/HeroRuntimeServiceTests.cs + + Use `SampleContent.CreateRegistry()` and the default hero config from the implementation. These tests must not launch Godot. + +5. Add Godot hero runtime scripts: + + godot/scripts/hero/HeroActor.cs + godot/scripts/hero/HeroStateHudController.cs + godot/scripts/debug/HeroSandboxController.cs + + `HeroActor` owns movement and bounds clamping. `HeroStateHudController` displays state. `HeroSandboxController` owns sandbox wiring, debug command handlers, buttons, keyboard shortcuts, restart, and headless smoke. + +6. Add scenes: + + godot/scenes/hero/Hero.tscn + godot/scenes/debug/HeroSandbox.tscn + + Use built-in Godot nodes and placeholder shapes only. No art assets are required for this slice. + +7. Extend shared debug command files: + + godot/scripts/debug/commands/DebugCommandId.cs + godot/scripts/debug/commands/DebugCommandService.cs + godot/scripts/debug/commands/DebugRuntimeState.cs if needed + godot/scripts/debug/DebugOverlay.cs + + Add hero command ids, generic scene command handler registration, and optional hero summary display. Preserve all Slice 3 command behavior. + +8. Wire boot and input: + + godot/scripts/debug/DebugBootMode.cs + godot/scripts/bootstrap/GameRoot.cs + godot/scenes/bootstrap/GameRoot.tscn + godot/project.godot + + Add `HeroSandbox`, the exported scene reference, the boot switch branch, and missing hero debug input actions. + +9. Format touched C# files. Include every C# file touched by this slice. Use separate path arguments: + + jb cleanupcode --build=False godot\scripts\hero\rules\HeroLifeState.cs godot\scripts\hero\rules\HeroRuleConfig.cs godot\scripts\hero\rules\HeroRuleResult.cs godot\scripts\hero\rules\HeroRunState.cs godot\scripts\hero\rules\HeroRuntimeService.cs godot\scripts\hero\rules\HeroMissionSnapshot.cs godot\scripts\hero\HeroActor.cs godot\scripts\hero\HeroStateHudController.cs godot\scripts\debug\HeroSandboxController.cs godot\scripts\debug\commands\DebugCommandId.cs godot\scripts\debug\commands\DebugCommandService.cs godot\scripts\debug\DebugOverlay.cs godot\scripts\debug\DebugBootMode.cs godot\scripts\bootstrap\GameRoot.cs tests\SideScrollerGame.Content.Tests\HeroRuntimeServiceTests.cs + + Adjust the file list to include the real touched files. If `jb cleanupcode` is unavailable, record the exact error in `Surprises & Discoveries`, keep formatting manually consistent, and continue validation. + +10. Validate the slice: + + dotnet test SideScrollerGame.sln + dotnet build SideScrollerGame.sln + .\godot --headless --path godot --build-solutions --quit + .\godot --headless --path godot -- --debug-boot=hero-sandbox --debug-script=hero-smoke --seed=444 + .\godot --headless --path godot -- --debug-boot=debug-sandbox --debug-script=foundation-smoke --seed=333 + .\godot --headless --path godot -- --debug-boot=content-browser --content-validate-only + .\godot --headless --path godot -- --debug-boot=smoke --seed=12345 + + Expected result: tests pass, the solution builds with 0 errors, Godot solution build exits, the hero sandbox smoke prints `Hero sandbox smoke succeeded`, the debug sandbox still prints `Debug foundation smoke succeeded`, the content browser still validates sample content, and the existing smoke scene still prints `Smoke scene loaded`. + +11. Check the diff: + + git status --short + git diff -- SLICE4.md godot/project.godot godot/scripts/hero godot/scripts/debug godot/scripts/bootstrap godot/scenes/hero godot/scenes/debug godot/scenes/bootstrap tests + + Expected result: the diff includes only Slice 4 files and minimal edits to shared debug/root files. User changes outside the slice must not be reverted. + +12. Commit this slice after validation: + + git add SLICE4.md godot/project.godot godot/scripts/hero godot/scripts/debug godot/scripts/bootstrap godot/scenes/hero godot/scenes/debug godot/scenes/bootstrap tests + git commit -m "Add hero runtime" + + If Godot generates `.cs.uid` files for new scripts during validation, include the generated files with the relevant scripts. + +## Validation and Acceptance + +This slice is accepted when the following observable behaviors are true. + +Running tests succeeds: + + dotnet test SideScrollerGame.sln + +Expected final output should include a passed test summary and no failed tests. The exact number of tests may change, but it should include the existing content validation and debug command tests plus new hero runtime tests. + +Building from the repository root succeeds: + + dotnet build SideScrollerGame.sln + +Expected final lines include: + + Build succeeded. + 0 Error(s) + +Godot can build the C# solution and exit: + + .\godot --headless --path godot --build-solutions --quit + +The command should exit without a stuck Godot process. If it prints warnings that do not fail the process, record them in this plan. + +The hero sandbox can be booted directly and can exercise hero runtime rules headlessly: + + .\godot --headless --path godot -- --debug-boot=hero-sandbox --debug-script=hero-smoke --seed=444 + +Expected output includes: + + Debug boot: HeroSandbox + Seed: 444 + Hero sandbox smoke loaded + Hero command: DamageHero + Hero command: AddHeroPoints 500 + Hero command: GiveSpecialAmmo 3 + Hero command: GiveSquadronMate squadron.orbit + Hero command: KillHero + Hero command: RebirthHero + Hero command: SetHeroRetries 0 + Hero command: KillHero + Hero state: GameOver + Hero sandbox smoke succeeded + +Existing direct boots must still work: + + .\godot --headless --path godot -- --debug-boot=debug-sandbox --debug-script=foundation-smoke --seed=333 + +Expected output includes: + + Debug foundation smoke succeeded + +And: + + .\godot --headless --path godot -- --debug-boot=content-browser --content-validate-only + +Expected output includes: + + Content validation succeeded + +And: + + .\godot --headless --path godot -- --debug-boot=smoke --seed=12345 + +Expected output includes: + + Debug boot: Smoke + Seed: 12345 + Smoke scene loaded + +Opening the editor should show the project and root scene: + + .\godot --editor --path godot + +In the editor, running with `-- --debug-boot=hero-sandbox` should show the hero sandbox. The hero should move with movement input and remain inside visible bounds. Debug buttons and keyboard shortcuts should update the HUD and overlay immediately. Damage should consume shields, kill should enter dead state, rebirth should consume a retry and restore shields, points should level up the hero, primary slot toggling should change the selected slot, and restart should return the sandbox to its initial hero state. + +## Idempotence and Recovery + +The implementation is mostly additive and safe to repeat. Creating directories that already exist should do nothing. Re-running tests, builds, and smoke commands should produce the same result. + +If `godot/project.godot` already contains one of the planned hero debug input actions, keep the existing action and add only missing actions. Do not duplicate action blocks. + +If `.\godot --headless --path godot --build-solutions --quit` generates new `.cs.uid` files for scripts, keep them if Godot generated them during the project scan. Do not hand-edit generated UID values. + +If the hero sandbox smoke command hangs, inspect running processes and stop only the Godot process whose command line contains `--debug-boot=hero-sandbox --debug-script=hero-smoke` and this repository path. Record the command and process detail in `Surprises & Discoveries`, then fix the smallest affected script. + +If hero movement behaves differently in headless mode than in the editor, keep automated smoke focused on state commands and validate movement bounds manually in the editor. Record the limitation in `Surprises & Discoveries` and `Outcomes & Retrospective`. + +If user changes appear in files outside this slice, leave them untouched. If user changes conflict with `godot/project.godot`, `GameRoot.tscn`, or shared debug scripts, read the file and merge only the smallest required change. + +Do not use `git restore`, `git checkout --`, reset commands, or equivalent rollback commands to discard local changes. If generated files are unwanted, leave them unstaged unless the user explicitly asks for cleanup. + +## Artifacts and Notes + +Current successful Slice 3 commands before Slice 4 implementation: + + dotnet test SideScrollerGame.sln + Passed! - Failed: 0, Passed: 22, Skipped: 0, Total: 22 + + .\godot --headless --path godot -- --debug-boot=debug-sandbox --debug-script=foundation-smoke --seed=333 + Debug foundation smoke succeeded + + .\godot --headless --path godot -- --debug-boot=content-browser --content-validate-only + Content validation succeeded + +The hero sandbox smoke output after this slice should resemble: + + Debug boot: HeroSandbox + Seed: 444 + Hero sandbox smoke loaded + Hero command: DamageHero + Hero state: Alive level=1 points=0 shields=2 retries=3 + Hero command: AddHeroPoints 500 + Hero state: Alive level=2 points=500 shields=3 retries=3 + Hero command: TogglePrimaryWeaponSlot + Hero command: GivePrimaryWeapon weapon.primary.basic + Hero command: GiveSpecialAmmo 3 + Hero command: GiveSquadronMate squadron.orbit + Hero command: ToggleInvulnerability + Hero command: DamageHero + Hero state: Alive level=2 points=500 shields=3 retries=3 + Hero command: KillHero + Hero state: Dead level=2 points=500 shields=0 retries=3 + Hero command: RebirthHero + Hero state: Alive level=2 points=500 shields=3 retries=2 + Hero command: SetHeroRetries 0 + Hero command: KillHero + Hero state: GameOver level=2 points=500 shields=0 retries=0 + Hero sandbox smoke succeeded + +## Interfaces and Dependencies + +The project uses Godot 4.5.1 .NET through `Godot.NET.Sdk/4.5.1` and targets `net8.0` for desktop builds. New Godot-facing scripts must use the `Godot` namespace and partial Godot classes where required by the engine. Pure hero rule types should avoid Godot node dependencies so xUnit tests can run without launching the engine. + +At the end of this slice, these public C# types should exist. The exact supporting members may be adjusted during implementation, but the names, responsibilities, and observable behaviors should remain stable. + +In `godot/scripts/hero/rules/HeroLifeState.cs`: + + namespace SideScrollerGame.Hero.Rules; + + public enum HeroLifeState + { + Alive, + Dead, + GameOver + } + +In `godot/scripts/hero/rules/HeroRuleConfig.cs`: + + namespace SideScrollerGame.Hero.Rules; + + public sealed class HeroRuleConfig + { + public int PrimaryWeaponSlotCount { get; } + public string BasePrimaryWeaponId { get; } + public string BaseSecondaryWeaponId { get; } + public string DefaultSpecialWeaponId { get; } + public int DefaultSpecialAmmo { get; } + public int MaxSquadronMateCount { get; } + public IReadOnlyList PointThresholds { get; } + public static HeroRuleConfig CreateDefault(); + } + +In `godot/scripts/hero/rules/HeroRunState.cs`: + + namespace SideScrollerGame.Hero.Rules; + + public sealed class HeroRunState + { + public string ActiveDifficultyId { get; set; } + public HeroLifeState LifeState { get; set; } + public int Level { get; set; } + public int Points { get; set; } + public int ShieldCharges { get; set; } + public int RetryCount { get; set; } + public IReadOnlyList PrimaryWeaponSlots { get; } + public int SelectedPrimaryWeaponSlotIndex { get; set; } + public string CurrentSecondaryWeaponId { get; set; } + public string CurrentSpecialWeaponId { get; set; } + public int SpecialAmmo { get; set; } + public string SquadronMateTypeId { get; set; } + public int SquadronMateCount { get; set; } + public string LastStateChange { get; set; } + } + +The implementation may keep the primary slots internally mutable while exposing read-only access. + +In `godot/scripts/hero/rules/HeroRuntimeService.cs`: + + namespace SideScrollerGame.Hero.Rules; + + public sealed class HeroRuntimeService + { + public HeroRuntimeService(ContentRegistry registry, HeroRuleConfig config, string difficultyId); + public HeroRunState State { get; } + public HeroRuleResult ApplyHit(bool invulnerable); + public HeroRuleResult Kill(); + public HeroRuleResult Rebirth(); + public HeroRuleResult AddPoints(int points); + public HeroRuleResult SetLevel(int level); + public HeroRuleResult AddShieldCharge(int amount); + public HeroRuleResult RemoveShieldCharge(int amount); + public HeroRuleResult SetRetryCount(int retryCount); + public HeroRuleResult TogglePrimaryWeaponSlot(); + public HeroRuleResult ApplyPrimaryWeaponPickup(string weaponId); + public HeroRuleResult ApplySecondaryWeaponPickup(string weaponId); + public HeroRuleResult AddSpecialAmmo(int amount); + public HeroRuleResult ApplySquadronMatePickup(string squadronMateTypeId); + public HeroRuleResult ClearInventory(); + public HeroMissionSnapshot CreateMissionSnapshot(); + public event Action? StateChanged; + } + +The service should validate weapon ids and squadron mate ids against `ContentRegistry` where possible. Invalid ids should return failed `HeroRuleResult` values instead of throwing for normal content mistakes. + +In `godot/scripts/hero/HeroActor.cs`: + + namespace SideScrollerGame.Hero; + + public partial class HeroActor : Node2D + { + public override void _PhysicsProcess(double delta); + public void SetRuntime(HeroRuntimeService runtime); + public void SetPlayBounds(Rect2 bounds); + } + +The actor should read movement input, move at an exported speed, and clamp to play bounds. + +In `godot/scripts/hero/HeroStateHudController.cs`: + + namespace SideScrollerGame.Hero; + + public partial class HeroStateHudController : Control + { + public void Bind(HeroRuntimeService runtime); + public void Refresh(); + } + +In `godot/scripts/debug/HeroSandboxController.cs`: + + namespace SideScrollerGame.Debug; + + public partial class HeroSandboxController : Node + { + public override void _Ready(); + public override void _UnhandledInput(InputEvent @event); + } + +The sandbox should use `DebugCommandNode.Execute(...)` for keyboard and button commands. It should not maintain separate command rules. + +In `godot/scripts/debug/commands/DebugCommandService.cs`, add scene command handler support: + + public void RegisterCommandHandler(DebugCommandId commandId, Func handler); + public void ClearCommandHandler(DebugCommandId commandId); + +Existing debug commands should keep their current behavior. Registered scene handlers should be invoked for command ids the core debug service does not own or for command ids intentionally delegated to the scene. + +In `godot/scripts/debug/DebugBootMode.cs`, extend the enum to include: + + HeroSandbox + +In `godot/scripts/bootstrap/GameRoot.cs`, extend boot loading so: + + --debug-boot=hero-sandbox + +loads `res://scenes/debug/HeroSandbox.tscn`. + +Revision note 2026-04-21: Created this ExecPlan from the current Slice 3 project state. The plan scopes Slice 4 to hero runtime rules, a controllable hero actor, a hero sandbox, hero debug commands, and headless smoke validation while leaving weapons, pickups as actors, squadron actors, enemies, and mission flow to later slices.