Files
forgotten_caves/notes/roadmap.md
T
irrlichtandClaude Opus 5 03a12c82dc Add Brain enum with hunting foxes; move the A* into the sim
NPC strategy is now a Brain field on the entity (Idle, Wander, Hunt(Prey))
that Sim::think matches on, so behavior is data independent of species.
Hunt follows the first step of an A* route to the nearest prey in sight,
replanned every window; the pathfinder moves from game/ to sim/ for that.
A greedy step-toward-target was tried first and oscillated at walls. Bumps
between the same species no longer fight, and six foxes join the hens.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 11:27:47 +02:00

189 lines
10 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
(`sim/src/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::solid` (collidable tile ∨ out of world) and
`World::blocked` (solid ∨ occupied). The sim checks the geometry rule against
`solid` and resolves the target tile itself; the pathfinder plans with `blocked`.
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] Bump-to-attack and death: the geometry rule is checked against terrain only
(`World::solid`); an entity on the target tile turns the step into an attack
(`Entity::attack` damage, `hp` saturating). At 0 hp the entity is removed. The
sim reports both through its event queue — `sim::Event { Attacked, Died }`,
drained by the game with `Sim::take_events` after every window — which is the
seam for sound, message log, camera shake, spawns. Player death quits for now.
Click-to-move treats an occupied goal as a target (route ends in a bump) but
walks around entities in the way; keyboard steps always bump. A bump into the
same `type_id` (species) is a refused move, not a fight.
- [x] NPC brains: `Brain { Idle, Wander, Hunt(Prey) }` as a field on `Entity` — strategy
is data, independent of species and swappable at runtime. `Sim::think` matches on
it. `Hunt` takes the first step of an A* route (the pathfinder moved into `sim/`
for this) to the nearest prey within `SIGHT_RANGE` (8), replanned every window;
out of sight or unreachable it wanders. A greedy step-toward-target was tried
first and oscillated at any wall between hunter and prey. Six foxes
(`Hunt(Kind(hen))`, 8 hp / 2 dmg) clear 150 wandering hens off `map_test` in
about a minute; `cargo test -p game population -- --ignored --nocapture` watches
it headlessly.
- [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
- [x] Bump-to-attack, death, `Event` queue
- [x] `Brain` enum, `Hunt` via A*
- A* per hunter per window is fine for six foxes; the think budget below becomes
necessary once hunters number in the dozens (or `hunt` caches its route and replans
only when the target moved).
- Hunters don't defend themselves: a fox bitten by a hen does not retaliate unless
the hen happens to be its nearest prey. `Brain::Hunt` could switch target on `Attacked`.
- Stats beyond `hp`/`attack` (defense, speed) as plain fields; a `Stats` substruct once
they crowd `Entity`. No ECS until entities actually differ in *which* components they
carry — see the discussion of 2026-09-19.
- NPC reactions (flee when hit, aggro on the player) — `think` would need to see the
previous window's events; today the game drains them right after `step`
- 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)