#!/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!")