diff --git a/docs/development_path.md b/docs/development_path.md new file mode 100644 index 0000000..6e9e58b --- /dev/null +++ b/docs/development_path.md @@ -0,0 +1,166 @@ +# Development Path + +Thanatograph should be built around one early question: + +Can a previous failed run become a readable, physical actor that the player can use, fear, and plan around? + +Everything else should serve that test until the answer is clearly yes. + +## Current State + +- Odin/Raylib project scaffold is in place. +- Hot reload and release builds already exist. +- `src/game` currently owns only the exported game API, a `Game_Memory` pointer, and a blank render frame. +- The pitch defines a strong core mechanic, but no gameplay systems exist yet. + +## Development Principles + +- Prove the echo mechanic before building a broad roguelike. +- Prefer deterministic fixed-step simulation for gameplay, even if rendering stays variable. +- Keep persistent run history explicit and inspectable. +- Keep dungeon scope tiny until echo interaction is fun. +- Build tooling only when it shortens iteration on the core loop. +- Avoid engine generality unless the game immediately benefits from it. + +## Core Technical Risks + +- Deterministic replay must be stable enough that echoes feel like past selves, not approximate ghosts. +- Echoes need local recovery when the live world diverges, without losing the emotional truth of replay. +- Combat must stay readable when live player, enemies, and multiple echoes interact. +- Gear recovery needs clear ownership rules to avoid duplication exploits and player confusion. +- Hot reload is useful, but `Game_Memory` changes currently restart memory, so long-lived history should eventually sit behind stable allocations or serialization. + +## First Playable Target + +The first playable should be an echo laboratory, not a full dungeon crawler. + +Target experience: + +- Start in a small room-and-corridor dungeon. +- Move, attack, take damage, die, and restart. +- On the next run, the previous run replays as a physical echo. +- The echo can collide, distract an enemy, attack, and die. +- A death marker appears where the original run ended. +- If the echo reaches its death marker, it resolves visibly. + +If this is not compelling with one room, one enemy, one weapon, and one echo, adding more content will not fix it. + +## Suggested Architecture Order + +1. Establish an app/game loop split inside `src/game`. +2. Add a fixed-timestep simulation clock and frame-independent rendering interpolation only if needed. +3. Define a minimal world model: tiles, actors, hitboxes, health, attacks, and simple room reset. +4. Record player input per simulation tick, not positions first. +5. Replay an echo by feeding recorded input into the same actor controller. +6. Add divergence handling only after basic replay works. +7. Add death events, remembered gear, and corpse recovery as data owned by run history. +8. Expand dungeon generation/content after the run/echo loop is proven. + +## Three-Month Milestones + +### Month 1: Mechanic Prototype + +Goal: one death creates one useful physical echo. + +- Fixed-step simulation. +- Top-down movement and collision. +- One test dungeon layout. +- One melee attack. +- One enemy type with simple pursuit/attack behavior. +- Player death and run restart. +- Input recording and deterministic replay. +- One echo replaying the previous run. +- Basic debug overlay for tick, run id, actor count, and replay state. + +Exit criteria: + +- A previous run can distract or damage an enemy in a way the current player can exploit. +- Replay is stable across repeated restarts for the same recorded input. + +### Month 2: Game Loop Prototype + +Goal: echoes become strategic resources with consequences. + +- Multiple stored death events. +- Multiple simultaneous echoes with caps if needed. +- Echo death-marker resolution. +- Corpse and remembered gear recovery. +- Random live-run loot with non-duplicating remembered gear. +- Two or three enemy types. +- Traps or doors that make routing matter. +- First pass at rooms connected into a small dungeon. + +Exit criteria: + +- The player can intentionally route a bad run to create a future advantage. +- Recovering gear from a past death creates a meaningful risk/reward decision. + +### Month 3: Vertical Slice + +Goal: a short, shippable-feeling dungeon with the core identity intact. + +- One cohesive dungeon theme. +- Three to five enemy types total. +- One boss designed around echo interaction. +- Echo resolution outcomes: evaporate, corrupt, rare ally. +- Basic title, run start, death, and victory flow. +- Audio and visual readability pass. +- Performance and memory pass. +- Release build packaging pass. + +Exit criteria: + +- A new player can understand why an echo exists and make at least one clever use of it without explanation. +- The game can be played for 10-20 minutes and produce memorable failure stories. + +## Near-Term Implementation Queue + +1. Replace the blank frame with a controllable player dot in a fixed arena. +2. Add fixed-step input sampling and movement state. +3. Add simple tile collision. +4. Add one enemy and health/death. +5. Record input history for a run. +6. Restart after death and spawn an echo from the prior input history. +7. Make the echo share the same movement/combat code path as the player. +8. Add death markers and a simple resolution when an echo reaches its marker. + +## Early Non-Goals + +- Procedural generation. +- Inventory UI. +- Complex animation. +- Save files. +- Complex narrative systems. +- Online features. +- More than one player character. + +## Open Design Questions + +- Should echoes collide with the live player, or only with enemies/world geometry? **For now, let's have them phase through player** +- Should replay store raw inputs, resolved intentions, or both? **Need to think through this deeply** +- Should enemies be deterministic per run seed, or should echoes adapt to changed enemy positions immediately? **Need to think through this still** +- How many echoes can be present before readability breaks? +- Is corruption random, condition-based, or both? **All outcomes of echoes at end of life are random. Dissolution should be highest chance, with corruption / allyship as rarer events. My initial thought was 4/6 chance for dissolution, 1/6 for ally, 1/6 for corrupt. + +## Design Notes + +Top-down camera centered on player, not perfectly rigid. +- follow player with slight smoothing +- small mouse/look-ahead offset +- player viewport modest so offscreen timing matters +- no free camera pan in core game + +Movement is continuous and real-time +- WASD / left stick movement +- mouse / right stick for facing / aim +- immediate acceleration +- simple collision circles/capsules against tile walls +- no stamina, consider dodge +- brief commitment windows + +Visibility is low light / fog for gameplay readability, not just atmosphere + +Fixed authoritative simulation at 60hz with variable rendering +- game state only changes in fixed simulation tickets, rendering reads state and draws it +- real frame time -> accumulator -> 0..N fixed sim ticks -> render once +- SIM_HZ = 60, SIM_DT = 1.0 / 60.0 diff --git a/docs/index.md b/docs/index.md index 7a68b0c..8214a18 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,4 +1,6 @@ # Docs +- [Development path](development_path.md) +- [Tickets](tickets/index.md) - [Build notes](build.md) - [Hot reload](hot_reload.md) diff --git a/docs/tickets/001-architecture.md b/docs/tickets/001-architecture.md new file mode 100644 index 0000000..221083f --- /dev/null +++ b/docs/tickets/001-architecture.md @@ -0,0 +1,51 @@ +# T001: Establish Game Architecture and Folder Structure + +## Goal + +Create a minimal but durable game-layer structure that can support deterministic simulation, replay, rendering, hot reload, and later dungeon systems without turning into a generic engine. + +## Context + +The current `src/game` package is intentionally flat and almost empty. Before adding timestep and replay machinery, the code needs clear ownership boundaries so simulation state, input, replay history, and rendering do not become tangled. + +This ticket should not build gameplay. It should create the places gameplay will live. + +## Proposed Shape + +Keep everything in the `game` package for now. Split by responsibility, not by abstraction layer. + +Suggested files: + +- `src/game/app.odin`: exported `game_*` API coordination and top-level update flow. +- `src/game/memory.odin`: `Game_Memory` and long-lived allocations/state roots. +- `src/game/sim.odin`: fixed-step simulation types and entry points. +- `src/game/input.odin`: platform input sampling and normalized simulation commands. +- `src/game/replay.odin`: run input history, replay cursors, and deterministic replay helpers. +- `src/game/world.odin`: prototype world state, actors, rooms, and gameplay data. +- `src/game/render.odin`: rendering from simulation state. +- `src/game/config.odin`: constants and compile-time config. + +Exact names can change if implementation reveals a better split, but the ownership boundaries should remain clear. + +## Requirements + +- Preserve the existing hot-reload and release entry points. +- Keep the exported `game_*` API small and stable. +- Introduce a top-level game state root reachable from `Game_Memory`. +- Separate simulation mutation from rendering. +- Add placeholders/types only where they directly support the next two tickets. +- Avoid a broad ECS/framework unless a concrete need emerges. +- Keep the project passing `just check`. + +## Acceptance Criteria + +- `src/game` has a clear file structure for app lifecycle, simulation, input, replay, world, and rendering. +- `game_update` delegates to explicit update/render functions instead of owning all behavior inline. +- `Game_Memory` is the only persistent root needed by the hot-reload API. +- No gameplay behavior is implemented beyond what is necessary to keep the app running. +- `just check` passes. + +## Notes + +- This ticket may rename `src/game/game.odin` if that improves clarity. +- Keep changes small. The goal is a foundation, not architecture ceremony. diff --git a/docs/tickets/002-fixed-step-replay.md b/docs/tickets/002-fixed-step-replay.md new file mode 100644 index 0000000..090ba45 --- /dev/null +++ b/docs/tickets/002-fixed-step-replay.md @@ -0,0 +1,71 @@ +# T002: Build Fixed-Step Simulation and Replay Machinery + +## Goal + +Build the deterministic simulation clock and replay/input-history foundation before rendering gameplay. + +The project should be able to prove, in code, that the same initial state plus the same tick-indexed input stream produces the same resulting simulation state. + +## Context + +Thanatograph depends on past runs becoming physical echoes. That means the simulation must be authoritative, tick-based, and replayable from the beginning. Rendering should never be required to verify replay correctness. + +This is a core programming slice, not a visual slice. + +## Requirements + +- Add a fixed simulation rate of `60hz`. +- Advance gameplay only through fixed simulation ticks. +- Keep variable rendering independent from simulation advancement. +- Track a monotonically increasing simulation tick for the current run. +- Define a normalized `Input_Command` or equivalent per simulation tick. +- Record one input command per simulated tick for a run. +- Provide a way to replay a recorded command stream from tick `0` into a fresh simulation state. +- Add deterministic test coverage for replay behavior without requiring Raylib rendering. +- Keep all gameplay-relevant randomness behind deterministic seeds, even if randomness is only stubbed in this ticket. + +## Suggested Implementation Direction + +- Use an accumulator in `game_update` or an app-level update function. +- Clamp frame delta to avoid huge catch-up spikes. +- Cap max simulation ticks per rendered frame to avoid death spirals. +- Store timers as ticks, not seconds, for gameplay-relevant state. +- Keep replay tests focused on simple state transitions first, such as position changing under input commands. + +Recommended constants: + +- `SIM_HZ = 60` +- `SIM_DT = 1.0 / 60.0` +- `MAX_FRAME_DT = 0.25` +- `MAX_TICKS_PER_FRAME = 5` + +## Determinism Test Shape + +Create a minimal pure-simulation path that can run without opening a window. + +Example scenario: + +- Initialize a small simulation state with a known seed. +- Feed a fixed sequence of input commands for `N` ticks. +- Capture the resulting state digest or explicit final state. +- Re-initialize from the same starting state. +- Replay the recorded command stream. +- Assert that the final state matches exactly. + +The test does not need enemies, collision, rendering, or real player art. + +## Acceptance Criteria + +- Simulation state advances at exactly one logical tick per fixed update. +- Rendering can run at variable frame rates without changing simulation results. +- Input history contains exactly one command per simulated tick. +- A recorded input stream can be replayed into a fresh simulation state with identical results. +- At least one deterministic replay test can run from a command line recipe. +- Gameplay code does not depend on `rl.GetFrameTime()` for simulation mutation. +- `just check` passes. + +## Notes + +- This ticket intentionally comes before visual player rendering. +- Position recording may be useful for debugging, but replay should be driven by input commands, not authoritative position playback. +- The test harness can be minimal. It only needs to protect the core assumption that echoes are possible. diff --git a/docs/tickets/003-basic-rendering.md b/docs/tickets/003-basic-rendering.md new file mode 100644 index 0000000..f6538aa --- /dev/null +++ b/docs/tickets/003-basic-rendering.md @@ -0,0 +1,41 @@ +# T003: Render Player in a Small Room + +## Goal + +Render the first visible prototype scene on top of the fixed-step simulation foundation: a player represented by a simple shape inside a small room. + +## Context + +This ticket should happen after the fixed-step and replay slice is in place. Rendering should observe simulation state, not drive it. + +The visual target is intentionally primitive. The goal is to confirm the architecture supports drawing world state cleanly. + +## Requirements + +- Render a simple room or arena. +- Render a player marker from simulation state. +- Move the player using normalized simulation input and fixed-step updates. +- Use a player-follow camera. +- Keep rendering independent from simulation mutation. +- Add basic debug text for simulation tick and frame timing. +- Preserve hot reload behavior. + +## Suggested Prototype Details + +- Use simple Raylib rectangles/circles. +- Use a single hard-coded room before building tile maps. +- Draw the player as a circle with an aim/facing indicator if input already supports aim. +- Keep the camera centered on the player, with smoothing deferred unless needed. + +## Acceptance Criteria + +- Running the game shows a small room and a controllable player marker. +- Player movement is simulation-driven and consistent across render frame rates. +- Rendering uses current simulation state and optional interpolation state; it does not mutate gameplay state. +- Debug overlay shows at least current simulation tick. +- `just check` passes. + +## Notes + +- No fog of war, enemies, attacks, inventory, dungeon generation, or echo visuals in this ticket. +- If interpolation is cheap after T002, include it. Otherwise, defer it. diff --git a/docs/tickets/index.md b/docs/tickets/index.md new file mode 100644 index 0000000..792ed94 --- /dev/null +++ b/docs/tickets/index.md @@ -0,0 +1,7 @@ +# Tickets + +Initial development tickets for the first-month prototype slice. + +- [T001: Establish game architecture and folder structure](001-architecture.md) +- [T002: Build fixed-step simulation and replay machinery](002-fixed-step-replay.md) +- [T003: Render player in a small room](003-basic-rendering.md)