317 lines
9.7 KiB
Python
317 lines
9.7 KiB
Python
"""
|
|
🗺️ TILED COMPLETE TILESET ORGANIZER V2
|
|
========================================
|
|
Processes ALL sprite sheets automatically and organizes into TSX files
|
|
|
|
Author: Antigravity AI
|
|
Date: 2025-12-22
|
|
Project: Krvava Žetev / NovaFarma
|
|
"""
|
|
|
|
import os
|
|
from pathlib import Path
|
|
import xml.etree.ElementTree as ET
|
|
from xml.dom import minidom
|
|
|
|
# Base paths
|
|
BASE_DIR = Path(r"c:\novafarma\assets")
|
|
KRVAVA_SPRITES_DIR = BASE_DIR / "krvava_zetev_sprites"
|
|
TILESETS_OUTPUT_DIR = BASE_DIR / "maps" / "organized_tilesets"
|
|
|
|
# Ensure output directory exists
|
|
TILESETS_OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
def auto_categorize_sprite(filename):
|
|
"""
|
|
Automatically categorizes a sprite based on its filename
|
|
|
|
Args:
|
|
filename: Name of the PNG file (without extension)
|
|
|
|
Returns:
|
|
Category name
|
|
"""
|
|
fn = filename.lower()
|
|
|
|
# Characters & NPCs
|
|
if any(x in fn for x in ['character', 'npc', 'doctor', 'farmer', 'baker', 'blacksmith',
|
|
'trader', 'sonya', 'assistant', 'romance']):
|
|
return "01_Characters_NPCs"
|
|
|
|
# Animals & Pets
|
|
if any(x in fn for x in ['animal', 'farm_animals', 'dog', 'livestock', 'delivery_creatures']):
|
|
return "02_Animals_Pets"
|
|
|
|
# Buildings & Structures
|
|
if any(x in fn for x in ['building', 'house', 'barn', 'storage', 'greenhouse', 'mine',
|
|
'town_buildings', 'portal_structures', 'minting']):
|
|
return "03_Buildings_Upgrades"
|
|
|
|
# Environment & Terrain
|
|
if any(x in fn for x in ['grass', 'soil', 'tileset', 'terrain', 'biome', 'fence',
|
|
'obstacles', 'vegetation']):
|
|
return "04_Environment_Terrain"
|
|
|
|
# Crops & Farming
|
|
if any(x in fn for x in ['crop', 'wheat', 'seed', 'seasonal_seed', 'fruit_trees',
|
|
'sprinkler', 'children_5_growth']):
|
|
return "05_Crops_Farming"
|
|
|
|
# Weapons & Combat
|
|
if any(x in fn for x in ['weapon', 'melee', 'firearms', 'bow', 'arrow']):
|
|
return "06_Weapons_Combat"
|
|
|
|
# Crafting & Blueprints
|
|
if any(x in fn for x in ['blueprint', 'crafting', 'recipe', 'legendary']):
|
|
return "07_Crafting_Blueprints"
|
|
|
|
# Transport Systems
|
|
if any(x in fn for x in ['train', 'horse', 'cart', 'wagon', 'transport', 'vehicle']):
|
|
return "08_Transport_Systems"
|
|
|
|
# Magic System
|
|
if any(x in fn for x in ['magic', 'staff', 'spell', 'potion', 'elixir', 'portal_states']):
|
|
return "09_Magic_System"
|
|
|
|
# DLC: Dino World
|
|
if any(x in fn for x in ['dinosaur', 'dino']):
|
|
return "10_DLC_Dino_World"
|
|
|
|
# DLC: Mythical Highlands
|
|
if any(x in fn for x in ['mythical', 'highland']):
|
|
return "11_DLC_Mythical_Highlands"
|
|
|
|
# DLC: Amazon
|
|
if any(x in fn for x in ['amazon']):
|
|
return "12_DLC_Amazon"
|
|
|
|
# DLC: Egypt
|
|
if any(x in fn for x in ['egypt', 'pyramid', 'sphinx']):
|
|
return "13_DLC_Egypt"
|
|
|
|
# DLC: Atlantis
|
|
if any(x in fn for x in ['atlantis']):
|
|
return "14_DLC_Atlantis"
|
|
|
|
# DLC: Chernobyl
|
|
if any(x in fn for x in ['chernobyl', 'anomalous']):
|
|
return "15_DLC_Chernobyl"
|
|
|
|
# DLC: Paris
|
|
if any(x in fn for x in ['paris', 'catacomb']):
|
|
return "16_DLC_Paris"
|
|
|
|
# DLC: Loch Ness
|
|
if any(x in fn for x in ['loch', 'scotland']):
|
|
return "17_DLC_Loch_Ness"
|
|
|
|
# Monsters & Bosses
|
|
if any(x in fn for x in ['zombie', 'slime', 'troll', 'grok', 'monster', 'boss']):
|
|
return "18_Monsters_Bosses"
|
|
|
|
# Furniture & Interior
|
|
if any(x in fn for x in ['furniture', 'interior', 'bedroom', 'kitchen', 'living']):
|
|
return "19_Furniture_Interior"
|
|
|
|
# Misc Items (default)
|
|
return "20_Misc_Items"
|
|
|
|
|
|
def create_tsx_from_png(png_path, category_name, output_dir):
|
|
"""
|
|
Creates a Tiled TSX tileset file from a PNG sprite sheet
|
|
|
|
Args:
|
|
png_path: Path to the PNG file
|
|
category_name: Category folder name
|
|
output_dir: Output directory for TSX file
|
|
"""
|
|
if not png_path.exists():
|
|
print(f"⚠️ PNG not found: {png_path}")
|
|
return None
|
|
|
|
# Get image dimensions
|
|
from PIL import Image
|
|
try:
|
|
img = Image.open(png_path)
|
|
width, height = img.size
|
|
img.close()
|
|
except Exception as e:
|
|
print(f"❌ Error reading image {png_path}: {e}")
|
|
return None
|
|
|
|
# Determine tile size based on filename and category
|
|
fn_lower = png_path.stem.lower()
|
|
|
|
if "2x2_grid" in fn_lower or "character" in fn_lower:
|
|
tile_width, tile_height = 96, 96 # 2x2 grid on 48px base
|
|
elif "building" in fn_lower or "house" in fn_lower or "barn" in fn_lower:
|
|
tile_width, tile_height = 192, 192 # Larger buildings
|
|
elif "tree" in fn_lower and "growth" not in fn_lower:
|
|
tile_width, tile_height = 128, 128 # Trees
|
|
else:
|
|
tile_width, tile_height = 48, 48 # Default tile size
|
|
|
|
# Calculate columns and tile count
|
|
columns = max(1, width // tile_width)
|
|
rows = max(1, height // tile_height)
|
|
tilecount = columns * rows
|
|
|
|
# Create TSX structure
|
|
tileset = ET.Element("tileset", {
|
|
"version": "1.10",
|
|
"tiledversion": "1.11.1",
|
|
"name": png_path.stem.replace("_", " ").title(),
|
|
"tilewidth": str(tile_width),
|
|
"tileheight": str(tile_height),
|
|
"tilecount": str(tilecount),
|
|
"columns": str(columns)
|
|
})
|
|
|
|
# Relative path from TSX to PNG
|
|
rel_path = os.path.relpath(png_path, output_dir).replace("\\", "/")
|
|
|
|
image = ET.SubElement(tileset, "image", {
|
|
"source": f"../../{rel_path}",
|
|
"width": str(width),
|
|
"height": str(height)
|
|
})
|
|
|
|
# Pretty print XML
|
|
rough_string = ET.tostring(tileset, encoding='unicode')
|
|
reparsed = minidom.parseString(rough_string)
|
|
pretty_xml = reparsed.toprettyxml(indent=" ", encoding="UTF-8")
|
|
|
|
# Save TSX file
|
|
category_output = output_dir / category_name
|
|
category_output.mkdir(parents=True, exist_ok=True)
|
|
|
|
tsx_file = category_output / f"{png_path.stem}.tsx"
|
|
|
|
# Skip if already exists
|
|
if tsx_file.exists():
|
|
print(f"⏭️ Skipped (exists): {png_path.stem}")
|
|
return tsx_file
|
|
|
|
with open(tsx_file, 'wb') as f:
|
|
f.write(pretty_xml)
|
|
|
|
print(f"✅ Created: {category_name}/{png_path.stem}.tsx")
|
|
return tsx_file
|
|
|
|
|
|
def organize_all_tilesets():
|
|
"""
|
|
Processes ALL PNG files in krvava_zetev_sprites directory
|
|
"""
|
|
print("🗺️ TILED COMPLETE TILESET ORGANIZER V2")
|
|
print("=" * 70)
|
|
print(f"📂 Source: {KRVAVA_SPRITES_DIR}")
|
|
print(f"📂 Output: {TILESETS_OUTPUT_DIR}")
|
|
print("=" * 70)
|
|
print()
|
|
|
|
# Get all PNG files
|
|
all_pngs = list(KRVAVA_SPRITES_DIR.glob("*.png"))
|
|
print(f"📊 Found {len(all_pngs)} sprite sheets to process")
|
|
print("=" * 70)
|
|
print()
|
|
|
|
category_counts = {}
|
|
total_processed = 0
|
|
total_skipped = 0
|
|
total_errors = 0
|
|
|
|
for png_path in sorted(all_pngs):
|
|
# Auto-categorize
|
|
category = auto_categorize_sprite(png_path.stem)
|
|
|
|
# Track category counts
|
|
if category not in category_counts:
|
|
category_counts[category] = 0
|
|
category_counts[category] += 1
|
|
|
|
# Create TSX
|
|
tsx = create_tsx_from_png(png_path, category, TILESETS_OUTPUT_DIR)
|
|
|
|
if tsx and not tsx.exists():
|
|
total_processed += 1
|
|
elif tsx and tsx.exists():
|
|
total_skipped += 1
|
|
else:
|
|
total_errors += 1
|
|
|
|
# Summary
|
|
print()
|
|
print("=" * 70)
|
|
print("✅ PROCESSING COMPLETE!")
|
|
print("=" * 70)
|
|
print(f"📊 Total sprites: {len(all_pngs)}")
|
|
print(f"✅ Created: {total_processed} new TSX files")
|
|
print(f"⏭️ Skipped: {total_skipped} (already exist)")
|
|
print(f"❌ Errors: {total_errors}")
|
|
print()
|
|
print("📁 Category Breakdown:")
|
|
print("-" * 70)
|
|
for category, count in sorted(category_counts.items()):
|
|
print(f" {category:35} {count:3} files")
|
|
print("=" * 70)
|
|
print()
|
|
print("🎯 NEXT STEPS:")
|
|
print("1. Open Tiled Map Editor")
|
|
print("2. Map → Add External Tileset...")
|
|
print("3. Navigate to: assets/maps/organized_tilesets/")
|
|
print("4. Import TSX files by category")
|
|
print("=" * 70)
|
|
|
|
|
|
def create_category_readme():
|
|
"""
|
|
Creates README in each category folder
|
|
"""
|
|
categories = set()
|
|
|
|
# Find all categories used
|
|
for category_dir in TILESETS_OUTPUT_DIR.iterdir():
|
|
if category_dir.is_dir():
|
|
categories.add(category_dir.name)
|
|
|
|
for category in categories:
|
|
category_dir = TILESETS_OUTPUT_DIR / category
|
|
readme_path = category_dir / "README.md"
|
|
|
|
# Count TSX files
|
|
tsx_files = list(category_dir.glob("*.tsx"))
|
|
|
|
content = f"# {category.replace('_', ' ').title()}\n\n"
|
|
content += f"**Total Tilesets:** {len(tsx_files)}\n\n"
|
|
content += "## Contents:\n\n"
|
|
|
|
for tsx in sorted(tsx_files):
|
|
content += f"- `{tsx.stem}`\n"
|
|
|
|
content += "\n## Usage in Tiled:\n\n"
|
|
content += "1. **Map → Add External Tileset...**\n"
|
|
content += "2. Select .tsx files from this folder\n"
|
|
content += "3. Tilesets will appear in your Tilesets panel\n"
|
|
content += "4. Select tiles and place on map!\n"
|
|
|
|
with open(readme_path, 'w', encoding='utf-8') as f:
|
|
f.write(content)
|
|
|
|
print(f"📝 Updated: {category}/README.md ({len(tsx_files)} files)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
organize_all_tilesets()
|
|
print()
|
|
create_category_readme()
|
|
print()
|
|
print("🎉 ALL DONE!")
|
|
except Exception as e:
|
|
print(f"❌ ERROR: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|