-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmap_data.h
More file actions
732 lines (703 loc) · 39.6 KB
/
Copy pathmap_data.h
File metadata and controls
732 lines (703 loc) · 39.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
// Subworld map data — tile types, structures, cell context.
// Mirrors subworld/map-data.ts (compact).
#pragma once
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <array>
#include <vector>
#include "core/table_guard.h"
#include "macro/biomes.h"
#include "macro/features.h"
#include "macro/landmark_registry.h"
// THE step law: the subworld prices ground in the same units the macro march
// does (biome beds, feature beds, the canopy term, speed = base/√weight).
#include "macro/movement_cost.h"
namespace sm::sub {
constexpr int kCellSize = 1024; // tiles per macro cell
constexpr int kFullSize = kCellSize * 3; // 3×3 grid
// Tile constants for the subworld grid.
enum Tile : std::uint8_t {
TILE_EMPTY = 0, TILE_GRASS, TILE_FIELD, TILE_TREE_DECOR,
TILE_ROAD, TILE_HOUSE, TILE_WALL, TILE_WATER, TILE_SHORE,
TILE_SQUARE, TILE_ROCK,
TILE_COUNT,
};
// ── Ground movement: ONE law, both scales (owner's ruling, 2026-08-30) ─────
// How hard each ground type is to cross — and it is the SAME question the
// macro march answers, so it may not have a second answer. Until now it had
// one: the map priced a cell by bed + canopy (macro/movement_cost.h) and the
// subworld priced a tile by a hand-written multiplier table, two laws about
// one world differing by up to 40 %.
//
// So the tile carries a WEIGHT in the macro law's own units, and its speed is
// that law's own conversion, `terrain_speed_mult` = 1/√weight. Nothing here
// is authored twice: grass IS the meadow bed, a road IS the paved bed, a
// wooded tile IS meadow plus the full canopy term, water IS the unpayable
// water bed. The numbers this produces are the ones the old table already
// had — water 0.447 against its 0.45, scree 0.632 against 0.65, shore 0.707
// against 0.75 — because the table was always this law, written down once
// with its normalisation lost: it read "grass = 1", while the world's base
// (kSubworldWalkTilesPerSecond, derived from the 8 cells/hour macro march at
// bed 1.0) is the ROAD. Restoring that is the one visible change: a road
// finally beats open grass by 41 % instead of 15 %, and grass costs what a
// meadow costs on the map above.
//
// Hard blocking is NOT this table's job: solid structures (walls, houses)
// physically stop and carry bodies through sub/collide.h, which indexes the
// oriented Structure volumes below. The HOUSE/WALL rows here only price the
// unpainted verge tiles hugging a footprint — the solid itself refuses entry.
inline constexpr float kTileGroundWeight[TILE_COUNT] = {
biome_sp_weight(Meadow), // TILE_EMPTY — open ground
biome_sp_weight(Meadow), // TILE_GRASS
feature_bed_weight(FT_Field), // TILE_FIELD — ploughed bed
biome_sp_weight(Meadow) + kCanopySpWeight, // TILE_TREE_DECOR — full canopy
feature_bed_weight(FT_Road), // TILE_ROAD — the paved bed
// A footprint's verge is not a ground of the world and has no macro bed:
// squeezing along a wall is the WORST going there is, dearer than the
// mountain (5.0) that is the roughest ground a cell can hold. The solid
// itself refuses entry — these price the tiles hugging it.
8.0f, // TILE_HOUSE
11.0f, // TILE_WALL
biome_sp_weight(Water), // TILE_WATER — wading
biome_sp_weight(Swamp), // TILE_SHORE — wet sand
feature_bed_weight(FT_Road), // TILE_SQUARE — paved square
biome_sp_weight(Mountain), // TILE_ROCK — scree
};
static_assert(sizeof(kTileGroundWeight) / sizeof(float) == std::size_t(TILE_COUNT),
"every Tile id needs exactly one ground weight");
// ── GRIP: what the ground gives the feet to push and brake against ────────
// The second thing a surface says about walking on it, and the reason the
// player stopped feeling like he was on ice: a body cannot change its
// velocity faster than the ground lets it. 1.0 is honest footing (paved
// stone, packed earth); soft or wet ground gives less. Both halves of the
// change are scaled by it — you neither sprint away nor pull up short on a
// slick surface — which is what makes the ice the owner asked about
// (2026-08-30: «если потом в игре добавим лёд, то скольжение по льду
// бесплатное») exactly one row: TILE_ICE with grip near zero, and every body
// in the world slides on it, with no code anywhere knowing what ice is.
inline constexpr float kTileGroundGrip[TILE_COUNT] = {
1.00f, // TILE_EMPTY — bare ground
0.90f, // TILE_GRASS — turf gives a little
0.80f, // TILE_FIELD — broken soil underfoot
0.80f, // TILE_TREE_DECOR — roots and litter
1.00f, // TILE_ROAD — what a road IS for
1.00f, // TILE_HOUSE — a floor is a floor
1.00f, // TILE_WALL
0.55f, // TILE_WATER — nothing to push against but water
0.70f, // TILE_SHORE — wet sand
1.00f, // TILE_SQUARE — paving
0.85f, // TILE_ROCK — scree shifts underfoot
};
static_assert(sizeof(kTileGroundGrip) / sizeof(float) == std::size_t(TILE_COUNT),
"every Tile id needs exactly one grip row");
// The speeds themselves — the macro law's own conversion applied to the WEIGHT
// row, built once. Not `constexpr`: it needs a square root, and this toolchain's
// std::sqrt is not usable in a constant expression (measured, not assumed). So
// it is one static initialisation rather than per-query arithmetic in the
// mover — and every reader is inside a function body, so nothing can read it
// before it exists.
inline const std::array<float, std::size_t(TILE_COUNT)> kTileMovementSpeed = [] {
std::array<float, std::size_t(TILE_COUNT)> out{};
for (std::size_t i = 0; i < std::size_t(TILE_COUNT); ++i) {
out[i] = terrain_speed_mult(kTileGroundWeight[i]);
}
return out;
}();
enum class SubworldMode : std::uint8_t {
Open, City, Village, Forest, Mountain, Swamp, Ruin, Water, Grassland, Road, Spire,
Field,
// An INTERIOR scene (house / castle / cave floor) projected behind a door
// of some subworld structure — not the open air of its macro cell. Routed
// to the self-contained sub/dgn/ generators; never produced by the macro
// resolver (see CellContext::DungeonRef).
Dungeon,
};
// The landmark vocabulary is LandmarkType (macro/landmark_registry.h) — the
// one registry enum. A private five-value "CellLandmarkKind" lived here until
// 2026-08-24 (second copy; the fauna router carried a third), joined by a
// hand-written bridge in engine.cpp — and Lair/Shrine/Mine/Tower physically
// could not reach the microworld (canon-audit C1).
// Dungeon door reference. When `kind` is non-None the window cell is an
// INTERIOR scene entered through a door standing on macro cell (cx, cy) —
// not that cell's open air. The subworld engine synthesises these contexts
// for its dungeon session (the dungeon is a projection OF the subworld, the
// same way the subworld is a projection of the macro map); the macro
// resolver never sets one. Identity is {cell, ordinal, level}: ordinal is
// the building's deterministic index within its cell's generation order, so
// the same seed always opens the same interior behind the same door.
struct DungeonRef {
enum Kind : std::uint8_t {
None = 0,
House = 1, // a settlement building's interior
Cave = 2, // a cavern behind a mouth in rock — the first
// interior that is nobody's property
SpireTower = 3, // the spire's tower: storeys climbed bottom to top,
// ordinal = the spire spell's TIER = storey count
Void = 0xFF, // sealed filler for the dungeon window's ring cells
};
std::uint8_t kind = None;
// Storey, SIGNED: 0 = the level the door opens onto, +1 up, -1 a cellar.
// Stairs re-enter the same identity at level ± 1 (sub/dgn/dispatch.h).
std::int8_t level = 0;
std::uint16_t ordinal = 0; // building index within its owning cell
// Exterior footprint half-extents (tiles) of the entered building — the
// interior keeps the building's real proportions and scales them up
// (M&M scale, owner ruling 2026-08-12: inside is roomier than outside).
float footHx = 0.0f;
float footHy = 0.0f;
};
// The landmark standing on a cell — ONE payload, not a field per fact
// (owner ruling 2026-08-14: at two tenants ad-hoc fields were fine, at five
// they fold). A new landmark fact is a member here, not another CellContext
// column.
struct LandmarkContext {
LandmarkType kind = LandmarkType::None;
int id = -1; // the landmark's id within its kind; -1 = none
// Strength/size in the landmark's own currency: a settlement's population,
// a spire's spell tier (asked from the spell registry at resolve).
int size = 0;
// Owning kingdom (index into Politik::kingdoms; -1 = none / unowned). The
// subworld does not know what a kingdom IS — it carries the index so the
// engine can resolve the citizens' faction through the one macro resolver
// (faction_index_for_kingdom), instead of every settlement in the world
// fielding imperial guards.
int kingdomIdx = -1;
// Consumed (today: a depleted spire — the orb is gone and its light with
// it). Generation reads it like any other context fact, so the scene
// truthfully shows what the macro world remembers.
bool depleted = false;
};
// CellContext — what the macroworld knows about a single cell.
struct CellContext {
// WHICH CELL OF THE WORLD this is — the wrapped macro index, [0, worldCells).
// It used to be the window's running counter, which never wrapped: walk east
// off the last column and the cell called itself 1024 while reading macro
// cell 0's biome. Everything downstream that keys off the number — the
// detail-noise offset, the road anchor seed, the tile hash — therefore built
// a DIFFERENT subworld for the same place depending on how you arrived
// (measured: 100 % of tiles differ, up to 63 m of height, road anchors 146 m
// apart). A cell is a place, so its name is its place (CANON.md S1/S2).
int cx, cy;
// How big the world this cell belongs to is, in cells. The generator needs
// it to close its noise on the world rather than on nothing — the same law
// the macro layer follows. 0 = a bare fixture with no world around it.
int worldCellsX = 0, worldCellsY = 0;
float macroHeight; // 0..1
float macroTemperature = 0.5f; // 0..1, used for TS tree species bands
Biome biome;
// The unflooded ground of a Water cell: what its RAW climate would grow
// here had the river/lake not covered it (biome_from_climate of the raw
// temperature/moisture channels — NOT the seasoned macroTemperature
// above: a bank never reclassifies in winter, exactly as biome_at_cell
// never does). The macro resolver fills it; read it ONLY through
// ground_biome() below, which consults it for Water cells alone — land
// answers with its biome, so a hand-built context cannot drift the two
// apart. This is what paints a water cell's DRY margin (its banks): the
// ground-material dither blends ground aliases, never "water bed", so
// the old scan-order fallback that walled the grass mid-cell is gone
// (owner report 2026-08-29). Meadow default keeps bare fixtures green.
Biome groundBiome = Biome::Meadow;
FeatureType feature;
LandmarkContext landmark{};
std::uint32_t seed;
// Macro TreeLayer count for this cell (0..16384); -1 = unknown — the
// generator re-derives it from biome/features (tests, bare resolvers).
int treeCount = -1;
// Furrow orientation of this cell's ploughed-field material, resolved at
// macro resolve from the WRAPPED cell coords (field_furrows_vertical,
// sub/material.h — the twin of the map's macro.frag furrow hash). Carried
// like macroTemperature: generation ignores it, the renderer's material
// grid consumes it, so what the map paints is what lies underfoot.
bool fieldFurrowsVert = false;
// The cell's fertility (the macro moisture channel — the same number the
// field stamp scored cells by). Crop density is contextual on it.
float fertility01 = 0.5f;
// The cell's danger byte (macro/zones.h continuum) — the spawn law's
// zone contribution; 0 when the layer is not wired (silent legal zero).
std::uint8_t zone = 0;
// Live deposit kinds within the profession reach (bit per DepositKind) —
// the street crowd's trade gate (spawn law, macro/fauna.h).
std::uint8_t depositsNear = 0;
// The WORLD seed (not the per-cell hash above): seam-symmetric features
// — the field-plot lattice — need a seed every neighbour derives
// identically, or the two sides of a shared plot disagree. 0 in bare
// test contexts keeps them deterministic.
std::uint32_t worldSeed = 0;
// Stands the sickle has taken from this cell and regrowth has not yet
// returned (GameState::cropOverrides — the crop_count harvest scar).
// The crop scatter plants its natural yield minus this, so returning to
// a harvested field does NOT resurrect the wheat. 0 = unscarred.
int cropHarvested = 0;
// Interior-scene door (see DungeonRef). kind == None for every real
// macro cell; the engine's dungeon session is the only writer.
DungeonRef dungeon{};
};
// The effective landmark of a cell for terrain purposes. A macro settlement
// projects as a City cell even when landmark.kind is None (mirrors
// resolve_mode's `landmark.id >= 0` branch) — one helper so terrain
// flattening and mode resolution can never disagree.
inline LandmarkType effective_landmark(const CellContext& ctx) {
if (ctx.landmark.kind != LandmarkType::None) return ctx.landmark.kind;
if (ctx.landmark.id >= 0) return LandmarkType::City;
return LandmarkType::None;
}
// The GROUND a cell shows where it is dry — the effective_landmark pattern
// for the material dither. Land is its biome (groundBiome is never consulted,
// so it cannot drift); a flooded cell answers with its unflooded climate
// ground. Never Water: the ring built from this is what lets the dither drop
// its water special-casing entirely (sub/material.h).
inline Biome ground_biome(const CellContext& ctx) {
return ctx.biome == Biome::Water ? ctx.groundBiome : ctx.biome;
}
// ── What a prop DOES when the player presses E on it ────────────────────────
// The world is built in two passes: generators place PROPS (geometry — trees,
// walls, houses, doors, lanterns), then this one column says which of them are
// also INTERACTIVE and with what verb. Adding "drink from the well" is a row
// in the prop table plus a case in the one dispatcher — never a new system, and
// never a special case in the engine's input path.
enum class InteractId : std::uint8_t {
None = 0,
// Step through into the interior behind this door (sub/dgn). The prop's
// `tag` names WHICH building: the house's ordinal within its cell.
Door,
// Take the shaft this prop stands on: `tag` 1 = up, 0 = down.
Stairs,
// The corpse of something you killed — not a composite prop but an ECS
// body; it shares the verb so one prompt and one keypress serve both.
Loot,
// Drink: the well at the heart of a village. It pays in the ONE currency
// travel spends — stamina — because the world already prices a journey
// in SP and a drink is what shortens one.
Drink,
// Read: a signpost. It costs nothing and changes nothing; it tells you
// where you are. Extensibility is not only about mechanics: a verb with
// no consequence proves the table carries flavour as cheaply as force.
Read,
// A household chest: what it holds is not invented, it is TAKEN from the
// store of the place that owns the house (owner ruling W2: a landmark's
// store IS its inventory), so an emptied town has empty chests and a
// chest cannot be farmed by walking out and back in.
Search,
// Learn: the spire orb. The one interaction only the PLAYER can mean —
// it writes into his spell book — but the mechanism is the ordinary verb
// row: any future knowledge-granting prop is a row, not a system.
Learn,
Count,
};
struct InteractRow {
InteractId id; // MUST equal the row's index (guard below)
// Shown in the HUD prompt as "[E] <verb>".
const char* verb;
// How far the player may stand from the prop's surface, in tiles. A door
// is arm's length (the melee reach every other contact uses); a corpse is
// picked up from a little further because you loot what fell around you.
float reachTiles;
};
// Indexed by InteractId; the static_assert refuses a drifted table.
// (Scar: the rows once ran Search/Drink/Read against an enum that ran
// Drink/Read/Search, so the well prompted "Search", the sign "Drink" and the
// chest "Read". Same 5-tile reach on all three hid it from every smoke.)
inline constexpr InteractRow kInteractRows[int(InteractId::Count)] = {
{ InteractId::None, "", 0.0f},
{ InteractId::Door, "Enter", 5.0f},
{ InteractId::Stairs, "Take stairs", 5.0f},
{ InteractId::Loot, "Loot", 12.0f},
{ InteractId::Drink, "Drink", 5.0f},
{ InteractId::Read, "Read", 5.0f},
{ InteractId::Search, "Search", 5.0f},
{ InteractId::Learn, "Learn spell", 5.0f},
};
static_assert(rows_in_enum_order(kInteractRows, &InteractRow::id),
"kInteractRows row order must mirror InteractId");
inline constexpr const InteractRow& interact_row(InteractId i) {
return kInteractRows[int(i) < int(InteractId::Count) ? int(i) : 0];
}
struct Structure {
enum Kind : std::uint8_t { Tree = 0, Rock, House, Wall, Bridge, Crop,
Fence, Furnish, Door, Lantern, Stairs,
Chest, CaveMouth, Well, Sign, SpireGate,
SpireOrb, Kerb } kind;
static constexpr int kKindCount = int(Kerb) + 1;
// Footprint silhouette. Box is the default; Cylinder renders (and collides)
// as a round prism — wall towers, gate jambs, the spire. One byte, not a
// new Kind: shape is orthogonal to what the thing IS.
enum Shape : std::uint8_t { Box = 0, Cylinder = 1 };
float x, y;
float radius; // XZ half-extent (bounding); billboard scale for Tree
float height; // metres; NEGATIVE = decayed/abandoned (map_factory.h)
// ── Universal oriented-box extension (all zero ⇒ legacy square box). ──
float yaw = 0.0f; // rotation about vertical, radians, tile-space CCW
float hx = 0.0f; // half-extent along local X, tiles (0 ⇒ radius)
float hy = 0.0f; // half-extent along local Y, tiles (0 ⇒ radius)
float zBase = 0.0f; // bottom of the solid, metres. By default a lift
// ABOVE the terrain seat (gate lintels: bodies pass
// beneath, stand on top); with `zWorld` it is an
// absolute world height instead — see below.
Shape shape = Box;
// Does this thing sit on the GROUND, or stand at a level of the WORLD?
// Almost everything sits: a house follows the hill under it. But a
// structure over water answers to the water, not to the bed — the sea
// plane is one height everywhere (sub/height.h kSeaLevelM), so a bridge
// deck is level by the same law that makes the water level. Seating such
// a deck on the bed made its top wobble by the DIFFERENCE between two
// samplers — the generator reads the exact tile heightmap, the renderer
// the 16-tile mesh — which is what turned an honest span into a flight
// of steps a walker had to jump. An absolute span cannot drift, because
// nothing under it is consulted at all.
bool zWorld = false;
// Per-INSTANCE payload for the prop's interaction (the kind decides the
// verb, this decides which one of them this is): a door carries its
// building's ordinal, a stair carries its direction. Meaningless — and
// ignored — for a kind with no interaction.
std::uint16_t tag = 0;
};
// ── Structure geometry — the ONE contract shared by the 3D renderer and the
// collision index (sub/collide.h). Both derive a structure's oriented footprint
// and vertical span from these helpers, so what you SEE is exactly what blocks
// and carries you. ──────────────────────────────────────────────────────────
inline float structure_half_x(const Structure& s) {
return s.hx > 0.0f ? s.hx : s.radius;
}
inline float structure_half_y(const Structure& s) {
return s.hy > 0.0f ? s.hy : s.radius;
}
// Squared 2D distance from a point to the SURFACE of a prop's oriented
// footprint (0 inside it). Reach is measured to the surface, never to the
// centre, or a wide building would be unreachable from its own doorstep.
inline float structure_surface_dist2(const Structure& s, float px, float py) {
const float dx = px - s.x;
const float dy = py - s.y;
const float c = std::cos(s.yaw);
const float sn = std::sin(s.yaw);
const float lx = dx * c + dy * sn;
const float ly = -dx * sn + dy * c;
const float qx = std::max(0.0f, std::abs(lx) - structure_half_x(s));
const float qy = std::max(0.0f, std::abs(ly) - structure_half_y(s));
return qx * qx + qy * qy;
}
// ── The ONE per-kind prop row. The size-floor / loot forks that used to live
// here promised to collapse into a table "when environment props land" — the
// Crop kind is that third prop, so here is the table. Adding a structure kind
// = one row; every per-kind question below reads its column. ──
struct StructureKindRow {
// MUST equal the row's index in kStructureKindRows (guard below the table).
Structure::Kind kind;
// Key into the ONE loot registry (`roll_loot_profile`, macro/items.h) —
// the same resolver a kill goes through. Empty id = drops nothing (yet),
// and the harvest door skips the kind entirely.
const char* lootId;
// Degenerate-record floors keeping legacy records visible/solid.
// Formerly renderer literals; shared so collision can never disagree
// with the pixels.
float minHalfXy; // tiles
float minHeight; // metres
// Height at which the kind's loot profile pays verbatim; taller and
// shorter records scale off it (a 20 m mast outpays a tundra scrub, a
// wheat stand pays a stalk's worth). 0 = no yield scaling defined.
float yieldRefHeightM;
// Enters the collision index (sub/collide.h). Trees are deliberately
// non-solid — undergrowth is a battle speed cost, not a wall — and a
// stand of wheat is walked through; Rock is not part of the 3D
// structure pass yet and stays transparent with it.
bool solid;
// Status line the harvest door prints ("" = not harvestable).
const char* harvestMsg;
// Which pass draws this prop. Two hardcoded kind lists used to live in
// the renderer (one per pass), so every new prop was invisible until
// somebody remembered to edit both — the door bug that started this
// system. Now the row says it.
enum class Draw : std::uint8_t {
None = 0, // not drawn yet (Rock)
Billboard, // camera-facing quad from the tree atlas
Solid, // the oriented box / cylinder structure pass
};
Draw draw;
// Solid-pass material — WHAT IT LOOKS LIKE, resolved once per kind and
// handed to the structure shader as the instance's material index. The
// shader has one branch per row (shaders/struct.frag), so giving a new
// prop its own look is a row here and a branch there, never a pipeline.
// Before this it was a single wood/stone BOOL that meant "draw me like a
// house", which painted every door and every bed with the house's red
// ROOF band across its top — a door was a red smear on a wall, and on a
// reddish wall it was nothing at all.
enum class Material : std::uint8_t {
Stone = 0, // ramparts, field balks
House, // tan wall + red roof band (only a building has a roof)
Wood, // planks all over: furniture, decks
Door, // planked leaf with a frame and a handle
Lantern, // dark post with a burning head
Chest, // banded coffer: dark timber with iron straps
CaveMouth, // rock face swallowing a black opening
Well, // wet stone curb over dark water
Sign, // painted board on a post
SpireOrb, // dark plinth crowned by a burning blue orb
};
Material material;
// What pressing E on this prop does (InteractId::None = scenery).
InteractId interact;
// For a prop whose verb is Door: WHICH interior it opens. A column, not
// a branch in the engine — the day a keep or a barrow wants its own
// module, it is one row here and one case in the dungeon dispatch.
DungeonRef::Kind opens;
// Light this prop casts, as 0xRRGGBB + reach in tiles (0 radius = dark).
// The engine hangs a LightEmitter on every lit prop through the ONE
// point-light path (sub/lighting.h), so a lantern lights the street with
// the same code a carried torch lights a corridor.
std::uint32_t lightRgb;
float lightRadiusTiles;
// Metres above the prop's seat the light hangs — a lantern burns at its
// crown, not at the foot of its post.
float lightHeightM;
// ── THE GROUND THIS THING LAYS UNDER A BODY STANDING ON IT ───────────
// The tile id whoever stands on top walks over, so that WHAT CARRIES YOU
// decides how fast you move — not the terrain metres below it. A tile is
// one per column of the map and cannot say "water down there, masonry
// three metres up", which is why a body crossing a bridge deck waded at
// a river's speed while walking on stone (owner, 2026-08-30).
//
// Deliberately a TILE and not a second speed table: the world already has
// exactly one answer to "how fast is this ground" (kTileMovementSpeed),
// and a parallel per-material one would be a second law about the same
// question (CANON S26). A bridge lays road, because a bridge IS road.
//
// `kWalkTileTransparent` = this thing lays nothing and the terrain's own
// tile stands — the silent zero of the law, not a case to branch on.
std::uint8_t walkTile;
};
// "I lay no ground of my own": TILE_COUNT is not a tile, so it can never be
// confused with one (TILE_EMPTY is a real, walkable id).
inline constexpr std::uint8_t kWalkTileTransparent = std::uint8_t(TILE_COUNT);
inline constexpr StructureKindRow kStructureKindRows[Structure::kKindCount] = {
{ Structure::Tree, "tree", 1.6f, 3.5f, 14.0f, false, "You fell a tree",
StructureKindRow::Draw::Billboard,
StructureKindRow::Material::Wood,
InteractId::None, DungeonRef::None, 0u, 0.0f, 0.0f,
kWalkTileTransparent},
{ Structure::Rock, "", 1.6f, 3.5f, 0.0f, false, "",
StructureKindRow::Draw::None,
StructureKindRow::Material::Stone,
InteractId::None, DungeonRef::None, 0u, 0.0f, 0.0f,
kWalkTileTransparent},
{ Structure::House, "", 1.6f, 3.5f, 0.0f, true, "",
StructureKindRow::Draw::Solid,
StructureKindRow::Material::House,
InteractId::None, DungeonRef::None, 0u, 0.0f, 0.0f,
std::uint8_t(TILE_SQUARE)},
{ Structure::Wall, "", 1.2f, 4.0f, 0.0f, true, "",
StructureKindRow::Draw::Solid,
StructureKindRow::Material::Stone,
InteractId::None, DungeonRef::None, 0u, 0.0f, 0.0f,
std::uint8_t(TILE_SQUARE)},
// Bridge: honest masonry (owner, 2026-08-29 — "like the city gates: over
// the water, arches beneath, not a dam"). The generator emits it as
// zBase-lifted deck chords plus round piers (sub/gens/dispatch.cpp
// add_bridge_segment) — the SAME lintel mechanism a gate uses — so it is
// solid (you stand ON the deck top via support_at, walk around a pier)
// and drawn stone by the ordinary box/cylinder pass. Low floors: every
// record states its true size, and a pier must never be inflated.
{ Structure::Bridge, "", 0.4f, 0.5f, 0.0f, true, "",
StructureKindRow::Draw::Solid,
StructureKindRow::Material::Stone,
InteractId::None, DungeonRef::None, 0u, 0.0f, 0.0f,
std::uint8_t(TILE_ROAD)},
{ Structure::Crop, "crop", 0.4f, 0.5f, 1.2f, false, "You harvest the crop",
StructureKindRow::Draw::Billboard,
StructureKindRow::Material::Wood,
InteractId::None, DungeonRef::None, 0u, 0.0f, 0.0f,
kWalkTileTransparent},
// Fence: the field balks' boulder walls — knee-high, honest to walk
// around (solid), drawn by the same box pass as walls in stone flavour.
{ Structure::Fence, "", 0.3f, 0.4f, 0.0f, true, "",
StructureKindRow::Draw::Solid,
StructureKindRow::Material::Stone,
InteractId::None, DungeonRef::None, 0u, 0.0f, 0.0f,
kWalkTileTransparent},
// Furnish: interior furniture (beds, tables, chests — sub/dgn/). Solid so
// a room fights around its furniture; waist-high floor (0.4 m) so a chest
// is never inflated to a pillar by the legacy stub rule. Drawn wood-
// flavoured in the box pass. No loot row yet — a chest that PAYS is a
// future increment through this same row.
{ Structure::Furnish, "", 0.5f, 0.4f, 0.0f, true, "",
StructureKindRow::Draw::Solid,
StructureKindRow::Material::Wood,
InteractId::None, DungeonRef::None, 0u, 0.0f, 0.0f,
kWalkTileTransparent},
// Door: the way in. Not solid — it hangs flush on a wall that already
// blocks, and a door you bump into instead of opening is a door that
// fights the player. A leaf is 2 m tall (a body plus its hat) and half a
// tile wide, which is what makes it READ as a door from the street.
{ Structure::Door, "", 0.5f, 2.0f, 0.0f, false, "",
StructureKindRow::Draw::Solid,
StructureKindRow::Material::Door,
InteractId::Door, DungeonRef::House, 0u, 0.0f, 0.0f,
kWalkTileTransparent},
// Lantern: a post with a flame on top. Solid so it is a real obstacle you
// walk around, knee-thin. Warm 0xFFB060 at 24 tiles — the carried-torch
// family (sub/lighting.h), hung at 3 m: above a body's head, so it lights
// the street rather than the walker's boots.
{ Structure::Lantern, "", 0.3f, 3.0f, 0.0f, true, "",
StructureKindRow::Draw::Solid,
StructureKindRow::Material::Lantern,
InteractId::None, DungeonRef::None, 0xFFB060u, 24.0f, 3.0f,
kWalkTileTransparent},
// Stairs: the shaft between storeys, drawn as a low block you step onto.
// Not solid (you stand ON its tile and press E), knee-high so it reads as
// a flight of steps and not a table.
{ Structure::Stairs, "", 1.5f, 0.5f, 0.0f, false, "",
StructureKindRow::Draw::Solid,
StructureKindRow::Material::Wood,
InteractId::Stairs, DungeonRef::None, 0u, 0.0f, 0.0f,
kWalkTileTransparent},
// Chest: the household's store, waist-high and solid like the furniture
// it is. It has no loot ROW of its own on purpose — a chest does not
// conjure goods, it hands over what the place that owns it actually has
// (macro/economy.h settlement inventory), which is why it cannot be
// farmed and why an emptied town's chests are bare.
{ Structure::Chest, "", 0.8f, 0.9f, 0.0f, true, "",
StructureKindRow::Draw::Solid,
StructureKindRow::Material::Chest,
InteractId::Search, DungeonRef::None, 0u, 0.0f, 0.0f,
kWalkTileTransparent},
// CaveMouth: a dark opening in rock. The SAME door verb as a house's
// leaf — the column below is what makes it open a cavern instead of a
// parlour — so the player learns one key and the engine keeps one path.
// Wide and low, so it reads as a hole rather than a shed, and not solid:
// you walk into the mouth, you do not bump into it.
{ Structure::CaveMouth, "", 2.0f, 3.0f, 0.0f, false, "",
StructureKindRow::Draw::Solid,
StructureKindRow::Material::CaveMouth,
InteractId::Door, DungeonRef::Cave, 0u, 0.0f, 0.0f,
kWalkTileTransparent},
// Well: waist-high stonework you walk around (solid), drawn as a round
// curb because that is what a well IS — the cylinder pass already has
// the shape, so this costs no geometry.
{ Structure::Well, "", 1.6f, 1.2f, 0.0f, true, "",
StructureKindRow::Draw::Solid,
StructureKindRow::Material::Well,
InteractId::Drink, DungeonRef::None, 0u, 0.0f, 0.0f,
kWalkTileTransparent},
// Sign: a board at head height on a thin post. Not solid — you read it,
// you do not walk into it.
{ Structure::Sign, "", 0.9f, 2.2f, 0.0f, false, "",
StructureKindRow::Draw::Solid,
StructureKindRow::Material::Sign,
InteractId::Read, DungeonRef::None, 0u, 0.0f, 0.0f,
kWalkTileTransparent},
// SpireGate: the tower's own doorway — the CaveMouth pattern (a Door verb
// whose `opens` column names its OWN interior), sized like the house leaf
// it visually is (the Door material draws it). One kind serves both ends
// of the climb: the gate at the tower's foot and the roof hatch on the
// top storey — the dungeon session reads which end from the storey.
{ Structure::SpireGate, "", 0.5f, 2.0f, 0.0f, false, "",
StructureKindRow::Draw::Solid,
StructureKindRow::Material::Door,
InteractId::Door, DungeonRef::SpireTower, 0u, 0.0f, 0.0f,
kWalkTileTransparent},
// SpireOrb: the spell on the crown — a waist-high plinth (solid: you walk
// around a shrine, not through it) burning the spire's cold blue, the
// same tint the macro map glows at night (landmark_registry.h spire row,
// 0xA86CFF family). Lantern-law light: the emissive head reads from any
// distance, the point light is the row below.
{ Structure::SpireOrb, "", 0.8f, 1.6f, 0.0f, true, "",
StructureKindRow::Draw::Solid,
StructureKindRow::Material::SpireOrb,
InteractId::Learn, DungeonRef::None, 0xA86CFFu, 24.0f, 1.4f,
kWalkTileTransparent},
// Kerb: the parapet rail along a bridge's roadway — its own KIND, not a
// thin Bridge, because a bridge is what carries you and a kerb is what
// edges it: one is the road, the other its trim, and everything that
// asks about either (a chain check, a future toll gate, the day rails
// get their own material) would otherwise have to guess by measuring.
// Deliberately knee-high — under kStepUpM, so it is scenery a body steps
// over rather than a wall that can pin anyone against the water.
{ Structure::Kerb, "", 0.2f, 0.3f, 0.0f, true, "",
StructureKindRow::Draw::Solid,
StructureKindRow::Material::Stone,
InteractId::None, DungeonRef::None, 0u, 0.0f, 0.0f,
std::uint8_t(TILE_ROAD)},
};
static_assert(rows_in_enum_order(kStructureKindRows, &StructureKindRow::kind),
"kStructureKindRows row order must mirror Structure::Kind");
inline constexpr const StructureKindRow& structure_kind_row(Structure::Kind k) {
return kStructureKindRows[int(k) < Structure::kKindCount ? int(k) : 0];
}
inline constexpr float structure_min_half_xy(Structure::Kind k) {
return structure_kind_row(k).minHalfXy;
}
// The ground this kind lays under whoever stands ON it, or
// kWalkTileTransparent when it lays none and the terrain answers.
inline constexpr std::uint8_t structure_walk_tile(Structure::Kind k) {
return structure_kind_row(k).walkTile;
}
inline constexpr float structure_min_height(Structure::Kind k) {
return structure_kind_row(k).minHeight;
}
inline constexpr const char* structure_loot_id(Structure::Kind k) {
return structure_kind_row(k).lootId;
}
inline constexpr bool structure_is_solid(Structure::Kind k) {
return structure_kind_row(k).solid;
}
inline constexpr bool structure_is_lootable(Structure::Kind k) {
return structure_kind_row(k).lootId[0] != '\0';
}
inline constexpr StructureKindRow::Draw structure_draw(Structure::Kind k) {
return structure_kind_row(k).draw;
}
inline constexpr StructureKindRow::Material structure_material(Structure::Kind k) {
return structure_kind_row(k).material;
}
inline constexpr InteractId structure_interact(Structure::Kind k) {
return structure_kind_row(k).interact;
}
// Which interior a Door-verb prop opens (DungeonRef::None for every other).
inline constexpr DungeonRef::Kind structure_opens(Structure::Kind k) {
return structure_kind_row(k).opens;
}
inline constexpr bool structure_is_lit(Structure::Kind k) {
return structure_kind_row(k).lightRadiusTiles > 0.0f;
}
// Visible/solid height in metres. A decayed record (negative height — the
// map_factory.h sign-bit overlay) collapses to the per-kind stub floor, which
// is exactly how the renderer has always drawn it. The floor exists to keep
// degenerate LEGACY grounded records visible; a zBase-lifted body (gate
// lintel, deck) states its thickness explicitly and is never inflated —
// otherwise a 3 m lintel would grow a phantom metre that blocks whoever
// stands on it.
inline float structure_visible_height(const Structure& s) {
const float minH = structure_min_height(s.kind);
if (s.height < 0.0f) return minH;
if (s.zBase > 0.0f || s.zWorld) return s.height;
return std::max(s.height, minH);
}
// Vertical solid span in absolute metres given the terrain sample at the
// structure's centre (`seatM`). Grounded bodies rest on the seat; zBase lifts
// the bottom clear of it (gate lintels); a `zWorld` body ignores the seat and
// stands at its stated world height (a deck over the one sea plane).
inline void structure_solid_span(const Structure& s, float seatM,
float& z0, float& z1) {
z0 = s.zWorld ? s.zBase : seatM + s.zBase;
z1 = z0 + structure_visible_height(s);
}
struct SubworldMapData {
std::vector<std::uint8_t> tiles; // FullSize × FullSize
std::vector<std::uint8_t> trav; // 0 = wall, 1 = walkable
std::vector<float> heightmap; // FullSize × FullSize
std::vector<Structure> structures;
float waterLevel = 0.4f;
};
inline std::size_t tile_index(int x, int y) { return std::size_t(y) * kFullSize + x; }
// ── Geometry helpers (port of subworld/map-data.ts) ──────────────────
// Compass direction index — clockwise from north. Matches the 8 neighbour
// slots in NeighborGrid (N=0, NE=1, E=2, SE=3, S=4, SW=5, W=6, NW=7).
enum class Dir : std::uint8_t {
N = 0, NE = 1, E = 2, SE = 3,
S = 4, SW = 5, W = 6, NW = 7,
};
inline constexpr int kDirOffsets[8][2] = {
{ 0, -1}, { 1, -1}, { 1, 0}, { 1, 1},
{ 0, 1}, {-1, 1}, {-1, 0}, {-1, -1},
};
} // namespace sm::sub