Claude/core framework - #3
Merged
Merged
Conversation
Adds the Godot 4.3 project shell on top of the existing docs and art pipeline. No gameplay yet — this is the runnable container everything else sits inside. - 640x360 viewport upscaled to a 1280x720 window with integer scaling, nearest filtering, 2D pixel snapping and MSAA off, so the pixel art in assets/ renders without blur or shimmer. This deviates from the 1280x720 viewport suggested in godot/import_presets.md, which would draw a 48px sprite at 1:1; see ADR-002. - Texture importer defaults follow docs/art/GODOT_IMPORT_GUIDE.md (lossless, no mipmaps, no 3D detection) and are applied to all 62 PNGs. - Input map for movement, jump, light attack, confirm, restart and ten debug commands. - Named 2D physics layers so hit/hurt box masks read as words in scenes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jo4LHdYmCJBrqzgB5j8KGa
RunState is the authoritative source for run-level player state. - Fields are private; reads go through getters and writes through named commands, so the UI physically cannot poke a stat. Structural values (max HP, max stamina, armour, recovery, locks, taxes) are kept apart from momentary ones (current HP, current stamina) — the split that stops 'take damage, get a better sacrifice, heal, repeat' arbitrage. - snapshot/restore/clone/snapshot_hash support preview, rollback, logging and tests. recalculate_derived() is the single place derived values are produced. - EventBus carries a uniform payload envelope and is documented as observation-only: deterministic resolution uses explicit service calls. - RNGService gives each subsystem its own stream derived from the run seed via FNV-1a, so an extra roll in one system cannot shift another system's sequence. - BalanceConfig holds every tunable number; data/balance_config.tres carries the research report's Appendix A baselines. SoftCaps and DerivedStats keep the cap curves and EHP/DPS projections in one place each. - GameData autoloads the config, the stateless services and the sacrifice library. It deliberately does not own the active RunState. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jo4LHdYmCJBrqzgB5j8KGa
One damage pipeline, one integrity formula. CombatResolver implements the bucket order from research report 5.3: base -> additive -> more -> conditional -> crit -> armour. DamageContext carries every input so the resolver never reaches into a scene node; DamageResult exposes the per-stage breakdown so a wrong number can be traced to the bucket that made it. The crit roll is injectable and a context can pin or force it, which is what makes crit behaviour testable rather than a coin flip. IntegrityService implements I = 0.34h + 0.24s + 0.16a + 0.14r + 0.12f over structural values only. Current HP and current stamina are absent by design. Each component is capped at 1 so ordinary buffs cannot push integrity above the intact-body ceiling or launder away a sacrifice. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jo4LHdYmCJBrqzgB5j8KGa
SacrificeService runs one transaction in a fixed order: validate, snapshot, apply cost, recalculate integrity, apply reward, recalculate derived, apply imbalance, publish, return. The reward is priced against integrity *after* the cost, which is what makes a more broken body buy a sharper blade. preview() and apply() call the identical _run_transaction; preview runs it on a clone. There is no second code path, so a preview cannot disagree with the result. Any failure restores the pre-transaction snapshot whole. SacrificeDefinition is a Resource with the A8 field groups and a validate() used by the data test. The one M1 card, 断寿之契 (S2), lives entirely in data/sacrifices/severed_lifespan.tres: max HP x0.85, +0.18 additive, generic More x(1+G), +16 imbalance. No card values appear in script. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jo4LHdYmCJBrqzgB5j8KGa
Player is a CharacterBody2D with movement, jump, light attack, hit reaction, death and camera follow. It holds no HP of its own: current and maximum lifespan live in RunState, injected by RunCoordinator, so a sacrifice applied from the UI is visible to the player with no syncing. The action states (Grounded, Airborne, Attacking, Hurt, Dead) are one small class each under states/, with every transition funnelled through PlayerStateMachine._change_to. Dodge, Exhausted and SameDeath are documented as extension points with their entry conditions, and deliberately not stubbed. Hitbox and Hurtbox are the two halves of a damage exchange. A hitbox emits hit_actor once per actor per swing and knows nothing about damage numbers; the attacker builds the DamageContext. Hitbox keeps monitoring on and gates on an active flag, because toggling monitoring mid-frame makes Godot miss overlaps that started on the same frame. Art integration: tools/generate_sprite_frames.py emits the SpriteFrames resources from the frame table in docs/ART_SPEC.md and fails if a sheet's real dimensions disagree, so the FPS and frame counts exist once rather than in 70 hand-written AtlasTexture blocks. All 62 PNGs verified against the spec; only assets/effects/*.png disagree (single frame, metadata claims 2-4). Arena is one fixed 1024x360 space: tiled floor and walls with collision, three-layer parallax background, props, spawn markers and camera bounds. No procedural generation — a fixed space is what makes two runs comparable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jo4LHdYmCJBrqzgB5j8KGa
EnemyBase is the frozen contract: initialize / acquire_target / change_state / receive_damage / die, over an IDLE-CHASE-WINDUP-ATTACK-RECOVER-HURT-DEAD state machine with a readable wind-up telegraph. die() is idempotent. A second lethal hit in the same frame, a hit after death, or a direct second die() call all resolve to exactly one death event — there is a test for each. A corpse also stops running gravity and move_and_slide, since its body collider is gone and it would otherwise sink through the floor while the death animation plays. The reference enemy is 鬼卒 (Ghost Soldier) with the X01 timings: 180px detection, 65px attack range, 0.40 wind-up / 0.15 active / 0.60 recovery. Every number is in data/actors/ghost_melee.tres. BossActor extends EnemyBase rather than introducing a second combat system: it adds boss-scale stats, its own lifecycle events and a health bar, nothing more. It ships one melee attack because assets/boss/ contains exactly one attack sheet; the charge and ground slam are left to Codex X04 rather than faked by replaying the run cycle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jo4LHdYmCJBrqzgB5j8KGa
RunCoordinator drives BOOT -> WAVE -> SACRIFICE -> BOSS -> RESULT -> restart and owns the RunState for the run's lifetime. Every mutation of the run goes through it, which is what allows the UI to be a pure observer. UI: HUD (lifespan, stamina, attack, DPS, EHP, integrity, imbalance, boss health, phase), sacrifice preview and confirmation, and a shared death/victory screen. The sacrifice panel previews through SacrificeService — the same transaction on a clone — and confirms through the coordinator, so it cannot show a number the apply will not honour. Its visibility follows the run phase rather than its own button, so it also closes when the sacrifice is applied from debug or a test. Debug panel (F1) shows seed, phase, player state, full RunState and the snapshot hash, with commands for heal, damage, spawn enemy, start boss, preview and apply sacrifice, toggle hitboxes and restart. Every one is a call into RunCoordinator: a debug button that takes a shortcut hides the bug you are hunting. Debug damage in particular goes through the normal receive_damage path so a debug kill exercises real death handling. DebugShapeOverlay draws hit, hurt and body rectangles itself, because Godot's built-in collision debug cannot be switched on mid-session. Main.gd is the composition root: it binds every observer and only then starts the run, so nothing can miss the opening state_ready. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jo4LHdYmCJBrqzgB5j8KGa
52 tests, 205 assertions, all green against Godot 4.3 headless. Coverage is aimed at the rules that would be expensive to get wrong: - Combat: bucket order, armour as an equivalent-life model, penetration, forced and pinned crits, the crit-rate hard cap and the crit-damage/more soft caps, lethality boundaries, and a check that the breakdown multiplies back to the final number. - Integrity: current HP and stamina do not move integrity while max HP does — the arbitrage bug the research report was written to fix. Also the intact-body ceiling, that a buff cannot offset a structural loss, structural floors, and exact snapshot round-tripping. - Sacrifice: preview leaves the real state byte-identical, apply matches the preview it showed, a rejected transaction rolls back whole, the shipped card's numbers come from its .tres, the reward is priced against post-cost integrity, and a more broken body buys a bigger reward. - Enemy/Boss: damage intake through the resolver, death fires exactly once across four routes, a corpse takes no damage and stops behaving, and the Boss reuses EnemyBase rather than a second system. - RNG: same seed replays, different seeds diverge, and draining one stream does not shift another. - Run loop: the real Main scene is driven through wave, a real hitbox connect, sacrifice, boss, victory, restart and death, plus every debug command and a smoke check that all ten critical scenes instantiate. The harness is ~90 lines rather than a vendored plugin, because the Prototype Development Pack requires the project to test straight after clone with no extra downloads. If the suite outgrows plain asserts, swap in gdUnit4 rather than growing the harness. Local command: godot --headless --import godot --headless --path . res://tests/test_runner.tscn The new `godot-tests` workflow runs exactly that and leaves `repository-validation` untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jo4LHdYmCJBrqzgB5j8KGa
The warning line was restating effective HP, which the cost block on the card already shows. A warning that repeats the numbers next to it trains players to skip warnings, and the sacrifice contract needs high-risk rule costs to land. Warnings now cover what the cost block does not: a high warning level, each rule lock and action tax by name, and the permanent structural loss with its integrity delta and the reminder that healing cannot undo it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jo4LHdYmCJBrqzgB5j8KGa
Documentation now describes the code that exists rather than the code that was planned. - ARCHITECTURE.md: directory layout, scene tree, state ownership, the damage pipeline, the integrity formula, the sacrifice transaction ordering, the events/RNG rules, the player state machine with its extension points, the full pixel-art rendering configuration, and an explicit list of architectural gaps so absence is not mistaken for oversight. - INTERFACES.md: the first interface freeze. Every frozen signature and guarantee, the data contract, the list of files Codex must not modify, and the extension points that are open for work today. - DECISIONS.md: eleven ADRs, including deriving M1 from the Development Pack while leaving the PRD to the Game Director, the viewport deviation from import_presets.md, authored imbalance winning over the derived formula, the no-second-damage-path invariant that gives the rollback a real trigger, and why the Boss ships one attack. - TESTING.md: how to run the suite, why it runs as a scene rather than via --script, what is covered, and an honest list of what is not. - AI_HANDOFF.md: branch, playable status, implemented systems, frozen interfaces, protected files, the eight Codex tasks with their real blockers, known issues, test status, and a per-directory asset integration audit. - TASKS/README.md: C01-C09 marked done, X01-X08 marked ready or blocked with reasons, plus the framework work still owned by Claude. - README.md: how to launch and play, controls, debug keys, test commands. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jo4LHdYmCJBrqzgB5j8KGa
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Task
Implementation
Validation
Checklist
AGENTS.mdAI_HANDOFF.mdmainordevelop