Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
cfd12f2
feat(citygen): generate inside a drawn polygon boundary
Jul 31, 2026
2372c2e
feat(citygen): DRAW_AREA mode for tracing generation bounds
Jul 31, 2026
5f4cb2b
feat(citygen): GRID and SUPERBLOCK street layouts
Jul 31, 2026
a138d7c
feat(citygen): RING layout — beltways and radial spokes
Jul 31, 2026
155cdc1
fix(citygen): RING generated a sparse, fragmented city
Jul 31, 2026
4bab713
feat(citygen): RING arterials are elevated, freeing the ground beneath
Jul 31, 2026
9ee19e9
fix(citygen): decks no longer pierce buildings; spokes reach the ground
Jul 31, 2026
9980357
chore: release 1.8.0 — drawn bounds and street layouts
Jul 31, 2026
753bfeb
feat(citygen): graded road hierarchy, skyline taper, per-zone setbacks
Jul 31, 2026
5464e6f
fix(citygen): upper storeys floated above their shortened bases
Jul 31, 2026
3c9a461
fix(citygen): deck capping scales a whole plot, not each part
Jul 31, 2026
2316a94
feat(citygen): stay on the generator panel after generating
Jul 31, 2026
dec74c5
feat(citygen): seeded generation — the same seed rebuilds the same city
Aug 1, 2026
cfa0d6f
fix(citygen): a typed seed is used as typed; add UNDO to the generator
Aug 1, 2026
6c557f6
feat(citygen): REGENERATE clears the previous city before building
Aug 1, 2026
4758217
refactor(citygen): move region counting out of the admin panel
Aug 1, 2026
b21305a
fix(citygen): regenerating rolls a new seed unless one is asked for
Aug 2, 2026
6a980c0
feat(citygen): trash can for clearing the seed
Aug 2, 2026
c17a756
feat(citygen): generate rivers, coastlines and lakes
Aug 2, 2026
30e5bcd
feat(citygen): parks can have ponds
Aug 2, 2026
ef5ab80
fix(citygen): park ponds were puddles
Aug 2, 2026
e6bf731
docs: record the generator work in 1.8.0
Aug 2, 2026
cda6a2c
feat(citygen): ORGANIC_CELLS layout — a city that grew
Aug 2, 2026
fe868dd
feat(citygen): roundabouts where major roads meet
Aug 2, 2026
333ef77
fix(citygen): REGENERATE built around the river it deleted
Aug 2, 2026
81d19db
fix(citygen): roundabouts sat half in the water
Aug 2, 2026
52c5545
fix(citygen): roundabout islands counted as GM-authored
Aug 2, 2026
27d127e
feat(citygen): small civic monuments for roundabout islands
Aug 2, 2026
89dd8aa
feat(citygen): monuments with a silhouette, not box on box
Aug 2, 2026
e9feb90
fix(citygen): monuments glowed brighter than the city around them
Aug 2, 2026
20eec32
fix(citygen): monuments opted themselves out of the theme
Aug 2, 2026
34e0f36
fix(citygen): monuments used a segment count nothing else uses
Aug 2, 2026
ec4d888
fix(citygen): monuments published fake tokens and hid themselves
Aug 2, 2026
6b54192
docs: record the conventions a generator must not override
Aug 2, 2026
fab8c4a
docs: drop 1.8.0 fixes that were never user facing
Aug 2, 2026
658cec9
feat(citygen): DOWNTOWN layout — blocks cut into street-facing lots
Aug 2, 2026
3e2467d
fix(citygen): downtown blocks were all the same size
Aug 2, 2026
ed90e10
fix(citygen): downtown blocks were mostly empty in the middle
Aug 2, 2026
957a872
docs: refresh the lots.ts entry in the project structure
Aug 2, 2026
48feb77
docs: the layout selector offers six types, not four
Aug 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,55 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

---

## [1.8.0] - 2026-08-01

### Added

- **Drawn generation bounds** — the city generator gains a `DRAG_RECT` / `DRAW_AREA` toggle. `DRAW_AREA` traces a boundary the way water is drawn, and generation is confined to that shape: blocks centred outside it are dropped, road seams are clipped to it, and a footprint straddling its edge is rejected. A concave shape generates nothing in its notch, so an L or a crescent works as drawn. The traced outline stays on screen until GENERATE, unlike water, which saves immediately.
- **Street layouts** — a `LAYOUT` selector offering six distinct city types. Everything downstream of the block list is layout-agnostic, so a layout only has to produce blocks and the roads between them.
- **`GRID`** — two perpendicular families of streets with avenues every fourth line. Reads as Manhattan or Chicago, and is genuinely distinct from the default, which always produces *irregular* rectangles however it is tuned. That road hierarchy is most of what makes a grid look designed rather than generated.
- **`SUPERBLOCK`** — the same recursive split with a much larger floor: fewer roads, larger plots, open ground between them. Soviet microdistrict or corporate arcology.
- **`RING`** — a beltway city, San Antonio being the reference: concentric loop roads with elevated arterials running out from downtown. The corners of a square selection are left empty on purpose, because a ring city is round.
- **`ORGANIC_CELLS`** — a Voronoi diagram with streets along the cell boundaries. The only layout with no right angles in it: streets meet at odd angles and blocks are wedges and pentagons, which reads as a town that grew around footpaths rather than one a surveyor set out. Long cell boundaries become avenues, so the network gets a hierarchy without one being invented — the long runs across the diagram are the ones that would carry traffic anyway.
- **`DOWNTOWN`** — an elongated street grid whose blocks are cut into lots around their rim, facing the street, with the middle of the block left as back lots. Every other layout hands the generator one block per city block, so a block gets one structure — right for a tower in a park, wrong for a downtown, where what makes a dense city look dense is many narrow buildings shouldering together along the street. Roughly four times the buildings of `GRID` over the same ground. Block sizes vary rather than repeating one cell — short blocks, long ones, and the occasional enormous one where a street was never cut through, Washington Square being the obvious example — and the depth of the built-up rim varies with them, since a constant depth reads as a machine even when the frontages differ. A large block is ringed inward more than once so it reads as built out rather than hollow, stopping while there is still a back lot behind the buildings; a block too thin to have a rim and a middle becomes a terrace rather than one monolith the length of the block. `SUPERBLOCK` is deliberately left alone as the opposite idea rather than being turned into this.
- **`BSP`** stays the default and an unrecognised layout falls back to it, so existing generation is untouched and a stale saved option cannot produce an empty city.
- **Generated water** — a `WATER` selector offering a `RIVER` across the region, a `COAST` cutting one edge off, or a `LAKE` inside it. Rivers and coastlines are most of why real cities look like themselves: they force asymmetry, cut districts apart, and give bridges a reason to exist, which until now only happened if a GM had drawn water first. `NONE` is the default and doubles as the off switch, so generation produces water only when asked and a GM who wants to draw their own is never overruled.
- **Park ponds** — a `PARK_PONDS` toggle gives some parks water as well as trees, with the trees standing back from the edge. Separate from `WATER` because they are different scales of decision — a river reshapes the whole city, a pond is scenery in one plot — and either is wanted without the other. Off by default.
- **Roundabouts** — a `ROUNDABOUTS` selector (`OFF` / `SPARSE` / `NORMAL`) putting a circus where major roads meet. It is an overlay on the finished road network rather than another layout, so one implementation serves all five: approaches are cut back to the ring, the ring is laid, and the island gets a monument where there is room for one or a stand of trees otherwise. An empty disc reads as a hole in the network rather than a junction, so every island gets something.
- **Seeded generation** — an optional `SEED` field. The same seed over the same area with the same options rebuilds the same city, so a map can be recreated or shared as a short string. Leaving it blank rolls a fresh seed, and the seed actually used is reported back beneath the field rather than written into it.
- **`REGENERATE`** — clears the previous generation in the selected area and builds afresh, for iterating on a district without hand-deleting it first. It keeps anything the GM authored: named structures, tokens, battle-map content and hand-drawn water all survive. Plain `GENERATE` still adds to what is there.
- **`UNDO` on the generator panel** — the same server-side undo as the admin header, reachable without leaving the panel.
- The panel now stays open after generating, instead of dropping back to the main admin list — generation is something you do repeatedly while tuning.

### Changed

- **Road hierarchy, skyline taper and per-zone setbacks.** Road width is graded by split depth, so arterials read as arterials and side streets as side streets. Building height now blends continuously with distance from the centre instead of stepping in bands, which turned the skyline from flat plateaus with hard seams into a taper. Corporate plots leave forecourts and slums and markets build to the lot line, via a per-zone lot coverage applied after the aspect clamp.

### Fixed

- **Water bridges no longer pierce the buildings they pass over.** Placement deliberately ignores overpasses so the ground beneath a deck stays buildable — that is what stops an elevated road sterilising every block it crosses — but nothing then stopped a tower rising straight through one. Anything under a deck is now capped just below it, and where the deck is too low to build under at all, near its ramps, the building is dropped rather than squashed to nothing.

### Technical

- **A drawn boundary is a water polygon with the sign flipped** — water keeps what falls outside, a boundary keeps what falls inside. `clipSegmentToLand` was generalised into `clipSegmentToPolygons(seg, polys, keepInside)` so the two share one implementation and cannot drift; there is a test asserting they are exact inverses. `footprintOutsidePolygon` mirrors `footprintInWater` but is stricter: water asks whether a footprint touches water at all, a boundary asks whether all of it is inside.
- A boundary of fewer than three points cannot enclose an area and is treated as absent, falling back to the plain bounds rather than generating nothing and looking like a broken button.
- Skipping a block draws no randomness, so generation without a boundary splits byte-identically to before. Tests pin that at both the split and the whole-city level.
- **`RING` fills the disc with a single sub-layout and lays its arterials over the top.** A first version partitioned the disc into annular sectors and sub-laid each one, which produced a sparse, fragmented city — a sector's bounding box is far larger than the sector, so most of what each run generated fell outside its own region and was discarded. Density is now on par with the default over the same area.
- **`RING` elevates its spokes but leaves its loops on the ground.** A closed loop has no ends to ramp down at, so an elevated ring either never meets the street network or does so at one arbitrary point. Spoke ramps are sized as a fraction of the spoke rather than a fixed length, so both ends reach the ground however large the city is — a fixed ramp longer than half the deck leaves it ending in mid-air.
- Spokes run from the innermost loop outward rather than converging on a point, which removes a starburst of dead ground at the centre and is closer to how highways meet a downtown loop.
- `LayoutFn` may return overpasses alongside blocks and roads, and `generateCity` merges them with whatever bridges the water needed. `splitCity` gained an optional minimum block size rather than `SUPERBLOCK` being a parallel implementation.
- **`Block.lot` lets a layout subdivide a block itself.** The generator trims road padding off a block, clamps its aspect toward square and applies a per-zone setback — three rules that exist to turn a whole city block into one sensible plot. Applied to lots inside a subdivided block they pad the neighbours apart, square up the narrow frontages and pull the row back off the street, which is every ingredient of a street wall undone. A block flagged `lot` skips all three, its footprint being already decided. No existing layout sets it, so all five are untouched, and there is a test asserting that.
- **Three app-wide conventions a generator must not override.** None is what its name suggests, each is asserted in a test, and getting any of them wrong produces a structure that does not look like it belongs to the same city. `color: '#00ff00'` is not green, it is the sentinel meaning "inherit the theme" — the renderer resolves anything else verbatim, so naming a real colour opts a structure out of theming entirely. `polyCount: 5` is not a quality setting: everything is drawn as a wireframe, so the segment count *is* the look, and a 16-segment cylinder reads as a bright striped cage beside a city of pentagonal prisms. And `shape: 'rhombus'` is not an octahedron, it is a player or NPC token — `TOKEN_SHAPES` treats it as one on the server, a purge spares it as player content, and `OverlapChecker` publishes it in `activeRhombuses`, so a structure using one as a finial publishes a fake token inside itself.
- **A roundabout island is a tiny lake, as far as roads are concerned.** `clipSegmentToLand` already cuts a segment out of a polygon and leaves the approaches stopping at its edge, which is exactly what a junction does to the roads meeting it — so trimming reuses the water clipper rather than a second implementation, and the ring reuses the arc sampling `RING` uses for its beltways. Siting has to handle two kinds of junction: a BSP or Voronoi network joins at shared endpoints, but `GRID` lays each street as one full-length span, so its crossings share no endpoint and are found only by intersecting segments — `segmentCrossing` was promoted out of the water clipper for that. The whole pass runs *after* `consolidateRoads`, which snaps nearby endpoints together and would otherwise snap a ring of short segments into a blob.
- **A Voronoi cell is reduced to the largest rectangle that fits it.** `Block` is `{x, z, w, d}`, and the plot filler lays buildings out along a rectangle's axes — a pentagon has no axes to work with. Fitting a rectangle inside each cell keeps that 1180-line generator completely untouched while still delivering the irregular *street pattern*, which is where nearly all of the look comes from. The rectangle rarely fills its cell, so setbacks vary from plot to plot for free. Cells are built by half-plane clipping rather than a sweepline: for the hundred or so seeds a city needs it is fast enough, and Fortune's algorithm would be several hundred lines of beach line and event queue to save milliseconds nobody is waiting on. Seeds sit on a jittered lattice — a perfect lattice gives a honeycomb as machine-made as the grid, and fully random seeds clump into slivers too thin to build on.
- **Water is generated before the split; ponds after it.** The split is already water-aware, so generating a river first means the road grid stops at the banks of its own accord and bridges are sited from the stubs left there — generating it afterwards would mean cutting finished roads, which is a different and worse problem. A park pond is the opposite case: the park only exists once the split has produced the block it sits in. That is safe because a pond is contained by its plot and never reaches a road, and ponds are kept out of the water array the split, the shoreline roads and the bridge siting were built from. A test pins it: a ponded and an unponded run of one seed give identical roads and overpasses.
- **`water_bodies` gains a `generated` column**, so a regenerate can clear its own river without destroying a lake the GM drew. Existing rows default to `0`, so everything already on a map counts as hand-drawn. The migration runs on startup — a server on older code will accept generated water but store it as hand-drawn.
- **Seeding reached the buildings, not just the layout.** `cityGen/` had a single `Math.random` (the injected default), but `generateThemedBuildingsForPlot` had forty of its own, so a seed reproduced the street layout while the buildings on it changed every run. The rng is threaded through to the plot filler. It is deliberately not crypto-backed: 1.7.1 moved outcome-deciding rolls to OS entropy, and city layout is cosmetic.
- **`POST /purge-region`** clears a region's generated content in one transaction and emits a single update, rather than the panel issuing a delete per object. It distinguishes generated content from authored content by `isUserDefinedName`, token classification, battle-map membership and the new water flag.
- Region membership and counting moved out of `AdminPanel` into `cityGen/region.ts`, and seeding into `cityGen/rng.ts`, so both are testable without rendering the panel.

---

## [1.7.4] - 2026-07-28

### Added
Expand Down
Loading
Loading