Buildings.lua
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
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
-- Voxel world mode: a building voxelized from its own sprite. -- -- A Game Boy overworld building is a fake-3D projection that packs several -- different 3D facings into one flat drawing: the roof is drawn as if seen -- from above, the facade as if seen face-on, and the sloped ends as -- diagonal silhouettes. Raising the whole footprint as one box (what the -- generic volume path does) folds all three into a wall, so a house comes -- out as a cube wearing its own elevation. -- -- This module does the other thing: it classifies each BAND of the drawing -- by the surface it depicts and applies the matching operation per band -- -- the pipeline written up in assets/docs/buidling_to_voxel/. Two rules govern it: -- -- 1. Every visible voxel colour is a real texel of the drawing. Nothing -- is invented but the geometry the sprite implies and never paints -- (undersides, the depth behind the facade), and those wear the -- drawing's own four shades. -- 2. The sprite is ground truth, not the tile grid. The silhouette, the -- taper rate, the eave height and every window are MEASURED off the -- pixels; the profile only says which rows are roof and which are -- facade. -- -- The pipeline, per template (see data/voxel_heights.lua `buildings`): -- -- read composite the building out of the atlas and flood its -- silhouette in from the border through light pixels only -- -- the black outline and the #555 shading together are the -- boundary, and a "not black" test eats the shaded flanks. -- measure the topmost drawn row of each column IS the roof's elevation -- profile (the drawn taper is the slope); the facade's panes -- are the non-black regions its black frames seal off. -- build facade rows extrude straight back over the footprint, the -- awning band juts past them, panes sink one voxel, and the -- roof lays the top-facing rows flat -- level over the -- plateau, stepping down the drawn taper at the ends -- then -- overwrites the walls it intersects. -- emit cull to the shell and merge runs of texel-adjacent faces into -- single quads, so a 90k-voxel house ships as ~2k quads. -- -- One model is built per template and stamped at every placement: Red's -- and Blue's houses are the same seven-placement drawing, so they cost one -- build between them. mods/DRAMATIC_SHAPE/tools/building_voxels.py is the -- reference implementation of the same algorithm and prints the voxel and -- shell counts this one must agree with. -- -- Purely presentational, like everything else in the mod: the tiles a -- building claims keep the collision, warps and triggers they always had. -- the mod namespace (see main.lua): V.data loads a shipped data file local V = ... local Budget = V.require("BuildBudget") local Buildings = {} -- The four GB shades, lightest first (same cutoffs as Structures.shadeClass, -- which reasons about the same art). local WHITE, GREY, DARK, BLACK = 0, 1, 2, 3 -- A pane is a window or a doorway: a non-black region the drawing seals -- off behind its own black frame. Anything wider or taller than this is a -- band of the facade itself -- a siding course, the awning's grey field -- -- and must stay flush. local RECESS_MAX = 24 -- Face shades, matching the rest of the mod's objects: the south face is -- the drawing itself and draws at full brightness. local SHADE = { top = 0.95, south = 1.0, north = 0.68, side = 0.78, bottom = 0.5 } -- ------- how far a merged run may reach: the tile lattice -- -- Merging is what keeps a 90k-voxel house down to ~2k quads, and under a -- straight projection a run may be as long as it likes -- a straight line -- is a straight line however finely it is cut. THE WORLD CURVE IS NOT -- STRAIGHT. It drops every vertex by the square of its distance from the -- focus (see WorldCurve), so a quad's interior is the CHORD of a parabola -- its neighbours draw the arc of: a run of length L hangs k*L^2/4 below -- the short quads butted against it, and the join tears open. -- -- Nothing bounded a run's length before, and the runs that ran away were -- the ones wearing a CONSTANT texel -- the roof's black eave outline, its -- fascia, the shaded underside -- because a flat run has no art to break -- it. Those reached 102px across a gym, which at V-CURVE 3 hangs some -- three world pixels under the roof surface beside it: the eave tore off -- the roof and the drop showed the building's dark interior through the -- slot. (Strip runs, the drawing marching along the atlas, break at the -- tileset's own boundaries and were never the problem.) -- -- So a run stops at the next 8px lattice line. Buildings are stamped at -- tx*8 (see stamp), so the model's lattice IS the map's: every quad in the -- scene -- terrain, props, this -- now ends on the same lines, every join -- is vertex-for-vertex, and the bend carries them together. What is left -- is the sag WITHIN one cell, k*64/4, which is under a twentieth of a -- world pixel at any rung. -- -- It costs quads on a dense city map (Cerulean's object stream goes from -- 35.7k to 41.6k, and its longest edge from 102px to 8px) and it costs them -- whether the curve is on or not, which is the deliberate trade: the mesh -- is cached per map and built asynchronously over seconds, so meshing for -- the curve's sake only when the curve is on would mean rebuilding every -- live map on a keypress. local CELL = 8 -- How far a run starting at `a` may go before it crosses the next lattice -- line. Floor-mod, so the awning's negative z lands on the same lines the -- positive side does. local function runCap(a) return CELL - a % CELL end local function keyOf(tx, ty) return (ty + 64) * 4096 + (tx + 64) end local function shadeOf(r, g, b, a) if a == 0 then return WHITE end local v = math.min(r, g, b) if v <= 0.25 then return BLACK end if v <= 0.55 then return DARK end if v <= 0.85 then return GREY end return WHITE end -- The shape profile ships with the mod; absent or broken simply means no -- building templates, and every building falls back to the volume path. local spec = nil local function profile() if spec == nil then local ok, s = pcall(V.data, "voxel_heights") spec = (ok and type(s) == "table") and s or false end return spec or nil end local models = {} -- "<tileset>:<index>" -> prebuilt local quads local frontSets = {} -- tileset id -> { [tile] = true } or false -- The tileset's side-door art: the 2x2 tile grid of one doorway cell -- (data/voxel_heights.lua `sideDoors`), or nil when the tileset names none. function Buildings.sideDoorCell(tilesetId) local s = profile() return s and s.sideDoors and s.sideDoors[tilesetId] or nil end -- The tileset's front-only tiles as a set (data/voxel_heights.lua -- `frontOnly`): the doorways, shop signs and painted lettering that belong -- on a facade and on no other face of the same building. nil when the -- tileset names none, which is every indoor one. function Buildings.frontOnly(tilesetId) local hit = frontSets[tilesetId] if hit == nil then local s = profile() local list = s and s.frontOnly and s.frontOnly[tilesetId] if list then hit = {} for _, id in ipairs(list) do hit[id] = true end else hit = false end frontSets[tilesetId] = hit end return hit or nil end -- ------------------------------------------------------------------ read -- -- Which sprite pixel a BACK-facing voxel shows, where that is not the one -- the front shows. The facade extrudes straight through the footprint, so -- the far wall is the drawing again -- and read from behind it is the -- drawing mirrored, doorway, shop sign, GYM lettering and all. Those tiles -- are named per tileset in data/voxel_heights.lua `frontOnly`; every cell -- wearing one takes the art of an ordinary cell beside it in the same tile -- row. -- -- The donor is chosen per RUN of front-only cells, not per cell, so a -- two-tile doorway comes out as two tiles of the SAME wall rather than -- borrowing left from one side and right from the other. Between the two -- neighbours the one whose tile the row uses more often wins, which is -- what reaches past a gable's sloped corner (a 4x2 house draws its door -- against the slope: the corner is unique to the row, the wall beside it -- is not) for the wall the back should actually wear. -- -- Returns a SPARSE map, sprite index -> sprite index, empty entries meaning -- "unchanged"; nil when the drawing has no front-only tile at all, which is -- most of them. local function backMap(tiles, bw, bh, W, inside, frontOnly) if not frontOnly then return nil end local donor, any = {}, false for r = 1, bh do local row = tiles[r] local freq = {} for c = 1, bw do local id = row[c] if not frontOnly[id] then freq[id] = (freq[id] or 0) + 1 end end local c = 1 while c <= bw do if frontOnly[row[c]] then local c1 = c while c1 < bw and frontOnly[row[c1 + 1]] do c1 = c1 + 1 end local l, rt = c - 1, c1 + 1 local pick = nil if l >= 1 and rt <= bw then pick = ((freq[row[rt]] or 0) > (freq[row[l]] or 0)) and rt or l elseif l >= 1 then pick = l elseif rt <= bw then pick = rt end -- a row that is front-only end to end has no donor; it keeps its -- own art rather than inventing one if pick then for k = c, c1 do donor[(r - 1) * bw + (k - 1)] = pick - 1 end any = true end c = c1 + 1 else c = c + 1 end end end if not any then return nil end local back = {} for cell, dc in pairs(donor) do local r, c = math.floor(cell / bw), cell % bw for oy = 0, 7 do local sy = r * 8 + oy for ox = 0, 7 do local i = sy * W + c * 8 + ox local j = sy * W + dc * 8 + ox -- The donor must be DRAWN, or the substitution would hand the wall -- a texel from outside the silhouette. One row up is tried first, -- because the drawing's last row is the black threshold the -- building stands on: the doorway paints it (a door sits on the -- ground) and the wall beside it does not, so at the base course -- the same row of the donor column is off the shape. The model -- lifts that column's foot by exactly one row for the same reason -- (see `at`), and lifting the donor with it is what makes the back -- wall's bottom course continuous. if not inside[j] then j = j - W end if j >= 0 and inside[j] then back[i] = j end end end end return back end -- Composite the template out of the atlas and flood the silhouette in from -- the border. Returns flat arrays indexed y * W + x. -- -- `topRows`, when a template carries it, is extra drawing rows composited -- ABOVE the matched grid: rows of the same drawing that are not on the -- map this template places on. The Pokemon Tower is the case that needs -- it -- the drawing straddles the LAVENDER_TOWN / ROUTE_10 boundary, its -- roof band and top window courses standing in the route's last rows, so -- no single map's grid holds the whole building. The matcher never sees -- topRows (placement is still by `tiles` alone); they exist so the MODEL -- is built from the complete drawing and the tower rises to its real -- height instead of folding as two half-buildings. -- Composite one doorway cell PAST the end of the sprite, at indices -- W*H .. W*H+255, and hand back its base. The side-door pass paints with -- sprite indices like everything else -- `emit` resolves a voxel's colour -- through sp.ax/sp.ay and knows nothing about where the index came from -- -- so the door only has to BE in the sprite arrays to travel the rest of -- the pipeline untouched. Appending rather than drawing into the grid is -- the point: the drawing itself must not change, or the silhouette flood, -- the taper and every measured band would be read off art the tileset -- never placed here. -- -- Nothing else walks past W*H (measure's shadeTexel scan and the pane -- flood both stop there), so the block is invisible to measurement and -- visible only to the code that asks for it by index. -- -- WHICH OF THE 256 TEXELS ARE THE DOOR. A doorway cell is not a doorway -- edge to edge: the tileset draws it as a cell OF A FACADE, so its outer -- ring is the wall beside and above the frame, and its last row is the -- black threshold the building stands on with the door's own step cut into -- it. Painting all 16x16 onto a flank would stamp a one-pixel border of -- front-wall art around every door. -- -- The front facade tells the ring from the door by flooding: the wall -- around the door is one region with the whole facade, far too big to be a -- pane, and only what the black frame SEALS sinks. The same test, bounded -- to the block: flood the left, right and top edges through their own -- shade class, and what the flood reaches is context -- left unpainted, so -- the flank keeps the texel it already had. Not the bottom edge, because -- the bottom edge is the ground: the step under the door is sealed there -- on the drawn facade too, and it recesses with the rest of the doorway. -- -- What is painted then splits the way a facade's does: black is frame and -- stays flush with the wall, everything it seals sinks a voxel behind it. local function readDoor(sp, data, perRow, cell) local base = sp.W * sp.H local black = {} for dy = 0, 15 do local row = cell[math.floor(dy / 8) + 1] for dx = 0, 15 do local tile = row[math.floor(dx / 8) + 1] local px = (tile % perRow) * 8 + dx % 8 local py = math.floor(tile / perRow) * 8 + dy % 8 local k = dy * 16 + dx local i = base + k sp.ax[i], sp.ay[i] = px, py local r, g, b, a = data:getPixel(px, py) sp.col[i] = shadeOf(r, g, b, a) sp.inside[i] = true black[k] = sp.col[i] == BLACK end end local context, stack = {}, {} local function seed(dx, dy, cls) if dx < 0 or dx > 15 or dy < 0 or dy > 15 then return end local k = dy * 16 + dx if context[k] or black[k] ~= cls then return end context[k] = true stack[#stack + 1] = k end for dy = 0, 15 do seed(0, dy, black[dy * 16]) seed(15, dy, black[dy * 16 + 15]) end for dx = 0, 15 do seed(dx, 0, black[dx]) end while #stack > 0 do local k = table.remove(stack) local dx, dy, cls = k % 16, math.floor(k / 16), black[k] seed(dx + 1, dy, cls) seed(dx - 1, dy, cls) seed(dx, dy + 1, cls) seed(dx, dy - 1, cls) end sp.door = { base = base, black = black, context = context } end local function read(t, data, perRow, frontOnly) local tiles = t.tiles if t.topRows then tiles = {} for _, row in ipairs(t.topRows) do tiles[#tiles + 1] = row end for _, row in ipairs(t.tiles) do tiles[#tiles + 1] = row end end local bh, bw = #tiles, #t.tiles[1] local W, H = bw * 8, bh * 8 local col, ax, ay = {}, {}, {} for sy = 0, H - 1 do Budget.tick() local row = tiles[math.floor(sy / 8) + 1] for sx = 0, W - 1 do local tile = row[math.floor(sx / 8) + 1] local px = (tile % perRow) * 8 + sx % 8 local py = math.floor(tile / perRow) * 8 + sy % 8 local i = sy * W + sx ax[i], ay[i] = px, py local r, g, b, a = data:getPixel(px, py) col[i] = shadeOf(r, g, b, a) end end local outside = {} local queue, n = {}, 0 local function seed(x, y) local i = y * W + x if not outside[i] and col[i] <= GREY then outside[i] = true n = n + 1 queue[n] = i end end -- The flood comes in from the border, which assumes the drawing is -- bounded by its own outline on every side. A drawing trimmed flush to -- its art -- one whose base course is a row of brick rather than the -- black threshold every other building stands on -- names the sides it -- runs off in `seal`, and the flood does not seed there. Without it the -- flood climbs in through the light mortar and hollows the wall out. local seal = t.seal or "" local function sealed(side) return string.find(seal, side, 1, true) ~= nil end for x = 0, W - 1 do if not sealed("n") then seed(x, 0) end if not sealed("s") then seed(x, H - 1) end end for y = 0, H - 1 do if not sealed("w") then seed(0, y) end if not sealed("e") then seed(W - 1, y) end end while n > 0 do local i = queue[n] n = n - 1 local x, y = i % W, math.floor(i / W) if x + 1 < W then seed(x + 1, y) end if x > 0 then seed(x - 1, y) end if y + 1 < H then seed(x, y + 1) end if y > 0 then seed(x, y - 1) end end local inside = {} for i = 0, W * H - 1 do inside[i] = not outside[i] end -- `scrub` names pixel rects where the drawing paints an object standing -- ON the surface (Red's potted plant on the dining tabletop). The object -- keeps its own standee -- the template's `keep` leaves its tiles -- unclaimed -- so the band beneath it is the one surface the drawing -- implies but never paints clear: every rect pixel takes the field -- shade, sourced from the first field texel outside the rects, and the -- model's top comes out as the plain surface the object sat on. if t.scrub then local function inRect(x, y) for _, r in ipairs(t.scrub) do if x >= r[1] and x <= r[3] and y >= r[2] and y <= r[4] then return true end end return false end local donor = nil for i = 0, W * H - 1 do if col[i] == GREY and inside[i] and not inRect(i % W, math.floor(i / W)) then donor = i break end end for i = 0, W * H - 1 do if inRect(i % W, math.floor(i / W)) then col[i] = GREY ax[i], ay[i] = ax[donor], ay[donor] inside[i] = true end end end return { W = W, H = H, col = col, ax = ax, ay = ay, inside = inside, back = backMap(tiles, bw, bh, W, inside, frontOnly) } end -- --------------------------------------------------------------- measure -- local function measure(sp, t) local W, H = sp.W, sp.H local roofRows = t.roofRows -- The drawn taper IS the slope: the first drawn row of a column is how -- far the roof has stepped down by the time it reaches that column. local top = {} for x = 0, W - 1 do local r = roofRows for y = 0, roofRows - 1 do if sp.inside[y * W + x] then r = y break end end top[x] = r end -- The row a column's roof SURFACE may sink to. `top[x]` is the -- silhouette cap -- the black the drawing closes its shape with -- and -- the depth map spends most of a tapered column's depth above it, so -- clamping onto `top[x]` paints that one outline pixel the length of -- the slope and the courses beat against it. The surface belongs on the -- first PAINTED row instead: the same refusal to let the outline stand -- as a face that the side faces already make below. local surfaceTop = {} for x = 0, W - 1 do local y = top[x] while y < roofRows and sp.inside[y * W + x] and sp.col[y * W + x] == BLACK do y = y + 1 end if y < roofRows and sp.inside[y * W + x] then surfaceTop[x] = y else surfaceTop[x] = top[x] end end -- The drawing's own ground line: the row after the last drawn one. A -- building ends on the black threshold row it stands on (ground == H), -- but furniture is drawn standing on open floor -- the lab table's -- legs stop two rows short of its grid -- and extruding against H -- would float it that far above its own plot. local ground = roofRows for sy = H - 1, roofRows, -1 do local drawn = false for sx = 0, W - 1 do if sp.inside[sy * W + sx] then drawn = true break end end if drawn then ground = sy + 1 break end end local wallH = ground - roofRows local ytop = wallH - 1 + t.slab -- Side faces must not come out as slabs of outline black: where the -- drawing's own pixel is the outline, walk inward for the first painted -- colour, which is what the flanks of the real thing would show. local interior = {} for sy = roofRows, H - 1 do for sx = 0, W - 1 do local i = sy * W + sx local src = i if sp.inside[i] and sp.col[i] == BLACK then local step = sx < W / 2 and 1 or -1 for d = 1, 3 do local nx = sx + step * d if nx >= 0 and nx < W then local ni = sy * W + nx if sp.inside[ni] and sp.col[ni] ~= BLACK then src = ni break end end end end interior[i] = src end end -- Panes: the facade's non-black pixels split into regions across the -- black frames, and a region small enough to be a window or a doorway -- sinks a voxel. Frames stay proud, so the pane behind them reads as -- glass set into the wall -- and a nested frame (the door's own little -- window) layers for free. local recess, seen = {}, {} for sy = roofRows, H - 1 do for sx = 0, W - 1 do local i0 = sy * W + sx if not seen[i0] and sp.inside[i0] and sp.col[i0] ~= BLACK then local cells, stack = {}, { i0 } seen[i0] = true local x0, x1, y0, y1 = sx, sx, sy, sy local function step(nx, ny) if nx < 0 or nx >= W or ny < roofRows or ny >= H then return end local ni = ny * W + nx if not seen[ni] and sp.inside[ni] and sp.col[ni] ~= BLACK then seen[ni] = true stack[#stack + 1] = ni end end while #stack > 0 do local i = table.remove(stack) cells[#cells + 1] = i local cx, cy = i % W, math.floor(i / W) if cx < x0 then x0 = cx end if cx > x1 then x1 = cx end if cy < y0 then y0 = cy end if cy > y1 then y1 = cy end step(cx + 1, cy) step(cx - 1, cy) step(cx, cy + 1) step(cx, cy - 1) end if x1 - x0 < RECESS_MAX and y1 - y0 < RECESS_MAX then for _, i in ipairs(cells) do recess[i] = true end end end end end -- The pane rule reads a LIGHT region the drawing seals behind a BLACK -- frame. A drawing built the other way round -- the healing machine's -- dark screens sealed behind their own white bezels -- inverts under -- it: every lit edge sinks and the black panes stand proud, a black -- lattice a voxel off the face. `panes = false` says the drawing does -- not carry the rule's polarity, so the facade stays flush. if t.panes == false then recess = {} end -- One representative texel per shade, taken from the building's own art: -- the roof's fascia and its undersides are geometry the drawing implies -- but never paints, and they must still wear its palette (and pick up -- whatever SGB recolouring the atlas carries). local shadeTexel = {} for i = 0, sp.W * sp.H - 1 do if sp.inside[i] and not shadeTexel[sp.col[i]] then shadeTexel[sp.col[i]] = i end end for s = WHITE, BLACK do shadeTexel[s] = shadeTexel[s] or shadeTexel[BLACK] or 0 end -- Depth is the MATCHED footprint, not the sprite height. The two are -- the same number for every whole-drawing template (the sprite is -- built from `tiles` alone), but a template with `topRows` has a -- sprite taller than its footprint -- the tower's 16-row drawing -- stands on the 8 rows of it that are actually on the map, and D = H -- would have pushed its body 64px south into the town plaza. -- `depth` (in tile rows) names the plot when the grid runs PAST it -- onto ground the drawing merely stands its legs on: the lab table's -- third row is the walkable cell the player faces it from, and the -- full-grid depth would stand the model in their path. -- `depth` names the plot in TILE ROWS, which is the right grain for a -- building. `depthPx` names it in voxels, for an object whose real -- depth is not a whole tile row -- the Bike Shop toolbox is a box -- standing in the middle of its own cell, not a thing that fills a plot. return { top = top, surfaceTop = surfaceTop, ytop = ytop, D = t.depthPx or ((t.depth or #t.tiles) * 8), ground = ground, recess = recess, interior = interior, shadeTexel = shadeTexel } end -- ----------------------------------------------------------------- build -- -- A desk with separately-classified objects on it (a template's `parts` -- list): the methodology's region classification at part granularity. -- Upright parts anchor their drawn bottom row to the desk's top plane -- and wear their own drawn tops as lids; flat parts (a keyboard, a -- sheet of paper) lie one voxel proud at drawn row = depth row -- the -- same 1:1 the tabletop itself is drawn with, so an object's height ON -- the drawing is its position ON the desk. The desk is the lab-table -- slab + base; its lid is the one synthesized surface in the model -- (the objects cover every drawn pixel of the tabletop), continued -- from the sibling tables' pattern in the drawing's own shades. -- tools/building_voxels.py `build_desk_set` is the reference twin. local function deskSetModel(sp, pr, t) local W, H, D = sp.W, sp.H, pr.D local ground = pr.ground local col, inside = sp.col, sp.inside local vox = {} local function key(x, y, z) return (y * D + z) * W + x end local function put(x, y, z, i) vox[key(x, y, z)] = i end -- de-outline walk bounded to the part, so a part's side faces show -- its own material and never the neighbour's (the sprite-wide walk -- the facade path uses would cross the black seam between units) local function interiorAt(sx, sy, lo, hi) local i = sy * W + sx if col[i] ~= BLACK then return sx end local step = sx < math.floor((lo + hi) / 2) and 1 or -1 for d = 1, 3 do local nx = sx + step * d if nx >= lo and nx <= hi then local ni = sy * W + nx if inside[ni] and col[ni] ~= BLACK then return nx end end end return sx end -- The parts list, shared by every base piece: a desk plane or an -- open tray rim alike, `plane` is simply the height they ride. local ytop = 0 local function buildParts(plane) for _, p in ipairs(t.parts) do Budget.tick() local x0 = p.x and p.x[1] or 0 local x1 = p.x and p.x[2] or (W - 1) if p.kind == "flat" then -- drawn row = depth row by default; `z` renames the origin when -- the flat sits below the desk's own drawn top span (the Center -- PC's keyboard). `at` names the sheet's own height when it does -- not lie on the desk plane (the healing machine's keyboard is a -- shelf mounted on the cabinet's side); `thick` gives it a body -- -- layers below the sheet repeating each column's own texel, -- the same continuation rule every synthesized surface follows. local r0 = p.rows[1] local z0 = p.z or r0 local atY = p.at or plane local thick = p.thick or 1 if atY > ytop then ytop = atY end for sy = r0, p.rows[2] do local z = z0 + (sy - r0) if z >= 0 and z < D then for sx = x0, x1 do if inside[sy * W + sx] then for y = math.max(0, atY - thick + 1), atY do put(sx, y, z, sy * W + sx) end end end end end elseif p.kind == "box" then -- A BOX part is a drawn rect standing at its own drawn -- elevation -- equipment attached to the machine rather than an -- object on the desk plane. The rows are face-on art: the top -- row's drawn height IS the box's top (ground - 1 - r0, -- measured), and the box runs down to `base` (default the drawn -- extent; 0 continues it to the floor, the legs-continue rule). -- Height beyond the drawn rows fills the way a roof band does: -- rows before `cycle` map 1:1 from the top, rows after it 1:1 -- from the bottom -- the healing machine hoses' foot lands ON -- the floor -- and the cycle window repeats between. local r0, r1 = p.rows[1], p.rows[2] local c0 = p.cycle and p.cycle[1] or r1 local c1 = p.cycle and p.cycle[2] or r1 local pz = p.z or 0 local pd = p.depth local top = pr.ground - 1 - r0 local bot = p.base or (pr.ground - 1 - r1) local nTop, nBot = c0 - r0, r1 - c1 if top > ytop then ytop = top end for y = bot, top do local k, j = top - y, y - bot local sy if k < nTop then sy = r0 + k elseif j < nBot then sy = r1 - j else sy = c0 + (k - nTop) % (c1 - c0 + 1) end for sx = x0, x1 do local i = sy * W + sx if inside[i] then local ix = interiorAt(sx, sy, x0, x1) for z = pz, pz + pd - 1 do if z >= 0 and z < D then local px = (z == pz or z == pz + pd - 1) and sx or ix put(sx, y, z, sy * W + px) end end end end end elseif p.kind == "iso" then -- An ISO part is drawn in 2:1 isometric -- a box TURNED 45 -- degrees to the map, so one rhombus carries its top, its front -- and its side at once and no band or facade split can reach -- them. Un-projecting it is that projection run backwards: the -- box stands as a real diamond in plan and every voxel wears the -- texel the drawing paints where that voxel projects TO. The -- drawn top lands on the top, the screen on the screen-facing -- side and the flank on the flank, and nothing is segmented by -- hand -- which is the only way to get this right, because the -- three faces meet on a diagonal no rectangle can name. -- -- Everything but the depth centre falls out of the drawn rect, -- because the projection fixes it: the half-width is the drawn -- rhombus's x radius, HALF that again its z radius (2:1 is what -- makes it isometric), the near corner's drawn row is the base -- rhombus's front tip, and whatever drawn height is left once -- that rhombus is accounted for is the box's own height. Bill's -- computer: rx 6, rz 3, base centre row 10, and 6 voxels tall -- -- which puts its left corner's vertical edge at drawn rows -- 4..10, exactly where the drawing paints one. -- -- `plan` is the one thing the drawing CANNOT state: 2:1 is the -- projection, not the object, so reading rz as the plan radius -- too builds a box half as deep as it is wide -- a slab, not the -- cube the drawing depicts. `plan` names the real z radius and -- the drawn row is scaled into it, so a cube is `plan = rx` and -- the drawing still lands on it pixel for pixel. local pr0, pr1 = p.rows[1], p.rows[2] local rx = math.floor((x1 - x0 + 1) / 2) local rz = math.floor(rx / 2) local plan = p.plan or rz local oy = pr1 - rz local h = oy - rz - pr0 local ytp = plane + h if ytp > ytop then ytop = ytp end for sx = x0, x1 do -- doubled, so a rect of even width keeps its centre between -- two columns instead of limping one to the left local dx2 = 2 * sx - (x0 + x1) for dz = -plan, plan do local z = p.z + dz local d2 = math.abs(dx2) * plan + 2 * math.abs(dz) * rx if z >= 0 and z < D and d2 <= (2 * rx + 1) * plan then -- the plan row scaled back into the drawn rhombus local dzs = math.floor((2 * dz * rz + plan) / (2 * plan)) for y = 0, h do local sy = oy + dzs - y local i = sy * W + sx if sy >= pr0 and sy <= pr1 and inside[i] then put(sx, plane + y, z, i) end end end end end elseif p.kind == "plan" then -- A PLAN part is a slab whose plan IS the drawn top view: the -- band's silhouette becomes the footprint pixel for pixel -- (drawn row = depth row, the same 1:1 every tabletop is drawn -- with), so an octagonal top stands as an octagon rather than -- the box no rectangular band can escape. The top layer wears -- the band itself, outline and all; the rim layers below wear -- the drawn fascia rows folded down the edge (x clamped into -- the drawn fascia's span), and the slab's unseen interior the -- field's dark texel. local r0, r1 = p.rows[1], p.rows[2] local f0, f1 = p.fascia[1], p.fascia[2] local fx0, fx1 = p.fasciaX[1], p.fasciaX[2] local rise = p.rise or 0 local h = (f1 - f0 + 1) + 1 if rise + h > ytop then ytop = rise + h end local function drawn(sx, z) return sx >= x0 and sx <= x1 and z >= 0 and z <= r1 - r0 and inside[(r0 + z) * W + sx] end for z = 0, r1 - r0 do if z >= 0 and z < D then local sy = r0 + z for sx = x0, x1 do if inside[sy * W + sx] then put(sx, rise + h - 1, z, sy * W + sx) local edge = not (drawn(sx - 1, z) and drawn(sx + 1, z) and drawn(sx, z - 1) and drawn(sx, z + 1)) for y = rise, rise + h - 2 do if edge then local fsx = math.max(fx0, math.min(fx1, sx)) put(sx, y, z, (f0 + (rise + h - 2 - y)) * W + fsx) else put(sx, y, z, pr.shadeTexel[DARK]) end end end end end end elseif p.kind == "disc" then -- A DISC part is ROUND IN PLAN -- the pedestal column and base -- the projection can only draw from the front. Centre and -- radius are measured off the drawn widths (a flattened arc is -- a horizontal circle seen from above); the circular footprint -- is synthesized like any continued geometry, and every voxel -- still wears the drawing: the side folds the drawn face-on -- rows around the hull (x clamped into the drawn span, rows -- repeating up the height), and `cap` lays the drawn top-view -- rows over the top layer's interior, drawn north rows to the -- plan's north. `cx2`/`cz2` are DOUBLED plan centres, so an -- even diameter keeps its centre between two voxels instead of -- limping one off. local r, rise, h = p.r, p.rise or 0, p.h local s0, s1 = p.side.rows[1], p.side.rows[2] local sa0, sa1 = p.side.x[1], p.side.x[2] local sn = s1 - s0 + 1 if rise + h > ytop then ytop = rise + h end local function inDisc(x, z) local dx = 2 * x + 1 - p.cx2 local dz = 2 * z + 1 - p.cz2 return dx * dx + dz * dz <= 4 * r * r end local zlo = math.floor((p.cz2 - 2 * r) / 2) for x = math.floor((p.cx2 - 2 * r) / 2), math.floor((p.cx2 + 2 * r) / 2) do for z = math.max(0, zlo), math.min(D - 1, math.floor((p.cz2 + 2 * r) / 2)) do if inDisc(x, z) then local edge = not (inDisc(x - 1, z) and inDisc(x + 1, z) and inDisc(x, z - 1) and inDisc(x, z + 1)) for y = rise, rise + h - 1 do local sx, sy if p.cap and y == rise + h - 1 and not edge then local c0, c1 = p.cap.rows[1], p.cap.rows[2] sy = math.min(c1, c0 + math.floor((z - zlo) * (c1 - c0 + 1) / (2 * r))) sx = math.max(p.cap.x[1], math.min(p.cap.x[2], x)) else sy = s0 + (rise + h - 1 - y) % sn sx = math.max(sa0, math.min(sa1, x)) end put(x, y, z, sy * W + sx) end end end end else local tr0, tr1 = p.top[1], p.top[2] local fr0, fr1 = p.facade[1], p.facade[2] local pd = p.depth -- `rise` lifts a part off the desk's top plane and `z` names its -- back-most depth row (the field a flat part already carries). An -- object STANDING on a desk needs neither: it starts on the plane -- at the plot's back. The healing machine's console needs both -- -- it stands in the FRONT map row of a grid whose back row is the -- wall band it leans against, and its screen head is MOUNTED on -- the console's front two voxels above the body's top. Both come -- off the drawing, not off taste. local base = plane + (p.rise or 0) local pz = p.z or 0 local ytp = base + (fr1 - fr0) if ytp > ytop then ytop = ytp end -- `inset` sinks an authored pane one voxel: the pane rule -- applied by hand, for a part whose screen IS sealed behind its -- own black frame while the template's `panes = false` (set for -- the polarity-inverted panel elsewhere in the same drawing) -- blocks the global pass. Same mechanism as a recess: the front -- voxel is simply not placed. local ins = p.inset for sx = x0, x1 do -- the lid: the part's drawn top laid across its depth from the -- back, last row continuing forward; the front lid row is the -- facade's own top row -- the drawn front-top edge. `stretch` -- maps the drawn band over the whole depth instead, the tray's -- rule: for a part authored DEEPER than its drawing (the house -- stool grown past its drawn seat), clamping would print the -- last row as a long smear off the back band's edge. for z = pz, pz + pd - 1 do local front = z == pz + pd - 1 local sy if front then sy = fr0 elseif p.stretch then sy = math.min(tr0 + math.floor((z - pz) * (tr1 - tr0 + 1) / (pd - 1)), tr1) else sy = math.min(tr0 + z - pz, tr1) end while sy <= tr1 and not inside[sy * W + sx] do sy = sy + 1 end local ok = sy <= tr1 or (front and inside[fr0 * W + sx]) if ok and z >= 0 and z < D then put(sx, ytp, z, (front and fr0 or sy) * W + sx) end end -- the body: facade rows anchored to the part's own base for sy = fr0 + 1, fr1 do local y = base + (fr1 - sy) local i = sy * W + sx if inside[i] then local ix = interiorAt(sx, sy, x0, x1) for z = pz, pz + pd - 1 do if z >= 0 and z < D then if z == pz + pd - 1 then local sunk = ins and sx >= ins.x[1] and sx <= ins.x[2] and sy >= ins.rows[1] and sy <= ins.rows[2] if not sunk and not pr.recess[i] then put(sx, y, z, i) end elseif z == pz then put(sx, y, z, i) else put(sx, y, z, sy * W + ix) end end end end end end end end end -- A TRAY is an open container -- the drawing looks down INTO it, so its -- top-view band is not a lid but the inside of the box, and the model -- has to be hollow. Bands, all measured 1:1 like any other band table: -- `top` is the opening (drawn row -> depth row), `front` the near wall -- seen face-on (drawn row -> elevation), `x` the box's outer span and -- `inner` the opening's, so the difference between them is the wall. -- Four walls stand to the rim, the floor slab lies `floor` voxels thick -- under the opening, and the cavity between them is left as AIR -- which -- is the whole point, and what an extruded facade can never be. Parts (a -- standing lid) then ride the rim like any object on a desk's plane. if t.tray then local tr = t.tray local top0 = tr.top[1] local fr0, fr1 = tr.front[1], tr.front[2] local bx0, bx1 = tr.x[1], tr.x[2] local ix0, ix1 = tr.inner[1], tr.inner[2] local floor = tr.floor or 0 local plane = fr1 - fr0 + 1 -- the rim: the wall's height -- Which drawn row lies at depth z. The far rim is the band's first -- row and the near rim the front wall's own, and the drawn inside -- STRETCHES over whatever depth is between them: a box deeper than -- its drawing has rows to spare is the ordinary case once the plot -- stops being the grid, and the alternative -- running out of rows -- and repeating the last one -- would print the wrench twice. local lo, hi = top0 + 1, tr.top[2] - 1 -- the drawn inside local span = math.max(1, D - 3) -- interior depth rows - 1 local function trayRow(z) if z == 0 then return top0 end if z == D - 1 then return fr0 end return lo + math.floor((z - 1) * (hi - lo) / span) end for sx = bx0, bx1 do Budget.tick() for z = 0, D - 1 do local hollow = sx >= ix0 and sx <= ix1 and z > 0 and z < D - 1 for y = 0, (hollow and floor or plane - 1) do if hollow or y == plane - 1 then -- the opening seen from above: the tray's own floor and -- whatever lies in it -- and the rim is the same band where -- the wall meets it local i = trayRow(z) * W + sx if inside[i] then put(sx, y, z, i) end else -- the wall below the rim: the front band folded up it, the -- drawn face on the front and back layers and the de-outlined -- interior between, exactly as a facade extrudes. -- -- NO recess pass here, and it must stay that way: a pane sinks -- by DELETING its front voxel so the one behind becomes the -- pane, and a container's wall is one voxel thick -- there is -- nothing behind it, so the front panel simply opened a hole -- straight into the box and you could see the wrench through it. local sy = fr1 - y local i = sy * W + sx if inside[i] then local px = (z == 0 or z == D - 1) and sx or interiorAt(sx, sy, bx0, bx1) put(sx, y, z, sy * W + px) end end end end end if plane > ytop then ytop = plane end buildParts(plane) return { at = function(x, y, z) if x < 0 or x >= W or y < 0 or z < 0 or z >= D then return nil end return vox[key(x, y, z)] end, W = W, ytop = ytop, zmin = 0, zmax = D - 1 } end -- No base piece at all: the drawing IS its parts (the house stool -- a -- seat and its legs, nothing under them but floor). The plane the parts -- anchor to is the ground itself. if not t.desk then buildParts(0) return { at = function(x, y, z) if x < 0 or x >= W or y < 0 or z < 0 or z >= D then return nil end return vox[key(x, y, z)] end, W = W, ytop = ytop, zmin = 0, zmax = D - 1 } end -- The desk's top plane. Usually the drawing states it: the fascia and -- base rows it paints below the objects ARE the front face, and their -- row count is the height. Bill's desk paints neither inside its grid -- -- its apron is drawn into the WALKABLE cell in front, and that cell -- is left out on purpose so the chair standing there keeps its own -- tiles -- so `plane` names the height directly and the body below the -- lid is synthesized: the band table's own rim treatment, a shaded box -- closed by the outline where it meets the floor, in the drawing's -- shades via shadeTexel. local f0, f1 = t.desk.fascia[1], t.desk.fascia[2] local b0, b1 = t.desk.base[1], t.desk.base[2] local plane = (b1 - b0 + 1) + (f1 - f0 + 1) -- The desk's own PLOT, when the grid holds more than the desk. Bill's -- grid runs on into the walkable cell, because the drawing puts the -- desk's apron AND the chair pushed up to it in the same tiles -- so -- the desk box has to stop at its own cell (`depth`) and stand on its -- own ground line rather than the grid's, which the chair's feet set -- eight rows lower. The base band's last row IS that ground line by -- definition, and for every desk drawn inside its own grid it is the -- measured one to the row (lab table, lab computers, Center PC, the -- Bike Shop toolbox), so this changes nothing for them. -- ...and in voxels (`depthPx`) plus a back origin (`z`) when the desk -- is shallower than a tile row and leans against something: the -- healing machine's cabinet is 10 deep -- its drawn top band's 9 rows -- plus the front edge -- standing against the wall band, so its box -- runs z 16..25 of a 32-deep plot. local deskD = t.desk.depthPx or (t.desk.depth and t.desk.depth * 8) or D local dz0 = t.desk.z or 0 local dz1 = dz0 + deskD - 1 local deskG = b1 + 1 -- ...and the desk's COLUMNS (`x`), when the grid is wider than the -- desk: the healing machine's grid carries its flanking hoses and -- keyboard, and the cabinet is only the middle 16 columns. local dx0 = t.desk.x and t.desk.x[1] or 0 local dx1 = t.desk.x and t.desk.x[2] or W - 1 -- The WALL element: the band the machine backs onto, whose tiles this -- grid claims. The drawing shows it only as the stripe background -- around the tower (the same standing as the potted plants' floor), -- so the block cycles the drawing's own stripe unit -- real pixels of -- column `x`, rows `cycle` -- at wall-band height over the back plot, -- exactly what the neighbouring cells' `wall` pins render. if t.wall then local wl = t.wall local c0, c1 = wl.cycle[1], wl.cycle[2] local cn = c1 - c0 + 1 local wx = wl.x or 0 for y = 0, wl.h - 1 do Budget.tick() local sy = c0 + (wl.h - 1 - y) % cn for sx = 0, W - 1 do for z = 0, wl.depthPx - 1 do put(sx, y, z, sy * W + wx) end end end end -- the base band, extruded exactly like every lab table's for sy = b0, b1 do Budget.tick() local y = deskG - 1 - sy for sx = dx0, dx1 do if inside[sy * W + sx] then local ix = interiorAt(sx, sy, dx0, dx1) for z = dz0, dz1 do local px = (z == dz0 or z == dz1) and sx or ix put(sx, y, z, sy * W + px) end end end end for i in pairs(pr.recess) do local sy = math.floor(i / W) local sx = i % W if sy >= b0 and sy <= b1 and sx >= dx0 and sx <= dx1 then vox[key(sx, deskG - 1 - sy, dz1)] = nil end end -- the slab: fascia rows wrap every side for sy = f0, f1 do Budget.tick() local y = plane - 1 - (sy - f0) for sx = dx0, dx1 do for z = dz0, dz1 do put(sx, y, z, sy * W + sx) end end end if t.desk.top then -- The lid wears the desk's own drawn top band -- the drawing DOES -- paint this tabletop (the healing machine's white top face with -- its lit west and shaded east strips), so nothing is synthesized -- where it is visible: band rows map back-to-front, the first -- fascia row is the drawn front-top edge, same rule as an upright -- part's lid. Where a part's drawing occludes the band (the monitor -- standing on it), the lid continues the nearest strip BESIDE the -- part -- still the drawing's own pixels, the same sibling-pattern -- rule every synthesized lid follows. local tr0, tr1 = t.desk.top[1], t.desk.top[2] for z = dz0, dz1 do Budget.tick() local sy = z == dz1 and f0 or math.min(tr0 + (z - dz0), tr1) for sx = dx0, dx1 do local px = sx for _, p in ipairs(t.parts) do local px0, px1 = p.x[1], p.x[2] local r0, r1 if p.kind == "flat" or p.kind == "iso" or p.kind == "box" then r0, r1 = p.rows[1], p.rows[2] else r0, r1 = p.top[1], p.facade[2] end if sx >= px0 and sx <= px1 and sy >= r0 and sy <= r1 then px = (sx - px0 < px1 - sx) and (px0 - 1) or (px1 + 1) px = math.max(dx0, math.min(dx1, px)) break end end put(sx, plane - 1, z, sy * W + px) end end else -- the lid continues the sibling tables' top -- black rim, white -- highlight courses along the north and west, grey field local field = t.desk.lid == "white" and WHITE or GREY for sx = dx0, dx1 do for z = dz0, dz1 do local shade = field if sx == dx0 or sx == dx1 or z == dz0 or z == dz1 then shade = BLACK elseif sx == dx0 + 1 or z == dz0 + 1 then shade = WHITE end put(sx, plane - 1, z, pr.shadeTexel[shade]) end end end if plane > ytop then ytop = plane end buildParts(plane) return { at = function(x, y, z) if x < 0 or x >= W or y < 0 or z < 0 or z >= D then return nil end return vox[key(x, y, z)] end, W = W, ytop = ytop, zmin = 0, zmax = D - 1 } end -- ------- the doorway a gate house is entered by from a side the drawing -- never shows it on (data/voxel_heights.lua `sideDoors`, and `sideDoorsAt` -- below for how the placements are found). -- -- One cell of the tileset's own doorway art, standing on the ground of the -- face the player walks into, and hung by the SAME rule the drawn facade -- hangs its own door by: the art's black frame stays flush with the wall -- and everything it seals sinks a voxel behind it (`measure`'s pane pass, -- applied here by hand because the art is not in the drawing to be flooded -- with it). So a side door and a front door are the same depth of the same -- opening, and the jamb faces the recess exposes come out of the mesher for -- free, wearing the frame's own texels. -- -- ORIENTATION IS NOT FREE. A flank quad carries one texel and the mesher -- picks it per voxel, so which art column lands at which world coordinate -- is decided HERE and nowhere else -- and a face is read from outside, so -- the art's own left-to-right runs with the viewer's, not with the world's: -- facing east at a west wall, south is to your right (+z); facing west at -- an east wall, north is (-z); facing south at a north wall, west is (-x). -- Two of the three are mirrored against the axis, which is the same reason -- `backMap` exists -- a wall seen from behind IS the drawing mirrored. local DOOR = 16 local function sideDoors(at, sp, doors, W) if not (doors and #doors > 0 and sp.door) then return at end local art = sp.door -- The face's OUTER surface, walked in from the box edge until the wall -- answers. A drawing inset from its own grid (B03's outer columns are -- terrain, not building) stands its flank a column or two in, and a door -- pinned to the box edge would hang in the air beside it. local function faceX(from, step, off) for k = 0, W - 1 do local x = from + step * k for y = 0, DOOR - 1 do for z = off, off + DOOR - 1 do if at(x, y, z) then return x end end end end return nil end local list = {} for _, d in ipairs(doors) do local face = 0 -- north: the facade's own z origin if d.side == "w" then face = faceX(0, 1, d.at) elseif d.side == "e" then face = faceX(W - 1, -1, d.at) end if face then list[#list + 1] = { side = d.side, off = d.at, face = face } end end if #list == 0 then return at end -- art column at (x, z) for door `e`, or nil when the voxel is not in it local function column(e, x, z) if e.side == "n" then if (z == 0 or z == 1) and x >= e.off and x < e.off + DOOR then return e.off + DOOR - 1 - x, z == 0 end elseif z >= e.off and z < e.off + DOOR then if e.side == "w" then if x == e.face then return z - e.off, true end if x == e.face + 1 then return z - e.off, false end else if x == e.face then return e.off + DOOR - 1 - z, true end if x == e.face - 1 then return e.off + DOOR - 1 - z, false end end end return nil end return function(x, y, z) local v = at(x, y, z) -- no wall here is the end of it: a door is hung ON the building, and -- nothing about it may add geometry the drawing does not stand up if v == nil or y >= DOOR then return v end for _, e in ipairs(list) do local c, outer = column(e, x, z) if c then -- art row 0 is the door's head, so the ground row is its last local k = (DOOR - 1 - y) * DOOR + c -- the art's own ring: wall, not door. The flank keeps its texel. if art.context[k] then return v end if art.black[k] then -- the frame, flush with the wall; behind it the wall stands on if outer then return art.base + k end return v end -- and what the frame seals sinks: the face voxel goes, the one -- behind it wears the art. (Written long: `outer and nil or i` -- returns i for BOTH, nil being false to `and`.) if outer then return nil end return art.base + k end end return v end end -- The voxel model as a lookup: `at(x, y, z)` is the index of the sprite -- pixel that voxel wears, or nil. Build ORDER is expressed as lookup -- order -- roof first, so it overwrites the walls it intersects, and walls -- are trimmed to its underside so nothing pokes through the surface. A -- gate's side doors are hung on the finished lookup, last of all, because -- they answer to the FACE rather than to any band of the drawing. local function model(sp, pr, t, doors) if t.parts then return deskSetModel(sp, pr, t) end local W, H, D = sp.W, sp.H, pr.D local slab, roofRows = t.slab, t.roofRows local top, ytop, ground = pr.top, pr.ytop, pr.ground local surfaceTop = pr.surfaceTop -- The roof's drawn span. A sprite inset from its box (B03) leaves outer -- columns undrawn in the roof band; they carry no roof at all, and the -- rim treatment belongs to the outermost drawn columns instead of the -- box edge. local x0d, x1d for x = 0, W - 1 do if top[x] < roofRows then x0d = x0d or x x1d = x end end local ledge0, ledge1 = nil, nil if t.ledge then ledge0, ledge1 = t.ledge[1], t.ledge[2] end local rz0, rz1 = 0, D - 1 + (t.frontEave or 0) local back, front = t.roofBack, t.roofFront local cyc0, cyc1 = t.roofCycle[1], t.roofCycle[2] local cycN = cyc1 - cyc0 + 1 -- Which drawn row lies at depth z. The drawing looks at the roof from -- the north, so its top rows ARE the far edge and its bottom rows the -- eave over the facade. The band is shallower than the building, so the -- rims map one row per voxel and the middle cycles a run whose period is -- the course rhythm -- picked up where the north rim left off, which -- continues both the course lines and the roof texture seamlessly. local roofSy = {} for z = rz0, rz1 do local df, db = z - rz0, rz1 - z -- from the north / south edge if df < back then roofSy[z] = df elseif db < front then roofSy[z] = roofRows - 1 - db else roofSy[z] = cyc0 + (df - cyc0) % cycN end end local T = {} for x = 0, W - 1 do T[x] = ytop - top[x] end -- the back layer's texel: the drawing again, minus what only the front -- may wear (see backMap) local back = sp.back local function backOf(i) return (back and back[i]) or i end local function at(x, y, z) if x < 0 or x >= W then return nil end local tx = T[x] -- roof: a solid of constant thickness following the elevation profile if top[x] < roofRows and y > tx - slab and y <= tx and z >= rz0 and z <= rz1 then if y == tx and x > x0d and x < x1d and z > rz0 and z < rz1 then -- the surface itself. Lifting the row into the column's first -- PAINTED row keeps the flank battens running down the slope -- instead of falling off the silhouette -- and off its cap, which -- is outline black and belongs to the rim, not to the surface. local sy = roofSy[z] if sy < surfaceTop[x] then sy = surfaceTop[x] end return sy * W + x end -- The rim reproduces the eave the drawing itself paints under the -- roof: a black outline, a shaded fascia, closed by the outline -- again. (A GREY fascia band -- what the first cut had -- comes out -- WHITE once the atlas is recoloured and turns every sloped end -- into a black-and-white zip.) Under the surface it is all shadow. local outer = x == x0d or x == x1d or z == rz0 or z == rz1 if not outer then return pr.shadeTexel[DARK] end if y == tx or y == tx - slab + 1 then return pr.shadeTexel[BLACK] end return pr.shadeTexel[DARK] end -- trimmed: under the slope. A column with no roof over it has no -- underside to trim to, and must not be cut away by a profile the -- drawing never set. if top[x] < roofRows and y > tx - slab then return nil end -- the awning: the band juts two voxels past the walls, front and back if ledge0 and (z == -2 or z == -1 or z == D or z == D + 1) then local sy = ground - 1 - y if sy >= ledge0 and sy <= ledge1 and sp.inside[sy * W + x] then -- z < 0 is the awning's NORTH end: same substitution the wall -- behind it makes, so a band that carries a sign does not carry -- it round the back local i = sy * W + x return z < 0 and backOf(i) or i end return nil end -- the facade, extruded straight back over the footprint. Rows map -- against the measured ground line, not the grid's last row: the two -- differ only for furniture standing on open floor (see measure). if z < 0 or z >= D then return nil end local sy = ground - 1 - y local i = sy * W + x if y == 0 and not sp.inside[i] and sy > 0 and sp.inside[i - W] then -- the drawing's last row is the ground the building stands on, so -- its base course is one row up; without this the walls float a -- voxel over their own plot sy, i = sy - 1, i - W end if not sp.inside[i] then return nil end if z == D - 1 then if pr.recess[i] then return nil end return i end if z == 0 then return backOf(i) end return pr.interior[i] end at = sideDoors(at, sp, doors, W) return { at = at, W = W, ytop = ytop, zmin = ledge0 and -2 or 0, zmax = math.max(rz1, ledge0 and (D + 1) or 0) } end -- ------------------------------------------------------------------ emit -- -- Cull to the shell and merge. A run of faces collapses into one quad when -- its texels are the SAME (a flat-coloured strip, which is most of a side -- face) or ADJACENT IN THE ATLAS along the run (the drawing continuing -- across the face, which is most of a front face or a roof top). Both keep -- every texel exactly where the sprite put it. local function emit(m, sp, atlasW, atlasH) local W = m.W local quads = { voxels = 0, shell = 0 } local cell = {} -- (y, z, x) -> sprite pixel index local zmin, zmax, ytop = m.zmin, m.zmax, m.ytop local zn = zmax - zmin + 1 local function ci(x, y, z) if x < 0 or x >= W or y < 0 or y > ytop or z < zmin or z > zmax then return nil end return cell[(y * zn + (z - zmin)) * W + x] end for y = 0, ytop do Budget.tick() for z = zmin, zmax do local base = (y * zn + (z - zmin)) * W for x = 0, W - 1 do local v = m.at(x, y, z) cell[base + x] = v if v then quads.voxels = quads.voxels + 1 end end end end -- the shell: what survives hidden-face culling. Counted here rather than -- derived from the quads because it is the number -- tools/building_voxels.py checks this build against. for y = 0, ytop do Budget.tick() for z = zmin, zmax do for x = 0, W - 1 do if ci(x, y, z) and not (ci(x + 1, y, z) and ci(x - 1, y, z) and ci(x, y + 1, z) and ci(x, y - 1, z) and ci(x, y, z + 1) and ci(x, y, z - 1)) then quads.shell = quads.shell + 1 end end end end -- u/v of a run: `n` texels starting at sprite pixel `i`, stepping along -- the atlas when the run is a strip and standing still when it is flat. local function uvOf(i, strip, n) local x0 = sp.ax[i] local y0 = sp.ay[i] local x1 = strip and (x0 + n) or (x0 + 1) return (x0 + 0.05) / atlasW, (x1 - 0.05) / atlasW, (y0 + 0.05) / atlasH, (y0 + 1 - 0.05) / atlasH end local function put(c1, c2, c3, c4, uv, shade) quads[#quads + 1] = { c1, c2, c3, c4, uv = uv, shade = shade } end -- How far a run of exposed faces reaches from `x`, and whether it is a -- strip (texels marching along the atlas) or flat (one texel repeated). local function runX(y, z, dx, dy, dz, x) local i0 = ci(x, y, z) local strip, n = nil, 1 local cap = runCap(x) while n < cap do local nx = x + n local i = ci(nx, y, z) if not i or ci(nx + dx, y + dy, z + dz) then break end local prev = ci(nx - 1, y, z) if sp.ay[i] ~= sp.ay[prev] then break end local d = sp.ax[i] - sp.ax[prev] if d == 1 then if strip == false then break end strip = true elseif d == 0 then if strip == true then break end strip = false else break end n = n + 1 end return i0, strip == true, n end -- ---- faces along +-Z (the facade, the roof's rims): merge along x ---- for _, d in ipairs({ 1, -1 }) do local shade = d == 1 and SHADE.south or SHADE.north for y = 0, ytop do Budget.tick() for z = zmin, zmax do local x = 0 while x < W do if ci(x, y, z) and not ci(x, y, z + d) then local i, strip, n = runX(y, z, 0, 0, d, x) local u0, u1, v0, v1 = uvOf(i, strip, n) local zf = d == 1 and (z + 1) or z if d == 1 then put({ x, y, zf }, { x + n, y, zf }, { x + n, y + 1, zf }, { x, y + 1, zf }, { { u0, v1 }, { u1, v1 }, { u1, v0 }, { u0, v0 } }, shade) else put({ x + n, y, zf }, { x, y, zf }, { x, y + 1, zf }, { x + n, y + 1, zf }, { { u1, v1 }, { u0, v1 }, { u0, v0 }, { u1, v0 } }, shade) end x = x + n else x = x + 1 end end end end end -- ---- faces along +-Y (roof surfaces, undersides): merge along x ---- for _, d in ipairs({ 1, -1 }) do local shade = d == 1 and SHADE.top or SHADE.bottom for y = 0, ytop do Budget.tick() -- the underside of the bottom layer is the ground it stands on if not (d == -1 and y == 0) then for z = zmin, zmax do local x = 0 while x < W do if ci(x, y, z) and not ci(x, y + d, z) then local i, strip, n = runX(y, z, 0, d, 0, x) local u0, u1, v0, v1 = uvOf(i, strip, n) local yf = d == 1 and (y + 1) or y if d == 1 then put({ x, yf, z }, { x + n, yf, z }, { x + n, yf, z + 1 }, { x, yf, z + 1 }, { { u0, v0 }, { u1, v0 }, { u1, v1 }, { u0, v1 } }, shade) else put({ x, yf, z + 1 }, { x + n, yf, z + 1 }, { x + n, yf, z }, { x, yf, z }, { { u0, v1 }, { u1, v1 }, { u1, v0 }, { u0, v0 } }, shade) end x = x + n else x = x + 1 end end end end end end -- ---- faces along +-X (the flanks): merge along z, one texel each ---- for _, d in ipairs({ 1, -1 }) do for y = 0, ytop do for x = 0, W - 1 do local z = zmin while z <= zmax do local i = ci(x, y, z) if i and not ci(x + d, y, z) then local n, cap = 1, runCap(z) while n < cap and z + n <= zmax do local j = ci(x, y, z + n) if j ~= i or ci(x + d, y, z + n) then break end n = n + 1 end local u0, u1, v0, v1 = uvOf(i, false, n) local xf = d == 1 and (x + 1) or x if d == 1 then put({ xf, y, z + n }, { xf, y, z }, { xf, y + 1, z }, { xf, y + 1, z + n }, { { u0, v1 }, { u1, v1 }, { u1, v0 }, { u0, v0 } }, SHADE.side) else put({ xf, y, z }, { xf, y, z + n }, { xf, y + 1, z + n }, { xf, y + 1, z }, { { u0, v1 }, { u1, v1 }, { u1, v0 }, { u0, v0 } }, SHADE.side) end z = z + n else z = z + 1 end end end end end return quads end -- ------------------------------------------------------------- placement -- -- Does the template's tile grid sit at (tx, ty)? local function matches(S, t, tx, ty) local tiles = t.tiles for r = 1, #tiles do local row = tiles[r] for c = 1, #row do if S.tileAt[keyOf(tx + c - 1, ty + r - 1)] ~= row[c] then return false end end end return true end -- ------- which of a placement's faces a gate is entered by -- -- Read off the MAP, not authored: a gate entrance is a warp that lands in a -- gate house, and the face is whichever one of this placement the warp cell -- stands against. Thirty-odd doors fall out of two lines of geometry, and -- none of them can drift out of step with a map edit the way a hand list -- would. Three tests, each of them load-bearing: -- -- the destination is a GATE tileset -- what makes a building a gate house -- rather than a house with a back door. GATE and FOREST_GATE both, so -- the Viridian Forest pair count; the Safari rest houses are on GATE -- too and are excluded by the next test, their warps being drawn doors -- already. -- the cell is not already a door tile -- a south entrance IS drawn, as a -- doorway block in the facade, and lib/Structures.lua folds it up into -- the front face. Adding a second one there would fight it. -- the cell is WALKABLE -- the ROM gives an unreachable twin warp to -- several gates (a fence cell beside the real opening on Route 7 west -- and Route 16 east, a tree beside Route 6's), and a door on the wall -- behind a fence is a door into nothing. The reachable cells are the -- entrance, and two of them side by side are the gate's real two-cell -- opening, which comes out as the double door it always was. -- -- The south face is skipped whether or not it is drawn: it is the one face -- the drawing states in full, so anything it needs it already has. local function sideDoorsAt(map, tileset, tx, ty, bw, bh) -- through the module, NOT a global: `Game` is a local everywhere in the -- engine (`local Game = require("src.core.Game")` in a dozen files) and -- reading `_G.Game` came back nil every time -- which fails silently and -- exactly like the feature being off, because a nil map table is also -- what a headless build legitimately has. local ok, G = pcall(require, "src.core.Game") local defs = ok and G and G.data and G.data.maps local warps = map.def and map.def.warps if not (defs and warps and warps[1]) then return nil end if not Buildings.sideDoorCell(tileset.id) then return nil end local out = nil for _, w in ipairs(warps) do local dest = defs[w.destMap] if dest and dest.tileset and dest.tileset:find("GATE", 1, true) and map:isWalkableCell(w.x, w.y) and not map:isDoorTileCell(w.x, w.y) then -- the cell in the model's own pixels: a cell is two tiles, a tile -- eight pixels, and the placement's origin is (tx, ty) in tiles local lx, lz = w.x * 16 - tx * 8, w.y * 16 - ty * 8 local side = nil if lz == -16 and lx >= 0 and lx < bw * 8 then side = "n" elseif lx == -16 and lz >= 0 and lz < bh * 8 then side = "w" elseif lx == bw * 8 and lz >= 0 and lz < bh * 8 then side = "e" end if side then out = out or {} out[#out + 1] = { side = side, at = side == "n" and lx or lz } end end end if out then table.sort(out, function(a, b) if a.side ~= b.side then return a.side < b.side end return a.at < b.at end) end return out end -- The model cache key's door half. A template's doors belong to the -- PLACEMENT -- the same 6x4 block is the gate on four routes and the warps -- sit at different rows of it on each -- so two placements of one drawing -- are two models, and only placements that agree share one. local function doorKey(doors) if not doors then return "" end local parts = {} for i, d in ipairs(doors) do parts[i] = d.side .. d.at end return "#" .. table.concat(parts, ",") end -- Find every placement of every template for this map's tileset, build one -- model per template, and stamp it. Returns nothing; the quads land in -- S.objectQuads and the tiles are claimed so the volume path never boxes a -- building this module has already modelled. function Buildings.build(S, map, data, perRow) if not data then return end local tileset = map.tileset local s = profile() local list = s and s.buildings and s.buildings[tileset.id] if not list then return end local atlasW = tileset.imageWidth or 128 local atlasH = tileset.imageHeight or 48 local tw, th = map.def.width * 4, map.def.height * 4 local quads = S.objectQuads for index, t in ipairs(list) do if type(t.tiles) == "table" and #t.tiles > 0 then local bh, bw = #t.tiles, #t.tiles[1] local first = t.tiles[1][1] -- the model this placement stamps. Not hoisted out of the loops any -- more: a template's doors belong to the placement, so two hits of -- one drawing on the same map can be two models (see doorKey). local built = nil for ty = 0, th - bh do Budget.tick() for tx = 0, tw - bw do -- A placement never stamps into cells another template already -- claimed. Templates are matched independently, and one -- drawing can satisfy two grids: the Pokemon Tower's upper -- twelve rows on ROUTE_10 are a standard 6-cell block tile for -- tile, so `gabled_block_6x6` matched there and stood a whole -- second building behind the tower. First claim wins, so the -- list order below is the priority order -- the tower's own -- templates come first precisely so they take those cells. local free = S.tileAt[keyOf(tx, ty)] == first if free then for r = 0, bh - 1 do for c = 0, bw - 1 do if S.skip[keyOf(tx + c, ty + r)] then free = false break end end if not free then break end end end if free and matches(S, t, tx, ty) then do -- keyed per PLACEMENT once a gate's doors are in play (see -- doorKey): the drawing is shared, the openings are not local doors = not t.claimOnly and sideDoorsAt(map, tileset, tx, ty, bw, bh) or nil local key = tileset.id .. ":" .. index .. doorKey(doors) if not models[key] then if t.claimOnly then -- claim the cells, stamp nothing: the drawing here is -- the off-map half of a building another map models in -- full (the tower's roof rows on ROUTE_10 -- Lavender's -- placement composites them via topRows). Left to the -- detector they stood as a second half-building. models[key] = {} else local sp = read(t, data, perRow, Buildings.frontOnly(tileset.id)) if doors then readDoor(sp, data, perRow, Buildings.sideDoorCell(tileset.id)) end local pr = measure(sp, t) models[key] = emit(model(sp, pr, t, doors), sp, atlasW, atlasH) end end built = models[key] end Buildings.stamp(S, map, built, tx, ty, bw, bh, t) end end end end end end -- One placement: claim its tiles (so the detector leaves them alone and -- the mesher paints ground under them) and copy the model into place. -- -- Two template fields alter what a claim means, for a drawing that -- carries a STANDEE on its surface (Red's potted plant on the dining -- table). `keep` names tile ids the stamp must NOT claim: their authored -- pins stay live, so the standee scan still stands the object exactly as -- it always did. `support` is the model's top plane in voxels: the claim -- shape carries it as its height, which is what tells that scan the -- standee's shelf -- a plain claim stays at h = 0, and Structures treats -- a building claim with height as a full model (skip, never a second -- box; see its support branches). function Buildings.stamp(S, map, quads, tx, ty, bw, bh, t) local shape = { class = "building", h = (t and t.support) or 0, art = "building", flat = false, authored = true } local keep = nil if t and t.keep then keep = {} for _, id in ipairs(t.keep) do keep[id] = true end end -- the ground the building stands on: the commonest flat tile around its -- feet, so a house on a path keeps its path local votes, best, bestN = {}, nil, 0 local function vote(x, y) local k = keyOf(x, y) local ns = S.shapeAt[k] if ns and ns.flat and ns.class ~= "void" then local tile = S.tileAt[k] votes[tile] = (votes[tile] or 0) + 1 if votes[tile] > bestN then best, bestN = tile, votes[tile] end end end for c = 0, bw - 1 do vote(tx + c, ty - 1) vote(tx + c, ty + bh) end for r = 0, bh - 1 do vote(tx - 1, ty + r) vote(tx + bw, ty + r) end for r = 0, bh - 1 do for c = 0, bw - 1 do local k = keyOf(tx + c, ty + r) if keep and keep[S.tileAt[k]] then -- unclaimed by request: the tile keeps its pin (the plant's -- cutout pool) and the standee scan finds it there. Only the -- ground is set now, so the scan's own claim of these tiles has -- the building's floor to paint when no flat tile touches a -- cluster ringed by its own furniture. S.ground[k] = best or false else S.shapeAt[k] = shape S.skip[k] = true S.ground[k] = best or false end end end local mx, mz = tx * 8, ty * 8 local out = S.objectQuads for _, q in ipairs(quads) do out[#out + 1] = { { q[1][1] + mx, q[1][2], q[1][3] + mz }, { q[2][1] + mx, q[2][2], q[2][3] + mz }, { q[3][1] + mx, q[3][2], q[3][3] + mz }, { q[4][1] + mx, q[4][2], q[4][3] + mz }, uv = q.uv, shade = q.shade, -- placements only ever scan the BODY, so a building is always this -- map's own structure: the mesher's edge keep-rules must not eat -- the parts that poke past the boundary (an edge-row house's eave -- juts frontEave voxels into the neighbour's airspace, and the -- neighbour-body mask read that overhang as a ring scrap -- which -- opened the roof rim into the sky from across the seam) own = true, } end end -- What the models built so far cost, keyed "<tileset>:<index>": the voxel -- and shell counts tools/building_voxels.py checks this implementation -- against (Stage 5 of the methodology), and the quad count that ships. function Buildings.stats() local out = {} for key, quads in pairs(models) do out[key] = { voxels = quads.voxels, shell = quads.shell, quads = #quads } end return out end -- Drop the prebuilt models (hot reload, or a mod shadowing the profile). function Buildings.invalidate() spec = nil models = {} frontSets = {} end return Buildings