chore: Update LDtk project with new layers and fixed assets dimensions (using sips)
This commit is contained in:
164
scripts/generate_tall_grass.py
Normal file
164
scripts/generate_tall_grass.py
Normal 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)!")
|
||||
Reference in New Issue
Block a user