Approved design for the seeded multi-stratum cyberpunk city generator, its ratatui app (generate, watch, explore, walk), the Net graph and digital overlay, persistence, and testing strategy. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
524 lines
24 KiB
Markdown
524 lines
24 KiB
Markdown
# City Generator — Design Spec
|
||
|
||
Date: 2026-10-12
|
||
Status: approved in brainstorming, ready for implementation plan
|
||
|
||
## 1. Purpose
|
||
|
||
A terminal game in a full cyberpunk setting, built from scratch in this repo.
|
||
The first deliverable is the **city generator and its interactive app**: a
|
||
seeded, deterministic generator that produces a multi-level cyberpunk city as
|
||
walkable tile maps, wrapped in a full-screen terminal application with animated
|
||
tiles and animated menus, rich UTF-8 glyphs, and a controllable `@` to prove the
|
||
city is playable as a roguelike space.
|
||
|
||
### Success criteria
|
||
|
||
Run the binary, land in an animated title menu, configure a seed and
|
||
parameters, watch the city grow stage by stage, pan across four strata that read
|
||
as cyberpunk at a glance, toggle a digital overlay showing the Net, then walk an
|
||
`@` from a sewer tunnel up a lift to a corp rooftop. Save the city, quit, reload
|
||
it: same city. Same seed and parameters: byte-identical city, every time.
|
||
|
||
### Out of scope for this deliverable
|
||
|
||
- Combat, inventory, NPCs with logic, dialogue, quests.
|
||
- The cyberspace stratum as a playable space, ICE behavior, hacking actions,
|
||
netrunning rules. The Net *graph* and its *overlay* are in scope; playing from
|
||
inside the Net is a later spec.
|
||
- Pedestrian simulation. Traffic is in scope as a purely visual overlay.
|
||
- Game saves. Only the city is persisted.
|
||
- Mouse input, 256-color design target, Windows testing (crossterm keeps the
|
||
door open; macOS is the development target).
|
||
|
||
## 2. Decisions
|
||
|
||
| Topic | Decision |
|
||
|---|---|
|
||
| Game type | Roguelike / RPG on foot: tile-level walkable maps with interiors |
|
||
| Vertical model | Named strata, each a full tile map: Undercity, Street, Skyways, Heights. Open-ended list; a `Net` stratum is reserved for later |
|
||
| Stack | Rust (edition 2024, stable toolchain) + ratatui + crossterm |
|
||
| First cut | Generate, watch, explore, walk (`@` with FOV, collision, stairs and lifts), save and load |
|
||
| Fiction | Shared cyberpunk canon (Night City, Blade Runner, the Sprawl). Night City's district model. Original names by default, kept in a swappable flavor data file |
|
||
| Generation approach | Top-down hierarchical plan (districts → roads → blocks → lots → buildings → verticals → interiors → connectivity → dressing → naming → net), with noise fields used only for wealth/decay modulation in dressing |
|
||
| Default size | 240 × 120 tiles per stratum (reads roughly square in 1:2 terminal cells) |
|
||
| Districts | 6–8 per city by default, each with a theme driving building mix, street style, palette and signage |
|
||
| Determinism | Same seed + params ⇒ byte-identical city |
|
||
| Terminal target | Modern truecolor terminals (iTerm, Ghostty, kitty, WezTerm). Must not crash on 256 colors |
|
||
|
||
## 3. Architecture
|
||
|
||
### Workspace
|
||
|
||
```
|
||
cyberpunk/
|
||
Cargo.toml workspace, resolver 3, edition 2024
|
||
rust-toolchain.toml stable
|
||
crates/
|
||
citygen/ lib: world model + generator + net graph
|
||
deps: rand, rand_chacha, noise, serde, thiserror
|
||
NO terminal dependencies, ever
|
||
city/ bin: the app and CLI
|
||
deps: ratatui, crossterm, citygen, serde, postcard, anyhow, clap
|
||
src/main.rs CLI parse → run app or headless `gen`
|
||
src/app.rs App state, screen state machine, frame loop
|
||
src/screens/ title, configure, generating, explore, walk, files
|
||
src/render/ tileset, lighting, weather, traffic, overlay (net), viewport, minimap
|
||
src/anim/ timeline, easing, effects (glitch, typewriter, flicker, slide)
|
||
src/ui/ menu, form, panel, status bar widgets built on anim
|
||
src/walker/ player, fov, movement
|
||
flavor/
|
||
names.ron embedded by citygen: syllables, corp names, landmark templates
|
||
style.ron embedded by city: theme palettes, glyph sets
|
||
docs/superpowers/specs/
|
||
cities/ runtime save directory (gitignored)
|
||
```
|
||
|
||
The future game links `citygen` directly and lifts `render`, `anim`, `ui` and
|
||
`walker` out of `city` into crates when it needs them. They are modules with
|
||
clean boundaries now, crates later.
|
||
|
||
### Boundaries
|
||
|
||
- `citygen` knows nothing about colors, glyphs or terminals. It produces data.
|
||
- `render` is a set of pure functions from world data + render context to
|
||
styled cells. It is testable with ratatui's `TestBackend`.
|
||
- `anim` knows nothing about the city. It maps ticks to progress values and
|
||
applies effects to text/cells.
|
||
- `screens` compose `render`, `ui` and `walker` and own input handling.
|
||
|
||
### Frame loop
|
||
|
||
Fixed 30 frames per second. Each frame:
|
||
|
||
1. Poll crossterm events with a timeout equal to the remaining frame budget.
|
||
2. Increment the global `tick: u64`.
|
||
3. `screen.update(&mut app, input)` — may transition to another screen.
|
||
4. `screen.draw(frame, &app)`.
|
||
|
||
All animation is a pure function of `tick` (and tile position/variant), never
|
||
of wall-clock time, so any frame can be reproduced in a test by setting `tick`.
|
||
|
||
## 4. World model (`citygen`)
|
||
|
||
All types derive `Serialize, Deserialize, Clone, Debug`; enums also `Copy,
|
||
PartialEq, Eq, Hash` where sensible.
|
||
|
||
```rust
|
||
pub struct City {
|
||
pub seed: u64,
|
||
pub params: GenParams,
|
||
pub name: String,
|
||
pub size: Size, // { w: u32, h: u32 }
|
||
pub strata: Vec<Stratum>, // physical strata, lookup by kind
|
||
pub districts: Vec<District>,
|
||
pub buildings: Vec<Building>,
|
||
pub connectors: Vec<Connector>,
|
||
pub net: Net,
|
||
}
|
||
|
||
pub enum StratumKind { Undercity, Street, Skyways, Heights, Net /* reserved */ }
|
||
|
||
pub struct Stratum {
|
||
pub kind: StratumKind,
|
||
pub tiles: Grid<Tile>, // row-major, w*h
|
||
pub light: Grid<u8>, // 0 dark .. 255 bright, precomputed
|
||
}
|
||
|
||
pub struct Tile {
|
||
pub terrain: Terrain,
|
||
pub feature: Option<Feature>,
|
||
pub district: Option<DistrictId>,
|
||
pub building: Option<BuildingId>,
|
||
pub variant: u8, // hash(seed, x, y, stratum): glyph pick + anim phase
|
||
}
|
||
|
||
pub enum Terrain {
|
||
Void, Water, Sewage,
|
||
Road, Sidewalk, Alley, Plaza,
|
||
Floor, Wall, Door, Window,
|
||
Rooftop, Walkway, Bridge, Tunnel, Rail,
|
||
Stair, Lift, Ladder, Grate,
|
||
}
|
||
|
||
pub enum Feature {
|
||
Neon(Hue), HoloAd, Lamp, Vent, Puddle, Trash, Barrier, Stall,
|
||
Sign(SignGlyph), Terminal, Camera, AcUnit, Helipad, Pipe, Cable,
|
||
}
|
||
|
||
pub struct District {
|
||
pub id: DistrictId,
|
||
pub name: String,
|
||
pub theme: Theme,
|
||
pub center: Pos,
|
||
pub wealth: f32, // 0..1
|
||
pub decay: f32, // 0..1
|
||
pub bounds: Rect,
|
||
}
|
||
|
||
pub enum Theme {
|
||
CorpCore, NightMarket, Megablocks, CombatZone,
|
||
Docks, Industrial, NeonStrip, OldTown,
|
||
}
|
||
|
||
pub struct Building {
|
||
pub id: BuildingId,
|
||
pub kind: BuildingKind,
|
||
pub district: DistrictId,
|
||
pub footprint: Rect, // at Street level
|
||
pub span: StrataSpan, // lowest..=highest physical stratum occupied
|
||
pub name: Option<String>,
|
||
}
|
||
|
||
pub enum BuildingKind {
|
||
CorpTower, Office, Megablock, Apartment, Hotel,
|
||
Shop, NoodleBar, Bar, Club, Arcade, Pawn, Clinic, Temple,
|
||
Warehouse, Factory, Depot, Garage, Parking, Market,
|
||
}
|
||
|
||
pub struct Connector {
|
||
pub kind: ConnectorKind, // Stair, Lift, Ladder, Manhole, Jack /* reserved */
|
||
pub pos: Pos,
|
||
pub serves: Vec<StratumKind>, // ordered low → high; Stair/Ladder/Manhole have exactly 2
|
||
pub public: bool, // reachable from the street without entering a building
|
||
}
|
||
```
|
||
|
||
Walkability and opacity are **methods** on `Tile`, derived from terrain and
|
||
feature (`is_walkable()`, `blocks_sight()`), never stored. `variant` is
|
||
deterministic per tile so two neon signs never flicker in sync.
|
||
|
||
Glyph constraint: every tile is exactly one terminal cell. Signage uses
|
||
single-width glyphs only: half-width katakana (U+FF66–FF9D), Cyrillic, Greek,
|
||
currency and symbol blocks. No full-width CJK, no Hangul.
|
||
|
||
### Net graph
|
||
|
||
```rust
|
||
pub struct Net {
|
||
pub subnets: Vec<Subnet>,
|
||
pub nodes: Vec<Node>,
|
||
pub links: Vec<Link>, // (NodeId, NodeId), undirected
|
||
}
|
||
pub struct Subnet { pub id: SubnetId, pub owner: SubnetOwner, pub security: u8 }
|
||
pub enum SubnetOwner { Building(BuildingId), District(DistrictId), City }
|
||
pub struct Node {
|
||
pub id: NodeId,
|
||
pub kind: NodeKind,
|
||
pub anchor: (StratumKind, Pos),
|
||
pub subnet: SubnetId,
|
||
pub building: Option<BuildingId>,
|
||
}
|
||
pub enum NodeKind {
|
||
Terminal, Camera, DoorLock, LiftCtrl, AdBoard, LampGrid,
|
||
Server, DataVault, Ice, Backbone,
|
||
}
|
||
```
|
||
|
||
The Net lives beside the tile grid, not inside it. Tiles are looked up by anchor
|
||
when rendering the overlay.
|
||
|
||
## 5. Generation pipeline (`citygen`)
|
||
|
||
### API
|
||
|
||
```rust
|
||
pub struct GenParams {
|
||
pub seed: u64,
|
||
pub size: SizePreset, // Small 160×80, Medium 240×120, Large 320×160, Custom{w,h}
|
||
pub districts: u8, // 4..=12, default 7
|
||
pub density: f32, // 0..1, default 0.6
|
||
pub wealth_skew: f32, // -1..1, default 0.0
|
||
pub decay: f32, // 0..1, default 0.4
|
||
pub water: WaterMode, // None, Bay, River; default Bay
|
||
pub flavor: String, // flavor pack name, default "default"
|
||
}
|
||
impl GenParams { pub fn validate(&self) -> Result<(), GenError>; }
|
||
|
||
pub struct Generator { /* partial city + stage cursor */ }
|
||
impl Generator {
|
||
pub fn new(params: GenParams) -> Result<Self, GenError>;
|
||
pub fn step(&mut self) -> Progress; // Running { stage, pct } | Done
|
||
pub fn city(&self) -> &City; // partial while running
|
||
pub fn finish(self) -> Result<City, GenError>; // runs City::validate()
|
||
pub fn run(params: GenParams) -> Result<City, GenError>; // convenience
|
||
}
|
||
pub enum Stage {
|
||
Terrain, Districts, Arterials, Streets, Lots, Verticals,
|
||
Interiors, Connectivity, Dressing, Naming, Net,
|
||
}
|
||
```
|
||
|
||
Each stage is chunked into small steps (one road, one block, one building per
|
||
step). The UI calls `step()` several times per frame and draws `city()` in
|
||
between: that is the entire "watch it grow" animation. Headless callers loop to
|
||
completion. No threads, no snapshots.
|
||
|
||
### Determinism
|
||
|
||
One `ChaCha8Rng` seeded from `params.seed`. Each stage derives its own sub-RNG
|
||
from `seed ^ STAGE_CONST[stage]` so a change to one stage does not reshuffle
|
||
earlier ones. Noise fields (`noise` crate, OpenSimplex) take the same seed.
|
||
Iteration order over collections is always index order, never hash order.
|
||
|
||
### Stages
|
||
|
||
1. **Terrain.** Choose water per `WaterMode`: Bay carves a noisy coastline along
|
||
one random edge covering 10–20% of the map; River cuts a meandering channel
|
||
6–10 tiles wide; None leaves all land. Water is `Terrain::Water` on Street,
|
||
`Void` above, `Sewage` channels later in Undercity.
|
||
2. **Districts.** Place `params.districts` seeds with Poisson-style minimum
|
||
spacing on land. Grow Voronoi regions. Assign themes by rule, in order: CorpCore to the
|
||
seed nearest the land centroid; Docks to a seed touching water (if any);
|
||
Industrial adjacent to Docks (or edge if no water); CombatZone to the seed
|
||
farthest from CorpCore; remaining seeds draw from the mix
|
||
{Megablocks, NightMarket, NeonStrip, OldTown} without repeating until
|
||
exhausted. Wealth = theme base + `wealth_skew` + low-frequency noise, clamped.
|
||
Decay = theme base + `params.decay` + noise, clamped.
|
||
3. **Arterials.** Minimum spanning tree over district centers plus
|
||
`max(2, districts/3)` extra edges forming loops. Route each edge with A* on a
|
||
cost grid: water impassable, turns cost extra, existing roads cheap. Arterials
|
||
are 4 tiles of `Road` with 1 tile of `Sidewalk` each side.
|
||
4. **Streets.** Per district, subdivide its region into blocks in its style:
|
||
- CorpCore: orthogonal grid, blocks ~24×12, streets 2 wide + sidewalks.
|
||
- NightMarket, OldTown: recursive split with jitter, alleys 1 wide, dead ends allowed.
|
||
- Megablocks: 2–4 enormous blocks per district.
|
||
- Docks, Industrial: long parallel rows, access roads every 20–30 tiles.
|
||
- CombatZone: grid with 30% of streets removed and `Barrier` features at cuts.
|
||
- NeonStrip: one wide main strip with tight side alleys.
|
||
5. **Lots.** Split each block into lots along the long axis (BSP) with minimum
|
||
lot size from theme. Each lot draws a `BuildingKind` from the theme's
|
||
weighted mix and a height class (Low: Street only; Mid: Street+Skyways;
|
||
Tall: Street..Heights). CorpCore and Megablocks also get basements
|
||
(Undercity). `density` scales the fraction of lots that become Plaza or
|
||
Parking instead of buildings. Footprints are written as `Wall` perimeter +
|
||
`Floor` interior on Street.
|
||
6. **Verticals.**
|
||
- Undercity: `Tunnel` under every arterial and ⅓ of streets; `Sewage`
|
||
channels under arterials with `Grate` crossings; maintenance chambers under
|
||
buildings with basements; one `Rail` line across the map.
|
||
- Skyways: `Walkway` between buildings with span ≥ Skyways when within 12
|
||
tiles; `Walkway` platforms over intersections in CorpCore and NeonStrip;
|
||
Mid/Tall buildings get `Wall`/`Floor` footprints at this stratum.
|
||
- Heights: `Rooftop` over Tall buildings with `Helipad`/`AcUnit` features;
|
||
`Bridge` between Tall buildings within 8 tiles; everything else `Void`.
|
||
7. **Interiors.** For each building and each stratum it spans: BSP rooms within
|
||
the footprint (min room 3×3), corridors joining rooms, a `Door` on a
|
||
street-facing wall at Street level, `Window` on street-facing walls, internal
|
||
`Stair` (span 2) or `Lift` shaft (span ≥ 3) linking every stratum spanned.
|
||
8. **Connectivity.** Flood fill walkable tiles per stratum. Components smaller
|
||
than 1% of walkables are filled `Void` (Undercity/Skyways/Heights) or joined
|
||
(Street). Remaining disconnected components are joined by carving the
|
||
shortest `Door`/`Tunnel`/`Walkway` path to the main component. Then build the
|
||
connector graph across strata; add public connectors (street stairwells to
|
||
Skyways, `Manhole` to Undercity, lift kiosks, fire ladders) in each district
|
||
until every stratum is reachable from Street. At most 3 repair rounds, then
|
||
`GenError::Unconnected { stratum }`.
|
||
9. **Dressing.** `Neon(hue)` and `HoloAd` on street-facing facades, density by
|
||
theme and wealth; `Lamp` along sidewalks every 8–12 tiles; `Vent` (steam),
|
||
`Puddle`, `Trash` scaled by decay; `Barrier` in CombatZone; `Stall` in
|
||
NightMarket plazas; `Terminal` in lobbies and on some sidewalks; `Camera` on
|
||
walls facing streets/corridors scaled by wealth; `Pipe`/`Cable` in Undercity;
|
||
`Sign` glyphs above shop doors. Compute `light` per stratum: lamps and neon
|
||
radiate with falloff, sky exposure adds base light (Heights > Street >
|
||
Skyways > Undercity). Assign `variant` for every tile.
|
||
10. **Naming.** City name, district names (theme syllable tables), corp names
|
||
for CorpTower/Office/Megablock, landmark templates for a few notable
|
||
buildings per district. Flavor pack from `flavor/names.ron`.
|
||
11. **Net.** One `Subnet` per building (owner Building, security from theme +
|
||
wealth), one per district (owner District, `Backbone` node at a Terminal in
|
||
the district), one city core (owner City) in CorpCore. Nodes from existing
|
||
features: `Terminal`→Terminal, `Camera`→Camera, `Door`→DoorLock,
|
||
`Lift`→LiftCtrl, `HoloAd`→AdBoard, one `LampGrid` per district, one
|
||
`Server` per building in an interior room, `DataVault` in CorpTower/Clinic,
|
||
`Ice` anchored at servers with security ≥ 160. Links: node → its building's
|
||
Server; Server → district Backbone; Backbone → city core.
|
||
|
||
### Validation
|
||
|
||
`City::validate() -> Result<(), Vec<Violation>>` checks:
|
||
|
||
- every `Connector` endpoint tile is walkable on every stratum it serves;
|
||
- every `Building` has at least one `Door` on Street;
|
||
- every stratum is reachable from Street via connectors;
|
||
- every tile's `district`/`building` ids exist;
|
||
- no two building footprints overlap;
|
||
- every Net node anchors to a tile of the expected terrain/feature;
|
||
- every district has ≥ 1 building and ≥ 1 public connector.
|
||
|
||
## 6. Rendering and animation (`city/src/render`)
|
||
|
||
### Tileset
|
||
|
||
```rust
|
||
pub fn cell(tile: &Tile, ctx: &RenderCtx) -> Cell
|
||
// RenderCtx: theme palette, light value, tick, pos, visibility, stratum kind
|
||
```
|
||
|
||
Pure function: glyph from terrain+feature+variant, color from theme palette,
|
||
light applied, animation phase from tick+variant. Tested by exact glyph/color
|
||
assertions.
|
||
|
||
### Glyph vocabulary (single width)
|
||
|
||
| Thing | Glyphs |
|
||
|---|---|
|
||
| Walls | light `─│┌┐└┘├┤┬┴┼` shops; heavy `━┃┏┓┗┛` megablocks; double `═║╔╗╚╝` corp |
|
||
| Mass / rooftop | `█▓▒░▀▄` by height class |
|
||
| Stairs / lifts / ladders / manholes | `▲▼` `⇅≡` `╫` `◍` |
|
||
| Water / sewage | `≈~∼` cycled |
|
||
| Rain | braille `⠁⠂⠄⡀⢀⠈⠐⠠` |
|
||
| Lamps | `●○◉`; vents `☰`, steam `°∘○` |
|
||
| Signage | half-width katakana, Cyrillic, Greek, `¥$€` |
|
||
| Rail / grate / cable / barrier / stall / holo ad | `╬` `▦▩` `╌` `▬` `▤` `▣` |
|
||
| Net overlay nodes | `◇◆⬡⌗` |
|
||
|
||
### Palette
|
||
|
||
Dark night base (`#0a0b10` background family). Theme accents:
|
||
|
||
| Theme | Accent pair |
|
||
|---|---|
|
||
| CorpCore | cold cyan, steel white |
|
||
| NightMarket | magenta, orange |
|
||
| Megablocks | concrete grey, amber |
|
||
| CombatZone | red, sodium yellow |
|
||
| Docks | teal, rust |
|
||
| Industrial | sulfur, sick green |
|
||
| NeonStrip | hot pink, electric blue |
|
||
| OldTown | warm paper, jade |
|
||
|
||
Stratum tint: Undercity toward green-black, Heights toward blue with more
|
||
light. Palettes live in `flavor/style.ron`.
|
||
|
||
### Overlay stack
|
||
|
||
Rendering a frame of the map is: base tiles → lighting → overlays in order:
|
||
|
||
1. **Tile phase** (inside `cell()`): neon flicker schedule from variant, holo ad
|
||
glyph cycle, water ripple, vent puff, lift indicator, rail blink.
|
||
2. **Lighting**: static light map × slow flicker factor near neon. Undercity
|
||
dark with pools of light; Heights brightest.
|
||
3. **Weather**: rain streaks falling one cell per tick with wind drift; density
|
||
by stratum (none Undercity, light Street, medium Skyways, heavy Heights);
|
||
thin fog overlay in Undercity.
|
||
4. **Traffic**: `▬` cars with headlight tint moving along arterials; spawned and
|
||
despawned at viewport edges; position + direction only, no simulation.
|
||
5. **Digital overlay** (toggle `d`): Net nodes drawn as wireframe glyphs in
|
||
signal cyan over their anchor tile; building subnet outline faint; when the
|
||
inspector points at a node, its subnet's nodes brighten and links draw as
|
||
Bresenham lines.
|
||
6. **Walker**: FOV shading (Visible normal, Remembered dimmed and unanimated,
|
||
Unseen black) and the `@`.
|
||
|
||
### Viewport and panels
|
||
|
||
- Map viewport fills the left. Camera in tile coords, clipped to the stratum,
|
||
eased toward its target over a few frames.
|
||
- Right panel (toggle `i`): tile inspector (coords, stratum, terrain, feature,
|
||
district, building name, Net node if any), legend for the current stratum
|
||
(toggle `l`), stratum selector showing all strata with the current one
|
||
highlighted.
|
||
- Minimap (toggle `m`), bottom right: 1 cell per 4×4 tiles, colored by
|
||
district, viewport rectangle and `@` marked.
|
||
- Status bar: city name, seed, stratum, FPS, key hints.
|
||
- Minimum terminal 100×30; below that, draw a centered resize message.
|
||
|
||
## 7. App flow (`city/src/screens`, `ui`, `anim`)
|
||
|
||
```
|
||
Title ──▶ Configure ──▶ Generating ──▶ Explore ◀──▶ Walk
|
||
│ │
|
||
└──▶ Files (load) ─────────────────────┘ (save from Explore/Walk)
|
||
```
|
||
|
||
- **Title.** Game name in block-element letters with neon flicker and a
|
||
scanline sweep. Menu: New City, Load City, Quit. Items slide in from the left
|
||
with typewriter reveal; selected item has pulsing `▸` and accent color.
|
||
- **Configure.** Form over `GenParams`: seed (random / typed), size preset,
|
||
districts, density, wealth skew, decay, water, flavor. Sliders as `━━━◉━━━`.
|
||
Generate button. Esc back.
|
||
- **Generating.** Street stratum grows in the viewport. Right panel: eleven
|
||
stages as a checklist, active one with progress bar
|
||
`[▸] Routing arterials ▓▓▓▓▓░░░ 62%`. Space pause, `+`/`-` steps per
|
||
frame (1..64, default 8), Enter skip to end. On `GenError`, show the error
|
||
and offer `r` reseed. Glitch wipe into Explore.
|
||
- **Explore.** Free camera. Keys: arrows/hjkl pan, Shift fast pan, `[`/`]`
|
||
stratum, `i` inspector, `m` minimap, `l` legend, `d` digital overlay, `w`
|
||
Walk, `s` save, `r` regenerate with fresh seed, `g` Configure, `?` help,
|
||
`q` Title.
|
||
- **Walk.** Camera follows `@`. hjkl/yubn/arrows move, `<`/`>` use connector,
|
||
`.` wait, Tab back to Explore leaving `@` in place. Panels and `d` as above.
|
||
- **Files.** List `./cities/*.city` with name, seed, size, date. Enter loads,
|
||
Esc back. Save prompts for a name, default the city name.
|
||
|
||
**Transitions**: every screen change is a 150–250 ms effect (glitch wipe of
|
||
shuffled cells, or vertical slide); any key skips it. Panels fade in by
|
||
sweeping their border.
|
||
|
||
**`anim`**: `Timeline` of tweens (start tick, duration, easing); `Effects`
|
||
providing glitch, typewriter, flicker, slide as functions of progress ∈ [0,1].
|
||
|
||
## 8. Walker (`city/src/walker`)
|
||
|
||
```rust
|
||
pub struct Player { pub pos: Pos, pub stratum: StratumKind, pub memory: HashMap<StratumKind, Grid<Visibility>> }
|
||
pub enum Visibility { Unseen, Remembered, Visible }
|
||
```
|
||
|
||
- Eight-direction movement; a move succeeds if the target `is_walkable()`.
|
||
Doors are passable and render open while occupied.
|
||
- `<`/`>` on Stair/Ladder/Manhole: move to the other stratum. On Lift: step
|
||
through `serves` in order; inspector shows the lift panel with current floor.
|
||
- FOV: symmetric shadowcasting. Radius 14 on Street/Skyways/Heights, 8 in
|
||
Undercity. Memory per stratum.
|
||
- Spawn on first entering Walk: a random public connector on Street in a
|
||
non-CorpCore district. `@` persists across Explore/Walk until regeneration.
|
||
- Camera follows with a deadzone of ⅓ viewport, eased.
|
||
|
||
## 9. Persistence and CLI (`city`)
|
||
|
||
- File: magic `CITY` (4 bytes), `u16` format version, postcard body of `City`.
|
||
Extension `.city`, directory `./cities/`.
|
||
- Unknown/newer version ⇒ clear error, not a panic.
|
||
- CLI: `city` (interactive) and `city gen --seed N --size small|medium|large
|
||
--out file.city [--json]`. `--json` writes serde_json instead. Nothing else.
|
||
- Player position is not saved.
|
||
|
||
## 10. Error handling
|
||
|
||
- `citygen`: `thiserror`, `GenError { InvalidParams { field, reason },
|
||
Unconnected { stratum }, Flavor(String) }`. Never panics on valid params.
|
||
- `city`: `anyhow`. A terminal guard restores screen, raw mode and cursor on
|
||
exit, error and panic (panic hook). Resize below 100×30 shows a message.
|
||
|
||
## 11. Testing
|
||
|
||
- `citygen` property-style tests over fixed seeds: 50 Small, 10 Medium, 3 Large
|
||
in the default run; an `#[ignore]` extended test runs 200 seeds. Each seed:
|
||
byte-identical output on two runs, `validate()` passes, Street reaches every
|
||
stratum, every district has a building and a public connector, every Net node
|
||
anchors correctly, no footprint overlap. One timing test: Medium under 2 s in
|
||
debug.
|
||
- Per-stage unit tests on small hand-built cities: arterial router never
|
||
crosses water; BSP never emits a room under 3×3; connectivity repair joins two
|
||
halves; theme assignment rules hold.
|
||
- `city` via `TestBackend`: tileset exact glyph/color at fixed ticks; viewport
|
||
clipping at all four edges; title menu at tick 0 and after slide-in; file
|
||
header round-trip.
|
||
- FOV: symmetry, walls block, radius respected, Undercity radius.
|
||
- Gate: `cargo fmt --check`, `cargo clippy -D warnings`, `cargo test`.
|
||
|
||
## 12. Dependencies
|
||
|
||
ratatui, crossterm, rand, rand_chacha, noise, serde (derive), postcard,
|
||
serde_json, thiserror, anyhow, clap (derive), ron. Latest stable majors at
|
||
implementation time.
|
||
|
||
## 13. Future work (not this deliverable)
|
||
|
||
- `StratumKind::Net` generated as a projection of the physical city; `Jack`
|
||
connectors from terminals; ICE behavior; netrunning actions.
|
||
- NPCs and pedestrians, combat, inventory, factions per district.
|
||
- Extracting `render`, `anim`, `ui`, `walker` into crates for the game.
|