STATUS: ACTIVE
Append decisions in this format; do not rewrite accepted history.
- Status: Proposed / Accepted / Superseded
- Date: YYYY-MM-DD
- Owner:
- Context:
- Decision:
- Consequences:
- Interfaces affected:
- Status: Accepted
- Date: 2026-08-02
- Owner: Claude
- Context:
docs/PRD.mdanddocs/PROTOTYPE_CONTRACT.mdare stillAWAITING GAME DIRECTOR SOURCE MATERIALstubs. The Game Director supplied 《九幽》白盒原型联合开发包 v1.0 and 《残躯换锋》2D 动作肉鸽核心机制深度研究报告 as reference material for M1.AGENTS.mdforbids engineers inventing product rules, and the M1 brief forbids rewriting the PRD or replacing the Contract. - Decision: Implement M1 against the Development Pack (Part I contract,
Part II architecture) and the research report's formulas. Leave
PRD.mdandPROTOTYPE_CONTRACT.mduntouched. Record every derived number's source section in code comments and indocs/ARCHITECTURE.md. - Consequences: The repository's declared highest source of truth is still a stub, so the traceability chain runs through this ADR rather than through the PRD. The Game Director should land the real PRD and Prototype Contract before Codex starts X01–X08, so later work has a document to be reviewed against rather than an ADR.
- Interfaces affected: none directly; all balance constants in
data/balance_config.tres.
- Status: Accepted
- Date: 2026-08-02
- Owner: Claude
- Context:
godot/import_presets.mdrecommendsviewport_width = 1280,viewport_height = 720withstretch/mode = viewport. At that setting the game renders 1:1, so a 48×48 player is about 6% of screen height and a 32px tile is barely visible — the art reads as tiny rather than as pixel art. - Decision: Viewport 640×360, window 1280×720,
stretch/mode = viewport,stretch/scale_mode = integer. Nearest filtering, no mipmaps, 2D pixel snapping on, MSAA off,gl_compatibilityrenderer as recommended. - Consequences: Every asset renders at an exact integer scale at any window
size; no blur and no shimmer during camera movement. 320×180
bg_deeptiles cleanly at 1:1. The recommendation ingodot/import_presets.mdis superseded for the viewport size only; every other setting in that file is honoured. Asset dimensions were not changed. - Interfaces affected:
project.godot[display]and[rendering].
- Status: Accepted
- Date: 2026-08-02
- Owner: Claude
- Context: Development Pack P12 specifies whitebox readability — blue-grey
rectangle player, dark red enemies, purple Boss. Since that pack was written,
WorkBuddy delivered a complete placeholder sprite pipeline (62 PNGs plus
docs/ART_SPEC.md, which declares itself the source of truth for visual assets), and the M1 brief requires those assets to be used. - Decision: Use the pixel art. Keep P12's readability requirements — the
enemy wind-up is a colour flash driven by
EnemyConfig.telegraph_tint, and F8 draws hit/hurt/body boxes. - Consequences: The prototype looks further along than it is; the magenta placeholder borders make that visible. Readability obligations are met by behaviour rather than by flat colours.
- Interfaces affected: none.
- Status: Accepted
- Date: 2026-08-02
- Owner: Claude
- Context: Sixteen animations across player, reference enemy and Boss come
to about 70
AtlasTexturesub-resources. Hand-writing them duplicates the frame counts and FPS that already exist indocs/ART_SPEC.mdand each directory'smetadata.md. - Decision:
tools/generate_sprite_frames.pyowns the frame table and emitsactors/*/**_frames.tres. The generated files are committed. The generator verifies each sheet's real pixel dimensions against the table and aborts on a mismatch. - Consequences: When WorkBuddy replaces a sheet, re-run the generator and commit the diff. A sheet whose frame count silently changes fails loudly instead of producing sliced animations.
- Interfaces affected: none.
- Status: Accepted
- Date: 2026-08-02
- Owner: Claude
- Context: Research report 5.6 gives
ΔB = C × (0.75 + 0.05·S). For 断寿之契 that yields 12.75, but the card table in section 9.2 lists +16, and the JSON example in 13.3 carries an explicitimbalanceFlat. The two sources disagree, and the card tables are the ones a designer will tune. - Decision:
SacrificeDefinition.imbalance_flatis authoritative when greater than zero; otherwise the formula applies. The derived cost scoreCis always computed and returned inSacrificeResult.cost_scorefor telemetry and for the future slotValueRatioscoring. - Consequences: Designers tune imbalance per card without fighting a formula, and the formula remains the default for cards that do not specify. Reconcile if the Game Director rules one way.
- Interfaces affected:
SacrificeDefinition,SacrificeService.
- Status: Accepted
- Date: 2026-08-02
- Owner: Claude
- Context: Development Pack A2 requires the project to run and test straight
after clone with no extra downloads. Vendoring GUT or gdUnit4 for a dozen
assertions costs more than it saves. Separately, running the suite through
godot --headless --script res://tests/run_tests.gdwas tried and failed: a MainLoop script is compiled before the autoload singletons are registered, soCombatResolver(which namesRNGService) andSacrificeService(which namesEventBus) fail to compile,GameDatadegrades to null services, and most tests fail withNonexistent function ... in base 'Nil'. - Decision: A ~90 line harness (
tests/framework/test_case.gd+tests/test_runner.gd) launched as a scene:godot --headless --path . res://tests/test_runner.tscn. - Consequences: No plugin dependency and no parse-order trap; the test process boots exactly like the game. If the suite outgrows plain assertions — parameterised cases, fixtures, mocking — swap in gdUnit4 rather than growing the harness.
- Interfaces affected:
docs/TESTING.md,.github/workflows/godot-tests.yml.
- Status: Accepted
- Date: 2026-08-02
- Owner: Claude
- Context: The arena is one fixed space with four collision rectangles.
Authoring a
TileSetresource plusTileMapLayerdata by hand adds a lot of generated content for a layout that does not vary. - Decision: Terrain visuals are
Sprite2Dnodes with repeating texture regions; collision is explicitStaticBody2D+RectangleShape2D. - Consequences: Simpler diffs and exact collision bounds. If the arena ever
needs varied terrain, one-way platforms or hazards, move to
TileMapLayer— the spawn markers and camera bounds onArenado not change. - Interfaces affected:
scenes/arena.gd(camera_bounds,enemy_spawn_points,player_spawn,boss_spawn).
- Status: Accepted
- Date: 2026-08-02
- Owner: Claude
- Context: The transaction needs a real failure mode for its rollback path
to be meaningful. The most likely data defect is a designer typing a cost
multiplier above 1 —
1.15where0.85was meant — which would increase a structural stat and quietly hand the player a reward for free. - Decision: After the cost step,
SacrificeServicecompares integrity with its pre-transaction value. If integrity rose, the transaction fails with a readable reason and the state is restored from the snapshot. - Consequences: The rollback path is exercised by a real, reachable defect rather than being dead code. Note the guard only trips when the affected integrity component is below its cap, since components are clamped at 1.
- Interfaces affected:
SacrificeService.apply/previewfailure results.
- Status: Accepted
- Date: 2026-08-02
- Owner: Claude
- Context: Development Pack A3 lists
core/,systems/,actors/*,ui/,data/,tests/anddocs/, but A4 requiresMain.tscnandArena.tscn, which belong to none of them. - Decision: Add
res://scenes/formain.tscn,main.gd,arena.tscnandarena.gd. Claude-owned, same rules ascore/. - Consequences: One directory beyond the pack; the ownership table in
docs/ARCHITECTURE.mdcovers it. - Interfaces affected: none.
- Status: Accepted
- Date: 2026-08-02
- Owner: Claude
- Context: Development Pack A14/X04 asks for three moves and two phases. The
M1 brief allows a second move "only if supported by the available animation
assets".
assets/boss/contains exactly one attack sheet (boss_attack.png, 5 frames) alongside idle, run, hurt and death. - Decision: Ship one telegraphed melee attack. Do not fake a charge by replaying the run cycle and do not add a phase controller.
- Consequences: The Boss fight is thin — a readable pattern with one answer. Codex X04 adds the charge and the ground slam; it needs either new art from WorkBuddy or an explicit ruling that reusing the run cycle for a charge is acceptable.
- Interfaces affected: none;
BossActorextendsEnemyBaseunchanged.
- Status: Accepted
- Date: 2026-08-02
- Owner: Claude
- Context: Research report 6.1 makes light attack cost 0 stamina on purpose,
so a player is never fully locked out of acting. M1's only player action is
the light attack, so nothing spends stamina. But a sacrifice that cuts max
stamina must still be visible, and
IntegrityServiceweights it at 0.24. - Decision:
RunStatetracks current and max stamina with delayed regeneration; the HUD shows the bar;Player.spend_staminaexists and is routed. No StaminaService, no exhaustion lock, no dodge. - Consequences: The stamina bar sits at full during normal play, which is
correct rather than broken. A future StaminaService can wrap
Player.spend_staminawithout changing its callers. - Interfaces affected:
RunStatestamina commands,Player.spend_stamina.
- Status: Accepted
- Date: 2026-08-02
- Owner: Codex
- Context: Art V2 frame maps declare visible startup, active, recovery, and projectile-release frames. The M1 actors previously used coarse timers, which could deal damage before or after the visible strike. The frozen combat and actor foundations must remain authoritative.
- Decision: Concrete actor/state adapters map existing gameplay states to
V2 animations and use
AnimatedSprite2D.frame_changedonly to gate the existingHitboxor emit the already-defined projectile command. Damage remainsHitbox→Hurtbox→CombatResolver. Player/melee active frames are 4–5, Gate Warden active frames are 4–5, and Ghost Archer release is shoot frame 1 after aim reaches frame 5. Boss victory and player defeat presentation wait for the death animation to complete. - Consequences: Visible and effective strikes agree without a second combat system. Concrete animations now define attack phase duration; a missing strip must use the existing configured timer as a documented fallback. Only actions already supported by gameplay are mapped.
- Interfaces affected: none. No frozen or protected framework file changed.
- Status: Accepted
- Date: 2026-08-02
- Owner: Claude
- Context: M2 needs duration, kills, sacrifices, integrity and imbalance at
the moment a run ends, and needs death and victory to travel one path. The
frozen signature was
run_finished(outcome: StringName), which forced every listener to reach back intoRunCoordinatorand reassemble the figures — a second place where "what the run amounted to" would be defined. - Decision:
RunCoordinator._finish_runbuilds aRunResultand emits it.RunCoordinatorgains a kill count and a start timestamp;RunStategainsadd_flat_attackfor meta progression. Nothing else in the run changes:CombatResolver, enemy AI, the sacrifice system andBalanceConfigare untouched. - Consequences: One listener needed updating (
ResultScreen). Any future telemetry sink gets the whole picture from the payload.RunResultis a plain value, so it outlives the run scene and a test can build one by hand. - Interfaces affected:
RunCoordinator.run_finished,ResultScreen.show_outcome,RunState.add_flat_attack(additive).
- Status: Accepted
- Date: 2026-08-02
- Owner: Claude
- Context: The first Ghost Market implementation kept both scenes in the
tree and toggled
visibleon the idle one. It drew the market's title, panel and braziers straight over the running game.GhostMarketandMainareNode2Ds whose UI lives onCanvasLayerchildren, and aCanvasLayerdoes not inherit visibility from a parentNode2D. - Decision:
Appinstantiates and frees. Exactly one of the market and the run exists at any moment. - Consequences: No visibility subtleties and no stale nodes between runs; a
second run rebuilds the run scene from scratch, which is a stronger reset than
_clear_actors. The in-scene restart path (debugR) still relies on_clear_actorsand has its own test. Rebuilding a scene per transition is negligible at this size. - Interfaces affected:
App.market()/App.run()return null when the other is active.