Files
cyberpunk/docs/superpowers/specs/2026-10-12-cyberspace-design.md
Jeffrey Wilcke 292e502807 docs: cyberspace implementation plan
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-12 21:31:05 +02:00

15 KiB

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

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

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

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)

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 ", "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.