-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathspawn.cpp
More file actions
1091 lines (1021 loc) · 53 KB
/
Copy pathspawn.cpp
File metadata and controls
1091 lines (1021 loc) · 53 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
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#include "sub/spawn.h"
#include "sub/city_layout.h"
#include "macro/entry_context.h"
#include "macro/faction.h"
#include "ecs/components.h"
#include "ecs/npc_character.h"
#include "core/rng.h"
#include "macro/npc.h"
#include "macro/character_sheet.h"
#include "macro/macro_stock.h"
#include "macro/tree_layer.h"
#include "sub/body.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <vector>
namespace sm::sub {
namespace {
constexpr int kMaxSubworldSpawnReaps = 2048;
// How fast a body's DRAWN position catches up to its logical one, and how much
// of its walking speed it uses when it has nowhere in particular to be. One
// value each, because two bodies of the same kind moved at different smoothing
// speeds depending on which spawner made them (32 vs 48).
constexpr float kBodyVisualCatchUp = 32.0f;
constexpr float kBodyWanderSpeedFraction = 0.35f;
// Safety valve for the macro→subworld projection (Inc 5d). Only macro NPCs whose
// integer cell falls in the 3×3 window are projected, so in normal play this is
// a handful; the cap merely bounds a pathological single-cell cluster and is not
// expected to bind. The projection's return count reflects what was projected.
constexpr int kMaxProjectedMacroNpcs = 128;
// Does something CARRY a body above the water plane here? The universal half
// of the wet-tile question (sub/height.h): the tile says what the ground is,
// this says what would hold a body up regardless of it — a bridge deck, a
// jetty, a wall walk. Without an index the answer is honestly "no", which is
// the old tile-only behaviour, unchanged.
bool carried_above_water(const StructureIndex* solids, float x, float y) {
if (!solids || solids->empty()) return false;
// Probe from above with no step allowance: the highest solid top under
// the probe ceiling, whatever it belongs to.
const float top = solids->support_at(x, y, kNpcBodyRadiusDefault,
kSeaLevelM + kDryFootingProbeM,
/*stepUp*/0.0f);
return is_dry_footing(top);
}
// Signed toroidal offset of macro cell `a` from window centre `c` on a torus of
// circumference `n`, folded to (-n/2, n/2]. A result in {-1,0,1} means `a` is in
// the 3×3 window at that cell offset; anything else is outside it. Matches the
// wrap semantics the macro AI uses (core/torus.h), so "in the window" here means
// exactly the same cells the seamless manager loads.
int toroidal_cell_offset(int a, int c, int n) {
if (n <= 0) return a - c;
int d = ((a - c) % n + n) % n; // [0, n)
if (d * 2 > n) d -= n; // fold to (-n/2, n/2]
return d;
}
// The face of a DERIVED body: appearance and name rolled from its seed, never
// stored anywhere above. `tintBase` used to be 150 here and 160 in the other two
// spawners — a difference nobody could see (no pass reads NpcCharacter's tint)
// and nobody could justify, which is exactly how the seven other divergences
// started. One value, named once.
constexpr int kBodyFaceTintBase = 160;
// hash3, not a XOR chain: the settlement populator passes seed = cellSeed ^
// (i*7919) and salt = i*7919, so `seed ^ type ^ salt` cancelled the i term and
// every citizen of one type in one town wore the SAME face (the clone crowds of
// 2026-08-10). A multiplicative avalanche hash cannot cancel equal
// contributions arriving through different arguments.
ecs::NpcCharacter derive_face(std::uint32_t seed, NPCType type,
std::uint32_t salt) {
Rng rng(hash3(seed, salt, std::uint32_t(type)));
return ecs::roll_npc_character(rng, kBodyFaceTintBase);
}
// Find a spot for one inhabitant of the settlement centred at (cx, cy) with
// built-up radius `radius` (sub/city_layout.h — the SAME number the generator
// stamped its walls from).
//
// This used to draw a uniform tile out of the whole 1024×1024 macro cell, which
// had nothing to do with where the town stood: a city walls 4–8 % of its cell
// and a village ~1 %, so nearly every "citizen" was born in the fields and
// forests outside the gates and the streets inside were deserted. Sampling is
// now confined to the built-up disk — strictly inside the walls, nobody outside.
//
// r = radius·√u, not radius·u: uniform in AREA. Sampling the radius linearly
// would pile the whole population onto the market square.
bool find_city_spawn_spot(const std::vector<std::uint8_t>& tiles,
Rng& rng,
float cx,
float cy,
float radius,
float& fx,
float& fy) {
if (tiles.size() < std::size_t(kFullSize) * std::size_t(kFullSize)) {
return false;
}
if (!(radius > 0.0f)) return false;
constexpr float kTau = 6.2831853f;
for (int attempt = 0; attempt < 64; ++attempt) {
const float a = rng.next_f01() * kTau;
const float r = radius * std::sqrt(rng.next_f01());
const int x = int(cx + std::cos(a) * r);
const int y = int(cy + std::sin(a) * r);
if (x < 0 || x >= kFullSize || y < 0 || y >= kFullSize) continue;
const std::uint8_t t = tiles[std::size_t(y) * kFullSize + x];
// Water drowns; house/wall footprints are SOLID now (sub/collide.h) —
// a body born inside masonry would have to walk out through the
// escape rule, so don't put it there in the first place.
if (t == TILE_WATER || t == TILE_HOUSE || t == TILE_WALL) continue;
fx = float(x) + 0.5f;
fy = float(y) + 0.5f;
return true;
}
return false;
}
// (pick_civilian_type lived here until 2026-08-24 — an RNG-only crowd that
// could not tell an iron town from a swamp one, canon-audit F4. The crowd
// rolls by THE spawn law now: fauna.h pick_town_row.)
// ── THE birth of a subworld humanoid ───────────────────────────────────────
//
// One function, because a body is one idea. Four places used to write this
// sequence by hand — citizens, the player's squad, the macro projection and the
// hostile spawner — and by 2026-08-06 they had drifted apart in seven ways, one
// of which the player could see: the SQUAD was invisible. Its bodies were built
// without `NpcCharacter`, so the paper-doll pass (which draws `Position +
// NpcCharacter`) never saw them, and their sprite kept the default archetype
// 0xFF, which the creature pass skips — you walked into the subworld with ten
// mercenaries and saw an empty field with something invisible swinging in it.
//
// That was not a missing component. It was a fifth dialect of "what a body is".
// So the component set lives HERE, derived from the ONE table row (macro/npc.h
// kNpcTypeDefs) plus the caller's CONTEXT — who it fights for, what rank it
// holds, why it is standing there. A new component on bodies is one line in
// this function and every kind of body has it; a caller cannot forget what it
// never spells out.
//
// What is context (a parameter) and what is data (the row) is the whole design:
// faction, level, position and role come from above — the world decided them.
// Reach, speed, damage, sight, the sprite and the light it carries come from
// the row, because those are what the THING is, and the table is the only place
// that says so.
//
// The component set itself. Only two things about a body are not settled by its
// row and its context — the face it wears and the wounds it already carries —
// and those are exactly what the axis in spawn.h decides (derived vs tracked).
// Everything below is the same for a peasant, a mercenary and a lord.
entt::entity emplace_body(entt::registry& reg, const BodySpec& body,
const ecs::NpcCharacter& face,
float healthFraction,
const BonusTotals* squadBonuses = nullptr) {
const NpcTypeDef& def = npc_def(body.type);
CharacterSheet sheet =
make_character_sheet(body.type, body.level, body.seed);
// The leader's buff lands IN the sheet, before anything is projected from
// it (character_sheet.h, ruling №2): from here down a buffed soldier is simply
// a soldier whose sheet says more, and no formula ever meets a second
// source of strength standing beside it.
if (squadBonuses) sheet = effective_sheet(sheet, *squadBonuses);
const CombatTemplate pc = project_combat(sheet, def.combat);
// Integers, not fractions: a body that hits for 17.85 accumulates a
// different wound than one that hits for 17, and the two spawners used to
// disagree about which it was. (The wound itself is dice + an already-
// floored flatAdd now — the strike assembly is integer end to end.)
const float maxHp = float(body_max_hp(sheet, def.combat));
// Wounds travel as a FRACTION, not as a number of points, so the two layers
// never have to agree on how big a lord's bar is. A tracked entity at two
// thirds arrives at two thirds whatever the sheet says down here, and the
// return trip needs no conversion table either.
const int hp = std::clamp(int(maxHp * healthFraction), 1, int(maxHp));
const auto e = reg.create();
reg.emplace<ecs::Position>(e, body.x, body.y, 0.0f);
reg.emplace<ecs::VisualPos>(e, body.x, body.y, kBodyVisualCatchUp);
reg.emplace<ecs::NPCKind>(e, std::uint16_t(body.type), body.faction);
reg.emplace<ecs::Health>(e, hp, int(maxHp));
// Its pace: the world's march (macro/movement_cost.h) times what this row
// is against a walking man. ONE scale for every body, the player's
// included — a peasant walks at exactly the speed the map says a man
// walks, and a bandit's 2.25 means he runs.
// The guard that keeps the two scales honest about a cell's LENGTH: the
// macro side derives the walking speed from kSubworldTilesPerMacroCell,
// and this file is where both constants stand in one scope.
static_assert(float(kCellSize) == kSubworldTilesPerMacroCell,
"sub/map_data.h kCellSize must equal the macro side's "
"kSubworldTilesPerMacroCell — the parity anchor rides on it");
const float bodySpeed = march_speed(pc.speedMarchMult);
reg.emplace<ecs::Combat>(e,
pc.dice, pc.flatAdd, std::int16_t(100), pc.luck,
std::uint8_t(pc.dmgType), bodySpeed, pc.attackRange, pc.cooldown, 0u,
pc.attackKind == CombatTemplate::Missile ? ecs::Combat::Missile
: ecs::Combat::Melee);
maybe_emplace_missile_attack(reg, e, pc);
reg.emplace<ecs::NpcLevel>(e, std::int16_t(body.level));
reg.emplace<ecs::SubworldTag>(e);
// How much room this body takes: the row's ONE width column, man-shaped
// default resolved (npc.h npc_body_radius). This is the ONE line where
// the creature birth and the humanoid birth used to differ about a
// number — and where a template shadow copy of the width used to answer.
const float bodyRadius = npc_body_radius(def);
reg.emplace<ecs::SubworldAi>(e,
body.combatant ? ecs::SubworldAi::Combat : subworld_ai_for(def.ai),
/*aiTimer*/0.0f, /*vx*/0.0f, /*vy*/0.0f,
/*wanderSpeed*/bodySpeed * kBodyWanderSpeedFraction,
/*radius*/bodyRadius);
reg.emplace<CharacterSheet>(e, sheet);
// A face for every body. This is the line the squad never had.
reg.emplace<ecs::NpcCharacter>(e, face);
// The sprite record. Colour comes from THE sprite table's row — the same
// place a wolf's grey and a peasant's cloth come from — and never from the
// call site: three spawners each used to invent a tint (a guard 170, a
// hostile always red) that only the procedural pass would have read anyway.
// A row with drawn art ignores it, because art speaks for itself.
// Width AND height both from the row (sub/body.h): what a thing is, how
// much room it takes and how big it looks are one decision. The height is
// varied by the body's own shape byte — the one the face rolls — so a crowd
// has tall and short people in it without a second field or a second roll.
const SpriteDef& look = sprite_row(def.sprite);
reg.emplace<ecs::Sprite>(e, std::uint16_t(body.type),
std::uint8_t((look.tint >> 16) & 0xFFu),
std::uint8_t((look.tint >> 8) & 0xFFu),
std::uint8_t( look.tint & 0xFFu),
std::uint8_t(255), bodyRadius, std::uint8_t(def.sprite),
body_height_m(def) * body_shape_height_scale(face.bodyShape));
maybe_emplace_carried_light(reg, e, def);
return e;
}
void spawn_settlement_population(ecs::World& w,
const SpawnContext& townCtx,
LandmarkType landmark,
const SeamlessSubworldManager& mgr,
std::uint32_t seed,
std::uint16_t settlementFaction,
int landmarkPop,
int originX,
int originY,
MacroStockKey populationKey) {
if (landmark != LandmarkType::City && landmark != LandmarkType::Village) {
return;
}
const int pop = std::max(0, landmarkPop);
if (pop == 0) return;
const bool city = landmark == LandmarkType::City;
const int target = pop;
const int guards = std::max(city ? 2 : 1, target / 10);
Rng rng(seed ^ (city ? 0xC1712E55u : 0xA117A6E5u));
const auto& tiles = mgr.tiles();
auto& reg = w.reg;
// Both generators build their settlement on the CELL CENTRE, and the disk
// they build it in is defined once in sub/city_layout.h. A town's people
// belong in that disk — see find_city_spawn_spot.
const float centerX = float(originX) + float(kCellSize) * 0.5f;
const float centerY = float(originY) + float(kCellSize) * 0.5f;
const float populationRadius = settlement_population_radius(city, pop);
for (int i = 0; i < target; ++i) {
float fx = 0.0f;
float fy = 0.0f;
if (!find_city_spawn_spot(tiles, rng, centerX, centerY,
populationRadius, fx, fy)) {
continue;
}
NPCType type = NPCType::Peasant;
if (i < guards) {
type = NPCType::Guard;
} else if (i == guards) {
type = NPCType::Merchant;
} else if (i == guards + 1) {
type = NPCType::Woodcutter;
} else {
std::uint32_t ts = rng.state;
type = pick_town_row(townCtx, ts);
rng.state = ts;
}
// A citizen is DERIVED — he is one unit of this place's population made
// visible, and nothing about him is remembered above. What is CONTEXT
// here: which town's faction he wears, and that he lives his errands
// rather than fighting. His STRENGTH is not context — it is his row.
// A capital's guard and a hamlet's guard are the same guard; the capital
// simply fields more of them (CANON.md S12).
//
// The loan says which stock he was drawn from, so his death pays the
// settlement back without anyone asking what kind of body it was: a town
// cannot be emptied in the subworld while the map still counts everyone
// as alive. Borrowing and returning are the same row of one table.
spawn_derived_body(reg,
BodySpec{
type, fx, fy, settlementFaction,
normalize_soldier_level(npc_def(type).baseLevel
+ int(rng.next_u32() % 3u)),
seed ^ (std::uint32_t(i) * 7919u),
/*combatant*/false},
/*faceSalt*/std::uint32_t(i) * 7919u,
BodyLoan::from(MacroStock::Population, populationKey));
}
}
} // namespace
// ── The two forms of birth (declared in spawn.h) ─────────────────────────
entt::entity spawn_derived_body(entt::registry& reg, const BodySpec& body,
std::uint32_t faceSalt, const BodyLoan& loan,
const BonusTotals* squadBonuses) {
const entt::entity e =
emplace_body(reg, body, derive_face(body.seed, body.type, faceSalt),
/*healthFraction*/1.0f, squadBonuses);
// The receipt, stamped by the birth rather than by the caller: borrowing is
// part of coming into being, not a line a spawner might remember to add.
// Nothing lent means nothing to stamp — a body drawn from thin air is honest
// about owing the map nothing.
if (loan.stock != MacroStock::Count) {
stamp_macro_debt(reg, e, loan.stock, loan.key, 1);
}
return e;
}
entt::entity spawn_tracked_body(entt::registry& reg, entt::entity macro,
float x, float y, std::uint32_t seed,
bool combatant) {
if (macro == entt::null || !reg.valid(macro)) return entt::null;
if (!reg.all_of<ecs::NPCKind, ecs::Health, ecs::NpcLevel,
ecs::NpcCharacter>(macro)) {
return entt::null;
}
const auto& kind = reg.get<ecs::NPCKind>(macro);
// A kind that names no row at all is refused; a kind that names one is
// trackable, whatever it is. The extra refusal that stood here — "not a
// creature" — died with the second birth: a pack leader is a macro entity
// like a lord, and his body copies his face and his wounds down the same
// way (CANON.md S4).
if (!valid_npc_kind(kind.type)) return entt::null;
const auto& health = reg.get<ecs::Health>(macro);
const float fraction = health.maxHp > 0
? std::clamp(float(health.hp) / float(health.maxHp), 0.0f, 1.0f)
: 1.0f;
BodySpec body{};
body.type = static_cast<NPCType>(kind.type);
body.x = x;
body.y = y;
// What it is, whose it is and how senior it is are read from the entity
// itself — a tracked body has no second opinion about its own identity.
body.faction = kind.factionIdx;
body.level = normalize_soldier_level(reg.get<ecs::NpcLevel>(macro).value);
body.seed = seed;
body.combatant = combatant;
const entt::entity e =
emplace_body(reg, body, reg.get<ecs::NpcCharacter>(macro), fraction);
// The belongings and the personality are STATE up there, so they are copied,
// not rolled. A derived body has neither on purpose: its loot is rolled from
// its seed at the moment it dies, which costs a city of five thousand people
// exactly nothing to carry.
if (const auto* bag = reg.try_get<ecs::NpcInventory>(macro)) {
reg.emplace<ecs::NpcInventory>(e, *bag);
}
if (const auto* traits = reg.try_get<ecs::NpcTraits>(macro)) {
reg.emplace<ecs::NpcTraits>(e, *traits);
}
// The backlink is part of being tracked, not an extra the caller attaches:
// it is the address the return trip writes to.
reg.emplace<ecs::MacroOrigin>(e, macro);
return e;
}
// ── Universal per-humanoid component attachers (declared in spawn.h) ─────
// ONE home for the rules every spawn site shares — settlement populator,
// squads, macro projection (this TU) and the console/encounter spawner
// (engine.cpp). These used to exist as byte-identical file-local twins in
// both TUs, each with a comment admitting the duplication.
void maybe_emplace_missile_attack(entt::registry& reg,
entt::entity e,
const CombatTemplate& combat) {
if (combat.attackKind != CombatTemplate::Missile) return;
reg.emplace<ecs::MissileAttack>(
e,
combat.missileSpeed > 0.0f ? combat.missileSpeed : 200.0f,
combat.missileBlast,
combat.missileColorRGBA);
}
// Attach the NPC type's carried light (torch / lantern / arcane glow), if it has
// one, as an ecs::LightEmitter — the SAME universal component the player lantern
// and spell bolts use, so the renderer's one gather_point_lights pass lights it
// with zero per-emitter code. Data-driven and strictly opt-in: a type with
// lightRadius <= 0 (every row that doesn't set the fields) gets nothing, so
// lighting a new type is one data row in kNpcTypeDefs and no code change here.
// Called from every humanoid spawn site after its Sprite emplace, so a guard is
// lit whether it is a settlement citizen, a projected macro body or a squad
// soldier — one rule, one place. Budget-safe: guards are bounded per settlement
// and the nearest-N cull (gather_point_lights) protects the SSBO regardless.
void maybe_emplace_carried_light(entt::registry& reg,
entt::entity e,
const NpcTypeDef& def) {
if (def.lightRadius <= 0.0f) return;
reg.emplace<ecs::LightEmitter>(
e, ecs::LightEmitter{0.0f, def.lightHeight, 0.0f,
def.lightR, def.lightG, def.lightB,
def.lightRadius, def.lightIntensity});
}
// ── Dungeon residents (sub/dgn interiors) ────────────────────────────────
int spawn_dungeon_residents(ecs::World& w,
const SeamlessSubworldManager& mgr,
std::uint32_t seed,
std::uint16_t settlementFaction,
std::uint8_t danger,
std::uint8_t depositsNear,
int count,
float x0, float y0, float x1, float y1,
std::uint8_t floorTile,
MacroStockKey populationKey) {
if (count <= 0) return 0;
const auto& tiles = mgr.tiles();
if (tiles.size() < std::size_t(kFullSize) * std::size_t(kFullSize)) {
return 0;
}
Rng rng(seed ^ 0xD0E51DE7u);
int placed = 0;
for (int i = 0; i < count; ++i) {
// Plain interior floor only: partitions paint TILE_WALL, furniture
// paints TILE_HOUSE, the threshold TILE_ROAD — a body stands on none
// of them, and WHICH tile is floor is the interior's own answer.
float fx = 0.0f, fy = 0.0f;
bool found = false;
for (int attempt = 0; attempt < 24 && !found; ++attempt) {
fx = x0 + rng.next_f01() * std::max(0.0f, x1 - x0);
fy = y0 + rng.next_f01() * std::max(0.0f, y1 - y0);
const int ix = int(fx), iy = int(fy);
if (ix < 1 || iy < 1 || ix >= kFullSize - 1 || iy >= kFullSize - 1) {
continue;
}
if (tiles[std::size_t(iy) * kFullSize + std::size_t(ix)]
!= floorTile) {
continue;
}
found = true;
}
if (!found) continue;
SpawnContext townCtx{};
townCtx.landmark = LandmarkType::City; // a household is town folk
townCtx.danger = danger;
townCtx.depositsNear = depositsNear;
std::uint32_t ts = rng.state;
const NPCType type = pick_town_row(townCtx, ts);
rng.state = ts;
// The same derived-citizen birth as the street (one row of one law):
// level from his own row, loan from the SAME population stock — a death
// in here pays the town back exactly like a death on the square.
spawn_derived_body(w.reg,
BodySpec{
type, fx, fy, settlementFaction,
normalize_soldier_level(npc_def(type).baseLevel
+ int(rng.next_u32() % 3u)),
seed ^ (std::uint32_t(i) * 7919u),
/*combatant*/false},
/*faceSalt*/std::uint32_t(i) * 7919u,
BodyLoan::from(MacroStock::Population, populationKey));
++placed;
}
return placed;
}
int spawn_dungeon_vermin(ecs::World& w,
const SeamlessSubworldManager& mgr,
std::uint32_t seed,
LandmarkType tableKind,
std::uint8_t danger,
Biome biome,
int treeCount,
int budget,
float x0, float y0, float x1, float y1,
std::uint8_t floorTile,
MacroStockKey faunaKey) {
if (budget <= 0) return 0;
const auto& tiles = mgr.tiles();
if (tiles.size() < std::size_t(kFullSize) * std::size_t(kFullSize)) {
return 0;
}
// The same ONE law the open cell runs (fauna.h roll_spawns): habitat ×
// danger-match over the body table; the den kind is the habitat bit.
SpawnContext sctx{};
sctx.biome = biome;
sctx.forest = is_forest_cell(treeCount);
sctx.landmark = tableKind;
sctx.danger = danger;
std::uint32_t rngState = seed ^ 0xCE11A5u;
auto picks = roll_spawns(sctx, rngState);
if (picks.empty()) return 0;
Rng pos(rngState);
auto& reg = w.reg;
int placed = 0;
for (const auto& p : picks) {
if (placed >= budget) break;
const FaunaEntry& f = *p.entry;
float fx = 0.0f, fy = 0.0f;
bool found = false;
for (int attempt = 0; attempt < 24 && !found; ++attempt) {
fx = x0 + pos.next_f01() * std::max(0.0f, x1 - x0);
fy = y0 + pos.next_f01() * std::max(0.0f, y1 - y0);
const int ix = int(fx), iy = int(fy);
if (ix < 1 || iy < 1 || ix >= kFullSize - 1 || iy >= kFullSize - 1) {
continue;
}
if (tiles[std::size_t(iy) * kFullSize + std::size_t(ix)]
!= floorTile) {
continue;
}
found = true;
}
if (!found) continue;
const int npcLevel = normalize_soldier_level(
int(f.baseLevel) + int(std::floor(pos.next_f01() * 2.0f)));
spawn_derived_body(reg,
BodySpec{f.type, fx, fy,
std::uint16_t(faction_index(p.factionId)), npcLevel,
seed ^ (std::uint32_t(placed) * 2654435761u),
/*combatant*/false},
/*faceSalt*/std::uint32_t(placed) * 7919u,
BodyLoan::from(MacroStock::FaunaCount, faunaKey));
++placed;
}
return placed;
}
// ── Per-cell population + seamless persistence helpers ───────────────────
void clear_subworld_world_entities(ecs::World& w) {
auto& reg = w.reg;
std::array<entt::entity, kMaxSubworldSpawnReaps> doomed{};
for (;;) {
int doomedCount = 0;
auto view = reg.view<ecs::SubworldTag>();
for (auto e : view) {
if (reg.any_of<ecs::PlayerSoldierTag, ecs::PlayerTag>(e)) continue;
// Projected macro NPCs (Inc 5d) mirror persistent overworld bodies,
// not a cell's procedural fill — a whole-window rebuild (respawn_fauna)
// must leave them be, exactly like the player-side projections above.
// On enter this is a no-op (projection runs after the clear).
if (reg.all_of<ecs::MacroOrigin>(e)) continue;
if (doomedCount >= kMaxSubworldSpawnReaps) break;
doomed[std::size_t(doomedCount++)] = e;
}
if (doomedCount == 0) break;
for (int i = 0; i < doomedCount; ++i) {
const entt::entity e = doomed[std::size_t(i)];
if (reg.valid(e)) reg.destroy(e);
}
}
}
void spawn_cell_npcs(ecs::World& w,
Biome biome,
int treeCount,
LandmarkType landmark,
std::uint8_t danger,
std::uint8_t depositsNear,
const SeamlessSubworldManager& mgr,
int ox,
int oy,
std::uint32_t cellSeed,
std::uint16_t settlementFaction,
int landmarkPop,
int landmarkSubjectId,
int macroCellX,
int macroCellY,
int faunaCount) {
auto& reg = w.reg;
const int originX = (ox + 1) * kCellSize;
const int originY = (oy + 1) * kCellSize;
// Context decides WHO and HOW MANY stand on this cell — never what they are
// worth. Each of the 3×3 cells populates on its own macro terms (a city cell
// fills with citizens even when it is not the centre — which is what stops a
// city from vanishing when you step one cell out), and every body that stands
// up is exactly its table row. The two markups that used to live here — the
// settlement's √(pop/100) level bonus and the danger zone's +1 level with a
// 1+0.18·(z−2) hp/damage multiplier — were a hidden auto-level: they made the
// same guard stronger for standing in a bigger town and the same wolf tougher
// for standing in a redder province. Deleted 2026-08-20 (CANON.md S12); when
// the zone is meant to matter it must weight the TABLE, not the body.
SpawnContext townCtx{};
townCtx.biome = biome;
townCtx.forest = is_forest_cell(treeCount);
townCtx.landmark = landmark;
townCtx.danger = danger;
townCtx.depositsNear = depositsNear;
spawn_settlement_population(w, townCtx, landmark, mgr, cellSeed,
settlementFaction,
landmarkPop, originX, originY,
MacroStockKey{landmarkSubjectId,
std::int16_t(macroCellX),
std::int16_t(macroCellY)});
// THE spawn law (fauna.h): the danger byte weights the TABLE — who is
// rolled — never the body after the pick (S12; the negative control in
// subworld_spawn_parity_test keeps the autolevel dead).
SpawnContext sctx{};
sctx.biome = biome;
sctx.forest = is_forest_cell(treeCount);
sctx.landmark = landmark;
sctx.danger = danger;
std::uint32_t rngState = cellSeed ^ 0xFAEAu;
auto picks = roll_spawns(sctx, rngState);
if (picks.empty()) return;
// The honest headcount: the roll proposes, the macro stock DISPOSES. A
// hunted cell embodies only what still stands on it — return after a
// cull and the survivors are all there is (the repopulate-on-recenter
// farm dies here). -1 = no macro context wired = the old unbounded roll.
int budget = faunaCount >= 0 ? faunaCount : int(picks.size());
const BodyLoan faunaLoan = faunaCount >= 0
? BodyLoan::from(MacroStock::FaunaCount,
MacroStockKey{-1, std::int16_t(macroCellX),
std::int16_t(macroCellY)})
: BodyLoan::none();
Rng pos(rngState);
const auto& tiles = mgr.tiles();
const bool tilesUsable =
tiles.size() >= std::size_t(kFullSize) * std::size_t(kFullSize);
for (const auto& p : picks) {
if (budget <= 0) break;
const FaunaEntry& f = *p.entry;
// Scatter within this cell's sub-region only. Up to 20 retries to dodge
// water; positions are composite-window tiles like everything else.
float fx = 0.0f, fy = 0.0f;
bool placed = false;
for (int attempt = 0; attempt < 20; ++attempt) {
fx = float(originX) + pos.next_f01() * float(kCellSize);
fy = float(originY) + pos.next_f01() * float(kCellSize);
const int ix = int(fx), iy = int(fy);
if (ix < 0 || ix >= kFullSize || iy < 0 || iy >= kFullSize) continue;
if (tilesUsable &&
tiles[std::size_t(iy) * kFullSize + ix] == TILE_WATER) {
continue;
}
placed = true;
break;
}
if (!placed) continue;
const int npcLevel = normalize_soldier_level(
int(f.baseLevel) + int(std::floor(pos.next_f01() * 2.0f)));
spawn_derived_body(reg,
BodySpec{f.type, fx, fy,
std::uint16_t(faction_index(p.factionId)), npcLevel,
cellSeed ^ (std::uint32_t(budget) * 2654435761u),
/*combatant*/false},
/*faceSalt*/std::uint32_t(budget) * 7919u, faunaLoan);
--budget;
}
}
void rebase_subworld_entities(ecs::World& w, float dxTiles, float dyTiles) {
auto& reg = w.reg;
// Shift the authoritative sim position AND the smoothed render position so a
// recentre neither drifts entities nor produces a one-frame interpolation
// streak. Both views are SubworldTag-gated, so the player squad shifts too.
auto posView = reg.view<ecs::SubworldTag, ecs::Position>();
for (auto e : posView) {
auto& p = posView.get<ecs::Position>(e);
p.x += dxTiles;
p.y += dyTiles;
}
auto visView = reg.view<ecs::SubworldTag, ecs::VisualPos>();
for (auto e : visView) {
auto& v = visView.get<ecs::VisualPos>(e);
v.vx += dxTiles;
v.vy += dyTiles;
}
}
void despawn_subworld_entities_outside_window(ecs::World& w) {
auto& reg = w.reg;
std::array<entt::entity, kMaxSubworldSpawnReaps> doomed{};
for (;;) {
int doomedCount = 0;
auto view = reg.view<ecs::SubworldTag, ecs::Position>();
for (auto e : view) {
if (reg.any_of<ecs::PlayerSoldierTag, ecs::PlayerTag>(e)) continue;
const auto& p = view.get<ecs::Position>(e);
const bool inside = p.x >= 0.0f && p.x < float(kFullSize)
&& p.y >= 0.0f && p.y < float(kFullSize);
if (inside) continue;
if (doomedCount >= kMaxSubworldSpawnReaps) break;
doomed[std::size_t(doomedCount++)] = e;
}
if (doomedCount == 0) break;
for (int i = 0; i < doomedCount; ++i) {
const entt::entity e = doomed[std::size_t(i)];
if (reg.valid(e)) reg.destroy(e);
}
}
}
void spawn_player_squad(ecs::World& w,
const SoldierSquad& squad,
const SeamlessSubworldManager& mgr,
float playerX,
float playerY,
std::uint32_t seed,
std::uint16_t faction,
const BonusTotals* squadBonuses) {
spawn_player_squad(w, squad, mgr.tiles(), playerX, playerY, seed, faction,
squadBonuses);
}
void spawn_player_squad(ecs::World& w,
const SoldierSquad& squad,
const std::vector<std::uint8_t>& tiles,
float playerX,
float playerY,
std::uint32_t seed,
std::uint16_t faction,
const BonusTotals* squadBonuses) {
if (squad.empty()) return;
auto& reg = w.reg;
Rng rng(seed ^ 0x51AD5A11u);
constexpr float kPi = 3.1415926535f;
constexpr float kTau = kPi * 2.0f;
const int count = std::max(1, squad.size());
const bool tilesUsable =
tiles.size() >= std::size_t(kFullSize) * std::size_t(kFullSize);
for (int i = 0; i < count; ++i) {
const SoldierRecord& soldier = squad[i];
if (!valid_npc_kind(soldier.kind)) continue;
const NPCType type = static_cast<NPCType>(soldier.kind);
const int level = normalize_soldier_level(soldier.level);
// The sheet, the combat template and the whole body are derived inside
// the one birth below; the slot only decides WHO stands here and where.
// Seeded per squad slot (kind + level + slot) so a squad reprojects
// identically, and level scaling stays in the sheet's spent points.
float fx = playerX;
float fy = playerY;
bool placed = false;
for (int attempt = 0; attempt < 24; ++attempt) {
const float baseAngle = (float(i) / float(count)) * kTau;
const float jitter = (rng.next_f01() - 0.5f) * 0.7f;
const float radius = 5.0f + float((i % 5) * 3) + rng.next_f01() * 2.0f;
fx = std::clamp(playerX + std::cos(baseAngle + jitter) * radius,
1.0f, float(kFullSize - 2));
fy = std::clamp(playerY + std::sin(baseAngle + jitter) * radius,
1.0f, float(kFullSize - 2));
const int ix = int(fx);
const int iy = int(fy);
if (tilesUsable &&
tiles[std::size_t(iy) * kFullSize + ix] == TILE_WATER) {
continue;
}
placed = true;
break;
}
if (!placed) continue;
// The squad is born through the ONE birth every subworld humanoid gets.
// A squad is not a kind of creature — it is CONTEXT from the map above:
// whoever the leader raised, embodied here under the leader's faction.
// Put a goblin or a dragon in the roster on the macro layer and that is
// what walks beside you, drawn from the same table as everything else.
// Before this, the squad had its own hand-written birth that forgot
// `NpcCharacter` — which is precisely why an army of ten was invisible.
//
// A soldier is DERIVED: the roster line says WHO stands here, the seed
// says everything else. Nothing is lent yet — the roster becomes a macro
// stock of its own when squads become the macro entity (macrosim.md,
// "Squad as THE macro entity"), and on that day this call gains a loan
// and nothing else changes.
const auto e = spawn_derived_body(reg,
BodySpec{
type, fx, fy, faction, level,
(std::uint32_t(i) * 2654435761u)
^ (std::uint32_t(soldier.kind) << 8)
^ std::uint32_t(level),
/*combatant*/true},
/*faceSalt*/std::uint32_t(i) * 2654435761u,
BodyLoan::none(), squadBonuses);
reg.emplace<ecs::PlayerSoldierTag>(e);
reg.emplace<ecs::SoldierLink>(e, soldier.entityId, soldier.kind,
std::int16_t(level));
}
}
// ── Macro→subworld projection (Inc 5d) ───────────────────────────────────
int project_macro_npcs_into_subworld(ecs::World& w,
const SeamlessSubworldManager& mgr,
int centerCx, int centerCy,
int mapW, int mapH,
std::uint32_t seed, bool* truncated,
const StructureIndex* solids) {
return project_macro_npcs_into_subworld(w, mgr.tiles(), centerCx, centerCy,
mapW, mapH, seed, truncated,
solids);
}
int project_macro_npcs_into_subworld(ecs::World& w,
const std::vector<std::uint8_t>& tiles,
int centerCx, int centerCy,
int mapW, int mapH,
std::uint32_t seed, bool* truncated,
const StructureIndex* solids) {
auto& reg = w.reg;
const bool tilesUsable =
tiles.size() >= std::size_t(kFullSize) * std::size_t(kFullSize);
// Snapshot the source set FIRST. Projecting a body emplaces into the very
// component pools this view iterates (Position / NPCKind / Health / …),
// which can reallocate and invalidate a live view iterator mid-loop. So we
// collect the persistent macro NPCs, then create their projections.
// MacroNpcRuntime is the macro discriminator (subworld bodies never have it);
// excluding SubworldTag/Dead keeps the source set to live overworld NPCs.
// PlayerTag skips a macro NPC the player is currently possessing (Inc 5e-2) —
// you don't meet a foreign projection of your own former body on enter.
// PlayerSquadTag skips the player's OWN squad, which since the merge looks
// exactly like any other party on the map. It is not a stranger to meet
// underground, and projecting it would open a second writeback path into
// the same numbers PlayerState already owns.
std::vector<entt::entity> sources;
{
auto view = reg.view<ecs::MacroNpcRuntime, ecs::Position, ecs::NPCKind,
ecs::Health, ecs::NpcLevel, ecs::NpcCharacter>(
entt::exclude<ecs::SubworldTag, ecs::Dead, ecs::PlayerTag,
ecs::PlayerSquadTag>);
for (auto macro : view) sources.push_back(macro);
}
int projected = 0;
for (const entt::entity macro : sources) {
const auto& mpos = reg.get<ecs::Position>(macro);
// Which of the 3×3 window cells does this macro NPC occupy (if any)?
const int ox = toroidal_cell_offset(int(mpos.x), centerCx, mapW);
const int oy = toroidal_cell_offset(int(mpos.y), centerCy, mapH);
if (ox < -1 || ox > 1 || oy < -1 || oy > 1) continue;
// The cap, checked AFTER the window filter so it only fires for a
// body that WOULD stand here — and it fires out loud (CANON S26):
// the macro entity persists untouched, but the scene is blind to it
// and the caller must be able to say so.
if (projected >= kMaxProjectedMacroNpcs) {
if (truncated) *truncated = true;
break;
}
const auto& kind = reg.get<ecs::NPCKind>(macro);
// Deterministic per-(cell, type, index) stream: the same overworld state
// reprojects identically, yet two same-type NPCs in one cell still differ
// (their integer coords or the running index diverge the salt).
const std::uint32_t salt =
(std::uint32_t(int(mpos.x)) * 73856093u) ^
(std::uint32_t(int(mpos.y)) * 19349663u) ^
(std::uint32_t(kind.type) << 11) ^
(std::uint32_t(projected) * 2654435761u);
Rng rng(seed ^ salt);
// Entry-side scatter within this window cell's sub-region
// (macro/entry_context.h): the band starts at the edge this NPC walked
// in from and deepens with its time in the macro cell, so a party that
// just chased somebody across the border materialises AT that border,
// behind them — while a local that has been here forever gets the full
// uniform cell (the band formula degrades to exactly the old scatter).
// Water is dodged per attempt like the fauna path (spawn_cell_npcs) —
// but what is dodged is WET FOOTING, not a wet tile: a body may
// stand on whatever would carry it above the water plane, so the
// deck of a bridge is a perfectly good place to meet a caravan
// (sub/height.h is_dry_footing). Falls back to the cell centre if 20
// tries all land in water (never lose the NPC).
const auto& mrt = reg.get<ecs::MacroNpcRuntime>(macro);
int sdx = 0, sdy = 0;
(void)unpack_entry_dir(mrt.entryDir, sdx, sdy);
const int originX = (ox + 1) * kCellSize;
const int originY = (oy + 1) * kCellSize;
float fx = float(originX) + float(kCellSize) * 0.5f;
float fy = float(originY) + float(kCellSize) * 0.5f;
for (int attempt = 0; attempt < 20; ++attempt) {
const float tx = float(originX) + entry_axis_pos(
sdx, mrt.entryTicks, float(kCellSize), rng.next_f01());
const float ty = float(originY) + entry_axis_pos(
sdy, mrt.entryTicks, float(kCellSize), rng.next_f01());
const int ix = int(tx), iy = int(ty);
if (ix < 0 || ix >= kFullSize || iy < 0 || iy >= kFullSize) continue;
if (tilesUsable &&
tiles[std::size_t(iy) * kFullSize + ix] == TILE_WATER
&& !carried_above_water(solids, tx, ty)) {
continue;
}
fx = tx; fy = ty;
break;
}
// THE tracked form, and the whole reason the axis exists: this body is
// not a body LIKE the lord up there — it IS him, wearing his face, his
// wounds and his belongings, with the backlink that lets what happens
// here be written back. Everything the projection used to spell out by
// hand — the sheet, the combat template, the hostility from the row, the
// copied identity — is the one birth now, so a lord and a townsman can
// no longer differ in anything but what the axis says they differ in.
const entt::entity leaderBody =
spawn_tracked_body(reg, macro, fx, fy, seed ^ salt ^ 0x5D0F11u,
/*combatant*/false);
if (leaderBody == entt::null) {
continue; // not a body-shaped macro entity; nothing was created
}
++projected;
// The leader's buff, read from the leader's OWN sheet (character_sheet.h squad_bonuses)
// — collected once per squad and applied into every member's sheet at
// birth. A generic leader's derived sheet carries no bonus sources
// today (no perks), so this collects empty and changes nothing; a
// hand-authored or persistent leader with a Leader-class perk buffs
// its troops through this same line with no further change anywhere.
BonusTotals leaderBonuses{};
if (const auto* leaderSheet = reg.try_get<CharacterSheet>(leaderBody)) {
leaderBonuses = squad_bonuses(*leaderSheet);
}
// The leader's troops. Each roster row is one unit of the squad's
// roster STOCK made visible: a DERIVED body — the row says WHO stands
// here, the seed says everything else — wearing the OWNER's faction
// (the banner rule, spawn.h) and carrying the receipt that pays its
// death back into the roster (macro/macro_stock.h "roster": subject =
// the squad's MacroSpawnId ordinal, detail = this member's entityId).
// Placed on a tight ring around the leader, dodging water like every
// other placement here; a member that finds no land stands ON the
// leader's spot rather than being lost. Counted against the same
// projection cap as everyone else.
if (const auto* roster = reg.try_get<ecs::SquadRoster>(macro)) {
const auto* sid = reg.try_get<ecs::MacroSpawnId>(macro);
constexpr float kTau = 6.2831853f;
const int memberCount = int(roster->squad.size());
for (int m = 0; m < memberCount; ++m) {
if (projected >= kMaxProjectedMacroNpcs) {
// Roster rows past the ceiling stay safe in the macro
// roster — but never disappear from the scene silently.
if (truncated) *truncated = true;
break;
}
const SoldierRecord& rec = roster->squad[std::size_t(m)];
if (!valid_npc_kind(rec.kind)) continue;
float mfx = fx, mfy = fy;
for (int attempt = 0; attempt < 20; ++attempt) {
const float ang = (float(m) / float(memberCount)) * kTau
+ (rng.next_f01() - 0.5f) * 0.9f;
const float rad = 2.0f + rng.next_f01() * 3.0f;
const float tx = std::clamp(fx + std::cos(ang) * rad,
1.0f, float(kFullSize - 2));
const float ty = std::clamp(fy + std::sin(ang) * rad,
1.0f, float(kFullSize - 2));
const int ix = int(tx), iy = int(ty);
if (tilesUsable &&
tiles[std::size_t(iy) * kFullSize + ix] == TILE_WATER) {
continue;
}
mfx = tx; mfy = ty;
break;
}
// No MacroSpawnId (synthetic setups only — make_npc always
// stamps one) means no addressable roster: an honest fiat body
// rather than a receipt against nobody.
const BodyLoan loan = sid
? BodyLoan::from(
MacroStock::Roster,
MacroStockKey{std::int32_t(sid->index),
std::int16_t(int(mpos.x)),
std::int16_t(int(mpos.y)),
std::int32_t(rec.entityId)})
: BodyLoan::none();
// ONE birth for every member — the sheet-less second birth is
// dead: man or beast, the row and level project a sheet through
// spawn_derived_body (leader's bonuses applied in it), and every
// body carries the same roster receipt, so a wolf's death pays
// the pack back exactly like a spearman's.
spawn_derived_body(reg,
BodySpec{
static_cast<NPCType>(rec.kind), mfx, mfy,
kind.factionIdx,
normalize_soldier_level(rec.level),