209 lines
7.8 KiB
Python
209 lines
7.8 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
TERRAIN TRANSITION GENERATOR
|
|
Creates smooth alpha-blended edge tiles for Tiled terrain sets.
|
|
Generates Water→Grass transitions using Wang/Blob tile patterns.
|
|
|
|
Usage: python scripts/generate_terrain_transitions.py
|
|
Output: assets/maps/tilesets/Terrain_Transitions.png + .tsx
|
|
"""
|
|
|
|
import os
|
|
from PIL import Image, ImageFilter, ImageDraw
|
|
import numpy as np
|
|
|
|
def create_smooth_gradient_mask(size, edge_type):
|
|
"""
|
|
Creates a gradient alpha mask for different edge types.
|
|
edge_type: 'top', 'bottom', 'left', 'right', 'tl', 'tr', 'bl', 'br', 'center'
|
|
"""
|
|
mask = Image.new('L', (size, size), 0)
|
|
draw = ImageDraw.Draw(mask)
|
|
|
|
# Full opacity in center, fade to edges
|
|
for i in range(size):
|
|
for j in range(size):
|
|
# Distance from center
|
|
dx = abs(i - size/2)
|
|
dy = abs(j - size/2)
|
|
|
|
# Gradient based on edge type
|
|
if edge_type == 'center':
|
|
# Full water
|
|
mask.putpixel((i, j), 255)
|
|
elif edge_type == 'top':
|
|
# Fade from bottom to top
|
|
alpha = int(255 * (1 - j / size))
|
|
mask.putpixel((i, j), alpha)
|
|
elif edge_type == 'bottom':
|
|
# Fade from top to bottom
|
|
alpha = int(255 * (j / size))
|
|
mask.putpixel((i, j), alpha)
|
|
elif edge_type == 'left':
|
|
# Fade from right to left
|
|
alpha = int(255 * (1 - i / size))
|
|
mask.putpixel((i, j), alpha)
|
|
elif edge_type == 'right':
|
|
# Fade from left to right
|
|
alpha = int(255 * (i / size))
|
|
mask.putpixel((i, j), alpha)
|
|
elif edge_type == 'tl':
|
|
# Top-left corner
|
|
dist = np.sqrt(dx**2 + dy**2)
|
|
alpha = int(255 * max(0, 1 - dist / (size * 0.7)))
|
|
mask.putpixel((i, j), alpha)
|
|
elif edge_type == 'tr':
|
|
# Top-right corner
|
|
dx_flip = abs(i - size/2)
|
|
dist = np.sqrt(dx_flip**2 + dy**2)
|
|
alpha = int(255 * max(0, 1 - dist / (size * 0.7)))
|
|
mask.putpixel((i, j), alpha)
|
|
elif edge_type == 'bl':
|
|
# Bottom-left corner
|
|
dy_flip = abs(j - size/2)
|
|
dist = np.sqrt(dx**2 + dy_flip**2)
|
|
alpha = int(255 * max(0, 1 - dist / (size * 0.7)))
|
|
mask.putpixel((i, j), alpha)
|
|
elif edge_type == 'br':
|
|
# Bottom-right corner
|
|
dx_flip = abs(i - size/2)
|
|
dy_flip = abs(j - size/2)
|
|
dist = np.sqrt(dx_flip**2 + dy_flip**2)
|
|
alpha = int(255 * max(0, 1 - dist / (size * 0.7)))
|
|
mask.putpixel((i, j), alpha)
|
|
|
|
# Apply Gaussian blur for ultra-smooth edges
|
|
mask = mask.filter(ImageFilter.GaussianBlur(radius=size // 8))
|
|
return mask
|
|
|
|
|
|
def generate_terrain_transitions():
|
|
"""
|
|
Generates a 47-tile Wang/Blob tileset for water-grass transitions.
|
|
Layout: 16x3 grid (48 tiles total, last one empty)
|
|
|
|
Tile layout (standard blob tileset):
|
|
Row 1: Center, edges (top, right, bottom, left, corners)
|
|
Row 2: Complex edges and inner corners
|
|
Row 3: Special cases
|
|
"""
|
|
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
# Load source textures
|
|
grass_path = os.path.join(base_dir, 'assets', 'grounds', 'grass.png')
|
|
water_path = os.path.join(base_dir, 'assets', 'grounds', 'water.png')
|
|
|
|
if not os.path.exists(grass_path) or not os.path.exists(water_path):
|
|
print("❌ Missing source textures (grass.png or water.png)")
|
|
return
|
|
|
|
grass_src = Image.open(grass_path).convert('RGBA')
|
|
water_src = Image.open(water_path).convert('RGBA')
|
|
|
|
# Tile size
|
|
tile_size = 32
|
|
|
|
# Output tileset (16 columns x 3 rows = 48 tiles)
|
|
output_width = 16 * tile_size # 512px
|
|
output_height = 3 * tile_size # 96px
|
|
output = Image.new('RGBA', (output_width, output_height), (0, 0, 0, 0))
|
|
|
|
# Helper to extract a tile from source texture
|
|
def get_tile(src, x_offset=0, y_offset=0):
|
|
"""Extract a 32x32 tile from source texture."""
|
|
return src.crop((x_offset, y_offset, x_offset + tile_size, y_offset + tile_size))
|
|
|
|
# Sample tiles from source
|
|
grass_tile = get_tile(grass_src, 0, 0)
|
|
water_tile = get_tile(water_src, 0, 0)
|
|
|
|
# Define tile types and their masks
|
|
tile_definitions = [
|
|
# Row 0 (Basic tiles + edges)
|
|
('center', 0, 0), # 0: Full water
|
|
('top', 1, 0), # 1: Water edge at top
|
|
('right', 2, 0), # 2: Water edge at right
|
|
('bottom', 3, 0), # 3: Water edge at bottom
|
|
('left', 4, 0), # 4: Water edge at left
|
|
('tl', 5, 0), # 5: Top-left outer corner
|
|
('tr', 6, 0), # 6: Top-right outer corner
|
|
('bl', 7, 0), # 7: Bottom-left outer corner
|
|
('br', 8, 0), # 8: Bottom-right outer corner
|
|
# ... add more complex tiles as needed
|
|
]
|
|
|
|
print("🌊 Generating Water→Grass terrain transitions...")
|
|
|
|
for edge_type, col, row in tile_definitions:
|
|
# Create mask
|
|
mask = create_smooth_gradient_mask(tile_size, edge_type)
|
|
|
|
# Composite: Water (foreground) over Grass (background)
|
|
composite = Image.new('RGBA', (tile_size, tile_size))
|
|
composite.paste(grass_tile, (0, 0))
|
|
|
|
# Apply water with mask
|
|
water_with_alpha = water_tile.copy()
|
|
water_with_alpha.putalpha(mask)
|
|
composite.paste(water_with_alpha, (0, 0), water_with_alpha)
|
|
|
|
# Place in output
|
|
output.paste(composite, (col * tile_size, row * tile_size))
|
|
print(f" ✅ Tile {col},{row}: {edge_type}")
|
|
|
|
# Save output
|
|
out_dir = os.path.join(base_dir, 'assets', 'maps', 'tilesets')
|
|
os.makedirs(out_dir, exist_ok=True)
|
|
|
|
out_path = os.path.join(out_dir, 'Terrain_Transitions.png')
|
|
output.save(out_path)
|
|
print(f"✅ Saved transition tileset: {out_path}")
|
|
|
|
# Generate TSX with Terrain definitions
|
|
generate_terrain_tsx(out_dir, tile_definitions)
|
|
|
|
|
|
def generate_terrain_tsx(out_dir, tile_definitions):
|
|
"""Generate .tsx file with Terrain Set definitions."""
|
|
import xml.etree.ElementTree as ET
|
|
|
|
# Create tileset XML
|
|
root = ET.Element('tileset', version="1.10", tiledversion="1.11.0")
|
|
root.set('name', 'Terrain_Transitions')
|
|
root.set('tilewidth', '32')
|
|
root.set('tileheight', '32')
|
|
root.set('tilecount', str(len(tile_definitions)))
|
|
root.set('columns', '16')
|
|
|
|
# Image source
|
|
img = ET.SubElement(root, 'image')
|
|
img.set('source', 'Terrain_Transitions.png')
|
|
img.set('width', '512')
|
|
img.set('height', '96')
|
|
|
|
# Terrain definitions (Tiled 1.10+ format)
|
|
terrainset = ET.SubElement(root, 'terraintypes')
|
|
terrain = ET.SubElement(terrainset, 'terrain', name="Water", tile="0")
|
|
|
|
# Assign terrain IDs to tiles (Wang blob pattern)
|
|
# This part is complex - for now, just mark center tile
|
|
for idx, (edge_type, col, row) in enumerate(tile_definitions):
|
|
if edge_type == 'center':
|
|
tile = ET.SubElement(root, 'tile', id=str(idx))
|
|
tile.set('terrain', '0,0,0,0') # Full water on all corners
|
|
|
|
# Write TSX
|
|
tree = ET.ElementTree(root)
|
|
ET.indent(tree, space=' ', level=0)
|
|
tsx_path = os.path.join(out_dir, 'Terrain_Transitions.tsx')
|
|
tree.write(tsx_path, encoding='UTF-8', xml_declaration=True)
|
|
print(f"✅ Saved TSX with terrain definitions: {tsx_path}")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
generate_terrain_transitions()
|
|
print("\n🎬 DONE! Now open Tiled and:")
|
|
print(" 1. Add 'Terrain_Transitions' tileset to your map")
|
|
print(" 2. Use the Terrain Brush (T) to paint smooth water edges")
|
|
print(" 3. Enjoy cinematic transitions! 🎥")
|