# -*- coding: utf-8 -*- # 맵 정의 조정(2026-09-28 BT36): 블렌더로 만든 소품을 맵 6종에 배치한다 — PD "오브젝트나 프랍들이 너무 반복적으로 재활용된 느낌 … 테마에 맞게 제작 후 배치" # ① 풀: 소품마다 bp_<이름> + 길가용 묶음 풀(bp_roadside · bp_roadside_low) # ② 장면 묶음: 새 소품을 그 맵의 작은 소품 · 광원 장치와 엮는다(bp_…) # ③ 공터 둘레 고리: 되풀이되던 묶음 자리에 새 묶음을 섞는다 # ④ 지정 배치: 큰 소품(랜드마크)을 공터 가장자리 밖 · 보스 자리 뒤에 놓는다 — 자리는 길 · 공터 · 액체를 피해 자동으로 고른다 # ⑤ 띠: 길가에 새 소품을 드문드문 흩는다 # 여러 번 돌려도 같다(앞서 넣은 bp_ 항목을 걷어내고 다시 넣는다). # 실행: python map_props1.py [--no-backup] import io, json, math, os, shutil, sys, time ROOT = r"C:\BurningTimes\FirstRising\Assets\BurningTimes\Maps" PROPS = "Assets/BurningTimes/Maps/_Shared/Props" BACKUP = r"E:\BurningTimes\공유\개발팀_백업\FirstRising\maps_json_" + time.strftime("%Y%m%d_%H%M") THEME = {"Graveyard": "Graveyard", "RuinedKeep": "Keep", "SpiderCave": "Cave", "PlagueCatacomb": "Catacomb", "HarborExecution": "Harbor", "ScorchedDesert": "Desert"} # ───────────────────────── 도우미 def mem(pool, x, z, yaw=0.0, jit=0.2, yawJit=10.0, chance=1.0, mul=1.0, align=""): return {"pool": pool, "fixture": "", "align": align, "x": x, "z": z, "yaw": yaw, "jit": jit, "yawJit": yawJit, "chance": chance, "mul": mul} def fix(name, x, z, chance=1.0): return {"pool": "", "fixture": name, "align": "", "x": x, "z": z, "yaw": 0.0, "jit": 0.2, "yawJit": 0.0, "chance": chance, "mul": 1.0} def bundle(name, radius, tall, note, *members): return {"name": name, "radius": radius, "tall": tall, "note": note, "priority": 1, "members": list(members)} # priority 1 = 되풀이 쓰던 묶음보다 먼저 자리를 잡는다 def ring(area, bundles, count, frac=0.9, a0=0.0, a1=360.0, jitter=6.0): return {"area": area, "bundles": list(bundles), "count": count, "frac": frac, "a0": a0, "a1": a1, "jitter": jitter} def band(name, d0, d1, step, density, pool, cam="", avoid=(), avoidR=0.0, seen=3.0): return {"name": name, "d0": d0, "d1": d1, "step": step, "density": density, "pool": pool, "camPool": cam, "near": [], "nearR": 0.0, "avoid": list(avoid), "avoidR": avoidR, "seen": seen, "occMul": 0.7} def seg_dist(px, pz, pts): best = 1e9 for k in range(0, len(pts) - 3, 2): ax, az, bx, bz = pts[k], pts[k + 1], pts[k + 2], pts[k + 3] dx, dz = bx - ax, bz - az L = dx * dx + dz * dz t = 0.0 if L == 0 else max(0.0, min(1.0, ((px - ax) * dx + (pz - az) * dz) / L)) best = min(best, math.hypot(px - (ax + dx * t), pz - (az + dz * t))) return best def liquid_dist(d, x, z): best = 1e9 for l in d.get("liquids", []): pts = l.get("pts") or [] if len(pts) >= 4: best = min(best, seg_dist(x, z, pts) - l.get("half", 0.0)) elif l.get("r", 0) > 0: best = min(best, math.hypot(x - l["x"], z - l["z"]) - l["r"]) elif l.get("hx", 0) > 0: best = min(best, max(abs(x - l["x"]) - l["hx"], abs(z - l["z"]) - l["hz"])) return best def area_of(d, name): for a in d["areas"]: if a["name"] == name: return a raise KeyError(name) def valid(d, x, z, r, taken): x0, x1, z0, z1 = d["bounds"] if x < x0 + 3 or x > x1 - 3 or z < z0 + 3 or z > z1 - 3: return False for rt in d["routes"]: if rt.get("floor", 0) < 0 and rt.get("half", 0) <= 0: continue if seg_dist(x, z, rt["pts"]) < rt["half"] + 0.8 + r * 0.55: return False for dk in d.get("decks", []): if seg_dist(x, z, [dk["ax"], dk["az"], dk["bx"], dk["bz"]]) < dk["half"] + 1.0 + r * 0.55: return False for a in d["areas"]: rr = a["r"] if a["shape"] == "circle" else max(a["hx"], a["hz"]) if math.hypot(x - a["x"], z - a["z"]) < rr + 0.3 + r * 0.3: return False if liquid_dist(d, x, z) < 1.2 + r * 0.6: return False for (tx, tz, tr) in taken: if math.hypot(x - tx, z - tz) < tr + r + 0.8: return False return True def find(d, area, angle, beyond, r, taken): """공터 가장자리 밖(beyond m)에서 angle(고리 각도: 0 = +z · 90 = +x · 180 = -z) 가까이의 빈 자리를 찾는다.""" a = area_of(d, area) rr = a["r"] if a["shape"] == "circle" else max(a["hx"], a["hz"]) # 가까운 각도부터 넓혀 가며 찾는다 — 섬 · 부두처럼 둘레가 액체 · 길로 막힌 공터는 멀리 돌아서라도 자리를 잡는다 for da in (0, 8, -8, 16, -16, 24, -24, 34, -34, 46, -46, 60, -60, 80, -80, 100, -100, 125, -125, 150, -150, 180): for extra in (0.0, 1.5, 3.0, 4.5, 6.5, 9.0): ang = math.radians(angle + da) dist = rr + beyond + extra x, z = a["x"] + math.sin(ang) * dist, a["z"] + math.cos(ang) * dist if valid(d, x, z, r, taken): # 공터 가운데를 보게 돌린다(소품의 앞 = +z) yaw = math.degrees(math.atan2(a["x"] - x, a["z"] - z)) return round(x, 1), round(z, 1), round(yaw, 0) return None # ───────────────────────── 테마별 꾸밈 def deco_graveyard(): B = [ bundle("bp_cross_plot", 3.4, True, "고리 십자가 + 새 무덤", mem("bp_cross_ring", 0, -1.0, yawJit=6), mem("bp_grave_mound", -2.0, 0.6, 80, yawJit=15), mem("bp_grave_mound", 2.0, 0.5, 100, yawJit=15, chance=0.7), mem("bush", -2.7, -1.2, chance=0.6), mem("grass", 0.8, 1.5), fix("candles", 0.3, 0.7)), bundle("bp_sarcophagus", 3.4, True, "열린 석관 + 유골 단지", mem("bp_sarcophagus_open", -0.3, -0.3, yawJit=12), mem("bp_urn_pedestal", 2.2, -0.8, chance=0.8), mem("bone", -1.7, 1.1), fix("lantern", -2.3, -0.6)), bundle("bp_mortsafe", 3.2, False, "쇠 우리 무덤 + 기운 묘비", mem("bp_mortsafe", 0.2, 0.5, yawJit=8), mem("bp_headstone_trio", -0.2, -1.4, yawJit=8), mem("grass", 1.9, 0.9), mem("bone", -1.9, 0.7, chance=0.5)), bundle("bp_bell", 3.2, True, "조종 틀 + 관 더미", mem("bp_bell_post", 0, -1.0, yawJit=6), mem("bp_coffin_stack", 1.6, 0.9, yaw=20, yawJit=20), fix("torch_warm", -2.0, 0.2), mem("grass", -1.2, 1.3)), bundle("bp_fountain", 3.2, False, "마른 분수", mem("bp_fountain_dry", 0, 0, yawJit=20), mem("bush", 2.3, -0.6, chance=0.7), mem("rock", -2.2, -0.5, mul=0.7), fix("candles", 1.3, 1.4, 0.6)), bundle("bp_crypt_gate", 5.0, True, "납골당 쇠문 + 푸른 횃불", mem("bp_crypt_gate", 0, -0.8, yawJit=4, jit=0.0), mem("wall_tall", -3.9, -0.9, yawJit=4, align="x"), mem("wall_tall", 3.9, -0.9, yawJit=4, align="x"), fix("torch_accent", -2.4, 0.6), fix("torch_accent", 2.4, 0.6), mem("bp_grave_mound", 0.3, 2.0, 90, chance=0.6)), bundle("bp_mound_field", 3.0, False, "새 무덤 밭", mem("bp_grave_mound", -1.2, -0.6, 85, yawJit=12), mem("bp_grave_mound", 1.2, -0.4, 95, yawJit=12), mem("stone", -1.2, -1.9, yawJit=10), mem("stone", 1.3, -1.8, yawJit=10), fix("candles", 0.0, 0.9), mem("grass", 2.2, 0.8)), ] R = [ ring("정문 광장", ["lantern_rest", "bp_fountain", "broken_fence", "bp_mound_field", "grave_row"], 5, 0.9), ring("중앙 추모 광장", ["bp_cross_plot", "mourner_statue", "bp_bell", "rock_outcrop"], 4, 0.93, 110, 250), ring("중앙 추모 광장", ["bone_pit", "bp_mortsafe", "open_grave", "bp_mound_field", "broken_fence"], 5, 0.92, 280, 440), ring("서쪽 묘역", ["dead_tree_clump", "bp_cross_plot", "mourner_statue", "bp_sarcophagus"], 4, 0.92, 110, 250), ring("서쪽 묘역", ["grave_row", "bp_mortsafe", "open_grave", "bp_mound_field", "grave_row", "bone_pit", "bp_fountain"], 7, 0.86, 260, 460), ring("동쪽 묘역", ["bp_sarcophagus", "dead_tree_clump", "bp_bell", "mourner_statue"], 4, 0.92, 110, 250), ring("동쪽 묘역", ["bp_mound_field", "grave_row", "bp_mortsafe", "open_grave", "lantern_rest", "grave_row", "bp_mound_field"], 7, 0.86, 260, 460), ring("납골당 거리", ["bp_mortsafe", "grave_row", "bone_pit", "bp_mound_field", "broken_fence", "bp_fountain"], 6, 0.88, 250, 470), ring("무너진 예배당", ["open_grave", "bp_mound_field", "grave_row", "bp_mortsafe", "rock_outcrop", "bp_fountain"], 6, 0.88, 250, 470), ring("서쪽 쉼터", ["bp_mound_field", "rock_outcrop", "grave_row"], 3, 0.9), ring("동쪽 쉼터", ["bone_pit", "bp_mortsafe", "rock_outcrop"], 3, 0.9), ring("묘지기 마당", ["broken_fence", "bp_mound_field", "rock_outcrop"], 3, 0.92, 250, 330), ] W = [("bp_crypt_gate", "서쪽 묘역", 225, 3.5, 5.0, "서쪽 묘역 쇠문"), ("bp_crypt_gate", "동쪽 묘역", 135, 3.5, 5.0, "동쪽 묘역 쇠문"), ("bp_bell", "대납골당 앞뜰", 235, 2.5, 3.2, "조종"), ("bp_cross_plot", "대납골당 앞뜰", 125, 2.5, 3.4, "큰 십자가"), ("bp_sarcophagus", "납골당 거리", 200, 2.5, 3.4, "열린 석관"), ("bp_cross_plot", "무너진 예배당", 170, 2.5, 3.4, "예배당 십자가"), ("bp_fountain", "정문 광장", 300, 2.5, 3.2, "마른 분수")] road = ["headstone_trio", "grave_mound", "urn_pedestal", "coffin_stack", "mortsafe"] low = ["grave_mound", "mortsafe"] return B, R, W, road, low, band("길가 새 무덤", 2.6, 6.5, 4.2, 0.45, "bp_roadside", "bp_roadside_low", ["대납골당 앞뜰"], 25, 2.0) def deco_keep(): B = [ bundle("bp_battlement", 3.8, True, "무너진 성가퀴 + 화로", mem("bp_battlement_chunk", 0, -0.8, yawJit=10), mem("debris", 2.8, 0.8), mem("pebble", -1.0, 1.6), fix("brazier_wood", -2.9, 0.4)), bundle("bp_bell", 3.4, False, "떨어진 종 + 숯 들보", mem("bp_bell_fallen", -0.4, -0.4, yawJit=25), mem("bp_beam_pile", 1.9, 0.6, yawJit=40, chance=0.8), fix("glow_lava", 1.9, 0.6), mem("pebble", -1.8, 1.2)), bundle("bp_armory", 3.2, True, "무기 걸이 + 불탄 깃발", mem("bp_weapon_rack", 0, -0.7, yawJit=8), mem("bp_banner_burnt", 2.1, -0.9, yawJit=20), mem("supply", -1.9, 0.4), mem("supply", -2.4, -0.5, chance=0.6), fix("torch_warm", 1.4, 0.9)), bundle("bp_monument", 3.0, True, "전사자 비(대검)", mem("bp_sword_monument", 0, -0.4, yawJit=10), mem("bone", 1.6, 1.0), mem("bone", -1.5, 0.9, chance=0.6), fix("candles", 0.2, 1.2), mem("rock", -2.0, -0.6, mul=0.7)), bundle("bp_siege", 3.8, True, "부서진 투석기", mem("bp_catapult_wreck", 0, -0.6, yawJit=25), mem("barricade", -3.0, 0.8, yaw=-20, align="x", chance=0.7), mem("debris", 2.6, 1.2), mem("pebble", 0.4, 1.9)), bundle("bp_portcullis", 3.2, False, "쓰러진 쇠창살문", mem("bp_portcullis_fallen", 0, -0.2, yawJit=10), mem("debris", 2.2, 0.6), mem("rock", -2.2, 0.2, mul=0.8), fix("torch_warm", -1.6, 1.3, 0.7)), bundle("bp_throne", 5.0, True, "금 간 옥좌 + 화로", mem("bp_throne_ruin", 0, -0.8, yawJit=4, jit=0.0), mem("column", -3.0, -0.4), mem("column", 3.0, -0.4), fix("brazier_accent", -2.4, 1.4), fix("brazier_accent", 2.4, 1.4), mem("bp_banner_burnt", -4.0, -1.4, yawJit=10, chance=0.8)), bundle("bp_embers", 2.8, False, "타다 남은 들보", mem("bp_beam_pile", 0, 0, yawJit=60), fix("glow_lava", 0, 0), mem("pebble", 1.5, 0.8), mem("debris", -1.7, 0.5, mul=0.6)), ] R = [ ring("남쪽 외성 광장", ["barricade", "bp_armory", "siege_supplies", "bp_embers", "rubble_heap"], 5, 0.9, 40, 320), ring("서쪽 병영 터", ["broken_wall_camp", "bp_battlement", "column_row", "collapsed_tower"], 4, 0.93, 200, 330), ring("서쪽 병영 터", ["bp_armory", "barricade", "bp_embers", "rubble_heap"], 4, 0.9, 20, 160), ring("동쪽 대장간 터", ["bp_siege", "broken_wall_camp", "bp_battlement", "collapsed_tower"], 4, 0.93, 30, 160), ring("동쪽 대장간 터", ["bp_bell", "barricade", "bp_portcullis", "siege_supplies"], 4, 0.9, 200, 340), ring("내성 문 앞", ["crystal_outcrop", "bp_monument", "column_row"], 3, 0.93, 110, 250), ring("내성 문 앞", ["bp_portcullis", "rubble_heap", "lava_vent"], 3, 0.9, 290, 430), ring("무너진 주탑 섬", ["bp_monument", "crystal_outcrop", "bp_battlement", "burnt_tree"], 4, 0.95, 120, 240), ring("무너진 주탑 섬", ["bp_embers", "lava_vent", "bp_bell", "rubble_heap"], 4, 0.95, 300, 420), ring("남서 쉼터", ["burnt_tree", "bp_embers", "rubble_heap"], 3, 0.9, 200, 330), ring("남동 쉼터", ["bp_armory", "burnt_tree", "rubble_heap"], 3, 0.9, 30, 160), ring("북서 쉼터", ["crystal_outcrop", "bp_portcullis", "lava_vent"], 3, 0.9, 170, 310), ring("북동 쉼터", ["bp_battlement", "crystal_outcrop", "lava_vent"], 3, 0.9, 50, 190), ring("성문 밖 야영지", ["barricade", "bp_embers", "rubble_heap"], 3, 0.92, 240, 340), ] W = [("bp_throne", "내성 안뜰", 180, 3.5, 5.0, "금 간 옥좌"), ("bp_siege", "남쪽 외성 광장", 250, 2.5, 3.8, "부서진 투석기"), ("bp_monument", "내성 안뜰", 120, 2.5, 3.0, "전사자 비"), ("bp_monument", "내성 안뜰", 240, 2.5, 3.0, "전사자 비"), ("bp_bell", "내성 문 앞", 90, 2.5, 3.4, "떨어진 종"), ("bp_battlement", "서쪽 병영 터", 270, 3.0, 3.8, "무너진 성가퀴"), ("bp_siege", "동쪽 대장간 터", 90, 3.0, 3.8, "부서진 투석기")] road = ["beam_pile", "weapon_rack", "banner_burnt", "portcullis_fallen", "bell_fallen"] low = ["beam_pile", "portcullis_fallen"] return B, R, W, road, low, band("길가 싸움 자국", 2.6, 6.5, 4.2, 0.45, "bp_roadside", "bp_roadside_low", ["내성 안뜰"], 25, 2.0) def deco_cave(): B = [ bundle("bp_egg_small", 2.6, False, "작은 알 무더기 + 빛", mem("bp_egg_cluster_small", 0, 0, yawJit=60), mem("lumie", 1.5, 0.7, chance=0.7), mem("pebble", -1.4, 0.8), fix("glow_egg", 0, 0.2)), bundle("bp_egg_large", 3.4, False, "큰 알 무더기 + 고치", mem("bp_egg_cluster_large", 0, -0.3, yawJit=60), mem("bp_cocoon_pile", 2.5, 0.5, yawJit=40, chance=0.7), mem("bone", -2.0, 1.0), fix("glow_egg", 0, 0)), bundle("bp_cocoon_pillar", 3.2, True, "고치를 매단 석순", mem("bp_cocoon_pillar", 0, -0.8, yawJit=40), mem("bp_egg_cluster_small", 1.9, 0.8, yawJit=60, chance=0.7), mem("bone", -1.7, 0.9), fix("glow_teal", 0.6, 0.6)), bundle("bp_web_gate", 4.0, True, "거미줄 문", mem("bp_web_gate", 0, -0.9, yawJit=6), mem("bp_cocoon_pile", 0.4, 1.0, yawJit=60, chance=0.6), mem("shroom", -2.6, 0.8), fix("glow_teal", 1.6, 0.6, 0.7)), bundle("bp_bone_nest", 3.0, False, "뼈 둥지", mem("bp_bone_nest", 0, 0, yawJit=60), mem("skull_big", 2.1, -0.6, chance=0.6), mem("bone", -1.9, 0.9), fix("glow_egg", 0, 0)), bundle("bp_fungus", 2.8, False, "빛나는 버섯 바위", mem("bp_fungus_rock", 0, -0.2, yawJit=60), mem("shroom", 1.7, 0.7), mem("lumie", -1.5, 0.8), fix("glow_teal", 0, 0.2)), bundle("bp_husk", 3.4, False, "죽은 거미의 허물", mem("bp_spider_husk", 0, -0.3, yawJit=60), mem("bp_egg_cluster_small", 2.4, 0.6, yawJit=60, chance=0.7), mem("bone", -2.1, 0.8), fix("glow_accent", 0.8, 0.8, 0.7)), bundle("bp_webbed_cart", 3.2, False, "거미줄에 싸인 수레", mem("bp_webbed_cart", 0, -0.2, yawJit=30), mem("supply", 2.2, 0.8, chance=0.7), mem("bone", -1.9, 1.0), fix("lantern", -2.4, -0.2, 0.8)), ] R = [ ring("버섯 공동", ["shroom_cluster", "bp_fungus", "stalagmite_group", "bp_cocoon_pillar", "shroom_cluster"], 5, 0.92, 100, 260), ring("버섯 공동", ["bp_fungus", "lumie_patch", "bp_webbed_cart", "bone_heap"], 4, 0.9, 290, 430), ring("고치 광장", ["bp_cocoon_pillar", "cocoon_cluster", "bp_web_gate", "stalagmite_group"], 4, 0.95, 120, 240), ring("고치 광장", ["bp_husk", "bone_heap", "bp_egg_small", "lumie_patch"], 4, 0.94, 300, 420), ring("수정 샘", ["crystal_vein", "bp_fungus", "shroom_cluster"], 3, 0.92, 120, 250), ring("수정 샘", ["bp_egg_small", "lumie_patch", "bone_heap"], 3, 0.9, 30, 110), ring("알 둥지", ["bp_egg_large", "egg_nest", "bp_cocoon_pillar", "cocoon_cluster", "bp_web_gate"], 5, 0.92, 110, 260), ring("알 둥지", ["bp_bone_nest", "egg_nest", "bp_egg_small"], 3, 0.9, 290, 420), ring("거미줄 회랑", ["bp_web_gate", "cocoon_cluster", "bp_cocoon_pillar", "stalagmite_group", "web_gate"], 5, 0.92, 100, 260), ring("거미줄 회랑", ["bp_husk", "bone_heap", "skull_totem"], 3, 0.9, 300, 420), ring("아랫굴 쉼터", ["bp_webbed_cart", "lumie_patch", "bp_egg_small"], 3, 0.9, 300, 420), ring("동쪽 굴 쉼터", ["bp_bone_nest", "web_gate", "bone_heap"], 3, 0.9, 30, 150), ring("서쪽 굴 쉼터", ["bp_fungus", "shroom_cluster", "bp_egg_small"], 3, 0.9, 210, 330), ring("동굴 어귀", ["stalagmite_group", "bp_webbed_cart", "lumie_patch"], 3, 0.92, 240, 340), ] W = [("bp_egg_large", "여왕의 둥지", 150, 2.5, 3.4, "여왕의 알 무더기"), ("bp_egg_large", "여왕의 둥지", 210, 2.5, 3.4, "여왕의 알 무더기"), ("bp_cocoon_pillar", "여왕의 둥지", 110, 2.5, 3.2, "고치 기둥"), ("bp_cocoon_pillar", "여왕의 둥지", 250, 2.5, 3.2, "고치 기둥"), ("bp_husk", "고치 광장", 180, 2.5, 3.4, "죽은 거미"), ("bp_bone_nest", "알 둥지", 200, 2.5, 3.0, "뼈 둥지"), ("bp_web_gate", "거미줄 회랑", 160, 2.5, 4.0, "거미줄 문")] road = ["egg_cluster_small", "fungus_rock", "cocoon_pile", "bone_nest", "cocoon_pillar"] low = ["egg_cluster_small", "bone_nest"] return B, R, W, road, low, band("길가 알 · 버섯", 2.6, 6.5, 4.2, 0.45, "bp_roadside", "bp_roadside_low", ["여왕의 둥지"], 25, 2.0) def deco_catacomb(): B = [ bundle("bp_plague_cart", 3.4, False, "주검을 실은 수레", mem("bp_plague_cart", 0, -0.2, yawJit=25), mem("bp_corpse_pile", 2.6, 0.7, yawJit=60, chance=0.6), mem("bone", -2.0, 1.0), fix("lantern", -2.6, -0.4, 0.8)), bundle("bp_corpse_pile", 2.8, False, "주검 더미 + 향로", mem("bp_corpse_pile", 0, 0, yawJit=60), mem("bp_censer_stand", 1.9, -0.5), fix("glow_acid", 1.9, -0.5), mem("bone", -1.7, 0.8)), bundle("bp_chained_tomb", 3.2, False, "사슬로 묶은 석관", mem("bp_sarcophagus_chained", 0, -0.3, yawJit=8), mem("pot", 2.0, 0.6, chance=0.7), mem("bone", -1.9, 0.9), fix("candles", 1.2, 1.2)), bundle("bp_urn_shelf", 3.0, True, "유골 단지 선반", mem("bp_urn_shelf", 0, -0.9, yawJit=5), mem("pot", 1.9, 0.4), mem("pot", -1.8, 0.5, chance=0.6), fix("candles", 0.2, 0.6)), bundle("bp_cage", 2.6, True, "쇠 우리 + 향로", mem("bp_iron_cage", -0.6, -0.4, yawJit=40), mem("bp_censer_stand", 1.2, 0.2), fix("glow_acid", 1.2, 0.2), mem("bone", 0.2, 1.3)), bundle("bp_cauldron", 2.8, False, "독이 끓는 솥", mem("bp_toxic_cauldron", 0, -0.2, yawJit=60), fix("glow_acid", 0, -0.2), mem("pot", 1.8, 0.6), mem("pebble", -1.6, 0.9), mem("bone", 0.6, 1.5, chance=0.6)), bundle("bp_quarantine", 3.2, False, "격리 방책", mem("bp_quarantine_barricade", 0, 0, yawJit=8), mem("supply", 2.4, 0.4, chance=0.7), mem("bone", -2.2, 0.8), fix("torch_accent", -2.4, -0.6, 0.8)), bundle("bp_embalming", 3.0, False, "염습대", mem("bp_embalming_table", 0, -0.3, yawJit=10), mem("pot", 2.1, 0.5), mem("bp_censer_stand", -2.0, -0.2, chance=0.7), fix("candles", 1.0, 1.2)), ] R = [ ring("남쪽 납골 회랑", ["tomb_shelf", "bp_urn_shelf", "pillar_pair", "bp_chained_tomb", "sarcophagus_row"], 5, 0.9, 110, 250), ring("남쪽 납골 회랑", ["bp_plague_cart", "ossuary", "bp_cauldron", "brazier_alcove"], 4, 0.9, 300, 420), ring("정화의 방", ["bp_cage", "pillar_pair", "bp_urn_shelf", "tomb_shelf"], 4, 0.92, 130, 230), ring("정화의 방", ["bp_embalming", "brazier_alcove", "bp_corpse_pile", "grate_drain"], 4, 0.92, 310, 410), ring("역병 병동", ["bp_embalming", "mummy_pile", "bp_corpse_pile", "tomb_shelf", "bp_plague_cart"], 5, 0.9, 150, 330), ring("역병 병동", ["bp_quarantine", "ossuary"], 2, 0.9, 20, 60), ring("해부실", ["bp_embalming", "mummy_pile", "bp_cage", "tomb_shelf", "bp_corpse_pile"], 5, 0.9, 30, 210), ring("해부실", ["bp_quarantine", "ossuary"], 2, 0.9, 300, 340), ring("오염 저수조", ["bp_cauldron", "grate_drain", "collapsed_arch", "bp_corpse_pile"], 4, 0.9, 150, 320), ring("감염자 감옥", ["bp_cage", "mummy_pile", "collapsed_arch", "bp_quarantine"], 4, 0.9, 40, 210), ring("남서 모퉁이 방", ["bp_chained_tomb", "ossuary", "bp_plague_cart"], 3, 0.9, 290, 410), ring("남동 모퉁이 방", ["ossuary", "bp_cauldron", "plague_cart"], 3, 0.9, 310, 430), ] W = [("bp_chained_tomb", "역병 군주의 제단", 180, 2.5, 3.2, "봉인된 석관"), ("bp_cauldron", "역병 군주의 제단", 135, 2.5, 2.8, "독 솥"), ("bp_cauldron", "역병 군주의 제단", 225, 2.5, 2.8, "독 솥"), ("bp_urn_shelf", "역병 군주의 제단", 105, 2.5, 3.0, "유골 선반"), ("bp_urn_shelf", "역병 군주의 제단", 255, 2.5, 3.0, "유골 선반"), ("bp_cage", "감염자 감옥", 120, 2.5, 2.6, "쇠 우리"), ("bp_plague_cart", "역병 병동", 240, 2.5, 3.4, "주검 수레")] road = ["corpse_pile", "censer_stand", "iron_cage", "toxic_cauldron", "quarantine_barricade"] low = ["corpse_pile", "quarantine_barricade"] return B, R, W, road, low, band("길가 역병 자국", 2.6, 6.5, 4.2, 0.45, "bp_roadside", "bp_roadside_low", ["역병 군주의 제단"], 25, 2.0) def deco_harbor(): B = [ bundle("bp_gallows", 4.4, True, "교수대", mem("bp_gallows", 0, -0.8, yawJit=6), mem("cargo", 3.2, 0.6, chance=0.7), mem("bone", -2.6, 1.2), fix("brazier_wood", -3.2, -0.2), fix("torch_warm", 2.8, 1.4, 0.7)), bundle("bp_gibbet", 2.8, True, "죄수 우리", mem("bp_gibbet", 0, -0.5, yawJit=30), mem("bone", 1.4, 0.9), mem("small", -1.5, 0.7, chance=0.7), fix("torch_accent", -1.9, -0.3, 0.8)), bundle("bp_pillory", 3.0, False, "형틀과 처형대", mem("bp_pillory", -1.2, -0.3, yawJit=15), mem("bp_chopping_block", 1.3, 0.2, yawJit=60), mem("bone", 0.2, 1.4), fix("torch_warm", -2.5, 0.5, 0.8)), bundle("bp_mooring", 2.8, False, "계선주와 닻", mem("bp_bollard_rope", -0.8, 0.3, yawJit=40), mem("bp_anchor_crate", 1.4, -0.5, yawJit=30), mem("small", 0.3, 1.5), fix("lantern", -2.2, -0.6, 0.8)), bundle("bp_wreck", 3.4, False, "부서진 나룻배", mem("bp_rowboat_wreck", 0, -0.2, yawJit=40), mem("bp_bollard_rope", 2.6, 0.7, yawJit=60, chance=0.6), mem("reed", -2.2, 0.9), mem("small", 0.8, 1.6)), bundle("bp_nets", 3.2, True, "그물 말리는 틀", mem("bp_net_rack", 0, -0.6, yawJit=8), mem("cargo", 2.4, 0.4, chance=0.7), mem("small", -2.0, 0.8), fix("lantern", 2.2, -0.9, 0.8)), bundle("bp_crane", 4.0, True, "하역 기중기", mem("bp_harbor_crane", 0, -0.8, yawJit=20), mem("cargo", 2.8, 0.8), mem("cargo", -2.6, 0.6, chance=0.7), mem("bp_bollard_rope", 0.6, 1.8, yawJit=60, chance=0.6), fix("brazier_wood", -3.0, -0.8)), bundle("bp_block", 2.6, False, "처형대 + 핏자국", mem("bp_chopping_block", 0, 0, yawJit=60), mem("bone", 1.5, 0.8), mem("coffin", -1.8, -0.4, chance=0.7), fix("candles", 0.8, 1.3, 0.6)), ] R = [ ring("서쪽 하역장", ["bp_crane", "warehouse_front", "cargo_stack", "watch_tower"], 4, 0.93, 190, 330), ring("서쪽 하역장", ["bp_mooring", "cargo_stack", "bp_wreck", "log_pile"], 4, 0.9, 30, 130), ring("어시장", ["bp_nets", "fish_stall", "warehouse_front"], 3, 0.93, 170, 290), ring("어시장", ["bp_nets", "cargo_stack", "bp_mooring", "tavern_corner"], 4, 0.9, 30, 140), ring("동쪽 조선소", ["bp_wreck", "shipwright", "bp_crane", "quay_tree", "log_pile"], 5, 0.9, 30, 160), ring("동쪽 조선소", ["warehouse_front", "bp_mooring", "log_pile"], 3, 0.92, 170, 240), ring("창고 마당", ["warehouse_front", "bp_crane", "cargo_stack", "quay_tree"], 4, 0.92, 160, 330), ring("창고 마당", ["bp_pillory", "checkpoint", "cargo_stack"], 3, 0.9, 10, 80), ring("중앙 광장", ["bp_gallows", "gibbet", "warehouse_front", "bp_gibbet"], 4, 0.95, 120, 240), ring("중앙 광장", ["bp_pillory", "tavern_corner", "bp_block", "checkpoint"], 4, 0.94, 300, 420), ring("세관 앞", ["bp_gibbet", "warehouse_front", "bp_pillory", "cargo_stack"], 4, 0.92, 30, 200), ring("세관 앞", ["checkpoint", "bp_block", "tavern_corner"], 3, 0.9, 280, 350), ring("뒷골목 쉼터", ["bp_block", "tavern_corner", "cargo_stack", "log_pile"], 4, 0.9, 290, 430), ] W = [("bp_gallows", "처형장", 150, 4.6, 4.4, "교수대"), ("bp_gallows", "처형장", 210, 4.6, 4.4, "교수대"), ("bp_gibbet", "처형장", 110, 2.5, 2.8, "죄수 우리"), ("bp_gibbet", "처형장", 250, 2.5, 2.8, "죄수 우리"), ("bp_crane", "서쪽 하역장", 40, 3.0, 4.0, "하역 기중기"), ("bp_crane", "동쪽 조선소", 320, 3.0, 4.0, "하역 기중기"), ("bp_pillory", "중앙 광장", 180, 2.5, 3.0, "형틀")] road = ["bollard_rope", "anchor_crate", "rowboat_wreck", "net_rack", "pillory", "chopping_block"] low = ["bollard_rope", "rowboat_wreck", "chopping_block"] return B, R, W, road, low, band("길가 항구 살림", 2.6, 6.5, 4.2, 0.45, "bp_roadside", "bp_roadside_low", ["처형장"], 25, 2.0) def deco_desert(): B = [ bundle("bp_obelisk", 3.4, True, "부러진 오벨리스크", mem("bp_obelisk_broken", 0, -0.8, yawJit=20), mem("bp_obelisk_top", 2.4, 0.8, yawJit=60), mem("pebble", -1.6, 1.0), mem("dgrass", -2.2, -0.4, chance=0.7), fix("glow_sun", 0.4, 0.8, 0.7)), bundle("bp_arch", 4.2, True, "무너진 아치", mem("bp_arch_ruin", 0, -0.9, yawJit=8), mem("bp_amphora_set", 2.6, 1.0, yawJit=60, chance=0.7), mem("flat_rock", -2.4, 1.1), mem("cactus", -3.2, -0.6, chance=0.6), fix("brazier_warm", 1.6, 1.4, 0.7)), bundle("bp_columns", 3.6, True, "부서진 기둥 줄", mem("bp_column_drums", -1.6, -0.6, yawJit=30), mem("bp_column_drums", 1.8, -0.4, yaw=160, yawJit=30, chance=0.8), mem("debris", 0.2, 1.0, mul=0.6), mem("pebble", -2.6, 0.9), mem("dgrass", 2.9, 0.8)), bundle("bp_altar", 3.6, True, "태양 제단", mem("bp_sun_altar", 0, -0.6, yawJit=6), mem("bp_amphora_set", 2.6, 0.8, yawJit=60, chance=0.7), fix("brazier_warm", -2.6, 0.6), fix("glow_sun", 0, 0.9)), bundle("bp_well", 3.2, False, "메마른 우물", mem("bp_well_dry", 0, -0.2, yawJit=20), mem("bp_amphora_set", 2.3, 0.8, yawJit=60), mem("dgrass", -2.0, 0.9), fix("lantern", -2.4, -0.6, 0.8)), bundle("bp_caravan", 3.8, False, "버려진 대상 수레", mem("bp_caravan_cart", 0, -0.3, yawJit=30), mem("bp_amphora_set", -2.6, 0.7, yawJit=60, chance=0.7), mem("supply", 2.6, 0.8), mem("bone", 0.6, 1.6), fix("lantern", 2.8, -0.6, 0.7)), bundle("bp_tent", 4.0, True, "유목민 천막", mem("bp_tent_nomad", 0, -0.8, yawJit=10), mem("bp_amphora_set", 2.9, 0.9, yawJit=60, chance=0.7), mem("supply", -2.8, 0.8), fix("brazier_wood", 0.4, 2.0), mem("bp_banner_pole", -3.2, -1.0, yawJit=20, chance=0.7)), bundle("bp_sunstone", 3.0, True, "해 돌 + 석상 받침", mem("bp_sunstone", -1.2, -0.5, yawJit=30), mem("bp_statue_feet", 1.8, -0.2, yawJit=20, chance=0.8), mem("pebble", 0.2, 1.3), fix("glow_sun", -1.2, 0.4, 0.7)), bundle("bp_banner", 2.4, True, "깃대 + 돌무지", mem("bp_banner_pole", 0, -0.3, yawJit=30), mem("rock", 1.3, 0.5, mul=0.7), mem("bone", -1.3, 0.7), mem("dgrass", 0.4, 1.2)), ] R = [ ring("메마른 우물", ["bp_well", "caravan_wreck", "bp_columns", "bp_caravan", "cactus_stand", "bp_tent"], 6, 0.9, 30, 330), ring("해골 분지", ["bone_field", "bp_obelisk", "rock_outcrop", "bp_sunstone"], 4, 0.93, 110, 260), ring("해골 분지", ["bp_caravan", "bone_field", "bp_banner", "dead_tree", "cactus_stand", "bp_well"], 6, 0.88, 280, 450), ring("모래 폭풍 고개", ["bp_arch", "rock_outcrop", "bp_obelisk"], 3, 0.93, 150, 280), ring("모래 폭풍 고개", ["stone_circle", "bp_banner", "cactus_stand", "bp_caravan"], 4, 0.9, 300, 440), ring("무너진 신전 터", ["bp_arch", "bp_columns", "bp_altar", "ruin_columns"], 4, 0.95, 120, 240), ring("무너진 신전 터", ["bp_columns", "stone_circle", "bp_sunstone", "caravan_wreck"], 4, 0.95, 300, 420), ring("오아시스 둔치", ["bp_tent", "oasis_reeds", "bp_well", "cactus_stand"], 4, 0.92, 20, 150), ring("오아시스 둔치", ["oasis_reeds", "bp_caravan", "dead_tree", "bp_banner", "oasis_reeds"], 5, 0.9, 190, 350), ring("붉은 바위 협곡", ["bp_obelisk", "rock_outcrop", "bp_sunstone", "stone_circle"], 4, 0.93, 60, 200), ring("붉은 바위 협곡", ["cactus_stand", "bp_columns", "bone_field", "bp_banner", "rock_outcrop"], 5, 0.9, 230, 400), ring("서쪽 그늘", ["bp_tent", "dead_tree", "rock_outcrop"], 3, 0.9), ring("협곡 그늘", ["rock_outcrop", "bp_banner", "cactus_stand"], 3, 0.9), ring("남서 모래턱", ["bp_caravan", "cactus_stand", "bone_field"], 3, 0.9), ring("대상 야영지", ["bp_tent", "caravan_wreck", "rock_outcrop"], 3, 0.92, 240, 340), ] W = [("bp_altar", "태양 제단", 180, 3.0, 3.6, "태양 제단"), ("bp_obelisk", "태양 제단", 140, 2.5, 3.4, "오벨리스크"), ("bp_obelisk", "태양 제단", 220, 2.5, 3.4, "오벨리스크"), ("bp_arch", "태양 제단", 105, 3.0, 4.2, "무너진 아치"), ("bp_arch", "태양 제단", 255, 3.0, 4.2, "무너진 아치"), ("bp_sunstone", "무너진 신전 터", 180, 2.5, 3.0, "해 돌"), ("bp_tent", "대상 야영지", 90, 2.5, 4.0, "유목민 천막")] road = ["column_drums", "obelisk_top", "amphora_set", "statue_feet", "banner_pole", "sunstone"] low = ["obelisk_top", "amphora_set"] return B, R, W, road, low, band("길가 유적 조각", 2.6, 6.5, 4.2, 0.45, "bp_roadside", "bp_roadside_low", ["태양 제단"], 25, 2.0) DECO = {"Graveyard": deco_graveyard, "RuinedKeep": deco_keep, "SpiderCave": deco_cave, "PlagueCatacomb": deco_catacomb, "HarborExecution": deco_harbor, "ScorchedDesert": deco_desert} def prop_item(theme, p, w=1): sx, sy, sz = p["size"][0], p["size"][1], p["size"][2] # 블렌더 좌표: x · y(깊이) · z(높이) block = p.get("meta", {}).get("block", "auto") return {"path": "%s/%s/Prefabs/FRProp_%s_%s.prefab" % (PROPS, theme, theme, p["name"]), "fit": "s", "a": 0.95, "b": 1.08, "occ": round(max(sx, sy) * 0.45, 2), "w": w, "fade": sz > 2.4, "anchor": "c", "sink": 0.02, "block": block, "tilt": 0.0, "yoff": 0.0} def main(): backup = "--no-backup" not in sys.argv if backup: os.makedirs(BACKUP, exist_ok=True) for name, fn in DECO.items(): theme = THEME[name] path = os.path.join(ROOT, name, name + ".map.json") if backup: shutil.copy2(path, os.path.join(BACKUP, name + ".map.json")) raw = io.open(path, "rb").read() bom = raw.startswith(b"\xef\xbb\xbf") d = json.loads(raw.decode("utf-8-sig")) listing = json.load(open(os.path.join(ROOT, "_Shared", "Props", theme, "props.json"), encoding="utf-8")) by = {p["name"]: p for p in listing["props"]} # 앞서 넣은 것을 걷어낸다 d["pools"] = [p for p in d["pools"] if not p["name"].startswith("bp_")] d["bundles"] = [b for b in d["bundles"] if not b["name"].startswith("bp_")] d["places"] = [p for p in d["places"] if not (p.get("bundle", "").startswith("bp_") or p.get("pool", "").startswith("bp_"))] d["bands"] = [b for b in d["bands"] if not b["pool"].startswith("bp_")] # 사막: 보스 자리 뒤 한가운데는 새로 만든 태양 제단이 차지한다(앞서 놓았던 기둥 하나는 뺀다) if name == "ScorchedDesert": d["places"] = [p for p in d["places"] if p.get("label") != "제단 기둥"] # 항구: 처형대 곁의 화로가 보스 자리(반지름 22 m) 안에 0.7 m 걸린다 → 처형대를 1.4 m 뒤로 물린다(여러 번 돌려도 같은 값) if name == "HarborExecution": for p in d["places"]: if p.get("label") == "처형대": p["z"] = -83.9 B, R, W, road, low, bd = fn() for p in listing["props"]: d["pools"].append({"name": "bp_" + p["name"], "items": [prop_item(theme, p)]}) d["pools"].append({"name": "bp_roadside", "items": [prop_item(theme, by[n]) for n in road]}) d["pools"].append({"name": "bp_roadside_low", "items": [prop_item(theme, by[n]) for n in low]}) pools = {p["name"] for p in d["pools"]} fixtures = {f["name"] for f in d["fixtures"]} bad = [] for b in B: for m in b["members"]: if m["fixture"]: if m["fixture"] not in fixtures: bad.append("%s: 광원 장치 %s" % (b["name"], m["fixture"])) elif m["pool"] not in pools: bad.append("%s: 풀 %s" % (b["name"], m["pool"])) d["bundles"] += B names = {b["name"] for b in d["bundles"]} areas = {a["name"] for a in d["areas"]} for r in R: if r["area"] not in areas: bad.append("고리 공터 " + r["area"]) for b in r["bundles"]: if b not in names: bad.append("고리 %s: 묶음 %s" % (r["area"], b)) if bad: print("!!", name, bad) sys.exit(1) d["rings"] = R # 지정 배치 — 이미 놓인 것과 겹치지 않게 radius = {b["name"]: b["radius"] for b in d["bundles"]} taken = [] for p in d["places"]: taken.append((p["x"], p["z"], radius.get(p.get("bundle", ""), 3.0) * 0.8)) placed = [] for bname, area, ang, beyond, r, label in W: pos = find(d, area, ang, beyond, r, taken) if pos is None: placed.append("%s(자리 없음)" % label) continue x, z, yaw = pos d["places"].append({"bundle": bname, "pool": "", "fixture": "", "align": "", "x": x, "z": z, "yaw": yaw, "mul": 1.0, "label": label, "mark": "landmark"}) taken.append((x, z, r * 0.8)) placed.append("%s(%.0f, %.0f)" % (label, x, z)) d["bands"].insert(0, bd) # 띠는 적힌 차례로 자리를 잡는다 — 새 소품의 띠를 맨 앞에 둔다 text = json.dumps(d, ensure_ascii=False, indent=2) io.open(path, "w", encoding="utf-8-sig" if bom else "utf-8", newline="\n").write(text + "\n") print("== %s: 소품 풀 %d · 새 묶음 %d · 고리 %d · 지정 배치 %s" % (name, len(listing["props"]), len(B), len(R), ", ".join(placed))) if backup: print("backup", BACKUP) main()