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>
110 lines
4.4 KiB
Python
110 lines
4.4 KiB
Python
#!/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}')
|