- 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)
315 lines
10 KiB
Python
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()
|