370 lines
34 KiB
Python
370 lines
34 KiB
Python
# -*- 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()
|