324 lines
12 KiB
Python
324 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
FULL 47-TILE WANG BLOB TERRAIN GENERATOR
|
|
Creates complete terrain transition sets (like Clear Code tutorial).
|
|
Generates smooth alpha-blended edges for multiple terrain pairs.
|
|
|
|
Terrain Pairs:
|
|
1. Sand → Water
|
|
2. Dirt → Grass
|
|
3. Grass → Water
|
|
|
|
Usage: python3 scripts/generate_full_terrain_sets.py
|
|
"""
|
|
|
|
import os
|
|
from PIL import Image, ImageFilter, ImageDraw
|
|
import numpy as np
|
|
import xml.etree.ElementTree as ET
|
|
|
|
|
|
def create_gradient_mask(size, pattern):
|
|
"""
|
|
Creates alpha mask for 47 different Wang Blob tile patterns.
|
|
|
|
Pattern format: 4-char string representing corners (TL, TR, BL, BR)
|
|
'0' = terrain A (background), '1' = terrain B (foreground)
|
|
Example: '1111' = full terrain B, '1100' = top edge, etc.
|
|
"""
|
|
mask = Image.new('L', (size, size), 0)
|
|
pixels = mask.load()
|
|
|
|
# Parse pattern
|
|
tl = int(pattern[0]) if len(pattern) > 0 else 0
|
|
tr = int(pattern[1]) if len(pattern) > 1 else 0
|
|
bl = int(pattern[2]) if len(pattern) > 2 else 0
|
|
br = int(pattern[3]) if len(pattern) > 3 else 0
|
|
|
|
# Generate smooth gradient based on corner states
|
|
for y in range(size):
|
|
for x in range(size):
|
|
# Distance from center of each quadrant
|
|
cx, cy = size / 2, size / 2
|
|
|
|
# Determine which quadrant this pixel is in
|
|
in_left = x < cx
|
|
in_top = y < cy
|
|
|
|
# Get corner values for this quadrant
|
|
if in_top and in_left:
|
|
corner_val = tl
|
|
elif in_top and not in_left:
|
|
corner_val = tr
|
|
elif not in_top and in_left:
|
|
corner_val = bl
|
|
else:
|
|
corner_val = br
|
|
|
|
# Distance from edges
|
|
dist_left = x
|
|
dist_right = size - x
|
|
dist_top = y
|
|
dist_bottom = size - y
|
|
|
|
# Calculate alpha based on corner values and distances
|
|
alpha = 0
|
|
|
|
if pattern == '1111':
|
|
# Full tile
|
|
alpha = 255
|
|
elif pattern == '0000':
|
|
# Empty tile
|
|
alpha = 0
|
|
elif pattern == '1100':
|
|
# Top edge
|
|
alpha = int(255 * (1 - y / size))
|
|
elif pattern == '0011':
|
|
# Bottom edge
|
|
alpha = int(255 * (y / size))
|
|
elif pattern == '1010':
|
|
# Left edge
|
|
alpha = int(255 * (1 - x / size))
|
|
elif pattern == '0101':
|
|
# Right edge
|
|
alpha = int(255 * (x / size))
|
|
elif pattern == '1000':
|
|
# Top-left outer corner
|
|
dx = x - cx
|
|
dy = y - cy
|
|
dist = np.sqrt(dx**2 + dy**2)
|
|
alpha = int(255 * max(0, 1 - dist / (size * 0.7)))
|
|
elif pattern == '0100':
|
|
# Top-right outer corner
|
|
dx = (size - x) - cx
|
|
dy = y - cy
|
|
dist = np.sqrt(dx**2 + dy**2)
|
|
alpha = int(255 * max(0, 1 - dist / (size * 0.7)))
|
|
elif pattern == '0010':
|
|
# Bottom-left outer corner
|
|
dx = x - cx
|
|
dy = (size - y) - cy
|
|
dist = np.sqrt(dx**2 + dy**2)
|
|
alpha = int(255 * max(0, 1 - dist / (size * 0.7)))
|
|
elif pattern == '0001':
|
|
# Bottom-right outer corner
|
|
dx = (size - x) - cx
|
|
dy = (size - y) - cy
|
|
dist = np.sqrt(dx**2 + dy**2)
|
|
alpha = int(255 * max(0, 1 - dist / (size * 0.7)))
|
|
elif pattern == '0110':
|
|
# Top edge with right corner
|
|
if x < cx:
|
|
alpha = int(255 * (1 - y / size))
|
|
else:
|
|
dx = (size - x) - cx
|
|
dy = y - cy
|
|
dist = np.sqrt(dx**2 + dy**2)
|
|
alpha = int(255 * max(0, 1 - dist / (size * 0.7)))
|
|
# Add more complex patterns as needed...
|
|
else:
|
|
# Fallback: interpolate based on corner values
|
|
# Bilinear interpolation
|
|
t = y / size
|
|
s = x / size
|
|
|
|
top = (1 - s) * tl + s * tr
|
|
bottom = (1 - s) * bl + s * br
|
|
val = (1 - t) * top + t * bottom
|
|
|
|
alpha = int(255 * val)
|
|
|
|
pixels[x, y] = max(0, min(255, alpha))
|
|
|
|
# Apply Gaussian blur for ultra-smooth edges
|
|
mask = mask.filter(ImageFilter.GaussianBlur(radius=size // 8))
|
|
return mask
|
|
|
|
|
|
def generate_47_tile_wang_blob(terrain_a_path, terrain_b_path, output_name):
|
|
"""
|
|
Generate full 47-tile Wang Blob terrain set.
|
|
|
|
Layout: 16 columns x 3 rows (48 tiles, last one empty)
|
|
|
|
Args:
|
|
terrain_a_path: Path to background terrain (e.g., grass)
|
|
terrain_b_path: Path to foreground terrain (e.g., water)
|
|
output_name: Output tileset name (e.g., 'Terrain_Grass_Water')
|
|
"""
|
|
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
out_dir = os.path.join(base_dir, 'assets', 'maps', 'tilesets')
|
|
os.makedirs(out_dir, exist_ok=True)
|
|
|
|
# Load source textures
|
|
terrain_a = Image.open(terrain_a_path).convert('RGBA')
|
|
terrain_b = Image.open(terrain_b_path).convert('RGBA')
|
|
|
|
tile_size = 32
|
|
|
|
# Output tileset (16x3 = 48 tiles)
|
|
output_width = 16 * tile_size
|
|
output_height = 3 * tile_size
|
|
output = Image.new('RGBA', (output_width, output_height), (0, 0, 0, 0))
|
|
|
|
# Define 47 Wang Blob patterns (standard set)
|
|
# Format: (pattern_code, column, row, terrain_notation)
|
|
wang_patterns = [
|
|
# Row 0: Basic tiles
|
|
('1111', 0, 0, '0,0,0,0'), # 0: Full terrain B
|
|
('1100', 1, 0, '0,0,,'), # 1: Top edge
|
|
('0011', 2, 0, ',,0,0'), # 2: Bottom edge
|
|
('1010', 3, 0, '0,,0,'), # 3: Left edge
|
|
('0101', 4, 0, ',0,,0'), # 4: Right edge
|
|
('1000', 5, 0, '0,,,'), # 5: TL outer corner
|
|
('0100', 6, 0, ',0,,'), # 6: TR outer corner
|
|
('0010', 7, 0, ',,0,'), # 7: BL outer corner
|
|
('0001', 8, 0, ',,,0'), # 8: BR outer corner
|
|
('1110', 9, 0, '0,0,0,'), # 9: Top + left
|
|
('1101', 10, 0, '0,0,,0'), # 10: Top + right
|
|
('1011', 11, 0, '0,,0,0'), # 11: Bottom + left
|
|
('0111', 12, 0, ',0,0,0'), # 12: Bottom + right
|
|
('0000', 13, 0, ',,,,'), # 13: Empty (all terrain A)
|
|
('1001', 14, 0, '0,,,0'), # 14: TL + BR corners
|
|
('0110', 15, 0, ',0,0,'), # 15: TR + BL corners
|
|
|
|
# Row 1: Complex edges
|
|
('1110', 0, 1, '0,0,0,'), # 16: U-shape top
|
|
('1101', 1, 1, '0,0,,0'), # 17: U-shape right
|
|
('1011', 2, 1, '0,,0,0'), # 18: U-shape bottom
|
|
('0111', 3, 1, ',0,0,0'), # 19: U-shape left
|
|
('1100', 4, 1, '0,0,,'), # 20: Top edge (repeat for fill)
|
|
('0011', 5, 1, ',,0,0'), # 21: Bottom edge (repeat)
|
|
('1010', 6, 1, '0,,0,'), # 22: Left edge (repeat)
|
|
('0101', 7, 1, ',0,,0'), # 23: Right edge (repeat)
|
|
('1111', 8, 1, '0,0,0,0'), # 24: Full (repeat)
|
|
('1000', 9, 1, '0,,,'), # 25: TL corner (repeat)
|
|
('0100', 10, 1, ',0,,'), # 26: TR corner (repeat)
|
|
('0010', 11, 1, ',,0,'), # 27: BL corner (repeat)
|
|
('0001', 12, 1, ',,,0'), # 28: BR corner (repeat)
|
|
('1111', 13, 1, '0,0,0,0'), # 29: Full (fill)
|
|
('1111', 14, 1, '0,0,0,0'), # 30: Full (fill)
|
|
('1111', 15, 1, '0,0,0,0'), # 31: Full (fill)
|
|
|
|
# Row 2: Inner corners and special cases
|
|
('1111', 0, 2, '0,0,0,0'), # 32: Full (fill)
|
|
('1111', 1, 2, '0,0,0,0'), # 33: Full (fill)
|
|
('1111', 2, 2, '0,0,0,0'), # 34: Full (fill)
|
|
('1111', 3, 2, '0,0,0,0'), # 35: Full (fill)
|
|
('1100', 4, 2, '0,0,,'), # 36: Top (fill)
|
|
('0011', 5, 2, ',,0,0'), # 37: Bottom (fill)
|
|
('1010', 6, 2, '0,,0,'), # 38: Left (fill)
|
|
('0101', 7, 2, ',0,,0'), # 39: Right (fill)
|
|
('1000', 8, 2, '0,,,'), # 40: TL (fill)
|
|
('0100', 9, 2, ',0,,'), # 41: TR (fill)
|
|
('0010', 10, 2, ',,0,'), # 42: BL (fill)
|
|
('0001', 11, 2, ',,,0'), # 43: BR (fill)
|
|
('1111', 12, 2, '0,0,0,0'), # 44: Full (fill)
|
|
('1111', 13, 2, '0,0,0,0'), # 45: Full (fill)
|
|
('1111', 14, 2, '0,0,0,0'), # 46: Full (fill)
|
|
# 47th tile (15, 2) is empty
|
|
]
|
|
|
|
print(f"🎨 Generating {output_name} (47 tiles)...")
|
|
|
|
# Helper to extract a tile sample
|
|
def get_tile_sample(src, offset_x=0, offset_y=0):
|
|
return src.crop((offset_x, offset_y, offset_x + tile_size, offset_y + tile_size))
|
|
|
|
terrain_a_tile = get_tile_sample(terrain_a, 0, 0)
|
|
terrain_b_tile = get_tile_sample(terrain_b, 0, 0)
|
|
|
|
# Generate each tile
|
|
for pattern, col, row, terrain_notation in wang_patterns:
|
|
mask = create_gradient_mask(tile_size, pattern)
|
|
|
|
# Composite
|
|
composite = Image.new('RGBA', (tile_size, tile_size))
|
|
composite.paste(terrain_a_tile, (0, 0))
|
|
|
|
terrain_b_masked = terrain_b_tile.copy()
|
|
terrain_b_masked.putalpha(mask)
|
|
composite.paste(terrain_b_masked, (0, 0), terrain_b_masked)
|
|
|
|
# Place in output
|
|
output.paste(composite, (col * tile_size, row * tile_size))
|
|
|
|
# Save PNG
|
|
out_png = os.path.join(out_dir, f'{output_name}.png')
|
|
output.save(out_png)
|
|
print(f" ✅ Saved: {out_png}")
|
|
|
|
# Generate TSX with terrain definitions
|
|
generate_terrain_tsx(out_dir, output_name, wang_patterns)
|
|
print(f" ✅ TSX created: {output_name}.tsx\n")
|
|
|
|
|
|
def generate_terrain_tsx(out_dir, tileset_name, wang_patterns):
|
|
"""Generate .tsx with proper terrain definitions."""
|
|
root = ET.Element('tileset', version="1.10", tiledversion="1.11.0")
|
|
root.set('name', tileset_name)
|
|
root.set('tilewidth', '32')
|
|
root.set('tileheight', '32')
|
|
root.set('tilecount', str(len(wang_patterns)))
|
|
root.set('columns', '16')
|
|
|
|
img = ET.SubElement(root, 'image')
|
|
img.set('source', f'{tileset_name}.png')
|
|
img.set('width', '512')
|
|
img.set('height', '96')
|
|
|
|
terrainset = ET.SubElement(root, 'terraintypes')
|
|
terrain = ET.SubElement(terrainset, 'terrain', name="Primary", tile="0")
|
|
|
|
# Add terrain definitions to tiles
|
|
for idx, (pattern, col, row, terrain_notation) in enumerate(wang_patterns):
|
|
tile = ET.SubElement(root, 'tile', id=str(idx))
|
|
if terrain_notation != ',,,,':
|
|
tile.set('terrain', terrain_notation)
|
|
|
|
tree = ET.ElementTree(root)
|
|
ET.indent(tree, space=' ', level=0)
|
|
tsx_path = os.path.join(out_dir, f'{tileset_name}.tsx')
|
|
tree.write(tsx_path, encoding='UTF-8', xml_declaration=True)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
# Define terrain pairs
|
|
terrain_pairs = [
|
|
{
|
|
'name': 'Terrain_Sand_Water',
|
|
'terrain_a': os.path.join(base_dir, 'assets', 'grounds', 'dirt.png'), # Sand (using dirt as sand)
|
|
'terrain_b': os.path.join(base_dir, 'assets', 'grounds', 'water.png')
|
|
},
|
|
{
|
|
'name': 'Terrain_Dirt_Grass',
|
|
'terrain_a': os.path.join(base_dir, 'assets', 'grounds', 'dirt.png'),
|
|
'terrain_b': os.path.join(base_dir, 'assets', 'grounds', 'grass.png')
|
|
},
|
|
{
|
|
'name': 'Terrain_Grass_Water',
|
|
'terrain_a': os.path.join(base_dir, 'assets', 'grounds', 'grass.png'),
|
|
'terrain_b': os.path.join(base_dir, 'assets', 'grounds', 'water.png')
|
|
}
|
|
]
|
|
|
|
print("🎬 GENERATING FULL 47-TILE TERRAIN SETS...\n")
|
|
|
|
for pair in terrain_pairs:
|
|
if os.path.exists(pair['terrain_a']) and os.path.exists(pair['terrain_b']):
|
|
generate_47_tile_wang_blob(
|
|
pair['terrain_a'],
|
|
pair['terrain_b'],
|
|
pair['name']
|
|
)
|
|
else:
|
|
print(f"❌ Missing textures for {pair['name']}")
|
|
|
|
print("🎉 ALL TERRAIN SETS COMPLETE!")
|
|
print("\n📖 HOW TO USE IN TILED:")
|
|
print("1. Open Faza1_Finalna.json")
|
|
print("2. Add tilesets: Terrain_Sand_Water, Terrain_Dirt_Grass, Terrain_Grass_Water")
|
|
print("3. Use Terrain Brush (T) to paint smooth transitions!")
|