310 lines
15 KiB
Markdown
310 lines
15 KiB
Markdown
# Cyberspace: the Net as a playable stratum
|
|
|
|
**Date:** 2026-10-12
|
|
**Status:** approved in conversation (design agreed; roaming rule ruled by Claude)
|
|
**Builds on:** `2026-10-12-city-generator-design.md` (the city, the Net graph, the
|
|
digital overlay). This spec turns the reserved `StratumKind::Net` into a real,
|
|
walkable stratum and adds jacking in and out.
|
|
|
|
## 1. Intent
|
|
|
|
The player can jack in at any street terminal. Their body stays on the physical
|
|
map at the port; their consciousness emerges in cyberspace at the same spot and
|
|
walks it. Jacking out returns them to the body.
|
|
|
|
Cyberspace is its own world with the classic look: a black void, a dotted grid
|
|
floor, and the city's subnets standing on it as geometric constructs. It keeps
|
|
the city's geography one to one, so a construct stands where its building
|
|
stands, the district hubs sit where the backbone terminals are, and the corp
|
|
core is the brightest thing on the map. The digital overlay on the physical
|
|
strata and cyberspace are the same thing seen from two sides.
|
|
|
|
Basics only. The point is to walk it and see whether it is cool.
|
|
|
|
### In scope
|
|
|
|
- `StratumKind::Net` generated as a fifth tile map, walkable, saved with the city.
|
|
- Constructs (one per building), district hubs, link lanes, gates, nodes placed
|
|
as tile features, ICE standing at construct gates.
|
|
- `Jack` connectors at street terminals; jack in / jack out with the body left
|
|
behind; a distinct avatar; field of view and memory in cyberspace.
|
|
- Rendering: a Net tileset, animated lanes, the inspector and legend for the Net,
|
|
the digital overlay drawing links in cyberspace, the strata selector showing
|
|
Net on top, Explore mode able to view the Net stratum.
|
|
- Save format version 2.
|
|
- A bounded fix shipped alongside: the Explore cursor moves freely and the camera
|
|
follows it, so edge tiles can be inspected.
|
|
|
|
### Not in scope (later specs)
|
|
|
|
ICE behaviour, hacking or any action on a node, skill gates on jacking in, a
|
|
cyberdeck or programs, NPC netrunners, lanes-only movement, damage or being
|
|
dumped. The Undercity, Skyways and Heights terminals do not get ports yet.
|
|
|
|
## 2. Rulings taken while designing
|
|
|
|
- **Free roaming.** The grid between constructs is walkable. Lanes are the lit
|
|
paths, not the only paths. Cost if wrong: a lanes-only rule is a one-line
|
|
walkability change later.
|
|
- **Nodes are tile features**, not overlay glyphs, on the Net stratum. They get
|
|
field of view, memory, minimap and inspector treatment for free. Cost if wrong:
|
|
a second anchor per node is carried in the save.
|
|
- **Ports are connectors, not tiles.** A `Jack` connector at a street terminal's
|
|
tile is the port on both strata. The tileset draws the Net side brighter when a
|
|
Jack is present. Cost if wrong: none; connectors are already per-tile data.
|
|
- **Avatar glyph** is `@` in signal cyan on the Net; the body is a dim grey `@`
|
|
on its physical stratum. Cost if wrong: one glyph constant.
|
|
- **Version 1 saves are rejected** with a message that names the seed to
|
|
regenerate from. Cost if wrong: the user regenerates a city in under a second.
|
|
- **Jack key is Enter**, not `j`: `j` is "move south" in Walk mode. Cost if
|
|
wrong: one key binding.
|
|
- **ICE is placed before other nodes** so it always takes the gate tile. Cost if
|
|
wrong: none visible; a door lock sits one tile over instead.
|
|
|
|
## 3. World model changes (`citygen`)
|
|
|
|
### Terrain and features
|
|
|
|
```rust
|
|
pub enum Terrain {
|
|
/* existing 20 ... */
|
|
NetGrid, // open cyberspace floor; walkable
|
|
NetLane, // a link drawn as a lane on the grid; walkable
|
|
NetWall, // construct or hub wall; blocks sight; not walkable
|
|
NetFloor, // inside a construct; walkable
|
|
NetGate, // opening in a construct or hub wall; walkable
|
|
}
|
|
pub const ALL_TERRAIN: [Terrain; 25];
|
|
|
|
pub enum Feature {
|
|
/* existing 15 ... */
|
|
Node(NodeKind), // a Net node standing on this cyberspace tile
|
|
}
|
|
pub const ALL_FEATURES: [Feature; 16]; // adds Feature::Node(NodeKind::Server)
|
|
|
|
impl Terrain {
|
|
/// Human label for the inspector and legend ("construct wall", "lane", "road").
|
|
pub fn label(self) -> &'static str;
|
|
}
|
|
```
|
|
|
|
Walkability: `NetGrid | NetLane | NetFloor | NetGate` walk; `NetWall` does not.
|
|
`Feature::Node(_)` never blocks walking in this spec. `NetWall` blocks sight;
|
|
`NetGate` does not.
|
|
|
|
### Strata
|
|
|
|
```rust
|
|
impl StratumKind {
|
|
pub const PHYSICAL: [StratumKind; 4]; // unchanged
|
|
pub const ALL: [StratumKind; 5]; // PHYSICAL + Net
|
|
}
|
|
```
|
|
|
|
A finished `City` has five strata, `Net` last. `City::empty` still creates the
|
|
four physical ones; the Cyberspace stage pushes the fifth. `Stratum::light` on
|
|
the Net encodes brightness: 200 on the grid, 255 on lanes, gates and hubs, and
|
|
`96 + security * 5 / 8` on construct walls and floors, so low-security
|
|
constructs render dim. Net tiles carry `district` (copied from the Street tile
|
|
at the same position) and `building` (on construct tiles).
|
|
|
|
### Net graph
|
|
|
|
```rust
|
|
pub struct Node {
|
|
pub id: NodeId,
|
|
pub kind: NodeKind,
|
|
pub anchor: (StratumKind, Pos), // physical tile, unchanged
|
|
pub cyber: Pos, // NEW: the node's tile on the Net stratum
|
|
pub subnet: SubnetId,
|
|
pub building: Option<BuildingId>,
|
|
}
|
|
|
|
impl Net {
|
|
pub fn node_in_cyber(&self, p: Pos) -> Option<&Node>; // NEW
|
|
}
|
|
```
|
|
|
|
The Net stage sets `cyber = anchor.1` when it creates a node; the Cyberspace
|
|
stage moves nodes that need moving (see 4).
|
|
|
|
### Connectors
|
|
|
|
`ConnectorKind::Jack` is used: `Connector { kind: Jack, pos: <terminal tile>,
|
|
serves: [Street, Net], public: <street tile has no building> }`. A Jack's Street
|
|
tile is the terminal itself (not walkable); it is used from an adjacent tile.
|
|
|
|
## 4. Generation: stage 12, `Cyberspace`
|
|
|
|
`Stage::Cyberspace` runs after `Net`. Label "Compiling cyberspace"; short label
|
|
"Cyber". `Stage::ALL` has 12 entries. The stage is resumable like the others;
|
|
its work queue is `Begin`, one `Construct` per building, one `Gate` per
|
|
building, one `Hub` per district, `Nodes`, one `Lane` per inter-construct link,
|
|
`Ports`.
|
|
|
|
1. **Begin.** Push `Stratum::new(Net, size)`. Every tile: `NetGrid`, `district`
|
|
from the Street tile, `light` 200, `variant` with bit 0 set on the lattice
|
|
positions `x % 4 == 0 && y % 2 == 0` and the other bits from `tile_hash`.
|
|
2. **Construct** (per building `b`). Footprint border → `NetWall`, inset → `NetFloor`
|
|
(a footprint too small to inset is all wall), `building = b.id`, light from
|
|
the building subnet's security.
|
|
2b. **Gate** (per building, after every construct stands). Among border tiles
|
|
that are not corners and whose outward neighbour is still open `NetGrid`, the
|
|
one nearest to the district backbone's anchor becomes `NetGate` (light 255);
|
|
if none is open, the nearest non-corner border tile. The stage remembers
|
|
`gate[b]`.
|
|
3. **Hub** (per district). Around the backbone anchor `c`, the border of
|
|
`Rect(c.x-2, c.y-2, 5, 5)` becomes `NetWall` (light 255) where it is still
|
|
`NetGrid`, never on a Street terminal tile and never 4-adjacent to a construct
|
|
gate, and its four edge midpoints become `NetGate`. If `c` lies inside a
|
|
building footprint, no hub: the construct is the hub.
|
|
4. **Nodes** (ICE first, then the rest in id order). Target `t = anchor.1`,
|
|
except ICE: `t = gate[building]`.
|
|
If the Net tile at `t` already holds a `Feature::Node`, `t` becomes the nearest
|
|
walkable Net tile without a node within Chebyshev distance 6, scanned in a
|
|
fixed order. Then `node.cyber = t` and the tile's feature is `Node(kind)`. A
|
|
node whose target is a `NetWall` stays on the wall (a sensor in the wall).
|
|
5. **Lane** (per link whose endpoints are not inside the same building). Start is
|
|
`gate[a.building]` if `a` is in a building, else `a.cyber`; likewise the goal.
|
|
A* over Net tiles; step cost into `NetLane | NetGate` 1, `NetGrid` 3, tiles with
|
|
a `Node` feature 1, `NetWall | NetFloor` impassable unless it is the start or
|
|
goal. Every `NetGrid` tile on the path becomes `NetLane` (light 255); features
|
|
and gates are kept. If A* fails the lane is skipped.
|
|
6. **Ports.** For every Street tile with `Feature::Terminal` that holds a Net node
|
|
(`net.node_at(Street, p)` is `Some`), push a `Jack` connector as in section 3.
|
|
|
|
## 5. Validation additions
|
|
|
|
- `Violation::CyberAnchor(NodeId)`: the Net tile at `node.cyber` must exist and
|
|
carry `Feature::Node(node.kind)`.
|
|
- `Violation::JackUnreachable { pos }`: a Jack's Street tile must have
|
|
`Feature::Terminal` and at least one walkable 8-neighbour on Street; its Net
|
|
tile must be walkable. The existing `ConnectorNotWalkable` check skips Jacks.
|
|
- `StratumUnreachable { stratum: Net, .. }`: at least `REACH_TARGET` of the Net's
|
|
walkable tiles must be reachable (4-neighbour flood) from the set of Jack
|
|
tiles. Skipped when the city has no Jacks.
|
|
- `reachable_from_street` keeps ignoring `Net` (it already does).
|
|
- The save loader requires `strata.len() == StratumKind::ALL.len()`.
|
|
|
|
## 6. The player (`cyberpunk::walker`)
|
|
|
|
```rust
|
|
pub struct Player {
|
|
pub pos: Pos,
|
|
pub stratum: StratumKind,
|
|
/// Where the body waits while jacked in: (stratum, tile). None when not jacked in.
|
|
pub body: Option<(StratumKind, Pos)>,
|
|
memory: Vec<Grid<Visibility>>, // one per StratumKind::ALL
|
|
}
|
|
|
|
impl Player {
|
|
/// Jacks in at a Jack connector on the player's tile or an 8-neighbour of it.
|
|
/// Leaves the body behind and moves to the port's Net tile. Err if none in reach
|
|
/// or already jacked in.
|
|
pub fn jack_in(&mut self, city: &City) -> Result<Pos, &'static str>;
|
|
/// Returns to the body. Err if not jacked in.
|
|
pub fn jack_out(&mut self, city: &City) -> Result<(), &'static str>;
|
|
pub fn jacked_in(&self) -> bool;
|
|
}
|
|
```
|
|
|
|
`use_connector` ignores `Jack` connectors. FOV radius on the Net is 24; `NetWall`
|
|
blocks sight. Memory has five grids indexed by `StratumKind::index()`.
|
|
|
|
## 7. Screens and rendering (`cyberpunk`)
|
|
|
|
### Keys
|
|
|
|
- **Walk** `enter`: jack in at an adjacent terminal, or jack out. Status: "jacked
|
|
in at <building or district name>", "jacked out", "no terminal within reach".
|
|
Hints line while jacked in: `⚡ jacked in · hjkl move · enter jack out · tab
|
|
camera · d net · ? keys`.
|
|
- **Explore** `[` `]` now range over all five strata; `]` from Heights shows the Net.
|
|
- Help overlay (Walk) gains the `enter` row. README documents the key and the stratum.
|
|
|
|
### Explore cursor (the bounded fix)
|
|
|
|
`MapScreen` keeps its own `cursor: Pos` for Explore mode. Pan keys move the
|
|
cursor (shift: 8 tiles); it is clamped to the map. The camera follows the cursor
|
|
with `Camera::follow`, as it follows the player in Walk mode, and stays clamped
|
|
so no void shows past the map edge. Leaving Walk mode puts the cursor on the
|
|
player. Every tile of the map can be put under the cursor.
|
|
|
|
### Drawing the Net stratum
|
|
|
|
Tileset (`render::tileset`):
|
|
|
|
| Tile | Glyph | Colour |
|
|
|---|---|---|
|
|
| `NetGrid` | `·` where `variant & 1 == 1`, else space | fg district accent mixed half with signal cyan, scaled 0.45; bg (3, 6, 10) |
|
|
| `NetLane` | double-line box set by mask (`═ ║ ╔ …`), `═` when unconnected | fg signal cyan scaled by `0.6 + 0.4 * pulse(tick + variant, 20)` |
|
|
| `NetWall` | heavy box set by mask; corp buildings use the double set (`wall_set`) | fg signal cyan, darkened by `light` (security) |
|
|
| `NetGate` | `▯` | fg (200, 255, 245) |
|
|
| `NetFloor` | space | bg (6, 18, 22) |
|
|
| `Feature::Node(kind)` | `tileset::node_glyph(kind)`: ◇ terminal, ◆ server, ⬡ backbone, ⌗ data vault, ◈ ice, ▪ others | fg (240, 255, 250) pulsing; emits (never darkened); bold when a Jack connector is present |
|
|
|
|
`connects`: `NetWall` with `NetWall | NetGate`; `NetLane` with `NetLane | NetGate`.
|
|
The digital overlay reuses `node_glyph` for its physical-side glyphs.
|
|
|
|
Explore and Walk draw the avatar as `@` bold signal cyan when the player's
|
|
stratum is the Net, white otherwise; when `player.body` is on the viewed
|
|
stratum, a dim grey `@` marks the body. Weather and traffic stay off the Net
|
|
(rain density 0 in the style pack already).
|
|
|
|
The digital overlay on the Net stratum draws only the focused subnet's links,
|
|
as dotted lines between `cyber` positions. `Digital::node_under` looks up
|
|
`node_in_cyber` when the stratum is the Net.
|
|
|
|
Panel: `strata_selector` lists `StratumKind::ALL` reversed (Net on top). The
|
|
inspector uses `Terrain::label`, shows the net block for the node under the
|
|
cursor on the Net, and lists the Jack like any connector ("jack Street · Net").
|
|
Legend for the Net: grid, lane, construct wall, gate, terminal/port, server,
|
|
backbone, data vault, ice, sensor.
|
|
|
|
Generating screen: the checklist gains "Cyber"; the growing map keeps showing
|
|
the Street stratum.
|
|
|
|
## 8. Save format
|
|
|
|
`FORMAT_VERSION = 2`. `split` accepts versions 1 and 2 so the file list can still
|
|
show version 1 cities; `load_city` rejects version 1 with
|
|
`"<path> was saved by an older version; regenerate it from seed <seed>"`.
|
|
|
|
## 9. Testing
|
|
|
|
`citygen`:
|
|
- Walkability and sight for the five Net terrains; `ALL_TERRAIN` is 25, `ALL_FEATURES` 16.
|
|
- On a generated 120x60 city: the Net stratum exists at city size; every building's
|
|
footprint border is `NetWall` or `NetGate` with exactly one gate on the border
|
|
and its inset is `NetFloor`; grid tiles carry the Street district.
|
|
- Every node's `cyber` tile holds `Feature::Node(kind)`; every ICE node stands on
|
|
or beside its building's gate.
|
|
- Every Street terminal tile with a node has a Jack serving `[Street, Net]`;
|
|
every Jack has a walkable Street neighbour and a walkable Net tile.
|
|
- At least one `NetLane` tile exists and the gate of every building reaches its
|
|
district backbone over walkable Net tiles.
|
|
- `validate` reports `CyberAnchor` for a node moved off its feature and
|
|
`JackUnreachable` for a Jack walled in on Street.
|
|
- The seed sweeps (determinism + validate) keep passing; medium under two seconds.
|
|
|
|
`cyberpunk`:
|
|
- Every new glyph is single width; Net cells are pure in their inputs.
|
|
- `jack_in` next to a terminal: stratum Net, `body` set, `pos` is the port's tile,
|
|
FOV marks more than ten tiles; `jack_in` far from a terminal is `Err`;
|
|
`jack_out` restores stratum and position; `use_connector` on a Jack tile is `None`.
|
|
- Explore `]` four times reaches the Net and the panel shows "Net"; the rendered
|
|
Net stratum contains a lane or wall glyph.
|
|
- Walk `enter` beside a terminal jacks in (screen-level test), the hints mention
|
|
"jack out", and `enter` again jacks out.
|
|
- The Explore cursor reaches `(0, 0)` and `(w-1, h-1)` while the camera origin
|
|
never goes negative or past the map.
|
|
- Save round trip at version 2; a version 1 file is listed but refused with a
|
|
message that contains its seed.
|
|
|
|
## 10. Performance
|
|
|
|
Lanes add one A* per inter-construct link (about one per building plus one per
|
|
district). Budget: a large city still generates in under 0.3 s in release; the
|
|
existing two-second medium test stands as the gate. If the budget is missed,
|
|
lanes fall back to routing only within the district's bounds.
|