@dreadwood-types/types 0.1.47 → 0.1.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/dist/adventure.d.ts +6 -6
  2. package/dist/adventure.d.ts.map +1 -1
  3. package/dist/appearance.d.ts +16 -6
  4. package/dist/appearance.d.ts.map +1 -1
  5. package/dist/appearance.js +43 -31
  6. package/dist/appearance.js.map +1 -1
  7. package/dist/areaTransitions.d.ts +1 -1
  8. package/dist/areaTransitions.d.ts.map +1 -1
  9. package/dist/areaTransitions.js +13 -0
  10. package/dist/areaTransitions.js.map +1 -1
  11. package/dist/bank.d.ts.map +1 -1
  12. package/dist/bearFloes.d.ts +83 -0
  13. package/dist/bearFloes.d.ts.map +1 -0
  14. package/dist/bearFloes.js +126 -0
  15. package/dist/bearFloes.js.map +1 -0
  16. package/dist/blacksmithing.d.ts +3 -0
  17. package/dist/blacksmithing.d.ts.map +1 -1
  18. package/dist/blacksmithing.js +81 -1
  19. package/dist/blacksmithing.js.map +1 -1
  20. package/dist/bloodRiver.d.ts +9 -6
  21. package/dist/bloodRiver.d.ts.map +1 -1
  22. package/dist/bloodRiver.js +19 -17
  23. package/dist/bloodRiver.js.map +1 -1
  24. package/dist/brokenRoad.d.ts.map +1 -1
  25. package/dist/capital.d.ts +541 -165
  26. package/dist/capital.d.ts.map +1 -1
  27. package/dist/capital.js +934 -163
  28. package/dist/capital.js.map +1 -1
  29. package/dist/charcoal.d.ts +43 -0
  30. package/dist/charcoal.d.ts.map +1 -0
  31. package/dist/charcoal.js +78 -0
  32. package/dist/charcoal.js.map +1 -0
  33. package/dist/clientConfig.d.ts +7 -2
  34. package/dist/clientConfig.d.ts.map +1 -1
  35. package/dist/clientConfig.js +14 -5
  36. package/dist/clientConfig.js.map +1 -1
  37. package/dist/constants.d.ts +196 -11
  38. package/dist/constants.d.ts.map +1 -1
  39. package/dist/constants.js +372 -21
  40. package/dist/constants.js.map +1 -1
  41. package/dist/cooking.d.ts.map +1 -1
  42. package/dist/cooking.js +2 -0
  43. package/dist/cooking.js.map +1 -1
  44. package/dist/dragonfire.d.ts +75 -0
  45. package/dist/dragonfire.d.ts.map +1 -0
  46. package/dist/dragonfire.js +73 -0
  47. package/dist/dragonfire.js.map +1 -0
  48. package/dist/dungeonCharts.d.ts +18 -0
  49. package/dist/dungeonCharts.d.ts.map +1 -0
  50. package/dist/dungeonCharts.js +67 -0
  51. package/dist/dungeonCharts.js.map +1 -0
  52. package/dist/equipment.d.ts +2 -0
  53. package/dist/equipment.d.ts.map +1 -1
  54. package/dist/equipment.js +37 -1
  55. package/dist/equipment.js.map +1 -1
  56. package/dist/frozenSet.d.ts +36 -0
  57. package/dist/frozenSet.d.ts.map +1 -0
  58. package/dist/frozenSet.js +58 -0
  59. package/dist/frozenSet.js.map +1 -0
  60. package/dist/goblinCave.d.ts +4 -4
  61. package/dist/goblinCave.d.ts.map +1 -1
  62. package/dist/goblinCave.js +1 -1
  63. package/dist/groundItems.d.ts.map +1 -1
  64. package/dist/groundItems.js +27 -0
  65. package/dist/groundItems.js.map +1 -1
  66. package/dist/harbour.d.ts +223 -0
  67. package/dist/harbour.d.ts.map +1 -0
  68. package/dist/harbour.js +264 -0
  69. package/dist/harbour.js.map +1 -0
  70. package/dist/iceDepths.d.ts +150 -0
  71. package/dist/iceDepths.d.ts.map +1 -0
  72. package/dist/iceDepths.js +132 -0
  73. package/dist/iceDepths.js.map +1 -0
  74. package/dist/index.d.ts +9 -0
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +9 -0
  77. package/dist/index.js.map +1 -1
  78. package/dist/interiorFurniture.d.ts +57 -0
  79. package/dist/interiorFurniture.d.ts.map +1 -0
  80. package/dist/interiorFurniture.js +159 -0
  81. package/dist/interiorFurniture.js.map +1 -0
  82. package/dist/landmarks.d.ts +83 -59
  83. package/dist/landmarks.d.ts.map +1 -1
  84. package/dist/landmarks.js +194 -134
  85. package/dist/landmarks.js.map +1 -1
  86. package/dist/messages.d.ts +77 -5
  87. package/dist/messages.d.ts.map +1 -1
  88. package/dist/messages.js.map +1 -1
  89. package/dist/mineCave.d.ts +7 -7
  90. package/dist/mineCave.d.ts.map +1 -1
  91. package/dist/mining.d.ts +8 -0
  92. package/dist/mining.d.ts.map +1 -1
  93. package/dist/mining.js +8 -0
  94. package/dist/mining.js.map +1 -1
  95. package/dist/questGuide.d.ts +30 -0
  96. package/dist/questGuide.d.ts.map +1 -0
  97. package/dist/questGuide.js +3 -0
  98. package/dist/questGuide.js.map +1 -0
  99. package/dist/quests.d.ts +29 -0
  100. package/dist/quests.d.ts.map +1 -1
  101. package/dist/quests.js +70 -0
  102. package/dist/quests.js.map +1 -1
  103. package/dist/snow.d.ts +2 -2
  104. package/dist/snow.d.ts.map +1 -1
  105. package/dist/snow.js +8 -5
  106. package/dist/snow.js.map +1 -1
  107. package/dist/spiderCave.d.ts.map +1 -1
  108. package/dist/travel.d.ts +7 -4
  109. package/dist/travel.d.ts.map +1 -1
  110. package/dist/travel.js +20 -4
  111. package/dist/travel.js.map +1 -1
  112. package/dist/woodwork.d.ts +8 -2
  113. package/dist/woodwork.d.ts.map +1 -1
  114. package/dist/woodwork.js +13 -3
  115. package/dist/woodwork.js.map +1 -1
  116. package/dist/xp.d.ts +18 -7
  117. package/dist/xp.d.ts.map +1 -1
  118. package/dist/xp.js +20 -9
  119. package/dist/xp.js.map +1 -1
  120. package/dist/zones.d.ts +5 -0
  121. package/dist/zones.d.ts.map +1 -1
  122. package/dist/zones.js +8 -3
  123. package/dist/zones.js.map +1 -1
  124. package/package.json +1 -1
package/dist/capital.js CHANGED
@@ -2,22 +2,47 @@
2
2
  * THE CAPITAL
3
3
  *
4
4
  * The walled capital of the northern kingdoms, in the open green east of the
5
- * Thaw, redesigned 2026-09-18 from James' picture and brief: a fortress
6
- * city on one axis. The Royal Way runs west to east from the main gate
7
- * straight at the keep, which stands on a raised mound at the east end so
8
- * it is above the roofline from the moment you step off the bridge. Every
9
- * district is a block off that axis, packed with terraced rowhouses; the
10
- * only lawns are the temple cloister and the noble gardens, both walled.
5
+ * Thaw. Redesigned 2026-09-25 from James's masterplan and district sheets
6
+ * (design-references/capital-*; the layout spec and its checking plan script
7
+ * are summarised in the phase-1 commit). The plan in one breath:
8
+ *
9
+ * - One north-south spine, the Royal Way (u 149-155), from the North Gate
10
+ * past Victory Plaza, the Crown Cross and the Noble Cross to the South
11
+ * Gate. Both of those gates are built and barred.
12
+ * - Two east-west primaries: Crown Street (v 62-66) the full width of the
13
+ * city on the painted road, and the King's Road (v 82-86, the keep axis,
14
+ * CAPITAL_AVENUE_Z) from the Great Gate in the west wall to the Crown
15
+ * Cross and up the ramp to the keep. The Harbour Road runs the same rows
16
+ * from the East Gate to the mound's foot.
17
+ * - The keep on its mound east of the Way, still facing west; the districts
18
+ * where the masterplan paints them: Trade NW, Temple Gardens NE, the
19
+ * Westgate Outpost, Forge and Military Ward west of the Way, the Keep and
20
+ * the Royal Quarter centre-east, the Noble Quarter SE.
21
+ *
22
+ * CITY-LOCAL TILES. Every plan table below is written in city-local tiles,
23
+ * `u = x - CAPITAL_MIN_X` (0..280, west to east) and `v = z - CAPITAL_NORTH_Z`
24
+ * (0..168, north to south): the walls are u 0-1, u 279-280, v 0-1 and
25
+ * v 167-168, the buildable interior u 2-278, v 2-166. `cu()` / `cv()` turn
26
+ * them into world tiles; every row also carries its world rect or tile, so
27
+ * nothing downstream has to convert by hand. Never write a world literal for
28
+ * a Capital coordinate: the map has already moved under the city once.
11
29
  *
12
30
  * World tiles; the north pad is part of the grid, so no toWorldZ.
13
31
  * Backend builder: capitalCity.ts. Assets: capitalAssets.ts (keep, statue,
14
- * fountain), capitalFabric.ts (walls, gatehouse, bridge, buildings, props).
32
+ * fountain), capitalFabric.ts (walls, gatehouse, bridge, buildings, props),
33
+ * buildingModels.ts (the Blender buildings, kind `capitalbuilding`).
15
34
  */
35
+ import { CASTLE_KEEP_HALF_X, CASTLE_KEEP_HALF_Z, CASTLE_PASSAGE_HALF_W, SurfaceType, legacyZ, statueStyleWithPlinth } from './constants.js';
36
+ import { BANK_RANGE } from './bank.js';
37
+ import { BLACKSMITH_RANGE, TEMPLE_PRAYER_RANGE, TRAINING_DUMMY_RANGE } from './blacksmithing.js';
38
+ import { CHARCOAL_MOUND_RANGE } from './charcoal.js';
39
+ import { CAMPFIRE_COOK_RANGE } from './cooking.js';
40
+ import { DEADLANDS_MIN_Z } from './zones.js';
16
41
  /** The outer wall: two tiles thick, these are the OUTER rows. */
17
42
  export const CAPITAL_MIN_X = 480;
18
43
  export const CAPITAL_MAX_X = 760;
19
- export const CAPITAL_NORTH_Z = 12;
20
- export const CAPITAL_SOUTH_Z = 180;
44
+ export const CAPITAL_NORTH_Z = 76;
45
+ export const CAPITAL_SOUTH_Z = 244;
21
46
  export const CAPITAL_WALL_THICK = 2;
22
47
  export const CAPITAL_BASTION_EVERY = 16;
23
48
  /** Inside the wall: the first row a building can stand on. */
@@ -25,36 +50,127 @@ export const CAPITAL_INNER_MIN_X = CAPITAL_MIN_X + CAPITAL_WALL_THICK;
25
50
  export const CAPITAL_INNER_MAX_X = CAPITAL_MAX_X - CAPITAL_WALL_THICK;
26
51
  export const CAPITAL_INNER_NORTH_Z = CAPITAL_NORTH_Z + CAPITAL_WALL_THICK;
27
52
  export const CAPITAL_INNER_SOUTH_Z = CAPITAL_SOUTH_Z - CAPITAL_WALL_THICK;
53
+ /* ------------------------------------------------------------------ *
54
+ * City-local tiles.
55
+ * ------------------------------------------------------------------ */
56
+ /** City-local column to world x: u 0 is the west wall's outer row. */
57
+ export function cu(u) {
58
+ return CAPITAL_MIN_X + u;
59
+ }
60
+ /** City-local row to world z: v 0 is the north wall's outer row. */
61
+ export function cv(v) {
62
+ return CAPITAL_NORTH_Z + v;
63
+ }
64
+ /** World x back to a city-local column. */
65
+ export function capU(x) {
66
+ return x - CAPITAL_MIN_X;
67
+ }
68
+ /** World z back to a city-local row. */
69
+ export function capV(z) {
70
+ return z - CAPITAL_NORTH_Z;
71
+ }
72
+ /** A city-local tile as a world tile. */
73
+ export function capWorld(u, v) {
74
+ return { x: cu(u), z: cv(v) };
75
+ }
76
+ /** A city-local rect (inclusive) as a world rect (inclusive). */
77
+ export function capWorldRect(r) {
78
+ return { minX: cu(r.u0), maxX: cu(r.u1), minZ: cv(r.v0), maxZ: cv(r.v1) };
79
+ }
80
+ /** The city's size in tiles, outer row to outer row. */
81
+ const SPAN_U = CAPITAL_MAX_X - CAPITAL_MIN_X; // 280
82
+ const SPAN_V = CAPITAL_SOUTH_Z - CAPITAL_NORTH_Z; // 168
83
+ /** The buildable interior, city-local (u 2-278, v 2-166). */
84
+ const IN_U0 = CAPITAL_WALL_THICK;
85
+ const IN_U1 = SPAN_U - CAPITAL_WALL_THICK;
86
+ const IN_V0 = CAPITAL_WALL_THICK;
87
+ const IN_V1 = SPAN_V - CAPITAL_WALL_THICK;
88
+ /** The plan's axes, city-local. */
89
+ const WAY_U = 152; // the Royal Way's centre column
90
+ const AVENUE_V = 84; // the King's Road, the keep axis, the Harbour Road
91
+ const CROWN_V = 64; // Crown Street
92
+ const NOBLE_V = 104; // the Noble Walk
93
+ const KEEP_U = 196; // the keep's centre tile
94
+ const PLATEAU_U0 = 166; // the plateau's west edge (the curtain)
95
+ const PLATEAU_U1 = 214; // the plateau's east edge
96
+ /* ------------------------------------------------------------------ *
97
+ * Walls, moat, heights, works.
98
+ * ------------------------------------------------------------------ */
28
99
  /** The moat: eight tiles of water outside the wall; the north side runs into the sea. */
29
100
  export const CAPITAL_MOAT_W = 8;
30
101
  export const CAPITAL_MOAT_MIN_X = CAPITAL_MIN_X - CAPITAL_MOAT_W;
31
102
  export const CAPITAL_MOAT_MAX_X = CAPITAL_MAX_X + CAPITAL_MOAT_W;
32
- export const CAPITAL_MOAT_NORTH_Z = 3;
103
+ export const CAPITAL_MOAT_NORTH_Z = CAPITAL_NORTH_Z - 9;
33
104
  export const CAPITAL_MOAT_SOUTH_Z = CAPITAL_SOUTH_Z + CAPITAL_MOAT_W;
105
+ /** The King's Road's rows: the keep axis, the Great Gate and the East Gate. 160 after the northern extension. */
106
+ export const CAPITAL_AVENUE_Z = cv(AVENUE_V);
107
+ export const CAPITAL_AVENUE_HALF_W = 2;
34
108
  /** The basin before the main gate: the moat widened west so the bridge is long. */
35
- export const CAPITAL_POOL = { minX: 456, maxX: CAPITAL_MOAT_MIN_X - 1, minZ: 74, maxZ: 118 };
109
+ export const CAPITAL_POOL = {
110
+ minX: CAPITAL_MIN_X - 24,
111
+ maxX: CAPITAL_MOAT_MIN_X - 1,
112
+ minZ: CAPITAL_AVENUE_Z - 22,
113
+ maxZ: CAPITAL_AVENUE_Z + 22,
114
+ };
36
115
  /** Ground inside the walls; the bank outside the moat; the water. */
37
116
  export const CAPITAL_FLOOR_H = 2;
38
117
  export const CAPITAL_BANK_H = 1;
39
118
  export const CAPITAL_MOAT_H = 0;
40
119
  /** Everything the city owns, for the sweep, the levelling and the discovery zone. */
41
- export const CAPITAL_WORKS = { minX: 436, maxX: CAPITAL_MOAT_MAX_X + 8, minZ: 3, maxZ: 191 };
42
- /** The Royal Way: five wide on the gate's rows, the keep's axis. */
43
- export const CAPITAL_AVENUE_Z = 96;
44
- export const CAPITAL_AVENUE_HALF_W = 2;
45
- /** The main gate in the west wall, the three side gates. Passage is five wide. */
120
+ export const CAPITAL_WORKS = {
121
+ minX: CAPITAL_MIN_X - 44,
122
+ maxX: CAPITAL_MOAT_MAX_X + 8,
123
+ minZ: CAPITAL_MOAT_NORTH_Z,
124
+ maxZ: CAPITAL_MOAT_SOUTH_Z + 3,
125
+ };
126
+ /* ------------------------------------------------------------------ *
127
+ * The axes and the gates.
128
+ * ------------------------------------------------------------------ */
129
+ /** The Royal Way: seven wide (u 149-155, Path kerbs on its outer columns), a grass verge four wide either side. */
130
+ export const CAPITAL_ROYAL_WAY_X = cu(WAY_U);
131
+ export const CAPITAL_ROYAL_WAY_HALF_W = 3;
132
+ export const CAPITAL_ROYAL_WAY_VERGE = 4;
133
+ /** Crown Street, five wide on the painted road (v 62-66). */
134
+ export const CAPITAL_CROWN_Z = cv(CROWN_V);
135
+ /** The Noble Walk, five wide along the mound's south foot (v 102-106). */
136
+ export const CAPITAL_NOBLE_WALK_Z = cv(NOBLE_V);
137
+ /** Primary routes are five wide: half-width two, Stone with Path kerb rows. */
138
+ export const CAPITAL_PRIMARY_HALF_W = 2;
139
+ /** Every gate passage is five wide. */
46
140
  export const CAPITAL_GATE_HALF_W = 2;
141
+ /** The Great Gate in the west wall, open: the land arrival (decision D1-B, unchanged). */
47
142
  export const CAPITAL_MAIN_GATE = { x: CAPITAL_MIN_X, z: CAPITAL_AVENUE_Z };
143
+ /** The East Gate, open: the harbour's Dock Gate. */
48
144
  export const CAPITAL_EAST_GATE = { x: CAPITAL_MAX_X, z: CAPITAL_AVENUE_Z };
49
- export const CAPITAL_NORTH_GATE = { x: 620, z: CAPITAL_NORTH_Z };
50
- export const CAPITAL_SOUTH_GATE = { x: 620, z: CAPITAL_SOUTH_Z };
145
+ /** The North Gate at the Way's north end: built, barred (the sea is beyond it). */
146
+ export const CAPITAL_NORTH_GATE = { x: CAPITAL_ROYAL_WAY_X, z: CAPITAL_NORTH_Z };
147
+ /**
148
+ * The South Gate at the Way's south end, the Dreadwoods gate. Open since
149
+ * James's word on D2 (2026-09-25, "yes we should add a pathway to
150
+ * dreadwoods"): the South Bridge carries the Way over the moat to the
151
+ * Dreadwoods north gate, straight into the PvP woods.
152
+ */
153
+ export const CAPITAL_SOUTH_GATE = { x: CAPITAL_ROYAL_WAY_X, z: CAPITAL_SOUTH_Z };
154
+ /**
155
+ * The four city gates as the builder places them. `style` is the gatehouse
156
+ * style (constants.ts `visitGatehouseOffsets`): a `shut` token keeps the
157
+ * passage blocked in the shared collision, so a barred gate needs nothing
158
+ * else from the builder, and the wall rows under it stay unpaved. The
159
+ * facing letter is always the LAST token (the frontend turns by it).
160
+ */
161
+ export const CAPITAL_GATES = [
162
+ { id: 'west', name: 'The Great Gate', x: CAPITAL_MAIN_GATE.x, z: CAPITAL_MAIN_GATE.z, style: 'main-w', open: true },
163
+ { id: 'east', name: 'The East Gate', x: CAPITAL_EAST_GATE.x, z: CAPITAL_EAST_GATE.z, style: 'side-e', open: true },
164
+ { id: 'north', name: 'The North Gate', x: CAPITAL_NORTH_GATE.x, z: CAPITAL_NORTH_GATE.z, style: 'main-shut-n', open: false },
165
+ { id: 'south', name: 'The South Gate', x: CAPITAL_SOUTH_GATE.x, z: CAPITAL_SOUTH_GATE.z, style: 'main-s', open: true },
166
+ ];
167
+ /** The first interior tile inside each open gate's passage, where the walks are measured from. */
168
+ export const CAPITAL_WEST_MOUTH = { u: IN_U0, v: AVENUE_V, x: cu(IN_U0), z: cv(AVENUE_V) };
169
+ export const CAPITAL_EAST_MOUTH = { u: IN_U1, v: AVENUE_V, x: cu(IN_U1), z: cv(AVENUE_V) };
51
170
  /** The bridge over the pool: seven rows, the outer two carry the parapet and the knights. */
52
171
  export const CAPITAL_BRIDGE_MIN_X = CAPITAL_POOL.minX - 2;
53
172
  /** The bridge's crown: the middle third of the deck stands one height up. */
54
173
  export const CAPITAL_BRIDGE_CROWN_H = CAPITAL_FLOOR_H + 1;
55
- /** Small plazas where the cross streets meet the Royal Way: the Way widened by two rows each side. */
56
- export const CAPITAL_WAY_PLAZA_HALF_X = 4;
57
- export const CAPITAL_WAY_PLAZA_HALF_Z = CAPITAL_AVENUE_HALF_W + 2;
58
174
  export const CAPITAL_BRIDGE_MAX_X = CAPITAL_MIN_X - 1;
59
175
  export const CAPITAL_BRIDGE_HALF_W = 3;
60
176
  /** The barbican on the far bank, astride the road before the bridge. */
@@ -65,114 +181,703 @@ export const CAPITAL_BRIDGE_KNIGHTS = [
65
181
  { x: CAPITAL_BRIDGE_MIN_X + 17, z: CAPITAL_AVENUE_Z - CAPITAL_BRIDGE_HALF_W, facing: 's' },
66
182
  { x: CAPITAL_BRIDGE_MIN_X + 17, z: CAPITAL_AVENUE_Z + CAPITAL_BRIDGE_HALF_W, facing: 'n' },
67
183
  ];
68
- /** The ring road inside the wall, and the street grid. Streets are three wide about these lines. */
184
+ /**
185
+ * The South Bridge (D2): the Way's seven rows carried on over the south moat,
186
+ * from the South Gate's passage to the grass strip under the Dreadwoods
187
+ * north wall. The deck is terrain at the city floor, the parapets run along
188
+ * its east and west edges.
189
+ */
190
+ export const CAPITAL_SOUTH_BRIDGE = {
191
+ minX: CAPITAL_ROYAL_WAY_X - CAPITAL_BRIDGE_HALF_W,
192
+ maxX: CAPITAL_ROYAL_WAY_X + CAPITAL_BRIDGE_HALF_W,
193
+ minZ: CAPITAL_SOUTH_Z + 1,
194
+ maxZ: DEADLANDS_MIN_Z - 1,
195
+ };
196
+ /** The rows that cross water (the moat); the rest of the bridge is its landing on the strip. */
197
+ export const CAPITAL_SOUTH_BRIDGE_DECK_MAX_Z = CAPITAL_MOAT_SOUTH_Z;
198
+ export function onCapitalSouthBridge(x, z) {
199
+ return x >= CAPITAL_SOUTH_BRIDGE.minX && x <= CAPITAL_SOUTH_BRIDGE.maxX && z >= CAPITAL_SOUTH_BRIDGE.minZ && z <= CAPITAL_SOUTH_BRIDGE.maxZ;
200
+ }
201
+ /** The Dreadwoods gate the South Bridge meets: five tiles cut from the woods' north wall on the Way's line. */
202
+ export const DREADWOODS_NORTH_GATE_X = CAPITAL_ROYAL_WAY_X;
203
+ export const DREADWOODS_NORTH_GATE_HALF_W = 2;
204
+ export function inDreadwoodsNorthGate(x, z) {
205
+ return z === DEADLANDS_MIN_Z && Math.abs(x - DREADWOODS_NORTH_GATE_X) <= DREADWOODS_NORTH_GATE_HALF_W;
206
+ }
207
+ /** The road in, gate throat included, until it gives out in the ash. */
208
+ export const DREADWOODS_NORTH_ROAD_INNER_TILES = 8;
209
+ export function inDreadwoodsNorthRoad(x, z) {
210
+ return (Math.abs(x - DREADWOODS_NORTH_GATE_X) <= DREADWOODS_NORTH_GATE_HALF_W &&
211
+ z >= DEADLANDS_MIN_Z &&
212
+ z <= DEADLANDS_MIN_Z + DREADWOODS_NORTH_ROAD_INNER_TILES);
213
+ }
214
+ /** The ring lane inside the wall: three wide, one tile in (u 2-4, u 276-278, v 2-4, v 164-166). */
69
215
  export const CAPITAL_RING_INSET = 1;
70
216
  export const CAPITAL_STREET_HALF_W = 1;
71
- export const CAPITAL_CROSS_STREET_XS = [540, 580, 620, 660];
217
+ /* ------------------------------------------------------------------ *
218
+ * The mound and the keep.
219
+ * ------------------------------------------------------------------ */
72
220
  /**
73
- * The primary routes (the readability pass, 2026-09-19): five wide with
74
- * kerb rows like the Way, so the player reads them as the way to somewhere.
75
- * The Way itself goes gate to keep; these go Way to market, Way to the
76
- * garrison, Way to the temple (the cross street and the Temple Walk along
77
- * the square's south side). Every other street is three wide.
221
+ * The keep's mound: a plateau at CAPITAL_MOUND_H with a slope of one height
222
+ * a tile all round it (u 160-220, v 66-102), the King's Road climbing the
223
+ * west slope as the ramp (u 160-165) to the Inner Gate. Crown Street runs on
224
+ * its north foot, the Noble Walk on its south foot, the Harbour Road stops at
225
+ * its east foot.
78
226
  */
79
- export const CAPITAL_PRIMARY_HALF_W = 2;
80
- export const CAPITAL_PRIMARY_ROUTES = [
81
- { x: 540, min: 14, max: 96 },
82
- { x: 620, min: 96, max: 178 },
83
- { x: 660, min: 14, max: 96 },
84
- { z: 76, min: 660, max: 716 },
227
+ export const CAPITAL_MOUND_H = 8;
228
+ export const CAPITAL_PLATEAU = {
229
+ minX: cu(PLATEAU_U0),
230
+ maxX: cu(PLATEAU_U1),
231
+ minZ: CAPITAL_AVENUE_Z - 12,
232
+ maxZ: CAPITAL_AVENUE_Z + 12,
233
+ };
234
+ /** The inner gatehouse on the plateau's west edge; the curtain runs round the edge with this one gap. */
235
+ export const CAPITAL_INNER_GATE = { x: CAPITAL_PLATEAU.minX + 2, z: CAPITAL_AVENUE_Z };
236
+ /**
237
+ * The keep, thirty tiles in from the plateau's west edge, door west onto the
238
+ * Audience Court (decision D3 default). Its half-extents are constants.ts's
239
+ * CASTLE_KEEP_HALF_X/Z (the collision and the asset read those).
240
+ */
241
+ export const CAPITAL_KEEP = { x: cu(KEEP_U), z: CAPITAL_AVENUE_Z };
242
+ /** The King's Statue in the Audience Court, between the Inner Gate and the keep door. */
243
+ export const CAPITAL_KING_STATUE = { x: CAPITAL_PLATEAU.minX + 10, z: CAPITAL_AVENUE_Z };
244
+ /** The King's Fountain on the Way's axis in the Crown Cross; the Way splits round it. */
245
+ export const CAPITAL_KINGS_FOUNTAIN = { x: CAPITAL_ROYAL_WAY_X, z: cv(74) };
246
+ /**
247
+ * The stand in the Drill Square with a clear three-tile ring round it: the
248
+ * outfit capture pass, walk-capture and the smokes' closing /tp use it.
249
+ */
250
+ export const CAPITAL_DRILL_STAND = { x: cu(140), z: cv(116) };
251
+ /** The road in: from the Thaw arch's east road, round to the barbican. */
252
+ export const CAPITAL_ROAD = [
253
+ { x: CAPITAL_MIN_X - 194, z: CAPITAL_AVENUE_Z - 46 },
254
+ { x: CAPITAL_MIN_X - 150, z: CAPITAL_AVENUE_Z - 44 },
255
+ { x: CAPITAL_MIN_X - 90, z: CAPITAL_AVENUE_Z - 26 },
256
+ { x: CAPITAL_MIN_X - 50, z: CAPITAL_AVENUE_Z - 4 },
257
+ { x: CAPITAL_BARBICAN.x - 4, z: CAPITAL_AVENUE_Z },
258
+ ];
259
+ function capRect(id, u0, u1, v0, v1) {
260
+ return { id, u0, u1, v0, v1, minX: cu(u0), maxX: cu(u1), minZ: cv(v0), maxZ: cv(v1) };
261
+ }
262
+ function capTile(u, v) {
263
+ return { u, v, x: cu(u), z: cv(v) };
264
+ }
265
+ export function capitalSurface(ground) {
266
+ switch (ground) {
267
+ case 'stone':
268
+ case 'flag':
269
+ return SurfaceType.Stone;
270
+ case 'setts':
271
+ case 'path':
272
+ case 'gravel':
273
+ return SurfaceType.Path;
274
+ case 'dirt':
275
+ return SurfaceType.Dirt;
276
+ default:
277
+ return SurfaceType.Grass;
278
+ }
279
+ }
280
+ function route(id, u0, u1, v0, v1, spec) {
281
+ return { ...capRect(id, u0, u1, v0, v1), ...spec };
282
+ }
283
+ const LAMPS16 = { every: 16, alternate: true };
284
+ export const CAPITAL_ROUTES = [
285
+ // The spine and the primaries.
286
+ route('royal-way', WAY_U - CAPITAL_ROYAL_WAY_HALF_W, WAY_U + CAPITAL_ROYAL_WAY_HALF_W, IN_V0, IN_V1, { name: 'The Royal Way', kind: 'way', surface: 'stone', kerb: true, verge: CAPITAL_ROYAL_WAY_VERGE }),
287
+ route('crown-street-w', 5, 140, CROWN_V - 2, CROWN_V + 2, { name: 'Crown Street', kind: 'primary', surface: 'stone', kerb: true, lamps: LAMPS16 }),
288
+ route('crown-street-e', 160, 225, CROWN_V - 2, CROWN_V + 2, { name: 'Crown Street', kind: 'primary', surface: 'stone', kerb: true, lamps: LAMPS16 }),
289
+ route('kings-road', IN_U0, 148, AVENUE_V - 2, AVENUE_V + 2, { name: "The King's Road", kind: 'primary', surface: 'stone', kerb: true, lamps: LAMPS16 }),
290
+ route('kings-road-ramp', 160, PLATEAU_U0 - 1, AVENUE_V - 2, AVENUE_V + 2, { name: "The King's Road", kind: 'primary', surface: 'stone', kerb: true }),
291
+ route('harbour-road', 221, IN_U1, AVENUE_V - 2, AVENUE_V + 2, { name: 'The Harbour Road', kind: 'primary', surface: 'stone', kerb: true, lamps: LAMPS16 }),
292
+ // The ring lane: the guards' beat and the back way round.
293
+ route('ring-w', IN_U0, IN_U0 + 2, IN_V0, IN_V1, { name: 'The Ring Lane', kind: 'lane', surface: 'path' }),
294
+ route('ring-e', IN_U1 - 2, IN_U1, IN_V0, IN_V1, { name: 'The Ring Lane', kind: 'lane', surface: 'path' }),
295
+ route('ring-n', IN_U0, IN_U1, IN_V0, IN_V0 + 2, { name: 'The Ring Lane', kind: 'lane', surface: 'path' }),
296
+ route('ring-s', IN_U0, IN_U1, IN_V1 - 2, IN_V1, { name: 'The Ring Lane', kind: 'lane', surface: 'path' }),
297
+ // Westgate.
298
+ route('cooks-lane', 20, 25, 67, 77, { name: "Cook's Lane", kind: 'lane', surface: 'path' }),
299
+ route('outpost-lane', 30, 32, 99, 116, { name: 'Outpost Lane', kind: 'lane', surface: 'path' }),
300
+ // Forge and Military.
301
+ route('colliers-lane', 91, 93, 107, 119, { name: "Collier's Lane", kind: 'lane', surface: 'dirt' }),
302
+ route('forge-lane', 45, 104, 120, 122, { name: 'Forge Lane', kind: 'lane', surface: 'dirt' }),
303
+ route('timber-path', 55, 57, 112, 119, { name: 'Timber Yard path', kind: 'walk', surface: 'dirt' }),
304
+ route('drill-lane', 105, 107, 87, 163, { name: 'Drill Lane', kind: 'lane', surface: 'stone' }),
305
+ // Temple Gardens.
306
+ route('temple-walk', 164, 262, 29, 31, { name: 'Temple Walk', kind: 'walk', surface: 'gravel' }),
307
+ route('graveyard-path', 191, 195, 34, 37, { name: 'Graveyard path', kind: 'walk', surface: 'gravel' }),
308
+ route('shrine-walk', 216, 220, 38, 61, { name: 'Shrine Walk', kind: 'walk', surface: 'flag' }),
309
+ route('memorial-path', 252, 256, 32, 37, { name: 'Memorial path', kind: 'walk', surface: 'gravel' }),
310
+ route('cloister-path', 249, 251, 20, 28, { name: 'Cloister Garth path', kind: 'walk', surface: 'gravel' }),
311
+ // The Keep and the Royal Quarter.
312
+ route('keep-east-walk', 221, 225, 67, 101, { name: 'Keep East Walk', kind: 'walk', surface: 'stone' }),
313
+ // Noble Quarter.
314
+ route('noble-walk', 160, 276, NOBLE_V - 2, NOBLE_V + 2, { name: 'The Noble Walk', kind: 'walk', surface: 'gravel' }),
315
+ route('manor-walk', 166, 250, 131, 137, { name: 'Manor Walk', kind: 'walk', surface: 'gravel' }),
316
+ route('garden-walk', 203, 213, 107, 163, { name: 'Garden Walk', kind: 'walk', surface: 'gravel' }),
317
+ route('blue-garden-path', 181, 183, 124, 129, { name: 'Blue Manor garden path', kind: 'walk', surface: 'gravel' }),
318
+ route('green-garden-path', 233, 235, 124, 129, { name: 'Green Manor garden path', kind: 'walk', surface: 'gravel' }),
319
+ route('red-garden-path', 181, 183, 139, 146, { name: 'Red Manor garden path', kind: 'walk', surface: 'gravel' }),
320
+ route('purple-garden-path', 233, 235, 139, 146, { name: 'Purple Manor garden path', kind: 'walk', surface: 'gravel' }),
321
+ ];
322
+ function plaza(id, u0, u1, v0, v1, spec) {
323
+ return { ...capRect(id, u0, u1, v0, v1), ...spec };
324
+ }
325
+ export const CAPITAL_PLAZAS = [
326
+ // Westgate.
327
+ plaza('gate-square', 5, 44, 78, 98, { name: 'Gate Square', surface: 'stone' }),
328
+ plaza('gate-apron-w', 9, 19, 99, 99, { name: 'Gate Square aprons', surface: 'stone', apron: true, forLandmark: 'wg-guard' }),
329
+ plaza('gate-apron-depot', 19, 29, 99, 99, { name: 'Gate Square aprons', surface: 'stone', apron: true, forLandmark: 'wg-depot' }),
330
+ plaza('gate-apron-e', 33, 43, 99, 99, { name: 'Gate Square aprons', surface: 'stone', apron: true, forLandmark: 'wg-ranger' }),
331
+ // Trade.
332
+ plaza('warehouse-apron', 31, 37, 11, 17, { name: 'Warehouse apron', surface: 'setts', apron: true }),
333
+ plaza('market-square', 42, 92, 16, 52, { name: 'Market Square', surface: 'setts' }),
334
+ plaza('market-link-w', 38, 41, 28, 50, { name: 'Shop fronts', surface: 'setts', apron: true }),
335
+ plaza('market-link-e', 93, 96, 27, 41, { name: 'Coin Square link', surface: 'setts', apron: true }),
336
+ plaza('warehouse-link-w', 38, 41, 11, 17, { name: 'Warehouse link', surface: 'setts', apron: true }),
337
+ plaza('warehouse-link-n', 42, 60, 12, 15, { name: 'Warehouse link', surface: 'setts', apron: true }),
338
+ plaza('market-gate-walk', 64, 70, 53, 56, { name: 'Market Gate walk', surface: 'setts', apron: true }),
339
+ plaza('shop-aprons', 35, 37, 28, 50, { name: 'Shop fronts', surface: 'setts', apron: true }),
340
+ plaza('coin-square', 97, 140, 27, 41, { name: 'Coin Square', surface: 'stone' }),
341
+ plaza('food-court', 17, 40, 50, 61, { name: 'Food Court', surface: 'setts' }),
342
+ plaza('market-gate-apron', 60, 74, 57, 61, { name: 'Market Gate', surface: 'setts', apron: true }),
343
+ // The Royal Way.
344
+ plaza('north-gate-court', 141, 163, 4, 10, { name: 'North Gate court', surface: 'stone' }),
345
+ plaza('victory-promenade-w', 141, 144, 11, 58, { name: 'Victory Plaza', surface: 'stone' }),
346
+ plaza('victory-promenade-e', 160, 163, 11, 58, { name: 'Victory Plaza', surface: 'stone' }),
347
+ plaza('victory-plinth-n', 145, 159, 15, 25, { name: 'Victory Plaza', surface: 'stone' }),
348
+ plaza('victory-plinth-s', 145, 159, 42, 50, { name: 'Victory Plaza', surface: 'stone' }),
349
+ plaza('crown-cross', 141, 159, 60, 90, { name: 'The Crown Cross', surface: 'stone' }),
350
+ plaza('noble-cross', 145, 159, 99, 109, { name: 'The Noble Cross', surface: 'stone' }),
351
+ plaza('south-gate-court', 141, 163, 154, 163, { name: 'South Gate court', surface: 'stone' }),
352
+ // Forge.
353
+ plaza('anvil-square', 59, 89, 87, 106, { name: 'Anvil Square', surface: 'stone' }),
354
+ plaza('artisan-apron', 58, 66, 123, 123, { name: 'Forge Lane aprons', surface: 'stone', apron: true, forLandmark: 'fg-artisan' }),
355
+ plaza('alchemy-apron', 82, 90, 123, 123, { name: 'Forge Lane aprons', surface: 'stone', apron: true, forLandmark: 'fg-alchemy' }),
356
+ // Military.
357
+ plaza('barracks-apron', 108, 140, 87, 87, { name: 'Barracks apron', surface: 'stone', apron: true }),
358
+ plaza('drill-square', 108, 144, 101, 121, { name: 'Drill Square', surface: 'stone' }),
359
+ // Temple Gardens.
360
+ plaza('temple-close', 176, 208, 27, 33, { name: 'Temple Close', surface: 'flag' }),
361
+ plaza('garden-hub', 211, 225, 24, 37, { name: 'Garden Hub', surface: 'flag' }),
362
+ plaza('shrine-apron', 221, 222, 49, 55, { name: 'Healing Shrine apron', surface: 'flag', apron: true }),
363
+ plaza('guild-apron', 164, 164, 39, 49, { name: 'Guild Hall apron', surface: 'stone', apron: true }),
364
+ // The Keep and the Royal Quarter.
365
+ plaza('audience-court', 171, 183, 73, 95, { name: 'The Audience Court', surface: 'stone' }),
366
+ plaza('royal-square', 226, 255, 62, 81, { name: 'Royal Square', surface: 'stone' }),
367
+ plaza('east-gate-court', 256, IN_U1, 77, 94, { name: 'East Gate court', surface: 'stone' }),
368
+ plaza('townhouse-apron-w', 232, 240, 87, 88, { name: 'Townhouse aprons', surface: 'stone', apron: true, forLandmark: 'kp-town1' }),
369
+ plaza('townhouse-apron-e', 244, 252, 87, 88, { name: 'Townhouse aprons', surface: 'stone', apron: true, forLandmark: 'kp-town2' }),
370
+ // Noble Quarter.
371
+ plaza('fountain-plaza', 202, 214, 128, 140, { name: 'Fountain Plaza', surface: 'gravel' }),
372
+ // Door aprons laid last in the plan.
373
+ plaza('mainforge-apron', 62, 78, 81, 81, { name: 'Main Forge apron', surface: 'stone', apron: true, forLandmark: 'fg-mainforge' }),
374
+ plaza('stables-apron', 15, 31, 118, 118, { name: 'Stables apron', surface: 'stone', apron: true, forLandmark: 'wg-stables' }),
375
+ plaza('store-apron-w', 109, 115, 74, 81, { name: 'Garrison Store apron', surface: 'stone', apron: true, forLandmark: 'ml-store1' }),
376
+ plaza('store-apron-e', 129, 135, 74, 81, { name: 'Garrison Store apron', surface: 'stone', apron: true, forLandmark: 'ml-store2' }),
377
+ ];
378
+ function green(id, u0, u1, v0, v1, spec) {
379
+ return { ...capRect(id, u0, u1, v0, v1), ...spec };
380
+ }
381
+ /*
382
+ * The greenery is the city's mass (the masterplan's defining look is density:
383
+ * built-up districts, trees in every garden and along the roads), and the
384
+ * hero-buildings rule forbids filling emptiness with filler buildings, so the
385
+ * 2026-09-25 review round spent the grove budget (spec E: at most 120 grove
386
+ * entities inside the walls) before anything else: more groves in the walled
387
+ * gardens, lawns with groves between the market and Crown Street, south of
388
+ * the Gate Square, round the Artisan plot, and three grass islands in the
389
+ * Drill Square's corners (an island green lies wholly inside its plaza, so it
390
+ * stays grass; the SE corner is the Drill stand's clear ring). The mound's
391
+ * slope takes none: a grove's trunks share its anchor's height, and the slope
392
+ * rises a height a tile.
393
+ */
394
+ export const CAPITAL_GREENS = [
395
+ // Westgate.
396
+ green('paddock', 5, 43, 117, 138, { name: 'The Paddock', kind: 'paddock', wall: 'fence', gates: [[31, 117], [43, 128]], groves: 2 }),
397
+ green('westgate-grove', 33, 44, 139, 163, { name: 'Westgate grove', kind: 'orchard', groves: 7 }),
398
+ // The lawn south of the Gate Square, behind the Guardhouse and Depot plots.
399
+ green('westgate-lawn', 5, 29, 107, 116, { name: 'Westgate lawn', kind: 'lawn', groves: 3 }),
400
+ // Trade.
401
+ green('merchants-garden', 5, 25, 28, 49, { name: "Merchants' Garden", kind: 'garden', groves: 6 }),
402
+ green('trade-north-strip', 42, 108, 5, 15, { name: 'Trade north strip', kind: 'orchard', groves: 8 }),
403
+ green('exchange-garden', 128, 140, 5, 26, { name: 'Exchange Garden', kind: 'garden', groves: 4 }),
404
+ green('bank-lawn-w', 97, 111, 42, 61, { name: 'Bank lawns', kind: 'lawn', groves: 4 }),
405
+ green('bank-lawn-e', 125, 140, 42, 61, { name: 'Bank lawns', kind: 'lawn', groves: 4 }),
406
+ // The lawns between Market Square and Crown Street, either side of the Market Gate walk.
407
+ green('market-lawn-w', 42, 63, 53, 61, { name: 'Market lawns', kind: 'lawn', groves: 3 }),
408
+ green('market-lawn-e', 71, 96, 53, 61, { name: 'Market lawns', kind: 'lawn', groves: 3 }),
409
+ // The Royal Way's verges.
410
+ green('verge-w', WAY_U - CAPITAL_ROYAL_WAY_HALF_W - CAPITAL_ROYAL_WAY_VERGE, WAY_U - CAPITAL_ROYAL_WAY_HALF_W - 1, IN_V0, IN_V1, { name: 'The Royal Way verge', kind: 'verge' }),
411
+ green('verge-e', WAY_U + CAPITAL_ROYAL_WAY_HALF_W + 1, WAY_U + CAPITAL_ROYAL_WAY_HALF_W + CAPITAL_ROYAL_WAY_VERGE, IN_V0, IN_V1, { name: 'The Royal Way verge', kind: 'verge' }),
412
+ // Forge.
413
+ green('herb-garden', 92, 103, 124, 140, { name: 'Herb Garden', kind: 'garden' }),
414
+ // The lawn round the Artisan plot, north of the Ore Yard (phase 3; lawn until then).
415
+ green('artisan-lawn', 45, 72, 124, 135, { name: 'Artisan lawn', kind: 'lawn', groves: 3 }),
416
+ green('forge-lawn', 73, 91, 134, 150, { name: 'Forge lawn', kind: 'lawn', groves: 4 }),
417
+ green('forge-grove', 45, 104, 151, 163, { name: 'Forge Grove', kind: 'orchard', groves: 12 }),
418
+ // Military: grass islands in three corners of the Drill Square, a grove on each.
419
+ green('drill-grove-nw', 111, 115, 102, 106, { name: 'Drill Square trees', kind: 'lawn', groves: 1 }),
420
+ green('drill-grove-ne', 137, 141, 102, 106, { name: 'Drill Square trees', kind: 'lawn', groves: 1 }),
421
+ green('drill-grove-sw', 111, 115, 116, 120, { name: 'Drill Square trees', kind: 'lawn', groves: 1 }),
422
+ // Temple Gardens.
423
+ green('graveyard', 180, 206, 38, 58, { name: 'The Graveyard', kind: 'graveyard', wall: 'garden', gates: [[193, 38]] }),
424
+ green('memorial-garden', 236, 272, 38, 58, { name: 'Memorial Garden', kind: 'garden', wall: 'noble', gates: [[254, 38], [236, 48]], groves: 8 }),
425
+ green('cloister-garth', 238, 262, 20, 28, { name: 'Cloister Garth', kind: 'garden', wall: 'garden', gates: [[250, 28], [250, 20]], groves: 2 }),
426
+ green('temple-orchard-w', 204, 234, 4, 22, { name: 'Temple Orchard', kind: 'orchard', groves: 10 }),
427
+ green('temple-orchard-e', 263, 275, 4, 27, { name: 'Temple Orchard', kind: 'orchard', groves: 6 }),
428
+ green('temple-lawn-w', 165, 179, 50, 61, { name: 'Temple lawns', kind: 'lawn', groves: 2 }),
429
+ green('temple-lawn-e', 207, 215, 38, 61, { name: 'Temple lawns', kind: 'lawn', groves: 2 }),
430
+ // The Keep and the Royal Quarter.
431
+ green('royal-square-lawn', 234, 248, 68, 75, { name: 'Royal Square lawn', kind: 'lawn', wall: 'hedge', gates: [[241, 68], [241, 75], [234, 72], [248, 72]] }),
432
+ green('kings-garden', 252, 275, 108, 163, { name: "The King's Garden", kind: 'garden', groves: 14 }),
433
+ // Noble Quarter: the four walled manor gardens, 2 x 2 round the Noble Fountain.
434
+ green('blue-garden', 166, 201, 109, 130, { name: 'Blue Manor garden', kind: 'garden', wall: 'noble', gates: [[182, 130], [166, 118]] }),
435
+ green('green-garden', 215, 250, 109, 130, { name: 'Green Manor garden', kind: 'garden', wall: 'noble', gates: [[234, 130], [250, 118]] }),
436
+ green('red-garden', 166, 201, 138, 162, { name: 'Red Manor garden', kind: 'garden', wall: 'noble', gates: [[182, 138]] }),
437
+ green('purple-garden', 215, 250, 138, 162, { name: 'Purple Manor garden', kind: 'garden', wall: 'noble', gates: [[234, 138]] }),
85
438
  ];
86
- /** East-west district streets as segments; the Way itself is separate. */
87
- export const CAPITAL_DISTRICT_STREETS = [
88
- { z: 36, minX: 483, maxX: 661 },
89
- { z: 55, minX: 483, maxX: 661 },
90
- { z: 76, minX: 483, maxX: 757 },
91
- { z: 118, minX: 483, maxX: 757 },
92
- { z: 137, minX: 483, maxX: 661 },
93
- { z: 157, minX: 483, maxX: 661 },
94
- { z: 147, minX: 659, maxX: 757 },
439
+ function yard(id, u0, u1, v0, v1, spec) {
440
+ return { ...capRect(id, u0, u1, v0, v1), ...spec };
441
+ }
442
+ export const CAPITAL_YARDS = [
443
+ yard('warehouse-yard-w', 5, 17, 5, 26, { name: 'Warehouse Yard', surface: 'dirt' }),
444
+ yard('warehouse-yard-s', 5, 40, 20, 26, { name: 'Warehouse Yard', surface: 'dirt' }),
445
+ yard('expedition-camp', 5, 32, 142, 162, { name: 'Expedition Camp', surface: 'dirt' }),
446
+ yard('furnace-yard', 45, 104, 67, 81, { name: 'Furnace Yard', surface: 'dirt' }),
447
+ yard('timber-yard', 45, 57, 104, 119, { name: 'Timber Yard', surface: 'dirt' }),
448
+ yard('charcoal-yard', 58, 90, 107, 118, {
449
+ name: 'Charcoal Yard',
450
+ surface: 'dirt',
451
+ fence: true,
452
+ gates: [[70, 107], [71, 107], [72, 107], [73, 107], [74, 107], [75, 107], [76, 107], [77, 107], [78, 107], [90, 113]],
453
+ }),
454
+ // The Ore Yard's ore crates and ingot stacks are phase-3 props: lawn until then.
455
+ yard('ore-yard', 45, 72, 136, 150, { name: 'Ore Yard', surface: 'dirt', phase: 3 }),
456
+ yard('muster-yard', 106, 140, 67, 80, { name: 'Muster Yard', surface: 'dirt' }),
457
+ yard('training-yard', 108, 144, 124, 142, {
458
+ name: 'Training Yard',
459
+ surface: 'dirt',
460
+ fence: true,
461
+ gates: [[126, 124], [127, 124], [107, 133], [108, 133], [144, 133]],
462
+ }),
463
+ yard('archery-range', 108, 144, 145, 163, { name: 'Archery Range', surface: 'dirt' }),
95
464
  ];
96
465
  /**
97
- * The keep's mound: a plateau at CAPITAL_MOUND_H with a slope of one height
98
- * a tile all round it, the Royal Way climbing the west slope as a ramp.
466
+ * Eight districts, ids kept from the old wards so no character is paid the
467
+ * Explorer XP twice; the labels and rects are new. The rects do not overlap
468
+ * and together they tile the interior (u 2-278, v 2-166), so lookup order
469
+ * does not matter. The catch-all `capital` zone keeps CAPITAL_WORKS.
99
470
  */
100
- export const CAPITAL_MOUND_H = 8;
101
- export const CAPITAL_PLATEAU = { minX: 698, maxX: 746, minZ: 84, maxZ: 108 };
102
- export const CAPITAL_MOUND_SLOPE = CAPITAL_MOUND_H - CAPITAL_FLOOR_H;
103
- /** The inner gatehouse on the plateau's west edge, and the curtain round the edge. */
104
- export const CAPITAL_INNER_GATE = { x: CAPITAL_PLATEAU.minX + 2, z: CAPITAL_AVENUE_Z };
105
- /** The keep and the inner court before it. */
106
- export const CAPITAL_KEEP = { x: 728, z: CAPITAL_AVENUE_Z };
107
- export const CAPITAL_KEEP_HALF_X = 12;
108
- export const CAPITAL_KEEP_HALF_Z = 9;
109
- export const CAPITAL_KING_STATUE = { x: 708, z: CAPITAL_AVENUE_Z };
110
- /** The squares: paved, reserved from building. */
111
- export const CAPITAL_MARKET_SQUARE = { minX: 486, maxX: 538, minZ: 16, maxZ: 54 };
112
- export const CAPITAL_MARKET_FOUNTAIN = { x: 512, z: 35 };
113
- export const CAPITAL_COIN_SQUARE = { minX: 622, maxX: 658, minZ: 38, maxZ: 53 };
114
- export const CAPITAL_COIN_FOUNTAIN = { x: 640, z: 46 };
115
- export const CAPITAL_TEMPLE_SQUARE = { minX: 684, maxX: 716, minZ: 40, maxZ: 74 };
116
- export const CAPITAL_DRILL_SQUARE = { minX: 604, maxX: 636, minZ: 120, maxZ: 136 };
117
- export const CAPITAL_TRAINING_YARD = { minX: 598, maxX: 642, minZ: 140, maxZ: 154 };
118
- /** The greens: the graveyard and cloister by the temple, and the noble gardens. */
119
- export const CAPITAL_GRAVEYARD = { minX: 664, maxX: 688, minZ: 16, maxZ: 32 };
120
- export const CAPITAL_CLOISTER = { minX: 712, maxX: 750, minZ: 16, maxZ: 32 };
121
- export const CAPITAL_NOBLE_GARDENS = [
122
- { minX: 666, maxX: 692, minZ: 121, maxZ: 144, manor: { x: 679, z: 131, roof: 'red' } },
123
- { minX: 706, maxX: 732, minZ: 121, maxZ: 144, manor: { x: 719, z: 131, roof: 'blue' } },
124
- { minX: 666, maxX: 692, minZ: 150, maxZ: 173, manor: { x: 679, z: 160, roof: 'blue' } },
125
- { minX: 706, maxX: 732, minZ: 150, maxZ: 173, manor: { x: 719, z: 160, roof: 'red' } },
471
+ function district(id, u0, u1, v0, v1, spec) {
472
+ return { ...capRect(id, u0, u1, v0, v1), ...spec, id };
473
+ }
474
+ export const CAPITAL_DISTRICTS = [
475
+ district('capital-castle', 160, IN_U1, 62, 101, { label: 'The Keep', street: 'stone', roofs: ['blue'], emblem: 'trident' }),
476
+ district('capital-merchant', 145, 159, IN_V0, IN_V1, { label: 'The Royal Way', street: 'stone', roofs: ['blue'], emblem: 'crown' }),
477
+ district('capital-gate', IN_U0, 44, 62, IN_V1, { label: 'Westgate Outpost', street: 'stone', roofs: ['blue'], emblem: 'tree' }),
478
+ district('capital-market', IN_U0, 144, IN_V0, 61, { label: 'Trade District', street: 'path', roofs: ['blue'], emblem: 'scales' }),
479
+ district('capital-smiths', 45, 104, 62, IN_V1, { label: 'Forge and Artisan Quarter', street: 'dirt', roofs: ['blue'], emblem: 'anvil' }),
480
+ district('capital-garrison', 105, 144, 62, IN_V1, { label: 'Military Ward', street: 'stone', roofs: ['blue'], emblem: 'lion' }),
481
+ district('capital-temple', 160, IN_U1, IN_V0, 61, { label: 'Temple Gardens', street: 'gravel', roofs: ['blue'], emblem: 'cross' }),
482
+ district('capital-noble', 160, IN_U1, 102, IN_V1, { label: 'Noble Quarter', street: 'gravel', roofs: ['blue', 'green', 'red', 'purple'], emblem: 'trident' }),
126
483
  ];
484
+ function districtAtLocal(u, v) {
485
+ return CAPITAL_DISTRICTS.find((d) => u >= d.u0 && u <= d.u1 && v >= d.v0 && v <= d.v1)?.id;
486
+ }
487
+ /* ------------------------------------------------------------------ *
488
+ * Buildings and monuments.
489
+ * ------------------------------------------------------------------ */
490
+ /**
491
+ * The phase the city is built to. A plan row whose `phase` is later than
492
+ * this stands as its `standIn` if the stand-in's phase has come, and
493
+ * otherwise is not built yet; its footprint is still reserved, so the later
494
+ * phase lands on a clear plot and reshuffles nothing.
495
+ */
496
+ export const CAPITAL_BUILD_PHASE = 1;
497
+ /** Exactly the plan script's `b(id, ch, name, cu, cv, halfU, halfV, face, entry)`. */
498
+ function landmark(id, name, u, v, hu, hv, face, mouth, spec) {
499
+ let door = null;
500
+ if (face) {
501
+ const du = face === 'e' ? u + hu : face === 'w' ? u - hu : u;
502
+ const dv = face === 's' ? v + hv : face === 'n' ? v - hv : v;
503
+ door = { ...capTile(du, dv), facing: face, mouth };
504
+ }
505
+ const district = districtAtLocal(u, v);
506
+ if (!district)
507
+ throw new Error(`capital landmark ${id} is outside every district`);
508
+ return {
509
+ id,
510
+ name,
511
+ district,
512
+ ...spec,
513
+ u,
514
+ v,
515
+ x: cu(u),
516
+ z: cv(v),
517
+ footprint: capRect(id, u - hu, u + hu, v - hv, v + hv),
518
+ door,
519
+ };
520
+ }
521
+ const SOLID = null;
127
522
  /**
128
- * The landmarks: walk-in buildings by kind and style. `board` puts a notice
129
- * board at the door for the ones whose trade is not on their face.
523
+ * The buildings, in placing order (spec B, plan script `B`). Phase-1 rows
524
+ * are REUSE kinds on their final plots; the Grand Exchange stands as its
525
+ * Blender building (decision D5); the Capital Bank, the Royal
526
+ * Administration and the Barracks stand as their stand-ins; the sheds, the
527
+ * new stonehouse variants and the crypt wait for phases 2 and 3.
130
528
  */
131
529
  export const CAPITAL_LANDMARKS = [
132
- { id: 'bank', kind: 'stonehouse', style: 'bank-s', x: 640, z: 30 },
133
- { id: 'guildhall', kind: 'stonehouse', style: 'hall-n', x: 640, z: 65, board: 'Guild Hall' },
134
- { id: 'temple', kind: 'temple', style: 'temple-s', x: 700, z: 30 },
135
- { id: 'blacksmith', kind: 'forgehall', style: 'forge-n', x: 512, z: 146, board: 'Blacksmith' },
136
- { id: 'armourer', kind: 'forgehall', style: 'forge-n', x: 560, z: 146, board: 'Armourer' },
137
- { id: 'barracks-w', kind: 'stonehouse', style: 'lodge-e', x: 592, z: 128, board: 'Barracks' },
138
- { id: 'barracks-e', kind: 'stonehouse', style: 'lodge-w', x: 648, z: 128 },
139
- { id: 'inn-n', kind: 'stonehouse', style: 'inn-s', x: 500, z: 88 },
140
- { id: 'inn-s', kind: 'stonehouse', style: 'pub-n', x: 500, z: 104 },
141
- { id: 'guardhouse', kind: 'stonehouse', style: 'shop-s', x: 524, z: 89, board: 'Watch House' },
142
- { id: 'warehouse-n', kind: 'warehouse', style: 'store-n', x: 560, z: 66 },
143
- { id: 'warehouse-e', kind: 'warehouse', style: 'store-w', x: 552, z: 26 },
144
- { id: 'warehouse-s', kind: 'warehouse', style: 'store-n', x: 552, z: 46 },
530
+ // Westgate Outpost.
531
+ landmark('wg-bank', 'Outpost Bank', 14, 73, 4, 4, 's', 1, { kind: 'stonehouse', style: 'bank-s', phase: 1, heroFront: true, emblem: 'tree' }),
532
+ landmark('wg-inn', 'Adventurer Inn', 32, 72, 5, 5, 's', 1, { kind: 'stonehouse', style: 'inn-s', phase: 1 }),
533
+ landmark('wg-guard', 'Checkpoint Guardhouse', 10, 103, 3, 3, 'n', 1, { kind: 'stonehouse', style: 'guardhouse-n', phase: 2 }),
534
+ landmark('wg-depot', 'Supply Depot', 24, 103, 5, 3, 'n', 5, { kind: 'shed', style: 'depot-mouth5-n', phase: 2 }),
535
+ landmark('wg-ranger', 'Ranger Hall', 38, 107, 5, 7, 'n', 1, { kind: 'stonehouse', style: 'lodge-n', phase: 1 }),
536
+ landmark('wg-stables', 'Stables and Pack Shed', 18, 122, 6, 3, 'n', 7, { kind: 'shed', style: 'stable-mouth7-n', phase: 2 }),
537
+ // Trade District.
538
+ landmark('tr-warehouse', 'Trade Warehouse', 24, 14, 6, 4, 'e', 1, { kind: 'warehouse', style: 'store-e', phase: 1 }),
539
+ landmark('tr-clothier', 'Clothier', 30, 33, 4, 4, 'e', 1, { kind: 'stonehouse', style: 'clothes-e', phase: 1 }),
540
+ landmark('tr-general', 'General Store', 30, 45, 4, 4, 'e', 1, { kind: 'stonehouse', style: 'shop-e', phase: 1 }),
541
+ landmark('tr-cookhouse', 'Cookhouse', 12, 55, 4, 4, 'e', 1, { kind: 'stonehouse', style: 'kitchen-e', phase: 1 }),
542
+ landmark('tr-ge', 'Grand Exchange', 118, 20, 9, 6, 's', 1, { kind: 'capitalbuilding', style: 'grandexchange-s', phase: 1, emblem: 'scales' }),
543
+ landmark('tr-bank', 'Capital Bank', 118, 48, 6, 6, 'n', 1, {
544
+ kind: 'stonehouse',
545
+ style: 'exchequer-n',
546
+ phase: 2,
547
+ // Centred on the plot, door two rows in from the plot's: pave (u 118, v 42-43) to join it.
548
+ standIn: { kind: 'stonehouse', style: 'bank-n' },
549
+ heroFront: true,
550
+ emblem: 'scales',
551
+ }),
552
+ // The Royal Way's civic hall, on Victory Plaza's east promenade.
553
+ landmark('rw-guild', 'Guild Hall', 170, 44, 5, 5, 'w', 1, { kind: 'stonehouse', style: 'hall-w', phase: 1, heroFront: true, emblem: 'crown' }),
554
+ // Forge and Artisan Quarter.
555
+ landmark('fg-mainforge', 'Main Forge', 70, 74, 8, 6, 's', 5, { kind: 'capitalbuilding', style: 'mainforge-mouth5-s', phase: 1 }),
556
+ landmark('fg-smelter', 'Smelter', 88, 74, 3, 3, SOLID, 0, { kind: 'capitalbuilding', style: 'smelter-s', phase: 1 }),
557
+ landmark('fg-smith', 'Blacksmith', 54, 97, 4, 5, 'e', 1, { kind: 'forgehall', style: 'forge-e', phase: 1, board: 'Blacksmith', emblem: 'anvil' }),
558
+ landmark('fg-armour', 'Armourer', 94, 97, 4, 5, 'w', 1, { kind: 'forgehall', style: 'forge-w', phase: 1, board: 'Armourer', emblem: 'anvil' }),
559
+ landmark('fg-kiosk', "Smiths' Strongbox", 74, 89, 1, 0, SOLID, 0, { kind: 'marketstall', style: 'bank-s', phase: 2 }),
560
+ landmark('fg-anvilshed', 'Anvil Shed', 64, 104, 3, 2, 'n', 5, { kind: 'shed', style: 'forge-mouth5-n', phase: 2 }),
561
+ landmark('fg-logshed', 'Forge Yard Shed', 51, 112, 3, 2, 'e', 3, { kind: 'shed', style: 'log-mouth3-e', phase: 2 }),
562
+ landmark('fg-artisan', 'Artisan Workshop', 62, 128, 4, 4, 'n', 1, { kind: 'stonehouse', style: 'artisan-n', phase: 2 }),
563
+ landmark('fg-alchemy', 'Alchemy Hut', 86, 128, 4, 4, 'n', 1, { kind: 'stonehouse', style: 'potion-n', phase: 1 }),
564
+ // Military Ward.
565
+ landmark('ml-barracks', 'Barracks', 122, 93, 10, 5, 'n', 1, {
566
+ kind: 'stonehouse',
567
+ style: 'barracks-n',
568
+ phase: 2,
569
+ // hall-n is 11 x 11 centred on the plot, its door on the plot's own door tile.
570
+ standIn: { kind: 'stonehouse', style: 'hall-n' },
571
+ emblem: 'lion',
572
+ }),
573
+ landmark('ml-store1', 'Garrison Store', 112, 71, 3, 2, 's', 5, { kind: 'shed', style: 'store-mouth5-s', phase: 2 }),
574
+ landmark('ml-store2', 'Garrison Store', 132, 71, 3, 2, 's', 5, { kind: 'shed', style: 'store-mouth5-s', phase: 2 }),
575
+ landmark('ml-tower', 'Yard Watchtower', 110, 127, 1, 1, SOLID, 0, { kind: 'citywall', style: 'tower-watch', phase: 1 }),
576
+ // Temple Gardens.
577
+ landmark('tg-temple', 'Temple of Light', 192, 18, 6, 8, 's', 1, { kind: 'temple', style: 'temple-s', phase: 1, heroFront: true, emblem: 'cross' }),
578
+ landmark('tg-cloister', 'Cloister', 250, 15, 10, 4, 's', 1, {
579
+ kind: 'stonehouse',
580
+ style: 'cloister-s',
581
+ phase: 3,
582
+ // The phase-2 Chapter House: hall-s one row north of the plot's centre, on the same door tile.
583
+ standIn: { kind: 'stonehouse', style: 'hall-s', phase: 2, v: 14 },
584
+ }),
585
+ landmark('tg-shrine', 'Healing Shrine', 226, 52, 3, 3, 'w', 5, { kind: 'shed', style: 'chapel-mouth5-w', phase: 2 }),
586
+ landmark('tg-mausoleum', 'Mausoleum', 193, 50, 2, 3, SOLID, 0, { kind: 'mausoleum', style: 'crypt', phase: 3 }),
587
+ // The Keep and the Royal Quarter. The keep is placed by the mound step, not place().
588
+ // Its door is the gate passage's mouth on the west face, three wide (CASTLE_PASSAGE_HALF_W).
589
+ landmark('kp-keep', 'The Keep', KEEP_U, AVENUE_V, CASTLE_KEEP_HALF_X, CASTLE_KEEP_HALF_Z, 'w', 2 * CASTLE_PASSAGE_HALF_W + 1, { kind: 'castle', style: 'keep', phase: 1 }),
590
+ landmark('kp-admin', 'Royal Administration', 262, 70, 6, 6, 's', 1, {
591
+ kind: 'stonehouse',
592
+ style: 'admin-s',
593
+ phase: 2,
594
+ // hall-s centred on the plot: its door is (262, 75), pave (262, 76) to the court.
595
+ standIn: { kind: 'stonehouse', style: 'hall-s' },
596
+ heroFront: true,
597
+ emblem: 'trident',
598
+ }),
599
+ landmark('kp-town1', 'Townhouse', 236, 93, 4, 4, 'n', 1, { kind: 'stonehouse', style: 'townhouse-n', phase: 2 }),
600
+ landmark('kp-town2', 'Townhouse', 248, 93, 4, 4, 'n', 1, { kind: 'stonehouse', style: 'townhouse-n', phase: 2 }),
601
+ landmark('kp-post', 'East Gate Guard Post', 266, 98, 1, 1, SOLID, 0, { kind: 'citywall', style: 'tower-watch', phase: 1 }),
602
+ // Noble Quarter. Green and purple roofs are the frontend's new manor roof tokens (phase 2); until then it draws its fallback roof.
603
+ landmark('nb-blue', 'Blue Manor', 182, 118, 5, 5, 's', 1, { kind: 'manor', style: 'blue-s', phase: 1 }),
604
+ landmark('nb-green', 'Green Manor', 234, 118, 5, 5, 's', 1, { kind: 'manor', style: 'green-s', phase: 1 }),
605
+ landmark('nb-red', 'Red Manor', 182, 152, 5, 5, 'n', 1, { kind: 'manor', style: 'red-n', phase: 1 }),
606
+ landmark('nb-purple', 'Purple Manor', 234, 152, 5, 5, 'n', 1, { kind: 'manor', style: 'purple-n', phase: 1 }),
607
+ landmark('nb-post', 'Garden Guard Post', 272, 110, 1, 1, SOLID, 0, { kind: 'citywall', style: 'tower-watch', phase: 1 }),
145
608
  ];
146
- /** The market's stall rows: x, z, facing; four rows across the square. */
147
- export const CAPITAL_STALL_ROWS = [
148
- { z: 19, facing: 's', minX: 492, maxX: 534 },
149
- { z: 25, facing: 'n', minX: 492, maxX: 534 },
150
- { z: 29, facing: 's', minX: 492, maxX: 534 },
151
- { z: 41, facing: 'n', minX: 492, maxX: 534 },
152
- { z: 45, facing: 's', minX: 492, maxX: 534 },
153
- { z: 51, facing: 'n', minX: 492, maxX: 534 },
609
+ /**
610
+ * A monument row. A statue (the row's own kind, or its stand-in's) stands on
611
+ * a stepped plinth filling its square: the `plinth3` / `plinth5` token
612
+ * (constants.ts statueStyleWithPlinth) goes into its style here, so the
613
+ * builder, the smoke and the client all read the one style, and the shared
614
+ * collision blocks the whole square (spec B.3: the Victory Statue's 5x5
615
+ * stepped plinth; a figure life-size on one tile did not read as a monument).
616
+ */
617
+ function monument(id, name, u, v, size, spec) {
618
+ const h = (size - 1) / 2;
619
+ const district = districtAtLocal(u, v);
620
+ if (!district)
621
+ throw new Error(`capital monument ${id} is outside every district`);
622
+ const style = spec.kind === 'statue' ? statueStyleWithPlinth(spec.style, size) : spec.style;
623
+ const standIn = spec.standIn?.kind === 'statue' ? { ...spec.standIn, style: statueStyleWithPlinth(spec.standIn.style, size) } : spec.standIn;
624
+ return { id, name, district, ...spec, style, standIn, u, v, x: cu(u), z: cv(v), size, footprint: capRect(id, u - h, u + h, v - h, v + h) };
625
+ }
626
+ const KING = { kind: 'statue', style: 'king' };
627
+ const PLAZA_FOUNTAIN = { kind: 'fountain', style: 'plaza' };
628
+ export const CAPITAL_MONUMENTS = [
629
+ monument('tr-fountain', 'Market Fountain', 67, 34, 3, { kind: 'fountain', style: 'plaza', phase: 1 }),
630
+ monument('tr-coinstatue', 'Coin Square Statue', 118, 34, 5, { kind: 'capitalbuilding', style: 'kingstatue-s', phase: 1, standIn: KING }),
631
+ monument('rw-victory', 'Victory Statue', WAY_U, 20, 5, { kind: 'capitalbuilding', style: 'victorystatue-s', phase: 1, standIn: KING }),
632
+ monument('rw-plinth', 'The Queen', WAY_U, 46, 3, { kind: 'hoodedstatue', style: 'pale', phase: 2 }),
633
+ monument('rw-fountain', "King's Fountain", capU(CAPITAL_KINGS_FOUNTAIN.x), capV(CAPITAL_KINGS_FOUNTAIN.z), 5, { kind: 'fountain', style: 'tiered', phase: 2, standIn: PLAZA_FOUNTAIN }),
634
+ monument('rw-noblecross', 'Noble Cross Statue', WAY_U, NOBLE_V, 3, { kind: 'statue', style: 'knight-n', phase: 1 }),
635
+ monument('tg-hubfountain', 'Stone Fountain', 218, 30, 5, { kind: 'fountain', style: 'tiered', phase: 2, standIn: PLAZA_FOUNTAIN }),
636
+ monument('tg-faith', 'Faith Statue', 254, 48, 3, { kind: 'hoodedstatue', style: 'pale', phase: 2 }),
637
+ monument('kp-statue', "King's Statue", capU(CAPITAL_KING_STATUE.x), AVENUE_V, 5, { kind: 'capitalbuilding', style: 'kingstatue-w', phase: 1, standIn: KING }),
638
+ // The keep lions flank the door (the Blender keeplion, placed in phase 1 on
639
+ // James's word, 2026-09-25). The knight on his plinth is the stand-in a
640
+ // rebuild at an earlier phase gets: a brazier there was one more fire in a
641
+ // court that already had nine in view.
642
+ monument('kp-lion-n', 'Keep Lion', 181, AVENUE_V - 5, 3, { kind: 'capitalbuilding', style: 'keeplion-w', phase: 1, standIn: { kind: 'statue', style: 'knight-w' } }),
643
+ monument('kp-lion-s', 'Keep Lion', 181, AVENUE_V + 5, 3, { kind: 'capitalbuilding', style: 'keeplion-w', phase: 1, standIn: { kind: 'statue', style: 'knight-w' } }),
644
+ monument('nb-fountain', 'Noble Fountain', 208, 134, 5, { kind: 'fountain', style: 'tiered', phase: 2, standIn: PLAZA_FOUNTAIN }),
154
645
  ];
155
- /** Stall pitch along a row. */
156
- export const CAPITAL_STALL_PITCH = 3;
157
- /** The districts, for discovery and the density pass. Roof tokens are the rowhouse palette. */
158
- export const CAPITAL_DISTRICTS = [
159
- { id: 'capital-gate', label: 'Gate Ward', minX: 482, maxX: 538, minZ: 57, maxZ: 135, roofs: ['tan', 'slate'], street: 'path' },
160
- { id: 'capital-market', label: 'Market Ward', minX: 482, maxX: 578, minZ: 14, maxZ: 93, roofs: ['tan', 'red', 'blue'], street: 'path' },
161
- { id: 'capital-smiths', label: "Smiths' Quarter", minX: 482, maxX: 578, minZ: 99, maxZ: 178, roofs: ['slate', 'tan'], street: 'dirt' },
162
- { id: 'capital-merchant', label: 'Merchant Ward', minX: 582, maxX: 658, minZ: 14, maxZ: 93, roofs: ['slate', 'blue', 'red'], street: 'path' },
163
- { id: 'capital-garrison', label: 'Garrison Ward', minX: 582, maxX: 658, minZ: 99, maxZ: 178, roofs: ['slate'], street: 'stone' },
164
- { id: 'capital-temple', label: 'Temple Ward', minX: 662, maxX: 758, minZ: 14, maxZ: 77, roofs: ['blue', 'tan'], street: 'path' },
165
- { id: 'capital-noble', label: 'Noble Quarter', minX: 662, maxX: 758, minZ: 115, maxZ: 178, roofs: ['red', 'blue'], street: 'path' },
166
- { id: 'capital-castle', label: 'Castle Ward', minX: 690, maxX: 758, minZ: 78, maxZ: 114, roofs: ['slate'], street: 'stone' },
646
+ /**
647
+ * What stands for a plan row in `phase` (default CAPITAL_BUILD_PHASE): the
648
+ * row itself once its phase has come, else its stand-in once the stand-in's
649
+ * phase has come, else null (not built yet; the plot stays reserved).
650
+ */
651
+ export function capitalPlacementNow(row, phase = CAPITAL_BUILD_PHASE) {
652
+ if (row.phase <= phase && row.kind !== 'shed' && row.kind !== 'mausoleum') {
653
+ return { kind: row.kind, style: row.style, u: row.u, v: row.v, x: cu(row.u), z: cv(row.v), standIn: false };
654
+ }
655
+ const s = row.standIn;
656
+ if (s && (s.phase ?? 1) <= phase) {
657
+ const u = s.u ?? row.u;
658
+ const v = s.v ?? row.v;
659
+ return { kind: s.kind, style: s.style, u, v, x: cu(u), z: cv(v), standIn: true };
660
+ }
661
+ return null;
662
+ }
663
+ /** The tiles a door opens on the ring: its middle tile and the rest of its mouth, along the facing side. */
664
+ export function capitalDoorTiles(door) {
665
+ const half = Math.floor((door.mouth - 1) / 2);
666
+ const out = [];
667
+ for (let i = -half; i <= half; i++) {
668
+ out.push(door.facing === 'n' || door.facing === 's' ? capTile(door.u + i, door.v) : capTile(door.u, door.v + i));
669
+ }
670
+ return out;
671
+ }
672
+ /** The approach tiles: one step out from each door tile. Nothing may stand on these (nor on the door). */
673
+ export function capitalDoorApproach(door) {
674
+ const [du, dv] = door.facing === 'n' ? [0, -1] : door.facing === 's' ? [0, 1] : door.facing === 'e' ? [1, 0] : [-1, 0];
675
+ return capitalDoorTiles(door).map((t) => capTile(t.u + du, t.v + dv));
676
+ }
677
+ function station(id, kind, u, v, range, extra = {}) {
678
+ const { phase = 1, ...rest } = extra;
679
+ return { id, kind, ...capTile(u, v), range, phase, ...rest };
680
+ }
681
+ const PICKUP = 1;
682
+ export const CAPITAL_STATIONS = [
683
+ // Bank points: the Outpost Bank, the Capital Bank, the Smiths' Strongbox.
684
+ station('chest-outpost', 'bankchest', 14, 71, BANK_RANGE, { inside: 'wg-bank' }),
685
+ station('chest-capital', 'bankchest', 116, 47, BANK_RANGE, { inside: 'tr-bank' }),
686
+ station('chest-strongbox', 'bankchest', 74, 90, BANK_RANGE, { inside: 'fg-kiosk' }),
687
+ // Cook spots.
688
+ station('stove-cookhouse', 'stove', 10, 53, CAMPFIRE_COOK_RANGE, { inside: 'tr-cookhouse' }),
689
+ station('stove-inn', 'stove', 29, 69, CAMPFIRE_COOK_RANGE, { inside: 'wg-inn' }),
690
+ station('stove-barracks', 'stove', 114, 90, CAMPFIRE_COOK_RANGE, { inside: 'ml-barracks', phase: 2 }),
691
+ station('fire-muster', 'campfire', 122, 76, CAMPFIRE_COOK_RANGE),
692
+ station('stove-cloister', 'stove', 244, 13, CAMPFIRE_COOK_RANGE, { inside: 'tg-cloister', phase: 2 }),
693
+ station('fire-camp', 'campfire', 24, 152, CAMPFIRE_COOK_RANGE, { phase: 3 }),
694
+ // Smithing: the Main Forge pair under its hearth arches, either side of the mouth column u 68-72.
695
+ station('furnace-mf-w', 'furnace', 65, 76, BLACKSMITH_RANGE, { inside: 'fg-mainforge' }),
696
+ station('furnace-mf-e', 'furnace', 75, 76, BLACKSMITH_RANGE, { inside: 'fg-mainforge' }),
697
+ station('anvil-mf-w', 'anvil', 66, 78, BLACKSMITH_RANGE, { inside: 'fg-mainforge' }),
698
+ station('anvil-mf-e', 'anvil', 74, 78, BLACKSMITH_RANGE, { inside: 'fg-mainforge' }),
699
+ station('furnace-smelter', 'furnace', 88, 79, BLACKSMITH_RANGE),
700
+ // The Blacksmith's and the Armourer's flank their doors on Anvil Square, off the door rows.
701
+ station('furnace-smith', 'furnace', 60, 93, BLACKSMITH_RANGE),
702
+ station('anvil-smith', 'anvil', 60, 101, BLACKSMITH_RANGE),
703
+ station('furnace-armour', 'furnace', 88, 93, BLACKSMITH_RANGE),
704
+ station('anvil-armour', 'anvil', 88, 101, BLACKSMITH_RANGE),
705
+ station('anvil-shed', 'anvil', 64, 104, BLACKSMITH_RANGE, { inside: 'fg-anvilshed' }),
706
+ station('mound-1', 'charcoalmound', 68, 111, CHARCOAL_MOUND_RANGE),
707
+ station('mound-2', 'charcoalmound', 74, 111, CHARCOAL_MOUND_RANGE),
708
+ station('mound-3', 'charcoalmound', 80, 111, CHARCOAL_MOUND_RANGE),
709
+ // Twelve training dummies in two rows across the Training Yard.
710
+ ...[0, 1, 2, 3, 4, 5].flatMap((i) => [
711
+ station(`dummy-a${i}`, 'yardprop', 112 + i * 5, 129, TRAINING_DUMMY_RANGE, { style: 'dummy' }),
712
+ station(`dummy-b${i}`, 'yardprop', 112 + i * 5, 136, TRAINING_DUMMY_RANGE, { style: 'dummy' }),
713
+ ]),
714
+ // Prayer: the Temple Altar at the back of the nave, the Healing Shrine.
715
+ station('shrine-temple', 'shrine', 192, 12, TEMPLE_PRAYER_RANGE, { inside: 'tg-temple' }),
716
+ station('shrine-healing', 'shrine', 227, 52, TEMPLE_PRAYER_RANGE, { inside: 'tg-shrine' }),
717
+ // Notice boards (the bounty board kind; its reach is BANK_RANGE in world.ts).
718
+ station('board-gate', 'bountyboard', 20, 80, BANK_RANGE),
719
+ station('board-coin', 'bountyboard', 128, 34, BANK_RANGE),
720
+ station('board-ranger', 'bountyboard', 35, 104, BANK_RANGE, { inside: 'wg-ranger' }),
721
+ station('board-harbour', 'bountyboard', 268, 79, BANK_RANGE),
722
+ // Work benches: face90 runs the bench along z, against the Ranger Hall's east wall.
723
+ station('bench-ranger', 'workbench', 41, 104, PICKUP, { inside: 'wg-ranger', style: 'face90' }),
724
+ station('bench-artisan', 'workbench', 62, 129, PICKUP, { inside: 'fg-artisan', style: 'face0', phase: 2 }),
167
725
  ];
168
- /** The road in: from the Thaw arch's east road, round to the barbican. */
169
- export const CAPITAL_ROAD = [
170
- { x: 286, z: 50 },
171
- { x: 330, z: 52 },
172
- { x: 390, z: 70 },
173
- { x: 430, z: 92 },
174
- { x: CAPITAL_BARBICAN.x - 4, z: CAPITAL_AVENUE_Z },
726
+ function stall(id, cluster, goods, u, v, facing) {
727
+ return { id, cluster, goods, facing, style: `${goods}-${facing}`, phase: 1, ...capTile(u, v) };
728
+ }
729
+ const STALL_CLUSTERS = [
730
+ { cluster: 'produce', goods: 'produce', u: 53, v: 25 },
731
+ { cluster: 'fish', goods: 'fish', u: 81, v: 25 },
732
+ { cluster: 'cloth', goods: 'cloth', u: 53, v: 45 },
733
+ { cluster: 'trader', goods: 'pots', u: 81, v: 45 },
734
+ ];
735
+ export const CAPITAL_STALLS = [
736
+ ...STALL_CLUSTERS.flatMap((c, i) => [
737
+ stall(`stall${i}a`, c.cluster, c.goods, c.u - 3, c.v, 'w'),
738
+ stall(`stall${i}b`, c.cluster, c.goods, c.u + 3, c.v, 'e'),
739
+ stall(`stall${i}c`, c.cluster, c.goods, c.u, c.v - 3, 'n'),
740
+ ]),
741
+ stall('stall-tova', 'food', 'produce', 28, 52, 's'),
742
+ stall('stall-food1', 'food', 'produce', 34, 52, 's'),
743
+ stall('stall-food2', 'food', 'produce', 22, 52, 's'),
744
+ ];
745
+ /** A fingerpost's signpost style: `fingerpost-<place>|<place>|...`. The frontend draws one slat per place. */
746
+ export function capitalFingerpostStyle(names) {
747
+ return `fingerpost-${names.join('|')}`;
748
+ }
749
+ /** The places on a `fingerpost-` style, or null for any other style. */
750
+ export function parseFingerpostStyle(style) {
751
+ if (!style || !style.startsWith('fingerpost-'))
752
+ return null;
753
+ return style.slice('fingerpost-'.length).split('|').filter((s) => s.length > 0);
754
+ }
755
+ function fingerpost(id, u, v, names) {
756
+ return { id, kind: 'signpost', style: capitalFingerpostStyle(names), names, ...capTile(u, v) };
757
+ }
758
+ export const CAPITAL_FIXTURES = [
759
+ fingerpost('fingerpost-gate', 40, 80, ['Market Square', 'The Royal Way', 'The Keep', 'Forge Quarter', "King's Harbour", 'The Dreadwoods']),
760
+ fingerpost('fingerpost-cross', 146, 70, ['The Keep', 'Temple of Light', 'Market Square', 'Westgate', "King's Harbour", 'Noble Quarter', 'The Dreadwoods']),
761
+ fingerpost('fingerpost-royal', 230, 72, ['The Royal Way', 'Market Square', 'The Keep', 'Temple of Light']),
762
+ { id: 'brazier-west-n', kind: 'yardprop', style: 'brazier', ...capTile(7, AVENUE_V - 5) },
763
+ { id: 'brazier-west-s', kind: 'yardprop', style: 'brazier', ...capTile(7, AVENUE_V + 5) },
764
+ { id: 'brazier-east-n', kind: 'yardprop', style: 'brazier', ...capTile(IN_U1 - 3, AVENUE_V - 6) },
765
+ { id: 'brazier-east-s', kind: 'yardprop', style: 'brazier', ...capTile(IN_U1 - 3, AVENUE_V + 6) },
766
+ ];
767
+ function npcSpot(id, name, role, u, v) {
768
+ return { id, name, role, ...capTile(u, v) };
769
+ }
770
+ export const CAPITAL_NPC_SPOTS = [
771
+ npcSpot('outpost-teller', 'Outpost Teller', 'banker', 16, 71),
772
+ npcSpot('adventurer-innkeeper', 'Innkeeper', 'merchant:inn', 32, 69),
773
+ npcSpot('gate-captain', 'Gate Captain', 'resident', 10, 104),
774
+ npcSpot('quartermaster', 'Quartermaster', 'merchant:supplies', 24, 104),
775
+ npcSpot('ranger', 'Ranger', 'merchant:ranger', 38, 110),
776
+ npcSpot('clothier', 'Clothier', 'merchant:clothier', 28, 33),
777
+ npcSpot('storekeeper', 'Storekeeper', 'merchant:general', 28, 45),
778
+ npcSpot('cook', 'Cook', 'merchant:cook', 10, 56),
779
+ npcSpot('tova', 'Tova', 'tova', 28, 51),
780
+ npcSpot('produce-keeper', 'Produce seller', 'merchant:produce', 53, 25),
781
+ npcSpot('fish-keeper', 'Fish seller', 'merchant:fish', 81, 25),
782
+ npcSpot('cloth-keeper', 'Cloth seller', 'merchant:cloth', 53, 45),
783
+ npcSpot('pedlar', 'Pedlar', 'merchant:pedlar', 81, 45),
784
+ npcSpot('exchange-clerk', 'Exchange Clerk', 'merchant:exchange', 118, 17),
785
+ npcSpot('capital-banker', 'Capital Banker', 'banker', 120, 49),
786
+ npcSpot('guild-clerk', 'Guild Clerk', 'resident', 172, 44),
787
+ npcSpot('forge-master', 'Forge Master', 'merchant:smith', 52, 97),
788
+ npcSpot('armourer', 'Armourer', 'merchant:armourer', 96, 97),
789
+ npcSpot('alchemist', 'Alchemist', 'merchant:alchemist', 86, 130),
790
+ npcSpot('artisan', 'Artisan', 'resident', 64, 130),
791
+ npcSpot('sergeant', 'Sergeant', 'resident', 126, 94),
792
+ npcSpot('drill-sergeant', 'Drill Sergeant', 'resident', 130, 110),
793
+ npcSpot('priest', 'Priest', 'resident', 194, 14),
794
+ npcSpot('brother-almoner', 'Brother Almoner', 'resident', 252, 14),
795
+ npcSpot('king', 'The King', 'resident:still', 200, AVENUE_V),
796
+ npcSpot('rimdel', 'Rimdel', 'rimdel', 197, AVENUE_V - 3),
797
+ npcSpot('royal-clerk', 'Royal Clerk', 'resident', 262, 67),
798
+ npcSpot('townsman-w', 'Retired captain', 'resident', 236, 95),
799
+ npcSpot('townswoman-e', "Clerk's widow", 'resident', 248, 95),
800
+ npcSpot('noble-blue', 'Blue Manor noble', 'resident', 182, 116),
801
+ npcSpot('noble-green', 'Green Manor noble', 'resident', 234, 116),
802
+ npcSpot('noble-red', 'Red Manor noble', 'resident', 182, 154),
803
+ npcSpot('noble-purple', 'Purple Manor noble', 'resident', 234, 154),
175
804
  ];
805
+ /**
806
+ * The Capital's guards (2026-09-21, redesigned 2026-09-25): level 40 in
807
+ * steel, in pairs, 14 of them. Inside the Great Gate, on the Audience Court
808
+ * either side of the keep's door, just inside the throne hall's door, inside
809
+ * the East Gate, at the harbour end of its bridge, and in the courts of the
810
+ * two barred gates. `faceX`/`faceZ` is what each pair watches. The hall pair
811
+ * is `still`: the hall's furniture is client-side and a pacing guard would
812
+ * walk through the feast tables. The first post is the admin links' Great
813
+ * Gate guard; keep it first.
814
+ */
815
+ export const CAPITAL_GUARD_POSTS = [
816
+ { id: 'west-gate-n', x: CAPITAL_INNER_MIN_X + 3, z: CAPITAL_AVENUE_Z - 4, faceX: CAPITAL_MIN_X, faceZ: CAPITAL_AVENUE_Z },
817
+ { id: 'west-gate-s', x: CAPITAL_INNER_MIN_X + 3, z: CAPITAL_AVENUE_Z + 4, faceX: CAPITAL_MIN_X, faceZ: CAPITAL_AVENUE_Z },
818
+ { id: 'keep-court-n', x: CAPITAL_KEEP.x - CASTLE_KEEP_HALF_X - 1, z: CAPITAL_AVENUE_Z - 3, faceX: CAPITAL_KING_STATUE.x, faceZ: CAPITAL_AVENUE_Z },
819
+ { id: 'keep-court-s', x: CAPITAL_KEEP.x - CASTLE_KEEP_HALF_X - 1, z: CAPITAL_AVENUE_Z + 3, faceX: CAPITAL_KING_STATUE.x, faceZ: CAPITAL_AVENUE_Z },
820
+ // One tile in from the hall's west wall (CASTLE_HALL_MIN_DX in constants.ts is -3).
821
+ { id: 'throne-hall-n', x: CAPITAL_KEEP.x - 2, z: CAPITAL_AVENUE_Z - 3, faceX: CAPITAL_KEEP.x - CASTLE_KEEP_HALF_X, faceZ: CAPITAL_AVENUE_Z, still: true },
822
+ { id: 'throne-hall-s', x: CAPITAL_KEEP.x - 2, z: CAPITAL_AVENUE_Z + 3, faceX: CAPITAL_KEEP.x - CASTLE_KEEP_HALF_X, faceZ: CAPITAL_AVENUE_Z, still: true },
823
+ { id: 'east-gate-n', x: CAPITAL_INNER_MAX_X - 1, z: CAPITAL_AVENUE_Z - 4, faceX: CAPITAL_MAX_X, faceZ: CAPITAL_AVENUE_Z },
824
+ { id: 'east-gate-s', x: CAPITAL_INNER_MAX_X - 1, z: CAPITAL_AVENUE_Z + 4, faceX: CAPITAL_MAX_X, faceZ: CAPITAL_AVENUE_Z },
825
+ { id: 'harbour-bridge-n', x: CAPITAL_MOAT_MAX_X + 3, z: CAPITAL_AVENUE_Z - 5, faceX: CAPITAL_MAX_X, faceZ: CAPITAL_AVENUE_Z },
826
+ { id: 'harbour-bridge-s', x: CAPITAL_MOAT_MAX_X + 3, z: CAPITAL_AVENUE_Z + 5, faceX: CAPITAL_MAX_X, faceZ: CAPITAL_AVENUE_Z },
827
+ { id: 'north-gate-w', x: CAPITAL_ROYAL_WAY_X - 4, z: CAPITAL_NORTH_Z + 7, faceX: CAPITAL_NORTH_GATE.x, faceZ: CAPITAL_NORTH_GATE.z },
828
+ { id: 'north-gate-e', x: CAPITAL_ROYAL_WAY_X + 4, z: CAPITAL_NORTH_Z + 7, faceX: CAPITAL_NORTH_GATE.x, faceZ: CAPITAL_NORTH_GATE.z },
829
+ { id: 'south-gate-w', x: CAPITAL_ROYAL_WAY_X - 4, z: CAPITAL_SOUTH_Z - 8, faceX: CAPITAL_SOUTH_GATE.x, faceZ: CAPITAL_SOUTH_GATE.z },
830
+ { id: 'south-gate-e', x: CAPITAL_ROYAL_WAY_X + 4, z: CAPITAL_SOUTH_Z - 8, faceX: CAPITAL_SOUTH_GATE.x, faceZ: CAPITAL_SOUTH_GATE.z },
831
+ ];
832
+ const STRONGBOX_FRONT = capTile(74, 91);
833
+ const CROWN_CROSS = capTile(WAY_U, 80);
834
+ const WEST_MOUTH = capTile(CAPITAL_WEST_MOUTH.u, CAPITAL_WEST_MOUTH.v);
835
+ const EAST_MOUTH = capTile(CAPITAL_EAST_MOUTH.u, CAPITAL_EAST_MOUTH.v);
836
+ function budget(id, from, to, max, planned) {
837
+ return { id, from, to, max, planned };
838
+ }
839
+ export const CAPITAL_WALK_BUDGETS = [
840
+ // The land arrival: everything a traveller needs within 40 tiles of the gate mouth.
841
+ budget('west-gate-to-outpost-chest', WEST_MOUTH, 'chest-outpost', 20, 16),
842
+ budget('west-gate-to-notice-board', WEST_MOUTH, 'board-gate', 20, 15),
843
+ budget('west-gate-to-supply-depot', WEST_MOUTH, 'wg-depot', 25, 22),
844
+ budget('west-gate-to-inn-stove', WEST_MOUTH, 'stove-inn', 30, 24),
845
+ budget('west-gate-to-inn', WEST_MOUTH, 'wg-inn', 35, 31),
846
+ budget('west-gate-to-ranger-hall', WEST_MOUTH, 'wg-ranger', 40, 37),
847
+ budget('west-gate-to-crown-cross', WEST_MOUTH, CROWN_CROSS, 152, 150),
848
+ // The forge loop: every furnace and anvil within 15 of the Smiths' Strongbox, every mound within 20.
849
+ budget('strongbox-to-furnace-mf-w', STRONGBOX_FRONT, 'furnace-mf-w', 15, 14),
850
+ budget('strongbox-to-furnace-mf-e', STRONGBOX_FRONT, 'furnace-mf-e', 15, 14),
851
+ budget('strongbox-to-anvil-mf-w', STRONGBOX_FRONT, 'anvil-mf-w', 15, 12),
852
+ budget('strongbox-to-anvil-mf-e', STRONGBOX_FRONT, 'anvil-mf-e', 15, 12),
853
+ budget('strongbox-to-furnace-smelter', STRONGBOX_FRONT, 'furnace-smelter', 15, 12),
854
+ budget('strongbox-to-furnace-smith', STRONGBOX_FRONT, 'furnace-smith', 15, 12),
855
+ budget('strongbox-to-anvil-smith', STRONGBOX_FRONT, 'anvil-smith', 15, 12),
856
+ budget('strongbox-to-furnace-armour', STRONGBOX_FRONT, 'furnace-armour', 15, 12),
857
+ budget('strongbox-to-anvil-armour', STRONGBOX_FRONT, 'anvil-armour', 15, 12),
858
+ budget('strongbox-to-anvil-shed', STRONGBOX_FRONT, 'anvil-shed', 15, 11),
859
+ budget('strongbox-to-mound-1', STRONGBOX_FRONT, 'mound-1', 20, 18),
860
+ budget('strongbox-to-mound-2', STRONGBOX_FRONT, 'mound-2', 20, 18),
861
+ budget('strongbox-to-mound-3', STRONGBOX_FRONT, 'mound-3', 20, 18),
862
+ // The sea arrival: straight to the King's Fountain, no detour (Chebyshev 124).
863
+ budget('east-gate-to-harbour-board', EAST_MOUTH, 'board-harbour', 10, 7),
864
+ budget('east-gate-to-royal-administration', EAST_MOUTH, 'kp-admin', 20, 17),
865
+ budget('east-gate-to-kings-fountain', EAST_MOUTH, { ...capTile(capU(CAPITAL_KINGS_FOUNTAIN.x), capV(CAPITAL_KINGS_FOUNTAIN.z)), range: 3 }, 130, 123),
866
+ // From the Crown Cross every district is one screen away.
867
+ budget('crown-cross-to-muster-campfire', CROWN_CROSS, 'fire-muster', 30, 27),
868
+ budget('crown-cross-to-barracks', CROWN_CROSS, 'ml-barracks', 35, 31),
869
+ budget('crown-cross-to-keep', CROWN_CROSS, 'kp-keep', 36, 32),
870
+ budget('crown-cross-to-guild-hall', CROWN_CROSS, 'rw-guild', 40, 37),
871
+ budget('crown-cross-to-capital-bank', CROWN_CROSS, 'tr-bank', 52, 47),
872
+ budget('crown-cross-to-nearest-dummy', CROWN_CROSS, 'dummy-a5', 58, 53),
873
+ budget('crown-cross-to-farthest-dummy', CROWN_CROSS, 'dummy-b0', 58, 57),
874
+ budget('crown-cross-to-grand-exchange', CROWN_CROSS, 'tr-ge', 60, 54),
875
+ budget('crown-cross-to-temple', CROWN_CROSS, 'tg-temple', 62, 57),
876
+ budget('crown-cross-to-healing-shrine', CROWN_CROSS, 'shrine-healing', 78, 72),
877
+ ];
878
+ /* ------------------------------------------------------------------ *
879
+ * Predicates.
880
+ * ------------------------------------------------------------------ */
176
881
  export function inCapitalWalls(x, z) {
177
882
  return x >= CAPITAL_MIN_X && x <= CAPITAL_MAX_X && z >= CAPITAL_NORTH_Z && z <= CAPITAL_SOUTH_Z;
178
883
  }
@@ -185,14 +890,6 @@ export function onCapitalWall(x, z) {
185
890
  export function inCapitalInterior(x, z) {
186
891
  return inCapitalWalls(x, z) && !onCapitalWall(x, z);
187
892
  }
188
- /** The passage tiles through each gate, the wall's own rows only. */
189
- export function inCapitalGate(x, z) {
190
- if (!onCapitalWall(x, z))
191
- return false;
192
- if (x < CAPITAL_INNER_MIN_X || x > CAPITAL_INNER_MAX_X)
193
- return Math.abs(z - CAPITAL_AVENUE_Z) <= CAPITAL_GATE_HALF_W;
194
- return Math.abs(x - CAPITAL_NORTH_GATE.x) <= CAPITAL_GATE_HALF_W;
195
- }
196
893
  export function onCapitalBridge(x, z) {
197
894
  return x >= CAPITAL_BRIDGE_MIN_X && x <= CAPITAL_BRIDGE_MAX_X && Math.abs(z - CAPITAL_AVENUE_Z) <= CAPITAL_BRIDGE_HALF_W;
198
895
  }
@@ -209,7 +906,7 @@ function shoreHash(x, z) {
209
906
  * is a rounded basin (an ellipse from the gate) rather than a box.
210
907
  */
211
908
  export function inCapitalMoat(x, z) {
212
- if (inCapitalWalls(x, z) || onCapitalBridge(x, z))
909
+ if (inCapitalWalls(x, z) || onCapitalBridge(x, z) || onCapitalSouthBridge(x, z))
213
910
  return false;
214
911
  const dx = Math.max(CAPITAL_MIN_X - x, 0, x - CAPITAL_MAX_X);
215
912
  const dz = Math.max(CAPITAL_NORTH_Z - z, 0, z - CAPITAL_SOUTH_Z);
@@ -222,7 +919,8 @@ export function inCapitalMoat(x, z) {
222
919
  if (d <= CAPITAL_MOAT_W - 2)
223
920
  return true;
224
921
  // Wobble: a smooth-ish threshold from two neighbouring hashes.
225
- const w = (shoreHash(Math.floor(x / 3), Math.floor(z / 3)) + shoreHash(Math.floor(x / 5) + 7, Math.floor(z / 5) + 3)) / 2;
922
+ const hz = legacyZ(x, z);
923
+ const w = (shoreHash(Math.floor(x / 3), Math.floor(hz / 3)) + shoreHash(Math.floor(x / 5) + 7, Math.floor(hz / 5) + 3)) / 2;
226
924
  const reach = CAPITAL_MOAT_W - 2 + Math.round(w * 4);
227
925
  if (d <= reach)
228
926
  return true;
@@ -231,7 +929,7 @@ export function inCapitalMoat(x, z) {
231
929
  const ex = (x - (CAPITAL_MIN_X - 1)) / (CAPITAL_MIN_X - 1 - CAPITAL_POOL.minX + 1);
232
930
  const ez = (z - CAPITAL_AVENUE_Z) / ((CAPITAL_POOL.maxZ - CAPITAL_POOL.minZ) / 2 + 2);
233
931
  if (x < CAPITAL_MIN_X && ex * ex + ez * ez <= 1) {
234
- const w = shoreHash(Math.floor(x / 2), Math.floor(z / 2));
932
+ const w = shoreHash(Math.floor(x / 2), Math.floor(legacyZ(x, z) / 2));
235
933
  return ex * ex + ez * ez <= 0.82 + w * 0.18;
236
934
  }
237
935
  return false;
@@ -246,61 +944,134 @@ export function capitalMoundHeight(x, z) {
246
944
  const d = Math.max(dx, dz);
247
945
  return Math.max(CAPITAL_FLOOR_H, CAPITAL_MOUND_H - d);
248
946
  }
249
- export function inCapitalKeep(x, z) {
250
- return Math.abs(x - CAPITAL_KEEP.x) <= CAPITAL_KEEP_HALF_X && Math.abs(z - CAPITAL_KEEP.z) <= CAPITAL_KEEP_HALF_Z;
947
+ function inWorldRect(r, x, z) {
948
+ return x >= r.minX && x <= r.maxX && z >= r.minZ && z <= r.maxZ;
949
+ }
950
+ /** The last row containing the tile (later rows win, as they paint). */
951
+ function lastAt(rows, x, z, keep) {
952
+ for (let i = rows.length - 1; i >= 0; i--) {
953
+ const r = rows[i];
954
+ if (inWorldRect(r, x, z) && (!keep || keep(r)))
955
+ return r;
956
+ }
957
+ return undefined;
958
+ }
959
+ /**
960
+ * The route on a tile, in paint order: the Royal Way over the primaries over
961
+ * the lanes and walks. Interior tiles only.
962
+ */
963
+ export function capitalRouteAt(x, z) {
964
+ if (!inCapitalInterior(x, z))
965
+ return undefined;
966
+ return (lastAt(CAPITAL_ROUTES, x, z, (r) => r.kind === 'way') ??
967
+ lastAt(CAPITAL_ROUTES, x, z, (r) => r.kind === 'primary') ??
968
+ lastAt(CAPITAL_ROUTES, x, z, (r) => r.kind === 'lane' || r.kind === 'walk'));
969
+ }
970
+ export function capitalPlazaAt(x, z) {
971
+ return inCapitalInterior(x, z) ? lastAt(CAPITAL_PLAZAS, x, z) : undefined;
251
972
  }
973
+ export function capitalGreenAt(x, z) {
974
+ return inCapitalInterior(x, z) ? lastAt(CAPITAL_GREENS, x, z) : undefined;
975
+ }
976
+ export function capitalYardAt(x, z) {
977
+ return inCapitalInterior(x, z) ? lastAt(CAPITAL_YARDS, x, z) : undefined;
978
+ }
979
+ /** A kerb row: the outer rows of a kerbed route, across its run (the Way's are its outer columns). */
980
+ function onKerb(r, x, z) {
981
+ if (!r.kerb)
982
+ return false;
983
+ const alongX = r.maxX - r.minX >= r.maxZ - r.minZ;
984
+ return alongX ? z === r.minZ || z === r.maxZ : x === r.minX || x === r.maxX;
985
+ }
986
+ /**
987
+ * What the plan lays on an interior tile, in the builder's paint order
988
+ * (spec G.2): the Royal Way, then the primaries, the lanes and walks, the
989
+ * plazas, the mound (plateau Stone, banks Grass), the greens and yards, and
990
+ * lawn under everything. Kerb rows of the kerbed routes are Path. Buildings
991
+ * floor their own footprints on top (buildingFloorSurface); the gate
992
+ * passages and the keep are the builder's. What is not built in this phase
993
+ * is not laid: an apron whose building does not stand yet, a yard of a later
994
+ * phase (the rects stay in the tables, reserved).
995
+ */
996
+ export function capitalGroundAt(x, z) {
997
+ if (!inCapitalInterior(x, z))
998
+ return undefined;
999
+ const route = capitalRouteAt(x, z);
1000
+ if (route)
1001
+ return { layer: route.kind, id: route.id, surface: onKerb(route, x, z) ? SurfaceType.Path : capitalSurface(route.surface) };
1002
+ // An apron whose building is not standing yet is not laid (the row keeps
1003
+ // its rect reserved, and capitalPlazaAt still answers for it); the next
1004
+ // plaza under it, or the ground, shows.
1005
+ const plaza = lastAt(CAPITAL_PLAZAS, x, z, apronLaidNow);
1006
+ if (plaza) {
1007
+ // A green lying wholly inside a plaza is an island in it (the Royal
1008
+ // Square's hedged lawn, the Drill Square's tree islands), so it stays
1009
+ // grass rather than being paved over.
1010
+ const island = capitalGreenAt(x, z);
1011
+ if (island && island.u0 >= plaza.u0 && island.u1 <= plaza.u1 && island.v0 >= plaza.v0 && island.v1 <= plaza.v1) {
1012
+ return { layer: 'green', id: island.id, surface: SurfaceType.Grass };
1013
+ }
1014
+ return { layer: 'plaza', id: plaza.id, surface: capitalSurface(plaza.surface) };
1015
+ }
1016
+ if (capitalMoundHeight(x, z) > CAPITAL_FLOOR_H) {
1017
+ return inCapitalPlateau(x, z) ? { layer: 'plateau', surface: SurfaceType.Stone } : { layer: 'mound', surface: SurfaceType.Grass };
1018
+ }
1019
+ const green = capitalGreenAt(x, z);
1020
+ if (green)
1021
+ return { layer: 'green', id: green.id, surface: SurfaceType.Grass };
1022
+ // A yard of a later phase is not laid yet: lawn until then.
1023
+ const yard = lastAt(CAPITAL_YARDS, x, z, (y) => (y.phase ?? 1) <= CAPITAL_BUILD_PHASE);
1024
+ if (yard)
1025
+ return { layer: 'yard', id: yard.id, surface: capitalSurface(yard.surface) };
1026
+ return { layer: 'lawn', surface: SurfaceType.Grass };
1027
+ }
1028
+ /** The landmarks standing in this phase (as themselves or a stand-in), for the aprons. */
1029
+ let standingLandmarks = null;
1030
+ /** Whether a plaza row is laid now: every plaza but an apron for a building not standing yet. */
1031
+ function apronLaidNow(p) {
1032
+ if (!p.forLandmark)
1033
+ return true;
1034
+ if (!standingLandmarks)
1035
+ standingLandmarks = new Set(CAPITAL_LANDMARKS.filter((row) => capitalPlacementNow(row) !== null).map((row) => row.id));
1036
+ return standingLandmarks.has(p.forLandmark);
1037
+ }
1038
+ /** The ring lane inside the wall. */
252
1039
  export function onCapitalRingRoad(x, z) {
253
1040
  if (!inCapitalInterior(x, z))
254
1041
  return false;
255
- const lo = CAPITAL_INNER_MIN_X + CAPITAL_RING_INSET;
256
- const hi = CAPITAL_INNER_MAX_X - CAPITAL_RING_INSET;
257
- const n = CAPITAL_INNER_NORTH_Z + CAPITAL_RING_INSET;
258
- const s = CAPITAL_INNER_SOUTH_Z - CAPITAL_RING_INSET;
259
- return (Math.abs(x - lo) <= CAPITAL_STREET_HALF_W || Math.abs(x - hi) <= CAPITAL_STREET_HALF_W ||
260
- Math.abs(z - n) <= CAPITAL_STREET_HALF_W || Math.abs(z - s) <= CAPITAL_STREET_HALF_W);
261
- }
262
- /** The Royal Way, from the gate to the inner gate on the plateau's edge. */
1042
+ return CAPITAL_ROUTES.some((r) => r.id.startsWith('ring-') && inWorldRect(r, x, z));
1043
+ }
1044
+ /** The Royal Way's paving (u 149-155, the North Gate to the South Gate). */
1045
+ export function onCapitalWay(x, z) {
1046
+ return capitalRouteAt(x, z)?.kind === 'way';
1047
+ }
1048
+ /**
1049
+ * The keep axis: the King's Road's rows from the Great Gate to the Inner
1050
+ * Gate on the plateau's edge (it crosses the Way at the Crown Cross and
1051
+ * climbs the ramp).
1052
+ */
263
1053
  export function onCapitalAvenue(x, z) {
264
1054
  if (!inCapitalInterior(x, z))
265
1055
  return false;
266
1056
  return Math.abs(z - CAPITAL_AVENUE_Z) <= CAPITAL_AVENUE_HALF_W && x <= CAPITAL_INNER_GATE.x;
267
1057
  }
268
- /** A street tile: the Way, the ring road, the cross streets and the district streets, off the mound. */
269
- /** A primary route tile (not the Way): the wide streets to the market, garrison and temple. */
1058
+ /** A primary route tile (Crown Street, the King's Road and its ramp, the Harbour Road); not the Way. */
270
1059
  export function onCapitalPrimary(x, z) {
271
- if (!inCapitalInterior(x, z) || capitalMoundHeight(x, z) > CAPITAL_FLOOR_H)
1060
+ if (!inCapitalInterior(x, z))
272
1061
  return false;
273
- for (const r of CAPITAL_PRIMARY_ROUTES) {
274
- if (r.x !== undefined && Math.abs(x - r.x) <= CAPITAL_PRIMARY_HALF_W && z >= r.min && z <= r.max)
275
- return true;
276
- if (r.z !== undefined && Math.abs(z - r.z) <= CAPITAL_PRIMARY_HALF_W && x >= r.min && x <= r.max)
277
- return true;
278
- }
279
- return false;
1062
+ return CAPITAL_ROUTES.some((r) => r.kind === 'primary' && inWorldRect(r, x, z));
280
1063
  }
1064
+ /** A street tile: any route in the table (the Way, the primaries, the lanes and walks, the ring lane). */
281
1065
  export function onCapitalStreet(x, z) {
282
- if (!inCapitalInterior(x, z))
283
- return false;
284
- if (onCapitalAvenue(x, z) || onCapitalRingRoad(x, z) || onCapitalPrimary(x, z))
285
- return true;
286
- if (capitalMoundHeight(x, z) > CAPITAL_FLOOR_H)
287
- return false;
288
- for (const sx of CAPITAL_CROSS_STREET_XS)
289
- if (Math.abs(x - sx) <= CAPITAL_STREET_HALF_W)
290
- return true;
291
- for (const st of CAPITAL_DISTRICT_STREETS) {
292
- if (x >= st.minX && x <= st.maxX && Math.abs(z - st.z) <= CAPITAL_STREET_HALF_W)
293
- return true;
294
- }
295
- return false;
1066
+ return capitalRouteAt(x, z) !== undefined;
1067
+ }
1068
+ /** A plaza, square, court or door apron tile. */
1069
+ export function onCapitalPlaza(x, z) {
1070
+ return capitalPlazaAt(x, z) !== undefined;
296
1071
  }
297
1072
  export function capitalDistrictAt(x, z) {
298
1073
  if (!inCapitalInterior(x, z))
299
1074
  return undefined;
300
- // Castle first: it overlaps the temple and noble bounds by design.
301
- const castle = CAPITAL_DISTRICTS.find((d) => d.id === 'capital-castle');
302
- if (x >= castle.minX && x <= castle.maxX && z >= castle.minZ && z <= castle.maxZ)
303
- return castle;
304
- return CAPITAL_DISTRICTS.find((d) => x >= d.minX && x <= d.maxX && z >= d.minZ && z <= d.maxZ);
1075
+ return CAPITAL_DISTRICTS.find((d) => inWorldRect(d, x, z));
305
1076
  }
306
1077
  //# sourceMappingURL=capital.js.map