RECORDANDRONCO/tools/paint_terrain.py
type-two e11ec402ff 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>
2026-07-25 22:37:38 +10:00

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}')