Tileset organization system - 25 TSX files + comprehensive workflow docs
This commit is contained in:
329
tools/organize_tilesets.py
Normal file
329
tools/organize_tilesets.py
Normal file
@@ -0,0 +1,329 @@
|
||||
"""
|
||||
🗺️ TILED BATCH TILESET ORGANIZER
|
||||
==================================
|
||||
Organizes 122 sprite sheets into categorized TSX files for Tiled Map Editor
|
||||
|
||||
Author: Antigravity AI
|
||||
Date: 2025-12-22
|
||||
Project: Krvava Žetev / NovaFarma
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
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)
|
||||
|
||||
# Category definitions with sprite sheet mappings
|
||||
CATEGORIES = {
|
||||
"01_Characters_NPCs": [
|
||||
"kai_character_2x2_grid",
|
||||
"ana_character_2x2_grid",
|
||||
"lena_farmer_2x2_grid",
|
||||
"marija_baker_2x2_grid",
|
||||
"ivan_blacksmith_2x2_grid",
|
||||
"jakob_trader_2x2_grid",
|
||||
"dr_chen_doctor_2x2_grid",
|
||||
"dr_krnic_villain_2x2_grid"
|
||||
],
|
||||
"02_Animals_Pets": [
|
||||
"farm_animals_family_grid",
|
||||
"dog_companions_5_breeds",
|
||||
"rare_livestock_animals",
|
||||
"delivery_creatures_bat_owl"
|
||||
],
|
||||
"03_Buildings_Upgrades": [
|
||||
"house_progression_5_stages",
|
||||
"barn_progression_4_stages",
|
||||
"storage_upgrades_4_stages",
|
||||
"greenhouse_upgrades_stages",
|
||||
"buildings_ruins_states"
|
||||
],
|
||||
"04_Environment_Terrain": [
|
||||
"biome_terrain_tiles",
|
||||
"trees_topdown_pack",
|
||||
"grass_soil_tileset",
|
||||
"seasonal_vegetation_4_seasons",
|
||||
"tree_growth_oak",
|
||||
"tree_growth_pine"
|
||||
],
|
||||
"05_Crops_Farming": [
|
||||
"crop_growth_all",
|
||||
"wheat_growth",
|
||||
"seasonal_crops_grid",
|
||||
"seasonal_seed_packets",
|
||||
"fruit_trees_growth"
|
||||
],
|
||||
"06_Weapons_Combat": [
|
||||
"weapons_melee_grid",
|
||||
"weapons_firearms_modern",
|
||||
"bows_5_types",
|
||||
"arrows_9_types_elemental"
|
||||
],
|
||||
"07_Crafting_Blueprints": [
|
||||
"blueprints_building_unlocks",
|
||||
"crafting_recipes_ui_display",
|
||||
"legendary_blueprint_golden"
|
||||
],
|
||||
"08_Transport_Systems": [
|
||||
"train_repairs_3_states",
|
||||
"horses_breeds_5_types",
|
||||
"cart_wagon_system",
|
||||
"water_transport_pack"
|
||||
],
|
||||
"09_Magic_System": [
|
||||
"magic_staffs_6_types",
|
||||
"spell_effects_animations",
|
||||
"potions_elixirs_grid"
|
||||
],
|
||||
"10_DLC_Dino_World": [
|
||||
"dinosaurs_animation_strips",
|
||||
"dino_world_clothing",
|
||||
"dino_world_food",
|
||||
"dino_world_items"
|
||||
],
|
||||
"11_DLC_Mythical_Highlands": [
|
||||
"mythical_creatures_pack",
|
||||
"highland_clothing",
|
||||
"highland_food",
|
||||
"highland_items"
|
||||
],
|
||||
"12_DLC_Amazon": [
|
||||
"amazon_creatures_pack",
|
||||
"amazon_clothing",
|
||||
"amazon_food",
|
||||
"amazon_items"
|
||||
],
|
||||
"13_DLC_Egypt": [
|
||||
"egyptian_structures_pack",
|
||||
"egyptian_pyramids_sphinx",
|
||||
"egypt_clothing",
|
||||
"egypt_food"
|
||||
],
|
||||
"14_DLC_Atlantis": [
|
||||
"atlantis_objects_pack",
|
||||
"atlantis_clothing",
|
||||
"atlantis_food",
|
||||
"atlantis_items"
|
||||
],
|
||||
"15_DLC_Chernobyl": [
|
||||
"chernobyl_structures_pack",
|
||||
"chernobyl_clothing",
|
||||
"chernobyl_food",
|
||||
"anomalous_creatures_detailed"
|
||||
],
|
||||
"16_DLC_Paris": [
|
||||
"paris_catacombs_pack",
|
||||
"paris_clothing",
|
||||
"paris_food"
|
||||
],
|
||||
"17_DLC_Loch_Ness": [
|
||||
"loch_ness_creatures",
|
||||
"scotland_clothing",
|
||||
"scotland_food"
|
||||
],
|
||||
"18_Monsters_Bosses": [
|
||||
"slimes_8_types_pack",
|
||||
"giant_troll_king_boss",
|
||||
"grok_fabulous_complete_sprite",
|
||||
"werewolf_moon_phases"
|
||||
],
|
||||
"19_Furniture_Interior": [
|
||||
"starter_house_interior",
|
||||
"bedroom_furniture_pack",
|
||||
"kitchen_furniture_pack",
|
||||
"living_room_pack"
|
||||
],
|
||||
"20_Misc_Items": [
|
||||
"backpack_upgrades_6_tiers",
|
||||
"tools_farming_grid",
|
||||
"fishing_equipment_pack",
|
||||
"anas_story_clues_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 (approx - Tiled will correct on load)
|
||||
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 category
|
||||
if "character" in png_path.stem.lower() or "npc" in png_path.stem.lower():
|
||||
tile_width, tile_height = 96, 96 # 2x2 grid on 48px base
|
||||
elif "building" in png_path.stem.lower() or "house" in png_path.stem.lower():
|
||||
tile_width, tile_height = 192, 192 # Larger buildings
|
||||
else:
|
||||
tile_width, tile_height = 48, 48 # Default tile size
|
||||
|
||||
# Calculate columns and tile count
|
||||
columns = width // tile_width
|
||||
rows = height // tile_height
|
||||
tilecount = columns * rows
|
||||
|
||||
if tilecount == 0:
|
||||
tilecount = 1 # Single tile
|
||||
columns = 1
|
||||
|
||||
# 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"
|
||||
with open(tsx_file, 'wb') as f:
|
||||
f.write(pretty_xml)
|
||||
|
||||
print(f"✅ Created: {tsx_file.relative_to(BASE_DIR)}")
|
||||
return tsx_file
|
||||
|
||||
|
||||
def find_matching_sprite(sprite_pattern, sprite_dir):
|
||||
"""
|
||||
Finds PNG files matching a pattern in the sprite directory
|
||||
|
||||
Args:
|
||||
sprite_pattern: Pattern to search for (e.g., "kai_character_2x2")
|
||||
sprite_dir: Directory to search in
|
||||
|
||||
Returns:
|
||||
List of matching PNG files
|
||||
"""
|
||||
matches = []
|
||||
for png_file in sprite_dir.glob("*.png"):
|
||||
if sprite_pattern.lower() in png_file.stem.lower():
|
||||
matches.append(png_file)
|
||||
return matches
|
||||
|
||||
|
||||
def organize_tilesets():
|
||||
"""
|
||||
Main function to organize all sprite sheets into categorized TSX files
|
||||
"""
|
||||
print("🗺️ TILED BATCH TILESET ORGANIZER")
|
||||
print("=" * 60)
|
||||
print(f"📂 Source: {KRVAVA_SPRITES_DIR}")
|
||||
print(f"📂 Output: {TILESETS_OUTPUT_DIR}")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
total_processed = 0
|
||||
total_found = 0
|
||||
|
||||
for category, sprite_patterns in CATEGORIES.items():
|
||||
print(f"\n📁 {category}")
|
||||
print("-" * 60)
|
||||
|
||||
for pattern in sprite_patterns:
|
||||
# Find matching sprites
|
||||
matches = find_matching_sprite(pattern, KRVAVA_SPRITES_DIR)
|
||||
|
||||
if not matches:
|
||||
print(f" ⚠️ No match for: {pattern}")
|
||||
continue
|
||||
|
||||
total_found += len(matches)
|
||||
|
||||
# Create TSX for each match
|
||||
for png_path in matches:
|
||||
tsx_created = create_tsx_from_png(png_path, category, TILESETS_OUTPUT_DIR)
|
||||
if tsx_created:
|
||||
total_processed += 1
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print(f"✅ COMPLETE!")
|
||||
print(f"📊 Found: {total_found} sprite sheets")
|
||||
print(f"📊 Processed: {total_processed} TSX files")
|
||||
print(f"📂 Output: {TILESETS_OUTPUT_DIR}")
|
||||
print("=" * 60)
|
||||
print()
|
||||
print("🎯 NEXT STEPS:")
|
||||
print("1. Open Tiled Map Editor")
|
||||
print("2. Open your map (e.g., micro_farm_128x128.tmx)")
|
||||
print("3. Map → Add External Tileset...")
|
||||
print("4. Navigate to assets/maps/organized_tilesets/")
|
||||
print("5. Select category folder and import TSX files")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
def create_category_readme():
|
||||
"""
|
||||
Creates a README in each category folder explaining contents
|
||||
"""
|
||||
for category, patterns in CATEGORIES.items():
|
||||
category_dir = TILESETS_OUTPUT_DIR / category
|
||||
readme_path = category_dir / "README.md"
|
||||
|
||||
content = f"# {category.replace('_', ' ').title()}\n\n"
|
||||
content += "## Sprite Sheets Included:\n\n"
|
||||
for pattern in patterns:
|
||||
content += f"- `{pattern}`\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"
|
||||
|
||||
if category_dir.exists():
|
||||
with open(readme_path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
organize_tilesets()
|
||||
create_category_readme()
|
||||
except Exception as e:
|
||||
print(f"❌ ERROR: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
Reference in New Issue
Block a user