docs: city generator design spec
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>
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/target
|
||||||
|
/cities
|
||||||
|
**/*.rs.bk
|
||||||
|
.DS_Store
|
||||||
523
docs/superpowers/specs/2026-10-12-city-generator-design.md
Normal file
523
docs/superpowers/specs/2026-10-12-city-generator-design.md
Normal file
@@ -0,0 +1,523 @@
|
|||||||
|
# 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.
|
||||||
Reference in New Issue
Block a user