diff --git a/AgentScripts/WL815c_MakeAsset.cs b/AgentScripts/WL815c_MakeAsset.cs new file mode 100644 index 000000000..ef0e3cf33 --- /dev/null +++ b/AgentScripts/WL815c_MakeAsset.cs @@ -0,0 +1,62 @@ +// WL-815c 설정 에셋 생성 — Assets/WL/Combat/Stage/Barrier/Resources/WL/WLStageBarrierSettings.asset +// unity command run_script --file AgentScripts/WL815c_MakeAsset.cs --entry WL815c_MakeAsset.Run + +public static class WL815c_MakeAsset +{ + const string kDir = "Assets/WL/Combat/Stage/Barrier/Resources/WL"; + const string kPath = kDir + "/WLStageBarrierSettings.asset"; + + public static string Run() + { + System.IO.Directory.CreateDirectory(kDir); + UnityEditor.AssetDatabase.Refresh(); + + var so = UnityEditor.AssetDatabase.LoadAssetAtPath(kPath); + bool made = false; + if (so == null) + { + so = UnityEngine.ScriptableObject.CreateInstance(); + UnityEditor.AssetDatabase.CreateAsset(so, kPath); + made = true; + } + + // ── ⓐ 링 = Hovl Marker 3 Zone Loop (기준서 §D-3 1순위 · 실측 시각 반경 1.10 m) + so.ringPrefab = Load("Assets/ResWork/VFX/Hovl Studio/Map track markers VFX/Prefabs/Marker 3 Zone Loop.prefab"); + so.ringPrefabVisualRadius = 1.1f; + + // ── ⓑ 프롭 = 울타리(LowPolyFantasyVillage Fence_01 실측 2.38 × 1.02 m · 120 tri) + 바위 2종 + so.propPrefabs = new UnityEngine.GameObject[] + { + Load("Assets/ResWork/LowPolyFantasyVillage_URP/Prefabs/Props/Fence_01.prefab"), + Load("Assets/ResWork/LowPolyFantasyVillage_URP/Prefabs/Props/Fence_01.prefab"), + Load("Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Rock Assets/Prefabs/With_Box_Colliders/1 Color/Block_Rocks/Rock_Block_crk_m_01.prefab"), + }; + + // ── ⓒ 불꽃 = LMHPOLY Fire_Particles (실측 3 PS · 정상상태 125 파티클) + so.firePrefabs = new UnityEngine.GameObject[] + { + Load("Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Bonus Assets/Prefabs/ParticleSystem/Fire_Particles.prefab"), + }; + + UnityEditor.EditorUtility.SetDirty(so); + UnityEditor.AssetDatabase.SaveAssets(); + UnityEditor.AssetDatabase.Refresh(); + + var chk = UnityEditor.AssetDatabase.LoadAssetAtPath(kPath); + var res = UnityEngine.Resources.Load(WL.Combat.Stage.WLStageBarrierSettings.ResourcesPath); + return (made ? "생성" : "갱신") + " " + kPath + + " · 에셋로드 " + (chk != null) + + " · Resources.Load " + (res != null) + + " · ring " + (chk != null && chk.ringPrefab != null ? chk.ringPrefab.name : "null") + + " · props " + (chk != null && chk.propPrefabs != null ? chk.propPrefabs.Length : 0) + + " · fires " + (chk != null && chk.firePrefabs != null ? chk.firePrefabs.Length : 0) + + " · style " + (chk != null ? chk.style.ToString() : "?"); + } + + static UnityEngine.GameObject Load(string p) + { + var g = UnityEditor.AssetDatabase.LoadAssetAtPath(p); + if (g == null) UnityEngine.Debug.Log("[WL815c] 프리팹 없음 " + p); + return g; + } +} diff --git a/AgentScripts/WL815c_PROBE.txt b/AgentScripts/WL815c_PROBE.txt new file mode 100644 index 000000000..1b85d5eaa --- /dev/null +++ b/AgentScripts/WL815c_PROBE.txt @@ -0,0 +1,84 @@ +WL-815c 스테이지 장벽 프로브 · 2026-09-11 00:51:58 + 앵커 = 기준서 §D-1 존1 (115.0, 0.4, -22.0) 반경 14 m + +① 설정 에셋(C45 — 값은 전부 SO) + PASS 1-1 에셋 로드 — Resources/WL/WLStageBarrierSettings + PASS 1-2 막기 값 — leaveMargin 2 · grace 0.5 s · 쿨 1 s · inset 1.5 m · 샘플 4/20 m + PASS 1-3 몹 값 — 몹여유 3 · 유예 1 s · 쿨 2 s + PASS 1-4 카브 기본 off — carveNavMesh 0 · arc 4 m · 상한 32 · 두께 1.5 · 높이 3 + PASS 1-5 스타일 — style=MagicRing · 입장 0.6 s · 클리어 0.6 s + PASS 1-6 ⓐ 링 프리팹 — Marker 3 Zone Loop · 실측 시각반경 1.1 m · 겹 2 + PASS 1-7 ⓑ 프롭 프리팹 — Fence_01, Fence_01, Rock_Block_crk_m_01 · 간격 2.5 m · 상한 64 + PASS 1-8 ⓒ 불꽃 프리팹 — Fire_Particles · 간격 3 m · 상한 48 · 파티클 총량 1200 + +② 임시 NavMesh 베이크(에디트 모드에서 실 워프 지점을 재기 위해) + PASS 2-1 NavMesh 베이크 — 정점 64 · 앵커 스냅 (115.00, 0.48, -22.00) + +③ Set / Clear 왕복 (오브젝트 0 · 풀 반납) + PASS 3-1 초기 — Active=false · IsInside(어디든)=true → True + PASS 3-2 Set — Active=true · 중심 (115.0, 0.4, -22.0) 반경 14 · 그림 인스턴스 2 · Instantiate 누계 2 + PASS 3-3 IsInside — 중심=안 · 13.9 m=안 · 14.1 m=밖 · 높이 무관(y+50 → True) + PASS 3-4a Clear 직후 = 페이드 중 — Active=false · 페이드 중 활성 2 · 진행도 1.00 + PASS 3-4 페이드 끝 → 활성 0 · 풀 반납 — 활성 0 · 풀 2(= Instantiate 누계 2) · Destroy 0 + PASS 3-5 재Set → 풀 재사용 — Instantiate 2 → 2(증가 0) · 활성 2 + PASS 3-6 입장/클리어 연출 — 스케일 t=0 0.00 → t=.5 6.23 → t=1 12.45 (appear 0.6 s / fade 0.6 s) + PASS 3-7 반경 0 → Clear — Set(r=0) → Active=False + +④ 되돌릴 지점 계산(순수 함수 · NearestInsidePoint) + (145.0, 0.4, -22.0) (중심 30.0 m) → (127.50, 0.40, -22.00) (중심 12.50 m · 방위 유지 Y) + (115.0, 0.4, -47.0) (중심 25.0 m) → (115.00, 0.40, -34.50) (중심 12.50 m · 방위 유지 Y) + (75.0, 3.4, 18.0) (중심 56.6 m) → (106.16, 3.40, -13.16) (중심 12.50 m · 방위 유지 Y) + (129.5, 0.4, -22.0) (중심 14.5 m) → (127.50, 0.40, -22.00) (중심 12.50 m · 방위 유지 Y) + PASS 4-1 안쪽 12.5 m · 방위 유지 — 4/4 · inset 1.5 m + PASS 4-2 정중앙 예외 — 중심 그대로 (115.00, 0.40, -22.00)(0 나누기 없음) + +⑤ 실 워프(원본 Actor.Set_Warp · NavMesh 스냅) + PASS 5-1 스냅 지점 = 안쪽 · NavMesh 위 — 밖 35 m → (127.50, 0.48, -22.00) · 중심 12.50 m(< 14) · NavMesh 위 True + PASS 5-2 NavMesh 없으면 워프 안 함 — 먼 좌표(9000,0,9000) → false = 워프 포기(지형 뚫기 방지) + PASS 5-3 PC 실 워프 — 밖 30.0 m → 중심 12.50 m · PlayerWarps 1 · (145.0, 0.4, -22.0) → (127.5, 0.5, -22.0) + PASS 5-4 안에 있으면 무동작 — 중심에서 ForceCheck → 워프 안 함 + PASS 5-5 몹 복귀 — 밖 28 m → 중심 12.50 m · MobWarps 1 + PASS 5-6 유예 안에서는 안 되돌린다 — grace 999 s · 틱 ×2 → 워프 0 (OutsideSeen 0) + +⑥ 스타일 전환 3회 (PD 가 값 하나로 바꾼다) + PASS 6-1 MagicRing — 활성 2 · 렌더러 8 · 머티리얼 1 · tri 0 · PS 8 · 빌드 1.4 ms · 링 스케일 12.45(반경 14 ÷ 실측 1.1) + PASS 6-2 Props — 활성 35 · 렌더러 35 · 머티리얼 2 · tri 4530 · PS 0 · 빌드 1.3 ms · 프롭 35개 · 실간격 2.51 m + PASS 6-3 Fire — 활성 29 · 렌더러 87 · 머티리얼 3 · tri 0 · PS 87 · 빌드 7.6 ms · 불꽃 29개 · 방출률 ×0.33 · 파티클 추정 1200(개당 125) + PASS 6-4 전환 누계 — StyleSwitches 3 · Instantiate 누계 66 + PASS 6-5 None → 그림 0 — 활성 그림 0 · 판정은 그대로 Active=True + +⑦ NavMesh 카브 on/off (옵션 · 기준서 §D-2 ②) + PASS 7-1 off(기본) → 카브 0 — carveNavMesh 0 → 오브젝트 0 + PASS 7-2 on → 세그먼트 생성 — 둘레 88.0 m ÷ arc 4 m = 22(상한 32) · 오브젝트 22 · 생성 0.41 ms · Set 전체 2.32 ms + PASS 7-3 세그먼트 기하 — carving=true 22 · Box 22 · 상자 4.60 × 두께 1.50 m > 이웃 간격 3.98 m(이론 4.00 · 겹침 15 %) · 원 이탈 최대 0.0000 m + NavMesh 삼각형 32 → 32 (에디트 모드는 carve 가 즉시 반영되지 않을 수 있다 — 실 반영·재베이크 ms 는 Play 확인) + PASS 7-4 Clear → 카브 회수 — 오브젝트 0 + +⑧ GC (프레임당 할당 0) + PASS 8-1 반경 안 · 틱 ×200 — 할당 0 B (틱마다 0.0 B) · 계측 = Profiler.GetMonoUsedSizeLong + 대조군(byte[65536] ×200 = 12.8 MB) 13926400 B · sink True + PASS 8-2 폴링 동작 — Scans 2 · Checks 0 → 1 (몹 1기 · 안쪽이라 워프 0) + PASS 8-3 몹 1기 · 틱 ×200 — 할당 0 B + +⑨ C8 롤백 진리표 + PASS 9-1 enabled=0 → Set 무동작 — Active=False · Instantiate 0 (오브젝트 0) + PASS 9-2 RuntimeDisabled=1 → Set 무동작 — Active=False · Instantiate 0 + PASS 9-3 에셋 없음 → Set 무동작 — Active=False · FeatureEnabled=False + PASS 9-4 blockEnabled=0 → 판정 0 · 그림은 남는다 — Checks 0 · 그림 2 + PASS 9-5 showVisual=0 → 그림 0 · 판정은 산다 — 그림 0 · Active=True + +⑩ 성능 표 (반경 14 m · 스타일별) + | 스타일 | 인스턴스 | 렌더러 | 머티리얼 | 삼각형 | 파티클시스템 | 정상상태 파티클 | 빌드 ms | + |---|---|---|---|---|---|---|---| + | MagicRing | 2 | 8 | 1 | 0 | 8 | 8 | 0.9 | + | Props | 35 | 35 | 2 | 4530 | 0 | 0 | 1.2 | + | Fire | 29 | 87 | 3 | 0 | 87 | 1200 | 8.2 | + PASS 10-1 성능 표 — 위 표 참조(에디트 모드 측정 · 실 드로우콜은 캡처의 UnityStats) + 반경별 프롭 개수(간격 2.5 m · 상한 64) + r 8 m(둘레 50.3 m) → 프롭 20 · 실간격 2.51 m · 카브 세그(on 일 때) 13 + r 14 m(둘레 88.0 m) → 프롭 35 · 실간격 2.51 m · 카브 세그(on 일 때) 22 + r 20 m(둘레 125.7 m) → 프롭 50 · 실간격 2.51 m · 카브 세그(on 일 때) 32 + r 30 m(둘레 188.5 m) → 프롭 64 · 실간격 2.95 m · 카브 세그(on 일 때) 32 + r 40 m(둘레 251.3 m) → 프롭 64 · 실간격 3.93 m · 카브 세그(on 일 때) 32 + +RESULT PASS · 43 PASS · 0 FAIL diff --git a/AgentScripts/WL815c_Probe.cs b/AgentScripts/WL815c_Probe.cs new file mode 100644 index 000000000..df39a7478 --- /dev/null +++ b/AgentScripts/WL815c_Probe.cs @@ -0,0 +1,636 @@ +// ───────────────────────────────────────────────────────────────────────────── +// WL815c_Probe.cs — 스테이지 장벽 검증(에디트 모드 · Play 0 · 로그인 0) +// unity command run_script --file AgentScripts/WL815c_Probe.cs --entry WL815c_Probe.RunAll +// 산출물: AgentScripts/WL815c_PROBE.txt +// ───────────────────────────────────────────────────────────────────────────── + +using System; +using System.Diagnostics; +using System.IO; +using System.Text; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; +using UnityEngine.AI; +using UnityEngine.Profiling; +using UnityEngine.SceneManagement; +using WL.Combat.Stage; + +public static class WL815c_Probe +{ + static StringBuilder s_sb; + static int s_pass, s_fail; + + // 기준서 §D-1 존1 앵커 + static readonly Vector3 kAnchor = new Vector3(115f, 0.4f, -22f); + const float kRadius = 14f; + + static void L(string s) { s_sb.AppendLine(s); } + static void Chk(string name, bool ok, string detail) + { + if (ok) s_pass++; else s_fail++; + L((ok ? " PASS " : " FAIL ") + name + " — " + detail); + } + + public static string RunAll() + { + s_sb = new StringBuilder(); s_pass = 0; s_fail = 0; + L("WL-815c 스테이지 장벽 프로브 · " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + L(" 앵커 = 기준서 §D-1 존1 " + kAnchor.ToString("F1") + " 반경 " + kRadius + " m"); + + var keepSo = WLStageBarrierSettings.Instance; + Scene temp = default(Scene); + try + { + temp = EditorSceneManager.NewScene( + NewSceneSetup.DefaultGameObjects, + NewSceneMode.Single); + + Sec1_Settings(); + var ground = MakeGround(); + bool navOk = BakeNav(ground); + Sec2_SetClear(); + Sec3_WarpMath(); + Sec4_WarpReal(navOk); + Sec5_Styles(); + Sec6_Carve(); + Sec7_GC(); + Sec8_C8(); + Sec9_Cost(); + } + catch (Exception ex) { s_fail++; L(" FAIL 예외 — " + ex.GetType().Name + " " + ex.Message + "\n" + ex.StackTrace); } + finally + { + try { StageBarrier.HardResetForProbe(); } catch { } + WLStageBarrierSettings.SetInstanceForProbe(keepSo); + WLStageBarrierSettings.RuntimeDisabled = false; + } + + L(""); + L("RESULT " + (s_fail == 0 ? "PASS" : "FAIL") + " · " + s_pass + " PASS · " + s_fail + " FAIL"); + File.WriteAllText("AgentScripts/WL815c_PROBE.txt", s_sb.ToString()); + return (s_fail == 0 ? "PASS" : "FAIL") + " " + s_pass + "/" + (s_pass + s_fail); + } + + // ═════════════════════════════════════════════════════════════════════════ + static void Sec1_Settings() + { + L(""); + L("① 설정 에셋(C45 — 값은 전부 SO)"); + var c = WLStageBarrierSettings.Instance; + Chk("1-1 에셋 로드", c != null, c != null ? "Resources/WL/WLStageBarrierSettings" : "null"); + if (c == null) return; + Chk("1-2 막기 값", c.leaveMargin > 0 && c.graceSeconds >= 0, + "leaveMargin " + c.leaveMargin + " · grace " + c.graceSeconds + " s · 쿨 " + c.warpCooldownSeconds + + " s · inset " + c.warpInsetMeters + " m · 샘플 " + c.warpNavSampleRadius + "/" + c.warpNavSampleRadiusFar + " m"); + Chk("1-3 몹 값", c.blockMobs, "몹여유 " + c.mobLeaveMargin + " · 유예 " + c.mobGraceSeconds + " s · 쿨 " + c.mobWarpCooldownSeconds + " s"); + Chk("1-4 카브 기본 off", !c.carveNavMesh, "carveNavMesh " + (c.carveNavMesh ? 1 : 0) + " · arc " + c.carveSegmentArcMeters + + " m · 상한 " + c.maxCarveSegments + " · 두께 " + c.carveThickness + " · 높이 " + c.carveHeight); + Chk("1-5 스타일", true, "style=" + c.style + " · 입장 " + c.appearSeconds + " s · 클리어 " + c.fadeSeconds + " s"); + Chk("1-6 ⓐ 링 프리팹", c.ringPrefab != null, (c.ringPrefab != null ? c.ringPrefab.name : "null") + + " · 실측 시각반경 " + c.ringPrefabVisualRadius + " m · 겹 " + (1 + c.ringExtraRings)); + Chk("1-7 ⓑ 프롭 프리팹", c.propPrefabs != null && c.propPrefabs.Length > 0, + Names(c.propPrefabs) + " · 간격 " + c.propSpacing + " m · 상한 " + c.maxProps); + Chk("1-8 ⓒ 불꽃 프리팹", c.firePrefabs != null && c.firePrefabs.Length > 0, + Names(c.firePrefabs) + " · 간격 " + c.fireSpacing + " m · 상한 " + c.maxFires + " · 파티클 총량 " + c.maxTotalParticles); + } + + static string Names(GameObject[] a) + { + if (a == null || a.Length == 0) return "(없음)"; + var sb = new StringBuilder(); + for (int i = 0; i < a.Length; i++) { if (i > 0) sb.Append(", "); sb.Append(a[i] != null ? a[i].name : "null"); } + return sb.ToString(); + } + + // ═════════════════════════════════════════════════════════════════════════ + static GameObject MakeGround() + { + var g = GameObject.CreatePrimitive(PrimitiveType.Plane); + g.name = "[WL815c] Ground"; + g.transform.position = new Vector3(kAnchor.x, kAnchor.y, kAnchor.z); + g.transform.localScale = Vector3.one * 12f; // 120 × 120 m + return g; + } + + static bool BakeNav(GameObject ground) + { + L(""); + L("② 임시 NavMesh 베이크(에디트 모드에서 실 워프 지점을 재기 위해)"); + try + { + var surf = ground.AddComponent(); + surf.collectObjects = Unity.AI.Navigation.CollectObjects.All; + surf.BuildNavMesh(); + var tri = NavMesh.CalculateTriangulation(); + NavMeshHit h; + bool hit = NavMesh.SamplePosition(kAnchor, out h, 5f, NavMesh.AllAreas); + Chk("2-1 NavMesh 베이크", tri.vertices != null && tri.vertices.Length > 0 && hit, + "정점 " + (tri.vertices != null ? tri.vertices.Length : 0) + " · 앵커 스냅 " + (hit ? h.position.ToString("F2") : "실패")); + return hit; + } + catch (Exception ex) { Chk("2-1 NavMesh 베이크", false, "미확인 — " + ex.Message); return false; } + } + + // ═════════════════════════════════════════════════════════════════════════ + static void Sec2_SetClear() + { + L(""); + L("③ Set / Clear 왕복 (오브젝트 0 · 풀 반납)"); + var B = typeof(StageBarrier); + StageBarrier.HardResetForProbe(); + + Chk("3-1 초기", !StageBarrier.Active, "Active=false · IsInside(어디든)=true → " + + StageBarrier.IsInside(new Vector3(999f, 0f, 999f))); + + StageBarrier.Set(kAnchor, kRadius); + int inst1 = StageBarrierVisual.Instantiates; + int live1 = StageBarrierVisual.LiveCount; + Chk("3-2 Set", StageBarrier.Active && live1 > 0, + "Active=true · 중심 " + StageBarrier.Center.ToString("F1") + " 반경 " + StageBarrier.Radius + + " · 그림 인스턴스 " + live1 + " · Instantiate 누계 " + inst1); + + Chk("3-3 IsInside", StageBarrier.IsInside(kAnchor) + && StageBarrier.IsInside(kAnchor + new Vector3(13.9f, 0f, 0f)) + && !StageBarrier.IsInside(kAnchor + new Vector3(14.1f, 0f, 0f)), + "중심=안 · 13.9 m=안 · 14.1 m=밖 · 높이 무관(y+50 → " + + StageBarrier.IsInside(kAnchor + new Vector3(0f, 50f, 0f)) + ")"); + + // 클리어 → 페이드 도중에는 아직 살아 있어야 한다(연출) + StageBarrier.Clear(); + StageBarrier.ForceTick(); + int liveMid = StageBarrierVisual.LiveCount; + Chk("3-4a Clear 직후 = 페이드 중", !StageBarrier.Active && liveMid > 0, + "Active=false · 페이드 중 활성 " + liveMid + " · 진행도 " + StageBarrierVisual.Phase.ToString("F2")); + + // 페이드 시계를 끝으로 → 실제 종료 경로(ReturnAll)를 탄다 + StageBarrierVisual.FinishPhaseForProbe(); + StageBarrier.ForceTick(); + int live2 = StageBarrierVisual.LiveCount; + int pooled = StageBarrierVisual.PooledInstances; + Chk("3-4 페이드 끝 → 활성 0 · 풀 반납", !StageBarrier.Active && live2 == 0 && pooled == inst1, + "활성 " + live2 + " · 풀 " + pooled + "(= Instantiate 누계 " + inst1 + ") · Destroy 0"); + + // 두 번째 Set — 풀에서 꺼내 쓰므로 Instantiate 가 늘지 않아야 한다 + StageBarrier.Set(kAnchor, kRadius); + int inst2 = StageBarrierVisual.Instantiates; + Chk("3-5 재Set → 풀 재사용", inst2 == inst1 && StageBarrierVisual.LiveCount == live1, + "Instantiate " + inst1 + " → " + inst2 + "(증가 " + (inst2 - inst1) + ") · 활성 " + StageBarrierVisual.LiveCount); + + // 입장 연출 + StageBarrierVisual.ForcePhase(0f); + float s0 = FirstScale(); + StageBarrierVisual.ForcePhase(0.5f); + float s5 = FirstScale(); + StageBarrierVisual.ForcePhase(1f); + float s1 = FirstScale(); + Chk("3-6 입장/클리어 연출", s0 < s5 && s5 < s1, "스케일 t=0 " + s0.ToString("F2") + " → t=.5 " + s5.ToString("F2") + + " → t=1 " + s1.ToString("F2") + " (appear " + WLStageBarrierSettings.Instance.appearSeconds + + " s / fade " + WLStageBarrierSettings.Instance.fadeSeconds + " s)"); + + Chk("3-7 반경 0 → Clear", true, ProbeZero()); + _ = B; + } + + static string ProbeZero() + { + StageBarrier.Set(kAnchor, 0f); + string r = "Set(r=0) → Active=" + StageBarrier.Active; + StageBarrier.Set(kAnchor, kRadius); + return r; + } + + static float FirstScale() + { + var root = StageBarrierVisual.Root; + if (root == null || root.childCount == 0) return -1f; + for (int i = 0; i < root.childCount; i++) + if (root.GetChild(i).gameObject.activeSelf) return root.GetChild(i).localScale.x; + return 0f; + } + + // ═════════════════════════════════════════════════════════════════════════ + static void Sec3_WarpMath() + { + L(""); + L("④ 되돌릴 지점 계산(순수 함수 · NearestInsidePoint)"); + var c = WLStageBarrierSettings.Instance; + float inset = c.warpInsetMeters; + float want = kRadius - inset; + + Vector3[] outs = { + kAnchor + new Vector3(30f, 0f, 0f), + kAnchor + new Vector3(0f, 0f, -25f), + kAnchor + new Vector3(-40f, 3f, 40f), + kAnchor + new Vector3(14.5f, 0f, 0f), + }; + bool all = true; + for (int i = 0; i < outs.Length; i++) + { + var p = StageBarrier.NearestInsidePoint(c, kAnchor, kRadius, outs[i]); + float d = new Vector2(p.x - kAnchor.x, p.z - kAnchor.z).magnitude; + bool ok = Mathf.Abs(d - want) < 0.01f; + // 방향이 유지되는가(가장 가까운 안쪽 지점 = 같은 방위각) + var dirIn = new Vector2(p.x - kAnchor.x, p.z - kAnchor.z).normalized; + var dirOut = new Vector2(outs[i].x - kAnchor.x, outs[i].z - kAnchor.z).normalized; + bool sameDir = Vector2.Dot(dirIn, dirOut) > 0.999f; + all = all && ok && sameDir; + L(" " + outs[i].ToString("F1") + " (중심 " + + new Vector2(outs[i].x - kAnchor.x, outs[i].z - kAnchor.z).magnitude.ToString("F1") + " m) → " + + p.ToString("F2") + " (중심 " + d.ToString("F2") + " m · 방위 유지 " + (sameDir ? "Y" : "N") + ")"); + } + Chk("4-1 안쪽 " + want + " m · 방위 유지", all, "4/4 · inset " + inset + " m"); + + var mid = StageBarrier.NearestInsidePoint(c, kAnchor, kRadius, kAnchor); + Chk("4-2 정중앙 예외", Mathf.Approximately(mid.x, kAnchor.x) && Mathf.Approximately(mid.z, kAnchor.z), + "중심 그대로 " + mid.ToString("F2") + "(0 나누기 없음)"); + } + + // ═════════════════════════════════════════════════════════════════════════ + static void Sec4_WarpReal(bool navOk) + { + L(""); + L("⑤ 실 워프(원본 Actor.Set_Warp · NavMesh 스냅)"); + var c = WLStageBarrierSettings.Instance; + StageBarrier.Set(kAnchor, kRadius); + StageBarrier.ResetDiagnostics(); + + if (!navOk) { Chk("5-0 NavMesh", false, "미확인 — 베이크 실패라 실 워프 지점은 재지 못했다"); } + else + { + Vector3 t; + bool ok = StageBarrier.ResolveWarpTarget(c, kAnchor, kRadius, kAnchor + new Vector3(35f, 0f, 0f), out t); + float d = new Vector2(t.x - kAnchor.x, t.z - kAnchor.z).magnitude; + NavMeshHit h; + bool onMesh = NavMesh.SamplePosition(t, out h, 0.3f, NavMesh.AllAreas); + Chk("5-1 스냅 지점 = 안쪽 · NavMesh 위", ok && d < kRadius && onMesh, + "밖 35 m → " + t.ToString("F2") + " · 중심 " + d.ToString("F2") + " m(< " + kRadius + ") · NavMesh 위 " + onMesh); + + // NavMesh 가 아예 없는 방향(평면 밖 200 m) + Vector3 t2; + bool ok2 = StageBarrier.ResolveWarpTarget(c, new Vector3(9000f, 0f, 9000f), kRadius, + new Vector3(9100f, 0f, 9000f), out t2); + Chk("5-2 NavMesh 없으면 워프 안 함", !ok2, "먼 좌표(9000,0,9000) → " + (ok2 ? "워프함(FAIL)" : "false = 워프 포기(지형 뚫기 방지)")); + } + + // 실 PC 프리팹으로 왕복 + var pcPrefab = AssetDatabase.LoadAssetAtPath("Assets/Res_Addr/PC/Ai01.prefab"); + if (pcPrefab == null) { Chk("5-3 PC 프리팹", false, "미확인 — Ai01.prefab 없음"); } + else + { + var pc = UnityEngine.Object.Instantiate(pcPrefab); + pc.transform.position = kAnchor + new Vector3(30f, 0f, 0f); + var actor = pc.GetComponent(); + if (actor == null) Chk("5-3 PCActor", false, "미확인 — Actor 컴포넌트 없음"); + else + { + // 🔴 에디트 모드는 Awake 가 돌지 않아 Actor.m_NavMeshAgent(비직렬화 protected)가 null 이다. + // Set_Warp 이 그것을 쓰므로 **프로브가** Awake 가 넣었을 값을 직접 심는다(게임 코드 무관 · 스캐폴딩). + SeedAgent(actor, pc); + try + { + Vector3 before = actor.Get_position(); + bool warped = StageBarrier.ForceCheck(actor, true); + Vector3 after = actor.Get_position(); + float dAfter = new Vector2(after.x - kAnchor.x, after.z - kAnchor.z).magnitude; + Chk("5-3 PC 실 워프", warped && dAfter < kRadius, + "밖 " + new Vector2(before.x - kAnchor.x, before.z - kAnchor.z).magnitude.ToString("F1") + + " m → 중심 " + dAfter.ToString("F2") + " m · PlayerWarps " + StageBarrier.PlayerWarps + + " · " + StageBarrier.LastWarpFrom.ToString("F1") + " → " + StageBarrier.LastWarpTo.ToString("F1")); + + // 안에 있으면 워프하지 않는다 + actor.transform.position = kAnchor; + bool w2 = StageBarrier.ForceCheck(actor, true); + Chk("5-4 안에 있으면 무동작", !w2, "중심에서 ForceCheck → 워프 " + (w2 ? "함(FAIL)" : "안 함")); + } + catch (Exception ex) { Chk("5-3 PC 실 워프", false, "미확인 — " + ex.GetType().Name + " " + ex.Message); } + } + UnityEngine.Object.DestroyImmediate(pc); + } + + // 몹 + var mobPrefab = AssetDatabase.LoadAssetAtPath("Assets/Res_Addr/Mobs/Mob/Batty_A.prefab"); + if (mobPrefab == null) { Chk("5-5 몹 프리팹", false, "미확인 — Batty_A.prefab 없음"); } + else + { + var mob = UnityEngine.Object.Instantiate(mobPrefab); + mob.transform.position = kAnchor + new Vector3(0f, 0f, 28f); + var ma = mob.GetComponent(); + if (ma == null) Chk("5-5 MobActor", false, "미확인 — Actor 없음"); + else + { + SeedAgent(ma, mob); + try + { + bool warped = StageBarrier.ForceCheck(ma, false); + float dAfter = new Vector2(ma.Get_position().x - kAnchor.x, ma.Get_position().z - kAnchor.z).magnitude; + Chk("5-5 몹 복귀", warped && dAfter < kRadius, + "밖 28 m → 중심 " + dAfter.ToString("F2") + " m · MobWarps " + StageBarrier.MobWarps); + } + catch (Exception ex) { Chk("5-5 몹 복귀", false, "미확인 — " + ex.GetType().Name + " " + ex.Message); } + } + UnityEngine.Object.DestroyImmediate(mob); + } + + // 유예 · 쿨다운 진리표(가짜 시각 없이 — 유예를 0 으로 둔 임시 SO) + var tmp = Clone(c); + tmp.graceSeconds = 999f; + WLStageBarrierSettings.SetInstanceForProbe(tmp); + StageBarrier.ResetDiagnostics(); + StageBarrier.Set(kAnchor, kRadius); + var g = new GameObject("probe_dummy"); + g.transform.position = kAnchor + new Vector3(40f, 0f, 0f); + StageBarrier.ForceTick(); + StageBarrier.ForceTick(); + Chk("5-6 유예 안에서는 안 되돌린다", StageBarrier.PlayerWarps == 0 && StageBarrier.MobWarps == 0, + "grace 999 s · 틱 ×2 → 워프 0 (OutsideSeen " + StageBarrier.OutsideSeen + ")"); + UnityEngine.Object.DestroyImmediate(g); + WLStageBarrierSettings.SetInstanceForProbe(c); + } + + // ═════════════════════════════════════════════════════════════════════════ + static void Sec5_Styles() + { + L(""); + L("⑥ 스타일 전환 3회 (PD 가 값 하나로 바꾼다)"); + var c = WLStageBarrierSettings.Instance; + StageBarrier.HardResetForProbe(); + WLStageBarrierSettings.SetInstanceForProbe(c); + + var styles = new StageBarrierStyle[] { + StageBarrierStyle.MagicRing, + StageBarrierStyle.Props, + StageBarrierStyle.Fire, + }; + for (int i = 0; i < styles.Length; i++) + { + StageBarrierVisual.StyleOverride = styles[i]; + StageBarrier.Set(kAnchor, kRadius); + StageBarrierVisual.ForcePhase(1f); + int rend, mats, ps; long tri; + StageBarrierVisual.MeasureCost(out rend, out mats, out tri, out ps); + string extra = ""; + if (styles[i] == StageBarrierStyle.MagicRing) + extra = " · 링 스케일 " + StageBarrierVisual.RingScale.ToString("F2") + "(반경 " + kRadius + " ÷ 실측 " + c.ringPrefabVisualRadius + ")"; + else if (styles[i] == StageBarrierStyle.Props) + extra = " · 프롭 " + StageBarrierVisual.PropCount + "개 · 실간격 " + StageBarrierVisual.PropSpacingUsed.ToString("F2") + " m"; + else + extra = " · 불꽃 " + StageBarrierVisual.FireCount + "개 · 방출률 ×" + StageBarrierVisual.FireRateMul.ToString("F2") + + " · 파티클 추정 " + StageBarrierVisual.FireParticleEstimate + "(개당 " + StageBarrierVisual.FirePerInstanceEstimate + ")"; + Chk("6-" + (i + 1) + " " + styles[i], StageBarrierVisual.CurrentStyle == styles[i] && StageBarrierVisual.LiveCount > 0, + "활성 " + StageBarrierVisual.LiveCount + " · 렌더러 " + rend + " · 머티리얼 " + mats + " · tri " + tri + + " · PS " + ps + " · 빌드 " + StageBarrierVisual.LastBuildMs.ToString("F1") + " ms" + extra); + } + Chk("6-4 전환 누계", StageBarrierVisual.StyleSwitches >= 2, + "StyleSwitches " + StageBarrierVisual.StyleSwitches + " · Instantiate 누계 " + StageBarrierVisual.Instantiates); + + // None 으로 되돌리기 + StageBarrierVisual.StyleOverride = StageBarrierStyle.None; + StageBarrier.Set(kAnchor, kRadius); + Chk("6-5 None → 그림 0", StageBarrierVisual.LiveCount == 0 && StageBarrier.Active, + "활성 그림 " + StageBarrierVisual.LiveCount + " · 판정은 그대로 Active=" + StageBarrier.Active); + StageBarrierVisual.StyleOverride = (StageBarrierStyle)(-1); + } + + // ═════════════════════════════════════════════════════════════════════════ + static void Sec6_Carve() + { + L(""); + L("⑦ NavMesh 카브 on/off (옵션 · 기준서 §D-2 ②)"); + var c = WLStageBarrierSettings.Instance; + + StageBarrier.HardResetForProbe(); + WLStageBarrierSettings.SetInstanceForProbe(c); + StageBarrier.Set(kAnchor, kRadius); + Chk("7-1 off(기본) → 카브 0", StageBarrier.CarveObjectCount == 0, + "carveNavMesh " + (c.carveNavMesh ? 1 : 0) + " → 오브젝트 " + StageBarrier.CarveObjectCount); + + var tmp = Clone(c); + tmp.carveNavMesh = true; + WLStageBarrierSettings.SetInstanceForProbe(tmp); + StageBarrier.HardResetForProbe(); + WLStageBarrierSettings.SetInstanceForProbe(tmp); + + int want = StageBarrier.CarveSegmentCount(tmp, kRadius); + float circ = 2f * Mathf.PI * kRadius; + + var before = NavMesh.CalculateTriangulation(); + var sw = Stopwatch.StartNew(); + StageBarrier.Set(kAnchor, kRadius); + sw.Stop(); + Chk("7-2 on → 세그먼트 생성", StageBarrier.CarveObjectCount == want && want > 0, + "둘레 " + circ.ToString("F1") + " m ÷ arc " + tmp.carveSegmentArcMeters + " m = " + want + + "(상한 " + tmp.maxCarveSegments + ") · 오브젝트 " + StageBarrier.CarveObjectCount + + " · 생성 " + StageBarrier.LastCarveBuildMs.ToString("F2") + " ms · Set 전체 " + sw.Elapsed.TotalMilliseconds.ToString("F2") + " ms"); + + // 세그먼트 기하 확인 — 카브 오브젝트는 HideAndDontSave 라 FindObjectsByType 에 안 잡힌다(내부 목록으로 읽는다) + int carving = 0, boxShape = 0; float gap = -1f, sizeX = 0f, sizeZ = 0f, ringDev = 0f; + Vector3 p0 = Vector3.zero, p1 = Vector3.zero; + for (int i = 0; i < StageBarrier.CarveObjectCount; i++) + { + var ob = StageBarrier.CarveObstacleAt(i); + if (ob == null) continue; + if (ob.carving) carving++; + if (ob.shape == NavMeshObstacleShape.Box) boxShape++; + if (i == 0) { p0 = ob.transform.position; sizeX = ob.size.x; sizeZ = ob.size.z; } + if (i == 1) p1 = ob.transform.position; + ringDev = Mathf.Max(ringDev, Mathf.Abs( + new Vector2(ob.transform.position.x - kAnchor.x, ob.transform.position.z - kAnchor.z).magnitude - kRadius)); + } + gap = Vector3.Distance(p0, p1); + float step = circ / Mathf.Max(1, want); + Chk("7-3 세그먼트 기하", carving == want && boxShape == want && sizeX > gap && ringDev < 0.01f, + "carving=true " + carving + " · Box " + boxShape + " · 상자 " + sizeX.ToString("F2") + " × 두께 " + sizeZ.ToString("F2") + + " m > 이웃 간격 " + gap.ToString("F2") + " m(이론 " + step.ToString("F2") + " · 겹침 " + (tmp.carveOverlap * 100f).ToString("F0") + + " %) · 원 이탈 최대 " + ringDev.ToString("F4") + " m"); + + // 재베이크(카브 반영) 비용 — 에디트 모드에서 실제로 반영되는지 + var after = NavMesh.CalculateTriangulation(); + L(" NavMesh 삼각형 " + (before.indices != null ? before.indices.Length / 3 : 0) + " → " + (after.indices != null ? after.indices.Length / 3 : 0) + + " (에디트 모드는 carve 가 즉시 반영되지 않을 수 있다 — 실 반영·재베이크 ms 는 Play 확인)"); + + StageBarrier.Clear(); + Chk("7-4 Clear → 카브 회수", StageBarrier.CarveObjectCount == 0, "오브젝트 " + StageBarrier.CarveObjectCount); + WLStageBarrierSettings.SetInstanceForProbe(c); + StageBarrier.HardResetForProbe(); + WLStageBarrierSettings.SetInstanceForProbe(c); + } + + // ═════════════════════════════════════════════════════════════════════════ + static void Sec7_GC() + { + L(""); + L("⑧ GC (프레임당 할당 0)"); + var c = WLStageBarrierSettings.Instance; + StageBarrier.Set(kAnchor, kRadius); + StageBarrierVisual.ForcePhase(1f); + + for (int i = 0; i < 10; i++) StageBarrier.ProbeTick(); // 워밍 + long a = Profiler.GetMonoUsedSizeLong(); + for (int i = 0; i < 200; i++) StageBarrier.ProbeTick(); + long b = Profiler.GetMonoUsedSizeLong(); + Chk("8-1 반경 안 · 틱 ×200", b - a == 0, "할당 " + (b - a) + " B (틱마다 " + ((b - a) / 200f).ToString("F1") + " B) · 계측 = Profiler.GetMonoUsedSizeLong"); + + // 대조군 — 확실히 할당되는 배열 200개(계측기가 살아 있는지 확인) + long c0 = Profiler.GetMonoUsedSizeLong(); + object sink = null; + for (int i = 0; i < 200; i++) sink = new byte[65536]; + long c1 = Profiler.GetMonoUsedSizeLong(); + L(" 대조군(byte[65536] ×200 = 12.8 MB) " + (c1 - c0) + " B · sink " + (sink != null)); + + // 폴링 검사가 실제로 도는지 — 씬에 몹을 하나 놓고 재수집·판정이 도는지 본다 + var mobPrefab = AssetDatabase.LoadAssetAtPath("Assets/Res_Addr/Mobs/Mob/Batty_A.prefab"); + GameObject probeMob = null; + if (mobPrefab != null) + { + probeMob = UnityEngine.Object.Instantiate(mobPrefab); + probeMob.transform.position = kAnchor; // 안쪽 = 워프 대상 아님 + } + int chk0 = StageBarrier.Checks; + StageBarrier.ForceTick(); + Chk("8-2 폴링 동작", StageBarrier.Scans > 0 && (probeMob == null || StageBarrier.Checks > chk0), + "Scans " + StageBarrier.Scans + " · Checks " + chk0 + " → " + StageBarrier.Checks + + " (몹 " + (probeMob != null ? 1 : 0) + "기 · 안쪽이라 워프 " + (StageBarrier.MobWarps) + ")"); + + // 몹이 있는 상태에서 다시 GC 측정(실전에 가까운 조건) + for (int i = 0; i < 10; i++) StageBarrier.ProbeTick(); + long d0 = Profiler.GetMonoUsedSizeLong(); + for (int i = 0; i < 200; i++) StageBarrier.ProbeTick(); + long d1 = Profiler.GetMonoUsedSizeLong(); + Chk("8-3 몹 1기 · 틱 ×200", d1 - d0 == 0, "할당 " + (d1 - d0) + " B"); + if (probeMob != null) UnityEngine.Object.DestroyImmediate(probeMob); + _ = c; + } + + // ═════════════════════════════════════════════════════════════════════════ + static void Sec8_C8() + { + L(""); + L("⑨ C8 롤백 진리표"); + var real = WLStageBarrierSettings.Instance; + + // enabled = 0 + var off = Clone(real); off.enabled = false; + StageBarrier.HardResetForProbe(); + WLStageBarrierSettings.SetInstanceForProbe(off); + StageBarrier.Set(kAnchor, kRadius); + Chk("9-1 enabled=0 → Set 무동작", !StageBarrier.Active && StageBarrierVisual.Instantiates == 0, + "Active=" + StageBarrier.Active + " · Instantiate " + StageBarrierVisual.Instantiates + " (오브젝트 0)"); + + // RuntimeDisabled + StageBarrier.HardResetForProbe(); + WLStageBarrierSettings.SetInstanceForProbe(real); + WLStageBarrierSettings.RuntimeDisabled = true; + StageBarrier.Set(kAnchor, kRadius); + Chk("9-2 RuntimeDisabled=1 → Set 무동작", !StageBarrier.Active, + "Active=" + StageBarrier.Active + " · Instantiate " + StageBarrierVisual.Instantiates); + WLStageBarrierSettings.RuntimeDisabled = false; + + // 에셋 없음 + StageBarrier.HardResetForProbe(); + WLStageBarrierSettings.SetInstanceForProbe(null); + StageBarrier.Set(kAnchor, kRadius); + Chk("9-3 에셋 없음 → Set 무동작", !StageBarrier.Active && !StageBarrier.FeatureEnabled, + "Active=" + StageBarrier.Active + " · FeatureEnabled=" + StageBarrier.FeatureEnabled); + + // blockEnabled = 0 → 그림만 + var noBlock = Clone(real); noBlock.blockEnabled = false; + StageBarrier.HardResetForProbe(); + WLStageBarrierSettings.SetInstanceForProbe(noBlock); + StageBarrier.Set(kAnchor, kRadius); + var d = new GameObject("probe_far"); d.transform.position = kAnchor + new Vector3(60f, 0f, 0f); + for (int i = 0; i < 5; i++) StageBarrier.ForceTick(); + Chk("9-4 blockEnabled=0 → 판정 0 · 그림은 남는다", + StageBarrier.Checks == 0 && StageBarrierVisual.LiveCount > 0, + "Checks " + StageBarrier.Checks + " · 그림 " + StageBarrierVisual.LiveCount); + UnityEngine.Object.DestroyImmediate(d); + + // showVisual = 0 → 판정만 + var noVis = Clone(real); noVis.showVisual = false; + StageBarrier.HardResetForProbe(); + WLStageBarrierSettings.SetInstanceForProbe(noVis); + StageBarrier.Set(kAnchor, kRadius); + Chk("9-5 showVisual=0 → 그림 0 · 판정은 산다", + StageBarrierVisual.LiveCount == 0 && StageBarrier.Active, + "그림 " + StageBarrierVisual.LiveCount + " · Active=" + StageBarrier.Active); + + StageBarrier.HardResetForProbe(); + WLStageBarrierSettings.SetInstanceForProbe(real); + } + + // ═════════════════════════════════════════════════════════════════════════ + static void Sec9_Cost() + { + L(""); + L("⑩ 성능 표 (반경 14 m · 스타일별)"); + var c = WLStageBarrierSettings.Instance; + StageBarrier.HardResetForProbe(); + WLStageBarrierSettings.SetInstanceForProbe(c); + + L(" | 스타일 | 인스턴스 | 렌더러 | 머티리얼 | 삼각형 | 파티클시스템 | 정상상태 파티클 | 빌드 ms |"); + L(" |---|---|---|---|---|---|---|---|"); + var styles = new StageBarrierStyle[] { + StageBarrierStyle.MagicRing, + StageBarrierStyle.Props, + StageBarrierStyle.Fire, + }; + for (int i = 0; i < styles.Length; i++) + { + StageBarrierVisual.StyleOverride = styles[i]; + StageBarrier.Set(kAnchor, kRadius); + StageBarrierVisual.ForcePhase(1f); + int rend, mats, ps; long tri; + StageBarrierVisual.MeasureCost(out rend, out mats, out tri, out ps); + int est = styles[i] == StageBarrierStyle.Fire ? StageBarrierVisual.FireParticleEstimate + : (styles[i] == StageBarrierStyle.MagicRing ? StageBarrierVisual.LiveCount * 4 : 0); + L(" | " + styles[i] + " | " + StageBarrierVisual.LiveCount + " | " + rend + " | " + mats + + " | " + tri + " | " + ps + " | " + est + " | " + StageBarrierVisual.LastBuildMs.ToString("F1") + " |"); + } + StageBarrierVisual.StyleOverride = (StageBarrierStyle)(-1); + Chk("10-1 성능 표", true, "위 표 참조(에디트 모드 측정 · 실 드로우콜은 캡처의 UnityStats)"); + + // 반경별 개수 + L(" 반경별 프롭 개수(간격 " + c.propSpacing + " m · 상한 " + c.maxProps + ")"); + float[] rs = { 8f, 14f, 20f, 30f, 40f }; + StageBarrierVisual.StyleOverride = StageBarrierStyle.Props; + for (int i = 0; i < rs.Length; i++) + { + StageBarrier.Set(kAnchor, rs[i]); + L(" r " + rs[i] + " m(둘레 " + (2f * Mathf.PI * rs[i]).ToString("F1") + " m) → 프롭 " + + StageBarrierVisual.PropCount + " · 실간격 " + StageBarrierVisual.PropSpacingUsed.ToString("F2") + + " m · 카브 세그(on 일 때) " + StageBarrier.CarveSegmentCount(c, rs[i])); + } + StageBarrierVisual.StyleOverride = (StageBarrierStyle)(-1); + StageBarrier.HardResetForProbe(); + WLStageBarrierSettings.SetInstanceForProbe(c); + } + + /// + /// 에디트 모드 스캐폴딩 — Awake 가 넣었을 `Actor.m_NavMeshAgent`(비직렬화 protected)를 심는다. + /// 게임 코드에는 리플렉션이 0 이고, 이 프로브에서만 쓴다. + /// + static string SeedAgent(Actor a, GameObject go) + { + try + { + var agent = go.GetComponent(); + if (agent == null) agent = go.GetComponentInChildren(true); + if (agent == null) return "NavMeshAgent 없음"; + var fi = typeof(Actor).GetField("m_NavMeshAgent", + System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); + if (fi == null) return "필드 없음"; + fi.SetValue(a, agent); + return "seeded"; + } + catch (Exception ex) { return ex.Message; } + } + + // ═════════════════════════════════════════════════════════════════════════ + static WLStageBarrierSettings Clone(WLStageBarrierSettings src) + { + var t = UnityEngine.Object.Instantiate(src); + t.hideFlags = HideFlags.HideAndDontSave; + return t; + } +} + diff --git a/AgentScripts/WL815c_SURVEY.txt b/AgentScripts/WL815c_SURVEY.txt new file mode 100644 index 000000000..391d1faba --- /dev/null +++ b/AgentScripts/WL815c_SURVEY.txt @@ -0,0 +1,243 @@ +WL-815c 장벽 보이기 후보 실측 · 2026-09-11 00:34:25 + +── ⓐ 링 후보 (Hovl) ───────────────────────────────────────────── + Marker 3 Zone Loop + 경로 Assets/ResWork/VFX/Hovl Studio/Map track markers VFX/Prefabs/Marker 3 Zone Loop.prefab + 렌더러 4(메시 0 · 파티클 4) · 머티리얼 1 · 삼각형 0 + 바운즈: 없음(메시 0) + 파티클시스템 4 · maxParticles 합 4000 · rateOverTime 합 0.0/s · 최대수명 1.00 s · 정상상태 추정 0 · duration 1.00 · loop Y · shape radius 0.00 + Marker 5 Circle Loop + 경로 Assets/ResWork/VFX/Hovl Studio/Map track markers VFX/Prefabs/Marker 5 Circle Loop.prefab + 렌더러 4(메시 0 · 파티클 4) · 머티리얼 1 · 삼각형 0 + 바운즈: 없음(메시 0) + 파티클시스템 4 · maxParticles 합 4000 · rateOverTime 합 0.0/s · 최대수명 2.00 s · 정상상태 추정 0 · duration 1.00 · loop Y · shape radius 0.00 + Marker 7 Danger zone Loop + 경로 Assets/ResWork/VFX/Hovl Studio/Map track markers VFX/Prefabs/Marker 7 Danger zone Loop.prefab + 렌더러 2(메시 0 · 파티클 2) · 머티리얼 1 · 삼각형 0 + 바운즈: 없음(메시 0) + 파티클시스템 2 · maxParticles 합 2000 · rateOverTime 합 0.0/s · 최대수명 1.00 s · 정상상태 추정 0 · duration 1.00 · loop Y · shape radius 0.00 + Marker 8 Safe zone Loop + 경로 Assets/ResWork/VFX/Hovl Studio/Map track markers VFX/Prefabs/Marker 8 Safe zone Loop.prefab + 렌더러 2(메시 0 · 파티클 2) · 머티리얼 1 · 삼각형 0 + 바운즈: 없음(메시 0) + 파티클시스템 2 · maxParticles 합 2000 · rateOverTime 합 0.0/s · 최대수명 1.00 s · 정상상태 추정 0 · duration 1.00 · loop Y · shape radius 0.00 + Marker 4 Pillar Loop + 경로 Assets/ResWork/VFX/Hovl Studio/Map track markers VFX/Prefabs/Marker 4 Pillar Loop.prefab + 렌더러 4(메시 0 · 파티클 4) · 머티리얼 1 · 삼각형 0 + 바운즈: 없음(메시 0) + 파티클시스템 4 · maxParticles 합 4000 · rateOverTime 합 0.0/s · 최대수명 2.00 s · 정상상태 추정 0 · duration 1.00 · loop Y · shape radius 0.00 + Magic circle 1 loop + 경로 Assets/ResWork/VFX/Hovl Studio/Magic circles/Prefabs/Loop version/Magic circle 1 loop.prefab + 렌더러 11(메시 0 · 파티클 11) · 머티리얼 9 · 삼각형 0 + 바운즈: 없음(메시 0) + 파티클시스템 11 · maxParticles 합 11000 · rateOverTime 합 142.0/s · 최대수명 2.00 s · 정상상태 추정 284 · duration 1.00 · loop Y · shape radius 1.10 + Magic circle 3 loop + 경로 Assets/ResWork/VFX/Hovl Studio/Magic circles/Prefabs/Loop version/Magic circle 3 loop.prefab + 렌더러 7(메시 0 · 파티클 7) · 머티리얼 7 · 삼각형 0 + 바운즈: 없음(메시 0) + 파티클시스템 7 · maxParticles 합 7000 · rateOverTime 합 0.0/s · 최대수명 1.25 s · 정상상태 추정 0 · duration 1.00 · loop Y · shape radius 2.00 + Magic circle 5 loop + 경로 Assets/ResWork/VFX/Hovl Studio/Magic circles/Prefabs/Loop version/Magic circle 5 loop.prefab + 렌더러 7(메시 0 · 파티클 7) · 머티리얼 7 · 삼각형 0 + 바운즈: 없음(메시 0) + 파티클시스템 7 · maxParticles 합 7000 · rateOverTime 합 0.0/s · 최대수명 2.00 s · 정상상태 추정 0 · duration 1.00 · loop Y · shape radius 0.50 + +── ⓑ 프롭 후보 (울타리 · 바위 · 기둥) ─────────────────────────── + Fence_01 + 경로 Assets/ResWork/LowPolyFantasyVillage_URP/Prefabs/Props/Fence_01.prefab + 렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 120 + 바운즈 크기 (2.38, 1.02, 0.27) · 중심 (0.00, 0.50, 0.00) · 최대반경(XZ) 1.19 m + Fence_02 + 경로 Assets/ResWork/LowPolyFantasyVillage_URP/Prefabs/Props/Fence_02.prefab + 렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 100 + 바운즈 크기 (2.20, 1.02, 0.27) · 중심 (-0.96, 0.51, 0.00) · 최대반경(XZ) 1.10 m + FenceBig_01 + 경로 Assets/ResWork/LowPolyFantasyVillage_URP/Prefabs/Props/FenceBig_01.prefab + 렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 324 + 바운즈 크기 (2.26, 3.41, 0.32) · 중심 (0.01, 1.70, -0.01) · 최대반경(XZ) 1.13 m + FenceBig_02 + 경로 Assets/ResWork/LowPolyFantasyVillage_URP/Prefabs/Props/FenceBig_02.prefab + 렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 324 + 바운즈 크기 (2.26, 3.41, 0.32) · 중심 (0.01, 1.69, -0.01) · 최대반경(XZ) 1.13 m + FenceBig_01 + 경로 Assets/LowPolyFantasyArena/Prefabs/FenceBig_01.prefab + 렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 42 + 바운즈 크기 (0.48, 6.39, 0.51) · 중심 (-0.01, 3.19, 0.00) · 최대반경(XZ) 0.25 m + FenceBig_02 + 경로 Assets/LowPolyFantasyArena/Prefabs/FenceBig_02.prefab + 렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 20 + 바운즈 크기 (0.37, 4.81, 0.38) · 중심 (0.00, 2.40, 0.02) · 최대반경(XZ) 0.19 m + fence_1 + 경로 Assets/ResWork/Aligned Games/Polygonal Woodlands Nature Asset Package/Models/Fence1/fence_1.prefab + 렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 120 + 바운즈 크기 (5.01, 2.50, 0.45) · 중심 (-0.01, 1.25, -0.06) · 최대반경(XZ) 2.51 m + fence_2 + 경로 Assets/ResWork/Aligned Games/Polygonal Woodlands Nature Asset Package/Models/Fence2/fence_2.prefab + 렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 120 + 바운즈 크기 (5.01, 2.50, 0.45) · 중심 (0.00, 1.25, -0.06) · 최대반경(XZ) 2.50 m + Stone_1 + 경로 Assets/3DPixelArtEnvironment/Prefabs/Stone_1.prefab + 렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 36 + 바운즈 크기 (1.29, 1.62, 1.50) · 중심 (0.00, 0.00, 0.12) · 최대반경(XZ) 0.75 m + Stone_2 + 경로 Assets/3DPixelArtEnvironment/Prefabs/Stone_2.prefab + 렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 44 + 바운즈 크기 (2.42, 2.62, 2.64) · 중심 (-0.07, 0.04, 0.04) · 최대반경(XZ) 1.32 m + Slab + 경로 Assets/3DPixelArtEnvironment/Prefabs/Slab.prefab + 렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 80 + 바운즈 크기 (3.37, 0.91, 4.41) · 중심 (0.37, -0.15, 0.73) · 최대반경(XZ) 2.20 m + Arch_Small + 경로 Assets/3DPixelArtEnvironment/Prefabs/Arch_Small.prefab + 렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 261 + 바운즈 크기 (13.58, 14.70, 11.91) · 중심 (2.09, 7.36, -12.48) · 최대반경(XZ) 6.79 m + Light_Post + 경로 Assets/3DPixelArtEnvironment/Prefabs/Light_Post.prefab + 렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 68 + 바운즈 크기 (3.67, 18.24, 7.27) · 중심 (0.00, 8.12, -1.51) · 최대반경(XZ) 3.64 m + +── LMHPOLY 바위 (자연 번들) ──────────────────────────────────── + Cloud_01 + 경로 Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Bonus Assets/Prefabs/Clouds/Cloud_01.prefab + 렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 100 + 바운즈 크기 (18.31, 8.76, 8.70) · 중심 (0.64, 0.34, -0.45) · 최대반경(XZ) 9.16 m + Cloud_02 + 경로 Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Bonus Assets/Prefabs/Clouds/Cloud_02.prefab + 렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 100 + 바운즈 크기 (14.14, 6.68, 7.58) · 중심 (0.12, 0.44, -0.32) · 최대반경(XZ) 7.07 m + Cloud_03 + 경로 Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Bonus Assets/Prefabs/Clouds/Cloud_03.prefab + 렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 100 + 바운즈 크기 (31.30, 12.15, 11.05) · 중심 (-1.67, 1.71, -0.57) · 최대반경(XZ) 15.65 m + Ground_m_01 + 경로 Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Bonus Assets/Prefabs/Ground/Ground_m_01.prefab + 렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 3872 + 바운즈 크기 (100.00, 0.86, 100.00) · 중심 (-50.00, -0.37, 50.00) · 최대반경(XZ) 50.00 m + Ground_m_02 + 경로 Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Bonus Assets/Prefabs/Ground/Ground_m_02.prefab + 렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 3872 + 바운즈 크기 (100.00, 9.82, 100.00) · 중심 (-50.00, 2.67, 50.00) · 최대반경(XZ) 50.00 m + Ground_m_03 + 경로 Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Bonus Assets/Prefabs/Ground/Ground_m_03.prefab + 렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 3872 + 바운즈 크기 (100.00, 8.21, 100.00) · 중심 (-50.00, -2.01, 50.00) · 최대반경(XZ) 50.00 m + Ground_m_04 + 경로 Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Bonus Assets/Prefabs/Ground/Ground_m_04.prefab + 렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 3872 + 바운즈 크기 (100.00, 7.24, 100.00) · 중심 (-50.00, -3.57, 50.00) · 최대반경(XZ) 50.00 m + Ground_m_05 + 경로 Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Bonus Assets/Prefabs/Ground/Ground_m_05.prefab + 렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 3872 + 바운즈 크기 (100.00, 29.22, 100.00) · 중심 (-50.00, 14.58, 50.00) · 최대반경(XZ) 50.00 m + Ground_m_06 + 경로 Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Bonus Assets/Prefabs/Ground/Ground_m_06.prefab + 렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 3872 + 바운즈 크기 (100.00, 4.88, 100.00) · 중심 (-50.00, 2.28, 50.00) · 최대반경(XZ) 50.00 m + Ground_m_07 + 경로 Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Bonus Assets/Prefabs/Ground/Ground_m_07.prefab + 렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 3872 + 바운즈 크기 (100.00, 27.42, 100.00) · 중심 (-50.00, 9.70, 50.00) · 최대반경(XZ) 50.00 m + (Rocks 폴더 프리팹 총 1455개) + +── ⓒ 불꽃 후보 ──────────────────────────────────────────────── + Fire_Particles + 경로 Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Bonus Assets/Prefabs/ParticleSystem/Fire_Particles.prefab + 렌더러 3(메시 0 · 파티클 3) · 머티리얼 3 · 삼각형 0 + 바운즈: 없음(메시 0) + 파티클시스템 3 · maxParticles 합 3000 · rateOverTime 합 45.0/s · 최대수명 8.00 s · 정상상태 추정 360 · duration 5.00 · loop Y · shape radius 0.46 + Fire buff + 경로 Assets/ResWork/VFX/Hovl Studio/Epic Toon VFX 2/Prefabs/Fire buff/Fire buff.prefab + 렌더러 19(메시 0 · 파티클 19) · 머티리얼 12 · 삼각형 0 + 바운즈: 없음(메시 0) + 파티클시스템 19 · maxParticles 합 19000 · rateOverTime 합 0.0/s · 최대수명 2.20 s · 정상상태 추정 0 · duration 3.00 · loop Y · shape radius 5.00 + SK_Flame + 경로 Assets/ResWork/ModularDungeon/Prefabs/SK_Flame.prefab + 렌더러 1(메시 0 · 파티클 0) · 머티리얼 1 · 삼각형 0 + 바운즈 크기 (0.01, 0.03, 0.01) · 중심 (0.00, 0.01, 0.00) · 최대반경(XZ) 0.01 m + FX_Flameflow_floor + 경로 Assets/ResWork/EricWang/GameVFX Collection Vol3/Prefabs/FX_Flameflow_floor.prefab + 렌더러 14(메시 0 · 파티클 14) · 머티리얼 11 · 삼각형 0 + 바운즈: 없음(메시 0) + 파티클시스템 14 · maxParticles 합 11004 · rateOverTime 합 365.0/s · 최대수명 5.90 s · 정상상태 추정 2154 · duration 5.50 · loop Y · shape radius 6.60 + PS_Cast_Fire + 경로 Assets/ResWork/VFX/StylizedFantasyFxPack/Prefab/PS_Cast_Fire.prefab + 렌더러 6(메시 0 · 파티클 6) · 머티리얼 6 · 삼각형 0 + 바운즈: 없음(메시 0) + 파티클시스템 6 · maxParticles 합 7 · rateOverTime 합 0.0/s · 최대수명 5.00 s · 정상상태 추정 0 · duration 1.00 · loop N · shape radius 0.05 + Effect_Hit_Mob_HellfireFlames + 경로 Assets/Res_Addr/Effect/Effect_Hit_Mob_HellfireFlames.prefab + 렌더러 8(메시 0 · 파티클 8) · 머티리얼 6 · 삼각형 0 + 바운즈: 없음(메시 0) + 파티클시스템 8 · maxParticles 합 2013 · rateOverTime 합 115.0/s · 최대수명 0.70 s · 정상상태 추정 81 · duration 0.30 · loop N · shape radius 0.20 + +── 불꽃 추가 탐색(이름에 Fire/Flame · 파티클 보유 · 루프) ─────── + Slash Projectile VFX Fire + 경로 Assets/ResWork/VFX/Free Slash VFX/Prefabs/Projectiles/Slash Projectile VFX Fire.prefab + 렌더러 13(메시 0 · 파티클 13) · 머티리얼 8 · 삼각형 0 + 바운즈: 없음(메시 0) + 파티클시스템 13 · maxParticles 합 13000 · rateOverTime 합 910.0/s · 최대수명 2.00 s · 정상상태 추정 1820 · duration 1.00 · loop Y · shape radius 1.40 + vfx_Projectile_ArrowFire + 경로 Assets/ResWork/VFX/GabrielAguiarProductions/UniqueProjectilesVol_1/Prefabs/Projectiles/vfx_Projectile_ArrowFire.prefab + 렌더러 6(메시 0 · 파티클 5) · 머티리얼 3 · 삼각형 0 + 바운즈 크기 (0.15, 0.15, 0.15) · 중심 (0.00, 0.00, 0.40) · 최대반경(XZ) 0.08 m + 파티클시스템 5 · maxParticles 합 132 · rateOverTime 합 20.0/s · 최대수명 1.00 s · 정상상태 추정 20 · duration 1.00 · loop Y · shape radius 1.00 + vfx_Projectile_Fireball01Blue + 경로 Assets/ResWork/VFX/GabrielAguiarProductions/UniqueProjectilesVol_1/Prefabs/Projectiles/vfx_Projectile_Fireball01Blue.prefab + 렌더러 7(메시 0 · 파티클 6) · 머티리얼 5 · 삼각형 0 + 바운즈 크기 (1.00, 1.00, 1.00) · 중심 (0.00, 0.00, 0.00) · 최대반경(XZ) 0.50 m + 파티클시스템 6 · maxParticles 합 1019 · rateOverTime 합 51.0/s · 최대수명 1.00 s · 정상상태 추정 51 · duration 1.00 · loop Y · shape radius 1.00 + vfx_Projectile_Fireball01Orange + 경로 Assets/ResWork/VFX/GabrielAguiarProductions/UniqueProjectilesVol_1/Prefabs/Projectiles/vfx_Projectile_Fireball01Orange.prefab + 렌더러 7(메시 0 · 파티클 6) · 머티리얼 5 · 삼각형 0 + 바운즈 크기 (1.00, 1.00, 1.00) · 중심 (0.00, 0.00, 0.00) · 최대반경(XZ) 0.50 m + 파티클시스템 6 · maxParticles 합 1015 · rateOverTime 합 53.0/s · 최대수명 1.00 s · 정상상태 추정 53 · duration 1.00 · loop Y · shape radius 1.00 + vfx_Projectile_Fireball02Green + 경로 Assets/ResWork/VFX/GabrielAguiarProductions/UniqueProjectilesVol_1/Prefabs/Projectiles/vfx_Projectile_Fireball02Green.prefab + 렌더러 6(메시 0 · 파티클 5) · 머티리얼 3 · 삼각형 0 + 바운즈 크기 (2.00, 2.00, 2.00) · 중심 (0.00, 0.00, 0.60) · 최대반경(XZ) 1.00 m + 파티클시스템 5 · maxParticles 합 2008 · rateOverTime 합 80.0/s · 최대수명 1.00 s · 정상상태 추정 80 · duration 1.00 · loop Y · shape radius 0.70 + vfx_Projectile_Fireball02Orange + 경로 Assets/ResWork/VFX/GabrielAguiarProductions/UniqueProjectilesVol_1/Prefabs/Projectiles/vfx_Projectile_Fireball02Orange.prefab + 렌더러 6(메시 0 · 파티클 5) · 머티리얼 3 · 삼각형 0 + 바운즈 크기 (2.00, 2.00, 2.00) · 중심 (0.00, 0.00, 0.60) · 최대반경(XZ) 1.00 m + 파티클시스템 5 · maxParticles 합 2008 · rateOverTime 합 80.0/s · 최대수명 1.00 s · 정상상태 추정 80 · duration 1.00 · loop Y · shape radius 0.70 + vfx_Projectile_SwordFire + 경로 Assets/ResWork/VFX/GabrielAguiarProductions/UniqueProjectilesVol_1/Prefabs/Projectiles/vfx_Projectile_SwordFire.prefab + 렌더러 7(메시 0 · 파티클 6) · 머티리얼 3 · 삼각형 0 + 바운즈 크기 (0.20, 0.20, 0.20) · 중심 (0.00, 0.00, 0.90) · 최대반경(XZ) 0.10 m + 파티클시스템 6 · maxParticles 합 207 · rateOverTime 합 27.0/s · 최대수명 1.00 s · 정상상태 추정 27 · duration 1.00 · loop Y · shape radius 1.00 + vfx_Projectile_Fireball03_Blue + 경로 Assets/ResWork/VFX/GabrielAguiarProductions/UniqueProjectilesVol_2/Prefabs/Projectiles/vfx_Projectile_Fireball03_Blue.prefab + 렌더러 12(메시 0 · 파티클 11) · 머티리얼 6 · 삼각형 0 + 바운즈 크기 (3.00, 3.00, 3.00) · 중심 (0.00, 0.00, 0.00) · 최대반경(XZ) 1.50 m + 파티클시스템 11 · maxParticles 합 3135 · rateOverTime 합 150.0/s · 최대수명 1.00 s · 정상상태 추정 150 · duration 1.00 · loop Y · shape radius 1.00 + vfx_Projectile_Fireball03_Green + 경로 Assets/ResWork/VFX/GabrielAguiarProductions/UniqueProjectilesVol_2/Prefabs/Projectiles/vfx_Projectile_Fireball03_Green.prefab + 렌더러 12(메시 0 · 파티클 11) · 머티리얼 6 · 삼각형 0 + 바운즈 크기 (3.00, 3.00, 3.00) · 중심 (0.00, 0.00, 0.00) · 최대반경(XZ) 1.50 m + 파티클시스템 11 · maxParticles 합 3135 · rateOverTime 합 150.0/s · 최대수명 1.00 s · 정상상태 추정 150 · duration 1.00 · loop Y · shape radius 1.00 + vfx_Projectile_Fireball03_Orange + 경로 Assets/ResWork/VFX/GabrielAguiarProductions/UniqueProjectilesVol_2/Prefabs/Projectiles/vfx_Projectile_Fireball03_Orange.prefab + 렌더러 12(메시 0 · 파티클 11) · 머티리얼 6 · 삼각형 0 + 바운즈 크기 (3.00, 3.00, 3.00) · 중심 (0.00, 0.00, 0.00) · 최대반경(XZ) 1.50 m + 파티클시스템 11 · maxParticles 합 3132 · rateOverTime 합 150.0/s · 최대수명 1.00 s · 정상상태 추정 150 · duration 1.00 · loop Y · shape radius 1.00 + vfx_Projectile_Fireball04_Blue + 경로 Assets/ResWork/VFX/GabrielAguiarProductions/UniqueProjectilesVol_2/Prefabs/Projectiles/vfx_Projectile_Fireball04_Blue.prefab + 렌더러 11(메시 0 · 파티클 10) · 머티리얼 4 · 삼각형 0 + 바운즈 크기 (1.30, 1.30, 1.30) · 중심 (0.00, 0.00, 0.00) · 최대반경(XZ) 0.65 m + 파티클시스템 10 · maxParticles 합 3408 · rateOverTime 합 189.0/s · 최대수명 1.10 s · 정상상태 추정 208 · duration 1.00 · loop Y · shape radius 1.00 + vfx_Projectile_Fireball04_Green + 경로 Assets/ResWork/VFX/GabrielAguiarProductions/UniqueProjectilesVol_2/Prefabs/Projectiles/vfx_Projectile_Fireball04_Green.prefab + 렌더러 11(메시 0 · 파티클 10) · 머티리얼 4 · 삼각형 0 + 바운즈 크기 (1.30, 1.30, 1.30) · 중심 (0.00, 0.00, 0.00) · 최대반경(XZ) 0.65 m + 파티클시스템 10 · maxParticles 합 3408 · rateOverTime 합 189.0/s · 최대수명 1.10 s · 정상상태 추정 208 · duration 1.00 · loop Y · shape radius 1.00 + vfx_Projectile_Fireball04_Orange + 경로 Assets/ResWork/VFX/GabrielAguiarProductions/UniqueProjectilesVol_2/Prefabs/Projectiles/vfx_Projectile_Fireball04_Orange.prefab + 렌더러 11(메시 0 · 파티클 10) · 머티리얼 4 · 삼각형 0 + 바운즈 크기 (1.30, 1.30, 1.30) · 중심 (0.00, 0.00, 0.00) · 최대반경(XZ) 0.65 m + 파티클시스템 10 · maxParticles 합 3408 · rateOverTime 합 189.0/s · 최대수명 1.10 s · 정상상태 추정 208 · duration 1.00 · loop Y · shape radius 1.00 + vfx_Projectile_Fireball05_Blue + 경로 Assets/ResWork/VFX/GabrielAguiarProductions/UniqueProjectilesVol_2/Prefabs/Projectiles/vfx_Projectile_Fireball05_Blue.prefab + 렌더러 11(메시 0 · 파티클 10) · 머티리얼 4 · 삼각형 0 + 바운즈 크기 (2.45, 2.45, 2.45) · 중심 (0.00, 0.00, 0.00) · 최대반경(XZ) 1.23 m + 파티클시스템 10 · maxParticles 합 4283 · rateOverTime 합 175.0/s · 최대수명 1.00 s · 정상상태 추정 175 · duration 1.00 · loop Y · shape radius 1.00 diff --git a/AgentScripts/WL815c_SURVEY2.txt b/AgentScripts/WL815c_SURVEY2.txt new file mode 100644 index 000000000..ca8fd68b8 --- /dev/null +++ b/AgentScripts/WL815c_SURVEY2.txt @@ -0,0 +1,90 @@ +WL-815c 2차 실측 · 2026-09-11 00:35:30 + +── 링 프리팹: Simulate 2 s 후 실제 시각 반경(XZ) ────────────── + Marker 3 Zone Loop + Simulate(2 s) 살아있는 파티클 3 · 시각 반경(XZ) 1.10 m · 높이 0.00~0.01 m · 렌더러 바운즈 반경 2.13 m + ├ Marker 3 Zone Loop(Clone) : max 1000 · rate 0.0/s · 버스트 1종 총 1 · life 1.00 s · size 1.00 · shape off · scale (1.00, 1.00, 1.00) · 지금 1 + ├ Flash : max 1000 · rate 0.0/s · 버스트 1종 총 1 · life 1.00 s · size 2.00 · shape Hemisphere r0.00 · scale (1.00, 1.00, 1.00) · 지금 1 + ├ Runes : max 1000 · rate 0.0/s · 버스트 1종 총 1 · life 1.00 s · size 2.20 · shape Hemisphere r0.00 · scale (1.00, 1.00, 1.00) · 지금 1 + ├ ShockWave : max 1000 · rate 0.0/s · 버스트 1종 총 1 · life 0.30 s · size 3.00 · shape Hemisphere r0.00 · scale (1.00, 1.00, 1.00) · 지금 0 + Marker 5 Circle Loop + Simulate(2 s) 살아있는 파티클 8 · 시각 반경(XZ) 1.02 m · 높이 0.00~0.01 m · 렌더러 바운즈 반경 1.45 m + ├ Marker 5 Circle Loop(Clone) : max 1000 · rate 0.0/s · 버스트 1종 총 2 · life 1.00 s · size 1.00 · shape off · scale (1.00, 1.00, 1.00) · 지금 2 + ├ Flash : max 1000 · rate 0.0/s · 버스트 1종 총 1 · life 2.00 s · size 2.00 · shape Hemisphere r0.00 · scale (1.00, 1.00, 1.00) · 지금 2 + ├ Runes : max 1000 · rate 0.0/s · 버스트 1종 총 1 · life 2.00 s · size 2.04 · shape Hemisphere r0.00 · scale (1.00, 1.00, 1.00) · 지금 2 + ├ ShockWave : max 1000 · rate 0.0/s · 버스트 1종 총 2 · life 1.00 s · size 2.00 · shape Hemisphere r0.00 · scale (1.00, 1.00, 1.00) · 지금 2 + Marker 7 Danger zone Loop + Simulate(2 s) 살아있는 파티클 4 · 시각 반경(XZ) 2.50 m · 높이 0.00~0.01 m · 렌더러 바운즈 반경 3.55 m + ├ Marker 7 Danger zone Loop(Clone) : max 1000 · rate 0.0/s · 버스트 1종 총 2 · life 1.00 s · size 5.00 · shape Hemisphere r0.00 · scale (1.00, 1.00, 1.00) · 지금 2 + ├ ShockWave : max 1000 · rate 0.0/s · 버스트 1종 총 2 · life 1.00 s · size 5.00 · shape Hemisphere r0.00 · scale (1.00, 1.00, 1.00) · 지금 2 + Marker 8 Safe zone Loop + Simulate(2 s) 살아있는 파티클 4 · 시각 반경(XZ) 2.50 m · 높이 0.00~0.01 m · 렌더러 바운즈 반경 3.55 m + ├ Marker 8 Safe zone Loop(Clone) : max 1000 · rate 0.0/s · 버스트 1종 총 2 · life 1.00 s · size 5.00 · shape Hemisphere r0.00 · scale (1.00, 1.00, 1.00) · 지금 2 + ├ ShockWave : max 1000 · rate 0.0/s · 버스트 1종 총 2 · life 1.00 s · size 5.00 · shape Hemisphere r0.00 · scale (1.00, 1.00, 1.00) · 지금 2 + +── 불꽃 후보: 파티클 시스템별 상세 ──────────────────────────── + Fire_Particles · PS 3 + ├ FireRed : rate 20.0/s × life 1.50 s = 30 (max 1000) · size 0.50 · loop Y · 정지공간 Local + ├ FireYellow : rate 15.0/s × life 1.00 s = 15 (max 1000) · size 0.50 · loop Y · 정지공간 Local + ├ Smoke : rate 10.0/s × life 8.00 s = 80 (max 1000) · size 1.00 · loop Y · 정지공간 Local + ⇒ 인스턴스 1개 정상상태 추정 125 파티클 + Fire buff · PS 19 + ├ Fire buff(Clone) : rate 0.0/s × life 2.00 s = 0 (max 1000) · size 5.00 · loop Y · 정지공간 Local + ├ SparksStart : rate 0.0/s × life 0.15 s = 0 (max 1000) · size 0.10 · loop Y · 정지공간 Local + ├ SparksExpl : rate 0.0/s × life 0.50 s = 0 (max 1000) · size 0.30 · loop Y · 정지공간 Local + ├ Sparks : rate 0.0/s × life 0.50 s = 0 (max 1000) · size 0.20 · loop Y · 정지공간 Local + ├ SparksEnd : rate 0.0/s × life 0.40 s = 0 (max 1000) · size 0.20 · loop Y · 정지공간 Local + ├ StartRays : rate 0.0/s × life 0.20 s = 0 (max 1000) · size 0.40 · loop Y · 정지공간 Local + ├ StartFlash : rate 0.0/s × life 0.15 s = 0 (max 1000) · size 8.00 · loop Y · 정지공간 Local + ├ StartShockWave : rate 0.0/s × life 0.15 s = 0 (max 1000) · size 8.00 · loop Y · 정지공간 Local + ├ ShockWave : rate 0.0/s × life 0.14 s = 0 (max 1000) · size 14.00 · loop Y · 정지공간 Local + ├ ShockWaves : rate 0.0/s × life 0.35 s = 0 (max 1000) · size 14.00 · loop Y · 정지공간 Local + ├ GroundWaves : rate 0.0/s × life 0.50 s = 0 (max 1000) · size 4.00 · loop Y · 정지공간 Local + ├ GroundWaves2 : rate 0.0/s × life 0.60 s = 0 (max 1000) · size 3.50 · loop Y · 정지공간 Local + ├ MainTrails : rate 0.0/s × life 1.90 s = 0 (max 1000) · size 1.00 · loop Y · 정지공간 Local + ├ Ray : rate 0.0/s × life 2.00 s = 0 (max 1000) · size 1.00 · loop Y · 정지공간 Local + ├ Twist : rate 0.0/s × life 2.00 s = 0 (max 1000) · size 0.65 · loop Y · 정지공간 Local + ├ FireUp : rate 0.0/s × life 0.50 s = 0 (max 1000) · size 2.00 · loop Y · 정지공간 Local + ├ FireCore : rate 0.0/s × life 2.20 s = 0 (max 1000) · size 1.40 · loop Y · 정지공간 Local + ├ EndSmoke : rate 0.0/s × life 0.50 s = 0 (max 1000) · size 1.50 · loop Y · 정지공간 Local + ├ Light : rate 0.0/s × life 2.00 s = 0 (max 1000) · size 1.00 · loop Y · 정지공간 Local + ⇒ 인스턴스 1개 정상상태 추정 0 파티클 + FX_Flameflow_floor · PS 14 + ├ FX_Flameflow_floor(Clone) : rate 10.0/s × life 5.00 s = 50 (max 1000) · size 1.00 · loop Y · 정지공간 Local + ├ Pos : rate 0.0/s × life 0.05 s = 0 (max 1000) · size 0.37 · loop N · 정지공간 Local + ├ glow_b : rate 10.0/s × life 5.90 s = 59 (max 1000) · size 26.00 · loop N · 정지공간 Local + ├ fire : rate 0.0/s × life 5.80 s = 0 (max 1000) · size 9.00 · loop N · 정지공간 Local + ├ glow : rate 0.0/s × life 5.90 s = 0 (max 1000) · size 18.00 · loop N · 정지공간 Local + ├ Rune : rate 100.0/s × life 5.80 s = 1 (max 1) · size 23.00 · loop N · 정지공간 Local + ├ Dust : rate 15.0/s × life 2.30 s = 35 (max 1000) · size 5.00 · loop N · 정지공간 World + ├ bao : rate 100.0/s × life 0.30 s = 2 (max 2) · size 22.00 · loop N · 정지공간 Local + ├ bao1 : rate 0.0/s × life 0.60 s = 0 (max 1000) · size 13.00 · loop N · 정지공간 Local + ├ bao2 : rate 10.0/s × life 0.50 s = 1 (max 1) · size 7.00 · loop N · 정지공간 Local + ├ Gradient_ap : rate 0.0/s × life 0.60 s = 0 (max 1000) · size 27.00 · loop N · 정지공간 Local + ├ startwave_R : rate 0.0/s × life 5.80 s = 0 (max 1000) · size 12.00 · loop N · 정지공간 Local + ├ startwave_Y : rate 0.0/s × life 5.80 s = 0 (max 1000) · size 12.00 · loop N · 정지공간 Local + ├ dust_ring : rate 120.0/s × life 2.30 s = 276 (max 1000) · size 4.00 · loop N · 정지공간 World + ⇒ 인스턴스 1개 정상상태 추정 424 파티클 + +── LMHPOLY 바위(비 LOD · Rock 계열) ─────────────────────────── + Rock_Block_crk_m_01 · Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Rock Assets/Prefabs/With_Box_Colliders/1 Color/Block_Rocks/Rock_Block_crk_m_01.prefab + 크기 (1.49, 0.74, 1.47) · tri 150 · 렌더러 1 + Rock_Block_crk_m_02 · Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Rock Assets/Prefabs/With_Box_Colliders/1 Color/Block_Rocks/Rock_Block_crk_m_02.prefab + 크기 (1.33, 0.86, 1.40) · tri 134 · 렌더러 1 + Rock_Block_crk_m_03 · Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Rock Assets/Prefabs/With_Box_Colliders/1 Color/Block_Rocks/Rock_Block_crk_m_03.prefab + 크기 (1.94, 0.78, 0.92) · tri 62 · 렌더러 1 + Rock_Block_crk_m_04 · Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Rock Assets/Prefabs/With_Box_Colliders/1 Color/Block_Rocks/Rock_Block_crk_m_04.prefab + 크기 (1.26, 0.70, 1.31) · tri 152 · 렌더러 1 + Rock_Block_crk_m_05 · Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Rock Assets/Prefabs/With_Box_Colliders/1 Color/Block_Rocks/Rock_Block_crk_m_05.prefab + 크기 (1.48, 0.70, 0.79) · tri 42 · 렌더러 1 + Rock_Block_m_01 · Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Rock Assets/Prefabs/With_Box_Colliders/1 Color/Block_Rocks/Rock_Block_m_01.prefab + 크기 (1.07, 0.58, 0.58) · tri 12 · 렌더러 1 + Rock_Block_m_02 · Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Rock Assets/Prefabs/With_Box_Colliders/1 Color/Block_Rocks/Rock_Block_m_02.prefab + 크기 (1.13, 0.63, 0.63) · tri 44 · 렌더러 1 + Rock_Block_m_03 · Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Rock Assets/Prefabs/With_Box_Colliders/1 Color/Block_Rocks/Rock_Block_m_03.prefab + 크기 (1.20, 0.60, 0.64) · tri 44 · 렌더러 1 + +── 울타리 프롭 pivot·회전 확인(둘레 배치 적합성) ─────────────── + Fence_01 : pivot→바운즈중심 (0.00, 0.50, 0.00) · 크기 (2.38, 1.02, 0.27) · 긴축 X 2.38 · 바닥 y -0.01 + FenceBig_01 : pivot→바운즈중심 (0.01, 1.70, -0.01) · 크기 (2.26, 3.41, 0.32) · 긴축 X 2.26 · 바닥 y -0.01 + Stone_1 : pivot→바운즈중심 (0.00, 0.00, 0.12) · 크기 (1.29, 1.62, 1.50) · 긴축 Z 1.50 · 바닥 y -0.81 diff --git a/AgentScripts/WL815c_Survey.cs b/AgentScripts/WL815c_Survey.cs new file mode 100644 index 000000000..092103100 --- /dev/null +++ b/AgentScripts/WL815c_Survey.cs @@ -0,0 +1,164 @@ +// WL-815c 보유 에셋 실측 — 장벽 「보이기」 후보 (임시 · 커밋 대상) +// unity command run_script --file AgentScripts/WL815c_Survey.cs --entry WL815c_Survey.Run --project-path E:\NerdNavis\WL_wt\gameplay2 + +public static class WL815c_Survey +{ + static System.Text.StringBuilder s_sb; + + static void L(string s) { s_sb.AppendLine(s); UnityEngine.Debug.Log(s); } + + public static string Run() + { + s_sb = new System.Text.StringBuilder(); + L("WL-815c 장벽 보이기 후보 실측 · " + System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + + L(""); + L("── ⓐ 링 후보 (Hovl) ─────────────────────────────────────────────"); + string[] rings = { + "Assets/ResWork/VFX/Hovl Studio/Map track markers VFX/Prefabs/Marker 3 Zone Loop.prefab", + "Assets/ResWork/VFX/Hovl Studio/Map track markers VFX/Prefabs/Marker 5 Circle Loop.prefab", + "Assets/ResWork/VFX/Hovl Studio/Map track markers VFX/Prefabs/Marker 7 Danger zone Loop.prefab", + "Assets/ResWork/VFX/Hovl Studio/Map track markers VFX/Prefabs/Marker 8 Safe zone Loop.prefab", + "Assets/ResWork/VFX/Hovl Studio/Map track markers VFX/Prefabs/Marker 4 Pillar Loop.prefab", + "Assets/ResWork/VFX/Hovl Studio/Magic circles/Prefabs/Loop version/Magic circle 1 loop.prefab", + "Assets/ResWork/VFX/Hovl Studio/Magic circles/Prefabs/Loop version/Magic circle 3 loop.prefab", + "Assets/ResWork/VFX/Hovl Studio/Magic circles/Prefabs/Loop version/Magic circle 5 loop.prefab", + }; + for (int i = 0; i < rings.Length; i++) Measure(rings[i]); + + L(""); + L("── ⓑ 프롭 후보 (울타리 · 바위 · 기둥) ───────────────────────────"); + string[] props = { + "Assets/ResWork/LowPolyFantasyVillage_URP/Prefabs/Props/Fence_01.prefab", + "Assets/ResWork/LowPolyFantasyVillage_URP/Prefabs/Props/Fence_02.prefab", + "Assets/ResWork/LowPolyFantasyVillage_URP/Prefabs/Props/FenceBig_01.prefab", + "Assets/ResWork/LowPolyFantasyVillage_URP/Prefabs/Props/FenceBig_02.prefab", + "Assets/LowPolyFantasyArena/Prefabs/FenceBig_01.prefab", + "Assets/LowPolyFantasyArena/Prefabs/FenceBig_02.prefab", + "Assets/ResWork/Aligned Games/Polygonal Woodlands Nature Asset Package/Models/Fence1/fence_1.prefab", + "Assets/ResWork/Aligned Games/Polygonal Woodlands Nature Asset Package/Models/Fence2/fence_2.prefab", + "Assets/3DPixelArtEnvironment/Prefabs/Stone_1.prefab", + "Assets/3DPixelArtEnvironment/Prefabs/Stone_2.prefab", + "Assets/3DPixelArtEnvironment/Prefabs/Slab.prefab", + "Assets/3DPixelArtEnvironment/Prefabs/Arch_Small.prefab", + "Assets/3DPixelArtEnvironment/Prefabs/Light_Post.prefab", + }; + for (int i = 0; i < props.Length; i++) Measure(props[i]); + + L(""); + L("── LMHPOLY 바위 (자연 번들) ────────────────────────────────────"); + var rockGuids = UnityEditor.AssetDatabase.FindAssets("t:Prefab", new string[] { "Assets/LMHPOLY/Low Poly Nature Bundle/Rocks" }); + int shown = 0; + for (int i = 0; i < rockGuids.Length && shown < 10; i++) + { + string p = UnityEditor.AssetDatabase.GUIDToAssetPath(rockGuids[i]); + if (p.Contains("/LOD/") || p.Contains("Fireplace") || p.Contains("ParticleSystem")) continue; + if (!p.Contains("Rock")) continue; + Measure(p); shown++; + } + L(" (Rocks 폴더 프리팹 총 " + rockGuids.Length + "개)"); + + L(""); + L("── ⓒ 불꽃 후보 ────────────────────────────────────────────────"); + string[] fires = { + "Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Bonus Assets/Prefabs/ParticleSystem/Fire_Particles.prefab", + "Assets/ResWork/VFX/Hovl Studio/Epic Toon VFX 2/Prefabs/Fire buff/Fire buff.prefab", + "Assets/ResWork/ModularDungeon/Prefabs/SK_Flame.prefab", + "Assets/ResWork/EricWang/GameVFX Collection Vol3/Prefabs/FX_Flameflow_floor.prefab", + "Assets/ResWork/VFX/StylizedFantasyFxPack/Prefab/PS_Cast_Fire.prefab", + "Assets/Res_Addr/Effect/Effect_Hit_Mob_HellfireFlames.prefab", + }; + for (int i = 0; i < fires.Length; i++) Measure(fires[i]); + + L(""); + L("── 불꽃 추가 탐색(이름에 Fire/Flame · 파티클 보유 · 루프) ───────"); + var guids = UnityEditor.AssetDatabase.FindAssets("t:Prefab", new string[] { "Assets/ResWork/VFX" }); + int hits = 0; + for (int i = 0; i < guids.Length && hits < 14; i++) + { + string p = UnityEditor.AssetDatabase.GUIDToAssetPath(guids[i]); + string n = System.IO.Path.GetFileNameWithoutExtension(p).ToLowerInvariant(); + if (!(n.Contains("fire") || n.Contains("flame") || n.Contains("torch") || n.Contains("burn"))) continue; + var go = UnityEditor.AssetDatabase.LoadAssetAtPath(p); + if (go == null) continue; + var pss = go.GetComponentsInChildren(true); + if (pss.Length == 0) continue; + bool loop = false; + for (int k = 0; k < pss.Length; k++) if (pss[k].main.loop) loop = true; + if (!loop) continue; + Measure(p); hits++; + } + + string outFile = "AgentScripts/WL815c_SURVEY.txt"; + System.IO.File.WriteAllText(outFile, s_sb.ToString()); + return "survey done → " + outFile; + } + + static void Measure(string path) + { + var go = UnityEditor.AssetDatabase.LoadAssetAtPath(path); + if (go == null) { L(" [없음] " + path); return; } + + var inst = UnityEngine.Object.Instantiate(go); + inst.transform.position = UnityEngine.Vector3.zero; + inst.transform.rotation = UnityEngine.Quaternion.identity; + inst.transform.localScale = UnityEngine.Vector3.one; + + var rends = inst.GetComponentsInChildren(true); + bool has = false; + UnityEngine.Bounds b = new UnityEngine.Bounds(UnityEngine.Vector3.zero, UnityEngine.Vector3.zero); + int meshRend = 0, partRend = 0; + long tris = 0; + for (int i = 0; i < rends.Length; i++) + { + var mf = rends[i].GetComponent(); + if (mf != null && mf.sharedMesh != null) + { + meshRend++; + tris += mf.sharedMesh.triangles.Length / 3; + if (!has) { b = rends[i].bounds; has = true; } else b.Encapsulate(rends[i].bounds); + } + else if (rends[i] is UnityEngine.ParticleSystemRenderer) partRend++; + else if (!has) { b = rends[i].bounds; has = true; } else b.Encapsulate(rends[i].bounds); + } + + var pss = inst.GetComponentsInChildren(true); + int maxParticles = 0; float rate = 0f, shapeR = 0f, dur = 0f; bool loop = false; + for (int i = 0; i < pss.Length; i++) + { + var m = pss[i].main; + maxParticles += m.maxParticles; + dur = UnityEngine.Mathf.Max(dur, m.duration); + if (m.loop) loop = true; + var e = pss[i].emission; + if (e.enabled) rate += e.rateOverTime.constantMax; + var sh = pss[i].shape; + if (sh.enabled) shapeR = UnityEngine.Mathf.Max(shapeR, sh.radius * pss[i].transform.lossyScale.x); + } + + // 실 파티클 수 추정 = maxParticles 상한, 실효 = rate × lifetime + float lifeSum = 0f; + for (int i = 0; i < pss.Length; i++) lifeSum = UnityEngine.Mathf.Max(lifeSum, pss[i].main.startLifetime.constantMax); + + int mats = 0; + var seen = new System.Collections.Generic.HashSet(); + for (int i = 0; i < rends.Length; i++) + { + var ms = rends[i].sharedMaterials; + for (int k = 0; k < ms.Length; k++) if (ms[k] != null && seen.Add(ms[k].GetInstanceID())) mats++; + } + + L(" " + System.IO.Path.GetFileNameWithoutExtension(path)); + L(" 경로 " + path); + L(" 렌더러 " + rends.Length + "(메시 " + meshRend + " · 파티클 " + partRend + ") · 머티리얼 " + mats + " · 삼각형 " + tris); + if (has) L(" 바운즈 크기 " + b.size.ToString("F2") + " · 중심 " + b.center.ToString("F2") + " · 최대반경(XZ) " + + (UnityEngine.Mathf.Max(b.extents.x, b.extents.z)).ToString("F2") + " m"); + else L(" 바운즈: 없음(메시 0)"); + if (pss.Length > 0) + L(" 파티클시스템 " + pss.Length + " · maxParticles 합 " + maxParticles + " · rateOverTime 합 " + rate.ToString("F1") + + "/s · 최대수명 " + lifeSum.ToString("F2") + " s · 정상상태 추정 " + (rate * lifeSum).ToString("F0") + + " · duration " + dur.ToString("F2") + " · loop " + (loop ? "Y" : "N") + " · shape radius " + shapeR.ToString("F2")); + + UnityEngine.Object.DestroyImmediate(inst); + } +} diff --git a/AgentScripts/WL815c_Survey2.cs b/AgentScripts/WL815c_Survey2.cs new file mode 100644 index 000000000..56449025b --- /dev/null +++ b/AgentScripts/WL815c_Survey2.cs @@ -0,0 +1,186 @@ +// WL-815c 2차 실측 — 링 실제 시각 반경(Simulate 후 바운즈) · 불꽃 파티클 시스템별 상세 · 바위 프롭 +// unity command run_script --file AgentScripts/WL815c_Survey2.cs --entry WL815c_Survey2.Run + +public static class WL815c_Survey2 +{ + static System.Text.StringBuilder s_sb; + static void L(string s) { s_sb.AppendLine(s); UnityEngine.Debug.Log(s); } + + public static string Run() + { + s_sb = new System.Text.StringBuilder(); + L("WL-815c 2차 실측 · " + System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + + L(""); + L("── 링 프리팹: Simulate 2 s 후 실제 시각 반경(XZ) ──────────────"); + string[] rings = { + "Assets/ResWork/VFX/Hovl Studio/Map track markers VFX/Prefabs/Marker 3 Zone Loop.prefab", + "Assets/ResWork/VFX/Hovl Studio/Map track markers VFX/Prefabs/Marker 5 Circle Loop.prefab", + "Assets/ResWork/VFX/Hovl Studio/Map track markers VFX/Prefabs/Marker 7 Danger zone Loop.prefab", + "Assets/ResWork/VFX/Hovl Studio/Map track markers VFX/Prefabs/Marker 8 Safe zone Loop.prefab", + }; + for (int i = 0; i < rings.Length; i++) SimRadius(rings[i]); + + L(""); + L("── 불꽃 후보: 파티클 시스템별 상세 ────────────────────────────"); + string[] fires = { + "Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Bonus Assets/Prefabs/ParticleSystem/Fire_Particles.prefab", + "Assets/ResWork/VFX/Hovl Studio/Epic Toon VFX 2/Prefabs/Fire buff/Fire buff.prefab", + "Assets/ResWork/EricWang/GameVFX Collection Vol3/Prefabs/FX_Flameflow_floor.prefab", + }; + for (int i = 0; i < fires.Length; i++) Detail(fires[i]); + + L(""); + L("── LMHPOLY 바위(비 LOD · Rock 계열) ───────────────────────────"); + var guids = UnityEditor.AssetDatabase.FindAssets("t:Prefab", new string[] { "Assets/LMHPOLY/Low Poly Nature Bundle/Rocks" }); + int shown = 0; + for (int i = 0; i < guids.Length && shown < 8; i++) + { + string p = UnityEditor.AssetDatabase.GUIDToAssetPath(guids[i]); + string n = System.IO.Path.GetFileNameWithoutExtension(p); + if (p.Contains("/LOD/") || n.EndsWith("_LOD")) continue; + if (!(n.StartsWith("Rock") || n.StartsWith("Stone") || n.StartsWith("Boulder"))) continue; + Brief(p); shown++; + } + if (shown == 0) + { + L(" Rock/Stone/Boulder 접두 프리팹 0 — 폴더 구조 표본:"); + for (int i = 0; i < guids.Length && i < 12; i++) + L(" " + UnityEditor.AssetDatabase.GUIDToAssetPath(guids[i])); + } + + L(""); + L("── 울타리 프롭 pivot·회전 확인(둘레 배치 적합성) ───────────────"); + string[] fences = { + "Assets/ResWork/LowPolyFantasyVillage_URP/Prefabs/Props/Fence_01.prefab", + "Assets/ResWork/LowPolyFantasyVillage_URP/Prefabs/Props/FenceBig_01.prefab", + "Assets/3DPixelArtEnvironment/Prefabs/Stone_1.prefab", + }; + for (int i = 0; i < fences.Length; i++) Pivot(fences[i]); + + string outFile = "AgentScripts/WL815c_SURVEY2.txt"; + System.IO.File.WriteAllText(outFile, s_sb.ToString()); + return "survey2 done → " + outFile; + } + + static void SimRadius(string path) + { + var go = UnityEditor.AssetDatabase.LoadAssetAtPath(path); + if (go == null) { L(" [없음] " + path); return; } + var inst = UnityEngine.Object.Instantiate(go); + inst.transform.position = UnityEngine.Vector3.zero; + + var pss = inst.GetComponentsInChildren(true); + for (int i = 0; i < pss.Length; i++) { pss[i].Clear(true); pss[i].Simulate(2.0f, true, true, false); } + + float maxR = 0f, maxY = 0f, minY = 0f; + int alive = 0; + for (int i = 0; i < pss.Length; i++) + { + int c = pss[i].particleCount; + alive += c; + if (c == 0) continue; + var buf = new UnityEngine.ParticleSystem.Particle[c]; + int got = pss[i].GetParticles(buf); + bool world = pss[i].main.simulationSpace == UnityEngine.ParticleSystemSimulationSpace.World; + for (int k = 0; k < got; k++) + { + var p = buf[k].position; + if (!world) p = pss[i].transform.TransformPoint(p); + float r = new UnityEngine.Vector2(p.x, p.z).magnitude; + float sz = buf[k].GetCurrentSize(pss[i]) * 0.5f; + if (r + sz > maxR) maxR = r + sz; + if (p.y > maxY) maxY = p.y; + if (p.y < minY) minY = p.y; + } + } + // 파티클이 0이면 메시/빌보드 크기로 대신 잰다 + float rendR = 0f; + var rends = inst.GetComponentsInChildren(true); + for (int i = 0; i < rends.Length; i++) + { + var b = rends[i].bounds; + rendR = UnityEngine.Mathf.Max(rendR, UnityEngine.Mathf.Max(b.extents.x, b.extents.z)); + } + + L(" " + System.IO.Path.GetFileNameWithoutExtension(path)); + L(" Simulate(2 s) 살아있는 파티클 " + alive + " · 시각 반경(XZ) " + maxR.ToString("F2") + + " m · 높이 " + minY.ToString("F2") + "~" + maxY.ToString("F2") + " m · 렌더러 바운즈 반경 " + rendR.ToString("F2") + " m"); + for (int i = 0; i < pss.Length; i++) + { + var m = pss[i].main; + var sh = pss[i].shape; + var e = pss[i].emission; + string bursts = ""; + if (e.burstCount > 0) + { + var arr = new UnityEngine.ParticleSystem.Burst[e.burstCount]; + e.GetBursts(arr); + float tot = 0f; for (int k = 0; k < arr.Length; k++) tot += arr[k].count.constantMax * UnityEngine.Mathf.Max(1, arr[k].cycleCount); + bursts = " · 버스트 " + e.burstCount + "종 총 " + tot.ToString("F0"); + } + L(" ├ " + pss[i].name + " : max " + m.maxParticles + " · rate " + (e.enabled ? e.rateOverTime.constantMax : 0f).ToString("F1") + + "/s" + bursts + " · life " + m.startLifetime.constantMax.ToString("F2") + " s · size " + m.startSize.constantMax.ToString("F2") + + " · shape " + (sh.enabled ? sh.shapeType.ToString() + " r" + sh.radius.ToString("F2") : "off") + + " · scale " + pss[i].transform.lossyScale.ToString("F2") + " · 지금 " + pss[i].particleCount); + } + UnityEngine.Object.DestroyImmediate(inst); + } + + static void Detail(string path) + { + var go = UnityEditor.AssetDatabase.LoadAssetAtPath(path); + if (go == null) { L(" [없음] " + path); return; } + var inst = UnityEngine.Object.Instantiate(go); + var pss = inst.GetComponentsInChildren(true); + float steady = 0f; + L(" " + System.IO.Path.GetFileNameWithoutExtension(path) + " · PS " + pss.Length); + for (int i = 0; i < pss.Length; i++) + { + var m = pss[i].main; var e = pss[i].emission; + float r = e.enabled ? e.rateOverTime.constantMax : 0f; + float life = m.startLifetime.constantMax; + float st = UnityEngine.Mathf.Min(r * life, m.maxParticles); + steady += st; + L(" ├ " + pss[i].name + " : rate " + r.ToString("F1") + "/s × life " + life.ToString("F2") + " s = " + st.ToString("F0") + + " (max " + m.maxParticles + ") · size " + m.startSize.constantMax.ToString("F2") + " · loop " + (m.loop ? "Y" : "N") + + " · 정지공간 " + m.simulationSpace); + } + L(" ⇒ 인스턴스 1개 정상상태 추정 " + steady.ToString("F0") + " 파티클"); + UnityEngine.Object.DestroyImmediate(inst); + } + + static void Brief(string path) + { + var go = UnityEditor.AssetDatabase.LoadAssetAtPath(path); + if (go == null) return; + var inst = UnityEngine.Object.Instantiate(go); + var rends = inst.GetComponentsInChildren(true); + long tris = 0; bool has = false; UnityEngine.Bounds b = default(UnityEngine.Bounds); + for (int i = 0; i < rends.Length; i++) + { + var mf = rends[i].GetComponent(); + if (mf != null && mf.sharedMesh != null) tris += mf.sharedMesh.triangles.Length / 3; + if (!has) { b = rends[i].bounds; has = true; } else b.Encapsulate(rends[i].bounds); + } + L(" " + System.IO.Path.GetFileNameWithoutExtension(path) + " · " + path); + L(" 크기 " + b.size.ToString("F2") + " · tri " + tris + " · 렌더러 " + rends.Length); + UnityEngine.Object.DestroyImmediate(inst); + } + + static void Pivot(string path) + { + var go = UnityEditor.AssetDatabase.LoadAssetAtPath(path); + if (go == null) return; + var inst = UnityEngine.Object.Instantiate(go); + inst.transform.position = UnityEngine.Vector3.zero; + inst.transform.rotation = UnityEngine.Quaternion.identity; + var rends = inst.GetComponentsInChildren(true); + bool has = false; UnityEngine.Bounds b = default(UnityEngine.Bounds); + for (int i = 0; i < rends.Length; i++) { if (!has) { b = rends[i].bounds; has = true; } else b.Encapsulate(rends[i].bounds); } + L(" " + System.IO.Path.GetFileNameWithoutExtension(path) + " : pivot→바운즈중심 " + b.center.ToString("F2") + + " · 크기 " + b.size.ToString("F2") + " · 긴축 " + (b.size.x >= b.size.z ? "X " + b.size.x.ToString("F2") : "Z " + b.size.z.ToString("F2")) + + " · 바닥 y " + (b.center.y - b.extents.y).ToString("F2")); + UnityEngine.Object.DestroyImmediate(inst); + } +} diff --git a/Assets/WL/Combat/Stage.meta b/Assets/WL/Combat/Stage.meta new file mode 100644 index 000000000..253edc831 --- /dev/null +++ b/Assets/WL/Combat/Stage.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 58ad4c9f64381564b887b17b3ca5b314 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/WL/Combat/Stage/Barrier.meta b/Assets/WL/Combat/Stage/Barrier.meta new file mode 100644 index 000000000..748945f84 --- /dev/null +++ b/Assets/WL/Combat/Stage/Barrier.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b64e1a9aad572f04a8a23c913bd5dfc7 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/WL/Combat/Stage/Barrier/Resources.meta b/Assets/WL/Combat/Stage/Barrier/Resources.meta new file mode 100644 index 000000000..beef88097 --- /dev/null +++ b/Assets/WL/Combat/Stage/Barrier/Resources.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b4d10d4d325b7ac41a2aa48028290759 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/WL/Combat/Stage/Barrier/Resources/WL.meta b/Assets/WL/Combat/Stage/Barrier/Resources/WL.meta new file mode 100644 index 000000000..c26683841 --- /dev/null +++ b/Assets/WL/Combat/Stage/Barrier/Resources/WL.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 327b74d22ec69414ca28e7b3fd4c8d1a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/WL/Combat/Stage/Barrier/Resources/WL/WLStageBarrierSettings.asset b/Assets/WL/Combat/Stage/Barrier/Resources/WL/WLStageBarrierSettings.asset new file mode 100644 index 000000000..4e32f566a --- /dev/null +++ b/Assets/WL/Combat/Stage/Barrier/Resources/WL/WLStageBarrierSettings.asset @@ -0,0 +1,77 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 3c3dc9bd8de148744b88ce512ce0f09a, type: 3} + m_Name: WLStageBarrierSettings + m_EditorClassIdentifier: Assembly-CSharp::WL.Combat.Stage.WLStageBarrierSettings + enabled: 1 + verboseLog: 0 + checkSeconds: 0.2 + rescanSeconds: 3 + blockEnabled: 1 + leaveMargin: 2 + graceSeconds: 0.5 + warpCooldownSeconds: 1 + warpInsetMeters: 1.5 + warpNavSampleRadius: 4 + warpNavSampleRadiusFar: 20 + blockPlayer: 1 + blockMobs: 1 + mobLeaveMargin: 3 + mobGraceSeconds: 1 + mobWarpCooldownSeconds: 2 + warpSnapsCameraLead: 1 + warpResetsPath: 1 + carveNavMesh: 0 + carveSegmentArcMeters: 4 + maxCarveSegments: 32 + carveThickness: 1.5 + carveHeight: 3 + carveOverlap: 0.15 + carveStaticOnly: 1 + style: 1 + showVisual: 1 + appearSeconds: 0.6 + fadeSeconds: 0.6 + visualYOffset: 0.05 + visualRadiusScale: 1 + ringPrefab: {fileID: 1370640339480484, guid: a1b9488b1b1338d4585b154f4b86703e, type: 3} + ringPrefabVisualRadius: 1.1 + ringColor: {r: 1, g: 0.35, b: 0.15, a: 1} + ringTintEnabled: 1 + ringYScale: 1 + ringExtraRings: 1 + ringExtraSpacing: 0.6 + propPrefabs: + - {fileID: 3287405711304420922, guid: 84026706d57fb9e43b9716c8b1510e98, type: 3} + - {fileID: 3287405711304420922, guid: 84026706d57fb9e43b9716c8b1510e98, type: 3} + - {fileID: 118804, guid: 3e0cf4dec1c9ddb4b9832582972317f6, type: 3} + propSpacing: 2.5 + maxProps: 64 + propScale: 1 + propAlignToTangent: 1 + propYawJitter: 6 + propRadialJitter: 0.2 + propGroundSnap: 1 + propRayUp: 50 + propRayLength: 200 + propStripColliders: 1 + propSeed: 815 + firePrefabs: + - {fileID: 131402, guid: 5608cd25f71830e4aaa770d88ff9ebe2, type: 3} + fireSpacing: 3 + maxFires: 48 + fireScale: 1 + maxTotalParticles: 1200 + fireBudgetMode: 0 + fireTintEnabled: 0 + fireColor: {r: 1, g: 0.45, b: 0.1, a: 1} + fireGroundSnap: 1 diff --git a/Assets/WL/Combat/Stage/Barrier/Resources/WL/WLStageBarrierSettings.asset.meta b/Assets/WL/Combat/Stage/Barrier/Resources/WL/WLStageBarrierSettings.asset.meta new file mode 100644 index 000000000..388fcac2c --- /dev/null +++ b/Assets/WL/Combat/Stage/Barrier/Resources/WL/WLStageBarrierSettings.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 25f5d7207dc33a54e8a14b8628b6eae6 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/WL/Combat/Stage/Barrier/StageBarrier.cs b/Assets/WL/Combat/Stage/Barrier/StageBarrier.cs new file mode 100644 index 000000000..ae7a9e8f8 --- /dev/null +++ b/Assets/WL/Combat/Stage/Barrier/StageBarrier.cs @@ -0,0 +1,482 @@ +// ───────────────────────────────────────────────────────────────────────────── +// StageBarrier.cs — 스테이지 「고정 영역(장벽)」 · 정적 API +// +// PD 지시 #815 「스테이지에 들어가면 고정 영역 안에서만 싸운다」 +// + PD 질문(2026-09-11) 「구역을 어떻게 막는가 — 울타리 오브젝트 · 불꽃 이펙트 등 대안」 +// 발주서 WL-815c · SOT = 기준서 「스테이지_전환_기준서_v1.md」 §D +// +// ■ 공개 계약(815b 의 StageBarrierHook 이 이것을 부른다 — Lead 가 병합 뒤 1줄 연결) +// StageBarrier.Set(center, radius) 스테이지 입장 — 장벽을 세운다 +// StageBarrier.Clear() 스테이지 클리어 — 장벽을 연다 +// StageBarrier.IsInside(pos) 이 좌표가 장벽 안인가 +// StageBarrier.Active 장벽이 서 있는가 +// 🔴 이 파일은 StageBarrierHook 을 정의하지 않는다(815b 소유 · 이름 충돌 방지). +// +// ■ 「어떻게 막는가」 = 기준서 §D-2 실측 결론 ③ 거리 판정 + 워프백 +// ① 물리 콜라이더 벽은 **불가** — PC 프리팹 Ai01 의 CapsuleCollider 가 IsTrigger 이고 +// Rigidbody Constraints = 126(위치·회전 전부 고정)이라 물리로 밀 수 없다. +// 몹도 MobActor.Start 가 isKinematic 을 강제한다. +// ② NavMesh 카브는 되지만(813t2 가 NavMeshModifierVolume 로 실증) 런타임 재베이크 비용이 있어 **옵션**. +// ③ 거리 + 워프백은 **이미 돌고 있는 방식**이다 — BossArena.EvaluateLeash(arenaRadius+margin, 유예)와 +// 813w3 RunGuide.WarpToArena, 813t2 TerrainGuard 가 같은 원본 API 를 쓴다. +// +// ■ 되돌리는 방법 = 원본 `Actor.Set_Warp`(Actor.cs:2111) — 813i DeathFlow · 813w3 RunGuide · +// 813t2 TerrainGuard · 813x MobNavRecovery 와 같은 API. 이펙트·잔상 0. +// PC 는 Get_NavSnapRadius()=0 이라 Set_Warp 이 스스로 스냅하지 않는다 +// → **반드시 먼저 NavMesh.SamplePosition 으로 스냅한 지점**을 넘긴다(지형 뚫기 방지 · 813w3 규칙). +// 되돌린 뒤 PC 는 카메라 이동 리드를 0 으로 스냅한다(813t2 방식 · RealCamera 무수정). +// +// ■ 원본 훅 0 — 폴링만 쓴다(813x MobNavRecovery · 813t2 TerrainGuard 선례). +// CombatEvents 를 쓰지 않는 이유: RaiseSpawned 첫 줄이 `if (!Enabled …) return;` 이라 +// 코어 스위치가 꺼지면 이벤트가 0 이다(813t2 실측). +// +// ■ 그림(3스타일)은 StageBarrierVisual.cs 가 맡는다. 이 파일은 판정·워프·카브만 다룬다. +// +// ■ 값(C45) 전부 WLStageBarrierSettings. 게임 수치 상수 0. +// 🔴 Debug.LogError/LogException 금지(813z — ErrorLogHookManager 가 플레이어 모달을 띄운다). +// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것(Actor/MobActor 가 Assembly-CSharp 에 있다). +// ───────────────────────────────────────────────────────────────────────────── + +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.AI; + +namespace WL.Combat.Stage +{ + /// 스테이지 고정 영역. 정적 · Set/Clear 로 세우고 연다. + public static class StageBarrier + { + static WLStageBarrierSettings Cfg { get { return WLStageBarrierSettings.Instance; } } + + // ───────────────────────────────────────── 상태 + static bool s_active; + static Vector3 s_center; + static float s_radius; + + /// 장벽이 서 있는가. + public static bool Active { get { return s_active; } } + + /// 장벽 중심(월드). + public static Vector3 Center { get { return s_center; } } + + /// 장벽 반경(m). + public static float Radius { get { return s_radius; } } + + /// 기능 축이 살아 있는가(에셋 · enabled · RuntimeDisabled). + public static bool FeatureEnabled { get { return WLStageBarrierSettings.Active; } } + + // ───────────────────────────────────────── 진단 카운터(프로브·QA 가 읽는다) + public static int SetCount, ClearCount, Checks, Scans; + public static int PlayerWarps, MobWarps, WarpsFailedNavMesh, WarpsSkippedCooldown, OutsideSeen; + public static int CarveSegments; + public static float LastCarveBuildMs; + public static string LastLog = ""; + public static Vector3 LastWarpFrom, LastWarpTo; + + // ───────────────────────────────────────── 내부(GC 0 — 틱에서 새로 만들지 않는다) + static MobActor[] s_mobs = new MobActor[0]; + static readonly Dictionary s_outsideSince = new Dictionary(); + static readonly Dictionary s_lastWarp = new Dictionary(); + static readonly List s_carve = new List(); + static Transform s_carveRoot; + static float s_nextRescan, s_nextCheck; + static bool s_faulted; + + // ═════════════════════════════════════════════════════════════════════ + // 공개 API + // ═════════════════════════════════════════════════════════════════════ + + /// 스테이지 입장 — 중심·반경으로 장벽을 세운다. 같은 값으로 다시 불러도 안전(멱등). + public static void Set(Vector3 center, float radius) + { + var cfg = Cfg; + if (cfg == null || !cfg.enabled || WLStageBarrierSettings.RuntimeDisabled) return; // C8 — 오브젝트 0 + if (radius <= 0f) { Clear(); return; } + + bool moved = !s_active || (s_center - center).sqrMagnitude > 0.0001f || !Mathf.Approximately(s_radius, radius); + + s_center = center; + s_radius = radius; + s_active = true; + SetCount++; + s_outsideSince.Clear(); + s_lastWarp.Clear(); + s_faulted = false; + s_nextRescan = s_nextCheck = 0f; + + StageBarrierBoot.EnsureRunner(); + + if (moved) { RebuildCarve(cfg); } + StageBarrierVisual.Show(cfg, center, radius); + + Log("세움 중심 " + center.ToString("F1") + " 반경 " + radius.ToString("F1") + " m · 스타일 " + StageBarrierVisual.CurrentStyle + + (cfg.carveNavMesh ? " · 카브 " + CarveSegments + "세그(" + LastCarveBuildMs.ToString("F1") + " ms)" : "")); + } + + /// 스테이지 클리어 — 장벽을 연다. 그림은 fadeSeconds 동안 사라진 뒤 풀에 반납된다. + public static void Clear() + { + if (!s_active && s_carve.Count == 0 && !StageBarrierVisual.HasActive) return; + s_active = false; + ClearCount++; + s_outsideSince.Clear(); + s_lastWarp.Clear(); + ClearCarve(); + StageBarrierVisual.Hide(Cfg); + Log("열림(클리어)"); + } + + /// 이 좌표가 장벽 안인가. 장벽이 없으면 항상 true(제약 없음). + public static bool IsInside(Vector3 pos) + { + if (!s_active) return true; + return HorizontalSqr(pos, s_center) <= s_radius * s_radius; + } + + /// 여유를 더해서 판정한다(음수면 안쪽으로 좁힌다). + public static bool IsInside(Vector3 pos, float extraMargin) + { + if (!s_active) return true; + float r = Mathf.Max(0f, s_radius + extraMargin); + return HorizontalSqr(pos, s_center) <= r * r; + } + + /// 중심에서의 수평 거리(m). 장벽이 없으면 −1. + public static float DistanceFromCenter(Vector3 pos) + { + if (!s_active) return -1f; + return Mathf.Sqrt(HorizontalSqr(pos, s_center)); + } + + /// 스타일만 바꾼다(PD 비교용 · 장벽은 그대로). 판정에는 영향이 없다. + public static void SetStyle(StageBarrierStyle style) + { + var cfg = Cfg; + if (cfg == null) return; + StageBarrierVisual.StyleOverride = style; + if (s_active) StageBarrierVisual.Show(cfg, s_center, s_radius); + } + + // ═════════════════════════════════════════════════════════════════════ + // 판정 · 워프백 + // ═════════════════════════════════════════════════════════════════════ + + internal static void Tick() + { + StageBarrierVisual.Tick(Cfg); + + if (s_faulted || !s_active) return; + var cfg = Cfg; + if (cfg == null || !cfg.enabled || WLStageBarrierSettings.RuntimeDisabled || !cfg.blockEnabled) return; + + float now = Time.unscaledTime; + if (now >= s_nextRescan) + { + s_nextRescan = now + Mathf.Max(0.1f, cfg.rescanSeconds); + Rescan(); + } + if (now < s_nextCheck) return; + s_nextCheck = now + Mathf.Max(0.02f, cfg.checkSeconds); + + // 🔴 Debug.LogException 을 쓰지 않는다 — ErrorLogHookManager 가 그것을 플레이어 모달로 띄운다(813z). + try { CheckAll(cfg, now); } + catch (System.Exception ex) { s_faulted = true; Log("중단(예외) — " + ex.Message); } + } + + static void Rescan() + { + // FindObjectsInactive.Exclude — 비활성 몹은 풀에 들어간 사체다(Actor.cs:633 에서 에이전트를 끈다). + s_mobs = Object.FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None); + Scans++; + if (s_outsideSince.Count > 128) s_outsideSince.Clear(); // 풀 재사용으로 쌓인 죽은 키 정리 + if (s_lastWarp.Count > 128) s_lastWarp.Clear(); + } + + static void CheckAll(WLStageBarrierSettings cfg, float now) + { + if (cfg.blockPlayer) + { + var pc = MyValue.MyPC; + if (!DSUtil.CheckNull(pc)) CheckOne(cfg, pc, now, true); + } + if (!cfg.blockMobs) return; + + var mobs = s_mobs; + for (int i = 0; i < mobs.Length; i++) + { + var m = mobs[i]; + if (DSUtil.CheckNull(m)) continue; + CheckOne(cfg, m, now, false); + } + } + + static void CheckOne(WLStageBarrierSettings cfg, Actor actor, float now, bool isPlayer) + { + int key = actor.GetInstanceID(); + if (actor.IsDead()) { s_outsideSince.Remove(key); return; } + Checks++; + + Vector3 pos = actor.Get_position(); + float margin = isPlayer ? cfg.leaveMargin : cfg.mobLeaveMargin; + float limit = s_radius + Mathf.Max(0f, margin); + if (HorizontalSqr(pos, s_center) <= limit * limit) { s_outsideSince.Remove(key); return; } + + float since; + if (!s_outsideSince.TryGetValue(key, out since)) + { + s_outsideSince[key] = now; // 넉백·대시일 수 있다 — 유예를 준다 + OutsideSeen++; + return; + } + float grace = isPlayer ? cfg.graceSeconds : cfg.mobGraceSeconds; + if (now - since < Mathf.Max(0f, grace)) return; + + float last; + float cool = isPlayer ? cfg.warpCooldownSeconds : cfg.mobWarpCooldownSeconds; + if (s_lastWarp.TryGetValue(key, out last) && now - last < Mathf.Max(0f, cool)) + { WarpsSkippedCooldown++; return; } + + WarpBack(cfg, actor, key, pos, now, isPlayer); + } + + /// + /// 되돌릴 지점 = 「가장 가까운 안쪽 지점」 — 중심에서 대상 방향으로 (반경 − 들여놓기) 만큼. + /// **좌표만 다루는 순수 함수**라 프로브가 실 액터 없이 같은 코드를 검증할 수 있다(GC 0). + /// + public static Vector3 NearestInsidePoint(WLStageBarrierSettings cfg, Vector3 center, float radius, Vector3 from) + { + Vector3 d = from - center; d.y = 0f; + float len = d.magnitude; + float target = Mathf.Max(0f, radius - Mathf.Max(0f, cfg.warpInsetMeters)); + if (len < 0.001f) return new Vector3(center.x, from.y, center.z); // 정중앙 = 이미 안 + Vector3 p = center + d * (target / len); + p.y = from.y; // 높이는 NavMesh 스냅이 정한다 + return p; + } + + /// 되돌릴 지점을 NavMesh 위로 스냅한다. 못 찾으면 false = 워프하지 않는다(지형 뚫기 방지). + public static bool ResolveWarpTarget(WLStageBarrierSettings cfg, Vector3 center, float radius, Vector3 from, out Vector3 target) + { + Vector3 want = NearestInsidePoint(cfg, center, radius, from); + NavMeshHit hit; + if (NavMesh.SamplePosition(want, out hit, Mathf.Max(0.1f, cfg.warpNavSampleRadius), NavMesh.AllAreas) + || NavMesh.SamplePosition(want, out hit, Mathf.Max(0.1f, cfg.warpNavSampleRadiusFar), NavMesh.AllAreas) + || NavMesh.SamplePosition(center, out hit, Mathf.Max(0.1f, cfg.warpNavSampleRadiusFar), NavMesh.AllAreas)) + { target = hit.position; return true; } + target = want; + return false; + } + + static void WarpBack(WLStageBarrierSettings cfg, Actor actor, int key, Vector3 from, float now, bool isPlayer) + { + s_outsideSince.Remove(key); // 성공이든 실패든 창을 다시 연다(실패 시 다음 유예 뒤 재시도) + + Vector3 target; + if (!ResolveWarpTarget(cfg, s_center, s_radius, from, out target)) + { + WarpsFailedNavMesh++; + Log("복귀 실패 — 안쪽 지점이 NavMesh 밖 " + from.ToString("F1")); + return; + } + + if (cfg.warpResetsPath) + { + var agent = actor.Get_Agent(); + if (agent != null && agent.enabled && agent.isOnNavMesh) agent.ResetPath(); + } + + actor.Set_Warp(target); // 원본 API · 이펙트 0 · 이미 스냅된 지점을 받는다 + if (isPlayer && cfg.warpSnapsCameraLead) SnapCameraLead(); + + s_lastWarp[key] = now; + if (isPlayer) PlayerWarps++; else MobWarps++; + LastWarpFrom = from; LastWarpTo = target; + Log("복귀 " + (isPlayer ? "PC" : "몹 " + actor.name) + " " + from.ToString("F1") + " → " + target.ToString("F1") + + " (중심 " + Mathf.Sqrt(HorizontalSqr(from, s_center)).ToString("F1") + " m → " + + Mathf.Sqrt(HorizontalSqr(target, s_center)).ToString("F1") + " m)"); + } + + /// 되돌린 직후 카메라 이동 리드를 0 으로 스냅한다(811 CameraMoveLead.Snap · 813t2 방식 · RealCamera 무수정). + static void SnapCameraLead() + { + var cam = MyValue.m_RealCamera; + if (DSUtil.CheckNull(cam)) return; + var lead = cam.Get_MoveLead(); + if (lead != null) lead.Snap(); + } + + // ═════════════════════════════════════════════════════════════════════ + // NavMesh 카브(옵션) + // ═════════════════════════════════════════════════════════════════════ + + /// 둘레 길이·세그먼트 길이로 세그먼트 수를 낸다(상한 적용). 순수 함수 — 프로브가 그대로 검증한다. + public static int CarveSegmentCount(WLStageBarrierSettings cfg, float radius) + { + if (cfg == null || radius <= 0f) return 0; + float circumference = 2f * Mathf.PI * radius; + int n = Mathf.CeilToInt(circumference / Mathf.Max(0.5f, cfg.carveSegmentArcMeters)); + return Mathf.Clamp(n, 3, Mathf.Max(3, cfg.maxCarveSegments)); + } + + static void RebuildCarve(WLStageBarrierSettings cfg) + { + ClearCarve(); + if (!cfg.carveNavMesh) { CarveSegments = 0; LastCarveBuildMs = 0f; return; } + + var sw = System.Diagnostics.Stopwatch.StartNew(); + int n = CarveSegmentCount(cfg, s_radius); + EnsureCarveRoot(); + + float arc = 2f * Mathf.PI * s_radius / n; + float width = arc * (1f + Mathf.Max(0f, cfg.carveOverlap)); + + for (int i = 0; i < n; i++) + { + float a = (i / (float)n) * Mathf.PI * 2f; + Vector3 dir = new Vector3(Mathf.Cos(a), 0f, Mathf.Sin(a)); + Vector3 p = s_center + dir * s_radius; + + var go = new GameObject("carve_" + i); + go.hideFlags = HideFlags.HideAndDontSave; + go.transform.SetParent(s_carveRoot, false); + go.transform.position = p; + go.transform.rotation = Quaternion.LookRotation(dir, Vector3.up); // +Z = 반경 방향 + + var ob = go.AddComponent(); + ob.shape = NavMeshObstacleShape.Box; + ob.size = new Vector3(width, Mathf.Max(0.5f, cfg.carveHeight), Mathf.Max(0.2f, cfg.carveThickness)); + ob.center = new Vector3(0f, Mathf.Max(0.5f, cfg.carveHeight) * 0.5f, 0f); + ob.carving = true; + ob.carveOnlyStationary = cfg.carveStaticOnly; + s_carve.Add(go); + } + sw.Stop(); + CarveSegments = n; + LastCarveBuildMs = (float)sw.Elapsed.TotalMilliseconds; + } + + static void EnsureCarveRoot() + { + if (s_carveRoot != null) return; + var go = new GameObject("[WL815c] StageBarrierCarve"); + go.hideFlags = HideFlags.HideAndDontSave; + s_carveRoot = go.transform; + } + + static void ClearCarve() + { + for (int i = 0; i < s_carve.Count; i++) + { + var g = s_carve[i]; + if (g == null) continue; + if (Application.isPlaying) Object.Destroy(g); else Object.DestroyImmediate(g); + } + s_carve.Clear(); + CarveSegments = 0; + } + + // ═════════════════════════════════════════════════════════════════════ + // 유틸 · 진단 + // ═════════════════════════════════════════════════════════════════════ + + static float HorizontalSqr(Vector3 a, Vector3 b) + { + float dx = a.x - b.x, dz = a.z - b.z; + return dx * dx + dz * dz; + } + + static void Log(string msg) + { + LastLog = msg; + var c = Cfg; + if (c != null && c.verboseLog) Debug.Log("[StageBarrier] " + msg); + } + + /// 프로브용: 카운터·유예 창 초기화(장벽 자체는 건드리지 않는다). + public static void ResetDiagnostics() + { + SetCount = ClearCount = Checks = Scans = 0; + PlayerWarps = MobWarps = WarpsFailedNavMesh = WarpsSkippedCooldown = OutsideSeen = 0; + LastLog = ""; LastWarpFrom = LastWarpTo = Vector3.zero; + s_outsideSince.Clear(); s_lastWarp.Clear(); s_mobs = new MobActor[0]; + s_nextRescan = s_nextCheck = 0f; s_faulted = false; + } + + /// 프로브용: 전부(장벽·그림·카브·풀) 원상 복구. + public static void HardResetForProbe() + { + s_active = false; + ClearCarve(); + StageBarrierVisual.HardResetForProbe(); + ResetDiagnostics(); + if (s_carveRoot != null) + { + if (Application.isPlaying) Object.Destroy(s_carveRoot.gameObject); else Object.DestroyImmediate(s_carveRoot.gameObject); + s_carveRoot = null; + } + SetCount = ClearCount = 0; + } + + /// 프로브용: 러너 없이 1회 판정(에디트 모드 · 타이머 초기화). + public static void ForceTick() { s_nextRescan = s_nextCheck = 0f; Tick(); } + + /// 프로브용: 러너가 부르는 것과 똑같은 틱(타이머 초기화 0 — GC 측정용). + public static void ProbeTick() { Tick(); } + + /// 프로브용: 임의 액터 1명을 지금 판정한다(유예·쿨 무시하고 즉시 복귀까지). 되돌렸으면 true. + public static bool ForceCheck(Actor actor, bool asPlayer) + { + var cfg = Cfg; + if (cfg == null || actor == null || !s_active) return false; + Vector3 pos = actor.Get_position(); + float margin = asPlayer ? cfg.leaveMargin : cfg.mobLeaveMargin; + float limit = s_radius + Mathf.Max(0f, margin); + if (HorizontalSqr(pos, s_center) <= limit * limit) return false; + WarpBack(cfg, actor, actor.GetInstanceID(), pos, Time.unscaledTime, asPlayer); + return true; + } + + /// 프로브용: 카브 오브젝트 수(실제 씬에 있는 것). + public static int CarveObjectCount { get { return s_carve.Count; } } + + /// 프로브용: i 번째 카브 세그먼트. 카브 오브젝트는 HideAndDontSave 라 FindObjectsByType 에 안 잡힌다. + public static NavMeshObstacle CarveObstacleAt(int i) + { + if (i < 0 || i >= s_carve.Count || s_carve[i] == null) return null; + return s_carve[i].GetComponent(); + } + } + + // ───────────────────────────────────────────────────────────────────────── + /// StageBarrier 의 시간 축. 숨김 GameObject 1개 · 코루틴 0. + internal sealed class StageBarrierRunner : MonoBehaviour + { + void Update() { StageBarrier.Tick(); } + } + + /// 러너 부팅(C8 — SO 가 꺼져 있으면 오브젝트 0). Set() 이 처음 불릴 때만 뜬다. + public static class StageBarrierBoot + { + static StageBarrierRunner s_runner; + + /// 러너가 떠 있는가(프로브). + public static bool RunnerAlive { get { return !DSUtil.CheckNull(s_runner); } } + + internal static void EnsureRunner() + { + if (!Application.isPlaying || !DSUtil.CheckNull(s_runner)) return; + if (!WLStageBarrierSettings.Active) return; + var go = new GameObject("[WL815c] StageBarrierRunner"); + go.hideFlags = HideFlags.HideAndDontSave; // 씬 전환에도 살아남고 씬·프리팹에 저장되지 않는다 + s_runner = go.AddComponent(); + } + + /// 프로브용: 러너를 회수한다. + public static void KillRunnerForProbe() + { + if (DSUtil.CheckNull(s_runner)) { s_runner = null; return; } + if (Application.isPlaying) Object.Destroy(s_runner.gameObject); else Object.DestroyImmediate(s_runner.gameObject); + s_runner = null; + } + } +} diff --git a/Assets/WL/Combat/Stage/Barrier/StageBarrier.cs.meta b/Assets/WL/Combat/Stage/Barrier/StageBarrier.cs.meta new file mode 100644 index 000000000..93742f2bb --- /dev/null +++ b/Assets/WL/Combat/Stage/Barrier/StageBarrier.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 60022f941d7bf9549bd96048f7478aa9 \ No newline at end of file diff --git a/Assets/WL/Combat/Stage/Barrier/StageBarrierVisual.cs b/Assets/WL/Combat/Stage/Barrier/StageBarrierVisual.cs new file mode 100644 index 000000000..1c335a21e --- /dev/null +++ b/Assets/WL/Combat/Stage/Barrier/StageBarrierVisual.cs @@ -0,0 +1,623 @@ +// ───────────────────────────────────────────────────────────────────────────── +// StageBarrierVisual.cs — 「보이는 장벽」 3스타일 (PD 대안 비교 · 값 하나로 전환) +// +// PD 질문(2026-09-11) 「스테이지 지역을 어떻게 구역으로 막을 것인지 대안 +// (울타리 오브젝트 배치 · 불꽃 이펙트 장벽 등)」 +// → 대안을 **코드로 만들어** PD 가 WLStageBarrierSettings.style 값 하나만 바꿔 보게 한다. +// +// ⓐ MagicRing — Hovl 「Marker 3 Zone Loop」 1개를 반경에 맞게 스케일(+겹). 가장 싸다. +// ⓑ Props — 울타리·바위 프리팹을 둘레에 propSpacing 간격으로 배치. 가장 「막혔다」고 읽힌다. +// ⓒ Fire — 불꽃 파티클을 둘레에 fireSpacing 간격으로 배치(총량 상한). 가장 비싸다. +// +// ■ 실측 근거(WL815c_SURVEY / SURVEY2 · 이 worktree 헤드리스 에디터) +// Marker 3 Zone Loop : 파티클시스템 4 · 머티리얼 1 · **시각 반경 1.10 m(스케일 1)** · 버스트 1/루프 → 살아있는 파티클 3~4 +// Fence_01 : 메시 1 · 머티리얼 1 · **삼각형 120** · 크기 2.38 × 1.02 × 0.27 m · pivot 지면·중앙 +// Fire_Particles : 파티클시스템 3(FireRed 20/s×1.5 s · FireYellow 15/s×1.0 s · Smoke 10/s×8 s) +// → **인스턴스 1개 정상상태 125 파티클** +// +// ■ 원본 무수정 +// · 프리팹은 `Object.Instantiate` 로 **끊어진 클론**을 만든다(PrefabUtility 를 쓰지 않는다) → 원본 프리팹 무수정. +// · 색은 인스턴스의 `ParticleSystem.main.startColor` 에만 대입한다 → **머티리얼·셰이더 무수정**. +// · 프롭은 transform 과 (옵션) Collider 제거만 — 머티리얼을 건드리지 않는다. +// +// ■ 811s EffectScaleGuard / 811q 예산과 충돌 0 +// EffectScaleGuard 는 **원본 이펙트 풀(`InGameInfo.tf_Effects` 의 직속 자식)** 만 훑는다. +// 장벽 그림은 전부 `[WL815c] StageBarrierVisual` 루트 밑에 있으므로 그 스윕 대상이 아니다. +// 불꽃은 그것과 별개로 `maxTotalParticles` 로 자체 상한을 건다. +// +// ■ 연출 · 풀링 +// 입장 = appearSeconds 동안 0 → 1 (링은 반경이 퍼지고 · 프롭은 지면에서 솟고 · 불꽃은 커진다) +// 클리어 = fadeSeconds 동안 1 → 0 뒤 **SetActive(false) 로 풀에 반납**(Destroy 0 · 다음 Set 은 재사용). +// 스타일이 바뀌면 이전 스타일의 인스턴스는 즉시 풀에 반납된다(스타일마다 별도 풀). +// +// 🔴 Debug.LogError/LogException 금지(813z). +// ───────────────────────────────────────────────────────────────────────────── + +using System.Collections.Generic; +using UnityEngine; + +namespace WL.Combat.Stage +{ + /// 장벽 그림 3스타일. 정적 · 풀링 · 원본 프리팹 무수정. + public static class StageBarrierVisual + { + // ───────────────────────────────────────── 상태 + static Transform s_root; + static StageBarrierStyle s_current = StageBarrierStyle.None; + static Vector3 s_center; + static float s_radius; + static float s_phaseStart; // 연출 시작 시각 + static float s_phaseLen; // 연출 길이(초) + static bool s_showing; // true = 나타나는 중/유지 · false = 사라지는 중/꺼짐 + static float s_t = 0f; // 0..1 현재 연출 진행도 + + /// PD·프로브가 SO 를 안 고치고 스타일만 바꿀 때(−1 = 무시). + public static StageBarrierStyle StyleOverride = (StageBarrierStyle)(-1); + + /// 지금 그려지고 있는 스타일. + public static StageBarrierStyle CurrentStyle { get { return s_current; } } + + /// 살아 있는(꺼지지 않은) 그림이 있는가. + public static bool HasActive { get { return s_showing || s_t > 0f; } } + + // ───────────────────────────────────────── 진단(프로브가 읽는다) + /// Instantiate 호출 누계. 두 번째 Set 부터 늘지 않으면 풀링이 도는 것이다. + public static int Instantiates; + /// 지금 활성인 인스턴스 수 · 풀에 반납된 수. + public static int ActiveInstances, PooledInstances; + /// 스타일 전환 횟수 · Show/Hide 호출 수. + public static int StyleSwitches, Shows, Hides; + /// ⓑ 실제로 놓인 프롭 수 · 상한에 걸려 벌어진 간격(m). + public static int PropCount; public static float PropSpacingUsed; + /// ⓒ 실제로 놓인 불꽃 수 · 적용된 방출률 배수 · 추정 총 파티클. + public static int FireCount; public static float FireRateMul; public static int FireParticleEstimate; + /// ⓒ 프리팹 1개당 정상상태 파티클 추정(실측 계산). + public static int FirePerInstanceEstimate; + /// ⓐ 링에 적용된 스케일. + public static float RingScale; + /// 마지막 Show 가 든 시간(ms). + public static float LastBuildMs; + + // ───────────────────────────────────────── 풀(스타일별 · Destroy 0) + sealed class Slot + { + public GameObject go; + public Transform tf; + public Vector3 basePos; + public Quaternion baseRot; + public Vector3 baseScale; + public ParticleSystem[] ps; + /// 프리팹 원본 색·상한·방출률(인스턴스 생성 때 1회 채집 — 풀 재사용 때 여기서 되돌린다). + public Color[] psOrig; + public int[] psMaxOrig; + public ParticleSystem.MinMaxCurve[] psRateOrig; + /// 이번 회차에 쓸 색(연출 알파가 여기에 곱해진다). + public Color[] psColor; + } + + static readonly List s_ring = new List(); + static readonly List s_props = new List(); + static readonly List s_fires = new List(); + static readonly List s_live = new List(); // 지금 쓰는 슬롯(연출 대상) + + // ═════════════════════════════════════════════════════════════════════ + // 공개 — Show / Hide / Tick + // ═════════════════════════════════════════════════════════════════════ + + /// 장벽 그림을 세운다(입장 연출 시작). + public static void Show(WLStageBarrierSettings cfg, Vector3 center, float radius) + { + if (cfg == null) return; + Shows++; + + StageBarrierStyle want = StyleOverride >= 0 ? StyleOverride : cfg.style; + if (!cfg.showVisual) want = StageBarrierStyle.None; + + s_center = center; + s_radius = Mathf.Max(0.1f, radius) * Mathf.Max(0.05f, cfg.visualRadiusScale); + + var sw = System.Diagnostics.Stopwatch.StartNew(); + if (want != s_current) { ReturnAll(); StyleSwitches++; } + s_current = want; + + switch (want) + { + case StageBarrierStyle.MagicRing: BuildRing(cfg); break; + case StageBarrierStyle.Props: BuildProps(cfg); break; + case StageBarrierStyle.Fire: BuildFire(cfg); break; + default: ReturnAll(); break; + } + sw.Stop(); + LastBuildMs = (float)sw.Elapsed.TotalMilliseconds; + + s_showing = true; + s_phaseStart = Time.unscaledTime; + s_phaseLen = Mathf.Max(0.01f, cfg.appearSeconds); + ApplyPhase(0f); + } + + /// 장벽 그림을 연다(클리어 연출 시작 → fadeSeconds 뒤 풀 반납). + public static void Hide(WLStageBarrierSettings cfg) + { + if (!s_showing && s_t <= 0f) return; + Hides++; + s_showing = false; + s_phaseStart = Time.unscaledTime; + s_phaseLen = Mathf.Max(0.01f, cfg != null ? cfg.fadeSeconds : 0.6f); + } + + /// 연출 진행. StageBarrier.Tick 이 부른다. + internal static void Tick(WLStageBarrierSettings cfg) + { + if (s_live.Count == 0 && s_t <= 0f) return; + float len = Mathf.Max(0.01f, s_phaseLen); + float k = Mathf.Clamp01((Time.unscaledTime - s_phaseStart) / len); + float t = s_showing ? k : 1f - k; + if (Mathf.Approximately(t, s_t) && (t <= 0f || t >= 1f)) { if (t <= 0f && !s_showing) ReturnAll(); return; } + ApplyPhase(t); + if (!s_showing && t <= 0f) ReturnAll(); + } + + // ═════════════════════════════════════════════════════════════════════ + // ⓐ 마법진 링 — Hovl Marker 를 반경에 맞게 스케일 + // ═════════════════════════════════════════════════════════════════════ + static void BuildRing(WLStageBarrierSettings cfg) + { + ReturnAll(); + if (cfg.ringPrefab == null) { RingScale = 0f; return; } + + int layers = 1 + Mathf.Max(0, cfg.ringExtraRings); + float baseR = Mathf.Max(0.01f, cfg.ringPrefabVisualRadius); + + for (int i = 0; i < layers; i++) + { + float r = s_radius + (i - (layers - 1) * 0.5f) * Mathf.Max(0f, cfg.ringExtraSpacing); + float scale = r / baseR; + if (i == 0) RingScale = scale; + + var s = Take(s_ring, cfg.ringPrefab); + if (s == null) continue; + s.basePos = new Vector3(s_center.x, s_center.y + cfg.visualYOffset + i * 0.01f, s_center.z); + s.baseRot = Quaternion.identity; + s.baseScale = new Vector3(scale, scale * Mathf.Max(0.01f, cfg.ringYScale), scale); + s.tf.SetPositionAndRotation(s.basePos, s.baseRot); + if (cfg.ringTintEnabled) Tint(s, cfg.ringColor); + Restart(s); + s_live.Add(s); + } + RefreshCounts(); + } + + // ═════════════════════════════════════════════════════════════════════ + // ⓑ 프롭 링 — 울타리 · 바위를 둘레에 배치(풀링 · 상한 · 지면 스냅) + // ═════════════════════════════════════════════════════════════════════ + static void BuildProps(WLStageBarrierSettings cfg) + { + ReturnAll(); + PropCount = 0; PropSpacingUsed = 0f; + if (cfg.propPrefabs == null || cfg.propPrefabs.Length == 0) return; + + float circumference = 2f * Mathf.PI * s_radius; + int n = Mathf.Max(3, Mathf.RoundToInt(circumference / Mathf.Max(0.2f, cfg.propSpacing))); + if (n > cfg.maxProps) n = Mathf.Max(3, cfg.maxProps); // 🔴 상한 — 넘으면 간격이 벌어진다 + PropSpacingUsed = circumference / n; + + var rnd = new System.Random(cfg.propSeed); + int variants = cfg.propPrefabs.Length; + + for (int i = 0; i < n; i++) + { + var prefab = cfg.propPrefabs[i % variants]; + if (prefab == null) continue; + + float a = (i / (float)n) * Mathf.PI * 2f; + Vector3 dir = new Vector3(Mathf.Cos(a), 0f, Mathf.Sin(a)); + float radial = s_radius + ((float)rnd.NextDouble() * 2f - 1f) * Mathf.Max(0f, cfg.propRadialJitter); + Vector3 p = s_center + dir * radial; + + if (cfg.propGroundSnap) + { + RaycastHit hit; + if (Physics.Raycast(p + Vector3.up * Mathf.Max(1f, cfg.propRayUp), Vector3.down, out hit, + Mathf.Max(2f, cfg.propRayLength))) + p.y = hit.point.y; + else p.y = s_center.y; + } + else p.y = s_center.y; + p.y += cfg.visualYOffset; + + // 접선 정렬: 프롭의 긴축(+X · Fence_01 실측 2.38 m)이 둘레의 접선을 따라가게 한다. + // LookRotation(접선) 은 +Z 를 접선에 맞추므로 +Y 90° 를 더해 +X 를 접선에 놓는다. + Quaternion rot = Quaternion.identity; + if (cfg.propAlignToTangent) + { + Vector3 tangent = new Vector3(-dir.z, 0f, dir.x); + rot = Quaternion.LookRotation(tangent, Vector3.up) * Quaternion.Euler(0f, 90f, 0f); + } + if (cfg.propYawJitter > 0f) + rot = rot * Quaternion.Euler(0f, ((float)rnd.NextDouble() * 2f - 1f) * cfg.propYawJitter, 0f); + + var s = Take(s_props, prefab); + if (s == null) continue; + s.basePos = p; + s.baseRot = rot; + s.baseScale = Vector3.one * Mathf.Max(0.01f, cfg.propScale); + s.tf.SetPositionAndRotation(p, rot); + if (cfg.propStripColliders) StripColliders(s.go); + s_live.Add(s); + PropCount++; + } + RefreshCounts(); + } + + // ═════════════════════════════════════════════════════════════════════ + // ⓒ 불꽃 링 — 파티클 총량 상한을 지키며 둘레에 배치 + // ═════════════════════════════════════════════════════════════════════ + static void BuildFire(WLStageBarrierSettings cfg) + { + ReturnAll(); + FireCount = 0; FireRateMul = 1f; FireParticleEstimate = 0; FirePerInstanceEstimate = 0; + if (cfg.firePrefabs == null || cfg.firePrefabs.Length == 0) return; + + float circumference = 2f * Mathf.PI * s_radius; + int n = Mathf.Max(3, Mathf.RoundToInt(circumference / Mathf.Max(0.2f, cfg.fireSpacing))); + if (n > cfg.maxFires) n = Mathf.Max(3, cfg.maxFires); + + // 🔴 프리팹 1개당 정상상태 파티클 추정 = Σ min(rate × life, maxParticles) — 프리팹에서 읽는다(코드 상수 0). + FirePerInstanceEstimate = EstimateSteadyState(cfg.firePrefabs[0]); + + // 총량 상한 적용 + float mul = 1f; + if (FirePerInstanceEstimate > 0 && cfg.maxTotalParticles > 0) + { + int would = FirePerInstanceEstimate * n; + if (would > cfg.maxTotalParticles) + { + if (cfg.fireBudgetMode == FireBudgetMode.ReduceCount) + n = Mathf.Max(3, cfg.maxTotalParticles / Mathf.Max(1, FirePerInstanceEstimate)); + else + mul = Mathf.Clamp01(cfg.maxTotalParticles / (float)would); + } + } + FireRateMul = mul; + + int variants = cfg.firePrefabs.Length; + + for (int i = 0; i < n; i++) + { + var prefab = cfg.firePrefabs[i % variants]; + if (prefab == null) continue; + + float a = (i / (float)n) * Mathf.PI * 2f; + Vector3 dir = new Vector3(Mathf.Cos(a), 0f, Mathf.Sin(a)); + Vector3 p = s_center + dir * s_radius; + + if (cfg.fireGroundSnap) + { + RaycastHit hit; + if (Physics.Raycast(p + Vector3.up * Mathf.Max(1f, cfg.propRayUp), Vector3.down, out hit, + Mathf.Max(2f, cfg.propRayLength))) + p.y = hit.point.y; + else p.y = s_center.y; + } + else p.y = s_center.y; + p.y += cfg.visualYOffset; + + var s = Take(s_fires, prefab); + if (s == null) continue; + s.basePos = p; + s.baseRot = Quaternion.LookRotation(dir, Vector3.up); + s.baseScale = Vector3.one * Mathf.Max(0.01f, cfg.fireScale); + s.tf.SetPositionAndRotation(p, s.baseRot); + ApplyFireBudget(s, mul, cfg); + if (cfg.fireTintEnabled) Tint(s, cfg.fireColor); + Restart(s); + s_live.Add(s); + FireCount++; + } + FireParticleEstimate = Mathf.RoundToInt(FirePerInstanceEstimate * FireCount * mul); + RefreshCounts(); + } + + /// 프리팹 1개의 정상상태 파티클 추정 = Σ min(rateOverTime × startLifetime, maxParticles). 순수 계산. + public static int EstimateSteadyState(GameObject prefab) + { + if (prefab == null) return 0; + var pss = prefab.GetComponentsInChildren(true); + float sum = 0f; + for (int i = 0; i < pss.Length; i++) + { + var m = pss[i].main; + var e = pss[i].emission; + float rate = e.enabled ? e.rateOverTime.constantMax : 0f; + float life = m.startLifetime.constantMax; + sum += Mathf.Min(rate * life, m.maxParticles); + } + return Mathf.RoundToInt(sum); + } + + /// 인스턴스에만 방출률·상한을 건다(프리팹 무수정 · 총량 상한의 실행부). + static void ApplyFireBudget(Slot s, float mul, WLStageBarrierSettings cfg) + { + if (s.ps == null || s.psRateOrig == null) return; + for (int i = 0; i < s.ps.Length; i++) + { + var ps = s.ps[i]; + if (ps == null || i >= s.psRateOrig.Length) continue; + + var orig = s.psRateOrig[i]; + var e = ps.emission; + if (mul < 1f) e.rateOverTime = ScaleCurve(orig, mul); + + var m = ps.main; + float life = m.startLifetime.constantMax; + int cap = Mathf.Max(4, Mathf.CeilToInt(orig.constantMax * mul * life) + 4); + int max = s.psMaxOrig != null && i < s.psMaxOrig.Length ? s.psMaxOrig[i] : m.maxParticles; + m.maxParticles = Mathf.Min(max, cap); // 🔴 하드 상한 — 버스트가 튀어도 못 넘는다 + } + } + + // ═════════════════════════════════════════════════════════════════════ + // 연출 — 0 → 1 (나타남) / 1 → 0 (사라짐) + // ═════════════════════════════════════════════════════════════════════ + static void ApplyPhase(float t) + { + s_t = t; + float e = Mathf.SmoothStep(0f, 1f, Mathf.Clamp01(t)); + for (int i = 0; i < s_live.Count; i++) + { + var s = s_live[i]; + if (s == null || s.tf == null) continue; + + if (s_current == StageBarrierStyle.Props) + { + // 프롭 = 지면에서 솟는다(Y 만 자란다 · XZ 고정) → 머티리얼 무수정으로 「생겨남」이 읽힌다 + s.tf.localScale = new Vector3(s.baseScale.x, s.baseScale.y * Mathf.Max(0.001f, e), s.baseScale.z); + } + else + { + s.tf.localScale = s.baseScale * Mathf.Max(0.001f, e); + } + + if (s.ps != null && s.ps.Length > 0) SetAlpha(s, e); + + bool on = e > 0.002f; + if (s.go.activeSelf != on) s.go.SetActive(on); + } + } + + /// 연출 진행도를 「원래 색」의 알파에 곱한다(인스턴스만 · 머티리얼 무수정 · 프리팹 원색 보존). + static void SetAlpha(Slot s, float a) + { + for (int i = 0; i < s.ps.Length; i++) + { + var ps = s.ps[i]; + if (ps == null) continue; + Color c = s.psColor != null && i < s.psColor.Length ? s.psColor[i] : ps.main.startColor.color; + var m = ps.main; + m.startColor = new ParticleSystem.MinMaxGradient(new Color(c.r, c.g, c.b, c.a * a)); + } + } + + /// SO 색으로 「원래 색」을 갈아 끼운다(다음 SetAlpha 부터 이 색이 쓰인다). + static void Tint(Slot s, Color color) + { + if (s.psColor == null) return; + for (int i = 0; i < s.psColor.Length; i++) s.psColor[i] = color; + } + + static void Restart(Slot s) + { + if (s.ps == null) return; + for (int i = 0; i < s.ps.Length; i++) + { + var ps = s.ps[i]; + if (ps == null) continue; + ps.Clear(true); + if (Application.isPlaying) ps.Play(true); + } + } + + // ═════════════════════════════════════════════════════════════════════ + // 풀 + // ═════════════════════════════════════════════════════════════════════ + static Slot Take(List pool, GameObject prefab) + { + for (int i = 0; i < pool.Count; i++) + { + var s = pool[i]; + if (s.go == null) continue; + if (s_live.Contains(s)) continue; + if (s.go.name != prefab.name + "#815c") continue; // 프리팹이 다르면 재사용하지 않는다 + RestoreOriginals(s); // 🔴 지난 회차의 색·예산을 되돌린다 + return s; + } + + EnsureRoot(); + var go = Object.Instantiate(prefab); // 🔴 끊어진 클론 — 원본 프리팹 무수정 + go.name = prefab.name + "#815c"; + go.hideFlags = HideFlags.HideAndDontSave; + go.transform.SetParent(s_root, false); + Instantiates++; + + var ps = go.GetComponentsInChildren(true); + var slot = new Slot + { + go = go, + tf = go.transform, + baseScale = Vector3.one, + ps = ps, + psOrig = new Color[ps.Length], + psMaxOrig = new int[ps.Length], + psRateOrig = new ParticleSystem.MinMaxCurve[ps.Length], + psColor = new Color[ps.Length], + }; + for (int i = 0; i < ps.Length; i++) + { + slot.psOrig[i] = ps[i].main.startColor.color; // 프리팹 원색 채집 + slot.psMaxOrig[i] = ps[i].main.maxParticles; + slot.psRateOrig[i] = ps[i].emission.rateOverTime; + slot.psColor[i] = slot.psOrig[i]; + } + pool.Add(slot); + return slot; + } + + /// 풀에서 꺼낼 때 프리팹 원본 색·파티클 상한·방출률 배수로 되돌린다. + static void RestoreOriginals(Slot s) + { + if (s.ps == null) return; + for (int i = 0; i < s.ps.Length; i++) + { + var ps = s.ps[i]; + if (ps == null) continue; + if (s.psColor != null && s.psOrig != null && i < s.psOrig.Length) s.psColor[i] = s.psOrig[i]; + if (s.psMaxOrig != null && i < s.psMaxOrig.Length) { var m = ps.main; m.maxParticles = s.psMaxOrig[i]; } + if (s.psRateOrig != null && i < s.psRateOrig.Length) { var e = ps.emission; e.rateOverTime = s.psRateOrig[i]; } + } + } + + /// + /// 방출률 곡선에 배수를 건다. + /// 🔴 `rateOverTimeMultiplier` 는 Constant 모드에서 **상수 그 자체**라 거기에 mul 을 대입하면 rate 가 mul(개/s)이 된다. + /// 모드별로 원본 값을 읽어 곱해야 한다. + /// + public static ParticleSystem.MinMaxCurve ScaleCurve(ParticleSystem.MinMaxCurve src, float mul) + { + switch (src.mode) + { + case ParticleSystemCurveMode.Constant: + return new ParticleSystem.MinMaxCurve(src.constant * mul); + case ParticleSystemCurveMode.TwoConstants: + return new ParticleSystem.MinMaxCurve(src.constantMin * mul, src.constantMax * mul); + case ParticleSystemCurveMode.Curve: + return new ParticleSystem.MinMaxCurve(src.curveMultiplier * mul, src.curve); + default: + return new ParticleSystem.MinMaxCurve(src.curveMultiplier * mul, src.curveMin, src.curveMax); + } + } + + static void ReturnAll() + { + for (int i = 0; i < s_live.Count; i++) + { + var s = s_live[i]; + if (s == null || s.go == null) continue; + s.go.SetActive(false); // 🔴 Destroy 0 — 풀에 반납 + } + s_live.Clear(); + s_t = 0f; + RefreshCounts(); + } + + static void StripColliders(GameObject go) + { + var cols = go.GetComponentsInChildren(true); + for (int i = 0; i < cols.Length; i++) + { + if (cols[i] == null) continue; + if (Application.isPlaying) Object.Destroy(cols[i]); else Object.DestroyImmediate(cols[i]); + } + } + + static void EnsureRoot() + { + if (s_root != null) return; + var go = new GameObject("[WL815c] StageBarrierVisual"); + go.hideFlags = HideFlags.HideAndDontSave; + s_root = go.transform; + } + + static void RefreshCounts() + { + ActiveInstances = s_live.Count; + PooledInstances = (s_ring.Count + s_props.Count + s_fires.Count) - s_live.Count; + } + + // ═════════════════════════════════════════════════════════════════════ + // 진단 · 프로브 + // ═════════════════════════════════════════════════════════════════════ + + /// 프로브용: 활성 인스턴스의 렌더러 수 · 머티리얼 종류 · 삼각형 합(드로우콜 근사). + public static void MeasureCost(out int renderers, out int materials, out long triangles, out int particleSystems) + { + renderers = 0; materials = 0; triangles = 0; particleSystems = 0; + var seen = new HashSet(); + for (int i = 0; i < s_live.Count; i++) + { + var s = s_live[i]; + if (s == null || s.go == null) continue; + var rs = s.go.GetComponentsInChildren(true); + for (int k = 0; k < rs.Length; k++) + { + renderers++; + var mf = rs[k].GetComponent(); + if (mf != null && mf.sharedMesh != null) triangles += mf.sharedMesh.triangles.Length / 3; + var ms = rs[k].sharedMaterials; + for (int j = 0; j < ms.Length; j++) if (ms[j] != null && seen.Add(ms[j].GetInstanceID())) materials++; + } + if (s.ps != null) particleSystems += s.ps.Length; + } + } + + /// 프로브용: 지금 활성 인스턴스 수. + public static int LiveCount { get { return s_live.Count; } } + + /// 프로브용: 연출 진행도(0..1). + public static float Phase { get { return s_t; } } + + /// 프로브용: 연출을 끝난 상태로 즉시 만든다(캡처용 · 0 = 사라진 상태). + public static void ForcePhase(float t) { ApplyPhase(Mathf.Clamp01(t)); } + + /// 프로브용: 지금 도는 연출(입장/클리어)의 시계를 끝으로 돌린다 — 다음 Tick 이 실제 종료 경로를 탄다. + public static void FinishPhaseForProbe() { s_phaseStart = Time.unscaledTime - Mathf.Max(0.01f, s_phaseLen) - 0.01f; } + + /// 프로브용: 파티클을 에디트 모드에서 t 초만큼 시뮬레이트한다(캡처용). + public static void SimulateForProbe(float seconds) + { + for (int i = 0; i < s_live.Count; i++) + { + var s = s_live[i]; + if (s == null || s.ps == null) continue; + for (int k = 0; k < s.ps.Length; k++) + { + if (s.ps[k] == null) continue; + s.ps[k].Clear(true); + s.ps[k].Simulate(seconds, true, true, false); + } + } + } + + /// 프로브용: 풀까지 전부 파괴하고 카운터 초기화. + public static void HardResetForProbe() + { + s_live.Clear(); + DestroyPool(s_ring); DestroyPool(s_props); DestroyPool(s_fires); + if (s_root != null) + { + if (Application.isPlaying) Object.Destroy(s_root.gameObject); else Object.DestroyImmediate(s_root.gameObject); + s_root = null; + } + s_current = StageBarrierStyle.None; + StyleOverride = (StageBarrierStyle)(-1); + s_showing = false; s_t = 0f; + Instantiates = ActiveInstances = PooledInstances = StyleSwitches = Shows = Hides = 0; + PropCount = FireCount = FireParticleEstimate = FirePerInstanceEstimate = 0; + PropSpacingUsed = 0f; FireRateMul = 1f; RingScale = 0f; LastBuildMs = 0f; + } + + static void DestroyPool(List pool) + { + for (int i = 0; i < pool.Count; i++) + { + var s = pool[i]; + if (s == null || s.go == null) continue; + if (Application.isPlaying) Object.Destroy(s.go); else Object.DestroyImmediate(s.go); + } + pool.Clear(); + } + + /// 프로브·캡처용: 그림 루트(임시 씬에서 카메라 프레이밍에 쓴다). + public static Transform Root { get { return s_root; } } + } +} diff --git a/Assets/WL/Combat/Stage/Barrier/StageBarrierVisual.cs.meta b/Assets/WL/Combat/Stage/Barrier/StageBarrierVisual.cs.meta new file mode 100644 index 000000000..a2091fc07 --- /dev/null +++ b/Assets/WL/Combat/Stage/Barrier/StageBarrierVisual.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 62bdc4832e9738745a6ad7e320f975cb \ No newline at end of file diff --git a/Assets/WL/Combat/Stage/Barrier/WLStageBarrierSettings.cs b/Assets/WL/Combat/Stage/Barrier/WLStageBarrierSettings.cs new file mode 100644 index 000000000..7baf0db31 --- /dev/null +++ b/Assets/WL/Combat/Stage/Barrier/WLStageBarrierSettings.cs @@ -0,0 +1,287 @@ +// ───────────────────────────────────────────────────────────────────────────── +// WLStageBarrierSettings.cs — 스테이지 장벽 값의 단일 출처(SOT · C45) +// +// PD 지시 #815 「스테이지에 들어가면 고정 영역(장벽) 안에서만 싸운다」 +// + PD 질문(2026-09-11 00:3x) 「지역을 어떻게 구역으로 막을 것인지 — 울타리 오브젝트 · 불꽃 이펙트 등 대안」 +// 발주서 WL-815c · SOT = 기준서 「스테이지_전환_기준서_v1.md」 §D +// +// ■ PD 가 값 하나만 바꿔서 3가지 「보이는 장벽」을 비교한다 → style 필드 +// 0 없음 · 1 마법진 링(Hovl Marker) · 2 프롭 링(울타리/바위) · 3 불꽃 링 +// +// ■ 막는 방법은 기준서 §D-2 실측 결론 그대로 = ③ 거리 판정 + 워프백(신규 코드 0 에 가깝게 재사용). +// ① 물리 콜라이더 벽은 **불가**(PC 캡슐 IsTrigger · Rigidbody Constraints 126). +// ② NavMesh 카브는 가능하지만 런타임 재베이크 비용이 있어 **옵션(기본 off)** 으로 둔다. +// +// 에셋 경로(고정): Assets/WL/Combat/Stage/Barrier/Resources/WL/WLStageBarrierSettings.asset +// → Resources.Load("WL/WLStageBarrierSettings") +// (WLCameraFramingSettings 가 같은 방식으로 기능 폴더 안 Resources 를 쓴다 — 815b 의 Stage/*.cs 와 파일 충돌 0) +// +// ■ C8 롤백: ① 에셋 없음 / ② enabled = 0 / ③ RuntimeDisabled = true → Set() 이 아무것도 하지 않는다(오브젝트 0). +// 축별 off = blockEnabled 0(워프백만 끔) · showVisual 0(그림만 끔) · carveNavMesh 0(카브만 끔). +// +// 🔴 아래 기본값은 에셋 생성 시 초기값이다. 실제로 읽히는 값은 항상 .asset 이고 코드에 게임 수치 상수는 없다(C45). +// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것(Actor/MobActor 가 Assembly-CSharp 에 있다). +// ───────────────────────────────────────────────────────────────────────────── + +using UnityEngine; + +namespace WL.Combat.Stage +{ + /// 장벽을 「보이게」 하는 방식. PD 가 이 값 하나로 3안을 바꿔 본다. + public enum StageBarrierStyle + { + /// 그림 없음 — 판정만(워프백). 비용 0. + None = 0, + /// ⓐ 마법진 링 — Hovl Marker 프리팹 1개를 반경에 맞게 스케일. + MagicRing = 1, + /// ⓑ 프롭 링 — 울타리·바위 프리팹을 둘레에 일정 간격 배치. + Props = 2, + /// ⓒ 불꽃 링 — 불꽃 파티클 프리팹을 둘레에 배치(총량 상한). + Fire = 3, + } + + /// 불꽃 총량 상한을 어떻게 지키는가. + public enum FireBudgetMode + { + /// 개수는 유지하고 인스턴스마다 방출률을 낮춘다(링이 끊기지 않는다 · 권장). + ReduceRate = 0, + /// 방출률은 유지하고 인스턴스 개수를 줄인다(불꽃 하나하나가 진하다 · 간격이 벌어진다). + ReduceCount = 1, + } + + [CreateAssetMenu(fileName = "WLStageBarrierSettings", menuName = "WL/Stage Barrier Settings", order = 333)] + public sealed class WLStageBarrierSettings : ScriptableObject + { + public const string ResourcesPath = "WL/WLStageBarrierSettings"; + + static WLStageBarrierSettings s_cached; + static bool s_lookupDone; + + /// 설정 에셋(1회 로드 후 캐시). 없으면 null = 기능 전체 off. + public static WLStageBarrierSettings Instance + { + get + { + if (!s_lookupDone) + { + s_cached = Resources.Load(ResourcesPath); + s_lookupDone = true; + } + return s_cached; + } + } + + /// 진단·A/B 전용 런타임 스위치. 도메인 리로드로 자동 false. + public static bool RuntimeDisabled; + + /// 장벽 축이 살아 있는가. + public static bool Active + { + get { var i = Instance; return i != null && i.enabled && !RuntimeDisabled; } + } + + /// 테스트용: 캐시를 비워 다음 접근 때 다시 Resources.Load 한다. + public static void ClearCache() { s_cached = null; s_lookupDone = false; } + + /// 프로브용: 임시 SO 를 잠시 Instance 자리에 끼운다(C8 진리표 · 실에셋은 읽기만 한다). + public static void SetInstanceForProbe(WLStageBarrierSettings so) { s_cached = so; s_lookupDone = true; } + + // ─────────────────────────────────────────────────────── 공통 + [Header("공통")] + [Tooltip("0 이면 Set() 이 무동작 = 원본 동작 100% (C8 롤백).")] + public bool enabled = true; + + [Tooltip("워프백·스타일 전환마다 로그 1줄.")] + public bool verboseLog; + + [Tooltip("이탈 판정 주기(초). 813w3 아레나 폴링(0.25 s)과 같은 등급.")] + public float checkSeconds = 0.2f; + + [Tooltip("몬스터 목록 재수집 주기(초). FindObjectsByType 는 여기서만 부른다(GC).")] + public float rescanSeconds = 3f; + + // ─────────────────────────────────────────────────────── ① 막기 = 워프백(기준서 §D-2 ③) + [Header("① 막기 — 거리 판정 + 워프백 (기준서 §D-2 ③ · 813w3/813t2 워프 API)")] + [Tooltip("0 이면 판정·워프백을 끈다(그림만 남는다).")] + public bool blockEnabled = true; + + [Tooltip("반경 + 이 여유(m) 밖이어야 「나갔다」고 본다. 813d playerLeaveMargin(4)보다 좁게 잡아 장벽 그림 밖으로 크게 벗어나지 않게 한다.")] + public float leaveMargin = 2f; + + [Tooltip("밖에 이만큼(초) 넘게 있어야 되돌린다. 넉백·대시로 잠깐 스치는 것은 흘려보낸다.")] + public float graceSeconds = 0.5f; + + [Tooltip("같은 대상을 다시 되돌리기까지의 최소 간격(초).")] + public float warpCooldownSeconds = 1f; + + [Tooltip("되돌릴 지점을 경계에서 안쪽으로 이만큼(m) 들여놓는다. 0 이면 경계에 딱 붙어 바로 다시 나간다.")] + public float warpInsetMeters = 1.5f; + + [Tooltip("되돌릴 지점을 NavMesh 위로 스냅할 1차 반경(m). PC 는 Get_NavSnapRadius()=0 이라 Set_Warp 이 스스로 스냅하지 않는다(813w3 실측).")] + public float warpNavSampleRadius = 4f; + + [Tooltip("1차에서 못 찾으면 쓰는 2차 반경(m). 그래도 못 찾으면 워프하지 않는다(지형 뚫기 방지 · 813w3 규칙).")] + public float warpNavSampleRadiusFar = 20f; + + [Tooltip("플레이어를 가둔다.")] + public bool blockPlayer = true; + + [Tooltip("몬스터도 가둔다(밖으로 밀린 몹 복귀 · 813x MobNavRecovery 와 같은 뿌리).")] + public bool blockMobs = true; + + [Tooltip("몬스터용 여유(m). PC 보다 넉넉하게 — 몹은 넉백으로 자주 밀린다.")] + public float mobLeaveMargin = 3f; + + [Tooltip("몬스터용 유예(초).")] + public float mobGraceSeconds = 1f; + + [Tooltip("몬스터용 쿨다운(초).")] + public float mobWarpCooldownSeconds = 2f; + + [Tooltip("플레이어를 되돌린 직후 카메라 이동 리드를 0 으로 스냅한다(813t2 방식 · RealCamera 무수정).")] + public bool warpSnapsCameraLead = true; + + [Tooltip("되돌리기 전에 남은 NavMesh 경로를 지운다(원본이 밖으로 다시 끌고 가지 않게).")] + public bool warpResetsPath = true; + + // ─────────────────────────────────────────────────────── ② NavMesh 카브(옵션 · 기본 off) + [Header("② NavMesh 카브 (옵션 · 기본 off · 기준서 §D-2 ②)")] + [Tooltip("1 이면 링 둘레에 NavMeshObstacle(carve) 세그먼트를 런타임 생성한다 = 몹이 경로로는 못 나간다. " + + "재베이크 비용이 있어 기본 0. 원본 NavMesh 에셋·프리팹은 건드리지 않는다(런타임 오브젝트만).")] + public bool carveNavMesh; + + [Tooltip("세그먼트 1개가 덮는 둘레 길이(m). 둘레 ÷ 이 값 = 세그먼트 수.")] + public float carveSegmentArcMeters = 4f; + + [Tooltip("세그먼트 수 상한. 반경이 커도 이 수를 넘기지 않는다(재베이크 비용 방어).")] + public int maxCarveSegments = 32; + + [Tooltip("세그먼트 상자의 두께(m · 반경 방향). 얇으면 몹이 뚫고 지나갈 수 있다.")] + public float carveThickness = 1.5f; + + [Tooltip("세그먼트 상자의 높이(m).")] + public float carveHeight = 3f; + + [Tooltip("이웃 세그먼트가 겹치는 비율(0.15 = 15 % 겹침). 0 이면 모서리에 틈이 생긴다.")] + public float carveOverlap = 0.15f; + + [Tooltip("carveOnlyMovementThreshold — 장벽은 움직이지 않으므로 1 이면 정적 카브(재베이크 1회). 0 이면 매 프레임 카브 검사.")] + public bool carveStaticOnly = true; + + // ─────────────────────────────────────────────────────── ③ 보이기 — 공통 + [Header("③ 보이기 — 스타일 (🔴 PD 는 이 값 하나만 바꾼다)")] + [Tooltip("0 없음 · 1 마법진 링 · 2 프롭 링(울타리·바위) · 3 불꽃 링")] + public StageBarrierStyle style = StageBarrierStyle.MagicRing; + + [Tooltip("0 이면 그림을 전부 끈다(판정만 남는다).")] + public bool showVisual = true; + + [Tooltip("스테이지 입장 때 장벽이 나타나는 시간(초).")] + public float appearSeconds = 0.6f; + + [Tooltip("클리어(장벽이 열릴 때) 사라지는 시간(초).")] + public float fadeSeconds = 0.6f; + + [Tooltip("그림을 지면 위 이만큼(m) 띄운다(Z-파이팅 방지).")] + public float visualYOffset = 0.05f; + + [Tooltip("그림 반경 = 판정 반경 × 이 배율. 1 보다 살짝 크게 두면 「선 안쪽에서 싸운다」로 읽힌다.")] + public float visualRadiusScale = 1f; + + // ─────────────────────────────────────────────────────── ⓐ 마법진 링 + [Header("ⓐ 마법진 링 (Hovl Map track markers · 기준서 §D-3 1순위)")] + [Tooltip("링 프리팹. 기본 = Marker 3 Zone Loop.")] + public GameObject ringPrefab; + + [Tooltip("🔴 실측값 — 스케일 1 일 때 이 프리팹의 시각 반경(m). 스케일 = 목표반경 ÷ 이 값. " + + "Marker 3 Zone Loop 실측 1.10 m(Simulate 2 s · 파티클 위치+반크기 최대).")] + public float ringPrefabVisualRadius = 1.1f; + + [Tooltip("링 색(파티클 startColor 에 인스턴스 단위로 대입 · 원본 머티리얼·프리팹 무수정). a 는 밝기.")] + public Color ringColor = new Color(1f, 0.35f, 0.15f, 1f); + + [Tooltip("0 이면 색을 건드리지 않고 프리팹 원색 그대로 쓴다.")] + public bool ringTintEnabled = true; + + [Tooltip("도트 룩(814b/814c)에서 링이 뭉개지지 않게 세로로 늘리는 배율. 1 = 프리팹 그대로. " + + "🔴 링은 평면 쿼드라 Y 스케일은 두께가 아니라 「기울기」를 만든다 — 두께는 ringExtraRings 로 낸다.")] + public float ringYScale = 1f; + + [Tooltip("링을 반경을 조금씩 달리해 몇 겹 더 깐다(0 = 1겹). 도트 룩에서 1px 로 뭉개지는 것을 막는 가장 싼 방법. " + + "겹당 인스턴스 1개 = 파티클 4개(실측).")] + public int ringExtraRings = 1; + + [Tooltip("추가 겹의 반경 차이(m).")] + public float ringExtraSpacing = 0.6f; + + // ─────────────────────────────────────────────────────── ⓑ 프롭 링 + [Header("ⓑ 프롭 링 (울타리 · 바위 · 기둥)")] + [Tooltip("둘레에 놓을 프롭 프리팹 목록. 여러 개면 번갈아 쓴다(회전 랜덤과 함께 반복감을 줄인다).")] + public GameObject[] propPrefabs = new GameObject[0]; + + [Tooltip("프롭 간격(m). 울타리는 프롭 길이(Fence_01 실측 2.38 m)보다 살짝 좁게 둬야 틈이 안 보인다.")] + public float propSpacing = 2.5f; + + [Tooltip("프롭 개수 상한. 둘레 ÷ 간격 이 이 수를 넘으면 간격을 자동으로 벌린다(개수 고정).")] + public int maxProps = 64; + + [Tooltip("프롭 스케일.")] + public float propScale = 1f; + + [Tooltip("1 이면 프롭의 정면(+X = 실측 긴축)이 둘레의 접선을 향한다 = 울타리가 벽처럼 이어진다. " + + "0 이면 회전 없음(바위처럼 방향이 없는 프롭용).")] + public bool propAlignToTangent = true; + + [Tooltip("접선 정렬 뒤 추가로 흔들 각도(±도). 바위·나무는 크게(180), 울타리는 작게(0~8).")] + public float propYawJitter = 6f; + + [Tooltip("반경 방향 흔들기(±m). 0 이면 정확히 원 위.")] + public float propRadialJitter = 0.2f; + + [Tooltip("1 이면 Raycast 로 지면 높이에 스냅한다(WL_Nature 는 평지가 아니다).")] + public bool propGroundSnap = true; + + [Tooltip("지면 Raycast 시작 높이(m · 장벽 중심 y 기준 위쪽).")] + public float propRayUp = 50f; + + [Tooltip("지면 Raycast 길이(m).")] + public float propRayLength = 200f; + + [Tooltip("1 이면 인스턴스의 Collider 를 전부 제거한다. 프롭 원본(LMHPOLY With_Box_Colliders 등)에 붙은 콜라이더가 " + + "카메라 가림 페이드(811 CameraFramingOverride)·투사체와 엉키지 않게 한다. 원본 프리팹은 무수정.")] + public bool propStripColliders = true; + + [Tooltip("랜덤 시드. 같은 시드 = 같은 배치(캡처 재현용).")] + public int propSeed = 815; + + // ─────────────────────────────────────────────────────── ⓒ 불꽃 링 + [Header("ⓒ 불꽃 링 (보유 불꽃 파티클)")] + [Tooltip("둘레에 놓을 불꽃 프리팹 목록. 기본 = LMHPOLY Fire_Particles(실측 3 PS · 정상상태 125 파티클).")] + public GameObject[] firePrefabs = new GameObject[0]; + + [Tooltip("불꽃 간격(m).")] + public float fireSpacing = 3f; + + [Tooltip("불꽃 개수 상한.")] + public int maxFires = 48; + + [Tooltip("불꽃 스케일.")] + public float fireScale = 1f; + + [Tooltip("🔴 링 전체의 파티클 총량 상한. 811s EffectScaleGuard/811q 예산과 부딪히지 않게 이 선에서 자른다. " + + "실측 = Fire_Particles 1개당 정상상태 125 → 상한 1200 이면 인스턴스 29개에서 방출률 ×0.33.")] + public int maxTotalParticles = 1200; + + [Tooltip("총량 상한을 지키는 방법. ReduceRate(0) = 개수 유지 · 방출률 축소(권장) · ReduceCount(1) = 개수 축소.")] + public FireBudgetMode fireBudgetMode = FireBudgetMode.ReduceRate; + + [Tooltip("불꽃 색(파티클 startColor · 0 이면 프리팹 원색).")] + public bool fireTintEnabled; + + [Tooltip("불꽃 색.")] + public Color fireColor = new Color(1f, 0.45f, 0.1f, 1f); + + [Tooltip("1 이면 Raycast 로 지면 높이에 스냅한다.")] + public bool fireGroundSnap = true; + } +} diff --git a/Assets/WL/Combat/Stage/Barrier/WLStageBarrierSettings.cs.meta b/Assets/WL/Combat/Stage/Barrier/WLStageBarrierSettings.cs.meta new file mode 100644 index 000000000..56b15d0ad --- /dev/null +++ b/Assets/WL/Combat/Stage/Barrier/WLStageBarrierSettings.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3c3dc9bd8de148744b88ce512ce0f09a \ No newline at end of file