Repository navigation
Expand file tree
/
Copy pathmain.lua
More file actions
2018 lines (1910 loc) · 88.2 KB
/
Copy pathmain.lua
File metadata and controls
2018 lines (1910 loc) · 88.2 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
-- Better Buildings -- FireRed-styled exteriors for Kanto's shops, gyms,
-- landmarks and ordinary town buildings.
--
-- These buildings are drawn from tiles the whole overworld shares: the roof
-- diamond (tile $12) is in eight blocks, the brick course (tile $0A) in
-- nineteen, and every gym in Kanto is the same three facade blocks repeated
-- -- which is why the four-wide gyms read "GYM GYM". Repainting any of it
-- in place would re-skin every house in Kanto, so this mod takes the
-- surgical route instead:
--
-- 1. transforms.lua grows the player's own extracted atlas downward and
-- paints original drawings into it (ids 96 and up).
-- 2. New blocks are appended to the OVERWORLD tileset, fitted to the exact
-- vanilla arrangements each drawing stands in for.
-- 3. Maps are repointed only after a complete chassis and, for landmarks,
-- its destination warp match. Unrelated lookalikes such as Saffron's
-- Fighting Dojo keep the art they always had.
--
-- Footprint, collision and the warp cell are preserved to the pixel: each
-- doorway still sits in the cell it always did and its bottom-left tile is
-- still the tile the engine reads for the warp, so every map's warp table
-- keeps working untouched. That covers the doors; the route gates and the
-- back entrances are opened by the tile in FRONT of the player instead, so
-- the new tiles inherit that facing too -- see CARPET_DIRS below.
--
-- Voxel mode reads the atlas through Assets.resolve, so it sees the grown
-- image. The drawings normally keep the voxel detector's natural curved
-- roof profiles. Only Fuchsia Gym and Fuchsia's Center need exact templates:
-- they touch solid scenery and would otherwise be flood-filled into it.
local mod = ...
local ATLAS_W, ATLAS_H = 128, 48 -- the vanilla overworld atlas
local PER_ROW = 16
local BASE_TILES = (ATLAS_W / 8) * (ATLAS_H / 8) -- 96
local BAND_ROWS = 8 -- one drawing band, in tile rows
local SHOP_ROW = ATLAS_H / 8 -- 6: the Center/Mart band
-- The gym roster, in the SAME ORDER as draw_gyms.GYMS -- band i of the
-- sheet holds gym i, so reordering one list without the other paints
-- Cerulean's roof on Pewter's gym. tools/emit_transform.py checks the two
-- agree at authoring time.
--
-- `blocks` is the building's width in map blocks; `mirror` marks the one gym
-- (Fuchsia) whose vanilla building puts the doorway at the building's LEFT
-- edge, in the second cell of that block rather than the first.
local GYMS = {
{ warp = "PEWTER_GYM", blocks = 3, mirror = false },
{ warp = "CERULEAN_GYM", blocks = 4, mirror = false },
{ warp = "VERMILION_GYM", blocks = 3, mirror = false },
{ warp = "CELADON_GYM", blocks = 4, mirror = false },
{ warp = "FUCHSIA_GYM", blocks = 3, mirror = true },
{ warp = "SAFFRON_GYM", blocks = 4, mirror = false },
{ warp = "CINNABAR_GYM", blocks = 3, mirror = false },
{ warp = "VIRIDIAN_GYM", blocks = 3, mirror = false },
}
local GYM_BAND = SHOP_ROW + BAND_ROWS -- 14: the first gym band
-- ------- the ordinary buildings
--
-- Three chassis carry every house, shop and gate in Kanto, and the towns just
-- repeat them: a one-block-tall cottage terrace (blocks 2/48 + 9* + 3), a
-- gabled house (56/57 over 60/61 with a door, or 16/0 without) and the
-- flat-roofed civic block (32/13*/33 over 124 or 55, 58 or 125, and 126).
-- That last one is the SAME chassis the Center and the Mart are built from,
-- which is why re-skinning those two left everything beside them looking
-- wrong.
--
-- Because the game assembles those at any width, this mod ships KITS rather
-- than buildings: interchangeable one-block modules that tile into a run.
-- That is also the only way per-town styles fit. A map's block ids are
-- capped at 255 (src/mods/Schemas.lua), the landmarks above already reach
-- 193, and one drawing per building would want more than a hundred ids.
-- Module reuse, borrowed kit parts and painting the regional defaults into
-- their vanilla slots keep the expanded roster inside the limit.
--
-- ORDER MATCHES draw_houses.py. Band positions are computed from a style's
-- index in these lists, so reordering one list without the other hands
-- Lavender's slate to Viridian's cottages; tools/emit_transform.py checks the
-- two agree at authoring time. `parts` is the subset of a kit the towns on
-- that style actually place -- drawing a module nobody places is free, but
-- APPENDING one is not.
local COTTAGES = {
{ key = "viridian", parts = "door right" },
{ key = "pewter", parts = "door right" },
{ key = "cerulean", parts = "door plain mid right" },
{ key = "lavender", parts = "door right" },
{ key = "fuchsia", parts = "door plain right" },
{ key = "celadon", parts = "plain mid right" },
{ key = "kanto", parts = "door plain mid right" },
}
-- one entry per DRAWING, two to a band. A "plain" entry is the doorless
-- scenery variant of the style named before the `_plain` suffix and shares
-- its roof blocks -- vanilla draws both variants under the same roof pair.
--
-- `base` is the other direction: a style that claims only some of the kit and
-- borrows the rest from the style named. Pallet's two houses are one drawing
-- in two paints -- the player's roof red, the rival's blue -- and are
-- identical below the eave, so the rival appends a roof and nothing else.
local HOUSES = {
{ key = "pallet", parts = "roof door" },
{ key = "fuchsia", parts = "roof door" },
{ key = "saffron", parts = "roof door" },
{ key = "saffron_plain", parts = "plain" },
{ key = "kanto", parts = "roof door" },
{ key = "pallet_rival", parts = "roof", base = "pallet" },
}
-- `base` works here exactly as it does for the houses above: a style that
-- claims only some of the kit and takes the rest from the style named. It is
-- what makes Celadon's named buildings affordable. Most claim one doorway
-- bay and borrow the neighbours' roof. The Game Corner and Mansion each
-- claim repeating roof and wall bays too. Hotel and Diner use Celadon's
-- standard entrance, and the residential row repeats one sealed section, to
-- keep both landmarks under the block ceiling.
local FLATS = {
{ key = "town", parts = "section door_r door_l" },
{ key = "port", parts = "plain door_r right" },
{ key = "celadon", parts = "plain door_r door_l right" },
-- Saffron never puts a doorway in a bay's LEFT cell
{ key = "saffron", parts = "section door_r" },
{ key = "celadon_row", parts = "section" },
{ key = "celadon_corner", parts = "roof facade door_l", base = "celadon" },
{ key = "celadon_hotel", parts = "", base = "celadon" },
{ key = "celadon_diner", parts = "", base = "celadon" },
{ key = "celadon_mansion", parts = "roof facade door_l", base = "celadon" },
-- Kanto's route gates. Same chassis as everything above, which is the
-- whole problem: in a town's shopfront kit the one building you WALK
-- THROUGH comes out as a sealed shop. `gate_ends` carries the two cap
-- modules that put the stepped entrance in the OUTER cell -- the end the player
-- actually steps in at -- in a band of its own because the kit above
-- already spends all four columns.
{ key = "gate", parts = "section door_r door_l" },
{ key = "gate_ends", parts = "end_w end_e", base = "gate" },
{ key = "vermilion", parts = "roof", base = "port" },
}
local COT_ROWS, HOUSE_ROWS, FLAT_ROWS = 4, 6, 8 -- one band, in tile rows
local COT_BAND = GYM_BAND + BAND_ROWS * #GYMS -- 78
local HOUSE_BAND = COT_BAND + COT_ROWS * #COTTAGES -- 106
local FLAT_BAND = HOUSE_BAND
+ HOUSE_ROWS * math.ceil(#HOUSES / 2) -- 124
local DEPT_ROWS = 4
local DEPT_BAND = FLAT_BAND + FLAT_ROWS * #FLATS -- 204
local OAK_ROWS = 4
local OAK_BAND = DEPT_BAND + DEPT_ROWS -- 208
-- One dormant row belongs to the Better Paths and Grass companion. This
-- mod owns the resolved OVERWORLD atlas, so the terrain art has to live here
-- to keep a second whole-atlas override from hiding the building bands. No
-- Better Buildings block uses these ids on its own.
local TERRAIN_ROWS = 1
local TERRAIN_BAND = OAK_BAND + OAK_ROWS
local GROWN_H = (TERRAIN_BAND + TERRAIN_ROWS) * 8
-- which tile column of a band each module is drawn in
local COT_COL = { door = 0, plain = 4, mid = 8, right = 12 }
-- the two cap modules live in a band of their own, so they take the first
-- two columns rather than colliding with the doorways they never share a
-- band with
local FLAT_COL = { plain = 0, door_r = 4, door_l = 8, right = 12,
end_w = 0, end_e = 4 }
local DEPT_COL = { roof = 0, upper = 4, ground = 8, door = 12 }
local OAK_COL = { roof = 0, side = 4, door = 8 }
-- Kanto's towns, and the style each one builds in. Anything not listed --
-- the route houses, Celadon's terrace, Cinnabar's civic pair -- takes the
-- regional default.
local COTTAGE_STYLE = {
VIRIDIAN_CITY = "viridian", PEWTER_CITY = "pewter",
CERULEAN_CITY = "cerulean", LAVENDER_TOWN = "lavender",
FUCHSIA_CITY = "fuchsia", CELADON_CITY = "celadon",
}
local HOUSE_STYLE = {
PALLET_TOWN = "pallet", FUCHSIA_CITY = "fuchsia", SAFFRON_CITY = "saffron",
}
-- A house that overrides its town's style, keyed by what is behind its door.
-- Pallet Town places the same gabled house twice, and the only thing that
-- tells the player's from the rival's is which one you can walk into and find
-- your mother in -- so the warp is what this reads, exactly the way the gyms
-- are found by their `*_GYM` warps rather than by their blocks.
local HOUSE_STYLE_BY_WARP = {
REDS_HOUSE_1F = "pallet", -- the player's own: a red roof
BLUES_HOUSE = "pallet_rival", -- the rival's, across the street: blue
}
local FLAT_STYLE = {
CELADON_CITY = "celadon", SAFFRON_CITY = "saffron",
VERMILION_CITY = "vermilion", CERULEAN_CITY = "port",
}
-- The style a town's civic kit switches to for a run with no doorway in it.
--
-- Celadon is nineteen buildings on one chassis, and thirteen of them cannot
-- be entered at all -- so drawing every one as retail frontage turned the
-- whole city into the department store repeated down both sides of the
-- street. A run with a doorway module is somewhere you can go in; a run
-- without is where people live. That is a distinction the vanilla map
-- already draws, so it needs no per-building list to read it.
local FLAT_RESIDENTIAL = { celadon = "celadon_row" }
-- A civic run that takes a style of its own, keyed by what is behind its
-- door -- the same way the gyms are found by their `*_GYM` warps and
-- Pallet's two houses are told apart. The Game Corner owns both its doorway
-- plus repeating roof and wall panels; Mansion does the same. Hotel and
-- Diner use the standard Celadon doorway, and residential buildings repeat
-- one sealed section; every remaining module is borrowed rather than added.
local FLAT_STYLE_BY_WARP = {
GAME_CORNER = "celadon_corner",
CELADON_HOTEL = "celadon_hotel",
CELADON_DINER = "celadon_diner",
CELADON_MANSION_1F = "celadon_mansion",
}
local DEFAULT_COTTAGE, DEFAULT_HOUSE, DEFAULT_FLAT = "kanto", "kanto", "town"
-- the vanilla blocks each chassis is assembled from
local COT_LEFT = { [2] = "door", [48] = "plain" }
local COT_MID, COT_RIGHT = 9, 3
local HOUSE_ROOF_L, HOUSE_ROOF_R = 56, 57
local HOUSE_FACE = { [60] = "door", [16] = "plain" } -- keyed by left block
local HOUSE_FACE_R = { door = 61, plain = 0 }
local FLAT_LEFT = { [124] = "door_r", [55] = "plain" }
local FLAT_MID = { [58] = "door_l", [125] = "plain" }
local FLAT_RIGHT = 126
local FLAT_STOREY_L, FLAT_STOREY_M, FLAT_STOREY_R = 104, 127, 105
local FLAT_ROOF_L, FLAT_ROOF_M, FLAT_ROOF_R = 32, 13, 33
-- Where the DEFAULT style's modules are painted: straight onto the vanilla
-- blocks they stand in for, rather than into ids of their own.
--
-- Every Kanto map that places one of these chassis is converted above, so
-- once this mod has run not one map in the region still points at 2/48/9/3,
-- 56/57/60/61 or 124 -- which makes appending a copy of each pure waste of
-- the scarcest thing the mod has. Painting the default kit in place costs
-- nine ids less, and is the more correct answer besides: a map that places a
-- vanilla cottage gets the improved art without this mod having to have
-- known about that map.
--
-- Only the DEFAULT styles may do this. A town style painted onto a shared
-- vanilla block would put Lavender's slate on every route house in Kanto.
-- And only blocks nothing else reads: 16 and 0 carry the doorless house but
-- also the gyms and the grass, so `kanto`'s house claims neither.
local COT_REUSE = { kanto = { door = 2, plain = 48, mid = 9, right = 3 } }
local HOUSE_REUSE = {
kanto = { roofL = 56, roofR = 57, doorL = 60, doorR = 61 },
}
local FLAT_REUSE = { town = { doorR = 124 } }
-- The gate kit is painted the same way, but onto ids DISCOVERED rather than
-- named -- because by the time it was written there were none left to name.
-- The landmark and town layers spend every id up to the 255 ceiling, so a
-- gate style that appended its six modules would push the ordinary-building
-- layer over the edge and cost Kanto every house kit it has.
--
-- What is left is the blockset's own slack: Kanto's OVERWORLD tileset ships
-- 128 blocks and the region's maps place only 121 of them. The remaining
-- seven have no placement anywhere to disturb, so the gate kit paints into
-- them. Discovered from the MERGED maps rather than hard-coded, so a map
-- mod that starts placing one of those blocks simply leaves fewer spares and
-- the gates keep the civic art they have today.
local GATE_PARTS = { "roofA", "facA", "doorR", "doorL", "endW", "endE" }
-- Kanto's gates, found the way every other building here is found: by what
-- is behind the door. Every one of the fourteen leads to a map named for
-- it, and nothing else in the region is.
local function isGateMap(id)
return type(id) == "string"
and (id:match("_GATE$") ~= nil or id:match("_GATE_[12]F$") ~= nil)
end
-- A map block id is validated as a byte, so 255 is the last id this mod can
-- hand a map. Vanilla spends 128 and the landmarks 70; the kits use all 58
-- slots that remain, so a tileset mod loaded ahead of this one can leave the
-- ordinary-building layer without room.
local BLOCK_CEILING = 256
-- the vanilla shop pair this mod stands in for: a 2x2 arrangement of blocks
-- whose south-east corner carries the POKe (114) or MART (115) sign
local SHOPS = {
center = { se = 114, sw = 124, nw = 32, ne = 33 },
mart = { se = 115, sw = 124, nw = 32, ne = 33 },
}
-- where each shop drawing sits in its band: the Center in tile columns 0-7,
-- the Mart in columns 8-15
local SHOP_COLUMN = { center = 0, mart = 8 }
-- The vanilla gym, left to right along its facade row: a left cap, one or
-- more sign blocks, a right cap. The plain caps (16/18) carry the doorway
-- on the right; the house-style caps (124/126) carry it on the left, which
-- is how Fuchsia's gym is built.
local GYM_LEFT = { [16] = true, [124] = true }
local GYM_MID = { [17] = true }
local GYM_RIGHT = { [18] = true, [126] = true }
-- and the roof row above it
local GYM_ROOF_LEFT = { [12] = true, [32] = true }
local GYM_ROOF_MID = { [13] = true }
local GYM_ROOF_RIGHT = { [14] = true, [33] = true }
local function tileId(row, col) return row * PER_ROW + col end
-- Stable inter-mod seam for Better Paths and Grass. Four variants are
-- available per floor material; tall grass intentionally keeps one canonical
-- id so the engine's grassTile contract, encounter overlay and voxel tuft
-- classifier continue to agree.
local terrainTiles = {
lawn = {}, grass = {}, path = {}, tall = tileId(TERRAIN_BAND, 12),
}
for col = 0, 3 do
terrainTiles.lawn[#terrainTiles.lawn + 1] = tileId(TERRAIN_BAND, col)
terrainTiles.grass[#terrainTiles.grass + 1] = tileId(TERRAIN_BAND, 4 + col)
terrainTiles.path[#terrainTiles.path + 1] = tileId(TERRAIN_BAND, 8 + col)
end
-- Fuchsia's Center touches the city's solid scenery, so voxel mode needs a
-- tile grid unique to that one placement. One copied south-west block is
-- enough to make the complete 8x8-tile grid unique; its pixels are identical
-- to the normal Center and only its tile ids differ. The other three blocks
-- are shared with every Center, reclaiming exactly the three byte-sized block
-- ids Professor Oak's fixed landmark needs below.
local FUCHSIA_CENTER_PAD = 12
local FUCHSIA_CENTER_LEFT_TOP = GYM_BAND
local function fuchsiaCenterTile(row, col)
return tileId(FUCHSIA_CENTER_LEFT_TOP + row, FUCHSIA_CENTER_PAD + col)
end
local function fuchsiaCenterBlock(row, col)
local out = {}
for r = 0, 3 do
for c = 0, 3 do
out[#out + 1] = fuchsiaCenterTile(row + r, col + c)
end
end
return out
end
-- the 16 tile ids of one 4x4 block, reading rows then columns
local function blockAt(row, col)
local out = {}
for r = 0, 3 do
for c = 0, 3 do out[#out + 1] = tileId(row + r, col + c) end
end
return out
end
-- The doorway's collision tile: the bottom-left 8x8 tile of the doorway
-- cell, which is what pokered (and this engine) reads for walkability, the
-- door animation and the warp. It is the last row of the facade block row,
-- so seven tile rows down from the band's top.
local function doorTile(top, col) return tileId(top + 7, col) end
-- ------- 1. grow the tileset
local tileset = mod.content.tilesets:get("OVERWORLD")
if not tileset then
mod.log:warn("the OVERWORLD tileset is not in the merged view -- is a "
.. "total conversion loaded ahead of this mod? nothing re-skinned")
return
end
if tileset.imageWidth ~= ATLAS_W or tileset.imageHeight ~= ATLAS_H then
mod.log:warn("the OVERWORLD atlas is %sx%s, not the %dx%d layout these "
.. "drawings were authored against -- another tileset mod is probably "
.. "loaded ahead of this one; nothing re-skinned",
tostring(tileset.imageWidth), tostring(tileset.imageHeight),
ATLAS_W, ATLAS_H)
return
end
if type(tileset.blocks) ~= "table" or #tileset.blocks == 0 then
mod.log:warn("the OVERWORLD tileset carries no blocks -- nothing re-skinned")
return
end
-- block ids are 0-based indices into `blocks`, so the first id we append is
-- the current length. Reading it here (rather than hard-coding 128) keeps
-- us correct behind another mod that already appended blocks of its own.
--
-- The list is copied rather than extended through `__append`, because the
-- default kits are painted over blocks that already exist (see *_REUSE) and
-- a wrapper can only reach the end. Building on the merged view we just
-- read leaves every block another mod contributed exactly where it put it.
local blocksOut = {}
for i = 1, #tileset.blocks do blocksOut[i] = tileset.blocks[i] end
local firstBlockId = #blocksOut
-- Collision is a property of a tile id, but maps are assembled from blocks.
-- Every generated block therefore inherits the four cell roles of the
-- vanilla block it replaces. This matters for more than visible doors: a
-- few exterior blocks carry walk-behind / edge passage cells, and another
-- supported ROM or an earlier tileset mod may put those roles on a different
-- tile id. Registering only the doorway drawn by this mod silently turns
-- such a passage into a wall even though the map footprint never changed.
local roleAdds = { walkable = {}, doorTiles = {}, warpTiles = {} }
local roleSeen = { walkable = {}, doorTiles = {}, warpTiles = {} }
local sourceRoles = { walkable = {}, doorTiles = {}, warpTiles = {} }
for name, set in pairs(sourceRoles) do
for _, tile in ipairs(tileset[name] or {}) do set[tile] = true end
end
local function addRole(name, tile)
if type(tile) == "number" and not roleSeen[name][tile] then
roleSeen[name][tile] = true
roleAdds[name][#roleAdds[name] + 1] = tile
end
end
-- A second, quieter kind of collision role, and the one that seals a route
-- gate if it is missed. ExtraWarpCheck (pokered IsWarpTileInFrontOfPlayer,
-- src/world/Warp.lua) fires a warp the player is STANDING on from the tile
-- in FRONT of them, matched against field.warpCarpets.tiles for the facing
-- direction -- so the tile that opens the door is one the player never
-- steps on and no tileset role names. Every Kanto route gate is entered
-- that way (they have no door cell at all: you walk into the building's
-- side), and so is every back entrance -- Cerulean's two cut-through
-- houses, Celadon Mansion's rear stair, Fuchsia's Good Rod house. Repaint
-- such a bay and its collision, its footprint and its warp table are all
-- still correct while the building has quietly become unenterable.
--
-- The fix is the role rule again, one level out: a new tile drawn where a
-- vanilla carpet tile stood inherits that tile's direction.
local CARPET_DIRS = { "up", "down", "left", "right" }
local warpCarpets = mod.content.field and mod.content.field:get("warpCarpets")
local carpetAdds, carpetSeen, sourceCarpets = {}, {}, {}
local anyCarpet = false
for _, dir in ipairs(CARPET_DIRS) do
carpetAdds[dir], carpetSeen[dir], sourceCarpets[dir] = {}, {}, {}
local tiles = type(warpCarpets) == "table" and warpCarpets.tiles or nil
for _, tile in ipairs(type(tiles) == "table" and tiles[dir] or {}) do
sourceCarpets[dir][tile] = true
end
end
local function addCarpet(dir, tile)
if type(tile) == "number" and not carpetSeen[dir][tile] then
carpetSeen[dir][tile] = true
carpetAdds[dir][#carpetAdds[dir] + 1] = tile
anyCarpet = true
end
end
-- The engine reads the bottom-left tile of each 2x2-tile cell. Accepting a
-- list of source blocks lets a repeated kit module preserve the union of the
-- compatible vanilla positions it replaces (for example a civic middle bay
-- on either the ground floor or the storey above it).
local CELL_TILES = { 5, 7, 13, 15 }
local function inheritRoles(block, sources)
if type(sources) == "number" then sources = { sources } end
for _, sourceId in ipairs(sources or {}) do
local source = tileset.blocks[sourceId + 1]
if type(source) == "table" then
for _, pos in ipairs(CELL_TILES) do
local oldTile, newTile = source[pos], block[pos]
for name, set in pairs(sourceRoles) do
if set[oldTile] then addRole(name, newTile) end
end
for _, dir in ipairs(CARPET_DIRS) do
if sourceCarpets[dir][oldTile] then addCarpet(dir, newTile) end
end
end
end
end
return block
end
local function addDoor(tile)
addRole("walkable", tile)
addRole("doorTiles", tile)
addRole("warpTiles", tile)
end
local function append(block, sources)
inheritRoles(block, sources)
blocksOut[#blocksOut + 1] = block
return #blocksOut - 1
end
-- `id` is the vanilla block this drawing stands in for, or nil to append
local function put(id, block, sources)
sources = sources or id
if id == nil then return append(block, sources) end
inheritRoles(block, sources)
blocksOut[id + 1] = block
return id
end
-- how many ids each layer will spend, so the ceiling can be checked before
-- any of it is written rather than discovered as a schema error on patch.
-- A module the style paints onto a vanilla block spends none.
local function partCount(roster, cost, reuse)
local n = 0
for _, entry in ipairs(roster) do
local reused = (reuse or {})[entry.key] or {}
for part in entry.parts:gmatch("%S+") do
for _, slot in ipairs(cost[part] or { part }) do
if reused[slot] == nil then n = n + 1 end
end
end
end
return n
end
-- ------- the gate kit's ids: the blockset's own unplaced slack
--
-- Run before the budget below, because a gate module that finds a spare id
-- costs the accounting nothing: `partCount` skips a part the *_REUSE table
-- already answers for. Ids below `firstBlockId` only -- the appended range
-- is this mod's own and every id in it is spoken for.
local placedIds = {}
for _, map in mod.content.maps:each() do
if map.tileset == "OVERWORLD" then
for _, id in ipairs(map.blocks or {}) do placedIds[id] = true end
if type(map.borderBlock) == "number" then placedIds[map.borderBlock] = true end
end
end
-- a spare this mod is about to paint for another style is not spare
for _, reuse in pairs(FLAT_REUSE) do
for _, id in pairs(reuse) do placedIds[id] = true end
end
for _, reuse in pairs(COT_REUSE) do
for _, id in pairs(reuse) do placedIds[id] = true end
end
for _, reuse in pairs(HOUSE_REUSE) do
for _, id in pairs(reuse) do placedIds[id] = true end
end
local spareIds = {}
for id = 0, firstBlockId - 1 do
if not placedIds[id] then spareIds[#spareIds + 1] = id end
end
local gateReuse = nil
if #spareIds >= #GATE_PARTS then
gateReuse = {}
for i, part in ipairs(GATE_PARTS) do gateReuse[part] = spareIds[i] end
FLAT_REUSE.gate = { roofA = gateReuse.roofA, facA = gateReuse.facA,
doorR = gateReuse.doorR, doorL = gateReuse.doorL }
FLAT_REUSE.gate_ends = { endW = gateReuse.endW, endE = gateReuse.endE }
else
-- No slack: the gates keep the civic art of the town they stand outside.
-- They are still enterable -- the passage is what is missing, not the way
-- in -- so this is a look, not a regression.
mod.log:info("the blockset has %d unplaced ids and the gate kit wants %d "
.. "-- Kanto's gates keep their town's civic art", #spareIds, #GATE_PARTS)
end
-- shops, Fuchsia's one unique Center block, Department Store and Oak's Lab
local landmarkBlocks = 4 * 2 + 1 + 4 + 3
for _, gym in ipairs(GYMS) do landmarkBlocks = landmarkBlocks + 2 * gym.blocks end
-- each part, and the id slots it spends -- the same names the *_REUSE tables
-- and the kit tables below are keyed by
local houseBlocks = partCount(COTTAGES, {}, COT_REUSE)
+ partCount(HOUSES, { roof = { "roofL", "roofR" },
door = { "doorL", "doorR" },
plain = { "plainL", "plainR" } }, HOUSE_REUSE)
+ partCount(FLATS, { plain = { "roofA", "facA" },
right = { "roofR", "facR" },
section = { "roofA", "facA" },
roof = { "roofA" },
facade = { "facA" },
end_w = { "endW" }, end_e = { "endE" },
door_r = { "doorR" }, door_l = { "doorL" } },
FLAT_REUSE)
if firstBlockId + landmarkBlocks > BLOCK_CEILING then
mod.log:warn("the OVERWORLD tileset already carries %d blocks, and a map "
.. "block id is validated as a byte -- there is no room left to append "
.. "the %d this mod needs; nothing re-skinned",
firstBlockId, landmarkBlocks)
return
end
-- The houses are the part that goes if the room runs out: the landmarks are
-- what the mod is for, and half a town re-skinned still beats none of it.
local roomForHouses =
firstBlockId + landmarkBlocks + houseBlocks <= BLOCK_CEILING
if not roomForHouses then
mod.log:warn("no room under the 255-block id cap for the house kits (%d "
.. "ids wanted, %d free) -- the Centers, Marts and gyms are re-skinned "
.. "and every other building keeps its vanilla art",
houseBlocks, BLOCK_CEILING - firstBlockId - landmarkBlocks)
end
-- the shop pair: four blocks each, laid out like the vanilla 2x2
local shopIds = {}
for _, kind in ipairs({ "center", "mart" }) do
local col = SHOP_COLUMN[kind]
local vanilla = SHOPS[kind]
shopIds[kind] = {
nw = append(blockAt(SHOP_ROW, col), vanilla.nw),
ne = append(blockAt(SHOP_ROW, col + 4), vanilla.ne),
sw = append(blockAt(SHOP_ROW + 4, col), vanilla.sw),
se = append(blockAt(SHOP_ROW + 4, col + 4), vanilla.se),
}
addDoor(doorTile(SHOP_ROW, col + 2))
end
-- the gyms: two rows of `blocks` blocks each, from that gym's own band
local gymIds, gymByWarp = {}, {}
for i, gym in ipairs(GYMS) do
local top = GYM_BAND + (i - 1) * BAND_ROWS
local ids = { [0] = {}, [1] = {} }
for r = 0, 1 do
for c = 0, gym.blocks - 1 do
local source
if r == 0 then
source = (c == 0) and (gym.mirror and 32 or 12)
or (c == gym.blocks - 1) and (gym.mirror and 33 or 14) or 13
else
source = (c == 0) and (gym.mirror and 124 or 16)
or (c == gym.blocks - 1) and (gym.mirror and 126 or 18) or 17
end
ids[r][c] = append(blockAt(top + r * 4, c * 4), source)
end
end
gymIds[gym.warp] = ids
gymByWarp[gym.warp] = gym
-- the doorway is the last block's first cell, or -- mirrored -- the first
-- block's second cell
addDoor(doorTile(top, gym.mirror and 2 or (gym.blocks - 1) * 4))
end
-- One block used only by the Center placement on FUCHSIA_CITY. A single
-- unique quarter makes the complete tile grid unambiguous to the voxel
-- profile without spending four ids on pixel-identical copies.
local fuchsiaCenterIds = {
nw = shopIds.center.nw,
ne = shopIds.center.ne,
sw = append(fuchsiaCenterBlock(4, 0), SHOPS.center.sw),
se = shopIds.center.se,
}
addDoor(fuchsiaCenterTile(7, 2))
-- Celadon's fixed four-wide, four-row Department Store. The source map
-- repeats one middle roof, upper-storey and doorway block, so four authored
-- blocks are enough for a complete landmark: roof, upper bay, outer display
-- and entrance. Each of the two entrance placements keeps its own warp.
local departmentIds = {
roof = append(blockAt(DEPT_BAND, DEPT_COL.roof),
{ FLAT_ROOF_L, FLAT_ROOF_M, FLAT_ROOF_R }),
upper = append(blockAt(DEPT_BAND, DEPT_COL.upper),
{ FLAT_STOREY_L, FLAT_STOREY_M, FLAT_STOREY_R }),
ground = append(blockAt(DEPT_BAND, DEPT_COL.ground), { 55, 115 }),
door = append(blockAt(DEPT_BAND, DEPT_COL.door), 58),
}
addDoor(tileId(DEPT_BAND + 3, DEPT_COL.door))
-- Professor Oak's fixed three-wide, two-row Pallet Town landmark: a grey
-- research block under a clean flat grey-tiled roof. One roof bay repeats
-- across the front, matching dark-glazed window bays flank the entrance, and
-- the original OAKS_LAB warp remains in the left cell of the middle facade
-- block.
local oakLabIds = {
roof = append(blockAt(OAK_BAND, OAK_COL.roof), { 12, 13, 14 }),
side = append(blockAt(OAK_BAND, OAK_COL.side), { 16, 0 }),
door = append(blockAt(OAK_BAND, OAK_COL.door), 58),
}
addDoor(tileId(OAK_BAND + 3, OAK_COL.door))
-- the cottage kits: four interchangeable modules across one 32px band
local cottageIds = {}
local COT_SOURCE = { door = 2, plain = 48, mid = COT_MID, right = COT_RIGHT }
if roomForHouses then
for i, st in ipairs(COTTAGES) do
local top = COT_BAND + (i - 1) * COT_ROWS
local ids, reuse = {}, COT_REUSE[st.key] or {}
for part in st.parts:gmatch("%S+") do
ids[part] = put(reuse[part], blockAt(top, COT_COL[part]), COT_SOURCE[part])
if part == "door" then
-- one block tall, so the doorway cell is the block's own second
-- row rather than a facade row four rows down
addDoor(tileId(top + 3, COT_COL[part] + 2))
end
end
cottageIds[st.key] = ids
end
end
-- The gabled house. Its roof block's TOP HALF is walkable in vanilla -- the
-- player walks behind the building along there -- so those two tile rows are
-- copied from the vanilla block rather than painted: new art there would both
-- wall the player out and stamp one town's ground across every other's.
local function houseRoofBlock(row, col, vanillaId)
local src = tileset.blocks[vanillaId + 1]
if type(src) ~= "table" or #src < 16 then return nil end
local out = {}
for i = 1, 8 do out[i] = src[i] end
for r = 0, 1 do
for c = 0, 3 do out[#out + 1] = tileId(row + r, col + c) end
end
return out
end
local houseIds = {}
if roomForHouses then
for i, st in ipairs(HOUSES) do
local row = HOUSE_BAND + math.floor((i - 1) / 2) * HOUSE_ROWS
local col = ((i - 1) % 2) * 8
local style = st.key:match("^(.-)_plain$") or st.key
local ids = houseIds[style] or {}
local reuse = HOUSE_REUSE[style] or {}
houseIds[style] = ids
for part in st.parts:gmatch("%S+") do
if part == "roof" then
ids.roofL = houseRoofBlock(row, col, HOUSE_ROOF_L)
ids.roofR = houseRoofBlock(row, col + 4, HOUSE_ROOF_R)
ids.roofL = ids.roofL and put(reuse.roofL, ids.roofL, HOUSE_ROOF_L)
ids.roofR = ids.roofR and put(reuse.roofR, ids.roofR, HOUSE_ROOF_R)
else
-- the drawing's rows 2..5 are the facade block row
local sourceL = (part == "door") and 60 or 16
local sourceR = (part == "door") and 61 or 0
ids[part .. "L"] = put(reuse[part .. "L"],
blockAt(row + 2, col), sourceL)
ids[part .. "R"] = put(reuse[part .. "R"],
blockAt(row + 2, col + 4), sourceR)
if part == "door" then
addDoor(tileId(row + 5, col + 2))
end
end
end
end
-- and the borrowed halves: a style with a `base` claimed only part of the
-- kit, and takes the rest -- ids, not new blocks -- from the style it is a
-- repaint of. The doorway tile comes with them, so the rival's door still
-- warps from the cell it always did.
for _, st in ipairs(HOUSES) do
local ids, from = houseIds[st.key], st.base and houseIds[st.base]
if ids and from then
for part, id in pairs(from) do
if ids[part] == nil then ids[part] = id end
end
end
end
end
-- the flat-roofed civic kits. A doored module borrows its roof from the
-- plain one beside it, so a bay with a door and a bay without share a roof
-- and the kit costs six ids rather than eight.
local flatIds = {}
if roomForHouses then
for i, st in ipairs(FLATS) do
local top = FLAT_BAND + (i - 1) * FLAT_ROWS
local ids, reuse = {}, FLAT_REUSE[st.key] or {}
for part in st.parts:gmatch("%S+") do
if part == "plain" then
ids.roofA = put(reuse.roofA, blockAt(top, FLAT_COL.plain),
{ FLAT_ROOF_L, FLAT_ROOF_M })
ids.facA = put(reuse.facA, blockAt(top + 4, FLAT_COL.plain),
{ 55, 125, FLAT_STOREY_L, FLAT_STOREY_M })
elseif part == "right" then
ids.roofR = put(reuse.roofR, blockAt(top, FLAT_COL.right), FLAT_ROOF_R)
ids.facR = put(reuse.facR, blockAt(top + 4, FLAT_COL.right),
{ FLAT_RIGHT, FLAT_STOREY_R })
elseif part == "section" then
-- One sealed bay repeats as field and right cap. Residential
-- Celadon, regional civic buildings and Saffron each use this compact
-- form to preserve their material identity while reserving scarce
-- block ids for fixed landmarks.
ids.roofA = put(reuse.roofA, blockAt(top, FLAT_COL.plain),
{ FLAT_ROOF_L, FLAT_ROOF_M, FLAT_ROOF_R })
ids.roofR = ids.roofA
ids.facA = put(reuse.facA, blockAt(top + 4, FLAT_COL.plain),
{ 55, 125, FLAT_STOREY_L, FLAT_STOREY_M,
FLAT_RIGHT, FLAT_STOREY_R })
ids.facR = ids.facA
elseif part == "end_w" or part == "end_e" then
-- A cap that carries the gate's stepped door in its OUTER cell, which is
-- the cell the player steps into off the path. Facade only: the roof
-- over it is the same panel the rest of the run carries, so the
-- canopy runs unbroken from one end of the gate to the other.
local slot = (part == "end_w") and "endW" or "endE"
ids[slot] = put(reuse[slot], blockAt(top + 4, FLAT_COL[part]),
(part == "end_w") and 55 or FLAT_RIGHT)
elseif part == "door_r" then
ids.doorR = put(reuse.doorR,
blockAt(top + 4, FLAT_COL.door_r), 124)
addDoor(doorTile(top, FLAT_COL.door_r + 2))
elseif part == "door_l" then
ids.doorL = put(reuse.doorL,
blockAt(top + 4, FLAT_COL.door_l), 58)
addDoor(doorTile(top, FLAT_COL.door_l))
elseif part == "roof" then
-- A sectional industrial roof: the same sealed panel repeats across
-- the whole run and also serves as its right cap, costing one id.
ids.roofA = append(blockAt(top, FLAT_COL.plain),
{ FLAT_ROOF_L, FLAT_ROOF_M, FLAT_ROOF_R })
ids.roofR = ids.roofA
elseif part == "facade" then
-- A sealed casino wall bay repeats on both sides of the entrance.
-- One block serving as field and right cap is what makes a full
-- Game Corner facade affordable under the 255-id ceiling.
ids.facA = append(blockAt(top + 4, FLAT_COL.plain),
{ 55, 125, FLAT_STOREY_L, FLAT_STOREY_M,
FLAT_RIGHT, FLAT_STOREY_R })
ids.facR = ids.facA
end
end
flatIds[st.key] = ids
end
-- and the borrowed modules, exactly as the houses do it: a venue claimed
-- its doorway bay and takes roof, walls and right-hand cap from the style
-- it is a repaint of
for _, st in ipairs(FLATS) do
local ids, from = flatIds[st.key], st.base and flatIds[st.base]
if ids and from then
for part, id in pairs(from) do
if ids[part] == nil then ids[part] = id end
end
end
end
end
mod.content.tilesets:patch("OVERWORLD", {
-- the rows transforms.lua paints; the width is unchanged
imageHeight = GROWN_H,
blocks = blocksOut,
-- the new doorway tiles inherit every role tile $1B held: standable,
-- door-animated, and the cell a warp fires from
walkable = { __append = roleAdds.walkable },
doorTiles = { __append = roleAdds.doorTiles },
warpTiles = { __append = roleAdds.warpTiles },
})
-- and the same for the tiles ExtraWarpCheck reads in front of the player,
-- which live in field rather than in the tileset. Appending leaves every
-- vanilla id in place, so a gate keeps working whether the player faces the
-- vanilla wall or the new one; a base with no warpCarpets at all has nothing
-- to inherit and needs none -- extraCheck falls back to the map-edge test
-- when the table is absent, which no tile id can break.
if anyCarpet then
local tiles = {}
for _, dir in ipairs(CARPET_DIRS) do
if #carpetAdds[dir] > 0 then tiles[dir] = { __append = carpetAdds[dir] } end
end
mod.content.field:patch("warpCarpets", { tiles = tiles })
end
-- ------- 2. full-colour art: opt the new tiles out of the RED++ bake
--
-- Under COLORS=RED++ the renderer bakes one of eight four-colour palettes
-- onto every tile GRAPHIC (TileRenderer.getGbcAtlas, and the voxel mod's
-- mirror of it). A tile with no group would be left alone -- but
-- worldGroupAt floors unknown ids at group 7 (TEXT), which would drag these
-- drawings through a four-shade remap and lose them. Returning nil for our
-- own ids instead is the "leave it alone" answer the bake already knows how
-- to take, and it is what makes the Center red, the Mart blue and each gym
-- its leader's type colour in every town rather than town-palette coloured.
-- Vanilla ids are untouched.
local okFx, PaletteFX = pcall(require, "src.render.PaletteFX")
if okFx and type(PaletteFX) == "table" and PaletteFX.worldGroupAt
and not PaletteFX.__betterBuildingsTrueColor then
local inner = PaletteFX.worldGroupAt
PaletteFX.worldGroupAt = function(tilesetId, mapId, tile)
if tilesetId == "OVERWORLD" and type(tile) == "number"
and tile >= BASE_TILES then
return nil
end
return inner(tilesetId, mapId, tile)
end
PaletteFX.__betterBuildingsTrueColor = true
else
mod.log:warn("could not reach the palette bake -- the new tiles will be "
.. "shade-remapped with the rest of the atlas under COLORS=RED++; every "
.. "other COLORS mode is unaffected")
end
-- ------- 3. repoint the maps
local converted = { center = 0, mart = 0, gym = 0, department = 0, lab = 0,
cottage = 0, house = 0, flat = 0, gate = 0, ends = 0, skipped = 0 }
-- every gate this pass rebuilt, so the voxel diorama below can be handed the
-- grids it actually placed rather than a guess at Kanto's gate arrangements
local gates = {}
-- Every pitched building this pass actually rebuilt. The voxel companion's
-- generic detector sees the new full-colour tile ids but not the architectural
-- bands they depict, so it otherwise raises the roof and the facade as two
-- unrelated volumes. That is the triangular daylight gap visible under the
-- eaves. Keep exact block grids here and register them through the companion's
-- public building profile below. A narrowly scoped in-memory adapter retains
-- the detector's 16-voxel gable rise for these rectangular roof drawings and
-- extends the facade to its underside, closing the gap without flattening it.
local slopedVoxelGrids = {}
local function rememberSloped(kind, grid, options)
slopedVoxelGrids[#slopedVoxelGrids + 1] = {
kind = kind, grid = grid, options = options or {},
}
end
-- the vanilla shop pair, anchored on its sign block at (x, y)
local function shopMatches(at, x, y, kind)
if x < 1 or y < 1 then return false end
local v = SHOPS[kind]
return at(x - 1, y - 1) == v.nw and at(x, y - 1) == v.ne
and at(x - 1, y) == v.sw
end
-- The Department Store's exact vanilla footprint, anchored on the first
-- ground-floor block. The full pattern check prevents the shared MART sign
-- block 115 from turning another structure into this landmark.
local function departmentMatches(at, w, h, x0, by)
if x0 < 0 or x0 + 3 >= w or by < 3 or by >= h then return false end
local rows = {
{ 32, 13, 13, 33 },
{ 104, 127, 127, 105 },
{ 104, 127, 127, 105 },
{ 55, 58, 58, 115 },
}
for r = 0, 3 do
for c = 0, 3 do
if at(x0 + c, by - 3 + r) ~= rows[r + 1][c + 1] then return false end
end
end
return true
end
-- Oak's Lab is the one 3x2 placement of the gym-like chassis whose doorway
-- warp leads to OAKS_LAB. Checking both complete rows keeps unrelated uses
-- of these heavily shared vanilla blocks untouched.
local function oakLabMatches(at, w, h, x0, by)
if x0 < 0 or x0 + 2 >= w or by < 1 or by >= h then return false end
local roof, face = { 12, 13, 14 }, { 16, 58, 0 }
for c = 0, 2 do
if at(x0 + c, by - 1) ~= roof[c + 1]
or at(x0 + c, by) ~= face[c + 1] then return false end
end
return true
end
-- The vanilla gym around the block a `*_GYM` warp fires from. Anchoring on
-- the warp rather than on the block pattern is what keeps the Fighting Dojo
-- -- built from the identical blocks, flush against Saffron's gym -- out of
-- the rewrite. Returns the facade row's first and last block columns.
local function gymExtent(at, w, bx, by, gym)
if by < 1 or bx < 0 or bx >= w then return nil end
local x0 = bx
if not GYM_LEFT[at(x0, by)] then
repeat x0 = x0 - 1 until x0 < 0 or not GYM_MID[at(x0, by)]
end
local x1 = bx
if not GYM_RIGHT[at(x1, by)] then
repeat x1 = x1 + 1 until x1 >= w or not GYM_MID[at(x1, by)]
end
if x0 < 0 or x1 >= w then return nil end
if not (GYM_LEFT[at(x0, by)] and GYM_RIGHT[at(x1, by)]) then return nil end
if x1 - x0 + 1 ~= gym.blocks then return nil end
-- the doorway has to sit in the cap this gym's drawing puts it in
if bx ~= (gym.mirror and x0 or x1) then return nil end
-- and the roof above has to be the vanilla one
if not (GYM_ROOF_LEFT[at(x0, by - 1)] and GYM_ROOF_RIGHT[at(x1, by - 1)]) then
return nil
end
for c = x0 + 1, x1 - 1 do
if not (GYM_MID[at(c, by)] and GYM_ROOF_MID[at(c, by - 1)]) then
return nil
end
end
return x0, x1
end
local pending = {}
for mapId, map in mod.content.maps:each() do
if map.tileset == "OVERWORLD" and type(map.blocks) == "table"
and map.width and map.height then
local w, h = map.width, map.height
local blocks, dirty = nil, false
local function at(bx, by)
if bx < 0 or by < 0 or bx >= w or by >= h then return nil end
return (blocks or map.blocks)[by * w + bx + 1]
end
-- copy-on-write: an untouched map is left as the record the merge built.
-- A default-styled building is painted onto the very blocks it already
-- names (see *_REUSE), so writing it back changes nothing and must not
-- be what marks the map dirty.
local function edit(bx, by, id)
if at(bx, by) == id then return end
if not blocks then
blocks = {}
for j = 1, #map.blocks do blocks[j] = map.blocks[j] end
end
blocks[by * w + bx + 1] = id
dirty = true
end
-- the shop pair, found by its sign block
for i = 1, #map.blocks do
local value = map.blocks[i]
for kind, v in pairs(SHOPS) do
if value == v.se then
local x, y = (i - 1) % w, math.floor((i - 1) / w)
if shopMatches(at, x, y, kind) then
local new = (mapId == "FUCHSIA_CITY" and kind == "center")
and fuchsiaCenterIds or shopIds[kind]
edit(x - 1, y - 1, new.nw)
edit(x, y - 1, new.ne)
edit(x - 1, y, new.sw)
edit(x, y, new.se)
rememberSloped("shop_" .. kind,
{ { new.nw, new.ne }, { new.sw, new.se } })
converted[kind] = converted[kind] + 1
end