chore: Update LDtk project with new layers and fixed assets dimensions (using sips)

This commit is contained in:
2026-01-14 23:22:59 +01:00
parent 4ef1adc413
commit bc2225ad64
3494 changed files with 49932 additions and 25218 deletions

View File

@@ -0,0 +1,74 @@
import os
from PIL import Image, ImageFilter, ImageEnhance
def process_dreamy_intro():
# Paths
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
src_dir = os.path.join(base_dir, 'assets', 'references', 'intro_shots')
out_dir = os.path.join(base_dir, 'assets', 'images', 'intro_sequence')
if not os.path.exists(src_dir):
print(f"Error: Source directory not found: {src_dir}")
return
if not os.path.exists(out_dir):
os.makedirs(out_dir)
print(f"Created output directory: {out_dir}")
# List of key images to process
target_files = [
'otac_longboard_pier.png',
'birthday_cake_rd.png',
'kai_first_dreads_family.png',
'ana_barbershop_dreads.png',
'zombie_silhouettes_panic.png',
'chaos_streets_apocalypse.png',
'parents_transparent_ghosts.png',
'family_portrait_complete.png'
]
print("--- 🌫️ CREATING DREAMY FILTER VERSIONS 🌫️ ---")
for filename in target_files:
src_path = os.path.join(src_dir, filename)
if not os.path.exists(src_path):
print(f"⚠️ Missing: {filename}")
continue
try:
# Load Clean Image
img = Image.open(src_path).convert('RGB')
# 1. Save Clean Version (Optimized/Resized if needed, but keeping original size for now)
clean_name = filename.replace('.png', '_clean.png')
clean_path = os.path.join(out_dir, clean_name)
img.save(clean_path)
print(f"✅ Saved Clean: {clean_name}")
# 2. Create Dreamy/Blur Version
# Effect: Gaussian Blur + slightly increased Brightness (bloom effect)
blur_img = img.filter(ImageFilter.GaussianBlur(radius=15))
# Add "Bloom" (Brighten light areas)
enhancer = ImageEnhance.Brightness(blur_img)
blur_img = enhancer.enhance(1.2)
# Add slight saturation decrease for "fade" look?
# sat_enhancer = ImageEnhance.Color(blur_img)
# blur_img = sat_enhancer.enhance(0.8)
blur_name = filename.replace('.png', '_dreamy.png')
blur_path = os.path.join(out_dir, blur_name)
blur_img.save(blur_path)
print(f"🌫️ Saved Dreamy: {blur_name}")
except Exception as e:
print(f"❌ Error processing {filename}: {e}")
print("--- DONE ---")
print(f"Images are ready in: {out_dir}")
if __name__ == "__main__":
process_dreamy_intro()

View File

@@ -0,0 +1,323 @@
#!/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!")

View File

@@ -0,0 +1,142 @@
import os
import xml.etree.ElementTree as ET
from PIL import Image
import numpy as np
def generate_nature_animation():
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
out_dir = os.path.join(base_dir, 'assets', 'maps', 'tilesets')
# 1. GRASS
grass_src = os.path.join(base_dir, 'assets', 'grounds', 'grass.png')
# 2. WATER
water_src = os.path.join(base_dir, 'assets', 'grounds', 'water.png')
# 3. CANNABIS
# Try finding loop to locate specific file
import glob
cannabis_matches = glob.glob(os.path.join(base_dir, '**', '*cannabis*stage4*.png'), recursive=True)
cannabis_src = None
if cannabis_matches:
cannabis_src = cannabis_matches[0] # Pick first valid
else:
# Fallback
cannabis_src = os.path.join(base_dir, 'assets', 'intro_assets', 'cannabis_stage4.png') # If exists?
targets = []
if os.path.exists(grass_src):
targets.append(('Grass_Animated', grass_src))
if os.path.exists(water_src):
targets.append(('Water_Animated', water_src))
if cannabis_src and os.path.exists(cannabis_src):
targets.append(('Cannabis_Animated', cannabis_src))
else:
print("⚠️ Could not find Cannabis Stage 4 image.")
if not os.path.exists(out_dir):
os.makedirs(out_dir)
# Function to create wind sway (Skew) - FOR GRASS ONLY
def create_sway_frame(image, factor=0.0):
"""Subtle skew transform for grass wind effect."""
width, height = image.size
return image.transform((width, height), Image.AFFINE, (1, factor, 0, 0, 1, 0), resample=Image.BILINEAR)
# Function to create water glimmer (Subtle brightness/color shift)
def create_water_glimmer_frame(image, brightness_factor=1.0):
"""
Creates a subtle glimmer effect for water.
brightness_factor: 1.0 = normal, 1.05 = slightly brighter (glimmer peak)
"""
from PIL import ImageEnhance
# Brightness shift for shimmer
enhancer = ImageEnhance.Brightness(image)
return enhancer.enhance(brightness_factor)
for name, src in targets:
print(f"Processing {name} from {src}...")
img = Image.open(src).convert('RGBA')
width, height = img.size
# Different animation based on type
if 'Grass' in name or 'Cannabis' in name:
# GRASS: Wind sway (gentle skew)
print(f" → Grass/Plant: Using wind sway animation")
f1 = img
f2 = create_sway_frame(img, -0.08) # Gentle right sway
f3 = img
f4 = create_sway_frame(img, 0.08) # Gentle left sway
elif 'Water' in name:
# WATER: Glimmer effect (brightness shift, NO movement)
print(f" → Water: Using glimmer animation (no skew)")
f1 = img
f2 = create_water_glimmer_frame(img, 1.03) # Subtle brighten
f3 = img
f4 = create_water_glimmer_frame(img, 0.97) # Subtle darken
else:
# Fallback
f1 = f2 = f3 = f4 = img
frames = [f1, f2, f3, f4]
# Combine
total_height = height * 4
combined = Image.new('RGBA', (width, total_height))
for i, f in enumerate(frames):
combined.paste(f, (0, i * height))
out_img_path = os.path.join(out_dir, f"{name}.png")
combined.save(out_img_path)
print(f"Saved PNG: {out_img_path}")
# Generate TSX
tile_w, tile_h = 32, 32 # Assuming standard tiles
# If image is larger (like Cannabis sprite), we treat it as big tiles?
# Tiled animations work on per-tile basis.
# If Cannabis is 64x128, it's composed of 8 tiles (2x4).
# We need to animate EACH tile.
cols = width // tile_w
rows = height // tile_h
total_tiles_per_frame = cols * rows
total_tiles_all = total_tiles_per_frame * 4
root = ET.Element("tileset")
root.set("version", "1.10")
root.set("tiledversion", "1.11.0")
root.set("name", name)
root.set("tilewidth", str(tile_w))
root.set("tileheight", str(tile_h))
root.set("tilecount", str(total_tiles_all))
root.set("columns", str(cols))
image_node = ET.SubElement(root, "image")
image_node.set("source", f"{name}.png")
image_node.set("width", str(width))
image_node.set("height", str(total_height))
# Anim definitions
for i in range(total_tiles_per_frame):
tile_node = ET.SubElement(root, "tile")
tile_node.set("id", str(i))
anim_node = ET.SubElement(tile_node, "animation")
for k in range(4):
frame_node = ET.SubElement(anim_node, "frame")
frame_node.set("tileid", str(i + k * total_tiles_per_frame))
frame_node.set("duration", "200") # Slower wind
tree = ET.ElementTree(root)
ET.indent(tree, space=" ", level=0)
out_tsx_path = os.path.join(out_dir, f"{name}.tsx")
tree.write(out_tsx_path, encoding='UTF-8', xml_declaration=True)
print(f"Saved TSX: {out_tsx_path}")
if __name__ == "__main__":
generate_nature_animation()

View File

@@ -0,0 +1,164 @@
#!/usr/bin/env python3
"""
TALL GRASS ANIMATED GENERATOR
Creates individual animated tall grass tufts for Stardew-style harvesting.
Features:
- Transparent background (prosojen backdrop)
- Wind sway animation (4 frames)
- Harvestable property (isHarvestable: true)
- No LSD glitching (smooth, subtle animation)
Output: Tall_Grass_Animated.tsx + PNG
"""
import os
from PIL import Image, ImageDraw, ImageFilter, ImageEnhance
import xml.etree.ElementTree as ET
def generate_grass_tuft(width, height, grass_color, sway_offset=0):
"""
Generate a single grass tuft sprite with wind sway.
Args:
width: Tuft width in pixels
height: Tuft height in pixels
grass_color: RGB tuple for grass color
sway_offset: Horizontal offset for wind animation (-3 to +3 pixels)
Returns:
PIL Image with transparent background
"""
img = Image.new('RGBA', (width, height), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
# Grass blade parameters
num_blades = 8
blade_width = 3
blade_height = height - 4
# Draw individual grass blades
for i in range(num_blades):
x_base = (i / num_blades) * width
# Add sway offset (wind effect)
x_top = x_base + sway_offset
# Blade color variation (darker at base, lighter at tip)
r, g, b = grass_color
shade_factor = 0.7 + (i / num_blades) * 0.3
blade_color = (
int(r * shade_factor),
int(g * shade_factor),
int(b * shade_factor),
255
)
# Draw blade as tapered polygon
blade_points = [
(x_base, height), # Base left
(x_base + blade_width, height), # Base right
(x_top + blade_width/2, 2) # Top (tapered)
]
draw.polygon(blade_points, fill=blade_color)
# Apply slight blur for softer edges (anti-aliasing)
img = img.filter(ImageFilter.SMOOTH)
return img
def generate_tall_grass_animated():
"""Generate full Tall_Grass_Animated tileset with 4-frame wind animation."""
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)
# Tile params
tile_size = 32
tuft_width = 24
tuft_height = 28
# Grass color (vibrant green)
grass_color = (60, 180, 50)
# 4 animation frames (wind sway)
sway_offsets = [0, -2, 0, 2] # Gentle sway (NO LSD!)
# Output tileset: 4 columns x 1 row (4 frames)
output_width = 4 * tile_size
output_height = 1 * tile_size
output = Image.new('RGBA', (output_width, output_height), (0, 0, 0, 0))
print("🌾 Generating Tall_Grass_Animated (4 frames)...")
for frame_idx, sway in enumerate(sway_offsets):
# Generate grass tuft
tuft = generate_grass_tuft(tuft_width, tuft_height, grass_color, sway)
# Create tile-sized container with centered tuft
tile = Image.new('RGBA', (tile_size, tile_size), (0, 0, 0, 0))
# Center tuft in tile
offset_x = (tile_size - tuft_width) // 2
offset_y = tile_size - tuft_height
tile.paste(tuft, (offset_x, offset_y), tuft)
# Place in output
output.paste(tile, (frame_idx * tile_size, 0))
print(f" ✅ Frame {frame_idx + 1}: Sway offset {sway}px")
# Save PNG
out_png = os.path.join(out_dir, 'Tall_Grass_Animated.png')
output.save(out_png)
print(f" ✅ Saved: {out_png}")
# Generate TSX with animation + harvestable property
generate_tall_grass_tsx(out_dir)
print(f" ✅ TSX created: Tall_Grass_Animated.tsx\n")
def generate_tall_grass_tsx(out_dir):
"""Generate .tsx with animation definitions and harvestable property."""
root = ET.Element('tileset', version="1.10", tiledversion="1.11.0")
root.set('name', 'Tall_Grass_Animated')
root.set('tilewidth', '32')
root.set('tileheight', '32')
root.set('tilecount', '4')
root.set('columns', '4')
img = ET.SubElement(root, 'image')
img.set('source', 'Tall_Grass_Animated.png')
img.set('width', '128')
img.set('height', '32')
# Tile 0: Animated with harvestable property
tile = ET.SubElement(root, 'tile', id='0')
# Properties
props = ET.SubElement(tile, 'properties')
prop = ET.SubElement(props, 'property', name='isHarvestable', type='bool', value='true')
# Animation (4 frames, 250ms each = smooth wind)
anim = ET.SubElement(tile, 'animation')
for frame_id in range(4):
frame = ET.SubElement(anim, 'frame', tileid=str(frame_id), duration='250')
tree = ET.ElementTree(root)
ET.indent(tree, space=' ', level=0)
tsx_path = os.path.join(out_dir, 'Tall_Grass_Animated.tsx')
tree.write(tsx_path, encoding='UTF-8', xml_declaration=True)
if __name__ == '__main__':
print("🌾 GENERATING TALL GRASS ANIMATED TILESET...\n")
generate_tall_grass_animated()
print("🎉 TALL GRASS COMPLETE!")
print("\n📖 HOW TO USE IN TILED:")
print("1. Add Tall_Grass_Animated.tsx to your map")
print("2. Paint grass tufts on a separate layer")
print("3. Each tuft has isHarvestable: true")
print("4. In game, Kai can harvest with scythe!")
print("\n⚠️ REMEMBER: Keep ground layer STATIC (no animation waste)!")

View File

@@ -0,0 +1,208 @@
#!/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! 🎥")

View File

@@ -0,0 +1,208 @@
#!/usr/bin/env python3
"""
GODOT 4 TILESET GENERATOR
Converts Phaser/Tiled tilesets to Godot 4 TileSet format (.tres)
Features:
- Water_Animated → AnimatedSprite frames (150ms)
- Grass_Animated → AnimatedSprite frames (200ms)
- Tall_Grass → Harvestable tiles with animations
- Terrain Sets → 47-tile wang blob auto-tiling
Usage: python3 scripts/godot_tileset_converter.py
"""
import os
from pathlib import Path
def generate_water_animated_tileset():
"""Generate Water_Animated.tres with animation frames."""
content = """[gd_resource type="TileSet" format=3]
[ext_resource type="Texture2D" path="res://assets/tilesets/Water_Animated.png" id="1"]
[sub_resource type="TileSetAtlasSource" id="TileSetAtlasSource_water"]
texture = ExtResource("1")
texture_region_size = Vector2i(32, 32)
0:0/0 = 0
0:0/0/terrain_set = 0
0:0/0/animation_columns = 4
0:0/0/animation_frames_durations = [0.15, 0.15, 0.15, 0.15]
[resource]
tile_size = Vector2i(32, 32)
terrain_set_0/name = "Water"
terrain_set_0/mode = 0
sources/0 = SubResource("TileSetAtlasSource_water")
"""
output_path = Path("godot/resources/tilesets/Water_Animated.tres")
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(content)
print(f"✅ Created: {output_path}")
def generate_grass_animated_tileset():
"""Generate Grass_Animated.tres with wind sway animation."""
content = """[gd_resource type="TileSet" format=3]
[ext_resource type="Texture2D" path="res://assets/tilesets/Grass_Animated.png" id="1"]
[sub_resource type="TileSetAtlasSource" id="TileSetAtlasSource_grass"]
texture = ExtResource("1")
texture_region_size = Vector2i(32, 32)
0:0/0 = 0
0:0/0/animation_columns = 4
0:0/0/animation_frames_durations = [0.2, 0.2, 0.2, 0.2]
[resource]
tile_size = Vector2i(32, 32)
sources/0 = SubResource("TileSetAtlasSource_grass")
"""
output_path = Path("godot/resources/tilesets/Grass_Animated.tres")
output_path.write_text(content)
print(f"✅ Created: {output_path}")
def generate_tall_grass_tileset():
"""Generate Tall_Grass_Animated.tres with harvestable property."""
content = """[gd_resource type="TileSet" format=3]
[ext_resource type="Texture2D" path="res://assets/tilesets/Tall_Grass_Animated.png" id="1"]
[sub_resource type="TileSetAtlasSource" id="TileSetAtlasSource_tall_grass"]
texture = ExtResource("1")
texture_region_size = Vector2i(32, 32)
0:0/0 = 0
0:0/0/animation_columns = 4
0:0/0/animation_frames_durations = [0.25, 0.25, 0.25, 0.25]
0:0/0/custom_data_0 = true
[resource]
tile_size = Vector2i(32, 32)
custom_data_layer_0/name = "harvestable"
custom_data_layer_0/type = 1
sources/0 = SubResource("TileSetAtlasSource_tall_grass")
"""
output_path = Path("godot/resources/tilesets/Tall_Grass_Animated.tres")
output_path.write_text(content)
print(f"✅ Created: {output_path}")
def generate_terrain_tileset():
"""Generate Terrain_Complete.tres with 47-tile wang blob system."""
content = """[gd_resource type="TileSet" format=3]
[ext_resource type="Texture2D" path="res://assets/tilesets/grass.png" id="1"]
[ext_resource type="Texture2D" path="res://assets/tilesets/dirt.png" id="2"]
[ext_resource type="Texture2D" path="res://assets/tilesets/water.png" id="3"]
[sub_resource type="TileSetAtlasSource" id="TileSetAtlasSource_grass"]
texture = ExtResource("1")
texture_region_size = Vector2i(32, 32)
0:0/0 = 0
0:0/0/terrain_set = 0
0:0/0/terrain = 0
0:0/0/terrains_peering_bit/right_side = 0
0:0/0/terrains_peering_bit/bottom_right_corner = 0
0:0/0/terrains_peering_bit/bottom_side = 0
0:0/0/terrains_peering_bit/bottom_left_corner = 0
0:0/0/terrains_peering_bit/left_side = 0
0:0/0/terrains_peering_bit/top_left_corner = 0
0:0/0/terrains_peering_bit/top_side = 0
0:0/0/terrains_peering_bit/top_right_corner = 0
[sub_resource type="TileSetAtlasSource" id="TileSetAtlasSource_dirt"]
texture = ExtResource("2")
texture_region_size = Vector2i(32, 32)
0:0/0 = 0
0:0/0/terrain_set = 0
0:0/0/terrain = 1
[sub_resource type="TileSetAtlasSource" id="TileSetAtlasSource_water"]
texture = ExtResource("3")
texture_region_size = Vector2i(32, 32)
0:0/0 = 0
0:0/0/terrain_set = 0
0:0/0/terrain = 2
[resource]
tile_size = Vector2i(32, 32)
terrain_set_0/mode = 0
terrain_set_0/terrain_0/name = "Grass"
terrain_set_0/terrain_0/color = Color(0, 0.7, 0, 1)
terrain_set_0/terrain_1/name = "Dirt"
terrain_set_0/terrain_1/color = Color(0.6, 0.4, 0.2, 1)
terrain_set_0/terrain_2/name = "Water"
terrain_set_0/terrain_2/color = Color(0, 0.4, 0.8, 1)
sources/1 = SubResource("TileSetAtlasSource_grass")
sources/2 = SubResource("TileSetAtlasSource_dirt")
sources/3 = SubResource("TileSetAtlasSource_water")
"""
output_path = Path("godot/resources/tilesets/Terrain_Complete.tres")
output_path.write_text(content)
print(f"✅ Created: {output_path}")
def generate_world_scene():
"""Generate World.tscn with TileMapLayer."""
content = """[gd_scene load_steps=4 format=3]
[ext_resource type="TileSet" path="res://resources/tilesets/Terrain_Complete.tres" id="1"]
[ext_resource type="PackedScene" path="res://scenes/characters/Player.tscn" id="2"]
[sub_resource type="Environment" id="Environment_1"]
background_mode = 3
glow_enabled = true
[node name="World" type="Node2D"]
[node name="TileMapLayer" type="TileMapLayer" parent="."]
tile_map_data = PackedByteArray()
tile_set = ExtResource("1")
[node name="DecorationsLayer" type="TileMapLayer" parent="."]
tile_map_data = PackedByteArray()
[node name="Player" parent="." instance=ExtResource("2")]
position = Vector2(512, 384)
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
environment = SubResource("Environment_1")
[node name="CanvasModulate" type="CanvasModulate" parent="."]
color = Color(1, 1, 1, 1)
"""
output_path = Path("godot/scenes/world/World.tscn")
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(content)
print(f"✅ Created: {output_path}")
if __name__ == '__main__':
print("🎮 GODOT 4 TILESET CONVERTER\n")
os.chdir(Path(__file__).parent.parent)
print("📦 Generating TileSet resources...")
generate_water_animated_tileset()
generate_grass_animated_tileset()
generate_tall_grass_tileset()
generate_terrain_tileset()
print("\n🌍 Generating World scene...")
generate_world_scene()
print("\n🎉 GODOT PROJECT READY!")
print("\n📖 NEXT STEPS:")
print("1. Open Godot 4")
print("2. Import project: /Users/davidkotnik/repos/novafarma/godot")
print("3. Open World.tscn")
print("4. Use TileMapLayer 'Terrain' brush to paint!")
print("5. Press F5 to run - Kai spawns with WASD movement! 🎮")