feat: Upgrade Editor to v2 (Sidebar, Layers, Ghost), Tiled Setup, Asset Integration
This commit is contained in:
@@ -1,127 +1,144 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
🧱 GENERATE TILED TILESETS (TSX)
|
||||
Creates .tsx files for all green-screened assets with transparency configured.
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import glob
|
||||
import xml.etree.ElementTree as ET
|
||||
from xml.dom import minidom
|
||||
|
||||
# Constants
|
||||
ASSET_ROOT = "assets"
|
||||
TILESET_OUTPUT_DIR = "assets/maps/tilesets"
|
||||
TRANSPARENT_COLOR = "00ff00" # Green Screen Color
|
||||
# Configuration
|
||||
ASSETS_DIR_REL = 'assets/DEMO_FAZA1'
|
||||
MAPS_DIR = 'assets/maps'
|
||||
TSX_NAME = 'clean_assets.tsx'
|
||||
TMX_NAME = 'game_map.tmx'
|
||||
|
||||
# XML Templates
|
||||
TSX_HEADER = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<tileset version="1.10" tiledversion="1.10.2" name="{name}" tilewidth="{width}" tileheight="{height}" tilecount="{count}" columns="0">
|
||||
<grid orientation="orthogonal" width="1" height="1"/>
|
||||
"""
|
||||
# Layer Structure
|
||||
LAYERS = [
|
||||
'Water',
|
||||
'Ground_Dirt',
|
||||
'Decorations',
|
||||
'Buildings_Solid',
|
||||
'Foreground_Top',
|
||||
]
|
||||
OBJECT_LAYERS = ['Collision_Logic']
|
||||
|
||||
TILE_TEMPLATE = """ <tile id="{id}">
|
||||
<image width="{width}" height="{height}" source="{source}" trans="{trans}"/>
|
||||
</tile>
|
||||
"""
|
||||
BASE_DIR = os.getcwd()
|
||||
|
||||
TSX_FOOTER = """</tileset>
|
||||
"""
|
||||
def prettify(elem):
|
||||
"""Return a pretty-printed XML string for the Element."""
|
||||
rough_string = ET.tostring(elem, 'utf-8')
|
||||
reparsed = minidom.parseString(rough_string)
|
||||
return reparsed.toprettyxml(indent=" ")
|
||||
|
||||
def get_image_size(path):
|
||||
# Try to get image dimensions without PIL to avoid dependency if possible,
|
||||
# but since we already used PIL for green screen, we can use it here.
|
||||
try:
|
||||
from PIL import Image
|
||||
with Image.open(path) as img:
|
||||
return img.width, img.height
|
||||
except Exception:
|
||||
return 32, 32 # Default fallback
|
||||
|
||||
def generate_tileset(directory, name_prefix):
|
||||
"""Generates a Collection of Images tileset for a directory"""
|
||||
def generate_tileset():
|
||||
# Create <tileset> root
|
||||
# version=1.10 tiledversion=1.10.2 name=clean_assets tilewidth=256 tileheight=256 tilecount=? columns=0
|
||||
# Image Collection means columns=0
|
||||
|
||||
# Find all PNGs
|
||||
png_files = sorted(list(Path(directory).rglob("*.png")))
|
||||
if not png_files:
|
||||
return False
|
||||
# We scan for images to determine max dimensions?
|
||||
# Or just set arbitrary? For image collection, tilewidth/height in header is usually max width?
|
||||
|
||||
root = ET.Element('tileset', {
|
||||
'version': '1.10',
|
||||
'tiledversion': '1.11.0',
|
||||
'name': 'clean_assets',
|
||||
'tilewidth': '256',
|
||||
'tileheight': '256',
|
||||
'tilecount': '0',
|
||||
'columns': '0'
|
||||
})
|
||||
|
||||
grid = ET.SubElement(root, 'grid', {'orientation': 'orthogonal', 'width': '1', 'height': '1'})
|
||||
|
||||
# Scan files
|
||||
assets_full_path = os.path.join(BASE_DIR, ASSETS_DIR_REL)
|
||||
tile_id = 0
|
||||
|
||||
print(f"Scanning {assets_full_path}...")
|
||||
|
||||
for root_dir, dirs, files in os.walk(assets_full_path):
|
||||
for f in files:
|
||||
if f.lower().endswith('.png') or f.lower().endswith('.jpg'):
|
||||
# Relative path from .tsx directory (assets/maps) to image
|
||||
# .tsx is in assets/maps
|
||||
# Image is in assets/DEMO_FAZA1/...
|
||||
|
||||
# Abs path of image
|
||||
img_abs = os.path.join(root_dir, f)
|
||||
|
||||
# Rel path from BASE
|
||||
# rel_from_base = os.path.relpath(img_abs, BASE_DIR)
|
||||
|
||||
# Rel path from MAPS dir
|
||||
rel_path = os.path.relpath(img_abs, os.path.join(BASE_DIR, MAPS_DIR))
|
||||
|
||||
# Create <tile id="X">
|
||||
tile_node = ET.SubElement(root, 'tile', {'id': str(tile_id)})
|
||||
|
||||
# <image width="W" height="H" source="PATH"/>
|
||||
# We technically should read width/height, but Tiled often auto-detects if omitted or 0?
|
||||
# Best to read it if possible, but for speed let's rely on Tiled.
|
||||
# Actually, standard TMX requires image tag.
|
||||
|
||||
ET.SubElement(tile_node, 'image', {
|
||||
'source': rel_path
|
||||
# 'width': '?', 'height': '?'
|
||||
})
|
||||
|
||||
tile_id += 1
|
||||
|
||||
root.set('tilecount', str(tile_id))
|
||||
|
||||
# Save
|
||||
out_path = os.path.join(BASE_DIR, MAPS_DIR, TSX_NAME)
|
||||
with open(out_path, 'w') as f:
|
||||
f.write(prettify(root))
|
||||
print(f"Generated Tileset: {out_path} with {tile_id} tiles.")
|
||||
return tile_id
|
||||
|
||||
tileset_name = f"{name_prefix}_{os.path.basename(directory)}"
|
||||
tsx_content = TSX_HEADER.format(
|
||||
name=tileset_name,
|
||||
width=32, # Default (doesn't matter much for collection of images)
|
||||
height=32,
|
||||
count=len(png_files)
|
||||
)
|
||||
|
||||
print(f"📦 Generating tileset: {tileset_name}.tsx ({len(png_files)} images)")
|
||||
|
||||
for i, img_path in enumerate(png_files):
|
||||
# Calculate relative path from tileset location to image
|
||||
# Tiled needs relative paths
|
||||
abs_img = img_path.resolve()
|
||||
abs_tileset_dir = Path(TILESET_OUTPUT_DIR).resolve()
|
||||
def generate_map(first_gid=1):
|
||||
# <map version="1.10" tiledversion="1.11.0" orientation="orthogonal" renderorder="right-down" width="50" height="50" tilewidth="16" tileheight="16" infinite="0" nextlayerid="7" nextobjectid="1">
|
||||
|
||||
root = ET.Element('map', {
|
||||
'version': '1.10',
|
||||
'tiledversion': '1.11.0',
|
||||
'orientation': 'orthogonal',
|
||||
'renderorder': 'right-down',
|
||||
'width': '50',
|
||||
'height': '50',
|
||||
'tilewidth': '16',
|
||||
'tileheight': '16', # User requested 16x16
|
||||
'infinite': '0'
|
||||
})
|
||||
|
||||
# <tileset firstgid="1" source="clean_assets.tsx"/>
|
||||
ET.SubElement(root, 'tileset', {'firstgid': '1', 'source': TSX_NAME})
|
||||
|
||||
# Data is empty CSV
|
||||
# 50*50 = 2500 zeros
|
||||
empty_csv = "0," * 2499 + "0"
|
||||
|
||||
layer_id = 1
|
||||
for layer_name in LAYERS:
|
||||
layer = ET.SubElement(root, 'layer', {
|
||||
'id': str(layer_id),
|
||||
'name': layer_name,
|
||||
'width': '50',
|
||||
'height': '50'
|
||||
})
|
||||
data = ET.SubElement(layer, 'data', {'encoding': 'csv'})
|
||||
data.text = '\n' + empty_csv + '\n'
|
||||
layer_id += 1
|
||||
|
||||
try:
|
||||
rel_path = os.path.relpath(abs_img, abs_tileset_dir)
|
||||
except ValueError:
|
||||
# Fallback if on different drives (unlikely here)
|
||||
rel_path = str(abs_img)
|
||||
|
||||
width, height = get_image_size(abs_img)
|
||||
for obj_layer_name in OBJECT_LAYERS:
|
||||
ET.SubElement(root, 'objectgroup', {
|
||||
'id': str(layer_id),
|
||||
'name': obj_layer_name
|
||||
})
|
||||
layer_id += 1
|
||||
|
||||
tsx_content += TILE_TEMPLATE.format(
|
||||
id=i,
|
||||
width=width,
|
||||
height=height,
|
||||
source=rel_path,
|
||||
trans=TRANSPARENT_COLOR # THIS IS THE MAGIC PART!
|
||||
)
|
||||
out_path = os.path.join(BASE_DIR, MAPS_DIR, TMX_NAME)
|
||||
with open(out_path, 'w') as f:
|
||||
f.write(prettify(root))
|
||||
print(f"Generated Map: {out_path}")
|
||||
|
||||
tsx_content += TSX_FOOTER
|
||||
|
||||
# Save TSX
|
||||
output_path = os.path.join(TILESET_OUTPUT_DIR, f"{tileset_name}.tsx")
|
||||
with open(output_path, "w") as f:
|
||||
f.write(tsx_content)
|
||||
|
||||
return True
|
||||
|
||||
def main():
|
||||
print("🧱 TILED TILESET GENERATOR")
|
||||
print("=" * 50)
|
||||
print(f"Target Transparency: #{TRANSPARENT_COLOR}")
|
||||
print(f"Output Directory: {TILESET_OUTPUT_DIR}\n")
|
||||
|
||||
os.makedirs(TILESET_OUTPUT_DIR, exist_ok=True)
|
||||
|
||||
directories_to_process = [
|
||||
("assets/PHASE_PACKS/0_DEMO", "DEMO"),
|
||||
("assets/PHASE_PACKS/1_FAZA_1", "FAZA1"),
|
||||
("assets/PHASE_PACKS/2_FAZA_2", "FAZA2"),
|
||||
("assets/sprites", "SPRITES"),
|
||||
("assets/crops", "CROPS"),
|
||||
("assets/characters", "CHARS")
|
||||
]
|
||||
|
||||
total_generated = 0
|
||||
|
||||
for dir_path, prefix in directories_to_process:
|
||||
if os.path.exists(dir_path):
|
||||
# Process main directory
|
||||
if generate_tileset(dir_path, prefix):
|
||||
total_generated += 1
|
||||
|
||||
# Optionally process subdirectories as separate tilesets if needed
|
||||
# For now, we put everything in one big tileset per main folder to be safe
|
||||
# But "sprites" is huge, let's split sprites by immediate subdirectory
|
||||
if "sprites" in dir_path:
|
||||
for subdir in Path(dir_path).iterdir():
|
||||
if subdir.is_dir():
|
||||
generate_tileset(subdir, f"SPRITE_{subdir.name.upper()}")
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print(f"✅ Generated {total_generated} main tilesets + sub-tilesets.")
|
||||
print("👉 Import these .tsx files into Tiled map to see automatic transparency!")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
if __name__ == "__main__":
|
||||
count = generate_tileset()
|
||||
generate_map()
|
||||
|
||||
Reference in New Issue
Block a user