165 lines
5.3 KiB
Python
165 lines
5.3 KiB
Python
#!/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)!")
|