Files
forgotten_caves/notes/roadmap.md
T
irrlichtandClaude Opus 5 c512bf49da Ease the camera toward the player's interpolated position
The camera target was the sim tile, which jumps 8 px per movement window,
and the follow was a constant-speed chase that stopped abruptly. Now the
target is the render-side lerp position and the camera closes the gap
exponentially, so it accelerates and settles smoothly. Also spawns the
first NPC and switches the game to World::blocked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 10:51:44 +02:00

158 lines
8.2 KiB
Markdown
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Roadmap
Local single-player roguelike with a continuously simulated, tick-based world.
Workspace layout: `sim/` (headless world simulation — tiles, entities, movement rules,
tick loop; no platform dependencies, unit-testable) and `game/` (the executable — window,
rendering, input, pathfinding, camera; owns a `Sim` and drives it). The platform layer —
window, GPU, input, RGB565 framebuffer — lives in the external `pbio` crate (git dependency,
branch `truecolor16`),
not in this repo.
History: this fork started from a client/server multiplayer architecture (authoritative UDP
server, thin client with prediction). The scope was cut to single player in September 2026;
the sim kept its authoritative shape, everything network-related was removed. The `sim`
crate's `tick`/`set_action` interface is the seam a server could be wrapped around again.
---
## Achieved milestones
- [x] PNG loader: `Image::from_png` (via the `png` crate, quantized to RGB565 on load, alpha
mask), `to_tileset` → `Vec<Tile>` in `game/src/assets.rs`. Framebuffer is 16-bit
truecolor (`pbio` branch `truecolor16`)
- [x] Pixel helpers: `set_pixel`, `extract`, `blit`, `blit_tile` in `game/src/game/pixelhelper.rs`
— signed `i32` offsets, clip against all four framebuffer edges, per-tile opacity mask
- [x] Input system: `GameAction`, `InputMap`, `InputState` in `game/src/input.rs` (over `pbio::Key`)
- [x] World model: `World` of 32×32 `Chunk`s (plain `[u16; 1024]` tile ids, flags derived
on lookup — the network-era 64-entry palette is gone), entities indexed per chunk,
`entities_in_viewport`. Map path is the first CLI argument; `assets/map_large` is a
generated 256×256 stress map
- [x] Map loader: Tiled-CSV parser in `sim/src/map.rs` (flip-flag masking, content-derived
dimensions), `load_world` slices it into chunks, invisible solid border at the map rim.
See `notes/07-map-loader.md`
- [x] Tile rendering: chunk-based world rendering straight from `World` (`render_viewport`)
- [x] Sprite-based entity rendering: entity `type_id` → tile in `entities.png`, blitted with transparency
- [x] Hybrid tick loop: 24 Hz base tick, movement in `entity_tick` every 4th tick (6 Hz);
the game loop drives the sim with a fixed-timestep accumulator. See `notes/06-hybrid-tick-loop.md`
- [x] Intent model: one scheduled action per entity per movement window (`Sim::set_action`),
replaced on re-set, consumed on execution. Headless tests in `sim/src/sim.rs`
- [x] 8-directional movement in chessboard geometry: world physics use the Chebyshev
metric — diagonal and cardinal steps are the same distance, a "circle" is a square
of tiles, matching the square viewport. The single-step rule lives in
`sim::step_allowed` (king move onto a free tile, diagonals additionally need both
orthogonal neighbors free — no corner cutting) and is the one function used by the
sim and the A*. Two held keys walk diagonally.
- [x] Collision vocabulary seeded with test values (`sim::tile_collidable`): id 146
(trees/rocks) and id 0 (empty / the invisible world border). A proper tile-data file
format replaces this table later.
- [x] Click-to-move: framebuffer click → world tile, A* over the world
(`game/src/game/pathfind.rs`, 8-connected, Chebyshev heuristic), route fed into the
sim one step per movement window. Keyboard input cancels the route; each step is
re-validated at scheduling time and a blocked step replans toward the persistent
goal. Click-and-hold steers continuously: while the button is held the route keeps
replanning toward the tile under the cursor (only when that tile changes — cursor
or camera movement), sweeping across blocked tiles keeps the current route.
- [x] Smooth camera follow: the camera is a float pixel position (`cam` in `game.rs`)
that eases toward the player's *interpolated* render position (exponential
approach, τ = 120 ms), snapping only to whole pixels at render time (and outright
on teleport-sized corrections). The viewport renders with sub-tile offsets
(31×31 tile pass + right-strip clip).
- [x] Entity blocking: `World::blocked` (solid tile ∨ out of world ∨ occupied) is the
one predicate the sim's `entity_tick` and the pathfinder share. Intents execute
in id order, so the player (id 1) wins a contested tile deterministically.
- [x] First NPC: `EntityKind { Player, Npc }` on `Entity`; `Sim::think` runs before
every movement window and schedules a random king move for each NPC one window
in four (seeded xorshift in `Sim`). Sprite = tile 1 in `entities.png` (a hen from
the Mini-Medieval pack). One is spawned at (6, 6) in `Game::start`.
- [x] Entity interpolation: per-id lerp table (`EntityLerp` in `game.rs`) — previous/current
tile plus a clock, rendered as a pixel lerp over one movement interval. Jumps of
more than one tile (Chebyshev) snap. Purely cosmetic; game logic keeps using the
sim's tile positions.
---
## Upcoming work
### 06 — Hybrid tick loop *(partially done)*
Design of record: **`notes/06-hybrid-tick-loop.md`**.
- [x] 24 Hz base loop driven from the game loop
- [x] Stride layout settled: `entity_tick` at `%4` (6 Hz, sets movement rate)
- [ ] Action-point model (`energy` / `speed` per entity, act when `energy >= ACTION_COST`).
Removed from `entity.rs` for now as premature — reintroduce when NPCs (08) actually need it.
Then `entity_tick` can move to `%2` (12 Hz) and `speed` sets the effective movement rate.
---
### 07 — Tiled map loader *(done — see Achieved / `notes/07-map-loader.md`)*
Remaining follow-ups when the need is concrete:
- Collision vocabulary: seeded with test values (0, 146) in `sim::tile_collidable`.
Decide on a proper file format for tile data (collision, opacity, …) instead of a
hardcoded match.
- Tile flipping: orientation is discarded on load; revisit with the sprite pass (09).
- Multiple / object layers (spawns, triggers) — not yet parsed. The player currently
spawns at (0, 0).
---
### 08 — Basic NPC entity + AI budget *(wanderer done — see Achieved)*
The wandering NPC and entity blocking have landed; `Sim::think` is the hook where the
budget model goes once there are enough NPCs to need it. The action-point model from
06 is still pending and only matters once NPC speeds should differ.
- [x] `EntityKind::Npc`, `Sim::think` scheduling intents through the same intent table
- Bump-to-attack: a step onto an occupied tile becomes an attack instead of a refused move
- Per sim-tick: distribute a fixed `think_budget` across entities ordered by player proximity
- Complex entities consume more budget; simple ones less. Loop breaks at zero — natural load shedding.
- No framework. No trait objects yet. A match on `EntityKind` is fine.
```rust
let mut budget: u32 = BASE_BUDGET;
for entity in entities_by_player_proximity() {
if budget == 0 { break; }
budget = budget.saturating_sub(entity.think(&mut world));
}
```
---
### 09 — Client camera *(done — see Achieved)*
Sprite rendering, camera follow and entity interpolation have all landed.
---
### Later — UI / HUD
- Bitmap font renderer: 8×8 glyph sheet PNG, `draw_text(frame, font, x, y, color, text)` — see `notes/04`
- UI primitives in `game/src/ui.rs`: `fill_rect`, `draw_rect`, `draw_hbar` — see `notes/05`
- HUD layout (320×240): HP bar + player name bottom 16px, message log right panel
- A real clip-rect on the blit primitives belongs here — once the viewport and HUD panels are
two distinct regions, the abstraction earns its keep. Not before.
---
### Later — Distribution
- Asset embedding: `include_bytes!` for single-binary distribution (assets are currently
read relative to the working directory)
- Save / load: the world is a plain `Sim` value — serialize it. Roguelike convention is a
single save slot deleted on load
---
### Later — World depth
Once the simulation architecture is stable and a real level exists:
- Doors, interactive objects
- Combat: melee range check, HP, death, permadeath
- Inventory
- Day/night and weather on the slow tick (`% 192`) — affect visibility, spawns
- Procedural dungeon generation (BSP or cellular automata feeding into the same tile format)