Repository navigation
Expand file tree
/
Copy pathsim-manual.html
More file actions
1052 lines (991 loc) · 121 KB
/
Copy pathsim-manual.html
File metadata and controls
1052 lines (991 loc) · 121 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
<!doctype html>
<meta charset="utf-8">
<title>BOATZ — Simulation Field Manual</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
:root{
color-scheme: light dark;
--paper:#e6ece9; --panel:#f2f5f3; --panel-2:#eaefec;
--ink:#152730; --ink-soft:#4d6169; --ink-faint:#75898f;
--rule:#c3d0cc; --rule-soft:#d6dfdb;
--accent:#0d757f; --accent-ink:#0a5a62; --brass:#9a7526;
--crit:#b23528; --warn:#9c6314; --ok:#2c7a53;
--crit-bg:#f6e2df; --warn-bg:#f4ead6; --ok-bg:#dcefe4;
--sea:#0c1c24; --sea-line:#1b3742; --sea-ink:#dbe8ea;
--shadow:0 1px 2px rgba(15,35,42,.08),0 6px 22px rgba(15,35,42,.06);
--mono:ui-monospace,SFMono-Regular,"Cascadia Code","JetBrains Mono",Consolas,monospace;
--serif:"Iowan Old Style","Palatino Linotype",Palatino,"Book Antiqua",Georgia,serif;
--sans:system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
}
@media (prefers-color-scheme:dark){:root{
--paper:#0b151b; --panel:#0f1e26; --panel-2:#0c1a21;
--ink:#dde8ea; --ink-soft:#9db2b8; --ink-faint:#6d858c;
--rule:#22404a; --rule-soft:#193038;
--accent:#4fcdd4; --accent-ink:#7fdde2; --brass:#d3ac54;
--crit:#e56a5f; --warn:#e0a94a; --ok:#63b98a;
--crit-bg:#3a1a20; --warn-bg:#352c14; --ok-bg:#16311f;
--sea:#0a171e; --sea-line:#173039; --sea-ink:#dbe8ea;
--shadow:0 1px 2px rgba(0,0,0,.3),0 8px 26px rgba(0,0,0,.28);
}}
:root[data-theme="light"]{
--paper:#e6ece9; --panel:#f2f5f3; --panel-2:#eaefec;
--ink:#152730; --ink-soft:#4d6169; --ink-faint:#75898f;
--rule:#c3d0cc; --rule-soft:#d6dfdb;
--accent:#0d757f; --accent-ink:#0a5a62; --brass:#9a7526;
--crit:#b23528; --warn:#9c6314; --ok:#2c7a53;
--crit-bg:#f6e2df; --warn-bg:#f4ead6; --ok-bg:#dcefe4;
--shadow:0 1px 2px rgba(15,35,42,.08),0 6px 22px rgba(15,35,42,.06);
}
:root[data-theme="dark"]{
--paper:#0b151b; --panel:#0f1e26; --panel-2:#0c1a21;
--ink:#dde8ea; --ink-soft:#9db2b8; --ink-faint:#6d858c;
--rule:#22404a; --rule-soft:#193038;
--accent:#4fcdd4; --accent-ink:#7fdde2; --brass:#d3ac54;
--crit:#e56a5f; --warn:#e0a94a; --ok:#63b98a;
--crit-bg:#3a1a20; --warn-bg:#352c14; --ok-bg:#16311f;
--shadow:0 1px 2px rgba(0,0,0,.3),0 8px 26px rgba(0,0,0,.28);
}
*{box-sizing:border-box}
html{scroll-behavior:smooth}
@media (prefers-reduced-motion:reduce){html{scroll-behavior:auto}}
body{
margin:0; background:var(--paper); color:var(--ink);
font-family:var(--sans); font-size:16px; line-height:1.62;
-webkit-font-smoothing:antialiased;
background-image:radial-gradient(circle at 1px 1px, var(--rule-soft) 1px, transparent 0);
background-size:26px 26px;
}
.shell{max-width:1280px; margin:0 auto; padding:0 clamp(16px,4vw,44px);}
/* ---- masthead ---- */
header.mast{padding:clamp(40px,7vw,84px) 0 30px; border-bottom:2px solid var(--ink); position:relative;}
.eyebrow{font-family:var(--mono); font-size:12.5px; letter-spacing:.28em; text-transform:uppercase; color:var(--accent-ink); margin:0 0 14px;}
h1.title{font-family:var(--serif); font-weight:600; font-size:clamp(2.4rem,6vw,4.3rem); line-height:1.02; letter-spacing:-.01em; margin:0; text-wrap:balance;}
h1.title em{font-style:italic; color:var(--accent-ink);}
.dek{font-family:var(--serif); font-size:clamp(1.05rem,2.2vw,1.4rem); color:var(--ink-soft); max-width:60ch; margin:18px 0 0; text-wrap:balance;}
.mast-meta{display:flex; flex-wrap:wrap; gap:8px 10px; margin-top:26px;}
.chip{font-family:var(--mono); font-size:12px; letter-spacing:.02em; padding:5px 11px; border:1px solid var(--rule); border-radius:2px; background:var(--panel); color:var(--ink-soft);}
.chip b{color:var(--ink); font-weight:600;}
.themebtn{position:absolute; top:clamp(40px,7vw,84px); right:0; font-family:var(--mono); font-size:12px; letter-spacing:.08em; text-transform:uppercase; padding:8px 14px; border:1px solid var(--rule); background:var(--panel); color:var(--ink); border-radius:3px; cursor:pointer;}
.themebtn:hover{border-color:var(--accent);}
.themebtn:focus-visible{outline:2px solid var(--accent); outline-offset:2px;}
/* ---- layout ---- */
.grid{display:grid; grid-template-columns:236px minmax(0,1fr); gap:clamp(24px,4vw,56px); padding:40px 0 90px; align-items:start;}
nav.toc{position:sticky; top:18px; font-size:13.5px; max-height:calc(100vh - 36px); overflow:auto;}
nav.toc .toc-h{font-family:var(--mono); font-size:11px; letter-spacing:.2em; text-transform:uppercase; color:var(--ink-faint); margin:0 0 10px;}
nav.toc ol{list-style:none; margin:0 0 20px; padding:0; counter-reset:sec;}
nav.toc a{display:block; padding:5px 10px; color:var(--ink-soft); text-decoration:none; border-left:2px solid var(--rule-soft); line-height:1.35;}
nav.toc a:hover{color:var(--ink); border-left-color:var(--accent); background:var(--panel-2);}
nav.toc a.num::before{counter-increment:sec; content:counter(sec) " · "; font-family:var(--mono); font-size:11px; color:var(--accent-ink);}
main{min-width:0;}
section{scroll-margin-top:20px; margin:0 0 8px; padding:38px 0; border-top:1px solid var(--rule);}
section:first-of-type{border-top:none; padding-top:8px;}
h2{font-family:var(--serif); font-weight:600; font-size:clamp(1.55rem,3.4vw,2.15rem); letter-spacing:-.01em; margin:0 0 6px; line-height:1.1; text-wrap:balance;}
h2 .roman{font-family:var(--mono); font-size:.62em; letter-spacing:.05em; color:var(--accent-ink); margin-right:.6em; vertical-align:.12em;}
h3{font-family:var(--sans); font-weight:700; font-size:1.06rem; letter-spacing:.005em; margin:30px 0 8px;}
.decides{font-family:var(--mono); font-size:12.5px; color:var(--ink-soft); margin:4px 0 20px; padding:9px 14px; background:var(--panel); border-left:3px solid var(--accent); border-radius:0 3px 3px 0;}
.decides b{color:var(--accent-ink); letter-spacing:.06em; text-transform:uppercase; font-size:11px;}
p{margin:0 0 14px; max-width:74ch;}
ul.tight{margin:0 0 16px; padding-left:0; list-style:none; max-width:78ch;}
ul.tight>li{position:relative; padding-left:20px; margin:0 0 9px;}
ul.tight>li::before{content:""; position:absolute; left:3px; top:.62em; width:6px; height:6px; background:var(--accent); border-radius:50%;}
code,.k{font-family:var(--mono); font-size:.86em; background:var(--panel-2); padding:1px 5px; border-radius:3px; border:1px solid var(--rule-soft); color:var(--ink);}
.ref{font-family:var(--mono); font-size:.8em; color:var(--accent-ink); white-space:nowrap;}
b,strong{color:var(--ink); font-weight:700;}
em.term{font-style:normal; font-weight:600; color:var(--accent-ink);}
/* ---- chart panel (fixed dark chart-table in both themes) ---- */
/* A chart is a fixed-height window; the <pre> is the scroll surface, the svg is width-scaled to zoom.
Nothing here transforms or re-parents the svg, so it can never fight Mermaid's async render. */
.chart{position:relative; background:var(--sea); border:1px solid var(--sea-line); border-radius:6px; margin:22px 0 8px; height:clamp(380px,64vh,600px); overflow:hidden; box-shadow:var(--shadow);
background-image:linear-gradient(var(--sea-line) 1px,transparent 1px),linear-gradient(90deg,var(--sea-line) 1px,transparent 1px);
background-size:34px 34px,34px 34px;}
.chart pre.mermaid{position:absolute; inset:0; margin:0; padding:16px; overflow:auto; background:transparent; text-align:center; cursor:grab; touch-action:none; -webkit-user-select:none; user-select:none; scrollbar-width:thin; scrollbar-color:#2c515c transparent;}
.chart pre.mermaid.grabbing{cursor:grabbing;}
.chart pre.mermaid svg{display:block; margin:0 auto; width:100%; height:auto; max-width:none!important;}
.chart.pz-max{position:fixed; inset:14px; height:auto; z-index:1000; box-shadow:0 20px 70px rgba(0,0,0,.55);}
.pz-backdrop{position:fixed; inset:0; z-index:999; background:rgba(4,10,14,.66); backdrop-filter:blur(2px); border:0;}
body.pz-locked{overflow:hidden;}
/* per-chart controls */
.pz-tools{position:absolute; top:8px; right:8px; z-index:6; display:flex; gap:4px;}
.pz-tools button{width:30px; height:30px; display:flex; align-items:center; justify-content:center; font-family:var(--mono); font-size:15px; line-height:1; color:var(--sea-ink); background:rgba(10,23,30,.72); border:1px solid var(--sea-line); border-radius:4px; cursor:pointer; padding:0; backdrop-filter:blur(3px);}
.pz-tools button:hover{border-color:#4fcdd4; color:#9fe8ec;}
.pz-tools button:focus-visible{outline:2px solid #4fcdd4; outline-offset:1px;}
.pz-hint{position:absolute; left:11px; bottom:8px; z-index:5; font-family:var(--mono); font-size:10.5px; letter-spacing:.03em; color:#5f8a97; pointer-events:none; opacity:.85;}
.chart.pz-max{position:fixed; inset:14px; height:auto; z-index:1000; box-shadow:0 20px 70px rgba(0,0,0,.55);}
.pz-backdrop{position:fixed; inset:0; z-index:999; background:rgba(4,10,14,.66); backdrop-filter:blur(2px); border:0;}
body.pz-locked{overflow:hidden;}
.chart-cap{font-family:var(--mono); font-size:11.5px; letter-spacing:.04em; color:var(--ink-faint); margin:6px 2px 0; text-transform:uppercase;}
.legend{display:flex; flex-wrap:wrap; gap:14px; margin:14px 0 4px; font-family:var(--mono); font-size:11.5px; color:var(--ink-soft);}
.legend span{display:inline-flex; align-items:center; gap:6px;}
.swatch{width:22px; height:13px; border-radius:2px; border:1.5px solid;}
.sw-actor{background:#123c3f; border-color:#4fcdd4;}
.sw-dec{background:#15303a; border-color:#3c6f7c; border-radius:9px;}
.sw-issue{background:#3b1a22; border-color:#e0645a;}
.sw-trap{background:#352c14; border-color:#e0a94a;}
.sw-good{background:#173226; border-color:#63b98a;}
/* ---- callouts ---- */
.note{border:1px solid var(--rule); border-left-width:4px; border-radius:0 5px 5px 0; padding:13px 17px; margin:16px 0; background:var(--panel); font-size:14.5px;}
.note .tag{font-family:var(--mono); font-size:10.5px; letter-spacing:.14em; text-transform:uppercase; font-weight:700; display:inline-block; margin-right:8px;}
.note.crit{border-left-color:var(--crit); background:var(--crit-bg);} .note.crit .tag{color:var(--crit);}
.note.warn{border-left-color:var(--warn); background:var(--warn-bg);} .note.warn .tag{color:var(--warn);}
.note.ok{border-left-color:var(--ok); background:var(--ok-bg);} .note.ok .tag{color:var(--ok);}
.note p{margin:0; max-width:none;}
.note p+p{margin-top:8px;}
/* ---- tables ---- */
.twrap{overflow-x:auto; margin:20px 0; border:1px solid var(--rule); border-radius:6px; box-shadow:var(--shadow);}
table{border-collapse:collapse; width:100%; font-size:13.5px; min-width:640px;}
th,td{text-align:left; padding:10px 13px; border-bottom:1px solid var(--rule-soft); vertical-align:top;}
thead th{background:var(--panel); font-family:var(--mono); font-size:11px; letter-spacing:.08em; text-transform:uppercase; color:var(--ink-soft); position:sticky; top:0;}
tbody tr:hover{background:var(--panel-2);}
tbody tr:last-child td{border-bottom:none;}
td .ref{display:block; margin-top:3px;}
.sev{font-family:var(--mono); font-size:10.5px; font-weight:700; letter-spacing:.06em; padding:2px 8px; border-radius:3px; white-space:nowrap; text-transform:uppercase;}
.sev.high{background:var(--crit-bg); color:var(--crit); border:1px solid var(--crit);}
.sev.med{background:var(--warn-bg); color:var(--warn); border:1px solid var(--warn);}
.sev.low{background:var(--panel-2); color:var(--ink-soft); border:1px solid var(--rule);}
.actorcell{font-weight:700; white-space:nowrap;}
.sev.done{background:var(--ok-bg); color:var(--ok); border:1px solid var(--ok);}
tr.done td{background:linear-gradient(90deg,var(--ok-bg),transparent 60%);}
tr.done:hover td{background:var(--ok-bg);}
tr.done .find{text-decoration:line-through; text-decoration-color:var(--ink-faint); color:var(--ink-faint);}
.fix{display:block; margin-top:6px; font-family:var(--mono); font-size:11px; line-height:1.5; color:var(--ok); letter-spacing:.01em; text-decoration:none;}
.fix::before{content:"✓ RESOLVED "; font-weight:700; letter-spacing:.06em;}
/* ---- footer ---- */
footer{border-top:2px solid var(--ink); padding:26px 0 60px; font-family:var(--mono); font-size:12px; color:var(--ink-faint); letter-spacing:.02em;}
footer b{color:var(--ink-soft);}
@media (max-width:860px){
.grid{grid-template-columns:1fr; gap:8px;}
nav.toc{position:static; max-height:none; margin-bottom:14px; border:1px solid var(--rule); border-radius:6px; padding:16px; background:var(--panel);}
.themebtn{position:static; margin-top:22px;}
header.mast{padding-bottom:22px;}
}
</style>
<div class="shell">
<header class="mast">
<p class="eyebrow">Admiralty Dossier · System Map + Decision Atlas</p>
<h1 class="title">BOATZ — the <em>Simulation</em> Field Manual</h1>
<p class="dek">Every actor in this sea, every decision it makes, the numbers behind those decisions — and where the machinery deadlocks, starves, or quietly does nothing.</p>
<div class="mast-meta">
<span class="chip"><b>22</b> systems · fixed order</span>
<span class="chip">tick <b>20 Hz</b> · substep <b>0.05 s</b></span>
<span class="chip">1 sim-day = <b>60 s</b></span>
<span class="chip">server-authoritative · client renders only</span>
<span class="chip"><b>30</b> issues catalogued · <b style="color:var(--ok)">9 resolved</b></span>
</div>
<p class="dek" style="font-family:var(--mono); font-size:12.5px; line-height:1.5; color:var(--warn); background:var(--warn-bg); border-left:3px solid var(--warn); border-radius:0 3px 3px 0; padding:10px 14px; margin-top:20px; max-width:78ch;">⚠ <b>Living document.</b> Re-sync this manual after <em>any</em> change to the sim — systems, tuning, or bug fixes. A stale field manual is worse than none: it encodes decisions the code no longer makes. When you touch <code>game/sim/*</code> or <code>data/economy.json</code>, update the relevant section (and Figures) here in the same change, and reconcile the issue register.</p>
<button class="themebtn" id="themebtn" aria-label="Toggle light or dark theme">◐ Theme</button>
</header>
<div class="grid">
<nav class="toc" aria-label="Contents">
<p class="toc-h">The Chart Table</p>
<ol>
<li><a class="num" href="#pipeline">The tick & the pipeline</a></li>
<li><a class="num" href="#actors">The actors, at a glance</a></li>
<li><a class="num" href="#port">The Port — economy engine</a></li>
<li><a class="num" href="#hulls">The Port — buying hulls</a></li>
<li><a class="num" href="#magistrate">The Magistrate — policy loop & industry</a></li>
<li><a class="num" href="#planner">The Merchant Captain — planning a voyage</a></li>
<li><a class="num" href="#statemachine">The Ship — states & motion</a></li>
<li><a class="num" href="#crew">The Crew — morale & mutiny</a></li>
<li><a class="num" href="#pirate">The Pirate — the hunt</a></li>
<li><a class="num" href="#combat">Combat — how a fight resolves</a></li>
<li><a class="num" href="#navy">The Navy — danger, bounty, privateers</a></li>
<li><a class="num" href="#haven">The Haven & the Pirate Lord</a></li>
<li><a class="num" href="#diplomacy">The Diplomatic layer</a></li>
<li><a class="num" href="#weather">The Weather</a></li>
<li><a class="num" href="#info">The Information layer</a></li>
<li><a class="num" href="#register">Issue register</a></li>
</ol>
<p class="toc-h">Diagram key</p>
<div class="legend" style="flex-direction:column; gap:7px;">
<span><i class="swatch sw-actor"></i> actor / entry</span>
<span><i class="swatch sw-dec"></i> decision</span>
<span><i class="swatch sw-issue"></i> bug / deadlock</span>
<span><i class="swatch sw-trap"></i> fragile / trap</span>
<span><i class="swatch sw-good"></i> verified sound</span>
</div>
</nav>
<main>
<!-- ============ 1. PIPELINE ============ -->
<section id="pipeline">
<h2><span class="roman">I.</span>The tick & the pipeline</h2>
<div class="decides"><b>Decides:</b> nothing — this is the clockwork the actors run inside. Order is load-bearing.</div>
<p>The server owns the whole world. Every real tick (<code>20 Hz</code>), a <em class="term">clock</em> turns real elapsed time × the speed dial into <code>dtSim</code> sim-seconds (<code>0</code> when paused). The <em class="term">sim</em> then advances the world in fixed <code>0.05 s</code> substeps — so 10× fast-forward is literally ten 1× steps, identical dynamics at every speed <span class="ref">world.js:182</span>. A single ordered list of <b>22 systems</b> runs once per substep <span class="ref">systems.js:27</span>; the order below is exact, and it matters — production happens before pricing, pricing before dispatch, every fleet moves before the shore guns and the collision solver read final positions, and the magistrate's daily <code>policy</code> loop runs after <code>governance</code> (reading fresh loyalty/civ) and before <code>havens</code>.</p>
<p>The client is a pure viewer: it merges server snapshots by id and never simulates <span class="ref">SimClient.js:7</span>. The only authoritative thing a viewer can change today is the clock.</p>
<div class="chart">
<pre class="mermaid" id="fig1">
%%{init:{'theme':'base','themeVariables':{'fontSize':'14px','primaryColor':'#15303a','primaryTextColor':'#e7eff1','primaryBorderColor':'#3c6f7c','lineColor':'#5f8a97','clusterBkg':'#0e2029','clusterBorder':'#295059','edgeLabelBackground':'#0e2029','titleColor':'#dfe9ec'}}}%%
flowchart TB
CLK["CLOCK — realDt × speed → dtSim<br/>0 when paused"]:::actor --> LOOP
subgraph LOOP["SUBSTEP LOOP — repeat ⌈dtSim ÷ 0.05s⌉ times, ≤ 20"]
direction TB
I["applyIntents — player/ext commands"] --> ENV
subgraph ENV["① Environment"]
direction LR
WE["weather<br/>season · storms"] --> WN["wind drift"]
end
ENV --> ECO
subgraph ECO["② Economy — the port"]
direction LR
PB["produceBase"] --> PG["produceGoods"] --> POP["population<br/>eat · starve · grow"] --> PR["pricing"]
end
ECO --> TRD
subgraph TRD["③ Trade"]
direction LR
DI["dispatch<br/>assign voyages"] --> SH["ship<br/>move · dock · settle"]
end
TRD --> CFL
subgraph CFL["④ Conflict"]
direction LR
PI["piracy<br/>pirates move/fight"] --> AP["antipiracy<br/>privateers"] --> SB["shoreBatteries"] --> SEP["separation<br/>anti-collision"]
end
CFL --> SOC
subgraph SOC["⑤ Society"]
direction LR
CR["crew<br/>morale · mutiny"] --> UP["upkeep<br/>gold in/out · skim · workshop drift"] --> RE["reputation"] --> EV["events<br/>blight · plague"] --> GV["governance<br/>loyalty · rebellion"] --> PO["policy<br/>magistrate levers · daily"]:::good --> HV["havens<br/>fall · pirate lord"]
end
SOC --> GRW
subgraph GRW["⑥ Growth — daily-gated"]
direction LR
CT["contracts"] --> VY["reckonVoyages<br/>presume lost"]
end
end
LOOP --> CHR["CHRONICLE — drain events → SQLite<br/>write-only, never read back"]:::good
classDef actor fill:#123c3f,stroke:#4fcdd4,color:#d6faf9,stroke-width:1.4px;
classDef good fill:#173226,stroke:#63b98a,color:#cdeed9;
</pre>
</div>
<p class="chart-cap">Fig. I — one authoritative substep. Broadcast cadence: hot ship positions ~10 Hz; economy + cold ship panels ~1 Hz.</p>
</section>
<!-- ============ 2. ACTORS ============ -->
<section id="actors">
<h2><span class="roman">II.</span>The actors, at a glance</h2>
<div class="decides"><b>Decides:</b> who holds a will in this world, and what levers each one pulls.</div>
<p>There is no single "AI." Behaviour is distributed across independent agents, each mutating its own slice of state on its own trigger. Two of them mirror each other exactly: a ship's <b>crew</b> (morale → mutiny, gated by the captain's Command) and an island's <b>populace</b> (loyalty → rebellion, gated by the magistrate's skill) are the same machine at two scales.</p>
<div class="twrap">
<table>
<thead><tr><th>Actor</th><th>Trigger</th><th>Chief decisions</th><th>Owns / mutates</th></tr></thead>
<tbody>
<tr><td class="actorcell">Port / Island</td><td>every substep</td><td>produce (via mutable workshops), price, spend income, post contracts</td><td>stock, gold, price, population, workshops, targets</td></tr>
<tr><td class="actorcell">Magistrate</td><td>substep + daily</td><td>set ambition (retargets demand); run a daily <b>policy loop</b> over 5 levers — build/switch/demolish/repair workshops, set tax & tariff, garrison, public works, festivals & naval expansion, export holds; skim a corrupt hoard; hold or lose loyalty</td><td>loyalty, lawlessness, grievance, workshops, tax, tariff, development, _approval, hoard, targets</td></tr>
<tr><td class="actorcell">Populace</td><td>every substep</td><td>eat, grow/starve, migrate, rebel</td><td>population, civ score</td></tr>
<tr><td class="actorcell">Merchant Captain</td><td>when ship idle</td><td>plan the whole voyage basket (food→aid→exports→migration→scout)</td><td>ship.voyage, cargo plan</td></tr>
<tr><td class="actorcell">Ship</td><td>every substep</td><td>sail, steer round land, dock, flee, shelter, tack, heave-to & jury-rig (if stranded & hurt), render aid (supplies)</td><td>x/y/heading, state, hull, rig, hullSound/rigSound, _heaveUntil</td></tr>
<tr><td class="actorcell">Crew</td><td>every substep at sea</td><td>eat, hold or break morale, mutiny / desert / turn pirate (captain-led or crew cast-out)</td><td>morale, hunger, unrest, uprising</td></tr>
<tr><td class="actorcell">Pirate Captain</td><td>every substep</td><td>hunt (gang up on prey · focus-fire a consort's mark), turn on / flee a hunter <em>by the local balance of force</em>, heave-to & jury-rig, hunt for timber, break off to mend, blockade, raid, defend a haven, resupply</td><td>_prey, _blockadeId, _heaveUntil, position, combat</td></tr>
<tr><td class="actorcell">Privateer</td><td>every substep</td><td>hunt pirates (gang up · focus-fire a claimed raider), break off a losing fight, heave-to & jury-rig, patrol, besiege havens, resupply</td><td>_prey, _guard, _breakoff, _heaveUntil, position, combat</td></tr>
<tr><td class="actorcell">Shore battery</td><td>every 12 s</td><td>shell the nearest pirate (port) or besieger (haven)</td><td>target hull/rig/guns</td></tr>
<tr><td class="actorcell">Haven</td><td>daily</td><td>entrench, build pirates, fence plunder, resist assault</td><td>havenStrength, lawlessness</td></tr>
<tr><td class="actorcell">Pirate Lord</td><td>daily</td><td>the dark mirror of the magistrate — run a war-economy loop: skim fenced plunder to a hoard, convert derelict civilian works into gun-foundries, entrench, build raiders</td><td>pirateLord (traits/agenda/hoard), workshops, havenStrength</td></tr>
<tr><td class="actorcell">Diplomacy</td><td>on trade + daily</td><td>warm/cool opinions, form blocs, embargo, betray, send aid</td><td>reputation matrix</td></tr>
<tr><td class="actorcell">Weather</td><td>substep + daily</td><td>swing seasonal production, spawn/steer storms, drift the wind</td><td>_prodMult, storms, wind vector</td></tr>
<tr><td class="actorcell">The record (beliefs)</td><td>on dock / at sea</td><td>carry imperfect prices & facts port→ship→port; forget the stale</td><td>island.beliefs, ship.knows, intel</td></tr>
</tbody>
</table>
</div>
</section>
<!-- ============ 3. PORT ECONOMY ============ -->
<section id="port">
<h2><span class="roman">III.</span>The Port — the economy engine</h2>
<div class="decides"><b>Decides:</b> what a port makes, what it charges, and whether its people eat or breed. Runs blind (no will) — pure functions of stock and population.</div>
<ul class="tight">
<li><b>Produce.</b> Each island mines its primary raw at <code>10/s</code> and secondary at <code>×0.25</code>, with a logistic taper toward a <code>2000</code> stockpile cap <span class="ref">production.js:17</span>. Recipe goods scale to population (<code>0.07 × pop</code>) but are clamped so ≥25% of any local raw survives as exportable surplus — this is what <em class="term">forces trade</em> <span class="ref">production.js:46-62</span>. Finished <em class="term">industrial</em> goods (Weapons, Clothing, Luxury, Ships) now flow from <b>mutable workshops</b> — their output is further scaled by each workshop's <code>condition × staffing</code>, so a squeezed port's manufacturing visibly rots (see §V). <b>Food and Ale are excluded</b> — survival goods stay always-on, never workshop-gated, so a poor port can never starve itself into a famine spiral <span class="ref">production.js:37-56</span>.</li>
<li><b>Consume.</b> Population eats <code>0.011 food/head</code>, plus comfort goods (Ale, Clothing, Weapons, Luxury) burned as demand sinks so every port keeps importing <span class="ref">population.js:28-41</span>. On top of comfort, four goods gain a <em class="term">second, demand-scaling sink</em> so none merely piles to its cap: <b>Clothing</b> is issued as crew <b>slops</b> (worn at sea, re-issued at port like grog) <span class="ref">crew.js</span>; <b>Weapons</b> feed a standing <b>garrison militia</b> that drills daily — scaled by disorder — even in peacetime <span class="ref">upkeep.js</span>; <b>Luxury</b> (with some ale) is broken out at magistrate <b>festivals</b> (see §V) <span class="ref">policy.js · population.js</span>; and <b>Meat</b> is now a <em class="term">premium food raw</em> (a higher Food yield than Grain, so importers prefer it) rather than a pure Grain substitute <span class="ref">production.js · economy.json</span>.</li>
<li><b>Live or die.</b> Starvation deaths are rate-bounded and floored at <code>pop 5</code>; growth is <em class="term">gated on food security</em> (2+ days stored) so population can never outrun its larder <span class="ref">population.js:44-57</span>. There is no true death-spiral here.</li>
<li><b>Price.</b> Mid price = <code>base × clamp((target÷stock)^1, 0.33, 3.0)</code>, eased toward target by a non-overshooting 10%/s limiter; bid/ask are ±5% <span class="ref">pricing.js:13-26</span>.</li>
<li><b>Bank.</b> Income = <code>2.4 × pop × (0.4 + 0.6·civ)</code>, scaled by <code>(1 + tax·TAX_INCOME_W)</code>; sinks are ship upkeep + a <b>per-workshop upkeep</b> bill (<code>WORKSHOP_UPKEEP_PER_DAY·pop</code> each — this <em>replaces</em> the old flat <code>PROD_UPKEEP</code> aggregate, so a farm with no industrial works now pays zero factory upkeep), and any gold above <code>40 × pop</code> overflows and is destroyed. A corrupt magistrate also <b>skims</b> a slice of income into a hidden hoard here. <b>Gold is deliberately not conserved</b> <span class="ref">upkeep.js:25-92</span>.</li>
</ul>
<div class="chart">
<pre class="mermaid" id="fig2">
%%{init:{'theme':'base','themeVariables':{'fontSize':'14px','primaryColor':'#15303a','primaryTextColor':'#e7eff1','primaryBorderColor':'#3c6f7c','lineColor':'#5f8a97','clusterBkg':'#0e2029','clusterBorder':'#295059','edgeLabelBackground':'#0e2029'}}}%%
flowchart LR
RAW["Mine primary/secondary raw<br/>logistic taper → cap 2000"]:::actor --> MAKE
MAKE["Make recipe goods<br/>rate = 0.07 × pop<br/>clamp: leave ≥25% raw"] --> STOCK[("Stockpiles")]
STOCK --> EAT["Populace eats<br/>0.011 food/head + comforts"]
EAT --> Q1{"food ≥ 2 days<br/>stored ?"}
Q1 -->|yes| GROW["Grow: logistic toward K<br/>× order (1 − 0.8·lawless)"]
Q1 -->|deficit| STARVE["Deaths ∝ deficit<br/>floored at pop 5"]:::good
GROW --> STOCK
STARVE --> STOCK
STOCK --> PRICE["Price = base × clamp(target÷stock, 0.33×, 3×)<br/>eased 10%/s"]
PRICE --> BANK{"Bank: income − upkeep"}
BANK -->|"gt 40 × pop"| BURN["Overflow gold DESTROYED"]:::trap
BANK -->|else| HOARD[("Treasury")]
EAT -. "spoilage 0.0003" .-> SPOIL["Food decays here…"]:::issue
STOCK -. "spoilage 0.0004" .-> SPOIL2["…and AGAIN in upkeep"]:::issue
SPOIL --- SPOIL2
classDef actor fill:#123c3f,stroke:#4fcdd4,color:#d6faf9,stroke-width:1.4px;
classDef issue fill:#3b1a22,stroke:#e0645a,color:#ffdad4,stroke-width:1.3px;
classDef trap fill:#352c14,stroke:#e0a94a,color:#ffe9c2;
classDef good fill:#173226,stroke:#63b98a,color:#cdeed9;
</pre>
</div>
<p class="chart-cap">Fig. III — the port's blind loop. Red = defect; amber = intended-but-fragile.</p>
<div class="note warn"><span class="tag">Trap</span><p><b>Food is spoiled twice per substep.</b> <code>population</code> decays Food by <code>0.0003·h</code> and then <code>upkeep</code> decays <em>all</em> stock — Food included — by another <code>0.0004·h</code> <span class="ref">population.js:32 · upkeep.js:45</span>. Food loses ~2.3× the decay rate of every other good, tightening the very margin that drives the "starving tail."</p></div>
<div class="note ok"><span class="tag">Sound</span><p>Starvation is floored and growth is food-gated, so no runaway death-spiral. Every good/gold trade move goes through the conserved <code>transfer</code> primitive — the <em>only</em> sanctioned gold source/sink is <code>upkeep</code> (plus development & contract escrow).</p></div>
<div class="note ok"><span class="tag">Sinks</span><p><b>Every trade good now has a genuine demand sink, not just background spoilage.</b> Raws drain into recipes (plus Wood/Fiber into ship repair and Wood/Iron into hulls); <b>Food</b> → population, crew rations, raids, aid; <b>Ale</b> → comfort + crew grog + festivals; <b>Clothing</b> → comfort + crew slops; <b>Weapons</b> → comfort + combat/shore fire + garrison militia; <b>Luxury</b> → comfort + festivals (which finally pull its one-consumer input <b>PreciousMetal</b> through the chain); <b>Ships</b> → fleet purchases (producer-throttled at <code>SHIP_STOCK_CAP</code>). Meat is a demanded premium raw rather than a redundant Grain substitute. So no good sits inert at its cap.</p></div>
</section>
<!-- ============ 4. BUYING HULLS ============ -->
<section id="hulls">
<h2><span class="roman">IV.</span>The Port — buying hulls <span style="font-size:.5em; color:var(--ok); font-family:var(--mono); letter-spacing:.1em;">✓ repriced & unblocked</span></h2>
<div class="decides"><b>Decides:</b> how a port replaces lost ships. The old ~3× mispricing is fixed, and the parallel fleet-buyer is folded into the magistrate's policy budget.</div>
<p>A port grows its fleet two ways: opportunistically, by sending an idle hull to a shipyard on a voyage; or deliberately, via the magistrate's <b>naval-expansion lever</b> — fleet-buying is no longer a split-brain <code>development</code> system but one ambition-scored action inside the single treasury-budgeted <code>policy</code> loop (<code>development.js</code> is retained only as the executor the lever calls). <b>The pricing bug (#1) is fixed:</b> <code>TARGET_OVERRIDES.Ships = 3</code> matches the yard's <code>stock cap 3</code>, so the ratio is <code>3÷3 = 1</code> and the multiplier sits near <code>1×</code> — a hull asks its named base (~<code>1200g</code>) instead of the old pinned ~<code>3780g</code>. That drops the buy threshold from <b>pop ≈ 120 down to pop ≈ 57</b>, and the magistrate's tax + public-works levers give a modest port an emergent path to accumulate toward a hull (#2).</p>
<div class="chart">
<pre class="mermaid" id="fig3">
%%{init:{'theme':'base','themeVariables':{'fontSize':'14px','primaryColor':'#15303a','primaryTextColor':'#e7eff1','primaryBorderColor':'#3c6f7c','lineColor':'#5f8a97','clusterBkg':'#0e2029','clusterBorder':'#295059','edgeLabelBackground':'#0e2029'}}}%%
flowchart TB
ROOT["Shipyard stock.Ships capped at 3,<br/>price target = 3 (TARGET_OVERRIDES.Ships)<br/>⇒ ratio 3÷3 = 1 ⇒ multiplier ≈ 1×"]:::good
ROOT --> PRICE["Real ask ≈ base 1200g<br/>(the named price, at last)"]:::good
PRICE --> PA & PB
subgraph PATHA["Path A — buy-ship-on-voyage"]
direction TB
PA["Demand gate: owned in 1..5<br/>and gold gt 1800"] --> PA2{"believes yard price?<br/>(via beliefMid / believedHaven —<br/>no live remote read)"}
PA2 -->|"loads ~1.5× belief"| PA3{"gold ≥ ~1800 ?<br/>(pop ≈ 57, not 126)"}
PA3 -->|"common now"| PAOK["Buys a hull"]:::good
PA3 -->|"lean port"| PAF["Waits, accumulates"]
end
subgraph PATHB["Path B — magistrate NAVAL lever (daily)"]
direction TB
PB["fleet-ambition (grow/wealth/industry)<br/>+ treasury clears the reserve"] --> PB2{"gold ≥ ask ~1200 + reserve<br/>(believed yard price/haven)"}
PB2 -->|"pop ≳ 57"| PBOK["Commission a hull"]:::good
PB2 -->|"tiny port"| PBNO["Tax + public works lift pop<br/>→ higher cap → afford one"]:::good
end
CAP["Hoard cap 40 × pop<br/>now clears the ~1200 ask by pop ≈ 57"]:::good --> PA3
CAP --> PB2
classDef issue fill:#3b1a22,stroke:#e0645a,color:#ffdad4,stroke-width:1.3px;
classDef trap fill:#352c14,stroke:#e0a94a,color:#ffe9c2;
classDef good fill:#173226,stroke:#63b98a,color:#cdeed9;
</pre>
</div>
<p class="chart-cap">Fig. IV — the fleet-replacement machine, now unjammed: the yard prices at base and both paths clear the hoard cap for a mid-sized port.</p>
<div class="note ok"><span class="tag">Fixed · #1</span><p><b>Ships now cost their base price.</b> <code>TARGET_OVERRIDES.Ships = 3</code> was added so the Ships price target matches <code>SHIP_STOCK_CAP = 3</code>: the ratio is <code>3÷3 = 1</code> and the multiplier no longer pins at the <code>3.0</code> ceiling <span class="ref">economy.json:49 · pricing.js:14</span>. Ask ≈ the advertised <b>1200g</b>, not the old ~<code>3780g</code>. Every downstream threshold keyed to 1200 is now honest. (<code>simStability</code> held on all three seeds without re-deriving bounds.)</p></div>
<div class="note ok"><span class="tag">Fixed · #2</span><p><b>Mid-sized ports can replace a hull; the tail has a growth path.</b> At the corrected ~<code>1200g</code> ask, the buy threshold falls from pop ≈ 120 to <b>pop ≈ 57</b> — below the <code>40 × pop</code> hoard cap for most ports. The genuinely tiny (pop 40–57) tail is now relieved by the magistrate's <b>tax + public-works</b> levers: a modest port raises tax, funds public works that lift civ → population → a higher hoard cap, and accumulates toward a hull, rather than decaying forever. <span class="ref">upkeep.js:25 · policy.js:245-276</span></p></div>
</section>
<!-- ============ 5. MAGISTRATE ============ -->
<section id="magistrate">
<h2><span class="roman">V.</span>The Magistrate — the policy loop & mutable industry</h2>
<div class="decides"><b>Decides:</b> a port's economic <em>ambition</em> — which now drives real <b>actions</b> across five levers (industry, tax, garrison, infrastructure, trade), not just a demand curve — plus whether the populace stays loyal or burns the docks, and how much a corrupt ruler quietly skims.</div>
<p>Each island is ruled by a named magistrate with three traits (<code>firmness, generosity, integrity</code>) and one of six agendas — <code>grow, industry, wealth, fortify, splendor, order</code> — chosen by the island's nature <span class="ref">magistrate.js:69-85</span>. The agenda <b>retargets the economy</b>: it rewrites the port's <code>targets[]</code> (fortify boosts Weapons ×2.4, splendor Luxury ×2.2, etc.), and those targets <em>are</em> the port's dynamic demand that the trade planner reads. Food is always floored at base — no agenda can starve the port <span class="ref">magistrate.js:114-125</span>. <b>New:</b> the magistrate is no longer a one-decision-per-regime actor. A daily <code>policy</code> loop (below) turns that ambition into concrete decrees — building works, setting taxes and tariffs, garrisoning, funding public works — reasoning about the wider world only through the island's own stale <b>beliefs/intel</b> (no omniscience), so a governor can be as wrong as any captain.</p>
<p>Loyalty eases toward a steady value set by civ, skill, generosity and lawlessness, dragged down by famine, plague, crowding and graft, buoyed by "bread & circuses" (Ale+Luxury in stock) <span class="ref">magistrate.js:258-266</span>. Below loyalty <code>0.25</code>, unrest accrues; once it passes a skill-scaled grace window, the island <b>rebels</b> — halting all production and income for exactly one day — then resolves to a <em class="term">quell</em> or an <em class="term">overthrow</em> <span class="ref">magistrate.js:272-318</span>.</p>
<div class="chart">
<pre class="mermaid" id="fig4">
%%{init:{'theme':'base','themeVariables':{'fontSize':'14px','primaryColor':'#15303a','primaryTextColor':'#e7eff1','primaryBorderColor':'#3c6f7c','lineColor':'#5f8a97','clusterBkg':'#0e2029','clusterBorder':'#295059','edgeLabelBackground':'#0e2029'}}}%%
flowchart TB
INSTALL["Seat a magistrate<br/>traits + agenda"]:::actor --> RETARGET["Retarget demand<br/>targets[] × agenda bias"]
RETARGET --> DEMAND[("Port demand → trade planner")]:::good
INSTALL --> LOY["Loyalty eases to steady:<br/>+civ +skill +generosity<br/>−famine −plague −crowd −graft<br/>+circuses"]
LOY --> Q1{"loyalty lt 0.25 ?"}
Q1 -->|no| CALM["+6 xp/day, unrest decays"]:::good
Q1 -->|yes| UNREST["unrest += 1/day"]
UNREST --> Q2{"unrest ≥ grace<br/>(1.1–6.7 d, scales with skill/firmness,<br/>shortened by grievance) ?"}
Q2 -->|not yet| LOY
Q2 -->|yes| REBEL["REBELLION (1 day)<br/>production = 0, income = 0<br/>civ drains, upkeep still bleeds"]:::trap
REBEL --> RES{"resolve after 1 day:<br/>rng lt quell-odds ?"}
RES -->|quell| QUELL["Hold power<br/>grievance += 0.13"]
RES -->|overthrow| OVER["New magistrate + agenda<br/>civ ×0.75, gold ×0.65<br/>lawlessness += 0.18"]
QUELL --> GRIEV["Grievance ratchet ↑<br/>→ lawlessness ↑<br/>→ shorter next grace"]:::trap
GRIEV --> HAVEN{"lawlessness ≥ 0.58<br/>and civ ≤ 0.34, 5 days ?"}
OVER --> HAVEN
HAVEN -->|"yes and slot free"| FALL["Port FALLS to a haven → §XII"]
HAVEN -->|"cap full"| LOOP2["Can't fall — HAVEN_CAP_RELIEF eases<br/>lawlessness 0.03/day → exits the churn"]:::good
classDef actor fill:#123c3f,stroke:#4fcdd4,color:#d6faf9,stroke-width:1.4px;
classDef issue fill:#3b1a22,stroke:#e0645a,color:#ffdad4,stroke-width:1.3px;
classDef trap fill:#352c14,stroke:#e0a94a,color:#ffe9c2;
classDef good fill:#173226,stroke:#63b98a,color:#cdeed9;
</pre>
</div>
<p class="chart-cap">Fig. V — the mirror of crew mutiny at island scale. Grievance is the one-way ratchet toward a haven.</p>
<div class="note ok"><span class="tag">Sound</span><p>Rebellions always start when warranted and <b>always end</b> after exactly one day (resolution is unconditional). Overthrow vents grievance (×0.4) and haven-redemption caps it at 0.25, so an island is never <em>permanently</em> ungovernable — though the recovery road may run through becoming a haven first. The lawlessness↔civ loop converges (L≈0.27) rather than running away; it only tips to haven territory under an external shock (piracy danger).</p></div>
<div class="note ok"><span class="tag">Fixed · #13 / #29</span><p><b>The formerly-unwinnable agendas are winnable.</b> Industry is redefined <em>by</em> workshops: <code>ambitionSignal('industry')</code> now scores the count × condition of built works against <code>slotCap</code> — a port raising more, healthier workshops is <em>succeeding</em>, so a shipyard is no longer pinned at <code>−1</code> <span class="ref">magistrate.js:157</span>. <code>wealth</code> is now scored <b>treasury-relative</b> (gold per cap, not an absolute 2000g against a low hoard cap), so a shrunken port can still satisfy it <span class="ref">magistrate.js:168</span>. And <code>order</code> is functional — the tax/public-works/garrison levers give it real tools to buy down the lawlessness it was installed to fix.</p></div>
<div class="note ok"><span class="tag">Fixed · #14</span><p><b>The haven-cap rebellion loop has an exit.</b> When the pirate budget is full and a fully-failed island <em>cannot</em> fall to a haven, <code>maybeFall</code> now eases its lawlessness by <code>HAVEN_CAP_RELIEF</code> (0.03/day) and caps the pressure — so instead of churning overthrow-every-5-days forever, an at-cap port bleeds pressure back down and climbs out of the churn <span class="ref">havens.js · economy.json:489</span>.</p></div>
<h3>Mutable industry — workshops, slots & condition</h3>
<p>An island's finished-goods industry is no longer fixed at world-gen. A port's <b>industrial</b> goods (<code>Weapons, Clothing, LuxuryGoods, Ships</code>) now live in <b>mutable workshops</b> that can be built, switched, demolished and repaired. <code>island.workshops = [{good, condition}]</code> is the <b>single source of truth</b>; the old <code>island.produces[]</code> is now a derived cache rebuilt by the one mutator <code>mutateWorkshops()</code> that every path routes through <span class="ref">island.js · production.js:37-56</span>. <b>Food and Ale are deliberately excluded</b> — survival goods stay always-on (never gated by labour or funding), so a poor port can never decay its own food supply into a famine spiral.</p>
<ul class="tight">
<li><b>Slots bound the works.</b> <code>slotCap = min(6, 2 + popTiers + development)</code>, floored by what's already built — a seed port (pop 80) starts at <code>SLOT_BASE = 2</code> with vacant berths to industrialise into; passing <code>150 / 400 / 900 / 1800</code> population each grants one more, and the magistrate can buy <code>development</code> slots <span class="ref">economy.json:102-105</span>.</li>
<li><b>Workshops need labour + upkeep, and rot when starved.</b> Each demands <code>LABOR_PER_WORKSHOP = 10</code> from a labour pool of <code>pop × WORKFORCE_FRAC(0.5)</code>, so <code>staffing = min(1, pool ÷ Σdemand)</code> — an over-built or famine-shrunk port can't crew them all. Each bills <code>WORKSHOP_UPKEEP_PER_DAY(0.05)·pop</code> (this <em>replaced</em> the flat <code>PROD_UPKEEP</code> aggregate); ship upkeep is paid first, the rest funds the works. A workshop's <code>condition</code> eases toward <code>min(staffing, funding)</code> at <code>0.15/day</code> (like morale/loyalty), and its output scales <code>condition × staffing</code>. Derelict (condition ≈ 0) it makes nothing but holds its slot until repaired or demolished <span class="ref">upkeep.js:62-92</span>.</li>
<li><b>Three status bytes drive the UI + silhouette.</b> <code>st ∈ {running, idle, derelict}</code> is precomputed server-side (idle = unstaffed/unfunded or input-starved; derelict = condition ≈ 0), streamed with each workshop, and drives the InfoPanel status gauge and the map building sprite (disrepair changes the <em>silhouette</em>, not just tint) <span class="ref">snapshot.js · WorldRenderer.js</span>.</li>
</ul>
<h3>The daily policy loop — five levers</h3>
<div class="decides"><b>Decides:</b> once per day, the single capital action (plus a few cheap standing knobs) a magistrate takes to serve its ambition, its treasury, and its people. All cross-island reasoning runs on the island's own stale beliefs/intel — the loop honours the no-omniscience contract.</div>
<p>A new <code>policy</code> system (<code>policy.js</code>, daily-gated, runs after <code>governance</code>) skips any island that's a haven, in rebellion, or lacking a magistrate. For the rest it first turns the <b>continuous knobs</b> — tax, garrison, tariff, export holds — then, once off a per-island cooldown and above a treasury reserve, takes <b>at most one capital action</b> down a strict priority ladder. <code>scoreGood</code> ranks a candidate workshop as <code>1.0·margin + 0.9·ambition-fit + 0.2·welfare − 0.6·import-risk − 0.5·saturation + 0.3·diversity</code>; ambition is a <em>strong</em> steer (weight 0.9 — the feature thesis), while the saturation term (how many known ports already make the good) keeps industry from homogenising into all-Luxury/all-Weapons <span class="ref">policy.js:87-370 · economy.json:562-581</span>.</p>
<div class="chart">
<pre class="mermaid" id="fig4b">
%%{init:{'theme':'base','themeVariables':{'fontSize':'14px','primaryColor':'#15303a','primaryTextColor':'#e7eff1','primaryBorderColor':'#3c6f7c','lineColor':'#5f8a97','clusterBkg':'#0e2029','clusterBorder':'#295059','edgeLabelBackground':'#0e2029'}}}%%
flowchart TB
DAY["Once/day per island<br/>skip haven · rebellion · no magistrate"]:::actor --> CONT
subgraph CONT["Continuous knobs — every eligible day"]
direction LR
TAX["TAX ±0.05<br/>hysteresis band"] --> GAR["GARRISON<br/>Weapons target"] --> TAR["TARIFF<br/>foreign duty"] --> HLD["HOLDS<br/>export bans"]
end
CONT --> GATE{"off _policyCd AND<br/>treasury > reserve ?"}
GATE -->|no| WAIT["accumulate"]
GATE -->|yes| ONE["ONE capital action, first that fires:"]
ONE --> A1["① DEMOLISH a workshop idle/derelict ≥ 8d<br/>(free slot, stop the bleed)"]
A1 --> A2["② BUILD best good into an open slot<br/>(800g + 30 Wood + 15 Iron; staffing ≥ 0.8)"]:::good
A2 --> A3["③ NAVAL — commission a hull (fleet-ambition)"]
A3 --> AF["④ FESTIVAL — break out luxuries<br/>+approval +civ, draws trade (splendor)"]:::good
AF --> A4["⑤ PUBLIC WORKS 600g → +civ −lawless +approval"]
A4 --> A5["⑥ DEVELOP +1 slot 1500g (full & industrial)"]
A5 --> A6["⑦ SWITCH worst workshop to a much better good"]
A6 --> A7["⑧ REPAIR a below-0.4 workshop it can crew (250g)"]
A2 -. "scoreGood via BELIEFS/INTEL<br/>profit + 0.9·ambition − saturation" .-> BEL["stale reports only —<br/>the governor can be wrong"]:::good
classDef actor fill:#123c3f,stroke:#4fcdd4,color:#d6faf9,stroke-width:1.4px;
classDef good fill:#173226,stroke:#63b98a,color:#cdeed9;
</pre>
</div>
<p class="chart-cap">Fig. Va — the magistrate's daily decision. Cheap knobs always; then one throttled capital action, first-match-wins. <code>development.js</code>'s old fleet-buyer is now rung ③, budgeted alongside everything else.</p>
<ul class="tight">
<li><b>Industry</b> — build / switch / demolish / repair workshops (rungs ①②⑥⑦). Build cost is <code>800g + 30 Wood + 15 Iron</code>; a <code>WORKSHOP_COOLDOWN_DAYS(12)</code> stops build↔demolish thrashing one good; input-starvation (Idle for lack of inputs) is its own demolish trigger, so an import-starved money-pit doesn't live forever.</li>
<li><b>Taxation</b> — <code>island.tax ∈ [0, 0.4]</code> scales income <code>×(1 + tax)</code>, but also raises lawlessness (<code>LAWLESS_TAX·tax</code>, closing the gold→civ back-door so tax has an <em>interior</em> optimum) and sours <code>_approval</code>. Nudged ±0.05 inside a hysteresis band (cut below loyalty 0.35, raise above 0.6) so no approval limit-cycle forms.</li>
<li><b>Garrison</b> — a fortify/order mayor, or any port in dangerous waters, lifts its Weapons target to <code>GARRISON_WEAPONS_TARGET(80)</code> so trade imports guns for the shore batteries.</li>
<li><b>Infrastructure</b> — buy a <code>+1</code> slot (<code>1500g</code>), or fund <b>public works</b> (<code>600g</code> → <code>+0.05</code> civ, <code>−0.04</code> lawlessness/grievance, <code>+0.25</code> approval) — the welfare spend a flush grow/order mayor makes, and the emergent relief path for the small-port tail (#2).</li>
<li><b>Festival</b> — a splendor mayor's signature (a grow/order mayor, or any fragile-loyalty ruler, may also feast): break out the port's <b>Luxury</b> (and some ale) for a public celebration — <code>300g</code> + <code>30</code> luxuries → <code>+0.3</code> approval, <code>+civ</code>, and a transient <code>isl.festival</code> window (<code>4</code> days). It is Luxury's <em>active</em> demand <b>sink</b> (comfort consumption ×<code>3</code> through the feast keeps its price high) <b>and</b> a trade event: the port becomes a hungry buyer, the festival travels <b>by sea as a rumour</b> (<code>believedFestival</code> — no omniscience), ships that have heard of it divert to sell luxuries (a <code>FESTIVAL_ROUTE_BONUS</code> in partner-scoring), and supplying the feast earns the trader extra <b>reputation</b>, so festivals build blocs between splendor ports and their luxury suppliers. It also pulls the one-consumer <b>PreciousMetal → Luxury</b> chain through. See Fig. Vc <span class="ref">policy.js (tryFestival) · population.js · intel.js · queries.js · trade.js</span>.</li>
<li><b>Trade</b> — set a <b>tariff</b> (<code>0..0.3</code> duty the host adds to a foreign trader's ask via <code>tariffMult</code>, composed alongside reputation, fleet-mates exempt); add strategic <b>export holds</b> (withhold Food/Weapons in distress); and steer the good the port <b>contracts</b> for toward its ambition's key input.</li>
</ul>
<div class="chart">
<pre class="mermaid" id="fig5c">
%%{init:{'theme':'base','themeVariables':{'fontSize':'14px','primaryColor':'#15303a','primaryTextColor':'#e7eff1','primaryBorderColor':'#3c6f7c','lineColor':'#5f8a97','clusterBkg':'#0e2029','clusterBorder':'#295059','edgeLabelBackground':'#0e2029'}}}%%
flowchart LR
DEC["Splendor mayor + luxuries in hand<br/>tryFestival"]:::actor --> FEST["Declare FESTIVAL<br/>spend gold + break out Luxury/Ale"]
FEST --> MOOD["+approval +civ −lawless<br/>(a Luxury/Ale SINK)"]:::good
FEST --> DRAW["×3 luxury draw through the feast<br/>→ stock low → price HIGH"]
DRAW --> RUM["Rumour by sea: docking ships note it<br/>believedFestival (no omniscience)"]
RUM --> PULL["Ships that HEARD divert to sell<br/>+FESTIVAL_ROUTE_BONUS"]:::good
PULL --> SUP["Supplying the feast → +reputation<br/>(bumpRep beyond the plain sale)"]
SUP --> BLOC["Blocs form: splendor ports ↔ luxury suppliers"]:::good
PULL -. "imports feed the ×3 draw" .-> DRAW
classDef actor fill:#123c3f,stroke:#4fcdd4,color:#d6faf9,stroke-width:1.4px;
classDef good fill:#173226,stroke:#63b98a,color:#cdeed9;
</pre>
</div>
<p class="chart-cap">Fig. Vc — the festival loop: a Luxury <em>sink</em> that becomes a trade-and-reputation event. The price spike and the by-sea rumour pull merchants in; the imports they bring feed the ongoing draw, and supplying the feast builds lasting blocs.</p>
<h3>Taxation, approval & corruption</h3>
<p>Corruption is now a concrete <em>flow</em>, not just a static integrity penalty. A low-integrity magistrate <b>skims</b> <code>max(0, 0.5 − integrity)·SKIM_RATE·income</code> each substep into a hidden <code>magistrate.hoard</code> (bounded ≤30% of gross, so the income floor still protects civ) — starving the treasury, so gold → civ → loyalty all sag under the leak. Every policy spend is <b>grafted</b> <code>×(1 + (0.5 − integrity)·GRAFT_RATE)</code>, the excess pocketed, so a corrupt regime gets less per gold <span class="ref">upkeep.js:56 · policy.js:36-50</span>. Meanwhile <code>island._approval</code> — a signed, daily-decaying memory of recent policy — biases the loyalty attractor by <code>_approval·0.09</code> (kept small so an unpopular streak alone can't manufacture a rebellion); tax hikes and exposed graft push it down, tax cuts and public works push it up.</p>
<div class="note ok"><span class="tag">Depth</span><p><b>Who you overthrow now matters.</b> The hoard is <em>seized by the people</em> on an <b>overthrow</b> (<code>island.gold += hoard·HOARD_RECOVERY(0.7)</code>, a distinct <code>graft</code> handover cause + the grafter's self-justifying last line + a <code>graftseized</code> beat) — so toppling a corrupt hoarder returns wealth, while toppling an honest-but-failed ruler is pure loss. A <b>quell</b> lets the tyrant keep skimming (the doom loop deepens). Past <code>HOARD_EXPOSE(4000g)</code> the hoard surfaces as a public <b>scandal</b> that craters approval — the one at-a-glance tell, since the hidden hoard never leaks to the client until exposed <span class="ref">magistrate.js:294-298,341-366</span>.</p></div>
</section>
<!-- ============ 6. VOYAGE PLANNER ============ -->
<section id="planner">
<h2><span class="roman">VI.</span>The Merchant Captain — planning a voyage</h2>
<div class="decides"><b>Decides:</b> the single richest decision in the sim — an idle hull is handed a whole multi-stop basket, chosen by a strict priority ladder over the port's needs and the captain's personality.</div>
<p>When a ship falls idle at home, <code>dispatch</code> hands it to <code>planVoyage</code> (with a re-plan cooldown so idle hulls don't re-scan every substep) <span class="ref">trade.js:44-78</span>. The planner spends three budgets — hold space, <code>0.75 × treasury</code>, and a per-good cap — down a fixed ladder. Every price it reasons about is a <em class="term">belief</em> (stale rumour, §XV), not truth; only availability (who has stock/gold) is live. The captain's traits tune the bars: <b>greed</b> raises the minimum-profit floor, <b>boldness</b> widens range and shrinks provisioning, <b>wanderlust</b> drives scouting.</p>
<div class="chart">
<pre class="mermaid" id="fig5">
%%{init:{'theme':'base','themeVariables':{'fontSize':'14px','primaryColor':'#15303a','primaryTextColor':'#e7eff1','primaryBorderColor':'#3c6f7c','lineColor':'#5f8a97','clusterBkg':'#0e2029','clusterBorder':'#295059','edgeLabelBackground':'#0e2029'}}}%%
flowchart TB
IDLE["Ship idle at home,<br/>off re-plan cooldown"]:::actor --> S1
S1{"home food lt 3 days ?"} -->|yes| B1["① SURVIVAL FOOD<br/>reserve up to 70% of hold,<br/>even if can't yet afford"]
S1 -->|no| S2
S2{"food-secure (≥4d)<br/>and starving ally ?"} -->|yes| B2["② AID CONVOY<br/>gift 120 food to neediest ally"]
S2 -->|no| S3
B1 --> S3
S3{"home wantsShip ?"} -->|yes| B3["③ claim a shipyard stop"]:::trap
S3 -->|no| S4
B3 --> S4
S4["④ EXPORTS — up to 3 goods with<br/>stock ≥ 1.3× target, best margin first"] --> S5
S5["⑤ MIGRATION — push (famine/crowd)<br/>+ pull (best believed civ+food port)"] --> S6
S6["⑥ BACKHAUL — buy biggest deficit<br/>at a stop already selling it"] --> S7
S7{"budget gt 1000 ?"} -->|yes| B7["⑦ PROSPERITY — import luxuries it lacks"]
S7 -->|no| S8
B7 --> S8
S8{"any committed stop ?"} -->|no| SCOUT["⑧ SCOUT least-known ports"]
S8 -->|yes| S9
SCOUT --> S9
S9{"pure trade & profit lt min-bar ?"} -->|yes| REDIR["reject → send scouting instead"]:::good
S9 -->|no| ARM["⑨ arm up if route peril ≥ 0.25<br/>and armoury short"]
ARM --> ORDER["Order stops: sell-heavy first,<br/>buy-heavy last (earn before you spend)"] --> DEPART["Depart · note the voyage in the ledger"]:::actor
classDef actor fill:#123c3f,stroke:#4fcdd4,color:#d6faf9,stroke-width:1.4px;
classDef trap fill:#352c14,stroke:#e0a94a,color:#ffe9c2;
classDef good fill:#173226,stroke:#63b98a,color:#cdeed9;
</pre>
</div>
<p class="chart-cap">Fig. VI — the priority ladder. Survival first, opportunism last; a fruitless plan becomes a scouting run rather than idleness.</p>
<div class="note warn"><span class="tag">Note</span><p>Partner choice minimises <code>believed-ask + travel + peril</code> for imports and maximises <code>margin − travel − peril + contract-pull</code> for exports, where <code>peril = believedDanger × 7</code> <span class="ref">queries.js:57-112</span>. Believed-haven and embargoed partners are excluded. On <b>day 0</b> nothing is known, so every belief returns base price and routing collapses to a pure "nearest viable port" gravity model until ships start docking.</p></div>
</section>
<!-- ============ 7. SHIP STATE + MOTION ============ -->
<section id="statemachine">
<h2><span class="roman">VII.</span>The Ship — states & motion</h2>
<div class="decides"><b>Decides:</b> moment to moment, where the hull points and how it gets there without hitting land or other ships.</div>
<p>A merchant cycles <code>idle → outbound → trading → inbound → idle</code>, but four orthogonal flags pre-empt that switch: a crew <b>uprising</b> (dead in the water), being blown <b>adrift</b>, <b>fleeing</b> a raider, and <b>sheltering</b> in port. The <code>ship</code> system moves merchants only; pirates and privateers are moved by their own systems — which is why there are <b>three byte-identical copies</b> of the mover, kept in sync by hand to dodge an import cycle.</p>
<div class="chart">
<pre class="mermaid" id="fig6">
%%{init:{'theme':'base','themeVariables':{'fontSize':'14px','primaryColor':'#15303a','primaryTextColor':'#e7eff1','primaryBorderColor':'#3c6f7c','lineColor':'#5f8a97','clusterBkg':'#0e2029','clusterBorder':'#295059','edgeLabelBackground':'#0e2029'}}}%%
flowchart LR
IDLE(["idle"]):::actor -->|voyage assigned| OUT(["outbound"])
OUT -->|"sail() = arrived"| TRD(["trading — dwell 3s"])
TRD -->|more stops| OUT
TRD -->|last stop done| INB(["inbound"])
INB -->|arrived home| IDLE
OUT -. "raider near" .-> FLEE{{"flee / shelter"}}:::trap
INB -. "raider near" .-> FLEE
FLEE -. clear .-> OUT
IDLE -. "blockade + cautious" .-> SHEL{{"shelter in port"}}
OUT -. storm .-> ADR{{"adrift — jury-rig, drift"}}
ADR -. "regain bearings" .-> OUT
IDLE -. "morale collapse" .-> UPR{{"uprising — dead in water"}}:::issue
classDef actor fill:#123c3f,stroke:#4fcdd4,color:#d6faf9,stroke-width:1.4px;
classDef issue fill:#3b1a22,stroke:#e0645a,color:#ffdad4,stroke-width:1.3px;
classDef trap fill:#352c14,stroke:#e0a94a,color:#ffe9c2;
</pre>
</div>
<p class="chart-cap">Fig. VII-a — the merchant state machine and its pre-emptive flags.</p>
<div class="note ok"><span class="tag">New</span><p><b>A trader now flees a pack it would face down one-on-one.</b> A bold, armed captain still <em>runs the blockade</em> of a single weak raider she can stand up to — but the "can I stand?" test is now group-aware (<code>balanceOfForce</code>): she weighs the <b>summed</b> guns of every raider in her evade bubble, so three raiders off the bow send her fleeing where one would not. Consistent with the aggressors-only scope, merchants don't convoy — her ally side is herself alone, and <code>armed</code> stays a hard precondition (numbers never lend a cargo hull teeth it lacks) <span class="ref">ship.js:fleeTarget · balanceOfForce</span>.</p></div>
<p>Per substep, each mover runs the same chain: sight nearby islands (updating the intel book), steer around land, apply wind, roll for foundering, then move — after which a separate <code>separation</code> pass nudges hulls apart.</p>
<div class="chart">
<pre class="mermaid" id="fig7">
%%{init:{'theme':'base','themeVariables':{'fontSize':'14px','primaryColor':'#15303a','primaryTextColor':'#e7eff1','primaryBorderColor':'#3c6f7c','lineColor':'#5f8a97','clusterBkg':'#0e2029','clusterBorder':'#295059','edgeLabelBackground':'#0e2029'}}}%%
flowchart TB
SAIL["sail(): target = next waypoint"]:::actor --> SIGHT["sightAtSea — record every island<br/>within 700u into intel book"]
SIGHT --> STEER{"straight ray cuts an island's land?"}
STEER -->|no| AIM["aim = raw target"]
STEER -->|yes| CONE["Tangent-cone: aim along the bearing<br/>that grazes land at radius R×1.06,<br/>on the side the target lies"]:::good
AIM --> WIND["speed = base × rigMult × windMult<br/>(headwind softened by tacking skill,<br/>floored at 0.4 — never stalls)"]
CONE --> WIND
WIND --> SINK{"maybeSink: rng lt 0.0006 × dist × leak<br/>(never a port's LAST ship)"}
SINK -->|founder| LOST["Ship lost, bounty posted"]:::trap
SINK -->|survive| MOVE["Advance x/y; face heading<br/>(even on the arrival snap)"]
MOVE --> SEPPASS["separation pass (after ALL movers)"]
SEPPASS --> SEP1["Soft de-crowd within 44u"]
SEP1 --> SEP2["COLREGS: give way to starboard;<br/>chase pairs skip the courtesy"]
SEP2 --> SEP3["Hard anti-stack floor at 34u<br/>(≪ 80u combat standoff)"]:::good
SEP3 -. "no island awareness → can shove<br/>a hull onto land near crowded ports" .-> BAD["land penetration"]:::issue
classDef actor fill:#123c3f,stroke:#4fcdd4,color:#d6faf9,stroke-width:1.4px;
classDef issue fill:#3b1a22,stroke:#e0645a,color:#ffdad4,stroke-width:1.3px;
classDef trap fill:#352c14,stroke:#e0a94a,color:#ffe9c2;
classDef good fill:#173226,stroke:#63b98a,color:#cdeed9;
</pre>
</div>
<p class="chart-cap">Fig. VII-b — one substep of motion. The tangent-cone fix and the 34u anti-stack floor are genuinely structural; separation's land-blindness is not.</p>
<div class="note warn"><span class="tag">Fragile · med</span><p><b>Three movers, hand-synced.</b> <code>moveToward</code> and its <code>sail</code> wrapper are copied verbatim into <code>ship.js</code>, <code>piracy.js</code> and <code>antipiracy.js</code> to avoid an import cycle <span class="ref">ship.js:88 · piracy.js:45 · antipiracy.js:35</span>. Any future movement fix applied to one copy silently desyncs merchant vs pirate vs privateer motion.</p></div>
<div class="note crit"><span class="tag">Bug · med</span><p><b>Two ways a hull passes through land.</b> ① A privateer standing down or running to resupply calls the bare mover, skipping island avoidance — it sails straight through any island in the way <span class="ref">antipiracy.js:146,162</span>. ② The <code>separation</code> pass has zero island awareness and runs <em>after</em> avoidance; near a crowded port its accumulated shoves can push a hull into land that avoidance then won't rescue (an island at/behind the ship isn't treated as a blocker) <span class="ref">separation.js:97-111</span>.</p></div>
</section>
<!-- ============ 8. CREW ============ -->
<section id="crew">
<h2><span class="roman">VIII.</span>The Crew — morale & mutiny</h2>
<div class="decides"><b>Decides:</b> whether a crew keeps working, deserts to another port, or raises the black flag. Gated at every step by the captain's Command.</div>
<p>Crews eat food and drink ale (a master's crew eats less and cheers harder). At sea, morale eases toward a steady ~0.6, dragged by sea-fatigue and — decisively — by an empty larder. A <b>fed crew mathematically never mutinies</b>: steady-state morale sits well above the <code>0.3</code> mutiny line. Trouble only begins when food runs out <span class="ref">crew.js:130-144</span>. <b>Two exits end at the black flag:</b> a defeated mutiny may install a <em>new</em> pirate master (stat-driven — likelier the hungrier the crew and the more lawless its home port <span class="ref">crew.js:178-186</span>), while — quite apart from any crisis — a bold, greedy captain of a still-loyal crew may hoist it <em>himself</em> and lead them over (<code>maybeTurnRogue</code>, weighed each substep for candidates above the mutiny line; see Fig. IXa) <span class="ref">piracy.js:117-136</span>.</p>
<div class="chart">
<pre class="mermaid" id="fig8">
%%{init:{'theme':'base','themeVariables':{'fontSize':'14px','primaryColor':'#15303a','primaryTextColor':'#e7eff1','primaryBorderColor':'#3c6f7c','lineColor':'#5f8a97','clusterBkg':'#0e2029','clusterBorder':'#295059','edgeLabelBackground':'#0e2029'}}}%%
flowchart TB
PROV["Provision at each dock<br/>(home = free from town stores)"]:::actor --> SEA["At sea: eat food, drink ale"]
SEA --> Q1{"food aboard ?"}
Q1 -->|yes| MO["Morale eases to ~0.6<br/>(steady, above mutiny line)"]:::good
Q1 -->|"empty"| WORRY{"worried captain and<br/>a larder reachable ?"}
WORRY -->|yes| DEV["Divert to nearest food port<br/>(primary survival valve)"]:::good
WORRY -->|"nowhere to go"| DROP["morale −1.5/day, hunger climbs"]
DROP --> Q2{"unrest ≥ grace<br/>(1.4–2.3 d, scales with Command)<br/>BEFORE hunger 2.2 ?"}
Q2 -->|"novice: mutiny fires"| UPR["UPRISING — dead in water 12s"]
Q2 -->|"skilled: grace too long"| STARVE["Ship LOST with all hands<br/>(mutiny never fired)"]:::issue
UPR --> RES{"quell odds = 0.4 + Command·0.42"}
RES -->|quell| HOLD["Captain holds — morale reset 0.7,<br/>redirect to resupply"]:::good
RES -->|"lose → roll"| OUT{"outcome"}
OUT -->|"stat: 0.3 + hunger + home lawlessness"| PIRATE["Turn PIRATE — crew take a NEW master<br/>(seized · if under 8% cap)"]:::trap
OUT -->|"0.50"| DEFECT["Desert — re-home to another port"]
OUT -->|else| NEWCAP["New novice captain; morale reset,<br/>redirect to resupply (crisis deferred)"]
MO -. "bold+greedy captain, per-substep" .-> SELFROGUE{{"self-turn: raise the black flag himself<br/>(ROGUE — keeps command, see Fig. IXa)"}}:::trap
classDef actor fill:#123c3f,stroke:#4fcdd4,color:#d6faf9,stroke-width:1.4px;
classDef issue fill:#3b1a22,stroke:#e0645a,color:#ffdad4,stroke-width:1.3px;
classDef trap fill:#352c14,stroke:#e0a94a,color:#ffe9c2;
classDef good fill:#173226,stroke:#63b98a,color:#cdeed9;
</pre>
</div>
<p class="chart-cap">Fig. VIII — the crew crisis tree. A mutiny doesn't kill the crew; it resets morale and defers the food crisis.</p>
<div class="note crit"><span class="tag">Inversion · med</span><p><b>A skilled captain's crew starves before it can mutiny.</b> Starvation is fixed at <code>2.2</code> days, but the mutiny grace window scales <em>up</em> with Command to ~<code>2.29</code> days for a near-master — so the survival reset (which would redirect the ship to food) fires reliably only for <em>low-Command</em> captains. The better the leader, the more likely the crew simply dies <span class="ref">crew.js:156 · economy.json:444</span>. The <code>deviationTarget</code> valve mitigates this — but only when a larder is actually reachable.</p></div>
<div class="note warn"><span class="tag">Trap</span><p><b>The provisioning poverty trap.</b> Home victualling is free, which is load-bearing: a ship whose purse was just emptied into the treasury would otherwise depart broke and spiral. But crews can strip a <em>foreign</em> port's Food down to a <code>10</code>-unit reserve, competing directly with the town's populace — a reserve that doesn't scale with how many crews the port must feed <span class="ref">crew.js:82</span>.</p></div>
</section>
<!-- ============ 9. PIRATE ============ -->
<section id="pirate">
<h2><span class="roman">IX.</span>The Pirate — the hunt</h2>
<div class="decides"><b>Decides:</b> a pirate's every move via a strict priority ladder, coloured by a fearsome captain's traits and sharpened by hunger.</div>
<p>Any hull can raise the black flag — a <b>seeded</b> raider, a <b>haven's forge</b>, a <b>prize</b> capture, a <b>mutiny</b> that turns rogue, a fallen haven's <b>home fleet</b>, or an honest <b>captain's own turn</b> to piracy (<code>maybeTurnRogue</code>, weighed each substep for a genuinely bold + greedy master) — always gated by <code>canTurnPirate</code> against a single unified <code>pirateBudget</code> (base <code>8%</code> of the fleet, lifted by active havens — see §XII #5) <span class="ref">piracy.js:99-115</span>. How she came to it decides <em>who commands</em>: the crew either keep their captain as their pirate leader (a <b>rogue</b> turn — he's hardened bold+greedy but keeps his name, portrait, and log-voice, an unbroken fall from honest trade) or throw him over for a fresh, fearsome master (a <b>seizure</b>). A <em>seeded/built</em> raider and a <em>mutiny</em> are always a new master; an <em>organic</em> turn rolls lead-vs-cast-out from the captain's boldness + greed + crew morale — so the self-turn, born of that same boldness+greed, comes up <em>rogue</em> almost always <span class="ref">piracy.js:104-171</span>. A pirate then runs the ladder below every substep. Hunger is the master dial: a hungry pirate never rests, uses full hunting range even mid-blockade, and drops its greedy captain's minimum-prize bar to zero. And a raider is no longer a lone wolf: every stand/flee weighs the <b>local balance of force</b> (<code>balanceOfForce</code> — its consorts summed against the hunter's pack), so raiders <b>gang up</b> on a fat trader and a lone weak raider flees a hunter it would have turned on with two consorts near; a prize <em>right alongside</em> now pre-empts a resting/greedy captain's other goals (the "blind fly-by" fix); and nearby raiders <b>focus-fire</b> the hull a consort already marks <span class="ref">piracy.js:106-146 · balanceOfForce</span>.</p>
<div class="chart">
<pre class="mermaid" id="fig9a">
%%{init:{'theme':'base','themeVariables':{'fontSize':'14px','primaryColor':'#15303a','primaryTextColor':'#e7eff1','primaryBorderColor':'#3c6f7c','lineColor':'#5f8a97','clusterBkg':'#0e2029','clusterBorder':'#295059','edgeLabelBackground':'#0e2029'}}}%%
flowchart TB
T["A hull turns pirate<br/>(gated by the 8% fleet cap)"]:::actor --> Q{"how did she come to it ?"}
Q -->|"seeded raider · haven-built · mutiny cast-out"| SEIZED
Q -->|"organic: a captain's own turn (maybeTurnRogue)<br/>or a fallen haven's home crew"| ROLL{"pLead roll<br/>boldness + greed + crew morale"}
ROLL -->|"he leads"| ROGUE["ROGUE — cause 'rogue'<br/>the SAME captain, hardened bold+greedy+blooded<br/>keeps name · portrait · voiceSeed → the log stays his hand"]:::good
ROLL -->|"crew throw him over"| SEIZED["SEIZED — cause 'pirate'<br/>a new fearsome master (makePirateCaptain)<br/>→ the log changes hands"]:::trap
classDef actor fill:#123c3f,stroke:#4fcdd4,color:#d6faf9,stroke-width:1.4px;
classDef trap fill:#352c14,stroke:#e0a94a,color:#ffe9c2;
classDef good fill:#173226,stroke:#63b98a,color:#cdeed9;
</pre>
</div>
<p class="chart-cap">Fig. IXa — raising the black flag: who commands, and why the same captain sometimes keeps the log (the rogue/seized split that drives the first-person Story voice).</p>
<div class="chart">
<pre class="mermaid" id="fig9">
%%{init:{'theme':'base','themeVariables':{'fontSize':'14px','primaryColor':'#15303a','primaryTextColor':'#e7eff1','primaryBorderColor':'#3c6f7c','lineColor':'#5f8a97','clusterBkg':'#0e2029','clusterBorder':'#295059','edgeLabelBackground':'#0e2029'}}}%%
flowchart TB
P["Pirate, every substep"]:::actor --> L1{"① pirate-hunter within 560u·reach<br/>(and NOT off its own den) ?"}
L1 -->|"LOCAL FORCE can match the odds<br/>(consorts vs the hunter's pack, by character)"| STAND["STAND and fight the hunter<br/>(close to 150u → skirmish;<br/>attrition, no plunder)"]
L1 -->|"outmatched · timid · too mauled"| FLEE["FLEE to its nearest haven<br/>(mend + shelter)"]
L1 -->|"none near"| L2
L2{"② near a haven under siege ?"} -->|"matched → charge / else shadow"| DEF["Defend the haven"]
L2 -->|no| L3
L3{"③ crippled & fed & NOT near a den<br/>& repair timber aboard ?"} -->|yes| HEAVE["HEAVE TO — dead in the water,<br/>jury-rig from the hold (committed 24s,<br/>catchable). Survive far from havens"]:::good
L3 -->|no| L4
L4{"④ crippled? hull lt 0.4∓boldness<br/>AND a haven exists ?"} -->|yes| MEND["Break off the hunt → run to the<br/>haven for a FULL refit<br/>(even with a prize in sight)"]:::good
L4 -->|no| L5
L5{"⑤ prey in range?<br/>(1600u, or 900 while blockading;<br/>a prize alongside pre-empts rest/greed)"} -->|yes| HUNT["Close to 150u → fight; reloading → hold at 80u.<br/>Timber-poor → prefer Wood/Fiber; FOCUS-FIRE a<br/>consort's marked prize (≤ striker cap)"]
L5 -->|no| L6
L6{"⑥ any haven exists AND<br/>hungry / laden / battered ?"} -->|yes| SUP["Sail to nearest haven<br/>(NO distance test on selection)"]:::issue
L6 -->|no| L7
L7{"⑦ hungry / timber-starved AND bold<br/>AND raid off cooldown ?"} -->|yes| RAID["Raid a port (approach to 144u,<br/>carry off food + TIMBER + gold)"]
L7 -->|no| L8
L8{"⑧ wanderer / hungry / timber-poor ?"} -->|yes| ROVE["Chase sea traffic"]
L8 -->|no| BLOCK["Blockade: orbit a port at 760u,<br/>+0.09 danger/day for 3 days"]
classDef actor fill:#123c3f,stroke:#4fcdd4,color:#d6faf9,stroke-width:1.4px;
classDef issue fill:#3b1a22,stroke:#e0645a,color:#ffdad4,stroke-width:1.3px;
classDef good fill:#173226,stroke:#63b98a,color:#cdeed9;
</pre>
</div>
<p class="chart-cap">Fig. IX — first match wins. Rungs ①/③/④ are the self-preservation guards: a raider weighs its own hide (a hunter bearing down, or a hull shot to pieces) <em>before</em> it presses a chase — and now, far from the sparse havens, it can HEAVE TO and jury-rig from its own hold (③) rather than only limping to a den. Rung ⑥ still pre-empts ⑦, the source of the starve-en-route trap.</p>
<div class="note ok"><span class="tag">Fixed</span><p><b>A hunted or crippled raider no longer blindly presses its hunt.</b> Two new self-preservation rungs run <em>before</em> prey acquisition. Rung ① — a <b>pirate-hunter bearing down</b>: the raider decides fight-or-flight, standing to fight only if its strength clears an odds bar set by character (the bold engage near parity, the timid only a plainly weaker foe; skill sharpens the read), otherwise breaking off and running for a haven. Rung ③ — a <b>hull mauled past what the captain will bear</b> (<code>PIRATE_FLEE_HULL</code> ∓ boldness): it quits the hunt and limps to a haven to mend, even with a fat prize alongside <span class="ref">piracy.js:206-241 · 273-283</span>.</p></div>
<div class="note ok"><span class="tag">New</span><p><b>Heave-to repair + the timber predator.</b> Rung ③ lets a crippled raider with Wood/Fiber aboard lie-to DEAD IN THE WATER and jury-rig at sea (2.5× the passive rate), <em>committed</em> for <code>HEAVE_COMMIT_SECONDS</code> — a real "repair vs. run" beat, catchable while it sits (a founder check runs; a hunter closing pulls it off via rung ①). A field patch is capped by structural <b>soundness</b> (see §X) and the per-track <code>JURYRIG_REACH</code>, so it gets seaworthy, never whole — only a den/home yard does that. A raider <em>out</em> of timber becomes a <b>timber predator</b>: it prefers Wood/Fiber-laden prey (⑤), plunders timber first, and its raids carry off spars & canvas (⑦) <span class="ref">repair.js:maybeHeaveToRepair · piracy.js needsTimber</span>.</p></div>
<div class="note crit"><span class="tag">Bug · med</span><p><b>A starving pirate still prefers a distant haven over an adjacent raid.</b> Rung ⑥ picks the nearest haven with <em>no distance test</em> and pre-empts the raid rung ⑦, so whenever <em>any</em> haven exists on the map, a hungry pirate sails toward it — and can starve mid-ocean while a raidable port sits alongside. The port-raid fallback is effectively unreachable in any world that has a haven <span class="ref">piracy.js:316-328</span>.</p></div>
<div class="note warn"><span class="tag">Character · low</span><p><b>Pirates still skew bold, but character is no longer a dead binary.</b> Every pirate captain is forged with <code>boldness ∈ [0.7,1.0]</code>, so few are "timid" in absolute terms. But boldness now varies raider behaviour <em>continuously</em>: it sets the odds bar a captain will accept when a hunter closes (rung ①), the hull at which he breaks off to mend (rungs ①/③, via <code>PIRATE_FLEE_HULL_BOLD</code>), and — with seamanship — the running <code>assessFlee</code> nerve mid-fight. So the old dead <code>timid</code>-flee branch is gone; boldness genuinely matters again <span class="ref">captains.js:69 · piracy.js:200-238</span>.</p></div>
<div class="note ok"><span class="tag">New</span><p><b>Group-aware combat — raiders hunt as a pack, not blind lone wolves.</b> Every fight/flee decision now weighs the <b>local balance of force</b> (<code>balanceOfForce</code>: allied vs hostile <code>combatStrength</code> summed within <code>GROUP_RALLY_RANGE ≈ 760u</code>) rather than a lone 1-v-1 against the single nearest foe. Three raiders TURN on a hunter one alone would flee; a raider caught by two hunters runs even at even guns. The captain's own nerve bar (boldness+skill) is <em>unchanged</em> — it just weighs the whole local force now, so a cautious captain still wants a clear edge and a bold one commits near parity (<code>GROUP_ODDS_PER_ALLY</code> eases the bar a touch per surplus consort). Three coupled behaviours fall out: (1) <b>the blindness fix</b> — a valid prize within <code>PACK_PREEMPT_RANGE</code> pre-empts a resting/greedy raider's lower-priority goals, so it stops sailing indifferently past a trader in pot-shot range; (2) <b>wolfpack focus-fire</b> — a raider prefers the hull a nearby consort already marks (id-keyed <code>_prey</code> tally, deterministic, no squadron object), capped at <code>GROUP_STRIKER_CAP</code> raiders per victim so a pack spreads rather than wiping one lane; (3) the <b>privateer-as-prey</b> bug is closed — a warship is fought through rung ① / <code>assessFlee</code>, never scored as plunder. All of it is pure & replay-deterministic (id-derived tie-breaks, per-substep derived grids, no new RNG), and the constants default so a <em>lone</em> attacker reproduces the old behaviour — the pack only bites when consorts are actually near <span class="ref">piracy.js:balanceOfForce · nearestPrey · antipiracy.js · economy.json GROUP_*/PACK_*</span>.</p></div>
</section>
<!-- ============ 10. COMBAT ============ -->
<section id="combat">
<h2><span class="roman">X.</span>Combat — how a fight resolves</h2>
<div class="decides"><b>Decides:</b> who wins a broadside exchange, and whether the loser sinks, strikes her colours, or is boarded and taken as a prize.</div>
<p>Combat is <em class="term">attrition</em>, not instant. Each round (every 1.2 s while in range) both ships exchange fire; damage scales with the strength ratio and is divided by hull armour. Doctrine differs by flag: pirates chain-shot the <b>rig</b> to cripple-and-board, the navy pounds the <b>hull</b> to sink, merchants shoot rig to <b>flee</b> <span class="ref">piracy.js:364-382</span>. Fighting strength blends a base, gunnery skill (the dominant term, ×1.2), morale, gun count, boldness, a flag bonus, ship class and hull condition <span class="ref">piracy.js:78-96</span>. Between exchanges each captain re-weighs the fight (<code>assessFlee</code>): break off only when hull <em>and</em> odds have <em>both</em> turned against her — later the bolder and more seasoned the captain, never if fearless, and never if dismasted (no sail to run under). The odds are now weighed across the <b>local balance of force</b> when the caller supplies its grids (its consorts vs the other side's pack), so a hunter winning 3-v-1 doesn't sheer off and one losing 1-v-3 breaks sooner — falling back to the old exact self-vs-foe test when called 1-v-1. It is the same running assessment a hunted pirate and a losing privateer both make, every round <span class="ref">piracy.js:assessFlee · balanceOfForce</span>.</p>
<p>Every blow also erodes <b>structural soundness</b> — a per-track ceiling (<code>hullSound</code>/<code>rigSound</code>) that sits above the working hull/rig (<code>hull ≤ hullSound ≤ 1</code>). A fraction of each hull/rig point lost is <em>permanent</em> until a real dry-dock (hull wears faster than rig — framing is less recoverable than canvas), floored at <code>SOUNDNESS_FLOOR</code> so a jury-rigged hull always stays seaworthy enough to fight or flee. At sea a jury-rig, and even a foreign yard, can only nurse a track back up to its soundness (and no higher than <code>JURYRIG_REACH</code>); only a ship's HOME port — or a pirate's own HAVEN — rebuilds soundness toward whole. So a ship that has been through several hard fights becomes progressively less repairable away from a dry-dock, and havens/home ports become the thing every battered hull must eventually seek <span class="ref">repair.js:damageHull/juryRig/repairAtPort</span>.</p>
<p><b>Self-repair & aid — one mechanism (a boat is a boat).</b> Every hull mends the same way: at a
dry-dock (a port, or a pirate's haven), or by <b>HEAVING TO</b> at sea. Merchant, pirate, and privateer
alike, a badly-hurt ship with repair timber aboard and <em>no dry-dock in reach</em> lies-to DEAD IN THE
WATER and jury-rigs from its own hold — the same careen state, badge, and jury-rig ceiling for all of them
<span class="ref">repair.js:maybeHeaveToRepair</span>. A merchant normally just docks to mend, so it only
heaves-to when genuinely stranded and hurt; a raider does it often (havens are sparse). There is no
separate "repair while sailing" path. <b>Aid at sea</b> (the mercy valve — a passing ally answering a ship
in distress, relationship-gated by carried alliances) is a <b>supply drop</b>, not an on-the-spot patch:
the helper hands SPARE canvas, timber & victuals into the stricken ship's own hold, and she mends
<em>herself</em> with them (heaving-to if stranded) — so a rescue recovers her gradually, at her own hands
and within her own soundness, never a teleport back to full <span class="ref">repair.js:renderAid · ship.js:updateShip</span>.</p>
<div class="chart">
<pre class="mermaid" id="fig10">
%%{init:{'theme':'base','themeVariables':{'fontSize':'14px','primaryColor':'#15303a','primaryTextColor':'#e7eff1','primaryBorderColor':'#3c6f7c','lineColor':'#5f8a97','clusterBkg':'#0e2029','clusterBorder':'#295059','edgeLabelBackground':'#0e2029'}}}%%
flowchart TB
ENG["In range + reloaded → exchangeFire"]:::actor --> DMG["Both take damage<br/>∝ strength share ÷ armour;<br/>burn guns, drift morale"]
DMG --> C1{"attacker hull ≤ 0 ?"}
C1 -->|yes| ATTDIE["Attacker founders<br/>→ defender wins, bounty paid"]:::good
C1 -->|no| C2{"victim hull ≤ 0 ?"}
C2 -->|yes| SINK["Victim founders<br/>CARGO + GOLD LOST"]:::issue
C2 -->|no| C3{"victim strikes colours?<br/>(hull ≤ 0.3 or morale ≤ 0.2;<br/>pirates never strike)"}
C3 -->|yes| BOARD["Plunder all gold + goods"]
C3 -->|no| C4{"assessFlee: hull AND odds<br/>both turned against her?<br/>(nerve = boldness+skill; odds weigh<br/>the LOCAL FORCE; fearless never · dismasted can't)"}
C4 -->|yes| BREAK["Break off / sheer away<br/>(→ run for a haven to mend)"]
C4 -->|no| ENG
BOARD --> TAKE{"take as prize?<br/>hull ≥ 0.15, p = f(gunnery,boldness),<br/>under the unified pirateBudget"}
TAKE -->|yes| PRIZE["Crew the hull → new pirate<br/>(re-homed to the captor — see §XV)"]:::good
TAKE -->|no| SCUT{"scuttle 6% ?"}
SCUT -->|yes| SINK
SCUT -->|no| FREE["Freed to limp home"]
classDef actor fill:#123c3f,stroke:#4fcdd4,color:#d6faf9,stroke-width:1.4px;
classDef issue fill:#3b1a22,stroke:#e0645a,color:#ffdad4,stroke-width:1.3px;
classDef good fill:#173226,stroke:#63b98a,color:#cdeed9;
</pre>
</div>
<p class="chart-cap">Fig. X — a fight is a loop of exchanges until someone sinks, strikes, or runs.</p>
<div class="note ok"><span class="tag">By design</span><p>Merchant-vs-pirate is intentionally lopsided: traders are capped at half their gun capacity and shoot to flee, while pirates carry a combat bonus and more guns. A trader's only wins are escape or a rare fend-off. The old "welded-together" stacking bug is genuinely fixed — coincident hulls get a deterministic golden-angle standoff, and <code>34u anti-stack ≪ 80u standoff ≪ 150u fire range</code> are internally consistent.</p></div>
<div class="note warn"><span class="tag">Tension · low</span><p><b>Combat destroys gold, which contradicts the "conserved invariant" label.</b> A hull sunk with cargo destroys its gold; a fenced-but-undelivered hoard lost to a storm destroys that gold; a bounty on a pirate killed by weather is never paid out. These are <em>intended</em> sinks, but <code>worldTotals</code> is labelled "the conserved invariant probe for tests" — any conservation assertion run over a combat/storm scenario would fail <span class="ref">piracy.js:474 · world.js:171</span>.</p></div>
</section>
<!-- ============ 11. NAVY ============ -->
<section id="navy">
<h2><span class="roman">XI.</span>The Navy — danger, bounty & privateers</h2>
<div class="decides"><b>Decides:</b> the economy's self-balancing answer to piracy — how feared waters get, who funds a hunter, and how that hunter behaves.</div>
<p>An attack marks nearby waters with <code>danger</code> (which decays daily and reroutes merchants), and the aggrieved port posts a <b>bounty</b> from its treasury. Separately, ports commission <b>privateers</b> when a threat is in range and they can afford it — the fleet size scales as <code>2.5 × pirates + 2 × havens</code> <span class="ref">antipiracy.js:71-99</span>. A commissioned privateer arms up, then hunts, patrols, or besieges a haven, breaking off to resupply and REFIT for free at its guard port when hungry or battered — and, like a pirate, if it is badly mauled with the guard port too far and repair timber aboard, it will <b>heave to</b> and jury-rig where it lies (both bounded by structural soundness — a foreign-water patch never makes a hull whole; only its own port does). Note both the haven refit and the guard-port refit now mend <em>gradually</em> (a small step each tick, paced by the day, capped by structural soundness; never mid-broadside) — fixing a bug where a hull loitering in range healed to full every substep, including in the middle of a fight, and a follow-on where the throttled version jerked up in visible chunks on a ship that never stopped. Like the raider it hunts, it also re-weighs each round whether to keep fighting (<code>assessFlee</code>): a hunter shot up by a heavily-gunned pirate <b>disengages</b> — clearing its prey and keeping clear for a <code>COMBAT_BREAKOFF_RESPITE</code> before it will re-engage — and limps home to refit rather than trading to the bottom <span class="ref">antipiracy.js:134 · 315-321</span>. Hunters are now <b>group-aware</b> too (<code>balanceOfForce</code>): a cautious privateer that alone would shadow a heavily-gunned raider <b>commits</b> once a consort is in the rally bubble (and won't throw itself alone at a pirate pack), and the navy <b>focus-fires</b> — a hunter prefers a raider a consort already engages, up to <code>GROUP_STRIKER_CAP</code> per hull so it concentrates without leaving other pirates unchecked <span class="ref">antipiracy.js:130-146</span>.</p>
<div class="chart">
<pre class="mermaid" id="fig11">
%%{init:{'theme':'base','themeVariables':{'fontSize':'14px','primaryColor':'#15303a','primaryTextColor':'#e7eff1','primaryBorderColor':'#3c6f7c','lineColor':'#5f8a97','clusterBkg':'#0e2029','clusterBorder':'#295059','edgeLabelBackground':'#0e2029'}}}%%
flowchart TB
RAID["Pirate raids / plunders"]:::actor --> DANGER["Mark waters: danger += 0.34–0.5<br/>decays 0.12/day"]
RAID --> BOUNTY["Port posts bounty from treasury<br/>(a gold sink if never claimed)"]
COMM{"Commission a privateer?<br/>threat in 2400u · activePriv lt budget<br/>· gold ≥ 680 · Weapons ≥ 6 · off cooldown"}:::actor
COMM -->|yes| ARM["Convert an idle trader → privateer,<br/>arm to 26 guns, victual 13 days"]
ARM --> HUNT{"Privateer AI ladder"}
HUNT --> H1["Fight pirate prey at gun-range<br/>(gang up: cautious COMMITS with a consort;<br/>focus-fire a claimed raider; break off — assessFlee)"]
HUNT --> H2["Chase a haven's defender screen"]
HUNT --> H3["Assault haven (−0.11 strength/day, throttled)"]:::trap
HUNT --> H4["Patrol orbit / sail to threat"]
HUNT --> H5{"food lt 1.6d or hull lt 0.6 ?"}
H5 -->|yes| RES["Resupply free at guard port"]:::good
HUNT --> H6{"surplus navy & empty seas ?"}
H6 -->|yes| STAND["Stand down → revert to trader"]:::good
KILL["Kill a pirate → claim bounty,<br/>maybe recover the hull as a lawful prize"] --> FLOOR
FLOOR["Pirates wiped below the √N floor →<br/>rogues respawn → re-commission → …"]:::trap
classDef actor fill:#123c3f,stroke:#4fcdd4,color:#d6faf9,stroke-width:1.4px;
classDef trap fill:#352c14,stroke:#e0a94a,color:#ffe9c2;
classDef good fill:#173226,stroke:#63b98a,color:#cdeed9;
</pre>
</div>
<p class="chart-cap">Fig. XI — the anti-piracy response. Self-limiting, but it oscillates rather than settling.</p>
<div class="note warn"><span class="tag">Balance · med</span><p><b>A lone privateer can't reliably break a haven.</b> Assault is throttled to one <code>−0.11</code> hit per striker per day, while the haven's shore guns + return fire drive a single besieger below its refit threshold in ~1.8 days and it entrenches <code>+0.01/day</code>. Net progress per cycle is only marginally positive — it works only because ~2 privateers are commissioned per haven; a solo siege is on the edge of not converging <span class="ref">havens.js:195 · shore.js</span>.</p></div>
<div class="note warn"><span class="tag">Oscillation · med</span><p>Privateers out-number (2.5:1) and out-run pirates, so they wipe them below the floor; the rogue-floor then respawns a batch, which re-triggers commissioning, which wipes them again. The system churns <em>spawn → commission → kill → stand-down</em> rather than settling into a stable pirate population — the floor papers over "piracy went invisible" rather than balancing it.</p></div>
</section>
<!-- ============ 12. HAVEN ============ -->
<section id="haven">
<h2><span class="roman">XII.</span>The Haven & the Pirate Lord</h2>
<div class="decides"><b>Decides:</b> when a failed island stops being governed and becomes a pirate base — who then <em>rules</em> that base, and how it's eventually retaken.</div>
<p>A port that stays lawless (≥0.58) and uncivilised (civ ≤0.34) for five days <b>falls</b>: it drops its magistrate, seeds three of its own idle crews as pirates, and starts a strength meter at 0.85. A named <b>Pirate Lord</b> — the dark mirror of the magistrate — seizes the den (§XII.b). Its workshops split: <b>civilian works rot</b> (their condition target drops to 0) while <b>war works</b> (Weapons + Ships) are kept and repaired from fenced plunder — so <em>which</em> island falls now matters, a fallen shipyard mass-produces raiders. Thereafter the den entrenches daily, forges fresh pirates when it can afford the wood and iron, fences plundered goods from visiting pirates, and refits them for free. It's redeemed only when a privateer's repeated assaults grind its strength to zero — and a redeemed port gets a <b>reconstruction grant</b> (condition restored to 0.45 + a 6-day upkeep holiday) so it isn't a revolving-door wreck <span class="ref">havens.js · pirateLord.js</span>.</p>
<div class="chart">
<pre class="mermaid" id="fig12">
%%{init:{'theme':'base','themeVariables':{'fontSize':'14px','primaryColor':'#15303a','primaryTextColor':'#e7eff1','primaryBorderColor':'#3c6f7c','lineColor':'#5f8a97','clusterBkg':'#0e2029','clusterBorder':'#295059','edgeLabelBackground':'#0e2029'}}}%%
flowchart LR
FAIL["Lawless ≥ 0.58 + civ ≤ 0.34,<br/>5 days, and a haven slot free"]:::actor --> FALL["FALL: drop magistrate, seat a Pirate Lord,<br/>seed 3 pirates, strength = 0.85<br/>civilian works ROT · war works KEPT"]
FALL --> DRIVE["Lord's war-economy loop: entrench +0.01/day;<br/>skim to hoard · convert a derelict slot → gun-foundry"]
DRIVE --> BUILD{"build a raider?<br/>based lt 5 · gold/wood/iron ok<br/>· under the UNIFIED pirateBudget<br/>(faster with a live Ships works)"}:::good
BUILD -->|yes| DRIVE
DRIVE --> HARBOUR["Harbour pirates within 900u:<br/>fence gold+goods, feed, refit free"]
HARBOUR --> DRIVE
DRIVE --> SIEGE{"privateer assault:<br/>strength −0.11/day (throttled)<br/>vs entrench +0.01/day"}
SIEGE -->|"strength gt 0"| DRIVE
SIEGE -->|"strength ≤ 0"| REDEEM["REDEEM: lawlessness→0.5,<br/>fresh magistrate, grievance capped 0.25"]:::good
classDef actor fill:#123c3f,stroke:#4fcdd4,color:#d6faf9,stroke-width:1.4px;
classDef trap fill:#352c14,stroke:#e0a94a,color:#ffe9c2;
classDef good fill:#173226,stroke:#63b98a,color:#cdeed9;
</pre>
</div>
<p class="chart-cap">Fig. XII — fall → the Lord's war-economy → redeem. Builds now draw from one unified pirate budget, so havens and sea-rogues share it.</p>
<div class="note ok"><span class="tag">Fixed · #5</span><p><b>One pirate budget, not two conflicting caps.</b> A single <code>pirateBudget(world)</code> now governs every source: <code>ships·PIRATE_MAX_FRAC(0.08)·min(1 + havens·HAVEN_PIRATE_LIFT(0.5), 2.5)</code>. Haven builds, sea-risen rogues, mutineers and captured prizes all draw from the <em>same</em> lifted ceiling — the old separate <code>20%</code> haven cap vs <code>8%</code> global cap is gone, so busy havens can no longer monopolise the budget and silently starve out the mutiny → pirate and <code>maybeTurnRogue</code> turns the Story-tab drama leans on <span class="ref">piracy.js (pirateBudget) · havens.js · economy.json:486-488</span>.</p></div>
<h3 id="piratelord">§XII.b — The Pirate Lord (dark mirror of the magistrate)</h3>
<p>A fallen den has no lawful levers (tax, tariff, public works are inert — the policy loop skips havens), so it gets a <b>Pirate Lord</b> that runs a symmetric <em>war</em>-economy loop in <code>driveHaven</code>. Seated in <code>fall()</code>, cleared on <code>redeem()</code> (its hoard scattered as a <b>sink</b>, never banked to the treasury). It mirrors the magistrate's shape — a named ruler with a portrait, an opaque <code>voiceSeed</code> (so the haven chapter narrates in its own pirate hand in the Story tab), an xp/skill curve, three traits and a war agenda <span class="ref">pirateLord.js · havens.js:117-141,262</span>.</p>
<ul class="tight">
<li><b>Traits</b> (the pirate echo of firmness/integrity/generosity): <b>cruelty</b> (brutal raiding — more plunder, faster-rising heat), <b>cunning</b> (smart entrenchment — the den survives sieges longer), <b>avarice</b> (skims fenced plunder into a private hoard via <code>PIRATELORD_SKIM_RATE(0.25)</code> instead of reinvesting — the corruption echo).</li>
<li><b>Agenda</b> (the war analog of ambition): <code>plunder</code> (raid hard), <code>armada</code> (build the biggest raider fleet — a fallen shipyard leans here), <code>fortress</code> (dig in, bank <code>havenStrength</code>), <code>hoard</code> (amass fenced wealth). One is rolled to suit the den.</li>
<li><b>The loop</b>: skim fenced gold to the hoard (by avarice); <b>convert</b> a derelict civilian slot into a war workshop for <code>PIRATELORD_CONVERT_GOLD(300)</code> — the cooperage becomes a gun-foundry; keep/repair Weapons + Ships works from fenced plunder while the rest rots; and build raiders faster when a live Ships works feeds the slipway (build cooldown <code>× PIRATELORD_SHIPYARD_BUILD_MULT(0.5)</code>).</li>
</ul>
<div class="note ok"><span class="tag">By design</span><p>Fall and redeem are now real <b>handovers</b> with their own causes (<code>piratefall</code> captures the outgoing magistrate; <code>redeem</code> reinstates a fresh one) — the haven period reads as a distinct dark chapter in the Lord's voice, and redemption's reconstruction grant keeps a retaken port from immediately re-failing.</p></div>
</section>
<!-- ============ 13. DIPLOMACY ============ -->
<section id="diplomacy">
<h2><span class="roman">XIII.</span>The Diplomatic layer</h2>
<div class="decides"><b>Decides:</b> how ports feel about each other — warming through trade, cooling through rivalry, hardening into blocs, embargoes and betrayals that skew prices and bar deals.</div>
<p>Every ordered pair starts just off neutral. Completed trades warm both sides and, via <b>association</b>, nudge third parties who are in fresh contact toward the trader's partner — emergent blocs, gated by the intel network <span class="ref">reputation.js:35-65</span>. Opinions decay daily; same-primary rivals cool; trusted partners can rarely <b>betray</b> for an instant embargo. Reputation has real teeth: it skews a host's quotes ±15% and, below −0.62, bars the deal entirely <span class="ref">reputation.js:72-89</span>.</p>
<div class="chart">
<pre class="mermaid" id="fig13">
%%{init:{'theme':'base','themeVariables':{'fontSize':'14px','primaryColor':'#15303a','primaryTextColor':'#e7eff1','primaryBorderColor':'#3c6f7c','lineColor':'#5f8a97','clusterBkg':'#0e2029','clusterBorder':'#295059','edgeLabelBackground':'#0e2029'}}}%%
flowchart TB
INIT["Seed each pair in ±0.15"]:::actor --> TRADE["Trade completes → both +0.02×volume<br/>+ association pulls bystanders into blocs"]
TRADE --> DAILY["Daily: decay 3%;<br/>same-primary rivals cool 0.02/day"]
DAILY --> BETRAY{"trusted pair (≥0.55)<br/>rolls betrayal 0.12%/day ?"}
BETRAY -->|yes| EMB["Both crash by 1.7 → instant embargo<br/>(the SOLE route past −0.62)"]
BETRAY -->|no| BLOC{"cross a bloc threshold?"}
EMB --> BLOC
BLOC -->|"≥ 0.32"| ALLY["Ally: aid convoys, price break"]:::good
BLOC -->|"≤ −0.32"| RIVAL["Rival"]
BLOC -->|"≤ −0.62"| EMBARGO["Embargo: ALL trade barred"]
RIVAL --> ATTR["Same-primary pairs with no active trade<br/>now settle at −0.50 — ABOVE the −0.62 line —<br/>so they stay rivals, thawable, never<br/>a permanent embargo"]:::good
classDef actor fill:#123c3f,stroke:#4fcdd4,color:#d6faf9,stroke-width:1.4px;
classDef issue fill:#3b1a22,stroke:#e0645a,color:#ffdad4,stroke-width:1.3px;
classDef good fill:#173226,stroke:#63b98a,color:#cdeed9;
</pre>
</div>
<p class="chart-cap">Fig. XIII — opinions warm slowly and cool to a rivalry floor, but no longer lock cold: the former −0.667 attractor now rests at −0.50, above the embargo line.</p>
<div class="note ok"><span class="tag">Fixed · #3</span><p><b>The permanent-embargo attractor is gone.</b> Lowering <code>REP_COMPETITION_PER_DAY</code> from <code>0.02</code> to <code>0.015</code> moves the same-primary fixed point of <code>v = v×0.97 − 0.015</code> from <code>−0.667</code> up to <code>−0.50</code> — now <em>above</em> the <code>−0.62</code> embargo threshold, so rivalrous pairs settle into a thawable rivalry instead of freezing into mutual embargo. To keep betrayal meaningful as <em>the sole route to a true embargo</em>, <code>REP_BETRAY_DROP</code> was raised <code>1.4 → 1.7</code> so a betrayal still crosses the line (from ~0.9 down to ~−0.8) <span class="ref">reputation.js · economy.json:521,542</span>.</p></div>
</section>
<!-- ============ 14. WEATHER ============ -->
<section id="weather">
<h2><span class="roman">XIV.</span>The Weather</h2>
<div class="decides"><b>Decides:</b> seasonal production swings, where storms roam and who they sink, and the single global wind every sail reads.</div>
<p>Seasons rotate every 15 days, swinging food-crop output hardest (winter ×0.78 → autumn ×1.28) and setting the storm rate and prevailing wind. Storms are born at random, drift with the wind, bounce off the map edges, and batter any ship inside them — damaging hull and rig and rolling to sink or blow a merchant adrift. The wind is one global vector easing toward periodically-rerolled targets, floored so a headwind slows but never <em class="term">stalls</em> a fleet <span class="ref">weather.js · wind.js</span>.</p>
<div class="chart">
<pre class="mermaid" id="fig14">
%%{init:{'theme':'base','themeVariables':{'fontSize':'14px','primaryColor':'#15303a','primaryTextColor':'#e7eff1','primaryBorderColor':'#3c6f7c','lineColor':'#5f8a97','clusterBkg':'#0e2029','clusterBorder':'#295059','edgeLabelBackground':'#0e2029'}}}%%
flowchart TB
SEASON["Season (every 15 days)<br/>sets _prodMult, storm rate, prevailing wind"]:::actor --> PROD["Food crops swing ×0.78 … ×1.28"]
SEASON --> WINDV["Global wind eases toward rerolled targets<br/>(floored 0.4 — never stalls)"]:::good
SEASON --> SPAWN{"spawn a storm?<br/>0.175–0.665/day, cap 5"}
SPAWN -->|yes| STORM["Storm drifts with wind,<br/>bounces off edges, lives ~2.5 days"]
STORM --> BATTER["Ship inside: hull/rig damage<br/>(softened by seamanship)"]
BATTER --> SANK{"hull ≤ 0 or sink-roll ?"}
SANK -->|yes| SUNK["Founders — bypasses the<br/>'never a port's last ship' guard"]:::issue
BATTER --> ADR{"merchant blown adrift? 30%×intensity"}
ADR -->|yes| DRIFT["Adrift: drifts, jury-rigs,<br/>elevated founder odds"]:::trap
classDef actor fill:#123c3f,stroke:#4fcdd4,color:#d6faf9,stroke-width:1.4px;
classDef issue fill:#3b1a22,stroke:#e0645a,color:#ffdad4,stroke-width:1.3px;
classDef trap fill:#352c14,stroke:#e0a94a,color:#ffe9c2;
classDef good fill:#173226,stroke:#63b98a,color:#cdeed9;
</pre>
</div>
<p class="chart-cap">Fig. XIV — weather genuinely sinks ships (two paths), and the wind floor guarantees the fleet never freezes.</p>
<div class="note crit"><span class="tag">Softlock · med</span><p><b>A storm can take a port's only ship.</b> The distance-based founder check explicitly guards a port's last hull ("with no ship it couldn't even sail to buy a replacement"), but the storm knockdown sinks with <em>no</em> such check <span class="ref">weather.js:118 vs events.js:84</span>. A poor, shipless port then can't afford the ~3400g development path <em>and</em> can't sail to buy one — permanently cut off from food imports, decaying to the population floor.</p></div>
</section>
<!-- ============ 15. INFORMATION ============ -->
<section id="info">
<h2><span class="roman">XV.</span>The Information layer</h2>
<div class="decides"><b>Decides:</b> what each port and ship <em>knows</em> — because prices don't teleport; they ride the ships. This is a genuine, load-bearing fog of price, not decoration.</div>
<p>A "belief" is a price book tagged with the day it was seen. Islands hold one; ships carry their own. When a ship docks it <b>observes</b> the port's live prices firsthand, and <b>reports</b> its own firsthand sightings of everywhere else — but never re-spreads the port's hearsay. That firsthand-only rule is what keeps knowledge geographic instead of saturating the map <span class="ref">beliefs.js:82-105</span>. Beliefs age toward base over ~20 days; the same machinery carries five non-price facts (danger, haven, food, civ, lawlessness), refreshed by sighting islands at sea every substep <span class="ref">intel.js</span>.</p>
<p>Crucially, the planner reasons on <b>beliefs</b> (partner choice, purse sizing, migration, scouting) while settlement at the dock uses <b>live</b> prices — so a port can be wrong about a distant price but is never wrong about whether that port <em>has</em> stock or gold (availability is deliberate common knowledge). A parallel <b>voyage ledger</b> lets a home port expect each ship until it's overdue, then presume it lost.</p>
<div class="chart">
<pre class="mermaid" id="fig15">
%%{init:{'theme':'base','themeVariables':{'fontSize':'14px','primaryColor':'#15303a','primaryTextColor':'#e7eff1','primaryBorderColor':'#3c6f7c','lineColor':'#5f8a97','clusterBkg':'#0e2029','clusterBorder':'#295059','edgeLabelBackground':'#0e2029'}}}%%
flowchart TB
DOCK["Ship docks at a port"]:::actor --> OBS["OBSERVE port's live prices/facts<br/>firsthand → into ship's book"]
OBS --> REP["REPORT ship's OTHER firsthand<br/>sightings → into port's book<br/>(never re-spreads hearsay)"]:::good
REP --> AGE["Beliefs age toward base over ~20d;<br/>facts over ~12d; forget after 30–40d"]
SEA["Sailing: sight every island in 700u<br/>→ refresh facts (danger/haven/food)"]:::actor --> AGE
AGE --> PLAN["Planner reasons on BELIEFS<br/>(routes, purse, scout triggers)"]
PLAN --> SETTLE["…but settlement uses LIVE price<br/>at the port you're docked at"]:::good
DEP["Depart"]:::actor --> LEDGER["Ledger: expect ship until dueDay"]
LEDGER --> OVER{"overdue?"}
OVER -->|"returned"| CLEAR["Clear entry"]
OVER -->|"gone + past due"| LOST["Presume lost — fire 'lost' event"]
OVER -->|"captured merchant"| GHOST["RE-HOMED to the captor on capture —<br/>origin's census drops it, ledger hopes<br/>until overdue → THEN presumed lost"]:::good
classDef actor fill:#123c3f,stroke:#4fcdd4,color:#d6faf9,stroke-width:1.4px;
classDef issue fill:#3b1a22,stroke:#e0645a,color:#ffdad4,stroke-width:1.3px;
classDef good fill:#173226,stroke:#63b98a,color:#cdeed9;
</pre>
</div>
<p class="chart-cap">Fig. XV — knowledge travels by hull. The belief layer drives decisions; only dock settlement is omniscient.</p>
<div class="note ok"><span class="tag">Fixed · #4</span><p><b>A captured merchant no longer inflates its home's fleet forever.</b> <code>tryTakePrize</code> now <b>re-homes</b> the prize to the captor (<code>victim.homeId = pirate.homeId</code>) the moment it's taken, so the origin's fleet census stops counting the turned-pirate hull as a still-owned trader — and the port resumes ordering replacements. The <em>belief</em> ledger still hopes for the ship until it's overdue (information-by-sea is preserved — the home doesn't magically know it was taken), then fires the normal <code>lost</code> presumption. A port bled by capture now recovers like one bled by sinking <span class="ref">piracy.js (tryTakePrize) · voyages.js · fleet.js</span>.</p></div>
<div class="note warn"><span class="tag">Dead agency · low</span><p>The <code>setVoyage</code> intent path is fully wired and validated — but <b>no client message reaches it</b>. In "pass 1" a viewer can only change the clock; the ownership/idle checks in <code>intents.js</code> are exercised only by a unit test. Real player agency over ships needs a new protocol message. <span class="ref">simHost.js:146-154</span></p></div>
</section>
<!-- ============ 16. REGISTER ============ -->
<section id="register">
<h2><span class="roman">XVI.</span>Issue register</h2>
<div class="decides"><b>Decides:</b> where to look first. 30 findings, severity-ranked; <b style="color:var(--ok)">9 now resolved</b> by the policy-loop / mutable-industry feature (struck green). "High" = distorts core behaviour or deadlocks an actor; "Med" = a real but bounded failure; "Low" = cosmetic, dead, or a perf/tuning smell.</div>
<div class="twrap">
<table>
<thead><tr><th>#</th><th>Sev</th><th>Actor</th><th>Finding</th><th>Where</th></tr></thead>
<tbody>
<tr class="done"><td>1</td><td><span class="sev done">fixed</span></td><td>Port</td><td><span class="find">Ships never cost base price — <code>stock cap 3</code> vs <code>target 250</code> pins the multiplier at 3× (ask ≈3780, not 1200)</span><span class="fix"><code>TARGET_OVERRIDES.Ships = 3</code> matches the stock cap → ratio 1 → ask ≈ base 1200. §IV</span></td><td><span class="ref">economy.json:49 · pricing.js:14</span></td></tr>
<tr class="done"><td>2</td><td><span class="sev done">fixed</span></td><td>Port</td><td><span class="find">Small ports can never replace a hull: buy needs pop ≥ ~120 vs the <code>40×pop</code> hoard cap — the "starving tail" engine</span><span class="fix">#1 drops the threshold to pop ≈ 57; the magistrate's tax + public-works levers give the pop-40–57 tail a growth path. §IV</span></td><td><span class="ref">upkeep.js:25 · policy.js:245-276</span></td></tr>
<tr class="done"><td>3</td><td><span class="sev done">fixed</span></td><td>Diplomacy</td><td><span class="find">Same-primary ports lock in permanent mutual embargo at −0.667 (below the −0.62 bar)</span><span class="fix"><code>REP_COMPETITION_PER_DAY</code> 0.02→0.015 lifts the attractor to −0.50 (above the bar, thawable); <code>REP_BETRAY_DROP</code> 1.4→1.7 keeps betrayal the sole embargo route. §XIII</span></td><td><span class="ref">reputation.js · economy.json:521,542</span></td></tr>
<tr class="done"><td>4</td><td><span class="sev done">fixed</span></td><td>Info</td><td><span class="find">Captured merchant keeps homeId → counted as owned forever → home stops ordering replacements</span><span class="fix"><code>tryTakePrize</code> re-homes the prize to the captor on capture, so the origin's census drops it (belief ledger still presumes-lost when overdue). §XV</span></td><td><span class="ref">piracy.js (tryTakePrize) · voyages.js</span></td></tr>
<tr class="done"><td>5</td><td><span class="sev done">fixed</span></td><td>Haven</td><td><span class="find">Haven build cap (20%) fights the global 8% pirate cap → havens starve out mutineers/rogues</span><span class="fix">One unified <code>pirateBudget(world)</code> governs every source (haven / rogue / mutiny / prize) — no separate 20% vs 8% caps. §XII</span></td><td><span class="ref">piracy.js (pirateBudget) · economy.json:486</span></td></tr>
<tr><td>6</td><td><span class="sev med">med</span></td><td>Pirate</td><td>Timid/bold/greed pirate branches are dead — trait floors make every pirate "always aggressive"</td><td><span class="ref">captains.js:69 · piracy.js:142</span></td></tr>
<tr><td>7</td><td><span class="sev med">med</span></td><td>Crew</td><td>Skilled captain's crew starves (2.2d) before its mutiny grace (~2.29d) — survival reset unreachable</td><td><span class="ref">crew.js:156</span></td></tr>
<tr><td>8</td><td><span class="sev med">med</span></td><td>Pirate</td><td>Hungry pirate prefers a distant haven over an adjacent raid — no reachability guard, can starve en route</td><td><span class="ref">piracy.js:231</span></td></tr>
<tr><td>9</td><td><span class="sev med">med</span></td><td>Weather</td><td>Storm knockdown bypasses the "never a port's last ship" guard → shipless-port softlock</td><td><span class="ref">weather.js:118</span></td></tr>
<tr><td>10</td><td><span class="sev med">med</span></td><td>Ship</td><td>Privateer stand-down/resupply moves skip island avoidance → sail through land</td><td><span class="ref">antipiracy.js:146,162</span></td></tr>
<tr><td>11</td><td><span class="sev med">med</span></td><td>Ship</td><td>Separation has no island awareness, runs after avoidance → can shove hulls onto land near crowded ports</td><td><span class="ref">separation.js:97</span></td></tr>
<tr><td>12</td><td><span class="sev med">med</span></td><td>Navy</td><td>Lone-privateer haven siege is borderline non-converging (0.11/day assault vs shore-fire refit cycle + entrench)</td><td><span class="ref">havens.js:195</span></td></tr>
<tr class="done"><td>13</td><td><span class="sev done">fixed</span></td><td>Magistrate</td><td><span class="find"><code>industry</code> ambition pinned at −1 for a pure shipyard — unwinnable agenda bleeds loyalty</span><span class="fix">Redefined <em>by</em> workshops: <code>ambitionSignal('industry')</code> now scores workshop count × condition vs <code>slotCap</code> — raising healthy works is winning. §V</span></td><td><span class="ref">magistrate.js:157</span></td></tr>
<tr class="done"><td>14</td><td><span class="sev done">fixed</span></td><td>Magistrate</td><td><span class="find">Haven-cap rebellion loop: a failed island at cap churns overthrow every 5 days with no exit</span><span class="fix"><code>HAVEN_CAP_RELIEF</code> (0.03/day) eases the lawlessness of an at-cap failing port that can't fall — an exit from the churn. §V</span></td><td><span class="ref">havens.js · economy.json:489</span></td></tr>
<tr><td>15</td><td><span class="sev med">med</span></td><td>Navy</td><td>Piracy oscillates (spawn→commission→kill→standdown) rather than settling to a stable population</td><td><span class="ref">antipiracy.js · havens.js:62</span></td></tr>
<tr><td>16</td><td><span class="sev low">low</span></td><td>Port</td><td>Food spoiled twice per substep (population + upkeep) → ~2.3× decay of any other good</td><td><span class="ref">population.js:32 · upkeep.js:45</span></td></tr>
<tr><td>17</td><td><span class="sev low">low</span></td><td>Ship</td><td>Three <code>moveToward</code>/<code>sail</code> copies kept in sync by hand — structural desync risk</td><td><span class="ref">ship.js:88 · piracy.js:45</span></td></tr>
<tr><td>18</td><td><span class="sev low">low</span></td><td>Combat</td><td>Gold destroyed in combat/storms contradicts the <code>worldTotals</code> "conserved invariant" label</td><td><span class="ref">piracy.js:474 · world.js:171</span></td></tr>
<tr><td>19</td><td><span class="sev low">low</span></td><td>Ship</td><td>Fleeing ship won't re-pick a bolt-hole that later gets blockaded → strands under the guns (comment/code mismatch)</td><td><span class="ref">ship.js:380</span></td></tr>
<tr><td>20</td><td><span class="sev low">low</span></td><td>Info</td><td><code>setVoyage</code> intent path fully wired but unreachable from any client message (dead player agency)</td><td><span class="ref">simHost.js:146</span></td></tr>
<tr><td>21</td><td><span class="sev low">low</span></td><td>Captains</td><td><code>personality</code> string computed & serialized but never read by any sim decision (display-only)</td><td><span class="ref">captains.js:56 · snapshot.js:117</span></td></tr>
<tr><td>22</td><td><span class="sev low">low</span></td><td>Captains</td><td>Merchant <code>sea</code> and <code>cmd</code> XP locked together (voyages award both equally) — no divergence</td><td><span class="ref">captains.js:209</span></td></tr>
<tr><td>23</td><td><span class="sev low">low</span></td><td>Captains</td><td><code>awardCommandXp</code> never called on quelling a mutiny despite its docstring — missed feedback loop</td><td><span class="ref">captains.js:220 · crew.js:176</span></td></tr>
<tr><td>24</td><td><span class="sev low">low</span></td><td>Crew</td><td>Foreign crews strip a port's Food to a flat 10-unit reserve that doesn't scale with fleet size</td><td><span class="ref">crew.js:82</span></td></tr>
<tr><td>25</td><td><span class="sev low">low</span></td><td>—</td><td>Dead constants: <code>PORT_STANDOFF</code>, <code>PIRATE_SINK_ON_FEND</code>, <code>PRIVATEER_LOSS_SINK</code>; <code>DANGER_FIGHT</code> branch unreachable</td><td><span class="ref">economy.json · bounty.js:26</span></td></tr>
<tr class="done"><td>26</td><td><span class="sev done">fixed</span></td><td>Port</td><td><span class="find">Contract "fulfilled" usually means the 65-unit purse emptied, not stock replenished — misleading log</span><span class="fix">The end-of-contract beat now distinguishes "the reward purse is spent" (claimed) from "holds replenished" (stocked → withdrawn). §XIII</span></td><td><span class="ref">contracts.js:30-36</span></td></tr>
<tr><td>27</td><td><span class="sev low">low</span></td><td>Info</td><td><code>sightAtSea</code> runs every substep per moving ship — O(N·S) redundant work (once/day would suffice)</td><td><span class="ref">ship.js:138</span></td></tr>
<tr><td>28</td><td><span class="sev low">low</span></td><td>Info</td><td><code>estimateReturnDay</code> fixed at departure, ignores detours → live-but-delayed ships silently un-expected</td><td><span class="ref">voyages.js:20</span></td></tr>
<tr class="done"><td>29</td><td><span class="sev done">fixed</span></td><td>Magistrate</td><td><span class="find"><code>order</code> ambition self-defeating on the lawless ports it's assigned to; <code>wealth</code> unreachable for shrunken ports</span><span class="fix"><code>wealth</code> is now scored treasury-<em>relative</em> (gold/cap, not an absolute 2000g); <code>order</code> is functional — the tax/public-works/garrison levers buy down lawlessness. §V</span></td><td><span class="ref">magistrate.js:168</span></td></tr>
<tr><td>30</td><td><span class="sev low">low</span></td><td>Ship</td><td>Separation reads raw <code>ship.speed</code>, ignoring faction multipliers → give-way slightly wrong for pirates/privateers</td><td><span class="ref">separation.js:42</span></td></tr>
</tbody>
</table>
</div>
<div class="note ok"><span class="tag">Verified sound</span><p>Not everything that looked risky is broken. Confirmed healthy: no population death-spiral (starvation floored, growth food-gated); rebellions always start-and-end; plague always recovers and floors population; wind never stalls the fleet; RNG is fully deterministic and serializable (per-system streams, roster on a separate LCG); the belief layer is genuinely load-bearing, not cosmetic; and the old stacking / center-park / frozen-standoff bugs are structurally fixed, not papered over.</p></div>
</section>
<footer>
<p><b>BOATZ Simulation Field Manual</b> — mapped from <code>game/sim/*</code> (≈7,000 LOC across 42 systems, incl. the new <code>policy.js</code> + <code>pirateLord.js</code>) + <code>data/economy.json</code> tuning. All file:line anchors are clickable in your editor. Six subsystem sweeps + independent spot-checks of every "dead code" claim. Diagrams render on a fixed chart-table ground in both themes; document chrome follows your light/dark preference.</p>
</footer>
</main>
</div>
</div>
<script>
/* ---------- theme toggle ---------- */
(function(){
var btn=document.getElementById('themebtn');
var root=document.documentElement;
function cur(){var a=root.getAttribute('data-theme'); if(a) return a; return matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light';}
btn.addEventListener('click',function(){
var next=cur()==='dark'?'light':'dark';
root.setAttribute('data-theme',next);
btn.textContent=(next==='dark'?'☾':'☀')+' Theme';
});
})();
/* ---------- pan / zoom / maximize on each flowchart ---------- */
(function(){
var clamp=function(v,a,b){return v<a?a:(v>b?b:v);};
// Wire a chart once Mermaid has drawn its svg. We NEVER re-parent or transform the svg — panning is
// native scroll of the <pre>, zoom just scales the svg's display width. So this cannot corrupt
// Mermaid's async render or pile diagrams on top of each other (the earlier bug).
function ready(svg){ try{ return !!(svg && (svg.querySelector('g')||svg.childElementCount>1)); }catch(e){ return false; } }
function wire(chart){
if(chart.__pz) return;
var pre=chart.querySelector('pre.mermaid');
var svg=pre && pre.querySelector('svg');
if(!ready(svg)) return; // not finished drawing — scan()/observer will retry
chart.__pz=true;
svg.style.maxWidth='none'; svg.style.height='auto';
var z=1;
function apply(){ svg.style.width=(z*100)+'%'; }
apply();
function zoomTo(nz,cx,cy){
nz=clamp(nz,0.5,9);
var w0=svg.getBoundingClientRect().width||1, sl=pre.scrollLeft, st=pre.scrollTop;
z=nz; apply();
var w1=svg.getBoundingClientRect().width||1, r=w1/w0;
if(cx!=null){ pre.scrollLeft=(sl+cx)*r-cx; pre.scrollTop=(st+cy)*r-cy; }
}
function center(f){ var r=pre.getBoundingClientRect(); zoomTo(z*f, r.width/2, r.height/2); }
function reset(){ z=1; apply(); pre.scrollLeft=0; pre.scrollTop=0; }
pre.addEventListener('wheel',function(e){ e.preventDefault(); var r=pre.getBoundingClientRect(); zoomTo(z*(e.deltaY<0?1.15:1/1.15), e.clientX-r.left, e.clientY-r.top); },{passive:false});
var down=false,sx,sy,ol,ot;
pre.addEventListener('pointerdown',function(e){ down=true; sx=e.clientX; sy=e.clientY; ol=pre.scrollLeft; ot=pre.scrollTop; try{pre.setPointerCapture(e.pointerId);}catch(_){}; pre.classList.add('grabbing'); });
pre.addEventListener('pointermove',function(e){ if(!down) return; pre.scrollLeft=ol-(e.clientX-sx); pre.scrollTop=ot-(e.clientY-sy); });
function up(){ down=false; pre.classList.remove('grabbing'); }
pre.addEventListener('pointerup',up); pre.addEventListener('pointercancel',up);
pre.addEventListener('dblclick',reset);
var tools=document.createElement('div'); tools.className='pz-tools';
function mk(t,title,fn){ var b=document.createElement('button'); b.type='button'; b.textContent=t; b.title=title; b.setAttribute('aria-label',title); b.addEventListener('click',fn); tools.appendChild(b); return b; }