Files
restitution/generate_testmap.py
kaykayyali 8fc45968b5 M2.3 + M2.4 + TeamManager integration: projectile sprites, death handling, build menu, production panel, building placer
- ProjectileSprite.js: physics arcade sprite, faction tint, off-screen culling
- CombatSystem: refactored enemy selection to use TeamManager instead of legacy containers
- Death handling: DYING alpha tween (500ms), smoke puff (300ms), unit:killed event, cleanup
- TeamManager: centralized team registry replacing goodGuys/badGuys containers
- HealthBarSystem, ResourceBar, CaptureProgressUI, BuildMenu, BuildingPlacer, BuildingRenderer, ProductionPanel
- Map_Player: wired new subsystems, removed legacy container creation
- Tests: ProjectileSprite (4), DeathHandling (13), CombatSystem updated

47 tests passed at dev time (M2.3), 158/158 at dev time (M2.4)
2026-06-01 05:18:33 +00:00

315 lines
10 KiB
Python

#!/usr/bin/env python3
"""
Generate a 128x128 test map for Restitution RTS.
Uses simplex noise for natural terrain, drunkard walks for paths,
and noise-based placement for props and water.
"""
import json
import math
import random
from noise import snoise2
# Map configuration
MAP_SIZE = 128
TILE_SIZE = 32
OUTPUT_PATH = "/root/restitution/public/tilemaps/testmap/test2.tmj"
# Tile ID mappings (based on floorsPrimary.tsx)
TILE_GRASS = list(range(0, 8)) # Grass variants (cost=1)
TILE_DIRT = list(range(8, 16)) # Dirt transitions (cost=1)
TILE_WATER = list(range(20, 25)) # Water tiles (cost=2)
TILE_PROPS = list(range(48, 68)) # Collision tiles: trees, bushes, rocks (collide=true)
# Clearing centers - 4 large open areas for base placement
CLEARINGS = [
{"x": 32, "y": 32, "radius": 15},
{"x": 96, "y": 32, "radius": 15},
{"x": 32, "y": 96, "radius": 15},
{"x": 96, "y": 96, "radius": 15},
]
def is_in_clearing(x, y):
"""Check if a tile is inside one of the base clearings."""
for c in CLEARINGS:
dx = x - c["x"]
dy = y - c["y"]
if dx * dx + dy * dy < c["radius"] * c["radius"]:
return True
return False
def generate_terrain():
"""Generate base terrain using simplex noise."""
floor_layer = [0] * (MAP_SIZE * MAP_SIZE)
# Noise parameters
scale = 0.05 # How "zoomed in" the noise is
octaves = 3
persistence = 0.5
lacunarity = 2.0
for y in range(MAP_SIZE):
for x in range(MAP_SIZE):
# Multi-octave noise for natural variation
noise_val = snoise2(
x * scale,
y * scale,
octaves=octaves,
persistence=persistence,
lacunarity=lacunarity,
repeatx=1024,
repeaty=1024,
base=random.randint(0, 10000)
)
# Normalize from [-1, 1] to [0, 1]
normalized = (noise_val + 1) / 2
# Clearings override to grass
if is_in_clearing(x, y):
tile_id = TILE_GRASS[0] # Plain grass in clearings
# Water in low areas (but not in clearings)
elif normalized < 0.15:
tile_id = random.choice(TILE_WATER)
# Dirt paths will be added later, for now use grass
else:
# Vary grass based on noise value
grass_index = min(int(normalized * len(TILE_GRASS)), len(TILE_GRASS) - 1)
tile_id = TILE_GRASS[grass_index]
floor_layer[y * MAP_SIZE + x] = tile_id + 1 # TMJ uses 1-based GIDs
return floor_layer
def generate_paths(floor_layer):
"""Generate winding dirt paths using drunkard walk algorithm."""
path_data = floor_layer.copy()
# Path endpoints - connect clearings
path_starts = [
(32, 64), # Top-left to center-top
(96, 64), # Top-right to center-top
(64, 32), # Top-center
(64, 96), # Bottom-center
(32, 96), # Bottom-left to center
(96, 96), # Bottom-right to center
]
random.seed(42) # Reproducible paths
for start_x, start_y in path_starts:
# Drunkard walk from clearing edge outward
x, y = start_x, start_y
steps = random.randint(80, 150)
for _ in range(steps):
# Place dirt tile
idx = y * MAP_SIZE + x
if 0 <= idx < len(path_data):
# Don't overwrite water
current = path_data[idx]
if current not in [t + 1 for t in TILE_WATER]:
path_data[idx] = random.choice(TILE_DIRT) + 1
# Random walk with bias toward map edges
direction = random.choice(["n", "s", "e", "w"])
if direction == "n" and y > 5:
y -= 1
elif direction == "s" and y < MAP_SIZE - 6:
y += 1
elif direction == "e" and x < MAP_SIZE - 6:
x += 1
elif direction == "w" and x > 5:
x -= 1
return path_data
def generate_props(floor_layer):
"""Generate collision layer with trees, bushes, rocks."""
rocks_layer = [0] * (MAP_SIZE * MAP_SIZE)
random.seed(123) # Different seed for props
for y in range(MAP_SIZE):
for x in range(MAP_SIZE):
# Skip clearings
if is_in_clearing(x, y):
continue
idx = y * MAP_SIZE + x
floor_tile = floor_layer[idx]
# Higher prop density near water
is_near_water = any(
floor_layer[ny * MAP_SIZE + nx] in [t + 1 for t in TILE_WATER]
for nx, ny in get_neighbors(x, y)
if 0 <= nx < MAP_SIZE and 0 <= ny < MAP_SIZE
)
# Base prop chance
prop_chance = 0.03
if is_near_water:
prop_chance = 0.15 # Much denser near water
# Don't place on paths or water
if floor_tile in [t + 1 for t in TILE_DIRT + TILE_WATER]:
continue
if random.random() < prop_chance:
# Cluster placement - check if neighbor has prop
has_neighbor_prop = any(
rocks_layer[ny * MAP_SIZE + nx] != 0
for nx, ny in get_neighbors(x, y)
if 0 <= nx < MAP_SIZE and 0 <= ny < MAP_SIZE
)
if has_neighbor_prop or random.random() < 0.3:
# Place prop (tree, bush, or rock)
prop_tile = random.choice(TILE_PROPS)
rocks_layer[idx] = prop_tile + 1
return rocks_layer
def get_neighbors(x, y):
"""Get 8 neighboring tile coordinates."""
neighbors = []
for dy in [-1, 0, 1]:
for dx in [-1, 0, 1]:
if dx == 0 and dy == 0:
continue
neighbors.append((x + dx, y + dy))
return neighbors
def generate_decorations(floor_layer):
"""Generate optional decoration layer (small accent tiles)."""
decor_layer = [0] * (MAP_SIZE * MAP_SIZE)
# Sparse decorative elements - flowers, small stones
random.seed(456)
for y in range(MAP_SIZE):
for x in range(MAP_SIZE):
if is_in_clearing(x, y):
continue
if random.random() < 0.01: # 1% chance
idx = y * MAP_SIZE + x
# Use a non-collision decorative tile
decor_layer[idx] = random.choice([38, 39, 40]) + 1
return decor_layer
def build_tilemap():
"""Generate the complete tilemap and save as TMJ."""
print("Generating terrain...")
floor_layer = generate_terrain()
print("Adding paths...")
floor_layer = generate_paths(floor_layer)
print("Placing props and collision tiles...")
rocks_layer = generate_props(floor_layer)
print("Adding decorations...")
decor_layer = generate_decorations(floor_layer)
# Build TMJ structure matching test1.tmj format
tilemap = {
"compressionlevel": -1,
"height": MAP_SIZE,
"infinite": False,
"layers": [
{
"data": floor_layer,
"height": MAP_SIZE,
"id": 1,
"name": "Floor",
"opacity": 1,
"type": "tilelayer",
"visible": True,
"width": MAP_SIZE,
"x": 0,
"y": 0
},
{
"data": rocks_layer,
"height": MAP_SIZE,
"id": 2,
"name": "Rocks",
"opacity": 1,
"type": "tilelayer",
"visible": True,
"width": MAP_SIZE,
"x": 0,
"y": 0
},
{
"data": decor_layer,
"height": MAP_SIZE,
"id": 3,
"name": "Decorations",
"opacity": 1,
"type": "tilelayer",
"visible": True,
"width": MAP_SIZE,
"x": 0,
"y": 0
}
],
"nextlayerid": 4,
"nextobjectid": 1,
"orientation": "isometric",
"renderorder": "right-down",
"tiledversion": "1.9.2",
"tileheight": 16, # Must be 16 for Phaser 3.55 tilemap parser
"tilesets": [
{
"columns": 11,
"firstgid": 1,
"image": "../../tilesets/floors32x32.png",
"imageheight": 352,
"imagewidth": 352,
"margin": 0,
"name": "floorsPrimary",
"objectalignment": "center",
"spacing": 0,
"tilecount": 121,
"tileheight": 32,
"tilewidth": 32
}
],
"tilewidth": TILE_SIZE,
"type": "map",
"version": "1.9",
"width": MAP_SIZE
}
# Write output
print(f"Writing to {OUTPUT_PATH}...")
with open(OUTPUT_PATH, "w") as f:
json.dump(tilemap, f, indent=1)
# Validate
print("Validating JSON...")
with open(OUTPUT_PATH, "r") as f:
loaded = json.load(f)
print(f"\n✓ Generated {MAP_SIZE}x{MAP_SIZE} map ({MAP_SIZE * MAP_SIZE * 3} tiles total)")
print(f"✓ Tile size: {TILE_SIZE}x{TILE_SIZE} (fixed from 16)")
print(f"✓ 3 layers: Floor, Rocks, Decorations")
print(f"✓ 4 base clearings at corners")
print(f"✓ Output: {OUTPUT_PATH}")
# Stats
floor_nonzero = sum(1 for t in floor_layer if t != 0)
rocks_nonzero = sum(1 for t in rocks_layer if t != 0)
decor_nonzero = sum(1 for t in decor_layer if t != 0)
print(f"\nLayer stats:")
print(f" Floor: {floor_nonzero}/{len(floor_layer)} tiles placed")
print(f" Rocks: {rocks_nonzero}/{len(rocks_layer)} collision tiles")
print(f" Decor: {decor_nonzero}/{len(decor_layer)} decorations")
if __name__ == "__main__":
build_tilemap()