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!")