Terrain overhaul: 52-tile atlas from 5 generated surfaces, tone-harmonized so they read as one material family
Replaces the single flat grey tile. Adds tools/build_tiles.py (atlas builder with edge-blending + tone matching), paint_terrain.py (surgical tile-plane repaint: asphalt base, concrete pads, dirt blobs, groove patches near wax, rubble clumps), jitter_wax.py, render_map.py (offline map preview), and check_assets.py. Also fixes the arena harness being rigged: both wax fields sat ~12 cells from BotA and ~18 from BotB, so BotB starved for map reasons, not balance reasons. Arena wax is now mirrored (7.1/18/18/19.8 from both starts). Seep interval 90 -> 20 after telemetry showed the slower rate still drained both economies. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
BIN
assets_src/map_arena.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
assets_src/map_detail.png
Normal file
|
After Width: | Height: | Size: 480 KiB |
BIN
assets_src/round5_roster.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
assets_src/terrain/asphalt.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
assets_src/terrain/concrete.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
assets_src/terrain/dirt.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
assets_src/terrain/grooves.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
assets_src/terrain/preview_asphalt.png
Normal file
|
After Width: | Height: | Size: 193 KiB |
BIN
assets_src/terrain/rubble.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
@ -60,9 +60,15 @@ Actors:
|
|||||||
Location: 24,25
|
Location: 24,25
|
||||||
waxseed0: waxseed
|
waxseed0: waxseed
|
||||||
Owner: Neutral
|
Owner: Neutral
|
||||||
Location: 8,20
|
Location: 8,25
|
||||||
waxseed1: waxseed
|
waxseed1: waxseed
|
||||||
Owner: Neutral
|
Owner: Neutral
|
||||||
Location: 18,10
|
Location: 12,12
|
||||||
|
waxseed2: waxseed
|
||||||
|
Owner: Neutral
|
||||||
|
Location: 21,21
|
||||||
|
waxseed3: waxseed
|
||||||
|
Owner: Neutral
|
||||||
|
Location: 25,8
|
||||||
|
|
||||||
Rules: rules.yaml
|
Rules: rules.yaml
|
||||||
|
|||||||
@ -37,6 +37,8 @@ player:
|
|||||||
printfarm: 1
|
printfarm: 1
|
||||||
mastering: 1
|
mastering: 1
|
||||||
algorithm: 1
|
algorithm: 1
|
||||||
|
launchramp: 1
|
||||||
|
droneport: 1
|
||||||
BuildingFractions:
|
BuildingFractions:
|
||||||
pressplant: 30
|
pressplant: 30
|
||||||
hornpit: 8
|
hornpit: 8
|
||||||
@ -48,6 +50,8 @@ player:
|
|||||||
printfarm: 1
|
printfarm: 1
|
||||||
mastering: 1
|
mastering: 1
|
||||||
algorithm: 1
|
algorithm: 1
|
||||||
|
launchramp: 2
|
||||||
|
droneport: 2
|
||||||
UnitBuilderBotModule:
|
UnitBuilderBotModule:
|
||||||
RequiresCondition: enable-wax-ai
|
RequiresCondition: enable-wax-ai
|
||||||
UnitsToBuild:
|
UnitsToBuild:
|
||||||
@ -60,10 +64,20 @@ player:
|
|||||||
bufferbot: 60
|
bufferbot: 60
|
||||||
drmwalker: 45
|
drmwalker: 45
|
||||||
scraper: 10
|
scraper: 10
|
||||||
|
boombike: 20
|
||||||
|
crawler: 8
|
||||||
|
flyer45: 25
|
||||||
|
compressor: 25
|
||||||
|
selector: 5
|
||||||
|
curator: 5
|
||||||
UnitLimits:
|
UnitLimits:
|
||||||
digger: 4
|
digger: 4
|
||||||
scraper: 4
|
scraper: 4
|
||||||
roadie: 2
|
roadie: 2
|
||||||
|
boombike: 3
|
||||||
|
crawler: 2
|
||||||
|
selector: 1
|
||||||
|
curator: 1
|
||||||
SupportPowerBotModule:
|
SupportPowerBotModule:
|
||||||
RequiresCondition: enable-wax-ai
|
RequiresCondition: enable-wax-ai
|
||||||
Decisions:
|
Decisions:
|
||||||
@ -79,6 +93,7 @@ player:
|
|||||||
SquadManagerBotModule:
|
SquadManagerBotModule:
|
||||||
RequiresCondition: enable-wax-ai
|
RequiresCondition: enable-wax-ai
|
||||||
SquadSize: 8
|
SquadSize: 8
|
||||||
ExcludeFromSquadsTypes: digger, scraper, popupvan, streamvan, roadie
|
AirUnitsTypes: flyer45, compressor
|
||||||
|
ExcludeFromSquadsTypes: digger, scraper, popupvan, streamvan, roadie, crawler
|
||||||
ConstructionYardTypes: popupshop, streamhub
|
ConstructionYardTypes: popupshop, streamhub
|
||||||
ProtectionTypes: digger, scraper, popupvan, streamvan, pressplant, transcoder, popupshop, streamhub, ampstack, datacentre, vault, merchtable, podfarm, garage, printfarm, mastering
|
ProtectionTypes: launchramp, droneport, digger, scraper, popupvan, streamvan, pressplant, transcoder, popupshop, streamhub, ampstack, datacentre, vault, merchtable, podfarm, garage, printfarm, mastering
|
||||||
|
|||||||
@ -284,7 +284,7 @@ waxseed:
|
|||||||
Terrain: Wax
|
Terrain: Wax
|
||||||
SeedsResource:
|
SeedsResource:
|
||||||
ResourceType: Wax
|
ResourceType: Wax
|
||||||
Interval: 90
|
Interval: 20
|
||||||
MaxRange: 60
|
MaxRange: 60
|
||||||
MapEditorData:
|
MapEditorData:
|
||||||
Categories: Resource spawn
|
Categories: Resource spawn
|
||||||
|
|||||||
@ -13,8 +13,88 @@ Terrain:
|
|||||||
Templates:
|
Templates:
|
||||||
Template@255:
|
Template@255:
|
||||||
Id: 255
|
Id: 255
|
||||||
Images: tilesets/vinylgod/vinylgod.png
|
Images: tilesets/vinylgod/asphalt.png
|
||||||
Size: 1,1
|
Size: 1,1
|
||||||
|
PickAny: True
|
||||||
Categories: Terrain
|
Categories: Terrain
|
||||||
Tiles:
|
Tiles:
|
||||||
0: Clear
|
0: Clear
|
||||||
|
1: Clear
|
||||||
|
2: Clear
|
||||||
|
3: Clear
|
||||||
|
4: Clear
|
||||||
|
5: Clear
|
||||||
|
6: Clear
|
||||||
|
7: Clear
|
||||||
|
8: Clear
|
||||||
|
9: Clear
|
||||||
|
10: Clear
|
||||||
|
11: Clear
|
||||||
|
12: Clear
|
||||||
|
13: Clear
|
||||||
|
14: Clear
|
||||||
|
15: Clear
|
||||||
|
Template@1:
|
||||||
|
Id: 1
|
||||||
|
Images: tilesets/vinylgod/concrete.png
|
||||||
|
Size: 1,1
|
||||||
|
PickAny: True
|
||||||
|
Categories: Terrain
|
||||||
|
Tiles:
|
||||||
|
0: Clear
|
||||||
|
1: Clear
|
||||||
|
2: Clear
|
||||||
|
3: Clear
|
||||||
|
4: Clear
|
||||||
|
5: Clear
|
||||||
|
6: Clear
|
||||||
|
7: Clear
|
||||||
|
8: Clear
|
||||||
|
Template@2:
|
||||||
|
Id: 2
|
||||||
|
Images: tilesets/vinylgod/dirt.png
|
||||||
|
Size: 1,1
|
||||||
|
PickAny: True
|
||||||
|
Categories: Terrain
|
||||||
|
Tiles:
|
||||||
|
0: Clear
|
||||||
|
1: Clear
|
||||||
|
2: Clear
|
||||||
|
3: Clear
|
||||||
|
4: Clear
|
||||||
|
5: Clear
|
||||||
|
6: Clear
|
||||||
|
7: Clear
|
||||||
|
8: Clear
|
||||||
|
Template@3:
|
||||||
|
Id: 3
|
||||||
|
Images: tilesets/vinylgod/grooves.png
|
||||||
|
Size: 1,1
|
||||||
|
PickAny: True
|
||||||
|
Categories: Terrain
|
||||||
|
Tiles:
|
||||||
|
0: Clear
|
||||||
|
1: Clear
|
||||||
|
2: Clear
|
||||||
|
3: Clear
|
||||||
|
4: Clear
|
||||||
|
5: Clear
|
||||||
|
6: Clear
|
||||||
|
7: Clear
|
||||||
|
8: Clear
|
||||||
|
Template@4:
|
||||||
|
Id: 4
|
||||||
|
Images: tilesets/vinylgod/rubble.png
|
||||||
|
Size: 1,1
|
||||||
|
PickAny: True
|
||||||
|
Categories: Terrain
|
||||||
|
Tiles:
|
||||||
|
0: Clear
|
||||||
|
1: Clear
|
||||||
|
2: Clear
|
||||||
|
3: Clear
|
||||||
|
4: Clear
|
||||||
|
5: Clear
|
||||||
|
6: Clear
|
||||||
|
7: Clear
|
||||||
|
8: Clear
|
||||||
|
|||||||
BIN
mods/vinylgod/tilesets/vinylgod/asphalt.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
mods/vinylgod/tilesets/vinylgod/concrete.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
mods/vinylgod/tilesets/vinylgod/dirt.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
mods/vinylgod/tilesets/vinylgod/grooves.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
mods/vinylgod/tilesets/vinylgod/rubble.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
117
tools/build_tiles.py
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Build the VINYLGOD terrain tile atlases from the flux textures.
|
||||||
|
|
||||||
|
One atlas PNG per surface, each holding N 32x32 variants as frames (FrameSize
|
||||||
|
metadata). Tiles are cut from a downscaled texture and edge-blended against
|
||||||
|
their own wrap so random placement does not show hard seams.
|
||||||
|
|
||||||
|
All tiles stay terrain type `Clear` — this is a pure visual upgrade, no
|
||||||
|
pathing/placement changes.
|
||||||
|
"""
|
||||||
|
import os, struct, zlib
|
||||||
|
from PIL import Image, ImageChops
|
||||||
|
|
||||||
|
SRC = os.path.join(os.path.dirname(__file__), '..', 'assets_src', 'terrain')
|
||||||
|
DST = os.path.join(os.path.dirname(__file__), '..', 'mods', 'vinylgod', 'tilesets', 'vinylgod')
|
||||||
|
os.makedirs(DST, exist_ok=True)
|
||||||
|
TILE = 32
|
||||||
|
|
||||||
|
# surface -> (grid, target average brightness nudge)
|
||||||
|
SURFACES = {
|
||||||
|
'asphalt': 4, # 16 base variants
|
||||||
|
'concrete': 3, # 9
|
||||||
|
'dirt': 3,
|
||||||
|
'grooves': 3,
|
||||||
|
'rubble': 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def seamless(im):
|
||||||
|
"""Cheap wrap-blend so tile edges meet: cross-fade with a half-offset copy."""
|
||||||
|
w, h = im.size
|
||||||
|
off = ImageChops.offset(im, w // 2, h // 2)
|
||||||
|
mask = Image.new('L', (w, h), 0)
|
||||||
|
px = mask.load()
|
||||||
|
fw, fh = w // 6, h // 6
|
||||||
|
for y in range(h):
|
||||||
|
for x in range(w):
|
||||||
|
ex = min(x, w - 1 - x)
|
||||||
|
ey = min(y, h - 1 - y)
|
||||||
|
v = 255
|
||||||
|
if ex < fw:
|
||||||
|
v = min(v, int(255 * ex / fw))
|
||||||
|
if ey < fh:
|
||||||
|
v = min(v, int(255 * ey / fh))
|
||||||
|
px[x, y] = 255 - v # strong at edges
|
||||||
|
return Image.composite(off, im, mask)
|
||||||
|
|
||||||
|
|
||||||
|
def chunk(tag, data):
|
||||||
|
return struct.pack('>I', len(data)) + tag + data + struct.pack('>I', zlib.crc32(tag + data))
|
||||||
|
|
||||||
|
|
||||||
|
def save_frames(path, frames):
|
||||||
|
w, h = TILE * len(frames), TILE
|
||||||
|
sheet = Image.new('RGB', (w, h))
|
||||||
|
for i, f in enumerate(frames):
|
||||||
|
sheet.paste(f, (i * TILE, 0))
|
||||||
|
raw = sheet.convert('RGBA').tobytes()
|
||||||
|
rows = b''.join(b'\x00' + raw[y * w * 4:(y + 1) * w * 4] for y in range(h))
|
||||||
|
out = b'\x89PNG\r\n\x1a\n' + chunk(b'IHDR', struct.pack('>IIBBBBB', w, h, 8, 6, 0, 0, 0))
|
||||||
|
out += chunk(b'tEXt', b'FrameSize\x00' + f'{TILE},{TILE}'.encode())
|
||||||
|
out += chunk(b'IDAT', zlib.compress(rows)) + chunk(b'IEND', b'')
|
||||||
|
open(path, 'wb').write(out)
|
||||||
|
|
||||||
|
|
||||||
|
def stats(im):
|
||||||
|
px = list(im.getdata())
|
||||||
|
n = len(px)
|
||||||
|
out = []
|
||||||
|
for c in range(3):
|
||||||
|
vals = [p[c] for p in px]
|
||||||
|
m = sum(vals) / n
|
||||||
|
sd = (sum((v - m) ** 2 for v in vals) / n) ** 0.5 or 1.0
|
||||||
|
out.append((m, sd))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def harmonize(im, target, strength=1.0):
|
||||||
|
"""Pull a surface toward the base asphalt's tone so surfaces read as one
|
||||||
|
material family instead of five clashing ones."""
|
||||||
|
src = stats(im)
|
||||||
|
px = im.load()
|
||||||
|
w, h = im.size
|
||||||
|
for y in range(h):
|
||||||
|
for x in range(w):
|
||||||
|
r, g, b = px[x, y][:3]
|
||||||
|
new = []
|
||||||
|
for c, v in enumerate((r, g, b)):
|
||||||
|
sm, ssd = src[c]
|
||||||
|
tm, tsd = target[c]
|
||||||
|
z = (v - sm) / ssd
|
||||||
|
nv = tm + z * tsd
|
||||||
|
nv = v + (nv - v) * strength
|
||||||
|
new.append(max(0, min(255, int(nv))))
|
||||||
|
px[x, y] = tuple(new)
|
||||||
|
return im
|
||||||
|
|
||||||
|
|
||||||
|
base = Image.open(os.path.join(SRC, 'asphalt.png')).convert('RGB')
|
||||||
|
TARGET = stats(base.resize((128, 128), Image.LANCZOS))
|
||||||
|
|
||||||
|
counts = {}
|
||||||
|
for name, grid in SURFACES.items():
|
||||||
|
src = Image.open(os.path.join(SRC, f'{name}.png')).convert('RGB')
|
||||||
|
src = src.resize((TILE * grid, TILE * grid), Image.LANCZOS)
|
||||||
|
if name != 'asphalt':
|
||||||
|
src = harmonize(src, TARGET)
|
||||||
|
frames = []
|
||||||
|
for gy in range(grid):
|
||||||
|
for gx in range(grid):
|
||||||
|
t = src.crop((gx * TILE, gy * TILE, (gx + 1) * TILE, (gy + 1) * TILE))
|
||||||
|
frames.append(seamless(t))
|
||||||
|
save_frames(os.path.join(DST, f'{name}.png'), frames)
|
||||||
|
counts[name] = len(frames)
|
||||||
|
print(f'{name}: {len(frames)} tiles')
|
||||||
|
|
||||||
|
print('counts =', counts)
|
||||||
23
tools/jitter_wax.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Jitter wax density per cell so neighbouring cells pick different sprite
|
||||||
|
frames — without it, equal-density cells draw the identical blob and the field
|
||||||
|
reads as a regular grid."""
|
||||||
|
import os, struct, glob, random
|
||||||
|
|
||||||
|
for i, m in enumerate(sorted(glob.glob(os.path.join(os.path.dirname(__file__), '..', 'mods', 'vinylgod', 'maps', '*')))):
|
||||||
|
p = os.path.join(m, 'map.bin')
|
||||||
|
if not os.path.exists(p):
|
||||||
|
continue
|
||||||
|
d = bytearray(open(p, 'rb').read())
|
||||||
|
w, h = struct.unpack_from('<HH', d, 1)
|
||||||
|
_, _, res_off = struct.unpack_from('<III', d, 5)
|
||||||
|
rng = random.Random(7000 + i)
|
||||||
|
n = 0
|
||||||
|
for x in range(w):
|
||||||
|
for y in range(h):
|
||||||
|
o = res_off + 2 * (x * h + y)
|
||||||
|
if d[o]:
|
||||||
|
d[o + 1] = max(1, min(12, d[o + 1] + rng.randint(-3, 3)))
|
||||||
|
n += 1
|
||||||
|
open(p, 'wb').write(d)
|
||||||
|
print(f'{os.path.basename(m):14s} jittered {n} wax cells')
|
||||||
109
tools/paint_terrain.py
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Repaint the tile plane of every map.bin with varied VINYLGOD terrain.
|
||||||
|
|
||||||
|
Surgical: only the tile plane is touched — resources, size and header are left
|
||||||
|
exactly as they are, so it is safe to re-run on any map at any time.
|
||||||
|
|
||||||
|
Layout logic (all tiles are terrain type Clear, so pathing is unchanged):
|
||||||
|
* asphalt base everywhere, random variant per cell
|
||||||
|
* concrete slabs — a few rectangular pads (old foundations / car parks)
|
||||||
|
* dirt — organic blobs
|
||||||
|
* grooves — concentric rings radiating out of each wax field, as if
|
||||||
|
the Black Wax etched the ground on its way up
|
||||||
|
* rubble — scatter, denser toward the map edges
|
||||||
|
"""
|
||||||
|
import os, struct, glob, random
|
||||||
|
|
||||||
|
TPL = {'asphalt': (255, 16), 'concrete': (1, 9), 'dirt': (2, 9), 'grooves': (3, 9), 'rubble': (4, 9)}
|
||||||
|
MAPS = sorted(glob.glob(os.path.join(os.path.dirname(__file__), '..', 'mods', 'vinylgod', 'maps', '*')))
|
||||||
|
|
||||||
|
|
||||||
|
def paint(path, seed):
|
||||||
|
d = bytearray(open(path, 'rb').read())
|
||||||
|
fmt, w, h = d[0], *struct.unpack_from('<HH', d, 1)
|
||||||
|
tiles_off, heights_off, res_off = struct.unpack_from('<III', d, 5)
|
||||||
|
assert fmt == 2 and heights_off == 0
|
||||||
|
rng = random.Random(seed)
|
||||||
|
|
||||||
|
surface = {} # (x, y) -> template name
|
||||||
|
|
||||||
|
# wax field centres, for the groove rings
|
||||||
|
wax = [(x, y) for x in range(w) for y in range(h)
|
||||||
|
if d[res_off + 2 * (x * h + y)] != 0]
|
||||||
|
seeds = []
|
||||||
|
if wax:
|
||||||
|
seen = set()
|
||||||
|
for s in wax:
|
||||||
|
if s in seen:
|
||||||
|
continue
|
||||||
|
stack, blob = [s], []
|
||||||
|
seen.add(s)
|
||||||
|
while stack:
|
||||||
|
cx, cy = stack.pop()
|
||||||
|
blob.append((cx, cy))
|
||||||
|
for n in ((cx+1, cy), (cx-1, cy), (cx, cy+1), (cx, cy-1)):
|
||||||
|
if n in set(wax) - seen if False else (n in wax and n not in seen):
|
||||||
|
seen.add(n)
|
||||||
|
stack.append(n)
|
||||||
|
if len(blob) >= 6:
|
||||||
|
seeds.append((sum(p[0] for p in blob) / len(blob),
|
||||||
|
sum(p[1] for p in blob) / len(blob)))
|
||||||
|
|
||||||
|
# concrete pads
|
||||||
|
for _ in range(max(1, w // 30)):
|
||||||
|
px, py = rng.randrange(w), rng.randrange(h)
|
||||||
|
pw, ph = rng.randint(3, 6), rng.randint(3, 6)
|
||||||
|
for x in range(px, min(w, px + pw)):
|
||||||
|
for y in range(py, min(h, py + ph)):
|
||||||
|
surface[(x, y)] = 'concrete'
|
||||||
|
|
||||||
|
# dirt blobs
|
||||||
|
for _ in range(max(2, w // 18)):
|
||||||
|
cx, cy, r = rng.randrange(w), rng.randrange(h), rng.randint(3, 7)
|
||||||
|
for x in range(cx - r, cx + r + 1):
|
||||||
|
for y in range(cy - r, cy + r + 1):
|
||||||
|
if 0 <= x < w and 0 <= y < h and (x - cx) ** 2 + (y - cy) ** 2 <= r * r + rng.randint(-3, 3):
|
||||||
|
surface[(x, y)] = 'dirt'
|
||||||
|
|
||||||
|
# groove patches: etched ground sitting just off each wax field.
|
||||||
|
# (tried concentric rings first — arcs don't line up across tiles and it
|
||||||
|
# reads as random squares, so coherent blobs it is)
|
||||||
|
import math as _m
|
||||||
|
for cx, cy in seeds:
|
||||||
|
for _ in range(2):
|
||||||
|
a = rng.random() * 2 * _m.pi
|
||||||
|
dist = rng.randint(6, 10)
|
||||||
|
bx, by = int(cx + dist * _m.cos(a)), int(cy + dist * _m.sin(a))
|
||||||
|
r = rng.randint(2, 3)
|
||||||
|
for x in range(bx - r, bx + r + 1):
|
||||||
|
for y in range(by - r, by + r + 1):
|
||||||
|
if 0 <= x < w and 0 <= y < h and (x - bx) ** 2 + (y - by) ** 2 <= r * r:
|
||||||
|
surface[(x, y)] = 'grooves'
|
||||||
|
|
||||||
|
# rubble: a few coherent clumps (never per-cell scatter — that checkerboards)
|
||||||
|
for _ in range(max(2, w // 16)):
|
||||||
|
cx, cy, r = rng.randrange(w), rng.randrange(h), rng.randint(2, 4)
|
||||||
|
for x in range(cx - r, cx + r + 1):
|
||||||
|
for y in range(cy - r, cy + r + 1):
|
||||||
|
if 0 <= x < w and 0 <= y < h and (x - cx) ** 2 + (y - cy) ** 2 <= r * r:
|
||||||
|
surface[(x, y)] = 'rubble'
|
||||||
|
|
||||||
|
for x in range(w):
|
||||||
|
for y in range(h):
|
||||||
|
tid, n = TPL[surface.get((x, y), 'asphalt')]
|
||||||
|
o = tiles_off + 3 * (x * h + y)
|
||||||
|
struct.pack_into('<HB', d, o, tid, rng.randrange(n))
|
||||||
|
|
||||||
|
open(path, 'wb').write(d)
|
||||||
|
counts = {}
|
||||||
|
for v in surface.values():
|
||||||
|
counts[v] = counts.get(v, 0) + 1
|
||||||
|
return w, h, len(seeds), counts
|
||||||
|
|
||||||
|
|
||||||
|
for i, m in enumerate(MAPS):
|
||||||
|
p = os.path.join(m, 'map.bin')
|
||||||
|
if not os.path.exists(p):
|
||||||
|
continue
|
||||||
|
w, h, ns, c = paint(p, seed=1000 + i)
|
||||||
|
print(f'{os.path.basename(m):14s} {w}x{h} seeps={ns} {c}')
|
||||||
47
tools/render_map.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Render a map.bin to a PNG using the real tile atlases + wax sprites.
|
||||||
|
|
||||||
|
Offline view of exactly what the engine will draw, so terrain can be checked
|
||||||
|
without a display. Usage: render_map.py <mapname> [out.png]
|
||||||
|
"""
|
||||||
|
import os, struct, sys
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
ROOT = os.path.join(os.path.dirname(__file__), '..')
|
||||||
|
MOD = os.path.join(ROOT, 'mods', 'vinylgod')
|
||||||
|
TPL = {255: 'asphalt', 1: 'concrete', 2: 'dirt', 3: 'grooves', 4: 'rubble'}
|
||||||
|
TILE = 32
|
||||||
|
|
||||||
|
name = sys.argv[1] if len(sys.argv) > 1 else 'arena'
|
||||||
|
out = sys.argv[2] if len(sys.argv) > 2 else os.path.join(ROOT, 'assets_src', f'map_{name}.png')
|
||||||
|
|
||||||
|
atlas = {}
|
||||||
|
for tid, n in TPL.items():
|
||||||
|
im = Image.open(os.path.join(MOD, 'tilesets', 'vinylgod', f'{n}.png')).convert('RGBA')
|
||||||
|
atlas[tid] = [im.crop((i * TILE, 0, (i + 1) * TILE, TILE)) for i in range(im.width // TILE)]
|
||||||
|
|
||||||
|
wax_sheet = Image.open(os.path.join(MOD, 'sequences', 'assets', 'wax.png')).convert('RGBA')
|
||||||
|
wax = [wax_sheet.crop((i * 32, 0, (i + 1) * 32, 32)) for i in range(wax_sheet.width // 32)]
|
||||||
|
|
||||||
|
d = open(os.path.join(MOD, 'maps', name, 'map.bin'), 'rb').read()
|
||||||
|
fmt, w, h = d[0], *struct.unpack_from('<HH', d, 1)
|
||||||
|
t_off, ht_off, res_off = struct.unpack_from('<III', d, 5)
|
||||||
|
|
||||||
|
img = Image.new('RGBA', (w * TILE, h * TILE))
|
||||||
|
for x in range(w):
|
||||||
|
for y in range(h):
|
||||||
|
tid, idx = struct.unpack_from('<HB', d, t_off + 3 * (x * h + y))
|
||||||
|
frames = atlas.get(tid) or atlas[255]
|
||||||
|
img.paste(frames[idx % len(frames)], (x * TILE, y * TILE))
|
||||||
|
|
||||||
|
for x in range(w):
|
||||||
|
for y in range(h):
|
||||||
|
rt, dens = d[res_off + 2 * (x * h + y)], d[res_off + 2 * (x * h + y) + 1]
|
||||||
|
if rt:
|
||||||
|
s = wax[min(dens, len(wax) - 1)]
|
||||||
|
img.paste(s, (x * TILE, y * TILE), s)
|
||||||
|
|
||||||
|
scale = max(1, min(2, 2600 // (w * TILE)))
|
||||||
|
img = img.resize((w * TILE * scale // 2 or w * TILE, h * TILE * scale // 2 or h * TILE), Image.LANCZOS)
|
||||||
|
img.convert('RGB').save(out)
|
||||||
|
print(f'{name}: {w}x{h} -> {out} ({img.width}x{img.height})')
|
||||||