Merge branch 'wl/systems/WL-813t3-sight-boss-hills'
This commit is contained in:
commit
2d3bdaa5ce
|
|
@ -0,0 +1,20 @@
|
|||
===== WL-813t2 베이크 APPLY(자산 갱신) · 2026-09-11 01:25:46 =====
|
||||
현행 자산 = Assets/WL/Settings/WL_Nature_NavMesh.asset · 550144 bytes · 로드 OK
|
||||
[현행(커밋본)] 삼각형 5317 · 정점 11909 · 총 면적 55621.4 m^2 · y -4.62 ~ 2.96
|
||||
[현행(커밋본)] y>1.5 924.5 · y>2.0 110.6 · y>2.5 7.5 · **y>3.0 0.0** · y>3.5 0.0 · y>5.0 0.0 m^2
|
||||
인스턴스 root transform pos=(0.000, 0.000, 0.000) rot=(0.0, 0.0, 0.0) scale=(1.000, 1.000, 1.000)
|
||||
NavMeshModifierVolume 1개
|
||||
[0] WL_Nature/WL813t2_HillNotWalkable · area=1(Not Walkable) · center(world)=(115.0, 24.0, 10.0) size=(260.0, 42.0, 260.0) · Y[3.00, 45.00] · active=True enabled=True · affectsAgent0=True layer=0
|
||||
베이크 0.1 s · 결과 OK
|
||||
[재베이크] 삼각형 5604 · 정점 12632 · 총 면적 57852.7 m^2 · y -3.04 ~ 2.96
|
||||
[재베이크] y>1.5 1572.9 · y>2.0 322.4 · y>2.5 35.3 · **y>3.0 0.0** · y>3.5 0.0 · y>5.0 0.0 m^2
|
||||
[재베이크] 시작→스포너 23 : PathComplete 23 · Partial/Invalid 0 · 밖 0
|
||||
[재베이크] 아레나 (139.0, 0.8, -20.0) → (139.00, 0.81, -20.00) · 시작→아레나 PathComplete
|
||||
[재베이크] 아레나 원주 12점 → 중심 PathComplete 8/12
|
||||
[재베이크] 앵커 Portal_Start (115.00, 0.00, 9.98) → (115.00, 0.11, 9.98)
|
||||
[재베이크] 앵커 Reincarnation_Start (115.00, 0.00, 9.98) → (115.00, 0.11, 9.98)
|
||||
[재베이크] 스폰 표집 1000점 · **y>3.0 0** · NavMesh 밖 0 · 원점 폴백 0
|
||||
y 분포: [-3]=1 [-2]=54 [-1]=70 [0]=700 [1]=94 [2]=81
|
||||
자산 저장 완료 · 550144 → 602396 bytes (52252)
|
||||
GUID = ef300913c80dbda47b476b9261fdd786 (기대 ef300913c80dbda47b476b9261fdd786)
|
||||
씬 정리 완료(저장 0) · scene=
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
===== WL-813t3 실측 후(슬로프 30 · 재베이크) · 2026-09-11 01:29:53 =====
|
||||
|
||||
── ① 시야·추적 경로 (표 1) ──────────────────────────────────────────
|
||||
table_GlobalValue 미로드(NullReferenceException) — JSON 직접 읽기로 대체
|
||||
원본 탐색 거리 table_GlobalValue.f_PC_FindEnemyRange = 6.00 m (+ AttackRange_Up)
|
||||
원본 펫 탐색 거리 f_Pet_FindEnemyRange = 5.00 m (PC 무관)
|
||||
WLTargetingSettings 실값 — retargetSearchRange 3 · chaseRange 3 · chaseDropIgnoresLock False · stopChaseMoveOnDrop True · chaseCheckSeconds 0.1 · frontConeDeg 90 · immediateAttackMargin 0.2 · autoEngage True · pcAggroOnHit False
|
||||
WLRunGuideSettings 실값 — bossLockTarget False · bossHoldRadius 14 · bossPriorityEnabled True · eliteLockTarget True · eliteTargetRadius 3 · guideLeashRadius 14 · zoneArriveRadius 6 · zoneHoldRadius 18
|
||||
WLBossArenaSettings 실값 — bossChaseRange 20(MonsterList 10006 f_BaseChaseRange 를 등장 시 덮어쓴다) · arenaRadius 20 · useFieldBossPositionAsCenter False · arenaCenter (139.00, 0.80, -20.00)
|
||||
스킬 자동 타깃 — Actor.cs:718 `if (!isTarget) Set_Target(true)` = forcefind → MyActor.cs:126 `float.MaxValue`(무제한).
|
||||
· 자동 전투 경로는 여기 안 온다 — PCActor.cs:93 이 이미 `isTarget && dist <= f_SkillRange` 를 요구한다(실측).
|
||||
· 수동 스킬 버튼만 이 경로다 → 「스킬 사거리 유지」(발주서 제안) = 무변경.
|
||||
Actor.cs:187 `Get_EnemyActors(…, 100f)` — OnDestroy 에서 주변 적의 타깃 참조를 끊는 **정리 경로**(ForceDel). 시야와 무관(실측).
|
||||
ProjectileBase.cs:348 `Get_NearstEnemy(20 m)` · :301 `Check_TargetBoss(8 m)` — 발사체가 표적을 잃었을 때의 보정. 시야와 무관(무변경).
|
||||
|
||||
[3 m 과 적 사거리 대조 — 실측]
|
||||
PC 공격 사거리(ClassConfig f_AttackRange · 10501) = 1.50 m → MyActor 정지거리 = 그 값 + AttackRange_Up(+ 여유 0.2)
|
||||
몹 101(None) f_BaseATKRange 2.00 m · f_BaseChaseRange 30 m
|
||||
몹 102(None) f_BaseATKRange 4.00 m · f_BaseChaseRange 30 m 🔴 PC 시야 3 m 보다 길다
|
||||
몹 103(None) f_BaseATKRange 2.00 m · f_BaseChaseRange 30 m
|
||||
몹 104(None) f_BaseATKRange 4.00 m · f_BaseChaseRange 30 m 🔴 PC 시야 3 m 보다 길다
|
||||
몹 118(None) f_BaseATKRange 2.00 m · f_BaseChaseRange 15 m
|
||||
몹 168(None) f_BaseATKRange 2.00 m · f_BaseChaseRange 15 m
|
||||
몹 173(None) f_BaseATKRange 2.00 m · f_BaseChaseRange 15 m
|
||||
몹 1023(Elite) f_BaseATKRange 2.50 m · f_BaseChaseRange 15 m
|
||||
몹 10006(Boss) f_BaseATKRange 3.50 m · f_BaseChaseRange 50 m 🔴 PC 시야 3 m 보다 길다
|
||||
→ 🔴 사거리가 3 m 를 넘는 적(102·104 4 m · 10006 보스 3.5 m)은 **PC 시야 밖에서 때릴 수 있다**. Lead/PD 결정 대상.
|
||||
NavMesh 자산 Assets/WL/Settings/WL_Nature_NavMesh.asset · 602396 bytes · GUID ef300913c80dbda47b476b9261fdd786
|
||||
Agent0(Humanoid) 실측 — radius 0.5 · height 2 · **slope 30** · climb 0.75 · minRegionArea 2 · voxel 0.16667
|
||||
|
||||
── ② 보스 스폰 지점 (표 2) ──────────────────────────────────────────
|
||||
FieldBossData 1기
|
||||
spawnerId 813010 monsterId 10006 · 노드 (139.00, 0.80, -20.00) → NavMesh (139.00, 0.81, -20.00) (Δ0.01 m)
|
||||
SO arenaCenter = (139.00, 0.80, -20.00) · useFieldBossPositionAsCenter = False
|
||||
813t2 y 컷(maxWalkableY) = 3.50 m
|
||||
[평탄도] 아레나 중심 (139.00, 0.80, -20.00) · 반경 6 m · 격자 113점
|
||||
지면 y 0.24 ~ 5.21 (Δ 4.97 m) · 경사 평균 19.7° 최대 67.0° · 30° 초과 29점 · 지면 없음 0
|
||||
NavMesh 위 113/113 · y 0.33 ~ 1.57 · y 컷 초과 0
|
||||
[평탄도] 보스 스포너 노드 (139.00, 0.80, -20.00) · 반경 6 m · 격자 113점
|
||||
지면 y 0.24 ~ 5.21 (Δ 4.97 m) · 경사 평균 19.7° 최대 67.0° · 30° 초과 29점 · 지면 없음 0
|
||||
NavMesh 위 113/113 · y 0.33 ~ 1.57 · y 컷 초과 0
|
||||
참고(813t2 실측) D-48 현장 = PC 가 올라갔던 언덕 (139, 3.66, -20) · (139, 5.48, -20) · (128, 8, -58) · (147, 6, 30)
|
||||
언덕 (139.00, 3.66, -20.00) → NavMesh 밖(1 m)
|
||||
언덕 (139.00, 5.48, -20.00) → NavMesh 밖(1 m)
|
||||
언덕 (128.00, 8.00, -58.00) → NavMesh 밖(1 m)
|
||||
언덕 (147.00, 6.00, 30.00) → NavMesh 밖(1 m)
|
||||
|
||||
── ②b 아레나 주변 평지 후보 탐색 (중심 139/-20 · ±20 m · 2 m 격자 · 각 후보 반경 6 m 평가) ──
|
||||
평가한 후보 441개
|
||||
중심 부근 (137.00, 0.93, -22.00) · 지면 Δy 6.13 m · 최대경사 67.0° · NavMesh 29/29 · 점수 18.96
|
||||
중심 부근 (137.01, 0.90, -19.17) · 지면 Δy 4.41 m · 최대경사 57.3° · NavMesh 29/29 · 점수 14.54
|
||||
중심 부근 (137.00, 0.87, -18.00) · 지면 Δy 4.05 m · 최대경사 67.0° · NavMesh 29/29 · 점수 14.79
|
||||
중심 부근 (138.33, 0.76, -22.71) · 지면 Δy 7.12 m · 최대경사 57.3° · NavMesh 29/29 · 점수 19.97
|
||||
중심 부근 (139.00, 0.81, -20.00) · 지면 Δy 4.97 m · 최대경사 67.0° · NavMesh 29/29 · 점수 16.63
|
||||
중심 부근 (139.00, 0.71, -18.00) · 지면 Δy 3.99 m · 최대경사 67.0° · NavMesh 29/29 · 점수 14.68
|
||||
중심 부근 (141.12, 0.80, -21.74) · 지면 Δy 5.43 m · 최대경사 67.0° · NavMesh 29/29 · 점수 17.56
|
||||
중심 부근 (141.00, 0.66, -20.00) · 지면 Δy 4.41 m · 최대경사 67.0° · NavMesh 29/29 · 점수 15.52
|
||||
중심 부근 (141.00, 0.55, -18.00) · 지면 Δy 3.83 m · 최대경사 67.0° · NavMesh 29/29 · 점수 14.36
|
||||
**가장 평평한 후보(±20 m) = (157.00, 0.04, 0.00) · 지면 Δy 0.12 m · 최대경사 3.3° · NavMesh 29/29 · 점수 0.57** (아레나 중심에서 26.9 m)
|
||||
|
||||
[넓은 평지 후보 — 반경 20 m 전체 평가 · ±60 m · 4 m 격자]
|
||||
후보(NavMesh 90 % 이상) 831개 · 상위 8
|
||||
| 중심 | 지면 Δy | 최대경사 | 30° 초과 | NavMesh | NavMesh Δy | 아레나에서 |
|
||||
| (179.00, 0.01, 4.00) | 0.47 m | 8.9° | 0/81 | 81/81 | 0.90 m | 46.7 m |
|
||||
| (179.00, -0.05, 8.00) | 0.63 m | 10.0° | 0/81 | 81/81 | 0.90 m | 48.8 m |
|
||||
| (175.00, 0.07, 4.00) | 0.67 m | 8.9° | 0/81 | 81/81 | 0.90 m | 43.3 m |
|
||||
| (187.00, -0.12, 16.00) | 0.73 m | 10.0° | 0/81 | 73/81 | 0.74 m | 60.0 m |
|
||||
| (183.00, -0.07, 16.00) | 0.82 m | 10.0° | 0/81 | 80/81 | 0.84 m | 56.9 m |
|
||||
| (179.00, -0.16, 12.00) | 0.85 m | 10.0° | 0/81 | 81/81 | 0.84 m | 51.2 m |
|
||||
| (187.00, -0.17, 20.00) | 0.99 m | 10.0° | 0/81 | 73/81 | 0.90 m | 62.5 m |
|
||||
| (175.00, -0.03, 8.00) | 1.00 m | 10.0° | 0/81 | 81/81 | 1.00 m | 45.6 m |
|
||||
(현재 아레나) (139.00, 0.81, -20.00) · 지면 Δy 15.58 m · 최대경사 84.1° · 30° 초과 23/81 · 순위 733/831
|
||||
|
||||
── ③ 언덕 격자 1000점 (표 3 · 813t2 D-48 클러스터 X[96,160] Z[-100,-16]) ──
|
||||
격자 1000점 · 지면 없음 0
|
||||
지면 y 대역 — [0,1) 375 · [1,3) 184 · y>=3 441
|
||||
지면 경사 30° 초과 254점 · 지면 y>3 441점
|
||||
**walkable 스냅(1 m) 498점** · 그 중 y>3 1 · 경사 30° 초과 지점 34
|
||||
walkable 최고 y = 2.79
|
||||
**언덕 옆면(지면 y 0~3 · 경사 30° 초과) 94점 · walkable(느슨 1 m) 34 · **walkable(엄격 0.35 m) 18**` · 느슨 walkable 중 최대 경사 67.0°
|
||||
**언덕 위(지면 y>3) 441점 · walkable(엄격) 0**
|
||||
|
||||
── ③b 계단·경사로 (표 4) ────────────────────────────────────────────
|
||||
🔴 이름에 stair/step/ramp/계단 이 든 노드 **0개** — WL_Nature 에 명시적 계단 프리팹은 없다(실측).
|
||||
|
||||
── ③c 경로 연결성 ───────────────────────────────────────────────────
|
||||
시작 (115.00, 0.00, 9.98) → 스냅 (115.00, 0.11, 9.98)
|
||||
**시작 → 스포너 23기 : PathComplete 23 · Partial/Invalid 0 · NavMesh 밖 0**
|
||||
시작 → 아레나 (139.00, 0.80, -20.00) 스냅 (139.00, 0.81, -20.00) = **PathComplete**
|
||||
아레나 원주 12점 → 중심 PathComplete 8/12
|
||||
씬 정리 완료(저장 0) · scene=
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
===== WL-813t3 실측 전(슬로프 45 · 커밋본) · 2026-09-11 01:24:49 =====
|
||||
|
||||
── ① 시야·추적 경로 (표 1) ──────────────────────────────────────────
|
||||
table_GlobalValue 미로드(NullReferenceException) — JSON 직접 읽기로 대체
|
||||
원본 탐색 거리 table_GlobalValue.f_PC_FindEnemyRange = 6.00 m (+ AttackRange_Up)
|
||||
원본 펫 탐색 거리 f_Pet_FindEnemyRange = 5.00 m (PC 무관)
|
||||
WLTargetingSettings 실값 — retargetSearchRange 3 · chaseRange 3 · chaseDropIgnoresLock False · stopChaseMoveOnDrop True · chaseCheckSeconds 0.1 · frontConeDeg 90 · immediateAttackMargin 0.2 · autoEngage True · pcAggroOnHit False
|
||||
WLRunGuideSettings 실값 — bossLockTarget False · bossHoldRadius 14 · bossPriorityEnabled True · eliteLockTarget True · eliteTargetRadius 3 · guideLeashRadius 14 · zoneArriveRadius 6 · zoneHoldRadius 18
|
||||
WLBossArenaSettings 실값 — bossChaseRange 20(MonsterList 10006 f_BaseChaseRange 를 등장 시 덮어쓴다) · arenaRadius 20 · useFieldBossPositionAsCenter False · arenaCenter (139.00, 0.80, -20.00)
|
||||
스킬 자동 타깃 — Actor.cs:718 `if (!isTarget) Set_Target(true)` = forcefind → MyActor.cs:126 `float.MaxValue`(무제한).
|
||||
· 자동 전투 경로는 여기 안 온다 — PCActor.cs:93 이 이미 `isTarget && dist <= f_SkillRange` 를 요구한다(실측).
|
||||
· 수동 스킬 버튼만 이 경로다 → 「스킬 사거리 유지」(발주서 제안) = 무변경.
|
||||
Actor.cs:187 `Get_EnemyActors(…, 100f)` — OnDestroy 에서 주변 적의 타깃 참조를 끊는 **정리 경로**(ForceDel). 시야와 무관(실측).
|
||||
ProjectileBase.cs:348 `Get_NearstEnemy(20 m)` · :301 `Check_TargetBoss(8 m)` — 발사체가 표적을 잃었을 때의 보정. 시야와 무관(무변경).
|
||||
|
||||
[3 m 과 적 사거리 대조 — 실측]
|
||||
PC 공격 사거리(ClassConfig f_AttackRange · 10501) = 1.50 m → MyActor 정지거리 = 그 값 + AttackRange_Up(+ 여유 0.2)
|
||||
몹 101(None) f_BaseATKRange 2.00 m · f_BaseChaseRange 30 m
|
||||
몹 102(None) f_BaseATKRange 4.00 m · f_BaseChaseRange 30 m 🔴 PC 시야 3 m 보다 길다
|
||||
몹 103(None) f_BaseATKRange 2.00 m · f_BaseChaseRange 30 m
|
||||
몹 104(None) f_BaseATKRange 4.00 m · f_BaseChaseRange 30 m 🔴 PC 시야 3 m 보다 길다
|
||||
몹 118(None) f_BaseATKRange 2.00 m · f_BaseChaseRange 15 m
|
||||
몹 168(None) f_BaseATKRange 2.00 m · f_BaseChaseRange 15 m
|
||||
몹 173(None) f_BaseATKRange 2.00 m · f_BaseChaseRange 15 m
|
||||
몹 1023(Elite) f_BaseATKRange 2.50 m · f_BaseChaseRange 15 m
|
||||
몹 10006(Boss) f_BaseATKRange 3.50 m · f_BaseChaseRange 50 m 🔴 PC 시야 3 m 보다 길다
|
||||
→ 🔴 사거리가 3 m 를 넘는 적(102·104 4 m · 10006 보스 3.5 m)은 **PC 시야 밖에서 때릴 수 있다**. Lead/PD 결정 대상.
|
||||
NavMesh 자산 Assets/WL/Settings/WL_Nature_NavMesh.asset · 593864 bytes · GUID ef300913c80dbda47b476b9261fdd786
|
||||
Agent0(Humanoid) 실측 — radius 0.5 · height 2 · **slope 30** · climb 0.75 · minRegionArea 2 · voxel 0.16667
|
||||
|
||||
── ② 보스 스폰 지점 (표 2) ──────────────────────────────────────────
|
||||
FieldBossData 1기
|
||||
spawnerId 813010 monsterId 10006 · 노드 (139.00, 0.80, -20.00) → NavMesh (139.00, 0.81, -20.00) (Δ0.01 m)
|
||||
SO arenaCenter = (139.00, 0.80, -20.00) · useFieldBossPositionAsCenter = False
|
||||
813t2 y 컷(maxWalkableY) = 3.50 m
|
||||
[평탄도] 아레나 중심 (139.00, 0.80, -20.00) · 반경 6 m · 격자 113점
|
||||
지면 y 0.24 ~ 5.21 (Δ 4.97 m) · 경사 평균 19.7° 최대 67.0° · 30° 초과 29점 · 지면 없음 0
|
||||
NavMesh 위 113/113 · y 0.33 ~ 1.98 · y 컷 초과 0
|
||||
[평탄도] 보스 스포너 노드 (139.00, 0.80, -20.00) · 반경 6 m · 격자 113점
|
||||
지면 y 0.24 ~ 5.21 (Δ 4.97 m) · 경사 평균 19.7° 최대 67.0° · 30° 초과 29점 · 지면 없음 0
|
||||
NavMesh 위 113/113 · y 0.33 ~ 1.98 · y 컷 초과 0
|
||||
참고(813t2 실측) D-48 현장 = PC 가 올라갔던 언덕 (139, 3.66, -20) · (139, 5.48, -20) · (128, 8, -58) · (147, 6, 30)
|
||||
언덕 (139.00, 3.66, -20.00) → NavMesh 밖(1 m)
|
||||
언덕 (139.00, 5.48, -20.00) → NavMesh 밖(1 m)
|
||||
언덕 (128.00, 8.00, -58.00) → NavMesh 밖(1 m)
|
||||
언덕 (147.00, 6.00, 30.00) → NavMesh 밖(1 m)
|
||||
|
||||
── ②b 아레나 주변 평지 후보 탐색 (중심 139/-20 · ±20 m · 2 m 격자 · 각 후보 반경 6 m 평가) ──
|
||||
평가한 후보 441개
|
||||
중심 부근 (137.00, 0.93, -22.00) · 지면 Δy 6.13 m · 최대경사 67.0° · NavMesh 29/29 · 점수 18.96
|
||||
중심 부근 (137.01, 0.87, -19.17) · 지면 Δy 4.41 m · 최대경사 57.3° · NavMesh 29/29 · 점수 14.54
|
||||
중심 부근 (137.00, 0.88, -18.00) · 지면 Δy 4.05 m · 최대경사 67.0° · NavMesh 29/29 · 점수 14.79
|
||||
중심 부근 (138.33, 0.76, -22.71) · 지면 Δy 7.12 m · 최대경사 57.3° · NavMesh 29/29 · 점수 19.97
|
||||
중심 부근 (139.00, 0.81, -20.00) · 지면 Δy 4.97 m · 최대경사 67.0° · NavMesh 29/29 · 점수 16.63
|
||||
중심 부근 (139.00, 0.71, -18.00) · 지면 Δy 3.99 m · 최대경사 67.0° · NavMesh 29/29 · 점수 14.68
|
||||
중심 부근 (141.12, 0.80, -21.74) · 지면 Δy 5.43 m · 최대경사 67.0° · NavMesh 29/29 · 점수 17.56
|
||||
중심 부근 (141.00, 0.66, -20.00) · 지면 Δy 4.41 m · 최대경사 67.0° · NavMesh 29/29 · 점수 15.52
|
||||
중심 부근 (141.00, 0.55, -18.00) · 지면 Δy 3.83 m · 최대경사 67.0° · NavMesh 29/29 · 점수 14.36
|
||||
**가장 평평한 후보(±20 m) = (157.00, 0.04, 0.00) · 지면 Δy 0.12 m · 최대경사 3.3° · NavMesh 29/29 · 점수 0.57** (아레나 중심에서 26.9 m)
|
||||
|
||||
[넓은 평지 후보 — 반경 20 m 전체 평가 · ±60 m · 4 m 격자]
|
||||
후보(NavMesh 90 % 이상) 851개 · 상위 8
|
||||
| 중심 | 지면 Δy | 최대경사 | 30° 초과 | NavMesh | NavMesh Δy | 아레나에서 |
|
||||
| (179.00, 0.01, 4.00) | 0.47 m | 8.9° | 0/81 | 81/81 | 0.90 m | 46.7 m |
|
||||
| (179.00, -0.05, 8.00) | 0.63 m | 10.0° | 0/81 | 81/81 | 0.90 m | 48.8 m |
|
||||
| (175.00, 0.07, 4.00) | 0.67 m | 8.9° | 0/81 | 81/81 | 0.90 m | 43.3 m |
|
||||
| (187.00, -0.12, 16.00) | 0.73 m | 10.0° | 0/81 | 73/81 | 0.74 m | 60.0 m |
|
||||
| (183.00, -0.07, 16.00) | 0.82 m | 10.0° | 0/81 | 80/81 | 0.84 m | 56.9 m |
|
||||
| (179.00, -0.16, 12.00) | 0.85 m | 10.0° | 0/81 | 81/81 | 0.84 m | 51.2 m |
|
||||
| (187.00, -0.17, 20.00) | 0.99 m | 10.0° | 0/81 | 73/81 | 0.90 m | 62.5 m |
|
||||
| (175.00, -0.03, 8.00) | 1.00 m | 10.0° | 0/81 | 81/81 | 1.00 m | 45.6 m |
|
||||
(현재 아레나) (139.00, 0.81, -20.00) · 지면 Δy 15.58 m · 최대경사 84.1° · 30° 초과 23/81 · 순위 746/851
|
||||
|
||||
── ③ 언덕 격자 1000점 (표 3 · 813t2 D-48 클러스터 X[96,160] Z[-100,-16]) ──
|
||||
격자 1000점 · 지면 없음 0
|
||||
지면 y 대역 — [0,1) 375 · [1,3) 184 · y>=3 441
|
||||
지면 경사 30° 초과 254점 · 지면 y>3 441점
|
||||
**walkable 스냅(1 m) 521점** · 그 중 y>3 4 · 경사 30° 초과 지점 54
|
||||
walkable 최고 y = 2.83
|
||||
**언덕 옆면(지면 y 0~3 · 경사 30° 초과) 94점 · walkable(느슨 1 m) 53 · **walkable(엄격 0.35 m) 33**` · 느슨 walkable 중 최대 경사 67.0°
|
||||
**언덕 위(지면 y>3) 441점 · walkable(엄격) 0**
|
||||
|
||||
── ③b 계단·경사로 (표 4) ────────────────────────────────────────────
|
||||
🔴 이름에 stair/step/ramp/계단 이 든 노드 **0개** — WL_Nature 에 명시적 계단 프리팹은 없다(실측).
|
||||
|
||||
── ③c 경로 연결성 ───────────────────────────────────────────────────
|
||||
시작 (115.00, 0.00, 9.98) → 스냅 (115.00, 0.11, 9.98)
|
||||
**시작 → 스포너 23기 : PathComplete 23 · Partial/Invalid 0 · NavMesh 밖 0**
|
||||
시작 → 아레나 (139.00, 0.80, -20.00) 스냅 (139.00, 0.81, -20.00) = **PathComplete**
|
||||
아레나 원주 12점 → 중심 PathComplete 8/12
|
||||
씬 정리 완료(저장 0) · scene=
|
||||
|
|
@ -0,0 +1,595 @@
|
|||
// 발주서 WL-813t3 — 실측 스크립트 (① 시야 경로 · ② 보스 스폰 지점 · ③ 언덕/계단)
|
||||
//
|
||||
// 실행 (CLAUDE.md §1: using 금지 · 네임스페이스 풀어 쓰기)
|
||||
// 전(현행 NavMesh · 슬로프 45) : unity command --project-path E:\NerdNavis\WL_wt\data run_script --file AgentScripts/WL813t3_Measure.cs --entry WL813t3_Measure.Before --timeout 1800
|
||||
// 후(재베이크 · 슬로프 30) : … --entry WL813t3_Measure.After --timeout 1800
|
||||
//
|
||||
// 🔴 임시 씬을 만들어 프리팹을 인스턴스화한다(저장 0). NavMesh 자산은 AddNavMeshData 로 잠깐 올렸다 내린다.
|
||||
|
||||
public static class WL813t3_Measure
|
||||
{
|
||||
const string kMapPrefab = "Assets/Res_Addr/Map/WL_Nature.prefab";
|
||||
const string kNavAsset = "Assets/WL/Settings/WL_Nature_NavMesh.asset";
|
||||
const string kOutDir = "AgentScripts/staging/WL813t3";
|
||||
|
||||
static System.Text.StringBuilder s;
|
||||
static void L(string t) { s.AppendLine(t); UnityEngine.Debug.Log("[WL813t3] " + t); }
|
||||
|
||||
static string Abs(string rel)
|
||||
{
|
||||
return System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(),
|
||||
rel.Replace('/', System.IO.Path.DirectorySeparatorChar));
|
||||
}
|
||||
|
||||
static string Flush(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
System.IO.Directory.CreateDirectory(Abs(kOutDir));
|
||||
System.IO.File.WriteAllText(Abs(path), s.ToString(), new System.Text.UTF8Encoding(true));
|
||||
}
|
||||
catch (System.Exception e) { UnityEngine.Debug.LogWarning("[WL813t3] 로그 저장 실패: " + e.Message); }
|
||||
return s.ToString();
|
||||
}
|
||||
|
||||
public static string Before() { return Run("전(슬로프 45 · 커밋본)", kOutDir + "/MEASURE_BEFORE.txt"); }
|
||||
public static string After() { return Run("후(슬로프 30 · 재베이크)", kOutDir + "/MEASURE_AFTER.txt"); }
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
static string Run(string tag, string logPath)
|
||||
{
|
||||
s = new System.Text.StringBuilder();
|
||||
L("===== WL-813t3 실측 " + tag + " · " + System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " =====");
|
||||
|
||||
Section1_Sight();
|
||||
|
||||
UnityEditor.AssetDatabase.ImportAsset(kNavAsset, UnityEditor.ImportAssetOptions.ForceUpdate);
|
||||
var data = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.AI.NavMeshData>(kNavAsset);
|
||||
if (data == null) { L("[에러] NavMesh 자산 로드 실패"); return Flush(logPath); }
|
||||
long bytes = 0; try { bytes = new System.IO.FileInfo(Abs(kNavAsset)).Length; } catch { }
|
||||
L("NavMesh 자산 " + kNavAsset + " · " + bytes + " bytes · GUID " + UnityEditor.AssetDatabase.AssetPathToGUID(kNavAsset));
|
||||
|
||||
// 베이크 설정(전역값) 실측
|
||||
var bs = UnityEngine.AI.NavMesh.GetSettingsByID(0);
|
||||
L("Agent0(Humanoid) 실측 — radius " + bs.agentRadius + " · height " + bs.agentHeight
|
||||
+ " · **slope " + bs.agentSlope + "** · climb " + bs.agentClimb + " · minRegionArea " + bs.minRegionArea
|
||||
+ " · voxel " + bs.voxelSize.ToString("F5"));
|
||||
|
||||
var prefab = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>(kMapPrefab);
|
||||
if (prefab == null) { L("[에러] 프리팹 로드 실패"); return Flush(logPath); }
|
||||
|
||||
var scene = UnityEditor.SceneManagement.EditorSceneManager.NewScene(
|
||||
UnityEditor.SceneManagement.NewSceneSetup.EmptyScene, UnityEditor.SceneManagement.NewSceneMode.Single);
|
||||
var go = UnityEditor.PrefabUtility.InstantiatePrefab(prefab) as UnityEngine.GameObject;
|
||||
if (go == null) { L("[에러] 인스턴스화 실패"); return Flush(logPath); }
|
||||
UnityEngine.Physics.SyncTransforms();
|
||||
|
||||
var inst = UnityEngine.AI.NavMesh.AddNavMeshData(data);
|
||||
try
|
||||
{
|
||||
Section2_BossSpawn(go);
|
||||
Section2b_FlatSearch();
|
||||
Section3_Hill();
|
||||
Section3b_Stairs(go);
|
||||
Section3c_Connectivity(go);
|
||||
}
|
||||
finally
|
||||
{
|
||||
UnityEngine.AI.NavMesh.RemoveNavMeshData(inst);
|
||||
UnityEngine.Object.DestroyImmediate(go);
|
||||
}
|
||||
L("씬 정리 완료(저장 0) · scene=" + scene.name);
|
||||
return Flush(logPath);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// ① 시야 · 추적 경로 전수
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
static void Section1_Sight()
|
||||
{
|
||||
L("");
|
||||
L("── ① 시야·추적 경로 (표 1) ──────────────────────────────────────────");
|
||||
float tableRange = -1f;
|
||||
try { tableRange = table_GlobalValue.Ins.Get_Float("f_PC_FindEnemyRange"); }
|
||||
catch (System.Exception e) { L(" table_GlobalValue 미로드(" + e.GetType().Name + ") — JSON 직접 읽기로 대체"); }
|
||||
if (tableRange < 0f) tableRange = ReadGlobalValueFromJson("f_PC_FindEnemyRange");
|
||||
L(" 원본 탐색 거리 table_GlobalValue.f_PC_FindEnemyRange = " + tableRange.ToString("F2") + " m (+ AttackRange_Up)");
|
||||
L(" 원본 펫 탐색 거리 f_Pet_FindEnemyRange = " + ReadGlobalValueFromJson("f_Pet_FindEnemyRange").ToString("F2") + " m (PC 무관)");
|
||||
|
||||
var t = UnityEngine.Resources.Load<WL.Settings.WLTargetingSettings>("WL/WLTargetingSettings");
|
||||
if (t == null) L(" [에러] WLTargetingSettings 에셋 로드 실패");
|
||||
else
|
||||
L(" WLTargetingSettings 실값 — retargetSearchRange " + t.retargetSearchRange
|
||||
+ " · chaseRange " + t.chaseRange + " · chaseDropIgnoresLock " + t.chaseDropIgnoresLock
|
||||
+ " · stopChaseMoveOnDrop " + t.stopChaseMoveOnDrop + " · chaseCheckSeconds " + t.chaseCheckSeconds
|
||||
+ " · frontConeDeg " + t.frontConeDeg + " · immediateAttackMargin " + t.immediateAttackMargin
|
||||
+ " · autoEngage " + t.autoEngage + " · pcAggroOnHit " + t.pcAggroOnHit);
|
||||
|
||||
var g = UnityEngine.Resources.Load<WL.Combat.Run.WLRunGuideSettings>("WL/WLRunGuideSettings");
|
||||
if (g == null) L(" [에러] WLRunGuideSettings 에셋 로드 실패");
|
||||
else
|
||||
L(" WLRunGuideSettings 실값 — bossLockTarget " + g.bossLockTarget + " · bossHoldRadius " + g.bossHoldRadius
|
||||
+ " · bossPriorityEnabled " + g.bossPriorityEnabled + " · eliteLockTarget " + g.eliteLockTarget
|
||||
+ " · eliteTargetRadius " + g.eliteTargetRadius + " · guideLeashRadius " + g.guideLeashRadius
|
||||
+ " · zoneArriveRadius " + g.zoneArriveRadius + " · zoneHoldRadius " + g.zoneHoldRadius);
|
||||
|
||||
var b = UnityEngine.Resources.Load<WL.Combat.Boss.WLBossArenaSettings>("WL/WLBossArenaSettings");
|
||||
if (b != null)
|
||||
L(" WLBossArenaSettings 실값 — bossChaseRange " + b.bossChaseRange + "(MonsterList 10006 f_BaseChaseRange 를 등장 시 덮어쓴다) · arenaRadius " + b.arenaRadius
|
||||
+ " · useFieldBossPositionAsCenter " + b.useFieldBossPositionAsCenter + " · arenaCenter " + b.arenaCenter.ToString("F2"));
|
||||
|
||||
L(" 스킬 자동 타깃 — Actor.cs:718 `if (!isTarget) Set_Target(true)` = forcefind → MyActor.cs:126 `float.MaxValue`(무제한).");
|
||||
L(" · 자동 전투 경로는 여기 안 온다 — PCActor.cs:93 이 이미 `isTarget && dist <= f_SkillRange` 를 요구한다(실측).");
|
||||
L(" · 수동 스킬 버튼만 이 경로다 → 「스킬 사거리 유지」(발주서 제안) = 무변경.");
|
||||
L(" Actor.cs:187 `Get_EnemyActors(…, 100f)` — OnDestroy 에서 주변 적의 타깃 참조를 끊는 **정리 경로**(ForceDel). 시야와 무관(실측).");
|
||||
L(" ProjectileBase.cs:348 `Get_NearstEnemy(20 m)` · :301 `Check_TargetBoss(8 m)` — 발사체가 표적을 잃었을 때의 보정. 시야와 무관(무변경).");
|
||||
|
||||
// 3 m 가 적 사거리보다 짧은지 — 실측
|
||||
L("");
|
||||
L(" [3 m 과 적 사거리 대조 — 실측]");
|
||||
L(" PC 공격 사거리(ClassConfig f_AttackRange · 10501) = " + ReadClassAttackRange(10501).ToString("F2")
|
||||
+ " m → MyActor 정지거리 = 그 값 + AttackRange_Up(+ 여유 " + (t != null ? t.immediateAttackMargin : 0.2f) + ")");
|
||||
int[] mobs = { 101, 102, 103, 104, 118, 168, 173, 1023, 10006 };
|
||||
for (int i = 0; i < mobs.Length; i++)
|
||||
{
|
||||
float atk, chase; string type;
|
||||
if (!ReadMonster(mobs[i], out atk, out chase, out type)) continue;
|
||||
L(" 몹 " + mobs[i] + "(" + type + ") f_BaseATKRange " + atk.ToString("F2") + " m · f_BaseChaseRange " + chase.ToString("F0") + " m"
|
||||
+ (atk > 3f ? " 🔴 PC 시야 3 m 보다 길다" : ""));
|
||||
}
|
||||
L(" → 🔴 사거리가 3 m 를 넘는 적(102·104 4 m · 10006 보스 3.5 m)은 **PC 시야 밖에서 때릴 수 있다**. Lead/PD 결정 대상.");
|
||||
}
|
||||
|
||||
static float ReadClassAttackRange(int classId)
|
||||
{
|
||||
try
|
||||
{
|
||||
string txt = System.IO.File.ReadAllText(Abs("Assets/ResWork/Table/Export/ClassConfig.json"));
|
||||
int i = txt.IndexOf("\"n_ClassID\": \"" + classId + "\"");
|
||||
if (i < 0) return -1f;
|
||||
int j = txt.IndexOf("\"f_AttackRange\": \"", i);
|
||||
if (j < 0) return -1f;
|
||||
j += 18;
|
||||
int k = txt.IndexOf('"', j);
|
||||
float v;
|
||||
if (float.TryParse(txt.Substring(j, k - j), System.Globalization.NumberStyles.Float,
|
||||
System.Globalization.CultureInfo.InvariantCulture, out v)) return v;
|
||||
}
|
||||
catch { }
|
||||
return -1f;
|
||||
}
|
||||
|
||||
static bool ReadMonster(int id, out float atkRange, out float chaseRange, out string type)
|
||||
{
|
||||
atkRange = chaseRange = -1f; type = "?";
|
||||
try
|
||||
{
|
||||
string txt = System.IO.File.ReadAllText(Abs("Assets/ResWork/Table/Export/MonsterList.json"));
|
||||
int i = txt.IndexOf("\"n_MonsterID\": \"" + id + "\"");
|
||||
if (i < 0) return false;
|
||||
int end = txt.IndexOf('}', i); if (end < 0) end = txt.Length;
|
||||
string row = txt.Substring(i, end - i);
|
||||
atkRange = Field(row, "f_BaseATKRange");
|
||||
chaseRange = Field(row, "f_BaseChaseRange");
|
||||
int t = row.IndexOf("\"e_MonsterType\": \"");
|
||||
if (t >= 0) { t += 18; int u = row.IndexOf('"', t); type = row.Substring(t, u - t); }
|
||||
return true;
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
static float Field(string row, string key)
|
||||
{
|
||||
int j = row.IndexOf("\"" + key + "\": \"");
|
||||
if (j < 0) return -1f;
|
||||
j += key.Length + 5;
|
||||
int k = row.IndexOf('"', j);
|
||||
float v;
|
||||
if (float.TryParse(row.Substring(j, k - j), System.Globalization.NumberStyles.Float,
|
||||
System.Globalization.CultureInfo.InvariantCulture, out v)) return v;
|
||||
return -1f;
|
||||
}
|
||||
|
||||
static float ReadGlobalValueFromJson(string id)
|
||||
{
|
||||
try
|
||||
{
|
||||
string txt = System.IO.File.ReadAllText(Abs("Assets/ResWork/Table/Export/GlobalValue.json"));
|
||||
int i = txt.IndexOf("\"s_ID\": \"" + id + "\"");
|
||||
if (i < 0) return -1f;
|
||||
int j = txt.IndexOf("\"n_Value\": \"", i);
|
||||
if (j < 0) return -1f;
|
||||
j += 12;
|
||||
int k = txt.IndexOf('"', j);
|
||||
float v;
|
||||
if (float.TryParse(txt.Substring(j, k - j), System.Globalization.NumberStyles.Float,
|
||||
System.Globalization.CultureInfo.InvariantCulture, out v)) return v;
|
||||
}
|
||||
catch { }
|
||||
return -1f;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// ② 보스 스폰 지점
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
static void Section2_BossSpawn(UnityEngine.GameObject root)
|
||||
{
|
||||
L("");
|
||||
L("── ② 보스 스폰 지점 (표 2) ──────────────────────────────────────────");
|
||||
|
||||
var fbds = root.GetComponentsInChildren<FieldBossData>(true);
|
||||
L(" FieldBossData " + fbds.Length + "기");
|
||||
for (int i = 0; i < fbds.Length; i++)
|
||||
{
|
||||
var p = fbds[i].transform.position;
|
||||
UnityEngine.AI.NavMeshHit h;
|
||||
bool on = UnityEngine.AI.NavMesh.SamplePosition(p, out h, 2f, UnityEngine.AI.NavMesh.AllAreas);
|
||||
L(" spawnerId " + fbds[i].n_SpawnerId + " monsterId " + fbds[i].n_MonsterID
|
||||
+ " · 노드 " + p.ToString("F2") + " → " + (on ? "NavMesh " + h.position.ToString("F2") + " (Δ" + UnityEngine.Vector3.Distance(p, h.position).ToString("F2") + " m)" : "NavMesh 밖(2 m)"));
|
||||
}
|
||||
|
||||
var cfg = UnityEngine.Resources.Load<WL.Combat.Boss.WLBossArenaSettings>("WL/WLBossArenaSettings");
|
||||
UnityEngine.Vector3 arena = cfg != null ? cfg.arenaCenter : new UnityEngine.Vector3(139f, 0.8f, -20f);
|
||||
L(" SO arenaCenter = " + arena.ToString("F2") + " · useFieldBossPositionAsCenter = " + (cfg != null ? cfg.useFieldBossPositionAsCenter.ToString() : "?"));
|
||||
|
||||
var tg = UnityEngine.Resources.Load<WL.Combat.Nav.WLTerrainGuardSettings>("WL/WLTerrainGuardSettings");
|
||||
float cut = tg != null ? tg.maxWalkableY : 3.5f;
|
||||
L(" 813t2 y 컷(maxWalkableY) = " + cut.ToString("F2") + " m");
|
||||
|
||||
Flatness("아레나 중심", arena, cut);
|
||||
Flatness("보스 스포너 노드", fbds.Length > 0 ? fbds[0].transform.position : arena, cut);
|
||||
|
||||
// 「현재 언덕 좌표」 비교용 — 813t2 D-48 실측 좌표
|
||||
L(" 참고(813t2 실측) D-48 현장 = PC 가 올라갔던 언덕 (139, 3.66, -20) · (139, 5.48, -20) · (128, 8, -58) · (147, 6, 30)");
|
||||
var hillProbe = new UnityEngine.Vector3[] {
|
||||
new UnityEngine.Vector3(139f, 3.66f, -20f), new UnityEngine.Vector3(139f, 5.48f, -20f),
|
||||
new UnityEngine.Vector3(128f, 8f, -58f), new UnityEngine.Vector3(147f, 6f, 30f) };
|
||||
for (int i = 0; i < hillProbe.Length; i++)
|
||||
{
|
||||
UnityEngine.AI.NavMeshHit h;
|
||||
bool on = UnityEngine.AI.NavMesh.SamplePosition(hillProbe[i], out h, 1f, UnityEngine.AI.NavMesh.AllAreas);
|
||||
L(" 언덕 " + hillProbe[i].ToString("F2") + " → " + (on ? "walkable " + h.position.ToString("F2") : "NavMesh 밖(1 m)"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>아레나 주변에서 「가장 평평하고 NavMesh 가 꽉 찬」 후보 지점을 찾는다(PD 「넓은 평지 중 한 장소」).</summary>
|
||||
static void Section2b_FlatSearch()
|
||||
{
|
||||
L("");
|
||||
L("── ②b 아레나 주변 평지 후보 탐색 (중심 139/-20 · ±20 m · 2 m 격자 · 각 후보 반경 6 m 평가) ──");
|
||||
var best = new System.Collections.Generic.List<string>();
|
||||
float bestScore = 1e9f; UnityEngine.Vector3 bestP = UnityEngine.Vector3.zero;
|
||||
string bestLine = "없음";
|
||||
int evaluated = 0;
|
||||
for (int i = -10; i <= 10; i++)
|
||||
for (int j = -10; j <= 10; j++)
|
||||
{
|
||||
var c = new UnityEngine.Vector3(139f + i * 2f, 0.8f, -20f + j * 2f);
|
||||
UnityEngine.AI.NavMeshHit ch;
|
||||
if (!UnityEngine.AI.NavMesh.SamplePosition(c, out ch, 2f, UnityEngine.AI.NavMesh.AllAreas)) continue;
|
||||
evaluated++;
|
||||
int n = 0, on = 0; float ymin = 1e9f, ymax = -1e9f, smax = 0f;
|
||||
for (int a = -3; a <= 3; a++)
|
||||
for (int b = -3; b <= 3; b++)
|
||||
{
|
||||
float dx = a * 2f, dz = b * 2f;
|
||||
if (dx * dx + dz * dz > 36f) continue;
|
||||
n++;
|
||||
var p = new UnityEngine.Vector3(ch.position.x + dx, ch.position.y, ch.position.z + dz);
|
||||
UnityEngine.RaycastHit rh;
|
||||
if (UnityEngine.Physics.Raycast(new UnityEngine.Vector3(p.x, p.y + 60f, p.z), UnityEngine.Vector3.down, out rh, 200f))
|
||||
{
|
||||
if (rh.point.y < ymin) ymin = rh.point.y;
|
||||
if (rh.point.y > ymax) ymax = rh.point.y;
|
||||
float ang = UnityEngine.Vector3.Angle(rh.normal, UnityEngine.Vector3.up);
|
||||
if (ang > smax) smax = ang;
|
||||
}
|
||||
UnityEngine.AI.NavMeshHit h2;
|
||||
if (UnityEngine.AI.NavMesh.SamplePosition(p, out h2, 1.5f, UnityEngine.AI.NavMesh.AllAreas)) on++;
|
||||
}
|
||||
if (n == 0) continue;
|
||||
float cover = (float)on / n;
|
||||
float span = (ymax > -1e8f) ? (ymax - ymin) : 99f;
|
||||
float score = span * 2f + smax * 0.1f + (1f - cover) * 20f;
|
||||
string line = ch.position.ToString("F2") + " · 지면 Δy " + span.ToString("F2") + " m · 최대경사 " + smax.ToString("F1")
|
||||
+ "° · NavMesh " + on + "/" + n + " · 점수 " + score.ToString("F2");
|
||||
if (score < bestScore) { bestScore = score; bestP = ch.position; bestLine = line; }
|
||||
if (UnityEngine.Mathf.Abs(i) <= 1 && UnityEngine.Mathf.Abs(j) <= 1) best.Add(" 중심 부근 " + line);
|
||||
}
|
||||
L(" 평가한 후보 " + evaluated + "개");
|
||||
for (int i = 0; i < best.Count; i++) L(best[i]);
|
||||
L(" **가장 평평한 후보(±20 m) = " + bestLine + "** (아레나 중심에서 "
|
||||
+ UnityEngine.Vector3.Distance(bestP, new UnityEngine.Vector3(139f, 0.8f, -20f)).ToString("F1") + " m)");
|
||||
|
||||
WideFlatSearch();
|
||||
}
|
||||
|
||||
/// <summary>넓은 범위(±60 m · 4 m 격자)에서 「아레나 반경 20 m 가 통째로 평지인」 후보 상위 8개.</summary>
|
||||
static void WideFlatSearch()
|
||||
{
|
||||
L("");
|
||||
L(" [넓은 평지 후보 — 반경 20 m 전체 평가 · ±60 m · 4 m 격자]");
|
||||
var list = new System.Collections.Generic.List<Cand>();
|
||||
for (int i = -15; i <= 15; i++)
|
||||
for (int j = -15; j <= 15; j++)
|
||||
{
|
||||
var c = new UnityEngine.Vector3(139f + i * 4f, 0.8f, -20f + j * 4f);
|
||||
UnityEngine.AI.NavMeshHit ch;
|
||||
if (!UnityEngine.AI.NavMesh.SamplePosition(c, out ch, 2f, UnityEngine.AI.NavMesh.AllAreas)) continue;
|
||||
int n = 0, on = 0, steep = 0; float ymin = 1e9f, ymax = -1e9f, smax = 0f, myMin = 1e9f, myMax = -1e9f;
|
||||
for (int a = -5; a <= 5; a++)
|
||||
for (int b = -5; b <= 5; b++)
|
||||
{
|
||||
float dx = a * 4f, dz = b * 4f;
|
||||
if (dx * dx + dz * dz > 400f) continue;
|
||||
n++;
|
||||
var p = new UnityEngine.Vector3(ch.position.x + dx, ch.position.y, ch.position.z + dz);
|
||||
UnityEngine.RaycastHit rh;
|
||||
if (UnityEngine.Physics.Raycast(new UnityEngine.Vector3(p.x, p.y + 80f, p.z), UnityEngine.Vector3.down, out rh, 240f))
|
||||
{
|
||||
if (rh.point.y < ymin) ymin = rh.point.y;
|
||||
if (rh.point.y > ymax) ymax = rh.point.y;
|
||||
float ang = UnityEngine.Vector3.Angle(rh.normal, UnityEngine.Vector3.up);
|
||||
if (ang > smax) smax = ang;
|
||||
if (ang > 30f) steep++;
|
||||
}
|
||||
UnityEngine.AI.NavMeshHit h2;
|
||||
if (UnityEngine.AI.NavMesh.SamplePosition(p, out h2, 2f, UnityEngine.AI.NavMesh.AllAreas))
|
||||
{ on++; if (h2.position.y < myMin) myMin = h2.position.y; if (h2.position.y > myMax) myMax = h2.position.y; }
|
||||
}
|
||||
if (n == 0 || on < n * 0.9f) continue; // NavMesh 가 90 % 미만이면 아레나로 못 쓴다
|
||||
float span = (ymax > -1e8f) ? (ymax - ymin) : 99f;
|
||||
list.Add(new Cand { p = ch.position, span = span, smax = smax, steep = steep, n = n, on = on, mySpan = myMax - myMin });
|
||||
}
|
||||
list.Sort(delegate (Cand a, Cand b) { return (a.span * 2f + a.steep).CompareTo(b.span * 2f + b.steep); });
|
||||
L(" 후보(NavMesh 90 % 이상) " + list.Count + "개 · 상위 8");
|
||||
L(" | 중심 | 지면 Δy | 최대경사 | 30° 초과 | NavMesh | NavMesh Δy | 아레나에서 |");
|
||||
for (int i = 0; i < list.Count && i < 8; i++)
|
||||
{
|
||||
var c = list[i];
|
||||
L(" | " + c.p.ToString("F2") + " | " + c.span.ToString("F2") + " m | " + c.smax.ToString("F1") + "° | "
|
||||
+ c.steep + "/" + c.n + " | " + c.on + "/" + c.n + " | " + c.mySpan.ToString("F2") + " m | "
|
||||
+ UnityEngine.Vector3.Distance(c.p, new UnityEngine.Vector3(139f, 0.8f, -20f)).ToString("F1") + " m |");
|
||||
}
|
||||
// 현재 아레나도 같은 잣대로
|
||||
for (int i = 0; i < list.Count; i++)
|
||||
if (UnityEngine.Vector3.Distance(list[i].p, new UnityEngine.Vector3(139f, 0.8f, -20f)) < 2.5f)
|
||||
{ var c = list[i]; L(" (현재 아레나) " + c.p.ToString("F2") + " · 지면 Δy " + c.span.ToString("F2") + " m · 최대경사 " + c.smax.ToString("F1") + "° · 30° 초과 " + c.steep + "/" + c.n + " · 순위 " + (i + 1) + "/" + list.Count); break; }
|
||||
}
|
||||
|
||||
struct Cand { public UnityEngine.Vector3 p; public float span, smax, mySpan; public int steep, n, on; }
|
||||
|
||||
/// <summary>지점 주변 반경 r 의 평탄도 — 지면 raycast y 범위 · NavMesh 스냅 y 범위 · 경사각 분포.</summary>
|
||||
static void Flatness(string label, UnityEngine.Vector3 center, float cut)
|
||||
{
|
||||
const float r = 6f;
|
||||
int n = 0, onMesh = 0, aboveCut = 0, steep = 0, noGround = 0;
|
||||
float gyMin = 1e9f, gyMax = -1e9f, myMin = 1e9f, myMax = -1e9f, slopeMax = 0f;
|
||||
double slopeSum = 0.0;
|
||||
for (int i = -6; i <= 6; i++)
|
||||
for (int j = -6; j <= 6; j++)
|
||||
{
|
||||
float dx = i * (r / 6f), dz = j * (r / 6f);
|
||||
if (dx * dx + dz * dz > r * r) continue;
|
||||
n++;
|
||||
var p = new UnityEngine.Vector3(center.x + dx, center.y, center.z + dz);
|
||||
UnityEngine.RaycastHit rh;
|
||||
if (UnityEngine.Physics.Raycast(new UnityEngine.Vector3(p.x, center.y + 60f, p.z), UnityEngine.Vector3.down, out rh, 200f))
|
||||
{
|
||||
if (rh.point.y < gyMin) gyMin = rh.point.y;
|
||||
if (rh.point.y > gyMax) gyMax = rh.point.y;
|
||||
float ang = UnityEngine.Vector3.Angle(rh.normal, UnityEngine.Vector3.up);
|
||||
slopeSum += ang; if (ang > slopeMax) slopeMax = ang;
|
||||
if (ang > 30f) steep++;
|
||||
}
|
||||
else noGround++;
|
||||
UnityEngine.AI.NavMeshHit h;
|
||||
if (UnityEngine.AI.NavMesh.SamplePosition(p, out h, 1.5f, UnityEngine.AI.NavMesh.AllAreas))
|
||||
{
|
||||
onMesh++;
|
||||
if (h.position.y < myMin) myMin = h.position.y;
|
||||
if (h.position.y > myMax) myMax = h.position.y;
|
||||
if (h.position.y > cut) aboveCut++;
|
||||
}
|
||||
}
|
||||
L(" [평탄도] " + label + " " + center.ToString("F2") + " · 반경 " + r + " m · 격자 " + n + "점");
|
||||
L(" 지면 y " + (gyMin > 1e8f ? "없음" : gyMin.ToString("F2") + " ~ " + gyMax.ToString("F2") + " (Δ " + (gyMax - gyMin).ToString("F2") + " m)")
|
||||
+ " · 경사 평균 " + (n > noGround ? (slopeSum / (n - noGround)).ToString("F1") : "?") + "° 최대 " + slopeMax.ToString("F1") + "° · 30° 초과 " + steep + "점 · 지면 없음 " + noGround);
|
||||
L(" NavMesh 위 " + onMesh + "/" + n + " · y " + (myMin > 1e8f ? "없음" : myMin.ToString("F2") + " ~ " + myMax.ToString("F2")) + " · y 컷 초과 " + aboveCut);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// ③ 언덕 격자 · 옆면 경사
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
static void Section3_Hill()
|
||||
{
|
||||
L("");
|
||||
L("── ③ 언덕 격자 1000점 (표 3 · 813t2 D-48 클러스터 X[96,160] Z[-100,-16]) ──");
|
||||
|
||||
// 32 x 32 = 1024 → 앞 1000점
|
||||
int want = 1000, taken = 0;
|
||||
int walkTotal = 0, walkAbove3 = 0, walkSteep = 0, steepTotal = 0, above3Total = 0, noGround = 0;
|
||||
int band01 = 0, band13 = 0, band3p = 0;
|
||||
float maxWalkY = -1e9f;
|
||||
for (int i = 0; i < 32 && taken < want; i++)
|
||||
for (int j = 0; j < 32 && taken < want; j++)
|
||||
{
|
||||
float x = 96f + (160f - 96f) * (i + 0.5f) / 32f;
|
||||
float z = -100f + (-16f + 100f) * (j + 0.5f) / 32f;
|
||||
taken++;
|
||||
UnityEngine.RaycastHit rh;
|
||||
if (!UnityEngine.Physics.Raycast(new UnityEngine.Vector3(x, 120f, z), UnityEngine.Vector3.down, out rh, 400f)) { noGround++; continue; }
|
||||
float gy = rh.point.y;
|
||||
float ang = UnityEngine.Vector3.Angle(rh.normal, UnityEngine.Vector3.up);
|
||||
bool steep = ang > 30f;
|
||||
bool above3 = gy > 3f;
|
||||
if (steep) steepTotal++;
|
||||
if (above3) above3Total++;
|
||||
if (gy < 1f) band01++; else if (gy < 3f) band13++; else band3p++;
|
||||
|
||||
UnityEngine.AI.NavMeshHit h;
|
||||
bool on = UnityEngine.AI.NavMesh.SamplePosition(rh.point, out h, 1f, UnityEngine.AI.NavMesh.AllAreas);
|
||||
if (!on) continue;
|
||||
walkTotal++;
|
||||
if (h.position.y > maxWalkY) maxWalkY = h.position.y;
|
||||
if (above3) walkAbove3++;
|
||||
if (steep) walkSteep++;
|
||||
}
|
||||
L(" 격자 " + taken + "점 · 지면 없음 " + noGround);
|
||||
L(" 지면 y 대역 — [0,1) " + band01 + " · [1,3) " + band13 + " · y>=3 " + band3p);
|
||||
L(" 지면 경사 30° 초과 " + steepTotal + "점 · 지면 y>3 " + above3Total + "점");
|
||||
L(" **walkable 스냅(1 m) " + walkTotal + "점** · 그 중 y>3 " + walkAbove3 + " · 경사 30° 초과 지점 " + walkSteep);
|
||||
L(" walkable 최고 y = " + (maxWalkY > -1e8f ? maxWalkY.ToString("F2") : "없음"));
|
||||
|
||||
// 옆면(y 0~3) 전용 — 경사 30° 초과인 지점만 다시 본다
|
||||
// 느슨(스냅 1 m · 옆 평지에 붙는 것 포함) / **엄격**(수평 0.35 m · |Δy| 0.5 m 안 = 그 지점 자체가 walkable)
|
||||
int sideN = 0, sideWalkLoose = 0, sideWalkStrict = 0;
|
||||
float sideMaxSlopeWalk = 0f;
|
||||
int hiN = 0, hiWalkStrict = 0; // 언덕 위(지면 y>3) 엄격
|
||||
for (int i = 0; i < 32; i++)
|
||||
for (int j = 0; j < 32; j++)
|
||||
{
|
||||
float x = 96f + (160f - 96f) * (i + 0.5f) / 32f;
|
||||
float z = -100f + (-16f + 100f) * (j + 0.5f) / 32f;
|
||||
UnityEngine.RaycastHit rh;
|
||||
if (!UnityEngine.Physics.Raycast(new UnityEngine.Vector3(x, 120f, z), UnityEngine.Vector3.down, out rh, 400f)) continue;
|
||||
float gy = rh.point.y;
|
||||
float ang = UnityEngine.Vector3.Angle(rh.normal, UnityEngine.Vector3.up);
|
||||
UnityEngine.AI.NavMeshHit h;
|
||||
bool on = UnityEngine.AI.NavMesh.SamplePosition(rh.point, out h, 1f, UnityEngine.AI.NavMesh.AllAreas);
|
||||
bool strict = false;
|
||||
if (on)
|
||||
{
|
||||
float ddx = h.position.x - rh.point.x, ddz = h.position.z - rh.point.z;
|
||||
strict = UnityEngine.Mathf.Sqrt(ddx * ddx + ddz * ddz) <= 0.35f
|
||||
&& UnityEngine.Mathf.Abs(h.position.y - gy) <= 0.5f;
|
||||
}
|
||||
if (gy > 3f) { hiN++; if (strict) hiWalkStrict++; continue; }
|
||||
if (gy < 0f || ang <= 30f) continue;
|
||||
sideN++;
|
||||
if (on) { sideWalkLoose++; if (ang > sideMaxSlopeWalk) sideMaxSlopeWalk = ang; }
|
||||
if (strict) sideWalkStrict++;
|
||||
}
|
||||
L(" **언덕 옆면(지면 y 0~3 · 경사 30° 초과) " + sideN + "점 · walkable(느슨 1 m) " + sideWalkLoose
|
||||
+ " · **walkable(엄격 0.35 m) " + sideWalkStrict + "**` · 느슨 walkable 중 최대 경사 " + sideMaxSlopeWalk.ToString("F1") + "°");
|
||||
L(" **언덕 위(지면 y>3) " + hiN + "점 · walkable(엄격) " + hiWalkStrict + "**");
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// ③b 계단 · 경사로 메시 실측
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
static void Section3b_Stairs(UnityEngine.GameObject root)
|
||||
{
|
||||
L("");
|
||||
L("── ③b 계단·경사로 (표 4) ────────────────────────────────────────────");
|
||||
string[] keys = { "stair", "step", "ramp", "slope", "계단", "ladder", "bridge" };
|
||||
var all = root.GetComponentsInChildren<UnityEngine.Transform>(true);
|
||||
int found = 0;
|
||||
for (int i = 0; i < all.Length; i++)
|
||||
{
|
||||
string n = all[i].name.ToLowerInvariant();
|
||||
bool hit = false;
|
||||
for (int k = 0; k < keys.Length; k++) if (n.Contains(keys[k])) { hit = true; break; }
|
||||
if (!hit) continue;
|
||||
var rend = all[i].GetComponentInChildren<UnityEngine.Renderer>(true);
|
||||
var col = all[i].GetComponentInChildren<UnityEngine.Collider>(true);
|
||||
var b = rend != null ? rend.bounds : (col != null ? col.bounds : new UnityEngine.Bounds(all[i].position, UnityEngine.Vector3.zero));
|
||||
found++;
|
||||
L(" [" + found + "] " + Path(all[i]) + " · pos " + all[i].position.ToString("F2")
|
||||
+ " · bounds c" + b.center.ToString("F2") + " sz" + b.size.ToString("F2")
|
||||
+ " · collider " + (col != null ? col.GetType().Name : "없음"));
|
||||
StairPath(all[i].name, b);
|
||||
}
|
||||
if (found == 0) L(" 🔴 이름에 stair/step/ramp/계단 이 든 노드 **0개** — WL_Nature 에 명시적 계단 프리팹은 없다(실측).");
|
||||
}
|
||||
|
||||
/// <summary>계단(또는 경사로) 바운즈의 아래쪽 끝 ↔ 위쪽 끝 사이 경로 연결성.</summary>
|
||||
static void StairPath(string name, UnityEngine.Bounds b)
|
||||
{
|
||||
var lo = new UnityEngine.Vector3(b.center.x, b.min.y + 0.5f, b.center.z);
|
||||
var hi = new UnityEngine.Vector3(b.center.x, b.max.y + 0.5f, b.center.z);
|
||||
// 긴 축을 따라 아래/위 끝을 잡는다
|
||||
if (b.size.x >= b.size.z)
|
||||
{ lo.x = b.min.x + 0.5f; hi.x = b.max.x - 0.5f; }
|
||||
else
|
||||
{ lo.z = b.min.z + 0.5f; hi.z = b.max.z - 0.5f; }
|
||||
UnityEngine.AI.NavMeshHit hl, hh;
|
||||
bool onl = UnityEngine.AI.NavMesh.SamplePosition(lo, out hl, 3f, UnityEngine.AI.NavMesh.AllAreas);
|
||||
bool onh = UnityEngine.AI.NavMesh.SamplePosition(hi, out hh, 3f, UnityEngine.AI.NavMesh.AllAreas);
|
||||
if (!onl || !onh) { L(" 상↔하 경로 = 판정 불가(하 " + (onl ? "on" : "밖") + " · 상 " + (onh ? "on" : "밖") + ")"); return; }
|
||||
var path = new UnityEngine.AI.NavMeshPath();
|
||||
UnityEngine.AI.NavMesh.CalculatePath(hl.position, hh.position, UnityEngine.AI.NavMesh.AllAreas, path);
|
||||
L(" 상↔하 경로 " + hl.position.ToString("F2") + " → " + hh.position.ToString("F2") + " = **" + path.status + "** · corners " + path.corners.Length);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// ③c 경로 연결성 (시작 → 스포너 23 · 아레나)
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
static void Section3c_Connectivity(UnityEngine.GameObject root)
|
||||
{
|
||||
L("");
|
||||
L("── ③c 경로 연결성 ───────────────────────────────────────────────────");
|
||||
var portal = FindByName(root.transform, "Portal_Start");
|
||||
UnityEngine.Vector3 startPos = portal != null ? portal.position : new UnityEngine.Vector3(115f, 0f, 10f);
|
||||
UnityEngine.AI.NavMeshHit sh;
|
||||
if (!UnityEngine.AI.NavMesh.SamplePosition(startPos, out sh, 5f, UnityEngine.AI.NavMesh.AllAreas))
|
||||
{ L(" [에러] 시작 지점이 NavMesh 밖 " + startPos.ToString("F2")); return; }
|
||||
L(" 시작 " + startPos.ToString("F2") + " → 스냅 " + sh.position.ToString("F2"));
|
||||
|
||||
var mgrs = root.GetComponentsInChildren<MobControlMgr>(true);
|
||||
int comp = 0, part = 0, none = 0;
|
||||
for (int i = 0; i < mgrs.Length; i++)
|
||||
{
|
||||
var p = mgrs[i].transform.position;
|
||||
UnityEngine.AI.NavMeshHit h;
|
||||
if (!UnityEngine.AI.NavMesh.SamplePosition(p, out h, 5f, UnityEngine.AI.NavMesh.AllAreas))
|
||||
{ none++; L(" #" + mgrs[i].SpawnerID + " NavMesh 밖 " + p.ToString("F2")); continue; }
|
||||
var path = new UnityEngine.AI.NavMeshPath();
|
||||
UnityEngine.AI.NavMesh.CalculatePath(sh.position, h.position, UnityEngine.AI.NavMesh.AllAreas, path);
|
||||
if (path.status == UnityEngine.AI.NavMeshPathStatus.PathComplete) comp++;
|
||||
else { part++; L(" #" + mgrs[i].SpawnerID + " " + path.status + " " + p.ToString("F2")); }
|
||||
}
|
||||
L(" **시작 → 스포너 " + mgrs.Length + "기 : PathComplete " + comp + " · Partial/Invalid " + part + " · NavMesh 밖 " + none + "**");
|
||||
|
||||
var cfg = UnityEngine.Resources.Load<WL.Combat.Boss.WLBossArenaSettings>("WL/WLBossArenaSettings");
|
||||
UnityEngine.Vector3 arena = cfg != null ? cfg.arenaCenter : new UnityEngine.Vector3(139f, 0.8f, -20f);
|
||||
UnityEngine.AI.NavMeshHit ah;
|
||||
if (UnityEngine.AI.NavMesh.SamplePosition(arena, out ah, 5f, UnityEngine.AI.NavMesh.AllAreas))
|
||||
{
|
||||
var p2 = new UnityEngine.AI.NavMeshPath();
|
||||
UnityEngine.AI.NavMesh.CalculatePath(sh.position, ah.position, UnityEngine.AI.NavMesh.AllAreas, p2);
|
||||
L(" 시작 → 아레나 " + arena.ToString("F2") + " 스냅 " + ah.position.ToString("F2") + " = **" + p2.status + "**");
|
||||
int okr = 0;
|
||||
for (int i = 0; i < 12; i++)
|
||||
{
|
||||
float ang = i * UnityEngine.Mathf.PI * 2f / 12f;
|
||||
var rp = arena + new UnityEngine.Vector3(UnityEngine.Mathf.Cos(ang), 0f, UnityEngine.Mathf.Sin(ang)) * (cfg != null ? cfg.arenaRadius * 0.8f : 16f);
|
||||
UnityEngine.AI.NavMeshHit rh2;
|
||||
if (!UnityEngine.AI.NavMesh.SamplePosition(rp, out rh2, 5f, UnityEngine.AI.NavMesh.AllAreas)) continue;
|
||||
var p3 = new UnityEngine.AI.NavMeshPath();
|
||||
UnityEngine.AI.NavMesh.CalculatePath(rh2.position, ah.position, UnityEngine.AI.NavMesh.AllAreas, p3);
|
||||
if (p3.status == UnityEngine.AI.NavMeshPathStatus.PathComplete) okr++;
|
||||
}
|
||||
L(" 아레나 원주 12점 → 중심 PathComplete " + okr + "/12");
|
||||
}
|
||||
else L(" 아레나 " + arena.ToString("F2") + " NavMesh 밖(5 m)");
|
||||
}
|
||||
|
||||
static string Path(UnityEngine.Transform t)
|
||||
{
|
||||
string p = t.name;
|
||||
while (t.parent != null) { t = t.parent; p = t.name + "/" + p; }
|
||||
return p;
|
||||
}
|
||||
|
||||
static UnityEngine.Transform FindByName(UnityEngine.Transform t, string name)
|
||||
{
|
||||
if (t.name == name) return t;
|
||||
for (int i = 0; i < t.childCount; i++)
|
||||
{
|
||||
var r = FindByName(t.GetChild(i), name);
|
||||
if (r != null) return r;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
===== WL-813t3 프로브 · 2026-09-11 01:27:33 =====
|
||||
에디트 모드 · Play 0 · 로그인 0 · 실 에셋은 읽기만 한다.
|
||||
|
||||
── ⓪ 준비
|
||||
[PASS] 가짜 메인 PC · MyValue.MyPC 등록
|
||||
[PASS] WLTargetingSettings 에셋 로드
|
||||
실값 retargetSearchRange=3 · chaseRange=3 · chaseDropIgnoresLock=False · stopChaseMoveOnDrop=True · chaseCheckSeconds=0.1
|
||||
[PASS] SightRangeGuard.Active (chaseRange > 0)
|
||||
PC Agent = 없음(에디트 모드 — 이동 정지 경로는 미확인)
|
||||
|
||||
── ① 시야 3 m 판정표 (SightRangeGuard.Decide · 순수 함수)
|
||||
| 거리 | 락 | ignoreLock | 판정 |
|
||||
[PASS] 0.0 m(붙어 있음) → 유지 (기대 유지)
|
||||
[PASS] 2.5 m → 유지 (기대 유지)
|
||||
[PASS] 3.0 m(경계 = 유지) → 유지 (기대 유지)
|
||||
[PASS] 3.01 m → 놓음 (기대 놓음)
|
||||
[PASS] 3.5 m → 놓음 (기대 놓음)
|
||||
[PASS] 14 m(813w 보스 홀드 반경) → 놓음 (기대 놓음)
|
||||
[PASS] 3.5 m · 락 · 락 존중 → 락유지 (기대 락유지)
|
||||
[PASS] 3.5 m · 락 · 락 무시 → 놓음 (기대 놓음)
|
||||
[PASS] chaseRange 0 = 축 off(C8) → 축off (기대 축off)
|
||||
|
||||
── ② 가짜 적으로 실호출 (SightRangeGuard.CheckOnce · 원본 Del_Target 경로)
|
||||
[PASS] 2.5 m 적 → 타깃 유지 (거리 실측 2.50 m)
|
||||
[PASS] 3.5 m 적 → **타깃 해제** (거리 실측 3.50 m · Dropped=1 · 마지막 __WL813t3_far)
|
||||
[PASS] 9 m 락 보스 → 유지(원본 락 존중 · LockKept=1) · SO `bossLockTarget` 을 0 으로 두면 813w 가 애초에 락을 걸지 않는다
|
||||
[PASS] chaseDropIgnoresLock=1 → 락 보스도 해제
|
||||
이동 정지 경로(MoveStopped=0 · MoveSkipped=2) — 🔴 에디트 모드엔 NavMeshAgent·애니메이터가 없어 **실이동 정지는 미확인**(§QA 로 확인).
|
||||
|
||||
── ③ 보스 평지 스폰 (SpawnPositionGuard.ResolveBossPin · 순수 함수)
|
||||
[PASS] WLTerrainGuardSettings 에셋 로드
|
||||
실값 bossSpawnPinEnabled=True · bossSpawnPinRadius=6 · bossSpawnPinTolerance=1 · maxWalkableY=3.5 · dwellSeconds=0.5
|
||||
BossArena.Center(에디트 모드 · 보류 목록 비어 있음 → SO arenaCenter) = (139.00, 0.80, -20.00)
|
||||
[PASS] WL_Nature_NavMesh.asset 로드·부착(삼각형 5604)
|
||||
[PASS] 언덕 위 가짜 보스 (139, 6.00, -20) → **옮김** (139.00, 0.81, -20.00) · y 컷(3.5) 이하 True
|
||||
[PASS] 아레나 제자리 (139.00, 0.80, -20.00) → 이미 제자리 (139.00, 0.81, -20.00)
|
||||
[PASS] D-48 언덕 (128, 8.00, -58) → **옮김** (139.00, 0.81, -20.00)
|
||||
[PASS] D-48 언덕 (147, 6.00, 30) → **옮김** (139.00, 0.81, -20.00)
|
||||
[PASS] C8: bossSpawnPinEnabled=0 → 손대지 않음(앵커없음(무개입))
|
||||
|
||||
── ④ C8 진리표 (값 원복 = 813t2 동작)
|
||||
[PASS] chaseRange=0 → Active=false · 3.5 m 적 유지(= 813t3 이전 동작 100%)
|
||||
[PASS] RuntimeDisabled=true → Active=false
|
||||
[PASS] retargetSearchRange=0 → MyActor.Get_WLFindRange() 가 원본 f_FindEnemyRange(table 6 m + AttackRange_Up)로 되돌아간다
|
||||
[PASS] WLTerrainGuardSettings.enabled=0 → 813t2 러너·스폰 보정 전부 off(보스 고정 포함)
|
||||
[PASS] 실 에셋 복구 확인(프로브가 실 에셋을 쓰지 않았다)
|
||||
[PASS] WLBossArenaSettings.useFieldBossPositionAsCenter=0 → 아레나 중심이 **SO 값**(139, 0.8, -20)으로 고정 (1 로 되돌리면 스포너 노드 위치)
|
||||
[PASS] WLRunGuideSettings.bossLockTarget=0 → 813w 보스 락타깃 off(1 로 되돌리면 원상)
|
||||
|
||||
── ⑤ GC (틱 · 판정 반복)
|
||||
[PASS] CheckOnce ×1000 + ResolveBossPin ×1000 + Decide ×1000 → Δ 0 B
|
||||
|
||||
RESULT PASS
|
||||
|
|
@ -0,0 +1,330 @@
|
|||
// 발주서 WL-813t3 프로브 — 에디트 모드 · Play 0 · 로그인 0
|
||||
//
|
||||
// unity command --project-path E:\NerdNavis\WL_wt\data run_script --file AgentScripts/WL813t3_Probe.cs --entry WL813t3_Probe.Run --timeout 900
|
||||
//
|
||||
// ① 시야 3 m (가짜 적 2.5 / 3.5 m → 유지 / 놓음 · 락 · 이동 정지 경로)
|
||||
// ② 보스 평지 스폰 (ResolveBossPin 실호출 · 아레나 앵커 · y 컷)
|
||||
// ③ C8 진리표 (값 원복 = 813t2 동작) · GC
|
||||
//
|
||||
// 🔴 using 금지(CLAUDE.md §1) — 네임스페이스를 풀어 쓴다.
|
||||
|
||||
public static class WL813t3_Probe
|
||||
{
|
||||
const string kOut = "AgentScripts/staging/WL813t3/PROBE.txt";
|
||||
static System.Text.StringBuilder sb;
|
||||
static int s_fail;
|
||||
|
||||
static void L(string m) { sb.AppendLine(m); }
|
||||
static string P(bool ok) { if (!ok) s_fail++; return ok ? "[PASS]" : "[FAIL]"; }
|
||||
static string V(UnityEngine.Vector3 v) { return v.ToString("F2"); }
|
||||
|
||||
static UnityEngine.GameObject s_pcGo, s_nearGo, s_farGo, s_lockGo;
|
||||
static PCActor s_pc;
|
||||
static Actor s_near, s_far, s_lock;
|
||||
static PCActor s_prevPc;
|
||||
|
||||
public static string Run()
|
||||
{
|
||||
sb = new System.Text.StringBuilder();
|
||||
s_fail = 0;
|
||||
L("===== WL-813t3 프로브 · " + System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " =====");
|
||||
L("에디트 모드 · Play 0 · 로그인 0 · 실 에셋은 읽기만 한다.");
|
||||
|
||||
try
|
||||
{
|
||||
Setup();
|
||||
Sec1_Decide();
|
||||
Sec2_Live();
|
||||
Sec3_BossPin();
|
||||
Sec4_C8();
|
||||
Sec5_GC();
|
||||
}
|
||||
catch (System.Exception e) { L("[에러] " + e.GetType().Name + " — " + e.Message); L(e.StackTrace); s_fail++; }
|
||||
finally { Teardown(); }
|
||||
|
||||
L("");
|
||||
L("RESULT " + (s_fail == 0 ? "PASS" : "FAIL(" + s_fail + ")"));
|
||||
try
|
||||
{
|
||||
System.IO.Directory.CreateDirectory(System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), "AgentScripts/staging/WL813t3"));
|
||||
System.IO.File.WriteAllText(System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), kOut.Replace('/', System.IO.Path.DirectorySeparatorChar)),
|
||||
sb.ToString(), new System.Text.UTF8Encoding(true));
|
||||
}
|
||||
catch (System.Exception e) { UnityEngine.Debug.LogWarning("[WL813t3] 프로브 저장 실패: " + e.Message); }
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
static Actor MakeActor(string name, eRole role, eSubRol sub, UnityEngine.Vector3 pos, out UnityEngine.GameObject go)
|
||||
{
|
||||
go = new UnityEngine.GameObject("__WL813t3_" + name);
|
||||
go.transform.position = pos;
|
||||
var a = go.AddComponent<Actor>();
|
||||
a.m_Role = role; a.m_SubRole = sub;
|
||||
return a;
|
||||
}
|
||||
|
||||
static void Setup()
|
||||
{
|
||||
s_prevPc = MyValue.MyPC;
|
||||
s_pcGo = new UnityEngine.GameObject("__WL813t3_PC");
|
||||
s_pcGo.transform.position = UnityEngine.Vector3.zero;
|
||||
s_pc = s_pcGo.AddComponent<PCActor>();
|
||||
s_pc.m_Role = eRole.PC; s_pc.m_SubRole = eSubRol.None;
|
||||
MyValue.MyPC = s_pc;
|
||||
|
||||
s_near = MakeActor("near", eRole.Mob, eSubRol.None, new UnityEngine.Vector3(2.5f, 0f, 0f), out s_nearGo);
|
||||
s_far = MakeActor("far", eRole.Mob, eSubRol.None, new UnityEngine.Vector3(3.5f, 0f, 0f), out s_farGo);
|
||||
s_lock = MakeActor("lockedBoss", eRole.Mob, eSubRol.Boss, new UnityEngine.Vector3(9f, 0f, 0f), out s_lockGo);
|
||||
s_lock.m_IsLockTarget = true;
|
||||
|
||||
WL.Settings.WLTargetingSettings.ClearCache();
|
||||
WL.Combat.Auto.SightRangeGuard.RuntimeDisabled = false;
|
||||
WL.Combat.Auto.SightRangeGuard.ResetDiagnostics();
|
||||
|
||||
L("");
|
||||
L("── ⓪ 준비");
|
||||
L(" " + P(s_pc.IsMainPC()) + " 가짜 메인 PC · MyValue.MyPC 등록");
|
||||
var t = WL.Settings.WLTargetingSettings.Instance;
|
||||
L(" " + P(t != null) + " WLTargetingSettings 에셋 로드");
|
||||
if (t != null)
|
||||
L(" 실값 retargetSearchRange=" + t.retargetSearchRange + " · chaseRange=" + t.chaseRange
|
||||
+ " · chaseDropIgnoresLock=" + t.chaseDropIgnoresLock + " · stopChaseMoveOnDrop=" + t.stopChaseMoveOnDrop
|
||||
+ " · chaseCheckSeconds=" + t.chaseCheckSeconds);
|
||||
L(" " + P(WL.Combat.Auto.SightRangeGuard.Active) + " SightRangeGuard.Active (chaseRange > 0)");
|
||||
L(" PC Agent = " + (s_pc.Get_Agent() == null ? "없음(에디트 모드 — 이동 정지 경로는 미확인)" : "있음"));
|
||||
}
|
||||
|
||||
static void Teardown()
|
||||
{
|
||||
try
|
||||
{
|
||||
WL.Settings.WLTargetingSettings.SetInstanceForProbe(null);
|
||||
WL.Settings.WLTargetingSettings.ClearCache();
|
||||
WL.Combat.Nav.WLTerrainGuardSettings.ClearCache();
|
||||
WL.Combat.Auto.SightRangeGuard.RuntimeDisabled = false;
|
||||
WL.Combat.Auto.SightRangeGuard.ResetDiagnostics();
|
||||
WL.Combat.Nav.SpawnPositionGuard.ResetDiagnostics();
|
||||
MyValue.MyPC = s_prevPc;
|
||||
Kill(s_pcGo); Kill(s_nearGo); Kill(s_farGo); Kill(s_lockGo);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
static void Kill(UnityEngine.GameObject go) { if (go != null) UnityEngine.Object.DestroyImmediate(go); }
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// ① 순수 판정 (Decide)
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
static void Sec1_Decide()
|
||||
{
|
||||
L("");
|
||||
L("── ① 시야 3 m 판정표 (SightRangeGuard.Decide · 순수 함수)");
|
||||
L(" | 거리 | 락 | ignoreLock | 판정 |");
|
||||
Row(3f, 0.0f, false, false, WL.Combat.Auto.SightRangeGuard.KeepTarget, "0.0 m(붙어 있음)");
|
||||
Row(3f, 2.5f, false, false, WL.Combat.Auto.SightRangeGuard.KeepTarget, "2.5 m");
|
||||
Row(3f, 3.0f, false, false, WL.Combat.Auto.SightRangeGuard.KeepTarget, "3.0 m(경계 = 유지)");
|
||||
Row(3f, 3.01f, false, false, WL.Combat.Auto.SightRangeGuard.DropTarget, "3.01 m");
|
||||
Row(3f, 3.5f, false, false, WL.Combat.Auto.SightRangeGuard.DropTarget, "3.5 m");
|
||||
Row(3f, 14f, false, false, WL.Combat.Auto.SightRangeGuard.DropTarget, "14 m(813w 보스 홀드 반경)");
|
||||
Row(3f, 3.5f, true, false, WL.Combat.Auto.SightRangeGuard.KeepByLock, "3.5 m · 락 · 락 존중");
|
||||
Row(3f, 3.5f, true, true, WL.Combat.Auto.SightRangeGuard.DropTarget, "3.5 m · 락 · 락 무시");
|
||||
Row(0f, 100f, false, false, WL.Combat.Auto.SightRangeGuard.KeepDisabled, "chaseRange 0 = 축 off(C8)");
|
||||
}
|
||||
|
||||
static void Row(float range, float dist, bool locked, bool ignore, int expect, string label)
|
||||
{
|
||||
int got = WL.Combat.Auto.SightRangeGuard.Decide(range, dist, locked, ignore);
|
||||
L(" " + P(got == expect) + " " + label + " → " + Name(got) + " (기대 " + Name(expect) + ")");
|
||||
}
|
||||
|
||||
static string Name(int v)
|
||||
{
|
||||
if (v == WL.Combat.Auto.SightRangeGuard.KeepTarget) return "유지";
|
||||
if (v == WL.Combat.Auto.SightRangeGuard.DropTarget) return "놓음";
|
||||
if (v == WL.Combat.Auto.SightRangeGuard.KeepByLock) return "락유지";
|
||||
return "축off";
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// ② 실제 액터로 CheckOnce
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
static void Sec2_Live()
|
||||
{
|
||||
L("");
|
||||
L("── ② 가짜 적으로 실호출 (SightRangeGuard.CheckOnce · 원본 Del_Target 경로)");
|
||||
WL.Combat.Auto.SightRangeGuard.ResetDiagnostics();
|
||||
|
||||
s_pc.Set_Target(s_near);
|
||||
bool dropped = WL.Combat.Auto.SightRangeGuard.CheckOnce();
|
||||
L(" " + P(!dropped && s_pc.isTarget && object.ReferenceEquals(s_pc.Get_Target(), s_near))
|
||||
+ " 2.5 m 적 → 타깃 유지 (거리 실측 " + WL.Combat.Auto.SightRangeGuard.LastDistance.ToString("F2") + " m)");
|
||||
|
||||
s_pc.Set_Target(s_far);
|
||||
dropped = WL.Combat.Auto.SightRangeGuard.CheckOnce();
|
||||
L(" " + P(dropped && !s_pc.isTarget)
|
||||
+ " 3.5 m 적 → **타깃 해제** (거리 실측 " + WL.Combat.Auto.SightRangeGuard.LastDistance.ToString("F2")
|
||||
+ " m · Dropped=" + WL.Combat.Auto.SightRangeGuard.Dropped + " · 마지막 " + WL.Combat.Auto.SightRangeGuard.LastDroppedName + ")");
|
||||
|
||||
// 락 타깃(813w 보스) — 기본값(락 존중)에서는 유지된다
|
||||
s_pc.Set_Target(s_lock);
|
||||
dropped = WL.Combat.Auto.SightRangeGuard.CheckOnce();
|
||||
bool lockKept = !dropped && s_pc.isTarget;
|
||||
L(" " + P(lockKept) + " 9 m 락 보스 → 유지(원본 락 존중 · LockKept=" + WL.Combat.Auto.SightRangeGuard.LockKept + ")"
|
||||
+ " · SO `bossLockTarget` 을 0 으로 두면 813w 가 애초에 락을 걸지 않는다");
|
||||
|
||||
// 락 무시 스위치
|
||||
var tmp = MakeTemp();
|
||||
tmp.chaseRange = 3f; tmp.chaseDropIgnoresLock = true; tmp.stopChaseMoveOnDrop = false;
|
||||
WL.Settings.WLTargetingSettings.SetInstanceForProbe(tmp);
|
||||
s_pc.Set_Target(s_lock);
|
||||
dropped = WL.Combat.Auto.SightRangeGuard.CheckOnce();
|
||||
L(" " + P(dropped && !s_pc.isTarget) + " chaseDropIgnoresLock=1 → 락 보스도 해제");
|
||||
WL.Settings.WLTargetingSettings.SetInstanceForProbe(null);
|
||||
UnityEngine.Object.DestroyImmediate(tmp);
|
||||
|
||||
s_pc.Del_Target(true);
|
||||
L(" 이동 정지 경로(MoveStopped=" + WL.Combat.Auto.SightRangeGuard.MoveStopped
|
||||
+ " · MoveSkipped=" + WL.Combat.Auto.SightRangeGuard.MoveSkipped
|
||||
+ ") — 🔴 에디트 모드엔 NavMeshAgent·애니메이터가 없어 **실이동 정지는 미확인**(§QA 로 확인).");
|
||||
}
|
||||
|
||||
static WL.Settings.WLTargetingSettings MakeTemp()
|
||||
{
|
||||
var so = UnityEngine.ScriptableObject.CreateInstance<WL.Settings.WLTargetingSettings>();
|
||||
var real = UnityEngine.Resources.Load<WL.Settings.WLTargetingSettings>("WL/WLTargetingSettings");
|
||||
if (real != null)
|
||||
{
|
||||
so.frontConeDeg = real.frontConeDeg; so.facingWeight = real.facingWeight;
|
||||
so.attackStartRetarget = real.attackStartRetarget; so.retargetOnKill = real.retargetOnKill;
|
||||
so.immediateAttackMargin = real.immediateAttackMargin; so.retargetSearchRange = real.retargetSearchRange;
|
||||
so.pcAggroOnHit = real.pcAggroOnHit; so.autoEngage = real.autoEngage;
|
||||
so.chaseRange = real.chaseRange; so.chaseDropIgnoresLock = real.chaseDropIgnoresLock;
|
||||
so.stopChaseMoveOnDrop = real.stopChaseMoveOnDrop; so.chaseCheckSeconds = real.chaseCheckSeconds;
|
||||
}
|
||||
return so;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// ③ 보스 평지 스폰
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
static void Sec3_BossPin()
|
||||
{
|
||||
L("");
|
||||
L("── ③ 보스 평지 스폰 (SpawnPositionGuard.ResolveBossPin · 순수 함수)");
|
||||
var cfg = WL.Combat.Nav.WLTerrainGuardSettings.Instance;
|
||||
L(" " + P(cfg != null) + " WLTerrainGuardSettings 에셋 로드");
|
||||
if (cfg == null) return;
|
||||
L(" 실값 bossSpawnPinEnabled=" + cfg.bossSpawnPinEnabled + " · bossSpawnPinRadius=" + cfg.bossSpawnPinRadius
|
||||
+ " · bossSpawnPinTolerance=" + cfg.bossSpawnPinTolerance + " · maxWalkableY=" + cfg.maxWalkableY
|
||||
+ " · dwellSeconds=" + cfg.dwellSeconds);
|
||||
|
||||
var arena = WL.Combat.Boss.BossArena.Center;
|
||||
L(" BossArena.Center(에디트 모드 · 보류 목록 비어 있음 → SO arenaCenter) = " + V(arena));
|
||||
|
||||
// 실 NavMesh 자산을 잠시 올린다(WL_Nature 재베이크본) — 표집이 실제로 돌게.
|
||||
var data = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.AI.NavMeshData>("Assets/WL/Settings/WL_Nature_NavMesh.asset");
|
||||
UnityEngine.AI.NavMeshDataInstance inst = default(UnityEngine.AI.NavMeshDataInstance);
|
||||
if (data != null) inst = UnityEngine.AI.NavMesh.AddNavMeshData(data);
|
||||
L(" " + P(data != null) + " WL_Nature_NavMesh.asset 로드·부착(삼각형 "
|
||||
+ (UnityEngine.AI.NavMesh.CalculateTriangulation().indices.Length / 3) + ")");
|
||||
try
|
||||
{
|
||||
UnityEngine.Vector3 outp;
|
||||
int r1 = WL.Combat.Nav.SpawnPositionGuard.ResolveBossPin(cfg, new UnityEngine.Vector3(139f, 6f, -20f), out outp);
|
||||
L(" " + P(r1 == WL.Combat.Nav.SpawnPositionGuard.BossPinMoved) + " 언덕 위 가짜 보스 (139, 6.00, -20) → " + PinName(r1) + " " + V(outp)
|
||||
+ " · y 컷(" + cfg.maxWalkableY + ") 이하 " + (outp.y <= cfg.maxWalkableY));
|
||||
int r2 = WL.Combat.Nav.SpawnPositionGuard.ResolveBossPin(cfg, arena, out outp);
|
||||
L(" " + P(r2 == WL.Combat.Nav.SpawnPositionGuard.BossPinAlready) + " 아레나 제자리 " + V(arena) + " → " + PinName(r2) + " " + V(outp));
|
||||
int r3 = WL.Combat.Nav.SpawnPositionGuard.ResolveBossPin(cfg, new UnityEngine.Vector3(128f, 8f, -58f), out outp);
|
||||
L(" " + P(r3 == WL.Combat.Nav.SpawnPositionGuard.BossPinMoved) + " D-48 언덕 (128, 8.00, -58) → " + PinName(r3) + " " + V(outp));
|
||||
int r5 = WL.Combat.Nav.SpawnPositionGuard.ResolveBossPin(cfg, new UnityEngine.Vector3(147f, 6f, 30f), out outp);
|
||||
L(" " + P(r5 == WL.Combat.Nav.SpawnPositionGuard.BossPinMoved) + " D-48 언덕 (147, 6.00, 30) → " + PinName(r5) + " " + V(outp));
|
||||
|
||||
var off = UnityEngine.ScriptableObject.CreateInstance<WL.Combat.Nav.WLTerrainGuardSettings>();
|
||||
off.bossSpawnPinEnabled = false;
|
||||
int r4 = WL.Combat.Nav.SpawnPositionGuard.ResolveBossPin(off, new UnityEngine.Vector3(139f, 6f, -20f), out outp);
|
||||
L(" " + P(r4 == WL.Combat.Nav.SpawnPositionGuard.BossPinNoAnchor) + " C8: bossSpawnPinEnabled=0 → 손대지 않음(" + PinName(r4) + ")");
|
||||
UnityEngine.Object.DestroyImmediate(off);
|
||||
}
|
||||
finally { if (inst.valid) UnityEngine.AI.NavMesh.RemoveNavMeshData(inst); }
|
||||
}
|
||||
|
||||
static string PinName(int v)
|
||||
{
|
||||
if (v == WL.Combat.Nav.SpawnPositionGuard.BossPinNoAnchor) return "앵커없음(무개입)";
|
||||
if (v == WL.Combat.Nav.SpawnPositionGuard.BossPinAlready) return "이미 제자리";
|
||||
if (v == WL.Combat.Nav.SpawnPositionGuard.BossPinMoved) return "**옮김**";
|
||||
return "앵커 부적격";
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// ④ C8 진리표
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
static void Sec4_C8()
|
||||
{
|
||||
L("");
|
||||
L("── ④ C8 진리표 (값 원복 = 813t2 동작)");
|
||||
|
||||
var t0 = MakeTemp();
|
||||
t0.chaseRange = 0f;
|
||||
WL.Settings.WLTargetingSettings.SetInstanceForProbe(t0);
|
||||
bool active0 = WL.Combat.Auto.SightRangeGuard.Active;
|
||||
s_pc.Set_Target(s_far);
|
||||
bool drop0 = WL.Combat.Auto.SightRangeGuard.CheckOnce();
|
||||
L(" " + P(!active0 && !drop0 && s_pc.isTarget) + " chaseRange=0 → Active=false · 3.5 m 적 유지(= 813t3 이전 동작 100%)");
|
||||
s_pc.Del_Target(true);
|
||||
WL.Settings.WLTargetingSettings.SetInstanceForProbe(null);
|
||||
UnityEngine.Object.DestroyImmediate(t0);
|
||||
|
||||
WL.Combat.Auto.SightRangeGuard.RuntimeDisabled = true;
|
||||
bool active1 = WL.Combat.Auto.SightRangeGuard.Active;
|
||||
WL.Combat.Auto.SightRangeGuard.RuntimeDisabled = false;
|
||||
L(" " + P(!active1) + " RuntimeDisabled=true → Active=false");
|
||||
|
||||
var t1 = MakeTemp(); t1.retargetSearchRange = 0f;
|
||||
WL.Settings.WLTargetingSettings.SetInstanceForProbe(t1);
|
||||
L(" " + P(WL.Settings.WLTargetingSettings.RetargetSearchRange == 0f)
|
||||
+ " retargetSearchRange=0 → MyActor.Get_WLFindRange() 가 원본 f_FindEnemyRange(table 6 m + AttackRange_Up)로 되돌아간다");
|
||||
WL.Settings.WLTargetingSettings.SetInstanceForProbe(null);
|
||||
UnityEngine.Object.DestroyImmediate(t1);
|
||||
|
||||
var g0 = UnityEngine.ScriptableObject.CreateInstance<WL.Combat.Nav.WLTerrainGuardSettings>();
|
||||
g0.enabled = false;
|
||||
WL.Combat.Nav.WLTerrainGuardSettings.SetInstanceForProbe(g0);
|
||||
L(" " + P(!WL.Combat.Nav.WLTerrainGuardSettings.Active && !WL.Combat.Nav.SpawnPositionGuard.Active)
|
||||
+ " WLTerrainGuardSettings.enabled=0 → 813t2 러너·스폰 보정 전부 off(보스 고정 포함)");
|
||||
WL.Combat.Nav.WLTerrainGuardSettings.SetInstanceForProbe(null);
|
||||
WL.Combat.Nav.WLTerrainGuardSettings.ClearCache();
|
||||
UnityEngine.Object.DestroyImmediate(g0);
|
||||
L(" " + P(WL.Combat.Nav.WLTerrainGuardSettings.Instance != null) + " 실 에셋 복구 확인(프로브가 실 에셋을 쓰지 않았다)");
|
||||
|
||||
var b = UnityEngine.Resources.Load<WL.Combat.Boss.WLBossArenaSettings>("WL/WLBossArenaSettings");
|
||||
L(" " + P(b != null && !b.useFieldBossPositionAsCenter)
|
||||
+ " WLBossArenaSettings.useFieldBossPositionAsCenter=0 → 아레나 중심이 **SO 값**(139, 0.8, -20)으로 고정 (1 로 되돌리면 스포너 노드 위치)");
|
||||
var rg = UnityEngine.Resources.Load<WL.Combat.Run.WLRunGuideSettings>("WL/WLRunGuideSettings");
|
||||
L(" " + P(rg != null && !rg.bossLockTarget) + " WLRunGuideSettings.bossLockTarget=0 → 813w 보스 락타깃 off(1 로 되돌리면 원상)");
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// ⑤ GC
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
static void Sec5_GC()
|
||||
{
|
||||
L("");
|
||||
L("── ⑤ GC (틱 · 판정 반복)");
|
||||
s_pc.Set_Target(s_near); // 유지 경로(로그·문자열 없음)로 돈다
|
||||
WL.Combat.Auto.SightRangeGuard.ResetDiagnostics();
|
||||
for (int i = 0; i < 50; i++) WL.Combat.Auto.SightRangeGuard.CheckOnce();
|
||||
System.GC.Collect();
|
||||
long a = System.GC.GetTotalMemory(false);
|
||||
for (int i = 0; i < 1000; i++) WL.Combat.Auto.SightRangeGuard.CheckOnce();
|
||||
var cfg = WL.Combat.Nav.WLTerrainGuardSettings.Instance;
|
||||
UnityEngine.Vector3 o;
|
||||
for (int i = 0; i < 1000; i++) WL.Combat.Nav.SpawnPositionGuard.ResolveBossPin(cfg, new UnityEngine.Vector3(139f, 6f, -20f), out o);
|
||||
for (int i = 0; i < 1000; i++) WL.Combat.Auto.SightRangeGuard.Decide(3f, 3.5f, false, false);
|
||||
long b = System.GC.GetTotalMemory(false);
|
||||
L(" " + P(b - a <= 0) + " CheckOnce ×1000 + ResolveBossPin ×1000 + Decide ×1000 → Δ " + (b - a) + " B");
|
||||
s_pc.Del_Target(true);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
// 발주서 WL-813t3 §1-3ⓐ — NavMesh 전역 에이전트 슬로프 변경 (Humanoid 45° → 30°)
|
||||
//
|
||||
// 보기 : unity command --project-path E:\NerdNavis\WL_wt\data run_script --file AgentScripts/WL813t3_Slope.cs --entry WL813t3_Slope.Show --timeout 300
|
||||
// 적용 : … --entry WL813t3_Slope.Set30
|
||||
// 되돌림 : … --entry WL813t3_Slope.Restore45
|
||||
//
|
||||
// 🔴 슬로프는 프로젝트 전역값(ProjectSettings/NavMeshAreas.asset)이다. **이미 구워진 다른 맵 NavMesh 데이터는
|
||||
// 바뀌지 않는다**(재베이크할 때만 영향). 이 프로젝트에서 재베이크하는 것은 WL_Nature 하나뿐이다.
|
||||
// 파일을 손으로 고치면 실행 중인 에디터의 메모리 값이 안 바뀌므로, 여기서는 Navigation 창과 같은 경로
|
||||
// (`Unsupported.GetSerializedAssetInterfaceSingleton("NavMeshProjectSettings")` + SerializedObject)로 고친다.
|
||||
|
||||
public static class WL813t3_Slope
|
||||
{
|
||||
public static string Show() { return Apply(-1f); }
|
||||
public static string Set30() { return Apply(30f); }
|
||||
public static string Restore45() { return Apply(45f); }
|
||||
|
||||
static string Apply(float slope)
|
||||
{
|
||||
var sb = new System.Text.StringBuilder();
|
||||
var singleton = UnityEditor.Unsupported.GetSerializedAssetInterfaceSingleton("NavMeshProjectSettings");
|
||||
if (singleton == null) { sb.AppendLine("[에러] NavMeshProjectSettings 싱글턴을 못 얻었다"); return sb.ToString(); }
|
||||
|
||||
var so = new UnityEditor.SerializedObject(singleton);
|
||||
var arr = so.FindProperty("m_Settings");
|
||||
var names = so.FindProperty("m_SettingNames");
|
||||
if (arr == null) { sb.AppendLine("[에러] m_Settings 프로퍼티 없음"); return sb.ToString(); }
|
||||
|
||||
sb.AppendLine("에이전트 타입 " + arr.arraySize + "개");
|
||||
for (int i = 0; i < arr.arraySize; i++)
|
||||
{
|
||||
var e = arr.GetArrayElementAtIndex(i);
|
||||
var id = e.FindPropertyRelative("agentTypeID");
|
||||
var r = e.FindPropertyRelative("agentRadius");
|
||||
var h = e.FindPropertyRelative("agentHeight");
|
||||
var sl = e.FindPropertyRelative("agentSlope");
|
||||
var cl = e.FindPropertyRelative("agentClimb");
|
||||
string nm = (names != null && i < names.arraySize) ? names.GetArrayElementAtIndex(i).stringValue : "?";
|
||||
sb.AppendLine(" [" + i + "] " + nm + " · agentTypeID " + (id != null ? id.intValue.ToString() : "?")
|
||||
+ " · radius " + (r != null ? r.floatValue.ToString("F3") : "?")
|
||||
+ " · height " + (h != null ? h.floatValue.ToString("F3") : "?")
|
||||
+ " · **slope " + (sl != null ? sl.floatValue.ToString("F2") : "?") + "**"
|
||||
+ " · climb " + (cl != null ? cl.floatValue.ToString("F3") : "?"));
|
||||
|
||||
if (slope > 0f && id != null && id.intValue == 0 && sl != null)
|
||||
{
|
||||
float old = sl.floatValue;
|
||||
sl.floatValue = slope;
|
||||
sb.AppendLine(" → Humanoid(agentTypeID 0) slope " + old.ToString("F2") + " → " + slope.ToString("F2") + " 로 변경");
|
||||
}
|
||||
}
|
||||
|
||||
if (slope > 0f)
|
||||
{
|
||||
so.ApplyModifiedPropertiesWithoutUndo();
|
||||
UnityEditor.AssetDatabase.SaveAssets();
|
||||
UnityEditor.EditorUtility.SetDirty(singleton);
|
||||
UnityEditor.AssetDatabase.SaveAssets();
|
||||
var check = UnityEngine.AI.NavMesh.GetSettingsByID(0);
|
||||
sb.AppendLine("저장 후 NavMesh.GetSettingsByID(0) 실측 — radius " + check.agentRadius + " · height " + check.agentHeight
|
||||
+ " · **slope " + check.agentSlope + "** · climb " + check.agentClimb
|
||||
+ " · minRegionArea " + check.minRegionArea + " · voxel " + check.voxelSize.ToString("F5"));
|
||||
}
|
||||
else
|
||||
{
|
||||
var check = UnityEngine.AI.NavMesh.GetSettingsByID(0);
|
||||
sb.AppendLine("현재 NavMesh.GetSettingsByID(0) — slope " + check.agentSlope + " · climb " + check.agentClimb);
|
||||
}
|
||||
|
||||
UnityEngine.Debug.Log("[WL813t3_Slope]\n" + sb.ToString());
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// SightRangeGuard.cs — PC 시야(추적) 반경 제한 · 반경 밖 적 무시
|
||||
//
|
||||
// PD 지시 #813 추가 (2026-09-11 00:5x) · 발주서 WL-813t3 §1-1
|
||||
// 「지금은 캐릭터의 시야 범위가 너무 넓어서 한참 동안 적을 추적하고 있어(보이지도 않는 위치까지 추적).
|
||||
// 플레이어의 크기를 1 m 라고 가정할 때 반경 3 m 정도로 제한해줘.」
|
||||
//
|
||||
// ■ 왜 값 하나로는 안 되는가 (코드 실측)
|
||||
// ① 「고르는 거리」 = `MyActor.Get_WLFindRange()`(MyActor.cs:21) → `WLTargetingSettings.RetargetSearchRange`.
|
||||
// 0 이면 원본 `f_FindEnemyRange` = `table_GlobalValue.f_PC_FindEnemyRange`(**실측 6 m**) + `AttackRange_Up`.
|
||||
// → 이 값을 3 으로 두면 **새로 고르는** 적은 3 m 안으로 제한된다.
|
||||
// ② 그런데 **이미 잡은 타깃을 놓는 규칙이 원본에 없다.** `MyActor.Update`(:191~)는 타깃이 죽거나
|
||||
// (펫 한정) 주인과 7 m 넘게 떨어질 때만 `Del_Target` 을 부르고, PC 는 그 외에는
|
||||
// `Play_Run()` → `Set_Path(m_Target.Get_position())`(MyActor.cs:335)로 **거리 제한 없이 계속 쫓아간다**.
|
||||
// → PD 가 본 「한참 동안 추적」이 이 경로다. 그래서 「놓는 거리」(chaseRange)가 따로 필요하다.
|
||||
//
|
||||
// ■ 이 파일이 하는 것 — 원본 훅 0 · 폴링만 (813x `MobNavRecovery` · 813t2 `TerrainGuard` 선례)
|
||||
// 0.1 s 마다 PC 의 현재 타깃까지의 거리를 재서 `chaseRange` 를 넘으면 원본 `Actor.Del_Target`(Actor.cs:957)로
|
||||
// 놓는다. 놓은 뒤 원본 `Check_Target`(MyActor.cs:357)이 같은 프레임에 다시 고르는데, 그때 쓰는 탐색 거리가
|
||||
// ①(3 m)이므로 **3 m 밖 적은 다시 잡히지 않는다** = 「무시」.
|
||||
//
|
||||
// ■ 이동도 같이 멈춘다 (실측 근거)
|
||||
// `Del_Target` 만으로는 **달리던 경로가 남는다** — `MyActor.Update` 의 타깃 없음 분기는
|
||||
// `else if (CurAnim != eAnim.Run …) Play_Idle();`(MyActor.cs:258) 라서 이미 Run 이면 아무것도 하지 않고,
|
||||
// `Play_Idle`(Actor.cs:1900)은 애니메이션만 바꾼다(에이전트 정지 아님).
|
||||
// → 타깃을 놓는 순간 `Get_Agent().ResetPath()` + `Play_Idle()` 로 세운다(원본 공개 API 만).
|
||||
// 🔴 단, **조이스틱 조작 중**(`PCActor.JoystickStatus > 0`)이거나 **813w 가이드가 이동을 쥐고 있을 때**
|
||||
// (`RunGuide.Steering`)는 손대지 않는다 — 두 이동이 싸우면 존 이동이 끊긴다.
|
||||
//
|
||||
// ■ 락 타깃
|
||||
// 813w `RunGuide` 는 보스에 원본 락(`Actor.m_IsLockTarget`)을 건다. 기본값(`chaseDropIgnoresLock = false`)은
|
||||
// `Del_Target(false)` 를 쓰므로 **원본이 스스로 락을 지킨다**(Actor.cs:961). 락을 무시하려면 SO 로 켠다.
|
||||
//
|
||||
// ■ 값(C45) 전부 `WLTargetingSettings`. 게임 수치 상수 0. `chaseRange = 0` 이면 러너 자체가 뜨지 않는다(C8).
|
||||
// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것(Actor/PCActor 가 Assembly-CSharp 에 있다).
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace WL.Combat.Auto
|
||||
{
|
||||
/// <summary>PC 가 시야(추적) 반경 밖 적을 계속 쫓지 않게 타깃을 놓는다. 정적 · 폴링 · 원본 훅 0.</summary>
|
||||
public static class SightRangeGuard
|
||||
{
|
||||
/// <summary>이 축이 살아 있는가. 반경 0 = 꺼짐(= 813t3 이전 동작 100 %).</summary>
|
||||
public static bool Active
|
||||
{
|
||||
get { return !RuntimeDisabled && WL.Settings.WLTargetingSettings.ChaseRange > 0f; }
|
||||
}
|
||||
|
||||
/// <summary>진단·A/B 전용 런타임 스위치. Play 종료 시 도메인 리로드로 자동 false.</summary>
|
||||
public static bool RuntimeDisabled;
|
||||
|
||||
// ───────────────────────────────────────── 진단 카운터 (프로브가 읽는다)
|
||||
public static int Ticks, Checks, Dropped, LockKept, MoveStopped, MoveSkipped;
|
||||
public static float LastDistance;
|
||||
public static string LastDroppedName = "";
|
||||
public static string LastLog = "";
|
||||
|
||||
// ───────────────────────────────────────── 내부 상태 (GC 0 — 틱에서 새로 만들지 않는다)
|
||||
static float s_nextCheck;
|
||||
static bool s_faulted;
|
||||
|
||||
internal static void Tick()
|
||||
{
|
||||
if (s_faulted || !Active) return;
|
||||
float now = Time.unscaledTime;
|
||||
if (now < s_nextCheck) return;
|
||||
s_nextCheck = now + Mathf.Max(0.02f, WL.Settings.WLTargetingSettings.ChaseCheckSeconds);
|
||||
Ticks++;
|
||||
|
||||
// 🔴 Debug.LogException 을 쓰지 않는다 — ErrorLogHookManager 가 그것을 플레이어 모달로 띄운다(813x).
|
||||
try { CheckOnce(); }
|
||||
catch (System.Exception ex) { s_faulted = true; Log("중단(예외) — " + ex.Message); }
|
||||
}
|
||||
|
||||
/// <summary>판정 결과 — 0 유지 · 1 놓는다 · 2 락 때문에 유지 · 3 축이 꺼져 있음.</summary>
|
||||
public const int KeepTarget = 0, DropTarget = 1, KeepByLock = 2, KeepDisabled = 3;
|
||||
|
||||
/// <summary>
|
||||
/// 놓을지 말지의 **순수 함수**(Actor 접근 0 · 프로브가 가짜 거리로 그대로 부른다).
|
||||
/// range 이하면 유지 · 넘으면 놓는다. 단 락 타깃은 ignoreLock 이 아니면 유지한다.
|
||||
/// </summary>
|
||||
public static int Decide(float range, float distance, bool targetLocked, bool ignoreLock)
|
||||
{
|
||||
if (range <= 0f) return KeepDisabled;
|
||||
if (distance <= range) return KeepTarget;
|
||||
if (targetLocked && !ignoreLock) return KeepByLock;
|
||||
return DropTarget;
|
||||
}
|
||||
|
||||
/// <summary>1회 판정. 놓았으면 true.</summary>
|
||||
public static bool CheckOnce()
|
||||
{
|
||||
float range = WL.Settings.WLTargetingSettings.ChaseRange;
|
||||
if (range <= 0f) return false;
|
||||
|
||||
var pc = MyValue.MyPC;
|
||||
if (DSUtil.CheckNull(pc) || pc.IsDead() || !pc.isTarget) return false;
|
||||
|
||||
var target = pc.Get_Target();
|
||||
if (DSUtil.CheckNull(target))
|
||||
{
|
||||
// 참조 없이 플래그만 남은 상태 — Del_Target 은 여기서 m_Target 을 NRE 로 만진다(813w 선례).
|
||||
pc.isTarget = false;
|
||||
return false;
|
||||
}
|
||||
if (target.IsDead()) return false; // 죽은 타깃 교체는 원본(#770-2)이 한다
|
||||
|
||||
Checks++;
|
||||
float d = Vector3.Distance(pc.Get_position(), target.Get_position());
|
||||
LastDistance = d;
|
||||
|
||||
bool ignoreLock = WL.Settings.WLTargetingSettings.ChaseDropIgnoresLock;
|
||||
int verdict = Decide(range, d, target.m_IsLockTarget, ignoreLock);
|
||||
if (verdict == KeepByLock) { LockKept++; return false; }
|
||||
if (verdict != DropTarget) return false;
|
||||
|
||||
string name = target.name;
|
||||
pc.Del_Target(ignoreLock);
|
||||
if (pc.isTarget) { LockKept++; return false; } // 원본이 락으로 거부했다
|
||||
|
||||
Dropped++;
|
||||
LastDroppedName = name;
|
||||
StopChase(pc);
|
||||
Log("타깃 무시 #" + Dropped + " " + name + " " + d.ToString("F2") + " m > " + range.ToString("F2") + " m");
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>달려가던 경로를 끊고 세운다. 조이스틱·813w 가이드가 이동을 쥐고 있으면 손대지 않는다.</summary>
|
||||
static void StopChase(PCActor pc)
|
||||
{
|
||||
if (!WL.Settings.WLTargetingSettings.StopChaseMoveOnDrop) { MoveSkipped++; return; }
|
||||
if (pc.JoystickStatus > 0 || WL.Combat.Run.RunGuide.Steering) { MoveSkipped++; return; }
|
||||
if (pc.Get_CurAnim() != eAnim.Run) { MoveSkipped++; return; } // 공격·스킬·회피 모션은 끊지 않는다
|
||||
|
||||
var agent = pc.Get_Agent();
|
||||
if (agent != null && agent.enabled && agent.isOnNavMesh)
|
||||
{
|
||||
agent.ResetPath();
|
||||
agent.velocity = Vector3.zero;
|
||||
}
|
||||
pc.Play_Idle();
|
||||
MoveStopped++;
|
||||
}
|
||||
|
||||
static void Log(string msg)
|
||||
{
|
||||
LastLog = msg;
|
||||
if (WL.Settings.WLTargetingSettings.ChaseVerboseLog) Debug.Log("[SightRangeGuard] " + msg);
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────── 진단 · 초기화
|
||||
/// <summary>프로브용: 카운터 초기화.</summary>
|
||||
public static void ResetDiagnostics()
|
||||
{
|
||||
Ticks = Checks = Dropped = LockKept = MoveStopped = MoveSkipped = 0;
|
||||
LastDistance = 0f; LastDroppedName = ""; LastLog = "";
|
||||
s_nextCheck = 0f; s_faulted = false;
|
||||
}
|
||||
|
||||
/// <summary>프로브용: 러너 없이 1회 틱(간격 무시).</summary>
|
||||
public static void ForceTick() { s_nextCheck = 0f; Tick(); }
|
||||
|
||||
/// <summary>프로브용: 러너가 부르는 것과 똑같은 틱(타이머 초기화 0 — GC 측정용).</summary>
|
||||
public static void ProbeTick() { Tick(); }
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
/// <summary>SightRangeGuard 의 시간 축. 숨김 GameObject 1개 · 코루틴 0.</summary>
|
||||
internal sealed class SightRangeGuardRunner : MonoBehaviour
|
||||
{
|
||||
void Update() { SightRangeGuard.Tick(); }
|
||||
}
|
||||
|
||||
/// <summary>러너 부팅(C8 — 반경이 0 이면 오브젝트 0).</summary>
|
||||
public static class SightRangeGuardBoot
|
||||
{
|
||||
static SightRangeGuardRunner s_runner;
|
||||
|
||||
internal static void EnsureRunner()
|
||||
{
|
||||
if (!Application.isPlaying || !DSUtil.CheckNull(s_runner)) return;
|
||||
var go = new GameObject("[WL813t3] SightRangeGuardRunner");
|
||||
go.hideFlags = HideFlags.HideAndDontSave; // 씬 전환에도 살아남고 씬·프리팹에 저장되지 않는다
|
||||
s_runner = go.AddComponent<SightRangeGuardRunner>();
|
||||
}
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
||||
static void Boot()
|
||||
{
|
||||
if (SightRangeGuard.Active) EnsureRunner();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8d13d0331a2b3c4d5e6f7a8b9c0d1e31
|
||||
|
|
@ -27,6 +27,10 @@
|
|||
// PC 는 `Get_NavSnapRadius()=0`(Actor.cs:2133) 이라 Set_Warp 이 스스로 스냅하지 않는다
|
||||
// → **반드시 먼저 `NavMesh.SamplePosition` 으로 스냅한 지점**을 넘긴다(지형 뚫기 방지).
|
||||
//
|
||||
// ■ 813t3 추가 (2026-09-11 · PD 「보스가 언덕에 생성된다」) — `SpawnPositionGuard` 에 **보스 평지 고정**을 넣었다.
|
||||
// 보스는 `FieldBossData`(MobControlMgr 가 아니다)가 스폰하므로 아래 ③ 스포너 노드 폴백이 **구조적으로 못 걸린다**
|
||||
// (`TryGetSpawnerNode` 캐시가 MobControlMgr 만 모은다 · 실측). → 보스는 등장 즉시 `BossArena.Center` 로 붙인다.
|
||||
//
|
||||
// ■ 값(C45) 전부 WLTerrainGuardSettings. 게임 수치 상수 0.
|
||||
// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것(Actor/MobActor 가 Assembly-CSharp 에 있다).
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
|
@ -255,6 +259,7 @@ namespace WL.Combat.Nav
|
|||
|
||||
// ───────────────────────────────────────── 진단 카운터
|
||||
public static int Seen, Checked, Fixed, FixFailed, FellBackToNode, Scans;
|
||||
public static int BossPinned, BossPinSkipped, BossPinFails; // 813t3 §1-2
|
||||
public static string LastLog = "";
|
||||
public static Vector3 LastFixFrom, LastFixTo;
|
||||
|
||||
|
|
@ -335,12 +340,58 @@ namespace WL.Combat.Nav
|
|||
return ResultFailed;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// 813t3 §1-2 — 보스 평지 스폰 (PD 2026-09-11 「보스가 언덕에 생성된다」)
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>보스 고정 판정 결과 — 0 앵커 없음(손대지 않음) · 1 이미 제자리 · 2 옮긴다 · 3 앵커가 부적격.</summary>
|
||||
public const int BossPinNoAnchor = 0, BossPinAlready = 1, BossPinMoved = 2, BossPinFailed = 3;
|
||||
|
||||
/// <summary>
|
||||
/// 보스를 고정할 지점 판정 — **좌표만 다루는 순수 함수**(프로브가 실 MobActor 없이 그대로 검증한다).
|
||||
/// 앵커 = `BossArena.Center`(SO `arenaCenter` · `useFieldBossPositionAsCenter = 0` 이면 스포너 노드가 아니라 이 값).
|
||||
/// 앵커를 NavMesh 에 스냅하고 813t2 의 높이 컷(maxWalkableY) 아래인지까지 본다.
|
||||
/// </summary>
|
||||
public static int ResolveBossPin(WLTerrainGuardSettings cfg, Vector3 pos, out Vector3 target)
|
||||
{
|
||||
target = pos;
|
||||
if (cfg == null || !cfg.bossSpawnPinEnabled) return BossPinNoAnchor;
|
||||
|
||||
Vector3 anchor = WL.Combat.Boss.BossArena.Center;
|
||||
if (anchor == Vector3.zero) return BossPinNoAnchor; // 아레나를 모른다 = 원본 위치 그대로
|
||||
|
||||
NavMeshHit hit;
|
||||
if (!NavMesh.SamplePosition(anchor, out hit, Mathf.Max(0.1f, cfg.bossSpawnPinRadius), NavMesh.AllAreas))
|
||||
return BossPinFailed;
|
||||
if (TerrainGuard.HeightRuleApplies(cfg) && hit.position.y > cfg.maxWalkableY) return BossPinFailed;
|
||||
|
||||
target = hit.position;
|
||||
// 🔴 수평만 보면 「아레나 바로 위 공중/바위 위」(예: 보스 점프 착지 y 6.0)를 「제자리」로 놓친다(프로브 실측).
|
||||
// 수평·수직 둘 다 여유 안이어야 제자리로 본다.
|
||||
float dx = pos.x - target.x, dz = pos.z - target.z;
|
||||
float tol = Mathf.Max(0f, cfg.bossSpawnPinTolerance);
|
||||
if (Mathf.Sqrt(dx * dx + dz * dz) <= tol && Mathf.Abs(pos.y - target.y) <= tol) return BossPinAlready;
|
||||
return BossPinMoved;
|
||||
}
|
||||
|
||||
/// <summary>스폰 지점이 walkable 인지 확인하고, 아니면 재시도 → 스포너 노드 순으로 보정한다.</summary>
|
||||
internal static bool Validate(WLTerrainGuardSettings cfg, MobActor mob)
|
||||
{
|
||||
Checked++;
|
||||
Vector3 pos = mob.Get_position();
|
||||
int spawnerId = mob.Get_Group();
|
||||
|
||||
// 813t3 §1-2 — 보스는 등장하는 순간 아레나 평지에 고정한다.
|
||||
// 🔴 아래 일반 경로의 ③ 스포너 노드 폴백은 보스에 **절대 안 걸린다**(실측) —
|
||||
// 노드 캐시(TryGetSpawnerNode)는 MobControlMgr 만 모으는데 보스 스포너는 FieldBossData 다.
|
||||
if (mob.IsSubRole(eSubRol.Boss))
|
||||
{
|
||||
Vector3 pin;
|
||||
int r = ResolveBossPin(cfg, pos, out pin);
|
||||
if (r == BossPinMoved) { BossPinned++; Apply(mob, pos, pin, "보스 아레나 고정"); return true; }
|
||||
if (r == BossPinAlready) { BossPinSkipped++; return false; }
|
||||
if (r == BossPinFailed) BossPinFails++; // 앵커가 부적격 → 아래 일반 경로로 계속
|
||||
}
|
||||
Vector3 target;
|
||||
int how = ResolveSpawnPoint(cfg, pos, spawnerId, out target);
|
||||
|
||||
|
|
@ -394,6 +445,7 @@ namespace WL.Combat.Nav
|
|||
public static void ResetDiagnostics()
|
||||
{
|
||||
Seen = Checked = Fixed = FixFailed = FellBackToNode = Scans = 0;
|
||||
BossPinned = BossPinSkipped = BossPinFails = 0;
|
||||
LastLog = ""; LastFixFrom = LastFixTo = Vector3.zero;
|
||||
s_seen.Clear(); s_spawnerNode.Clear(); s_mobs = new MobActor[0]; s_mgrs = new MobControlMgr[0];
|
||||
s_nextScan = 0f; s_faulted = false;
|
||||
|
|
|
|||
|
|
@ -138,5 +138,19 @@ namespace WL.Combat.Nav
|
|||
|
||||
[Tooltip("재시도가 전부 실패하면 스포너 노드 중심으로 옮긴다(발주서 §1-4).")]
|
||||
public bool spawnFallbackToSpawnerNode = true;
|
||||
|
||||
// ─────────────────────────────────────────────── 보스 평지 스폰(813t3 §1-2)
|
||||
[Header("보스 평지 스폰 — PD 2026-09-11 「보스가 언덕에 생성된다」")]
|
||||
[Tooltip("보스(eSubRol.Boss)가 처음 보이는 순간 아레나 평지(BossArena.Center)로 고정한다. " +
|
||||
"🔴 스포너 노드 폴백은 보스에 안 걸린다 — 노드 캐시는 MobControlMgr 만 모으는데 " +
|
||||
"보스는 FieldBossData(다른 컴포넌트)라서 TryGetSpawnerNode 가 항상 실패한다(실측). " +
|
||||
"0 이면 813t2 동작 그대로(원 위치가 NavMesh 위이기만 하면 손대지 않는다).")]
|
||||
public bool bossSpawnPinEnabled = true;
|
||||
|
||||
[Tooltip("보스를 고정할 지점을 찾는 NavMesh 표집 반경(m). 아레나 중심이 NavMesh 밖이면 이 반경 안에서 가장 가까운 walkable 로 붙인다.")]
|
||||
public float bossSpawnPinRadius = 6f;
|
||||
|
||||
[Tooltip("이 거리(m) 안이면 이미 제자리로 보고 워프하지 않는다(스폰 이펙트 직후 미세 이동 무시).")]
|
||||
public float bossSpawnPinTolerance = 1f;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ MonoBehaviour:
|
|||
requireZoneSpawnedOnce: 1
|
||||
gateOpenDelaySeconds: 0
|
||||
arenaEnabled: 1
|
||||
useFieldBossPositionAsCenter: 1
|
||||
useFieldBossPositionAsCenter: 0
|
||||
arenaCenter: {x: 139, y: 0.8, z: -20}
|
||||
arenaRadius: 20
|
||||
bossChaseRange: 20
|
||||
|
|
|
|||
|
|
@ -46,10 +46,10 @@ MonoBehaviour:
|
|||
bossPriorityEnabled: 1
|
||||
bossArriveRadius: 10
|
||||
bossHoldRadius: 14
|
||||
bossLockTarget: 1
|
||||
bossLockTarget: 0
|
||||
elitePriorityEnabled: 1
|
||||
eliteTargetRadius: 12
|
||||
eliteReleaseRadius: 18
|
||||
eliteTargetRadius: 3
|
||||
eliteReleaseRadius: 3
|
||||
eliteLockTarget: 1
|
||||
eliteRetargetOnlyInHold: 1
|
||||
warpBackEnabled: 1
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ MonoBehaviour:
|
|||
terrainGuardEnabled: 1
|
||||
maxWalkableY: 3.5
|
||||
offNavMeshTolerance: 1
|
||||
dwellSeconds: 1
|
||||
dwellSeconds: 0.5
|
||||
warpCooldownSeconds: 3
|
||||
warpNearRadius: 4
|
||||
warpFarRadius: 30
|
||||
|
|
@ -35,3 +35,6 @@ MonoBehaviour:
|
|||
spawnSampleRadius: 2
|
||||
spawnRetries: 3
|
||||
spawnFallbackToSpawnerNode: 1
|
||||
bossSpawnPinEnabled: 1
|
||||
bossSpawnPinRadius: 6
|
||||
bossSpawnPinTolerance: 1
|
||||
|
|
|
|||
|
|
@ -17,6 +17,11 @@ MonoBehaviour:
|
|||
attackStartRetarget: 1
|
||||
retargetOnKill: 1
|
||||
immediateAttackMargin: 0.2
|
||||
retargetSearchRange: 0
|
||||
retargetSearchRange: 3
|
||||
pcAggroOnHit: 0
|
||||
autoEngage: 1
|
||||
chaseRange: 3
|
||||
chaseDropIgnoresLock: 0
|
||||
stopChaseMoveOnDrop: 1
|
||||
chaseCheckSeconds: 0.1
|
||||
chaseVerboseLog: 0
|
||||
|
|
|
|||
|
|
@ -104,6 +104,28 @@ namespace WL.Settings
|
|||
"끄면 #770 검증 시점 동작(Auto OFF 계정은 공격 버튼/Auto ON 이 시작점)으로 돌아간다. 펫·소환수·몬스터 무관.")]
|
||||
public bool autoEngage = true;
|
||||
|
||||
// ── #813t3 : 시야(탐색) · 추적(이동) 반경 ────────────────────────────────
|
||||
[Header("■ #813t3 — 시야·추적 반경 (PD 2026-09-11 「반경 3 m 로 제한」)")]
|
||||
[Tooltip("추적(이동) 반경(m). 이미 잡은 타깃이 이 거리를 넘어가면 놓는다 = 「3 m 밖 적은 무시」. " +
|
||||
"위 retargetSearchRange(탐색)는 '새로 고르는' 거리이고 이 값은 '계속 쫓는' 거리다 — " +
|
||||
"탐색만 줄이면 한 번 잡은 적은 죽을 때까지 따라간다(PD 관측 「한참 동안 적을 추적」). " +
|
||||
"0 이면 놓지 않는다(813t3 이전 동작). 보통 retargetSearchRange 와 같은 값으로 둔다.")]
|
||||
[Min(0f)] public float chaseRange = 0f;
|
||||
|
||||
[Tooltip("타깃을 놓을 때 원본 「락 타깃」(Actor.m_IsLockTarget)까지 무시할지. " +
|
||||
"꺼 두면 813w 가이드가 보스에 건 락은 유지된다(= 락이 걸린 보스는 반경 밖이어도 계속 쫓는다).")]
|
||||
public bool chaseDropIgnoresLock = false;
|
||||
|
||||
[Tooltip("타깃을 놓는 순간 달려가던 NavMesh 경로도 끊고 Idle 로 세울지. " +
|
||||
"🔴 조이스틱 조작 중이거나 813w 가이드가 이동을 쥐고 있을 때는 건드리지 않는다(둘이 싸우지 않게).")]
|
||||
public bool stopChaseMoveOnDrop = true;
|
||||
|
||||
[Tooltip("시야 가드 폴링 간격(초).")]
|
||||
[Min(0.02f)] public float chaseCheckSeconds = 0.1f;
|
||||
|
||||
[Tooltip("시야 가드가 타깃을 놓을 때 로그 1줄.")]
|
||||
public bool chaseVerboseLog = false;
|
||||
|
||||
// ── 정적 접근자 (에셋 부재 시 = PD 지시대로 동작하는 값) ─────────────────
|
||||
public static bool AutoEngage { get { var i = Instance; return i == null || i.autoEngage; } }
|
||||
public static float FrontConeDeg { get { var i = Instance; return i != null ? i.frontConeDeg : 90f; } }
|
||||
|
|
@ -113,5 +135,23 @@ namespace WL.Settings
|
|||
public static float ImmediateAttackMargin { get { var i = Instance; return i != null ? i.immediateAttackMargin : 0.2f; } }
|
||||
public static float RetargetSearchRange { get { var i = Instance; return i != null ? i.retargetSearchRange : 0f; } }
|
||||
public static bool PcAggroOnHit { get { var i = Instance; return i != null && i.pcAggroOnHit; } }
|
||||
|
||||
// #813t3 — 0 = 기존 동작(놓지 않음). 값은 에셋(SO)이 갖는다.
|
||||
public static float ChaseRange { get { var i = Instance; return i != null ? i.chaseRange : 0f; } }
|
||||
public static bool ChaseDropIgnoresLock { get { var i = Instance; return i != null && i.chaseDropIgnoresLock; } }
|
||||
public static bool StopChaseMoveOnDrop { get { var i = Instance; return i == null || i.stopChaseMoveOnDrop; } }
|
||||
public static float ChaseCheckSeconds { get { var i = Instance; return i != null ? i.chaseCheckSeconds : 0.1f; } }
|
||||
public static bool ChaseVerboseLog { get { var i = Instance; return i != null && i.chaseVerboseLog; } }
|
||||
|
||||
/// <summary>
|
||||
/// 프로브 전용(813w5 `WLBossArenaSettings.SetInstanceForProbe` 선례) — 임시 SO 를 잠시 Instance 자리에 끼운다.
|
||||
/// C8 진리표를 실 에셋을 건드리지 않고 검증하기 위한 것. null 을 주면 캐시를 비우고 실 에셋으로 돌아간다.
|
||||
/// </summary>
|
||||
public static void SetInstanceForProbe(WLTargetingSettings so)
|
||||
{
|
||||
if (so == null) { ClearCache(); return; }
|
||||
s_cached = so;
|
||||
s_lookupDone = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -75,7 +75,7 @@ NavMeshProjectSettings:
|
|||
agentTypeID: 0
|
||||
agentRadius: 0.5
|
||||
agentHeight: 2
|
||||
agentSlope: 45
|
||||
agentSlope: 30
|
||||
agentClimb: 0.75
|
||||
ledgeDropHeight: 0
|
||||
maxJumpAcrossDistance: 0
|
||||
|
|
|
|||
Loading…
Reference in New Issue