Files
novafarma/update_ldtk_assets.py

349 lines
12 KiB
Python

import os
import json
import uuid
# Configuration
PROJECT_ROOT = '/Users/davidkotnik/repos/novafarma'
GODOT_ROOT = os.path.join(PROJECT_ROOT, 'godot')
LDTK_FILE = os.path.join(PROJECT_ROOT, 'AutoLayers_2_stamps.ldtk')
# User requested assets mapping (approximate filenames to search for)
# We will search for these filenames in the godot directory.
requested_assets = {
"Ground": [
"grass_placeholder.png", "stone.png", "dirt_path.png",
"dirt_path_corner_bottomleft.png", "dirt_path_corner_bottomright.png"
],
"Crops": [
"cannabis_s32_stage1_seeds.png", "cannabis_s32_stage2_sprout.png",
"cannabis_s32_stage3_young.png", "cannabis_s32_stage4_growing.png",
"cannabis_s32_stage5_ready.png", "cannabis_s32_stage6_harvested.png"
],
"Tools": [
"shovel.png", "watering_can.png", "stone_hoe.png", "hoe.png"
],
"Props": [
"blue_patch.png", "mixed_patch.png", "red_patch.png", "white_patch.png", "yellow_patch.png",
"bush_flowering.png", "bush_green.png", "bushes_set2.png",
"fallen_log.png", "mushroom_small.png", "mushrooms_large.png",
"rock_large.png", "rock_medium.png", "rock_small.png", "rocks_set2.png",
"tall_grass.png", "tall_grass_set1.png", "tall_grass_set2.png",
"tree_stump.png", "oak_summer.png", "pine.png", "willow.png"
],
"Entities": [
"kai_idle_down_v2", "kai_idle_right_v2", "kai_idle_up_v2",
"kai_walk_down_01_v2", "kai_walk_down_02_v2", "kai_walk_down_03_v2", "kai_walk_down_04_v2",
"kai_walk_right_01_v2", "kai_walk_right_02_v2", "kai_walk_right_03_v2", "kai_walk_right_04_v2",
"kai_walk_up_01_v2", "kai_walk_up_02_v2", "kai_walk_up_03_v2", "kai_walk_up_04_v2",
"kai_harvest_frame1", "kai_harvest_frame2", "kai_harvest_frame3", "kai_harvest_frame4",
"kai_plant_frame1", "kai_plant_frame2", "kai_plant_frame3", "kai_plant_frame4",
"kai_water_frame1", "kai_water_frame2", "kai_water_frame3", "kai_water_frame4",
"kai_shelter_wooden_hut", "kai_shelter_sleeping_bag"
]
}
# Helper to find file
def find_file(filename_part):
for root, dirs, files in os.walk(GODOT_ROOT):
for file in files:
if filename_part in file and file.endswith('.png'):
return os.path.join(root, file)
return None
# Load LDtk
with open(LDTK_FILE, 'r') as f:
ldtk_data = json.load(f)
# Ensure layers exist
defs_layers = ldtk_data['defs']['layers']
existing_layer_ids = [l['identifier'] for l in defs_layers]
# Helper to create layer definition
def create_layer_def(ident, uid):
return {
"__type": "Entities",
"identifier": ident,
"type": "Entities",
"uid": uid,
"gridSize": 32,
"displayOpacity": 1,
"pxOffsetX": 0,
"pxOffsetY": 0,
"requiredTags": [ident], # Restrict to entities with this tag
"excludedTags": [],
"intGridValues": [],
"autoRuleGroups": [],
"autoSourceLayerDefUid": null,
"tilesetDefUid": null,
"tilePivotX": 0,
"tilePivotY": 0
}
# NOTE: json does not support 'null', use None
null = None
# We need UIDs. Let's start from 600 for layers
next_layer_uid = 600
desired_layers = ["Ground", "Crops", "Props", "Entities"] # Entities already exists usually, but we check.
layer_uids = {}
for layer_name in desired_layers:
existing = next((l for l in defs_layers if l['identifier'] == layer_name), None)
if existing:
layer_uids[layer_name] = existing['uid']
# Update tags to enforce cleanliness?
# For now, let's allow overlapping or just set it up.
# existing['requiredTags'] = [layer_name] # Enforce strict layering?
# Maybe safer to not enforce strictly if logic already exists.
# But user asked for layers setup.
pass
else:
new_layer = {
"__type": "Entities",
"identifier": layer_name,
"type": "Entities",
"uid": next_layer_uid,
"gridSize": 32,
"displayOpacity": 1,
"pxOffsetX": 0,
"pxOffsetY": 0,
"requiredTags": [layer_name] if layer_name != "Entities" else [], # Entities is generic fallback
"excludedTags": [],
"intGridValues": [],
"autoRuleGroups": [],
"autoSourceLayerDefUid": None,
"tilesetDefUid": None,
"tilePivotX": 0,
"tilePivotY": 0
}
defs_layers.insert(0, new_layer) # proper order: Top to Bottom?
# LDtk renders bottom to top in list usually? Or top is top?
# "layers": [...] order is: 0 is TOP, N is BOTTOM.
# User wants: Ground (Bottom), Crops (Middle), Props (Top), Entities (Player?).
# So List order: Entities, Props, Crops, Ground, (Terrain_Visuals, Terrain_Control)
layer_uids[layer_name] = next_layer_uid
next_layer_uid += 1
# Re-order layers
# Desired visual order (Draw order): Ground -> Crops -> Props -> Entities
# In LDtk JSON "layers" array: The *first* element is the *top-most* layer.
# So we want: Entities, Props, Crops, Ground, Terrain_Visuals, Terrain_Control.
ordered_layers = []
def get_layer(name):
return next((l for l in defs_layers if l['identifier'] == name), None)
# Collect them
l_entities = get_layer("Entities")
l_props = get_layer("Props")
l_crops = get_layer("Crops")
l_ground = get_layer("Ground")
l_vis = get_layer("Terrain_Visuals")
l_ctrl = get_layer("Terrain_Control")
final_list = []
if l_entities: final_list.append(l_entities)
if l_props: final_list.append(l_props)
if l_crops: final_list.append(l_crops)
if l_ground: final_list.append(l_ground)
if l_vis: final_list.append(l_vis)
if l_ctrl: final_list.append(l_ctrl)
# Add any others that might exist (backups)
for l in defs_layers:
if l not in final_list:
final_list.append(l)
ldtk_data['defs']['layers'] = final_list
# Setup Entities
defs_entities = ldtk_data['defs']['entities']
defs_tilesets = ldtk_data['defs']['tilesets']
# Generate new UIDs
ent_start_uid = 1000
ts_start_uid = 2000
# Helper to find valid UID
def get_new_uid(existing_uids, start):
while start in existing_uids:
start += 1
return start
existing_ent_uids = {e['uid'] for e in defs_entities}
existing_ts_uids = {t['uid'] for t in defs_tilesets}
# Process requested assets
for category, items in requested_assets.items():
tag = category
if category == "Tools": tag = "Props" # Tools go to prop layer? Or Player? Let's say Props.
for item_name in items:
# 1. Find file
full_path = find_file(item_name)
if not full_path:
print(f"Warning: Could not find file for {item_name}")
continue
rel_path = os.path.relpath(full_path, PROJECT_ROOT)
# 2. Create Tileset for this image (LDtk needs tileset for visual entity)
# Check if tileset exists
existing_ts = next((t for t in defs_tilesets if t['relPath'] == rel_path), None)
if existing_ts:
ts_uid = existing_ts['uid']
px_wid = existing_ts['pxWid']
px_hei = existing_ts['pxHei']
else:
# Need image dimensions. Hack: LDtk might auto-detect if we save?
# No, we need to provide valid JSON.
# We can try to read png header or just assume 32x32 if fails, but correct is better.
# Let's use a quick png size reader or godot import.
# For this script simplicity, I'll check if I can import Pillow.
# If not, I'll rely on a basic hardcoded guess or try to read bytes.
# Simple PNG size parser
try:
with open(full_path, 'rb') as img_f:
head = img_f.read(24)
import struct
w, h = struct.unpack('>ii', head[16:24])
except:
w, h = 32, 32
ts_uid = get_new_uid(existing_ts_uids, ts_start_uid)
existing_ts_uids.add(ts_uid)
new_ts = {
"__cWid": 1,
"__cHei": 1,
"identifier": "TS_" + item_name.replace('.png','').replace(' ','_'),
"uid": ts_uid,
"relPath": rel_path,
"embedAtlas": None,
"pxWid": w,
"pxHei": h,
"tileGridSize": max(w, h), # Full image
"spacing": 0,
"padding": 0,
"tags": [],
"tagsSourceEnumUid": null,
"enumTags": [],
"customData": [],
"savedSelections": [],
"cachedPixelData": None
}
defs_tilesets.append(new_ts)
# 3. Create Entity Definition
# Clean identifier
ent_id = item_name.replace('.png','').replace(' ','_').capitalize()
# Ensure unique identifier
# existing_ent = next((e for e in defs_entities if e['identifier'] == ent_id), None)
# Actually user wants these specific ones.
# If it exists, update it?
# Remove existing if present to refresh
defs_entities = [e for e in defs_entities if e['identifier'] != ent_id]
ent_uid = get_new_uid(existing_ent_uids, ent_start_uid)
existing_ent_uids.add(ent_uid)
# Determine layer tag
layer_tag = tag # Default
if category == "Entities": layer_tag = "Entities" # Keep generic
new_ent = {
"identifier": ent_id,
"uid": ent_uid,
"tags": [layer_tag],
"exportToToc": False,
"allowOutOfBounds": False,
"doc": None,
"width": w,
"height": h,
"resizableX": False,
"resizableY": False,
"minWidth": None,
"maxWidth": None,
"minHeight": None,
"maxHeight": None,
"keepAspectRatio": True,
"tileOpacity": 1,
"fillOpacity": 0.08,
"lineOpacity": 0,
"hollow": False,
"color": "#94D0FF",
"renderMode": "Tile",
"showName": True,
"tilesetId": ts_uid,
"tileRenderMode": "FitInside",
"tileRect": {
"tilesetUid": ts_uid,
"x": 0,
"y": 0,
"w": w,
"h": h
},
"uiTileRect": None,
"nineSliceBorders": [],
"maxCount": 0,
"limitScope": "PerLevel",
"limitBehavior": "MoveLastOne",
"pivotX": 0.5,
"pivotY": 1,
"fieldDefs": []
}
defs_entities.append(new_ent)
ldtk_data['defs']['entities'] = defs_entities
ldtk_data['defs']['tilesets'] = defs_tilesets
# Ensure level instances have the new layers
for level in ldtk_data['levels']:
# Existing layer instances
existing_insts = level['layerInstances']
new_insts = []
# We must match the order of defs.layers
for layer_def in ldtk_data['defs']['layers']:
existing_inst = next((i for i in existing_insts if i['layerDefUid'] == layer_def['uid']), None)
if existing_inst:
new_insts.append(existing_inst)
else:
# Create new instance
new_insts.append({
"__identifier": layer_def['identifier'],
"__type": "Entities",
"__cWid": level['pxWid'] // 32, # Grid size 32
"__cHei": level['pxHei'] // 32,
"__gridSize": 32,
"__opacity": 1,
"__pxTotalOffsetX": 0,
"__pxTotalOffsetY": 0,
"__tilesetDefUid": None,
"__tilesetRelPath": None,
"iid": str(uuid.uuid4()),
"levelId": level['uid'],
"layerDefUid": layer_def['uid'],
"pxOffsetX": 0,
"pxOffsetY": 0,
"visible": True,
"optionalRules": [],
"intGridCsv": [],
"autoLayerTiles": [],
"seed": 0,
"overrideTilesetUid": None,
"gridTiles": [],
"entityInstances": []
})
level['layerInstances'] = new_insts
# Save
with open(LDTK_FILE, 'w') as f:
json.dump(ldtk_data, f, indent=2)
print("LDtk project updated successfully!")