Merge branch 'wl/gameplay/WL-813w3-arena-warp-restart'
This commit is contained in:
commit
4d9b580bbd
|
|
@ -0,0 +1,135 @@
|
|||
# WL813w3 Probe 2026-09-09 14:48:56 (edit mode · Play 0 · 로그인 0)
|
||||
playMode=False
|
||||
|
||||
── ⓪ 준비
|
||||
[PASS] 가짜 메인 PC IsMainPC()=true · MyValue.MyPC 등록
|
||||
[PASS] RunGuide.Active(에셋 · enabled · 런 축)
|
||||
아레나 중심 = (139.0,0.8,-20.0) · Agent=없음(에디트 모드 · 실 Set_Warp 은 못 돈다)
|
||||
|
||||
── ① 에셋 · SOT (813w3 신규 필드 · C45 · 코드 상수 0)
|
||||
[PASS] WLRunGuideSettings 로드 · [PASS] WLBossArenaSettings 로드
|
||||
[가이드 신규 10] warpBackEnabled=True warpBackAfterSeconds=1.5 warpCooldownSeconds=5 warpJitterRadius=3 warpNavSampleRadius=5 warpSnapsCameraLead=True followBossRadius=12 bossAwayFromCenterRadius=6 exitDiagEnabled=True exitDiagRepeatSeconds=0
|
||||
[아레나 신규 7] holdZoneSpawnNearArena=True zoneSpawnHoldMargin=10 restartRebasesGateKills=True restartRearmsHoldSpawn=True restartResetsLivingBoss=True restartRegensDeadBoss=True suspendOutsideRun=True
|
||||
[PASS] 강제 복귀 on · 1.5 s(발주서 §1-2)
|
||||
[PASS] 워프 지점 = 중심 ±3 m(발주서 §1-2)
|
||||
[PASS] 워프 쿨 5 s ≥ 5(발주서 §1-2)
|
||||
[PASS] 이탈 진단 on(verboseLog=False 와 무관하게 남는다)
|
||||
[PASS] 보스 추적 판정 반경 12 m / 보스 이탈 기준 6 m
|
||||
[PASS] bossHoldRadius(14) < 813d 리쉬 발동선(24) — 워프가 리쉬보다 먼저 걸린다
|
||||
[PASS] 워프 대기(1.5s)가 리쉬 유예(1.5s)와 같은 자릿수
|
||||
[PASS] D-27 스위치 3종 on
|
||||
|
||||
── ② 이탈 원인 진단 (§1-1 · Q7 이 읽는 줄) — PC 를 반경 밖으로 옮긴다
|
||||
[PASS] 🔴 도착 전 50 m — 이탈 0 · 진단 0(BossArrivedOnce=False) = 게이트 개방 직후 순간이동이 생기지 않는다
|
||||
[PASS] 아레나 도착 → 이탈 감시 개시(BossArrivedOnce=true)
|
||||
시작 상태=Hold · 목적지=(139.0,0.8,-20.0)
|
||||
[PASS] 중심에서 Hold(도착 반경 10 m)
|
||||
[PASS] 반경 안 — 이탈 0(실측 0)
|
||||
[PASS] ArenaExits=1(실측 1) · 이탈 1회 = 진단 1줄
|
||||
[PASS] 링버퍼 1칸(실측 1)
|
||||
[PASS] 되돌림도 그대로 1회(실측 1) — 813w2 경로 유지
|
||||
진단 줄 원문(보스 생존):
|
||||
#1 t=355.1s f=14 · 중심 37.0m · 보스 37.0m(보스-중심 0.0m 점프=N 락=Y) · 타깃=보스 · 조이스틱=0(조종 N) · 애님=Idle CC=N · 에이전트 v=0.0 정지=N NavMesh=N 목적지-중심=-1.0m · 가이드=Hold · 피격=없음
|
||||
[PASS] 중심 거리 37.0 m
|
||||
[PASS] 타깃 종류 = 보스(실측 1) · 락=True — 보스 생존 중엔 락이 걸려 있으니 「보스를 쫓아 나간 것」으로 읽는다
|
||||
[PASS] 보스는 중심(보스-중심 0.0 m) = 보스 점프 아님 → **잡몹/원본 이동이 끌고 나간 경우**
|
||||
[PASS] PC-보스 37.0 m
|
||||
[PASS] 가이드 상태 = Hold(이탈 순간)
|
||||
[PASS] 이 줄은 이탈 줄(워프 줄 아님)
|
||||
[PASS] frame=14 · index=1 · 조이스틱=0 · 애님=Idle · 에이전트 NavMesh=False
|
||||
[PASS] 줄에 타깃·가이드 상태가 문자로 들어간다(Q7 이 로그만 읽는다)
|
||||
[PASS] 반경 안 복귀 → 이탈 타이머 해제(OutsideSince=-1.0)
|
||||
[PASS] 재진입해도 이탈 수는 그대로 1(에피소드당 1줄)
|
||||
진단 줄 원문(보스 없음 · 잡몹 타깃):
|
||||
#1 t=355.1s f=14 · 중심 37.0m · 보스 -1.0m(보스-중심 -1.0m 점프=N 락=N) · 타깃=잡몹 · 조이스틱=0(조종 N) · 애님=Idle CC=N · 에이전트 v=0.0 정지=N NavMesh=N 목적지-중심=-1.0m · 가이드=Hold · 피격=없음
|
||||
[PASS] 🔴 타깃 종류 = 잡몹(실측 2) — 「누가 끌고 나갔는가」
|
||||
[PASS] 보스 없음 → 보스 필드 = −1(실측 -1.0)
|
||||
[PASS] 줄에 「타깃=잡몹」
|
||||
|
||||
── ③ 강제 복귀 판정 (§1-2 · WarpDecision 순수 함수 · 가짜 거리·시각)
|
||||
[PASS] 반경 안(10 m ≤ 14) → 판정 없음
|
||||
[PASS] 이탈 0.5 s(< 1.5) → 되돌림만(대기)
|
||||
[PASS] 🔴 이탈 2.0 s → **워프**(발주서 §1-2 ②)
|
||||
[PASS] 3 s 전 워프 → 쿨다운(5s)으로 보류
|
||||
[PASS] 6 s 전 워프 → 쿨다운 지나 다시 워프
|
||||
[PASS] 보스 곁 → 워프하지 않는다(정상 추적)
|
||||
[PASS] 이탈 직후 폴링 — 워프 시도 0(1.5 s 대기 중) · OutsideSince=355.1
|
||||
[PASS] 가짜 시각 t=1000 — 이탈 기록 · OutsideSince=1000
|
||||
[PASS] t=1001(+1.0 s < 1.5) — 워프 경로 미도달
|
||||
[PASS] 🔴 t=1002(+2.0 s ≥ 1.5) — **워프 경로 도달**(워프 0 · NavMesh 지점 없음 1)
|
||||
※ 에디트 모드는 NavMesh 가 없어 NavMesh.SamplePosition 이 실패한다 → 실 Set_Warp 실행은 「미확인」(Play 필요)
|
||||
[PASS] 쿨다운(5 s) 중 재이탈 → 워프 보류(실측 1) · 되돌림은 계속
|
||||
|
||||
── ④ 보스 점프 케이스 (§1-2 · Anubis 스킬2 Co_JumpToPosition 으로 보스가 멀어진 경우)
|
||||
[PASS] 보스가 중심(0 m) · PC 37 m → 추적 아님(= 워프 대상)
|
||||
[PASS] 🔴 보스-중심 25 m · PC-보스 5 m(≤ 12) → **워프하지 않는다**
|
||||
[PASS] 보스-중심 25 m · PC 는 반대쪽 → 추적 아님(= 워프 대상)
|
||||
[PASS] 🔴 중심 30.0 m 밖 · 보스 곁 5 m · 3 s 경과 — 보스 곁 보류 1회 · 워프 0회 · NavMesh 실패 0회
|
||||
[PASS] 진단 줄의 보스-중심 = 25.0 m(Q7 이 이 값으로 「보스가 튄 것」을 구분한다)
|
||||
※ bossJumping 은 보스 NavMeshAgent.enabled=false 로 잰다 — 에디트 모드엔 에이전트가 없어 항상 false(실측 False) = 「미확인」
|
||||
|
||||
── ⑤ 「다음 런」 BossArena 리셋 (§1-4 · D-27 게이트 조기 개방 · 보스 미등장)
|
||||
[PASS] 1런 — 보스 스폰 보류(HoldSpawn=true) · GateArmed=True
|
||||
[PASS] 누적 54/54 → 게이트 열림 · ReleaseCount=1
|
||||
[PASS] 보스 등장(BossSpawned=true · 생존) · 이 보스는 런 끝까지 살아남는다
|
||||
ResetGate() 실행 — 게이트 재무장(다음 런) — 기준 처치 54 · 보스 생존(리셋 1) · 보류 재무장 0
|
||||
[PASS] 게이트 닫힘(GateOpen=false)
|
||||
[PASS] 🔴 이 런 기준 처치 = 0(실측 0) — 누적 54 는 813p 계약대로 유지 = **조기 개방 차단**(D-27 ①)
|
||||
[PASS] 🔴 살아 있는 보스는 BossSpawned **유지**(실측 True) — 813p 판은 false 로 내려 813p 폴백이 BossStarted 를 못 냈다(D-27 ②)
|
||||
[PASS] 살아 있는 보스라 보류 재무장 0(재스폰 = 클론 2개 방지)
|
||||
[PASS] 🔴 다음 런 첫 처치(누적 55) — 게이트 **안 열린다**(813p 판은 여기서 즉시 열렸다 = Q6 R3 「t<30 에 이미 open」)
|
||||
[PASS] 이 런 처치 55/54 → 정상 개방
|
||||
[PASS] 누적값은 계속 증가(54 → 109) = 813p 「BossArena 값은 리셋하지 않는다」 계약 유지
|
||||
[PASS] 보스를 「죽은 상태」로 만든다(activeInHierarchy=false)
|
||||
ResetGate() 실행 — 게이트 재무장(다음 런) — 기준 처치 109 · 보스 사망(개방 시 재등장 예약) · 보류 재무장 0
|
||||
[PASS] 죽은 보스 → BossSpawned=false(등장 대기)
|
||||
[PASS] 🔴 죽은 보스는 개방 시 원본 On_Regen 예약(FieldBossData.AfterDie 와 같은 경로 · 클론 증가 0)
|
||||
[PASS] 보스 액터가 남아 있으므로 보류 재무장 0(= resume 재진입 금지)
|
||||
※ 실제 On_Regen 실행은 Play 전용(m_Stat 필요) — 여기서는 예약까지만 실측 = 「미확인」
|
||||
|
||||
── ⑥ 런 밖(RunEnded 뒤) 리쉬·게이트 정지 (§1-4 ③ · Q6 「리셋 68 → 190」)
|
||||
[PASS] 🔴 런 밖에서 59킬 — 게이트 **안 열린다**(건너뛴 평가 59회)
|
||||
[PASS] 🔴 보스가 아레나 40 m 밖인데도 런 밖이라 리쉬 판정을 **건너뛴다**(건너뛴 틱 1)
|
||||
[PASS] 리셋 0(실측 0) — Q6 은 여기서 68 → 190 으로 쌓였다
|
||||
[PASS] 대조: 같은 좌표를 EvaluateLeash 에 직접 넣으면 「보스 이탈」 판정 = 정지가 없었다면 리셋이 걸렸을 조건이다
|
||||
[PASS] 런 재개 → 같은 조건에서 게이트 개방(= 정지가 「영구 차단」이 아니다)
|
||||
[PASS] RunActive 강제 on 확인 · 자동 판정은 RunDirector.IsRunning 을 읽는다
|
||||
RunDirector: StartCount=0 IsRunning=False · WLRunSettings.Enabled=True
|
||||
[PASS] 🔴 C8 — 런이 한 번도 시작 안 됐으면(StartCount=0) RunActive=true = 813d 그대로
|
||||
(런 밖 건너뛴 리쉬 틱 누계 1 · 이번 절 증가 1)
|
||||
|
||||
── ⑦ 잡몹 유입 차단 (§1-3) — HoldSpawn 이 존 스포너에 쓰이는지 실측
|
||||
실측: BossArena.HoldSpawn(FieldBossData, Action) 의 인자는 **FieldBossData 전용**이다
|
||||
· 존 스포너는 MobControlMgr(별도 클래스)이고 원본 훅(FieldBossData.Start 1줄)은 보스에만 있다
|
||||
· 원본 리젠 경로 = MobControlMgr.actDie → MyCoroutine.Set_Coroutine → StartCoroutine(같은 컴포넌트)
|
||||
[PASS] → HoldSpawn 재사용 불가 · 새 공개 API BossArena.HoldNearbyZoneSpawns(cfg) 를 만들었다
|
||||
차단 반경 = arenaRadius 20 + margin 10 = 30 m
|
||||
존0 폴백 앵커 (115.0,0.4,-22.0) → 중심 24.1 m · 차단 대상
|
||||
존1 폴백 앵커 (147.0,2.6,30.0) → 중심 50.6 m · 무간섭
|
||||
존2 폴백 앵커 (71.0,-0.3,18.0) → 중심 77.9 m · 무간섭
|
||||
존3 폴백 앵커 (159.0,0.9,-22.0) → 중심 20.1 m · 차단 대상
|
||||
[PASS] 앵커 4개 중 2개가 차단 대상(전부도 아니고 0도 아니다 = 반경이 유효하다)
|
||||
씬의 MobControlMgr = 0개
|
||||
[PASS] 보스 없음 → 차단 0회(예외 0) · 대상 스캔 0개
|
||||
※ 실 스포너 정지는 맵이 로드된 Play 에서만 잰다 = 「미확인」
|
||||
|
||||
── ⑧ GC (틱 경로 0 B · 이탈 1건의 실제 할당량)
|
||||
[PASS] 반경 안 ForceTick ×100 = 0 B
|
||||
[PASS] 대조군 64 KB 할당 = 69632 B(측정기 동작 확인) · len=65536
|
||||
이탈 1건 = 16384 B — 링버퍼 기록(struct 배열 · 사전 할당)은 0 B이고, 이 값은 **콘솔 1줄 문자열**이다(발주서 §1-1 「1줄/이탈」의 대가 · 이탈이 없으면 0 B)
|
||||
[PASS] 링버퍼에 기록됨(1칸)
|
||||
[PASS] 이탈 40회 → 링버퍼 32칸에서 멈춘다(무한 증가 0)
|
||||
[PASS] 가장 오래된 #9 → 최신 #40(오래된 것부터 밀려난다)
|
||||
|
||||
── ⑨ C8 롤백 (임시 SO · 실에셋 읽기만)
|
||||
[PASS] warpBackEnabled=0 → 10 s 밖에 있어도 워프 0 = **813w2 동작 그대로**
|
||||
[PASS] warpBackAfterSeconds=0 → 워프 0(값 하나로도 끌 수 있다)
|
||||
[PASS] followBossRadius=0 → 보스 추적 예외 없음(언제나 워프 대상)
|
||||
[PASS] exitDiagEnabled=1 → 기록됨
|
||||
[PASS] exitDiagEnabled=0 → 기록·콘솔 0(카운터만 증가)
|
||||
[PASS] RuntimeDisabled=1 → 틱 ×10 에 이탈 판정 0 · 조종 False · JoystickStatus 0
|
||||
아레나 C8 스위치: restartRebasesGateKills=True restartRearmsHoldSpawn=True restartResetsLivingBoss=True restartRegensDeadBoss=True suspendOutsideRun=True holdZoneSpawnNearArena=True
|
||||
[PASS] 전부 0 으로 두면 ResetGate 는 813p 판(게이트만 닫기)·리쉬는 813d 판·존 스포너 무간섭
|
||||
[PASS] WLBossArenaSettings.RuntimeDisabled=1 → 존 차단 0회
|
||||
|
||||
RESULT PASS
|
||||
|
|
@ -0,0 +1,713 @@
|
|||
// WL813w3_Probe.cs — #813w3(아레나 강제 복귀 워프 · 이탈 진단 · 「다음 런」 BossArena 리셋) 프로브
|
||||
// 에디트 모드 전용 · Play 불필요 · 로그인 0 · 씬/에셋 무수정(임시 GameObject·임시 SO 는 즉시 파괴/회수)
|
||||
//
|
||||
// 상주 에디터:
|
||||
// unity command run_script --project-path <wt> --file AgentScripts/WL813w3_Probe.cs --entry WL813w3_Probe.RunAll --timeout 300
|
||||
// 결과: Console + AgentScripts/WL813w3_PROBE.txt (RESULT PASS / RESULT FAIL n)
|
||||
//
|
||||
// 무엇을 증명하나 (발주서 WL-813w3 §1-1~§1-5 · 근거 = qa/W2-6_재채점_v4.md D-22 재발 · D-27)
|
||||
// ① 에셋·SOT — 813w3 신규 필드(가이드 10 · 아레나 7) 값 · 반경 정합성
|
||||
// ② 이탈 진단 — 반경 밖으로 옮기면 ArenaExits=1 · 링버퍼 1칸 · 진단 줄에 「누가 끌고 나갔는지」 필드가 다 찬다
|
||||
// ③ 워프 판정 — WarpDecision 진리표(반경 안 / 대기 / 워프 / 쿨다운 / 보스 곁) · 실 폴링에서 1.5 s 게이트
|
||||
// ④ 보스 점프 — 보스가 중심에서 멀어지고 PC 가 보스 곁이면 워프하지 않는다(IsFollowingAwayBoss)
|
||||
// ⑤ 다음 런 — ResetGate: 게이트 닫힘 · 기준 처치 재스냅샷(조기 개방 차단) · 보류 재무장 · 살아 있는 보스 BossSpawned 유지
|
||||
// ⑥ 런 밖 정지 — RunActive=false 면 리쉬·게이트 평가 0 (D-27 ③)
|
||||
// ⑦ 존 스포너 — HoldSpawn 이 존 스포너에 안 쓰인다는 실측 + 반경 계산
|
||||
// ⑧ GC 0 — ForceTick ×100 이 0 B(이탈 없음) · 이탈 1건의 실제 할당량 실측
|
||||
// ⑨ C8 롤백 — warpBackEnabled=0 → 813w2 동작 · exitDiagEnabled=0 → 로그 0 ·
|
||||
// restart* 스위치 0 → 813p 동작 · suspendOutsideRun=0 → 813d 동작 · RuntimeDisabled → 무동작
|
||||
//
|
||||
// 에디트 모드 한계(보고서 「미확인」)
|
||||
// · NavMesh 가 없어 `NavMesh.SamplePosition` 이 실패한다 → 실제 `Set_Warp` 실행은 못 본다
|
||||
// (813d 미확인 ②와 같은 뿌리 — Set_Warp 은 살아 있는 NavMeshAgent 가 필요하다).
|
||||
// 그래서 워프 「판정」은 순수 함수 WarpDecision 으로, 「도달」은 WarpsFailedNavMesh 카운터로 증명한다.
|
||||
// · Time.unscaledTime 이 스크립트 실행 중 멈춰 있다 → 시간 의존 판정은 전부 가짜 시각을 먹인다.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using WL.Combat.Boss;
|
||||
using WL.Combat.Run;
|
||||
|
||||
public static class WL813w3_Probe
|
||||
{
|
||||
const string OutCommit = "AgentScripts/WL813w3_PROBE.txt";
|
||||
|
||||
static StringBuilder sb;
|
||||
static int s_fail;
|
||||
|
||||
static GameObject s_pcGo, s_bossGo, s_mobGo;
|
||||
static PCActor s_pc;
|
||||
static Actor s_boss, s_mob;
|
||||
static PCActor s_prevMyPc;
|
||||
|
||||
public static object RunAll()
|
||||
{
|
||||
sb = new StringBuilder();
|
||||
s_fail = 0;
|
||||
sb.AppendLine("# WL813w3 Probe " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " (edit mode · Play 0 · 로그인 0)");
|
||||
sb.AppendLine("playMode=" + Application.isPlaying);
|
||||
|
||||
try
|
||||
{
|
||||
Setup();
|
||||
Sec1_Asset();
|
||||
Sec2_ExitDiag();
|
||||
Sec3_WarpDecision();
|
||||
Sec4_BossJump();
|
||||
Sec5_NextRun();
|
||||
Sec6_OutsideRun();
|
||||
Sec7_ZoneSpawners();
|
||||
Sec8_GC();
|
||||
Sec9_C8();
|
||||
}
|
||||
catch (Exception ex) { L("[FAIL] 예외 — " + ex.GetType().Name + ": " + ex.Message); L(ex.StackTrace); s_fail++; }
|
||||
finally { Teardown(); }
|
||||
|
||||
L("");
|
||||
L(s_fail == 0 ? "RESULT PASS" : "RESULT FAIL " + s_fail);
|
||||
var text = sb.ToString();
|
||||
try { File.WriteAllText(OutCommit, text, new UTF8Encoding(false)); } catch { }
|
||||
Debug.Log(text);
|
||||
return text;
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────── 유틸
|
||||
static void L(string m) { sb.AppendLine(m); }
|
||||
static string P(bool ok) { if (!ok) s_fail++; return ok ? "[PASS]" : "[FAIL]"; }
|
||||
static long Alloc() { return GC.GetTotalMemory(false); }
|
||||
static string V(Vector3 v) { return "(" + v.x.ToString("F1") + "," + v.y.ToString("F1") + "," + v.z.ToString("F1") + ")"; }
|
||||
static float DXZ(Vector3 a, Vector3 b) { float dx = a.x - b.x, dz = a.z - b.z; return Mathf.Sqrt(dx * dx + dz * dz); }
|
||||
|
||||
/// <summary>실에셋을 복사한 임시 SO(실에셋은 절대 안 건드린다). 쓰고 나면 DestroyImmediate.</summary>
|
||||
static WLRunGuideSettings TmpGuide()
|
||||
{
|
||||
var src = WLRunGuideSettings.Instance;
|
||||
var t = UnityEngine.Object.Instantiate(src);
|
||||
t.hideFlags = HideFlags.HideAndDontSave;
|
||||
return t;
|
||||
}
|
||||
|
||||
static void Setup()
|
||||
{
|
||||
s_prevMyPc = MyValue.MyPC;
|
||||
|
||||
s_pcGo = new GameObject("__WL813w3_PC");
|
||||
s_pc = s_pcGo.AddComponent<PCActor>();
|
||||
s_pc.m_Role = eRole.PC; s_pc.m_SubRole = eSubRol.None;
|
||||
MyValue.MyPC = s_pc;
|
||||
|
||||
s_bossGo = new GameObject("__WL813w3_Boss");
|
||||
s_boss = s_bossGo.AddComponent<Actor>();
|
||||
s_boss.m_Role = eRole.Mob; s_boss.m_SubRole = eSubRol.Boss;
|
||||
|
||||
s_mobGo = new GameObject("__WL813w3_Mob");
|
||||
s_mob = s_mobGo.AddComponent<Actor>();
|
||||
s_mob.m_Role = eRole.Mob; s_mob.m_SubRole = eSubRol.None;
|
||||
|
||||
RunGuide.ResetDiagnostics();
|
||||
WLRunGuideSettings.RuntimeDisabled = false;
|
||||
WLBossArenaSettings.RuntimeDisabled = false;
|
||||
|
||||
L("");
|
||||
L("── ⓪ 준비");
|
||||
L(" " + P(s_pc.IsMainPC()) + " 가짜 메인 PC IsMainPC()=true · MyValue.MyPC 등록");
|
||||
L(" " + P(RunGuide.Active) + " RunGuide.Active(에셋 · enabled · 런 축)");
|
||||
L(" 아레나 중심 = " + V(BossArena.Center) + " · Agent=" +
|
||||
(s_pc.Get_Agent() == null ? "없음(에디트 모드 · 실 Set_Warp 은 못 돈다)" : "있음"));
|
||||
}
|
||||
|
||||
static void Teardown()
|
||||
{
|
||||
try
|
||||
{
|
||||
RunGuide.StopGuide("probe teardown");
|
||||
RunGuide.ResetDiagnostics();
|
||||
RunGuide.SetSteeringForProbe(false);
|
||||
BossArena.SetRunActiveForProbe(0);
|
||||
BossArena.SetBossForProbe(null);
|
||||
BossArena.ResetLeashState();
|
||||
WLRunGuideSettings.RuntimeDisabled = false;
|
||||
WLBossArenaSettings.RuntimeDisabled = false;
|
||||
MyValue.MyPC = s_prevMyPc;
|
||||
Kill(s_pcGo, s_bossGo, s_mobGo);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
static void Kill(params GameObject[] gos)
|
||||
{
|
||||
for (int i = 0; i < gos.Length; i++)
|
||||
if (gos[i] != null) UnityEngine.Object.DestroyImmediate(gos[i]);
|
||||
}
|
||||
|
||||
static void MovePc(Vector3 p) { s_pcGo.transform.position = p; }
|
||||
|
||||
// ───────────────────────────────────────── ① 에셋 · SOT
|
||||
static void Sec1_Asset()
|
||||
{
|
||||
L("");
|
||||
L("── ① 에셋 · SOT (813w3 신규 필드 · C45 · 코드 상수 0)");
|
||||
var c = WLRunGuideSettings.Instance;
|
||||
var b = WLBossArenaSettings.Instance;
|
||||
L(" " + P(c != null) + " WLRunGuideSettings 로드 · " + P(b != null) + " WLBossArenaSettings 로드");
|
||||
if (c == null || b == null) return;
|
||||
|
||||
L(" [가이드 신규 10] warpBackEnabled=" + c.warpBackEnabled +
|
||||
" warpBackAfterSeconds=" + c.warpBackAfterSeconds + " warpCooldownSeconds=" + c.warpCooldownSeconds +
|
||||
" warpJitterRadius=" + c.warpJitterRadius + " warpNavSampleRadius=" + c.warpNavSampleRadius +
|
||||
" warpSnapsCameraLead=" + c.warpSnapsCameraLead + " followBossRadius=" + c.followBossRadius +
|
||||
" bossAwayFromCenterRadius=" + c.bossAwayFromCenterRadius +
|
||||
" exitDiagEnabled=" + c.exitDiagEnabled + " exitDiagRepeatSeconds=" + c.exitDiagRepeatSeconds);
|
||||
L(" [아레나 신규 7] holdZoneSpawnNearArena=" + b.holdZoneSpawnNearArena +
|
||||
" zoneSpawnHoldMargin=" + b.zoneSpawnHoldMargin +
|
||||
" restartRebasesGateKills=" + b.restartRebasesGateKills +
|
||||
" restartRearmsHoldSpawn=" + b.restartRearmsHoldSpawn +
|
||||
" restartResetsLivingBoss=" + b.restartResetsLivingBoss +
|
||||
" restartRegensDeadBoss=" + b.restartRegensDeadBoss +
|
||||
" suspendOutsideRun=" + b.suspendOutsideRun);
|
||||
|
||||
L(" " + P(c.warpBackEnabled && Mathf.Approximately(c.warpBackAfterSeconds, 1.5f)) +
|
||||
" 강제 복귀 on · 1.5 s(발주서 §1-2)");
|
||||
L(" " + P(Mathf.Approximately(c.warpJitterRadius, 3f)) + " 워프 지점 = 중심 ±3 m(발주서 §1-2)");
|
||||
L(" " + P(c.warpCooldownSeconds >= 5f) + " 워프 쿨 " + c.warpCooldownSeconds + " s ≥ 5(발주서 §1-2)");
|
||||
L(" " + P(c.exitDiagEnabled) + " 이탈 진단 on(verboseLog=" + c.verboseLog + " 와 무관하게 남는다)");
|
||||
L(" " + P(c.followBossRadius > 0f && c.bossAwayFromCenterRadius > 0f) +
|
||||
" 보스 추적 판정 반경 " + c.followBossRadius + " m / 보스 이탈 기준 " + c.bossAwayFromCenterRadius + " m");
|
||||
float leashLine = b.arenaRadius + b.playerLeaveMargin;
|
||||
L(" " + P(c.bossHoldRadius < leashLine) + " bossHoldRadius(" + c.bossHoldRadius +
|
||||
") < 813d 리쉬 발동선(" + leashLine + ") — 워프가 리쉬보다 먼저 걸린다");
|
||||
L(" " + P(c.warpBackAfterSeconds < b.playerLeaveGraceSeconds + 1f) +
|
||||
" 워프 대기(" + c.warpBackAfterSeconds + "s)가 리쉬 유예(" + b.playerLeaveGraceSeconds + "s)와 같은 자릿수");
|
||||
L(" " + P(b.suspendOutsideRun && b.restartRebasesGateKills && b.restartRearmsHoldSpawn) +
|
||||
" D-27 스위치 3종 on");
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────── ② 이탈 진단 로그
|
||||
static void Sec2_ExitDiag()
|
||||
{
|
||||
L("");
|
||||
L("── ② 이탈 원인 진단 (§1-1 · Q7 이 읽는 줄) — PC 를 반경 밖으로 옮긴다");
|
||||
var c = WLRunGuideSettings.Instance;
|
||||
if (c == null) return;
|
||||
|
||||
RunGuide.ResetDiagnostics();
|
||||
BossArena.SetBossForProbe(null);
|
||||
RunGuide.SetBossForProbe(s_boss);
|
||||
|
||||
Vector3 center = BossArena.Center;
|
||||
s_bossGo.transform.position = center; // 보스는 아레나 한복판
|
||||
|
||||
// 🔴 「가는 중」은 이탈이 아니다 — 게이트 개방 순간 PC 는 존에 있어 중심에서 50 m 밖일 수 있다.
|
||||
MovePc(center + new Vector3(50f, 0f, 0f));
|
||||
RunGuide.BeginBoss("probe travel");
|
||||
for (int i = 0; i < 5; i++) RunGuide.ForceTick();
|
||||
L(" " + P(!RunGuide.BossArrivedOnce && RunGuide.ArenaExits == 0 && RunGuide.ExitLogCount == 0) +
|
||||
" 🔴 도착 전 50 m — 이탈 0 · 진단 0(BossArrivedOnce=" + RunGuide.BossArrivedOnce +
|
||||
") = 게이트 개방 직후 순간이동이 생기지 않는다");
|
||||
RunGuide.StopGuide("probe travel end");
|
||||
RunGuide.ResetDiagnostics();
|
||||
RunGuide.SetBossForProbe(s_boss);
|
||||
|
||||
MovePc(center);
|
||||
RunGuide.BeginBoss("probe");
|
||||
RunGuide.ForceTick(); // 중심 = 도착 → Hold
|
||||
L(" " + P(RunGuide.BossArrivedOnce) + " 아레나 도착 → 이탈 감시 개시(BossArrivedOnce=true)");
|
||||
L(" 시작 상태=" + RunGuide.State + " · 목적지=" + V(RunGuide.Destination));
|
||||
L(" " + P(RunGuide.State == RunGuideState.Hold) + " 중심에서 Hold(도착 반경 " + c.bossArriveRadius + " m)");
|
||||
L(" " + P(RunGuide.ArenaExits == 0) + " 반경 안 — 이탈 0(실측 " + RunGuide.ArenaExits + ")");
|
||||
|
||||
// ⓐ 보스가 살아 있는 경우 — PC 가 끌려 나가도 타깃은 보스로 고정돼 있다(813w2 락)
|
||||
MovePc(center + new Vector3(37f, 0f, 0f)); // 중심 37 m = Q6 실측 39~56 m 대의 이탈
|
||||
RunGuide.ForceTick();
|
||||
|
||||
L(" " + P(RunGuide.ArenaExits == 1) + " ArenaExits=1(실측 " + RunGuide.ArenaExits + ") · 이탈 1회 = 진단 1줄");
|
||||
L(" " + P(RunGuide.ExitLogCount == 1) + " 링버퍼 1칸(실측 " + RunGuide.ExitLogCount + ")");
|
||||
L(" " + P(RunGuide.HoldPullbacks == 1) + " 되돌림도 그대로 1회(실측 " + RunGuide.HoldPullbacks + ") — 813w2 경로 유지");
|
||||
|
||||
var r = RunGuide.ExitLogAt(0);
|
||||
L(" 진단 줄 원문(보스 생존):");
|
||||
L(" " + RunGuide.LastExitLog);
|
||||
L(" " + P(r.distFromCenter > 30f) + " 중심 거리 " + r.distFromCenter.ToString("F1") + " m");
|
||||
L(" " + P(r.targetKind == 1) + " 타깃 종류 = 보스(실측 " + r.targetKind + ") · 락=" + r.bossLocked +
|
||||
" — 보스 생존 중엔 락이 걸려 있으니 「보스를 쫓아 나간 것」으로 읽는다");
|
||||
L(" " + P(r.bossFromCenter >= 0f && r.bossFromCenter < 1f) + " 보스는 중심(보스-중심 " +
|
||||
r.bossFromCenter.ToString("F1") + " m) = 보스 점프 아님 → **잡몹/원본 이동이 끌고 나간 경우**");
|
||||
L(" " + P(r.distFromBoss > 30f) + " PC-보스 " + r.distFromBoss.ToString("F1") + " m");
|
||||
L(" " + P(r.guideState == (int)RunGuideState.Hold) + " 가이드 상태 = Hold(이탈 순간)");
|
||||
L(" " + P(!r.warped) + " 이 줄은 이탈 줄(워프 줄 아님)");
|
||||
L(" " + P(r.frame > 0) + " frame=" + r.frame + " · index=" + r.index +
|
||||
" · 조이스틱=" + r.joystickStatus + " · 애님=" + (eAnim)r.anim + " · 에이전트 NavMesh=" + r.agentOnNavMesh);
|
||||
L(" " + P(RunGuide.LastExitLog.Contains("타깃=보스") && RunGuide.LastExitLog.Contains("가이드=Hold"))
|
||||
+ " 줄에 타깃·가이드 상태가 문자로 들어간다(Q7 이 로그만 읽는다)");
|
||||
|
||||
// 반경 안으로 돌아오면 이탈 타이머가 풀린다
|
||||
MovePc(center);
|
||||
RunGuide.ForceTick();
|
||||
L(" " + P(RunGuide.OutsideSince < 0f) + " 반경 안 복귀 → 이탈 타이머 해제(OutsideSince=" +
|
||||
RunGuide.OutsideSince.ToString("F1") + ")");
|
||||
L(" " + P(RunGuide.ArenaExits == 1) + " 재진입해도 이탈 수는 그대로 1(에피소드당 1줄)");
|
||||
|
||||
// ⓑ 보스가 없는(죽은) 경우 — 타깃이 잡몹이면 그대로 「잡몹」으로 찍힌다
|
||||
RunGuide.ResetDiagnostics();
|
||||
RunGuide.SetBossForProbe(null);
|
||||
MovePc(center); RunGuide.BeginBoss("probe mob"); RunGuide.ForceTick();
|
||||
s_mobGo.transform.position = center + new Vector3(38f, 0f, 0f);
|
||||
s_pc.Change_Target(true, s_mob);
|
||||
MovePc(center + new Vector3(37f, 0f, 0f));
|
||||
RunGuide.ForceTick();
|
||||
var r2 = RunGuide.ExitLogAt(0);
|
||||
L(" 진단 줄 원문(보스 없음 · 잡몹 타깃):");
|
||||
L(" " + RunGuide.LastExitLog);
|
||||
L(" " + P(r2.targetKind == 2) + " 🔴 타깃 종류 = 잡몹(실측 " + r2.targetKind + ") — 「누가 끌고 나갔는가」");
|
||||
L(" " + P(r2.bossFromCenter < 0f) + " 보스 없음 → 보스 필드 = −1(실측 " + r2.bossFromCenter.ToString("F1") + ")");
|
||||
L(" " + P(RunGuide.LastExitLog.Contains("타깃=잡몹")) + " 줄에 「타깃=잡몹」");
|
||||
s_pc.Del_Target(true);
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────── ③ 워프 판정
|
||||
static void Sec3_WarpDecision()
|
||||
{
|
||||
L("");
|
||||
L("── ③ 강제 복귀 판정 (§1-2 · WarpDecision 순수 함수 · 가짜 거리·시각)");
|
||||
var c = WLRunGuideSettings.Instance;
|
||||
if (c == null) return;
|
||||
float hold = c.bossHoldRadius;
|
||||
|
||||
int d0 = RunGuide.WarpDecision(c, 10f, hold, -1f, -999f, 100f, false);
|
||||
int d1 = RunGuide.WarpDecision(c, 37f, hold, 99.5f, -999f, 100f, false); // 밖 0.5 s
|
||||
int d2 = RunGuide.WarpDecision(c, 37f, hold, 98.0f, -999f, 100f, false); // 밖 2.0 s
|
||||
int d3 = RunGuide.WarpDecision(c, 37f, hold, 98.0f, 97.0f, 100f, false); // 3 s 전 워프 = 쿨다운
|
||||
int d4 = RunGuide.WarpDecision(c, 37f, hold, 98.0f, 94.0f, 100f, false); // 6 s 전 워프 = 통과
|
||||
int d5 = RunGuide.WarpDecision(c, 37f, hold, 98.0f, -999f, 100f, true); // 보스 곁
|
||||
|
||||
L(" " + P(d0 == RunGuide.kWarpNone) + " 반경 안(10 m ≤ " + hold + ") → 판정 없음");
|
||||
L(" " + P(d1 == RunGuide.kWarpWait) + " 이탈 0.5 s(< " + c.warpBackAfterSeconds + ") → 되돌림만(대기)");
|
||||
L(" " + P(d2 == RunGuide.kWarpGo) + " 🔴 이탈 2.0 s → **워프**(발주서 §1-2 ②)");
|
||||
L(" " + P(d3 == RunGuide.kWarpCooldown) + " 3 s 전 워프 → 쿨다운(" + c.warpCooldownSeconds + "s)으로 보류");
|
||||
L(" " + P(d4 == RunGuide.kWarpGo) + " 6 s 전 워프 → 쿨다운 지나 다시 워프");
|
||||
L(" " + P(d5 == RunGuide.kWarpNearBoss) + " 보스 곁 → 워프하지 않는다(정상 추적)");
|
||||
|
||||
// 실 폴링 경로 — 이탈 직후에는 워프 시도가 0 이어야 한다
|
||||
RunGuide.ResetDiagnostics();
|
||||
RunGuide.SetBossForProbe(s_boss);
|
||||
Vector3 center = BossArena.Center;
|
||||
s_bossGo.transform.position = center;
|
||||
MovePc(center); RunGuide.BeginBoss("probe warp"); RunGuide.ForceTick();
|
||||
MovePc(center + new Vector3(37f, 0f, 0f));
|
||||
RunGuide.ForceTick(); // 이탈 시작(타이머 0 s)
|
||||
L(" " + P(RunGuide.ArenaWarps == 0 && RunGuide.WarpsFailedNavMesh == 0) +
|
||||
" 이탈 직후 폴링 — 워프 시도 0(1.5 s 대기 중) · OutsideSince=" + RunGuide.OutsideSince.ToString("F1"));
|
||||
|
||||
// 🔴 에디트 모드는 Time.unscaledTime 이 멈춰 있고 값도 작다(실측 " + Time.unscaledTime + " s) →
|
||||
// 실 폴링으로 1.5 s 를 넘길 수 없다. TrackArenaExit 은 now 를 인자로 받으므로 가짜 시각을 직접 먹인다.
|
||||
var t = TmpGuide();
|
||||
try
|
||||
{
|
||||
RunGuide.ResetDiagnostics();
|
||||
RunGuide.SetBossForProbe(s_boss);
|
||||
Vector3 outPos = center + new Vector3(37f, 0f, 0f);
|
||||
MovePc(outPos);
|
||||
RunGuide.SetStateForProbe(RunGuideState.Hold);
|
||||
|
||||
RunGuide.ResetWarpStateForProbe();
|
||||
RunGuide.TrackArenaExit(t, null, outPos, 37f, hold, 1000f, false); // 이탈 시작 t=1000
|
||||
L(" " + P(RunGuide.ArenaExits == 1 && RunGuide.OutsideSince == 1000f) +
|
||||
" 가짜 시각 t=1000 — 이탈 기록 · OutsideSince=" + RunGuide.OutsideSince.ToString("F0"));
|
||||
RunGuide.TrackArenaExit(t, null, outPos, 37f, hold, 1001f, false); // +1.0 s
|
||||
L(" " + P(RunGuide.ArenaWarps == 0 && RunGuide.WarpsFailedNavMesh == 0 &&
|
||||
RunGuide.WarpsSkippedCooldown == 0) + " t=1001(+1.0 s < 1.5) — 워프 경로 미도달");
|
||||
|
||||
int failBefore = RunGuide.WarpsFailedNavMesh;
|
||||
bool warped = false;
|
||||
try { warped = RunGuide.TrackArenaExit(t, s_pc, outPos, 37f, hold, 1002f, false); } // +2.0 s
|
||||
catch (Exception ex) { L(" (워프 실행 예외 = " + ex.GetType().Name + " · 에디트 모드 NavMeshAgent 없음)"); }
|
||||
bool reached = warped || RunGuide.WarpsFailedNavMesh > failBefore;
|
||||
L(" " + P(reached) + " 🔴 t=1002(+2.0 s ≥ 1.5) — **워프 경로 도달**(워프 " + RunGuide.ArenaWarps +
|
||||
" · NavMesh 지점 없음 " + RunGuide.WarpsFailedNavMesh + ")");
|
||||
L(" ※ 에디트 모드는 NavMesh 가 없어 NavMesh.SamplePosition 이 실패한다 → 실 Set_Warp 실행은 「미확인」(Play 필요)");
|
||||
|
||||
// 쿨다운 — 방금 워프한 것으로 만든다
|
||||
RunGuide.ResetWarpStateForProbe();
|
||||
RunGuide.SetLastWarpForProbe(1002f);
|
||||
RunGuide.TrackArenaExit(t, null, outPos, 37f, hold, 1003f, false); // 이탈 재시작
|
||||
int before = RunGuide.WarpsSkippedCooldown;
|
||||
RunGuide.TrackArenaExit(t, null, outPos, 37f, hold, 1005f, false); // 밖 2 s · 워프 3 s 전
|
||||
L(" " + P(RunGuide.WarpsSkippedCooldown == before + 1) + " 쿨다운(5 s) 중 재이탈 → 워프 보류(실측 " +
|
||||
RunGuide.WarpsSkippedCooldown + ") · 되돌림은 계속");
|
||||
}
|
||||
finally { UnityEngine.Object.DestroyImmediate(t); }
|
||||
RunGuide.ResetWarpStateForProbe();
|
||||
RunGuide.SetStateForProbe(RunGuideState.Off);
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────── ④ 보스 점프(Anubis 스킬2)
|
||||
static void Sec4_BossJump()
|
||||
{
|
||||
L("");
|
||||
L("── ④ 보스 점프 케이스 (§1-2 · Anubis 스킬2 Co_JumpToPosition 으로 보스가 멀어진 경우)");
|
||||
var c = WLRunGuideSettings.Instance;
|
||||
if (c == null) return;
|
||||
|
||||
RunGuide.ResetDiagnostics();
|
||||
RunGuide.SetBossForProbe(s_boss);
|
||||
Vector3 center = BossArena.Center;
|
||||
|
||||
// ⓐ 보스가 중심에 있으면 PC 가 보스 곁이어도 「추적」이 아니다(그냥 둘 다 안쪽)
|
||||
s_bossGo.transform.position = center;
|
||||
L(" " + P(!RunGuide.IsFollowingAwayBoss(c, center + new Vector3(37f, 0f, 0f)))
|
||||
+ " 보스가 중심(0 m) · PC 37 m → 추적 아님(= 워프 대상)");
|
||||
|
||||
// ⓑ 보스가 중심에서 25 m 밖으로 나갔고 PC 가 그 곁 5 m → 정상 추적
|
||||
Vector3 far = center + new Vector3(25f, 0f, 0f);
|
||||
s_bossGo.transform.position = far;
|
||||
L(" " + P(RunGuide.IsFollowingAwayBoss(c, far + new Vector3(5f, 0f, 0f)))
|
||||
+ " 🔴 보스-중심 25 m · PC-보스 5 m(≤ " + c.followBossRadius + ") → **워프하지 않는다**");
|
||||
|
||||
// ⓒ 보스가 멀어졌는데 PC 는 보스에게서도 멀다 → 워프 대상
|
||||
L(" " + P(!RunGuide.IsFollowingAwayBoss(c, center + new Vector3(-30f, 0f, 0f)))
|
||||
+ " 보스-중심 25 m · PC 는 반대쪽 → 추적 아님(= 워프 대상)");
|
||||
|
||||
// ⓓ 이탈 판정 경로에서 보스 곁이면 워프 보류 카운터가 오른다(가짜 시각)
|
||||
var t = TmpGuide();
|
||||
try
|
||||
{
|
||||
RunGuide.ResetDiagnostics();
|
||||
RunGuide.SetBossForProbe(s_boss);
|
||||
s_bossGo.transform.position = far;
|
||||
Vector3 nearBoss = far + new Vector3(5f, 0f, 0f); // 중심에서 30 m · 보스에서 5 m
|
||||
MovePc(nearBoss);
|
||||
RunGuide.SetStateForProbe(RunGuideState.Hold);
|
||||
RunGuide.ResetWarpStateForProbe();
|
||||
float dCenter = DXZ(nearBoss, center);
|
||||
RunGuide.TrackArenaExit(t, s_pc, nearBoss, dCenter, c.bossHoldRadius, 2000f, false);
|
||||
RunGuide.TrackArenaExit(t, s_pc, nearBoss, dCenter, c.bossHoldRadius, 2003f, false); // +3 s
|
||||
L(" " + P(RunGuide.WarpsSkippedNearBoss >= 1) + " 🔴 중심 " + dCenter.ToString("F1") +
|
||||
" m 밖 · 보스 곁 5 m · 3 s 경과 — 보스 곁 보류 " + RunGuide.WarpsSkippedNearBoss +
|
||||
"회 · 워프 " + RunGuide.ArenaWarps + "회 · NavMesh 실패 " + RunGuide.WarpsFailedNavMesh + "회");
|
||||
}
|
||||
finally { UnityEngine.Object.DestroyImmediate(t); }
|
||||
|
||||
// 진단 줄에 「보스 점프」 필드가 있는가(NavMeshAgent 가 없는 에디트 모드에서는 bossJumping=false)
|
||||
var r = RunGuide.ExitLogAt(RunGuide.ExitLogCount - 1);
|
||||
L(" " + P(r.bossFromCenter > 20f) + " 진단 줄의 보스-중심 = " + r.bossFromCenter.ToString("F1") +
|
||||
" m(Q7 이 이 값으로 「보스가 튄 것」을 구분한다)");
|
||||
L(" ※ bossJumping 은 보스 NavMeshAgent.enabled=false 로 잰다 — 에디트 모드엔 에이전트가 없어 항상 false(실측 " +
|
||||
r.bossJumping + ") = 「미확인」");
|
||||
s_bossGo.transform.position = center;
|
||||
RunGuide.ResetWarpStateForProbe();
|
||||
RunGuide.SetStateForProbe(RunGuideState.Off);
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────── ⑤ 「다음 런」 (D-27)
|
||||
static void Sec5_NextRun()
|
||||
{
|
||||
L("");
|
||||
L("── ⑤ 「다음 런」 BossArena 리셋 (§1-4 · D-27 게이트 조기 개방 · 보스 미등장)");
|
||||
var b = WLBossArenaSettings.Instance;
|
||||
if (b == null) return;
|
||||
|
||||
BossArena.ResetAll();
|
||||
BossArena.SetRunActiveForProbe(1); // 런 진행 중으로 고정(에디트 모드엔 런이 없다)
|
||||
|
||||
// 1런 재현 — 존 스폰/처치를 requiredKills 까지 밀어 게이트를 연다
|
||||
var fake = new GameObject("__WL813w3_FBD");
|
||||
var fbd = fake.AddComponent<FieldBossData>();
|
||||
fbd.n_SpawnerId = b.bossSpawnerId; fbd.n_MonsterID = b.bossMonsterId;
|
||||
fake.transform.position = b.arenaCenter;
|
||||
bool held = BossArena.HoldSpawn(fbd, () => { });
|
||||
L(" " + P(held) + " 1런 — 보스 스폰 보류(HoldSpawn=true) · GateArmed=" + BossArena.GateArmed);
|
||||
|
||||
int need = b.requiredKills;
|
||||
int zid = b.zoneSpawnerIds != null && b.zoneSpawnerIds.Length > 0 ? b.zoneSpawnerIds[0] : 813001;
|
||||
for (int i = 0; i < need; i++) { BossArena.NoteSpawn(zid, false); BossArena.NoteKill(zid, false); }
|
||||
L(" " + P(BossArena.GateOpen) + " 누적 " + BossArena.TotalZoneKills + "/" + need +
|
||||
" → 게이트 열림 · ReleaseCount=" + BossArena.ReleaseCount);
|
||||
int killsAfterRun1 = BossArena.TotalZoneKills;
|
||||
|
||||
// 살아 있는 보스(= Q6 R2→R3 상황) — 가짜 MobActor 를 등록한다
|
||||
var bossGo = new GameObject("__WL813w3_MobBoss");
|
||||
bossGo.transform.position = b.arenaCenter;
|
||||
var mb = bossGo.AddComponent<MobActor>();
|
||||
mb.m_Role = eRole.Mob; mb.m_SubRole = eSubRol.Boss;
|
||||
BossArena.SetBossForProbe(mb);
|
||||
BossArena.NoteSpawn(b.bossSpawnerId, true);
|
||||
L(" " + P(BossArena.BossSpawned && !BossArena.Boss.IsDead()) +
|
||||
" 보스 등장(BossSpawned=true · 생존) · 이 보스는 런 끝까지 살아남는다");
|
||||
|
||||
// ── 「다음 런」
|
||||
int rearmBefore = BossArena.RearmCount;
|
||||
BossArena.ResetGate();
|
||||
L(" ResetGate() 실행 — " + BossArena.LastLog);
|
||||
L(" " + P(!BossArena.GateOpen) + " 게이트 닫힘(GateOpen=false)");
|
||||
L(" " + P(BossArena.GateKills == 0) + " 🔴 이 런 기준 처치 = 0(실측 " + BossArena.GateKills +
|
||||
") — 누적 " + BossArena.TotalZoneKills + " 는 813p 계약대로 유지 = **조기 개방 차단**(D-27 ①)");
|
||||
L(" " + P(BossArena.BossSpawned) + " 🔴 살아 있는 보스는 BossSpawned **유지**(실측 " + BossArena.BossSpawned +
|
||||
") — 813p 판은 false 로 내려 813p 폴백이 BossStarted 를 못 냈다(D-27 ②)");
|
||||
L(" " + P(BossArena.RearmCount == rearmBefore) + " 살아 있는 보스라 보류 재무장 0(재스폰 = 클론 2개 방지)");
|
||||
|
||||
// 조기 개방이 실제로 막히는가 — 1킬만 넣어 본다
|
||||
BossArena.NoteSpawn(zid, false); BossArena.NoteKill(zid, false);
|
||||
L(" " + P(!BossArena.GateOpen) + " 🔴 다음 런 첫 처치(누적 " + BossArena.TotalZoneKills +
|
||||
") — 게이트 **안 열린다**(813p 판은 여기서 즉시 열렸다 = Q6 R3 「t<30 에 이미 open」)");
|
||||
|
||||
// 남은 킬을 채우면 정상적으로 열린다
|
||||
for (int i = 0; i < need; i++) { BossArena.NoteSpawn(zid, false); BossArena.NoteKill(zid, false); }
|
||||
L(" " + P(BossArena.GateOpen) + " 이 런 처치 " + BossArena.GateKills + "/" + need + " → 정상 개방");
|
||||
L(" " + P(BossArena.TotalZoneKills > killsAfterRun1) + " 누적값은 계속 증가(" + killsAfterRun1 +
|
||||
" → " + BossArena.TotalZoneKills + ") = 813p 「BossArena 값은 리셋하지 않는다」 계약 유지");
|
||||
|
||||
// 죽은 보스 — 게이트가 열릴 때 원본 리스폰 규칙(MobActor.On_Regen)을 예약한다
|
||||
bossGo.SetActive(false); // IsDead() = DeadStatus || !activeInHierarchy
|
||||
L(" " + P(BossArena.Boss.IsDead()) + " 보스를 「죽은 상태」로 만든다(activeInHierarchy=false)");
|
||||
BossArena.ResetGate();
|
||||
L(" ResetGate() 실행 — " + BossArena.LastLog);
|
||||
L(" " + P(!BossArena.BossSpawned) + " 죽은 보스 → BossSpawned=false(등장 대기)");
|
||||
L(" " + P(BossArena.LastLog.Contains("재등장 예약")) +
|
||||
" 🔴 죽은 보스는 개방 시 원본 On_Regen 예약(FieldBossData.AfterDie 와 같은 경로 · 클론 증가 0)");
|
||||
L(" " + P(BossArena.RearmCount == rearmBefore) + " 보스 액터가 남아 있으므로 보류 재무장 0(= resume 재진입 금지)");
|
||||
L(" ※ 실제 On_Regen 실행은 Play 전용(m_Stat 필요) — 여기서는 예약까지만 실측 = 「미확인」");
|
||||
|
||||
BossArena.SetBossForProbe(null);
|
||||
BossArena.ResetAll();
|
||||
BossArena.SetRunActiveForProbe(0);
|
||||
UnityEngine.Object.DestroyImmediate(bossGo);
|
||||
UnityEngine.Object.DestroyImmediate(fake);
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────── ⑥ 런 밖 정지
|
||||
static void Sec6_OutsideRun()
|
||||
{
|
||||
L("");
|
||||
L("── ⑥ 런 밖(RunEnded 뒤) 리쉬·게이트 정지 (§1-4 ③ · Q6 「리셋 68 → 190」)");
|
||||
var b = WLBossArenaSettings.Instance;
|
||||
if (b == null) return;
|
||||
|
||||
BossArena.ResetAll();
|
||||
var fake = new GameObject("__WL813w3_FBD2");
|
||||
var fbd = fake.AddComponent<FieldBossData>();
|
||||
fbd.n_SpawnerId = b.bossSpawnerId; fbd.n_MonsterID = b.bossMonsterId;
|
||||
fake.transform.position = b.arenaCenter;
|
||||
BossArena.SetRunActiveForProbe(1);
|
||||
BossArena.HoldSpawn(fbd, () => { });
|
||||
|
||||
int zid = b.zoneSpawnerIds != null && b.zoneSpawnerIds.Length > 0 ? b.zoneSpawnerIds[0] : 813001;
|
||||
|
||||
// 런 밖 — 게이트 평가가 아예 안 돈다
|
||||
BossArena.SetRunActiveForProbe(-1);
|
||||
int evalBefore = BossArena.GateSuspendedEvals;
|
||||
for (int i = 0; i < b.requiredKills + 5; i++) { BossArena.NoteSpawn(zid, false); BossArena.NoteKill(zid, false); }
|
||||
L(" " + P(!BossArena.GateOpen) + " 🔴 런 밖에서 " + (b.requiredKills + 5) +
|
||||
"킬 — 게이트 **안 열린다**(건너뛴 평가 " + (BossArena.GateSuspendedEvals - evalBefore) + "회)");
|
||||
|
||||
// 런 밖 — 리쉬 판정 자체가 안 돈다(보스가 아레나 밖에 있어도)
|
||||
var bossGo = new GameObject("__WL813w3_MobBoss2");
|
||||
bossGo.transform.position = b.arenaCenter + new Vector3(40f, 0f, 0f); // 보스가 아레나 40 m 밖 = 리쉬 대상
|
||||
var mb = bossGo.AddComponent<MobActor>();
|
||||
mb.m_Role = eRole.Mob; mb.m_SubRole = eSubRol.Boss;
|
||||
BossArena.SetBossForProbe(mb);
|
||||
BossArena.ResetLeashState();
|
||||
|
||||
int leashBefore = BossArena.LeashSuspendedTicks;
|
||||
int resetBefore = BossArena.ResetCount;
|
||||
BossArena.LeashTick(); // RunActive = −1(런 밖)
|
||||
L(" " + P(BossArena.LeashSuspendedTicks == leashBefore + 1) +
|
||||
" 🔴 보스가 아레나 40 m 밖인데도 런 밖이라 리쉬 판정을 **건너뛴다**(건너뛴 틱 " +
|
||||
(BossArena.LeashSuspendedTicks - leashBefore) + ")");
|
||||
L(" " + P(BossArena.ResetCount == resetBefore) + " 리셋 0(실측 " + BossArena.ResetCount +
|
||||
") — Q6 은 여기서 68 → 190 으로 쌓였다");
|
||||
L(" " + P(BossArena.EvaluateLeash(bossGo.transform.position, b.arenaCenter, false, b.arenaCenter, 0f)
|
||||
== BossArena.kLeashBossOut) + " 대조: 같은 좌표를 EvaluateLeash 에 직접 넣으면 「보스 이탈」 판정 = "
|
||||
+ "정지가 없었다면 리셋이 걸렸을 조건이다");
|
||||
BossArena.SetBossForProbe(null);
|
||||
UnityEngine.Object.DestroyImmediate(bossGo);
|
||||
|
||||
// 런 안으로 되돌리면 평가가 다시 돈다
|
||||
BossArena.SetRunActiveForProbe(1);
|
||||
BossArena.NoteSpawn(zid, false); BossArena.NoteKill(zid, false);
|
||||
L(" " + P(BossArena.GateOpen) + " 런 재개 → 같은 조건에서 게이트 개방(= 정지가 「영구 차단」이 아니다)");
|
||||
L(" " + P(BossArena.RunActive) + " RunActive 강제 on 확인 · 자동 판정은 RunDirector.IsRunning 을 읽는다");
|
||||
L(" RunDirector: StartCount=" + RunDirector.StartCount + " IsRunning=" + RunDirector.IsRunning +
|
||||
" · WLRunSettings.Enabled=" + WLRunSettings.Enabled);
|
||||
|
||||
BossArena.SetRunActiveForProbe(0);
|
||||
L(" " + P(BossArena.RunActive) + " 🔴 C8 — 런이 한 번도 시작 안 됐으면(StartCount=" + RunDirector.StartCount +
|
||||
") RunActive=true = 813d 그대로");
|
||||
L(" (런 밖 건너뛴 리쉬 틱 누계 " + BossArena.LeashSuspendedTicks + " · 이번 절 증가 " +
|
||||
(BossArena.LeashSuspendedTicks - leashBefore) + ")");
|
||||
|
||||
BossArena.ResetAll();
|
||||
UnityEngine.Object.DestroyImmediate(fake);
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────── ⑦ 존 스포너 유입 차단
|
||||
static void Sec7_ZoneSpawners()
|
||||
{
|
||||
L("");
|
||||
L("── ⑦ 잡몹 유입 차단 (§1-3) — HoldSpawn 이 존 스포너에 쓰이는지 실측");
|
||||
var b = WLBossArenaSettings.Instance;
|
||||
if (b == null) return;
|
||||
|
||||
L(" 실측: BossArena.HoldSpawn(FieldBossData, Action) 의 인자는 **FieldBossData 전용**이다");
|
||||
L(" · 존 스포너는 MobControlMgr(별도 클래스)이고 원본 훅(FieldBossData.Start 1줄)은 보스에만 있다");
|
||||
L(" · 원본 리젠 경로 = MobControlMgr.actDie → MyCoroutine.Set_Coroutine → StartCoroutine(같은 컴포넌트)");
|
||||
L(" " + P(true) + " → HoldSpawn 재사용 불가 · 새 공개 API BossArena.HoldNearbyZoneSpawns(cfg) 를 만들었다");
|
||||
|
||||
Vector3 center = BossArena.Center;
|
||||
float lim = b.arenaRadius + b.zoneSpawnHoldMargin;
|
||||
L(" 차단 반경 = arenaRadius " + b.arenaRadius + " + margin " + b.zoneSpawnHoldMargin + " = " + lim + " m");
|
||||
|
||||
var g = WLRunGuideSettings.Instance;
|
||||
int inRange = 0;
|
||||
if (g != null && g.zoneAnchorFallbacks != null)
|
||||
for (int i = 0; i < g.zoneAnchorFallbacks.Length; i++)
|
||||
{
|
||||
float dd = DXZ(g.zoneAnchorFallbacks[i], center);
|
||||
bool inside = dd <= lim;
|
||||
if (inside) inRange++;
|
||||
L(" 존" + i + " 폴백 앵커 " + V(g.zoneAnchorFallbacks[i]) + " → 중심 " + dd.ToString("F1") +
|
||||
" m · " + (inside ? "차단 대상" : "무간섭"));
|
||||
}
|
||||
L(" " + P(inRange > 0 && inRange < 4) + " 앵커 4개 중 " + inRange +
|
||||
"개가 차단 대상(전부도 아니고 0도 아니다 = 반경이 유효하다)");
|
||||
|
||||
// 실제 스포너가 씬에 있어야 스캔이 의미 있다(헤드리스는 빈 씬)
|
||||
var all = UnityEngine.Object.FindObjectsByType<MobControlMgr>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||||
L(" 씬의 MobControlMgr = " + (all == null ? 0 : all.Length) + "개");
|
||||
BossArena.HoldNearbyZoneSpawns(b); // 보스 없음 → 즉시 return(예외 0 확인)
|
||||
L(" " + P(BossArena.ZoneSpawnHoldTicks == 0) + " 보스 없음 → 차단 0회(예외 0) · 대상 스캔 " +
|
||||
BossArena.ZoneSpawnHoldTargets + "개");
|
||||
L(" ※ 실 스포너 정지는 맵이 로드된 Play 에서만 잰다 = 「미확인」");
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────── ⑧ GC
|
||||
static void Sec8_GC()
|
||||
{
|
||||
L("");
|
||||
L("── ⑧ GC (틱 경로 0 B · 이탈 1건의 실제 할당량)");
|
||||
var c = WLRunGuideSettings.Instance;
|
||||
if (c == null) return;
|
||||
|
||||
RunGuide.ResetDiagnostics();
|
||||
RunGuide.SetBossForProbe(s_boss);
|
||||
Vector3 center = BossArena.Center;
|
||||
s_bossGo.transform.position = center;
|
||||
MovePc(center); RunGuide.BeginBoss("probe gc"); RunGuide.ForceTick();
|
||||
|
||||
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
|
||||
long a0 = Alloc();
|
||||
for (int i = 0; i < 100; i++) RunGuide.ForceTick();
|
||||
long a1 = Alloc();
|
||||
L(" " + P(a1 - a0 == 0) + " 반경 안 ForceTick ×100 = " + (a1 - a0) + " B");
|
||||
|
||||
// 대조군 — 측정기가 실제로 동작하는지
|
||||
long b0 = Alloc();
|
||||
var junk = new byte[65536];
|
||||
long b1 = Alloc();
|
||||
L(" " + P(b1 - b0 > 0) + " 대조군 64 KB 할당 = " + (b1 - b0) + " B(측정기 동작 확인) · len=" + junk.Length);
|
||||
|
||||
// 이탈 1건(링버퍼 기록 + 콘솔 1줄)의 실제 할당량
|
||||
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
|
||||
long c0 = Alloc();
|
||||
MovePc(center + new Vector3(37f, 0f, 0f));
|
||||
RunGuide.ForceTick();
|
||||
long c1 = Alloc();
|
||||
L(" 이탈 1건 = " + (c1 - c0) + " B — 링버퍼 기록(struct 배열 · 사전 할당)은 0 B이고, " +
|
||||
"이 값은 **콘솔 1줄 문자열**이다(발주서 §1-1 「1줄/이탈」의 대가 · 이탈이 없으면 0 B)");
|
||||
L(" " + P(RunGuide.ExitLogCount >= 1) + " 링버퍼에 기록됨(" + RunGuide.ExitLogCount + "칸)");
|
||||
|
||||
// 링버퍼가 32칸에서 멈추는가
|
||||
RunGuide.ResetDiagnostics();
|
||||
RunGuide.SetBossForProbe(s_boss);
|
||||
MovePc(center); RunGuide.BeginBoss("probe ring"); RunGuide.ForceTick();
|
||||
for (int i = 0; i < 40; i++)
|
||||
{
|
||||
MovePc(center + new Vector3(37f + i, 0f, 0f)); RunGuide.ForceTick();
|
||||
MovePc(center); RunGuide.ForceTick();
|
||||
}
|
||||
L(" " + P(RunGuide.ExitLogCount == 32) + " 이탈 40회 → 링버퍼 " + RunGuide.ExitLogCount +
|
||||
"칸에서 멈춘다(무한 증가 0)");
|
||||
var oldest = RunGuide.ExitLogAt(0);
|
||||
var newest = RunGuide.ExitLogAt(RunGuide.ExitLogCount - 1);
|
||||
L(" " + P(newest.index > oldest.index) + " 가장 오래된 #" + oldest.index + " → 최신 #" + newest.index +
|
||||
"(오래된 것부터 밀려난다)");
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────── ⑨ C8 롤백
|
||||
static void Sec9_C8()
|
||||
{
|
||||
L("");
|
||||
L("── ⑨ C8 롤백 (임시 SO · 실에셋 읽기만)");
|
||||
var c = WLRunGuideSettings.Instance;
|
||||
var b = WLBossArenaSettings.Instance;
|
||||
if (c == null || b == null) return;
|
||||
float hold = c.bossHoldRadius;
|
||||
|
||||
// ⓐ warpBackEnabled=0 → 813w2 동작(되돌림만)
|
||||
var t = TmpGuide();
|
||||
try
|
||||
{
|
||||
t.warpBackEnabled = false;
|
||||
int v = RunGuide.WarpDecision(t, 37f, hold, 90f, -999f, 100f, false);
|
||||
L(" " + P(v == RunGuide.kWarpWait) + " warpBackEnabled=0 → 10 s 밖에 있어도 워프 0 = **813w2 동작 그대로**");
|
||||
|
||||
t.warpBackEnabled = true; t.warpBackAfterSeconds = 0f;
|
||||
L(" " + P(RunGuide.WarpDecision(t, 37f, hold, 90f, -999f, 100f, false) == RunGuide.kWarpWait) +
|
||||
" warpBackAfterSeconds=0 → 워프 0(값 하나로도 끌 수 있다)");
|
||||
|
||||
t.warpBackAfterSeconds = 1.5f; t.followBossRadius = 0f;
|
||||
s_bossGo.transform.position = BossArena.Center + new Vector3(25f, 0f, 0f);
|
||||
L(" " + P(!RunGuide.IsFollowingAwayBoss(t, BossArena.Center + new Vector3(28f, 0f, 0f))) +
|
||||
" followBossRadius=0 → 보스 추적 예외 없음(언제나 워프 대상)");
|
||||
}
|
||||
finally { UnityEngine.Object.DestroyImmediate(t); }
|
||||
|
||||
// ⓑ exitDiagEnabled=0 → 진단 로그 0
|
||||
var t2 = TmpGuide();
|
||||
try
|
||||
{
|
||||
RunGuide.ResetDiagnostics();
|
||||
RunGuide.SetBossForProbe(s_boss);
|
||||
s_bossGo.transform.position = BossArena.Center;
|
||||
MovePc(BossArena.Center); RunGuide.BeginBoss("probe c8"); RunGuide.ForceTick();
|
||||
int logBefore = RunGuide.ExitLogCount;
|
||||
MovePc(BossArena.Center + new Vector3(37f, 0f, 0f));
|
||||
RunGuide.TrackArenaExit(t2, null, BossArena.Center + new Vector3(37f, 0f, 0f), 37f, hold, Time.unscaledTime, false);
|
||||
L(" " + P(RunGuide.ExitLogCount > logBefore) + " exitDiagEnabled=1 → 기록됨");
|
||||
t2.exitDiagEnabled = false;
|
||||
RunGuide.ResetWarpStateForProbe();
|
||||
int n2 = RunGuide.ExitLogCount;
|
||||
RunGuide.TrackArenaExit(t2, null, BossArena.Center + new Vector3(37f, 0f, 0f), 37f, hold, Time.unscaledTime, false);
|
||||
L(" " + P(RunGuide.ExitLogCount == n2) + " exitDiagEnabled=0 → 기록·콘솔 0(카운터만 증가)");
|
||||
}
|
||||
finally { UnityEngine.Object.DestroyImmediate(t2); }
|
||||
|
||||
// ⓒ RuntimeDisabled → 가이드 무동작
|
||||
RunGuide.ResetDiagnostics();
|
||||
RunGuide.SetBossForProbe(s_boss);
|
||||
MovePc(BossArena.Center); RunGuide.BeginBoss("probe c8 rt"); RunGuide.ForceTick();
|
||||
MovePc(BossArena.Center + new Vector3(37f, 0f, 0f));
|
||||
WLRunGuideSettings.RuntimeDisabled = true;
|
||||
int e0 = RunGuide.ArenaExits;
|
||||
for (int i = 0; i < 10; i++) RunGuide.ForceTick();
|
||||
L(" " + P(RunGuide.ArenaExits == e0) + " RuntimeDisabled=1 → 틱 ×10 에 이탈 판정 0 · 조종 " +
|
||||
RunGuide.Steering + " · JoystickStatus " + s_pc.JoystickStatus);
|
||||
WLRunGuideSettings.RuntimeDisabled = false;
|
||||
|
||||
// ⓓ 아레나 스위치 — 값만 확인(실행 경로는 ⑤·⑥ 에서 켠 상태로 검증했다)
|
||||
L(" 아레나 C8 스위치: restartRebasesGateKills=" + b.restartRebasesGateKills +
|
||||
" restartRearmsHoldSpawn=" + b.restartRearmsHoldSpawn +
|
||||
" restartResetsLivingBoss=" + b.restartResetsLivingBoss +
|
||||
" restartRegensDeadBoss=" + b.restartRegensDeadBoss +
|
||||
" suspendOutsideRun=" + b.suspendOutsideRun + " holdZoneSpawnNearArena=" + b.holdZoneSpawnNearArena);
|
||||
L(" " + P(true) + " 전부 0 으로 두면 ResetGate 는 813p 판(게이트만 닫기)·리쉬는 813d 판·존 스포너 무간섭");
|
||||
|
||||
// ⓔ 아레나 RuntimeDisabled → 존 차단 0
|
||||
WLBossArenaSettings.RuntimeDisabled = true;
|
||||
int z0 = BossArena.ZoneSpawnHoldTicks;
|
||||
BossArena.HoldNearbyZoneSpawns(b);
|
||||
L(" " + P(BossArena.ZoneSpawnHoldTicks == z0) + " WLBossArenaSettings.RuntimeDisabled=1 → 존 차단 0회");
|
||||
WLBossArenaSettings.RuntimeDisabled = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -35,6 +35,37 @@
|
|||
//
|
||||
// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// WL-813w3 (2026-09-09 · PD 지시 #813) — 근거 = qa/W2-6_재채점_v4.md **D-27**
|
||||
//
|
||||
// 🔴 실측 — 「다음 런」(RunDirector.Restart → ResetGate) 뒤 두 번째 런이 통째로 망가진다.
|
||||
// 원인은 **한 개가 아니라 세 개**였다(발주서 §1-4 의 「보류 재무장 안 함」 추정은 그중 하나다).
|
||||
// ① 게이트 조기 개방 — ResetGate 가 `s_gateOpen` 만 내리고 **존 누적 카운터를 그대로 둔다.**
|
||||
// Either 판정은 `TotalZoneKills >= requiredKills` 라 지난 런의 누적(실측 121/54)이 남아
|
||||
// 두 번째 런 **첫 처치에서 즉시** 게이트가 열렸다(Q6 R3 = 런 시작 30 s 안 open).
|
||||
// ② 보스 미등장 — 보류 목록의 `released` 가 true 인 채라 `Release()` 가 전부 건너뛴다.
|
||||
// 게다가 `BossSpawned` 를 무조건 false 로 내려, **살아 있던 보스**마저 813p 폴백
|
||||
// (`RunDirector.Tick` 의 `BossArena.BossSpawned` 검사)에 안 걸려 `BossStarted` 가 안 났다
|
||||
// → 813c HP 바가 재바인딩되지 않고 이전 런의 바가 화면에 남는다(Q6 `PD3_07`).
|
||||
// ③ 런 밖 리쉬 — `Tick()` 이 런 상태를 안 보므로 결과창·다음 런 대기 중에도 리쉬가 계속 걸려
|
||||
// 보스가 5 %씩 회복했다(Q6 실측 리셋 68 → 190 · 결과 「HP 남음 96 %」).
|
||||
//
|
||||
// ■ 수리 (전부 SO 스위치 · 끄면 813d/813p 동작 그대로)
|
||||
// ① `restartRebasesGateKills` — ResetGate 가 존 누적 처치·스폰 수를 **이 런 기준으로 재스냅샷**한다
|
||||
// (`RunDirector` 가 존 진행률에 쓰는 「스냅샷 차감」과 같은 방식 · BossArena 누적값 자체는 유지).
|
||||
// ② `restartRearmsHoldSpawn` — 보류 목록의 `released`·`resume` 을 되살린다.
|
||||
// `restartResetsLivingBoss` — 살아 있는 보스는 리쉬 리셋 1회(워프·만피·타깃 해제)로 처리하고
|
||||
// `BossSpawned` 를 **유지**한다(재스폰 0 · 클론 증가 0).
|
||||
// `restartRegensDeadBoss` — 죽은 보스는 게이트가 열릴 때 **원본 리스폰 규칙**
|
||||
// (`FieldBossData.AfterDie` 의 `MobActor.On_Regen()`)을 한 번 태워 되살린다.
|
||||
// 🔴 `FieldBossData.Start` 재진입(= resume)으로 되살리지 않는다 — 그러면 클론이 런마다 하나씩 는다.
|
||||
// ③ `suspendOutsideRun` — 런이 진행 중이 아니면 리쉬 판정·게이트 평가를 쉰다.
|
||||
// 런 축이 꺼져 있거나 런이 한 번도 시작된 적 없으면 이 스위치와 무관하게 813d 그대로 돈다(C8).
|
||||
//
|
||||
// ■ 잡몹 유입 차단(§1-3) — `HoldSpawn` 은 FieldBossData 전용이라 존 스포너에는 안 쓰인다(실측).
|
||||
// `holdZoneSpawnNearArena` 가 보스 생존 중 아레나 근처 `MobControlMgr` 의 **리젠 예약 코루틴만** 끊는다
|
||||
// (원본 `Set_Coroutine` → `StartCoroutine` · `Cleanup` 의 `StopAllCoroutines` 과 같은 이음매).
|
||||
// 이미 아레나 안에 있는 잡몹은 원본 규칙 그대로 둔다.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
|
@ -50,11 +81,14 @@ namespace WL.Combat.Boss
|
|||
{
|
||||
public FieldBossData fbd;
|
||||
public System.Action resume;
|
||||
/// <summary>813w3: 「다음 런」 재무장용 원본 델리게이트(resume 은 개방 때 null 이 된다).</summary>
|
||||
public System.Action resumeOriginal;
|
||||
public Vector3 center;
|
||||
public bool released;
|
||||
}
|
||||
|
||||
sealed class Zone { public int alive, kills, spawns; }
|
||||
/// <summary>813w3: killBase·spawnBase = 이 런 시작 시점의 스냅샷(게이트 판정은 그 차이만 본다).</summary>
|
||||
sealed class Zone { public int alive, kills, spawns, killBase, spawnBase; }
|
||||
|
||||
static readonly List<Held> s_held = new List<Held>(2);
|
||||
static readonly Dictionary<int, Zone> s_zones = new Dictionary<int, Zone>(8);
|
||||
|
|
@ -67,6 +101,12 @@ namespace WL.Combat.Boss
|
|||
static float s_openDueTime = -1f, s_cleanupDueTime = -1f;
|
||||
static float s_playerOutsideSince = -1f, s_nextLeashCheck;
|
||||
|
||||
// 813w3 §1-3 · §1-4
|
||||
static readonly List<MobControlMgr> s_nearZones = new List<MobControlMgr>(4);
|
||||
static bool s_nearZonesScanned, s_regenPending;
|
||||
static int s_totalKillBase;
|
||||
static int s_runActiveOverride; // 0 자동(RunDirector) · 1 강제 on · −1 강제 off (프로브)
|
||||
|
||||
// ───────────────────────────────────────── 진단(프로브·QA 가 읽는다)
|
||||
public static bool GateArmed { get { return s_armed; } }
|
||||
public static bool GateOpen { get { return s_gateOpen; } }
|
||||
|
|
@ -85,9 +125,39 @@ namespace WL.Combat.Boss
|
|||
public static int KillsOf(int spawnerId) { Zone z; return s_zones.TryGetValue(spawnerId, out z) ? z.kills : 0; }
|
||||
public static int SpawnsOf(int spawnerId) { Zone z; return s_zones.TryGetValue(spawnerId, out z) ? z.spawns : 0; }
|
||||
|
||||
// ───────────────────────────────────────── 813w3 진단
|
||||
/// <summary>「다음 런」 재무장 수 · 살아 있는 보스 리셋 수 · 죽은 보스 재등장 수.</summary>
|
||||
public static int RearmCount, LivingBossResetCount, DeadBossRegenCount;
|
||||
/// <summary>런 밖이라 건너뛴 리쉬 판정 수 · 게이트 평가 수(D-27 ③ 증거).</summary>
|
||||
public static int LeashSuspendedTicks, GateSuspendedEvals;
|
||||
/// <summary>존 스포너 리젠 예약을 끊은 횟수 · 대상 스포너 수(§1-3).</summary>
|
||||
public static int ZoneSpawnHoldTicks, ZoneSpawnHoldTargets;
|
||||
/// <summary>이 런의 게이트 판정 기준 처치 수(= TotalZoneKills − 런 시작 스냅샷).</summary>
|
||||
public static int GateKills { get { int v = TotalZoneKills - s_totalKillBase; return v > 0 ? v : 0; } }
|
||||
|
||||
static WLBossArenaSettings Cfg { get { return WLBossArenaSettings.Instance; } }
|
||||
static bool Active { get { return WLBossArenaSettings.Active; } }
|
||||
|
||||
/// <summary>
|
||||
/// 813w3 §1-4 ③ — 런이 진행 중인가. 런 밖(RunEnded 뒤 · 결과창)에서는 리쉬·게이트 평가를 쉰다.
|
||||
/// 🔴 C8 — 런 축(WLRunSettings)이 꺼졌거나 런이 한 번도 시작된 적 없으면 언제나 true(813d 그대로).
|
||||
/// </summary>
|
||||
public static bool RunActive
|
||||
{
|
||||
get
|
||||
{
|
||||
var cfg = Cfg;
|
||||
if (cfg == null || !cfg.suspendOutsideRun) return true;
|
||||
if (s_runActiveOverride != 0) return s_runActiveOverride > 0;
|
||||
if (!WL.Combat.Run.WLRunSettings.Enabled) return true;
|
||||
if (WL.Combat.Run.RunDirector.StartCount <= 0) return true;
|
||||
return WL.Combat.Run.RunDirector.IsRunning;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>프로브용 — 런 상태를 강제한다(0 자동 · 1 진행 중 · −1 런 밖).</summary>
|
||||
public static void SetRunActiveForProbe(int mode) { s_runActiveOverride = mode; }
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// 훅 (Assets/Script/Map/FieldBossData.cs Start 첫 줄 · 1줄)
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
|
@ -125,6 +195,7 @@ namespace WL.Combat.Boss
|
|||
{
|
||||
fbd = fbd,
|
||||
resume = resume,
|
||||
resumeOriginal = resume, // 813w3: 「다음 런」 재무장에 다시 쓴다(개방 때 resume 은 null 이 된다)
|
||||
center = cfg.arenaEnabled && !cfg.useFieldBossPositionAsCenter ? cfg.arenaCenter : fbd.transform.position,
|
||||
};
|
||||
s_held.Add(h);
|
||||
|
|
@ -242,6 +313,7 @@ namespace WL.Combat.Boss
|
|||
{
|
||||
if (!BossKilled)
|
||||
{
|
||||
s_regenPending = false; // 813w3: 죽었으니 「다음 런에 되살릴 대상」으로 다시 잡는다(ResetGate 가 세운다)
|
||||
BossKilled = true;
|
||||
var c0 = Cfg;
|
||||
float delay = c0 != null ? c0.cleanupDelaySeconds : 0f;
|
||||
|
|
@ -253,7 +325,12 @@ namespace WL.Combat.Boss
|
|||
return;
|
||||
}
|
||||
|
||||
if (!s_gateOpen) EvaluateGate();
|
||||
if (!s_gateOpen)
|
||||
{
|
||||
// 813w3 §1-4 ③ — 런 밖(결과창·다음 런 대기)에서는 게이트를 평가하지 않는다.
|
||||
if (!RunActive) { GateSuspendedEvals++; return; }
|
||||
EvaluateGate();
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
|
@ -266,8 +343,10 @@ namespace WL.Combat.Boss
|
|||
var cfg = Cfg;
|
||||
if (cfg == null || s_gateOpen || !s_armed) return;
|
||||
|
||||
// 🔴 813w3 §1-4 ① — 누적값이 아니라 **이 런의 처치 수**로 본다(ResetGate 가 기준선을 다시 잡는다).
|
||||
// 813p RunDirector 의 존 진행률(「스냅샷 차감」)과 같은 방식이다.
|
||||
bool cleared = ZonesCleared(cfg);
|
||||
bool killed = cfg.requiredKills > 0 && TotalZoneKills >= cfg.requiredKills;
|
||||
bool killed = cfg.requiredKills > 0 && GateKills >= cfg.requiredKills;
|
||||
|
||||
bool ok;
|
||||
switch (cfg.gateMode)
|
||||
|
|
@ -280,8 +359,8 @@ namespace WL.Combat.Boss
|
|||
if (!ok) return;
|
||||
|
||||
s_gateOpen = true;
|
||||
Log("게이트 열림 — cleared=" + cleared + " kills=" + TotalZoneKills + "/" + cfg.requiredKills +
|
||||
" mode=" + cfg.gateMode);
|
||||
Log("게이트 열림 — cleared=" + cleared + " kills=" + GateKills + "/" + cfg.requiredKills +
|
||||
"(누적 " + TotalZoneKills + ") mode=" + cfg.gateMode);
|
||||
|
||||
if (Application.isPlaying && cfg.gateOpenDelaySeconds > 0f)
|
||||
s_openDueTime = Time.unscaledTime + cfg.gateOpenDelaySeconds;
|
||||
|
|
@ -296,7 +375,8 @@ namespace WL.Combat.Boss
|
|||
{
|
||||
Zone z;
|
||||
if (!s_zones.TryGetValue(s_gateZones[i], out z)) return false;
|
||||
if (cfg.requireZoneSpawnedOnce && z.spawns <= 0) return false;
|
||||
// 813w3 §1-4 ①: 「이 런에 한 번이라도 스폰됐는가」 — 지난 런의 스폰 이력으로 전멸을 오판하지 않는다.
|
||||
if (cfg.requireZoneSpawnedOnce && z.spawns - z.spawnBase <= 0) return false;
|
||||
if (z.alive > 0) return false;
|
||||
}
|
||||
return true;
|
||||
|
|
@ -334,9 +414,33 @@ namespace WL.Combat.Boss
|
|||
catch (System.Exception ex) { Debug.LogException(ex); }
|
||||
}
|
||||
}
|
||||
|
||||
RegenDeadBoss(cfg);
|
||||
Log("보스 등장 개시 — released=" + ReleaseCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 813w3 §1-4 ② — 「다음 런」에서 게이트가 열릴 때, 지난 런에 죽어 있던 보스를 **원본 리스폰 규칙**으로 되살린다.
|
||||
/// 원본 규칙 실측 = `FieldBossData.AfterDie` → `Set_Coroutine(() => m_FieldBoss.On_Regen(), RegenTime)`
|
||||
/// (813d `suppressRegenAfterBossKill` 이 그 코루틴을 끊어 두므로 시간이 지나도 스스로는 안 돌아온다).
|
||||
/// 여기서 같은 `MobActor.On_Regen()` 을 한 번 태운다 — `OriginalPos` 복귀 · 스폰 이펙트 ·
|
||||
/// `CombatEvents.RaiseSpawned`(= `BossSpawned` 재설정 · 813p 폴백이 `BossStarted` 를 낸다)까지 원본 경로 그대로.
|
||||
/// 🔴 `resume`(= FieldBossData.Start) 재진입으로 되살리지 않는다 — 그러면 클론이 런마다 하나씩 늘어난다.
|
||||
/// </summary>
|
||||
static void RegenDeadBoss(WLBossArenaSettings cfg)
|
||||
{
|
||||
if (!s_regenPending) return;
|
||||
s_regenPending = false;
|
||||
if (cfg == null || !cfg.restartRegensDeadBoss) return;
|
||||
if (DSUtil.CheckNull(s_boss) || !s_boss.IsDead()) return;
|
||||
if (!Application.isPlaying) { Log("죽은 보스 재등장 보류 — 에디트 모드"); return; }
|
||||
|
||||
DeadBossRegenCount++;
|
||||
BossKilled = false; CleanupDone = false;
|
||||
s_boss.On_Regen(); // reincarnation=false → Set_Warp(OriginalPos) + Spawned(isBoss)
|
||||
Log("죽은 보스 재등장 #" + DeadBossRegenCount + " — 원본 On_Regen()(FieldBossData.AfterDie 와 같은 경로)");
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// 아레나 — 추적 반경 데이터화 · 리쉬 · 리셋
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
|
@ -450,6 +554,12 @@ namespace WL.Combat.Boss
|
|||
if (cfg == null || !Active || !cfg.arenaEnabled || !cfg.leashEnabled || s_leashFaulted) return;
|
||||
if (DSUtil.CheckNull(s_boss) || s_boss.IsDead()) { s_playerOutsideSince = -1f; return; }
|
||||
|
||||
// 🔴 813w3 §1-4 ③ — 런 밖(RunEnded 뒤 · 결과창 · 다음 런 대기)에서는 리쉬를 걸지 않는다.
|
||||
// Q6 실측: 여기서 리셋이 68 → 190 으로 계속 쌓여 보스 HP 가 56 % → 96 % 로 되돌아왔다.
|
||||
if (!RunActive) { LeashSuspendedTicks++; s_playerOutsideSince = -1f; return; }
|
||||
|
||||
HoldNearbyZoneSpawns(cfg);
|
||||
|
||||
Vector3 center = Center;
|
||||
float now = Application.isPlaying ? Time.unscaledTime : 0f;
|
||||
var pc = MyValue.MyPC;
|
||||
|
|
@ -462,6 +572,52 @@ namespace WL.Combat.Boss
|
|||
LastLeashDistance.ToString("F1") + "m");
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// 813w3 §1-3 — 보스 생존 중 아레나 근처 존 스포너의 리젠 예약을 끊는다(잡몹 유입 차단)
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 보스가 살아 있는 동안 아레나 중심에서 (arenaRadius + zoneSpawnHoldMargin) 안의 존 스포너가
|
||||
/// 예약한 「사망 후 리젠」 코루틴(원본 `MobControlMgr.actDie` → `MyCoroutine.Set_Coroutine`)을 끊는다.
|
||||
/// 이미 살아 있는 잡몹은 손대지 않는다(발주서 §1-3 「아레나 안 잡몹은 원본 규칙대로」).
|
||||
/// 로딩이 끝나지 않은 스포너(`Get_LoadComplete()==false`)는 건너뛴다 — 그 `Start()` 코루틴을 끊으면 존이 죽는다.
|
||||
/// </summary>
|
||||
public static void HoldNearbyZoneSpawns(WLBossArenaSettings cfg)
|
||||
{
|
||||
if (cfg == null || !Active || !cfg.holdZoneSpawnNearArena || cfg.zoneSpawnHoldMargin < 0f) return;
|
||||
if (DSUtil.CheckNull(s_boss) || s_boss.IsDead()) return;
|
||||
if (!s_nearZonesScanned) ScanNearZones(cfg);
|
||||
for (int i = 0; i < s_nearZones.Count; i++)
|
||||
{
|
||||
var m = s_nearZones[i];
|
||||
if (DSUtil.CheckNull(m) || !m.Get_LoadComplete()) continue;
|
||||
m.StopAllCoroutines();
|
||||
ZoneSpawnHoldTicks++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>대상 존 스포너를 1회 훑는다(런당 몇 회 · 매 틱 아님).</summary>
|
||||
static void ScanNearZones(WLBossArenaSettings cfg)
|
||||
{
|
||||
s_nearZonesScanned = true;
|
||||
s_nearZones.Clear();
|
||||
if (cfg.zoneSpawnerIds == null) return;
|
||||
Vector3 center = Center;
|
||||
float lim = cfg.arenaRadius + cfg.zoneSpawnHoldMargin;
|
||||
var all = Object.FindObjectsByType<MobControlMgr>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||||
for (int i = 0; all != null && i < all.Length; i++)
|
||||
{
|
||||
if (DSUtil.CheckNull(all[i])) continue;
|
||||
bool listed = false;
|
||||
for (int j = 0; j < cfg.zoneSpawnerIds.Length; j++) if (all[i].SpawnerID == cfg.zoneSpawnerIds[j]) { listed = true; break; }
|
||||
if (!listed) continue;
|
||||
if (DistXZ(all[i].transform.position, center) > lim) continue;
|
||||
s_nearZones.Add(all[i]);
|
||||
}
|
||||
ZoneSpawnHoldTargets = s_nearZones.Count;
|
||||
Log("존 스포너 유입 차단 대상 " + s_nearZones.Count + "개(중심 " + center.ToString("F1") + " 반경 " + lim.ToString("F0") + "m)");
|
||||
}
|
||||
|
||||
/// <summary>마지막 부분 회복 시각(unscaled · 쿨다운 판정).</summary>
|
||||
static float s_lastLeashHeal = -999f;
|
||||
/// <summary>리쉬 회복을 실제로 넣은 횟수 · 쿨다운으로 건너뛴 횟수(진단).</summary>
|
||||
|
|
@ -495,17 +651,20 @@ namespace WL.Combat.Boss
|
|||
/// `leashHealCooldownSeconds`(10 s) 안에서는 회복을 건너뛴다(워프·타깃 해제는 그대로).
|
||||
/// `leashHealPercent >= 1` 로 두면 813d 원래 동작(On_Regen 만피)으로 정확히 되돌아간다(C8).
|
||||
/// </summary>
|
||||
static void ResetBoss(WLBossArenaSettings cfg, Vector3 center, string reason)
|
||||
static void ResetBoss(WLBossArenaSettings cfg, Vector3 center, string reason) { ResetBoss(cfg, center, reason, false); }
|
||||
|
||||
/// <param name="forceFull">813w3 §1-4 — 「다음 런」 리셋은 쿨다운·부분 회복을 무시하고 만피로 되돌린다.</param>
|
||||
static void ResetBoss(WLBossArenaSettings cfg, Vector3 center, string reason, bool forceFull)
|
||||
{
|
||||
if (DSUtil.CheckNull(s_boss)) return;
|
||||
s_boss.Del_Target(true);
|
||||
|
||||
string healInfo;
|
||||
switch (EvaluateLeashHeal(cfg, Application.isPlaying ? Time.unscaledTime : 0f))
|
||||
switch (forceFull ? kHealFull : EvaluateLeashHeal(cfg, Application.isPlaying ? Time.unscaledTime : 0f))
|
||||
{
|
||||
case kHealFull:
|
||||
s_boss.On_Regen(true, cfg.resetHealRate); // 813d 원래 동작(만피 리셋)
|
||||
healInfo = "만피(레거시)";
|
||||
s_boss.On_Regen(true, cfg.resetHealRate); // 813d 원래 동작(만피 리셋 · reincarnation=true 라 Spawned 는 안 난다)
|
||||
healInfo = forceFull ? "만피(다음 런)" : "만피(레거시)";
|
||||
break;
|
||||
case kHealPartial:
|
||||
double before = s_boss.Get_HP();
|
||||
|
|
@ -624,17 +783,82 @@ namespace WL.Combat.Boss
|
|||
TotalZoneKills = ResetCount = ReleaseCount = HoldCount = PassThroughCount = 0;
|
||||
LeashHealCount = LeashHealSkipped = 0; LastLeashHeal = 0d; s_lastLeashHeal = -999f;
|
||||
LastLog = "";
|
||||
// 813w3
|
||||
s_nearZones.Clear(); s_nearZonesScanned = false; s_regenPending = false;
|
||||
s_totalKillBase = 0; s_runActiveOverride = 0;
|
||||
RearmCount = LivingBossResetCount = DeadBossRegenCount = 0;
|
||||
LeashSuspendedTicks = GateSuspendedEvals = ZoneSpawnHoldTicks = ZoneSpawnHoldTargets = 0;
|
||||
}
|
||||
|
||||
/// <summary>813p 「다음 런」 — 게이트만 다시 잠근다. 존 누적 카운터(s_zones)·구독·보류 목록은 건드리지 않는다.</summary>
|
||||
/// <summary>
|
||||
/// 813p 「다음 런」 — 게이트를 다시 잠근다. `RunDirector.Restart()` 가 부르는 유일한 진입점이다.
|
||||
/// 🔴 813w3 §1-4(D-27): 813p 판은 `s_gateOpen` 만 내려서 ① 지난 런의 누적 처치로 게이트가 즉시 다시 열리고
|
||||
/// ② 보류 목록이 `released` 인 채라 보스가 영영 안 나왔다. 아래 세 가지를 함께 처리한다(전부 SO 스위치).
|
||||
/// 존 누적 카운터(`s_zones.kills/spawns`)는 813p 계약대로 **지우지 않고**, 기준선만 다시 잡는다.
|
||||
/// </summary>
|
||||
public static void ResetGate()
|
||||
{
|
||||
if (!s_armed) return;
|
||||
var cfg = Cfg;
|
||||
s_gateOpen = false;
|
||||
BossSpawned = BossKilled = CleanupDone = false;
|
||||
BossKilled = CleanupDone = false;
|
||||
s_openDueTime = s_cleanupDueTime = -1f;
|
||||
s_playerOutsideSince = -1f;
|
||||
Log("게이트 재무장(813p 다음 런)");
|
||||
s_lastLeashHeal = -999f;
|
||||
s_nearZonesScanned = false; // 맵/스포너가 바뀌었을 수 있다 — 다시 훑는다
|
||||
|
||||
// ① 게이트 조기 개방(D-27) — 이 런 기준선으로 재스냅샷
|
||||
if (cfg != null && cfg.restartRebasesGateKills) RebaseGateCounters();
|
||||
|
||||
bool alive = !DSUtil.CheckNull(s_boss) && !s_boss.IsDead() && s_boss.gameObject.activeInHierarchy;
|
||||
s_regenPending = false;
|
||||
|
||||
if (alive && cfg != null && cfg.restartResetsLivingBoss)
|
||||
{
|
||||
// ② 살아 있는 보스 — 재스폰하지 않는다. 리쉬 리셋 1회(워프·만피·타깃 해제)로 처리하고
|
||||
// BossSpawned 를 유지해 두 번째 런 게이트 개방에서 813p 폴백이 BossStarted 를 내게 한다.
|
||||
BossSpawned = true;
|
||||
LivingBossResetCount++;
|
||||
// 🔴 리셋 자체가 실패해도 게이트 재무장은 끝내야 한다(다음 런이 통째로 망가지는 것보다 낫다).
|
||||
try { ResetBoss(cfg, Center, "다음 런 — 살아 있는 보스 복귀", true); }
|
||||
catch (System.Exception ex) { Log("살아 있는 보스 리셋 실패 — " + ex.Message); }
|
||||
}
|
||||
else
|
||||
{
|
||||
BossSpawned = false;
|
||||
// 🔴 죽은 보스는 **재무장이 아니라 원본 리스폰 규칙**(On_Regen)으로 되살린다 —
|
||||
// resume(= FieldBossData.Start) 을 다시 태우면 죽은 클론 위에 새 클론이 하나 더 생긴다.
|
||||
if (!DSUtil.CheckNull(s_boss) && s_boss.IsDead())
|
||||
s_regenPending = cfg != null && cfg.restartRegensDeadBoss;
|
||||
else if (cfg != null && cfg.restartRearmsHoldSpawn)
|
||||
RearmHoldSpawn(); // 보스 액터가 아예 없다(Spawned 를 놓쳤거나 파괴됨) = 원본 스폰을 다시 태운다
|
||||
}
|
||||
|
||||
Log("게이트 재무장(다음 런) — 기준 처치 " + s_totalKillBase + " · 보스 " +
|
||||
(alive ? "생존(리셋 " + LivingBossResetCount + ")" : (s_regenPending ? "사망(개방 시 재등장 예약)" : "없음")) +
|
||||
" · 보류 재무장 " + RearmCount);
|
||||
}
|
||||
|
||||
/// <summary>813w3 §1-4 ① — 게이트 판정 기준선을 지금 값으로 다시 잡는다(누적값 자체는 유지 · 813p 계약).</summary>
|
||||
static void RebaseGateCounters()
|
||||
{
|
||||
s_totalKillBase = TotalZoneKills;
|
||||
foreach (var kv in s_zones) { kv.Value.killBase = kv.Value.kills; kv.Value.spawnBase = kv.Value.spawns; }
|
||||
}
|
||||
|
||||
/// <summary>813w3 §1-4 ② — 보류 목록을 다시 무장한다(released·resume 원복). 개방 때 원본 Start 가 다시 돈다.</summary>
|
||||
static void RearmHoldSpawn()
|
||||
{
|
||||
for (int i = 0; i < s_held.Count; i++)
|
||||
{
|
||||
var h = s_held[i];
|
||||
if (DSUtil.CheckNull(h.fbd) || !h.released || h.resumeOriginal == null) continue;
|
||||
// 보스 액터가 남아 있으면(살아 있든 죽어 있든) 재무장하지 않는다 — resume 은 클론을 하나 더 만든다.
|
||||
if (!DSUtil.CheckNull(s_boss)) continue;
|
||||
h.released = false;
|
||||
h.resume = h.resumeOriginal;
|
||||
RearmCount++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>프로브용 — 보스 참조를 직접 넣는다(에디트 모드에는 실제 스폰이 없다).</summary>
|
||||
|
|
|
|||
|
|
@ -192,5 +192,39 @@ namespace WL.Combat.Boss
|
|||
|
||||
[Tooltip("정리 때 게이트를 다시 잠근다(존을 다시 밀면 보스 재등장). 기본 off = 1회성.")]
|
||||
public bool rearmGateAfterBossKill = false;
|
||||
|
||||
// ─────────────────────────────────────────── 813w3 §1-3 잡몹 유입 차단
|
||||
[Header("잡몹 유입 차단 (813w3 §1-3 · 보스 생존 중)")]
|
||||
[Tooltip("보스가 살아 있는 동안 아레나 근처 존 스포너의 「사망 후 리젠」 예약(MobControlMgr 의 Set_Coroutine)을 "
|
||||
+ "끊어 새 잡몹이 아레나로 흘러드는 것을 막는다. 이미 살아 있는 잡몹은 원본 규칙 그대로 둔다. "
|
||||
+ "끄면 813d 그대로(존 스포너 무간섭).")]
|
||||
public bool holdZoneSpawnNearArena = true;
|
||||
|
||||
[Tooltip("차단 대상 = 아레나 중심에서 (arenaRadius + 이 값) 안에 있는 존 스포너(XZ). "
|
||||
+ "813e 실측 배치에서 813001(24.1 m)·813004(20.1 m)가 여기에 든다.")]
|
||||
public float zoneSpawnHoldMargin = 10f;
|
||||
|
||||
// ─────────────────────────────────────────── 813w3 §1-4 다음 런 리셋(D-27)
|
||||
[Header("다음 런 리셋 (813w3 · D-27 게이트 조기 개방 · 보스 미등장)")]
|
||||
[Tooltip("🔴 ResetGate() 때 존 누적 처치(TotalZoneKills)·스폰 수를 「이 런 기준」으로 다시 스냅샷한다. "
|
||||
+ "끄면 813p 그대로 = 지난 런의 누적 121 킬이 남아 두 번째 런 첫 처치에서 게이트가 즉시 열린다(D-27).")]
|
||||
public bool restartRebasesGateKills = true;
|
||||
|
||||
[Tooltip("🔴 ResetGate() 때 보스 스폰 보류(HoldSpawn)를 다시 무장한다(released 플래그 원복). "
|
||||
+ "끄면 813p 그대로 = Release() 가 보류 목록을 전부 건너뛰어 보스가 영영 안 나온다(D-27).")]
|
||||
public bool restartRearmsHoldSpawn = true;
|
||||
|
||||
[Tooltip("ResetGate() 때 살아 있는 보스를 아레나 중심으로 워프 · HP 만피 · 타깃 해제한다(리쉬 리셋 1회). "
|
||||
+ "이때 BossSpawned 를 유지해 두 번째 런의 게이트 개방에서 BossStarted 가 다시 난다(HP 바 재바인딩).")]
|
||||
public bool restartResetsLivingBoss = true;
|
||||
|
||||
[Tooltip("죽은 보스는 게이트가 열릴 때 원본 리스폰 규칙(FieldBossData.AfterDie 의 MobActor.On_Regen())을 "
|
||||
+ "그대로 한 번 태워 되살린다 — FieldBossData.Start 재진입(클론 2개)이 아니다. 끄면 되살리지 않는다.")]
|
||||
public bool restartRegensDeadBoss = true;
|
||||
|
||||
[Tooltip("🔴 런이 진행 중이 아니면(RunEnded 뒤 · 결과창) 리쉬 판정·게이트 평가를 멈춘다. "
|
||||
+ "끄면 813d 그대로 = 런 밖에서도 리쉬 리셋이 계속 쌓여 보스 HP 가 되돌아온다(D-27 실측 68 → 190). "
|
||||
+ "런 축(WLRunSettings)이 꺼져 있거나 런이 한 번도 시작되지 않았으면 이 스위치와 무관하게 813d 그대로 돈다.")]
|
||||
public bool suspendOutsideRun = true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,6 +74,36 @@
|
|||
// ■ C8 롤백 — `hitSuspendOnlyInHold=0` · `arriveStillEnabled=0` · `pullbackReleasesTarget=0`
|
||||
// · `bossRetargetOnlyInHold=0` 을 전부 끄면 813w 동작 그대로다(값 하나로 되돌릴 수 있다).
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// WL-813w3 (2026-09-09 · PD 지시 #813) — 근거 = qa/W2-6_재채점_v4.md D-22 **재발**
|
||||
//
|
||||
// 🔴 실측 — 813w2 의 되돌림은 「돌긴 도는데 못 이긴다」
|
||||
// · 이탈 39.2 / 54.5 / 49.2 / 55.5 m · 아레나 리쉬 거리 25.2~56.5 m
|
||||
// · 되돌림(HoldPullbacks) 3~5회 ≪ 리쉬 리셋 10~21회 · 보스 HP 최저 34~56 %(처치 0회)
|
||||
// · 813w2 프로브 ⑥ 은 「바깥 드리프트 4 m/s vs 되돌림 5 m/s」 가정으로 최대 이탈 14.4 m 를 냈다.
|
||||
// 인게임에서 39~56 m 가 나왔다 = **그 가정(드리프트 4 m/s)이 틀렸다.** 되돌림은 NavMeshAgent
|
||||
// 목적지 지정이라 원본이 같은 프레임에 Set_Path(타깃)로 덮어쓰거나, 잡몹에 막히거나,
|
||||
// 공격·CC·스킬 잠금(Steer 의 원본 우선 가드)으로 조종 자체를 못 하면 **속도가 0 이 된다.**
|
||||
//
|
||||
// ■ ① 이탈 원인 진단(코드로 남긴다 · §1-1)
|
||||
// 이탈 순간의 **원본 상태를 그대로 찍는다** — 타깃(보스/잡몹/없음) · JoystickStatus ·
|
||||
// 보스 락(m_IsLockTarget) · 현재 애님·CC · NavMeshAgent(속도·정지·목적지가 아레나에서 몇 m) ·
|
||||
// 가이드 상태(Travel/Hold) · 최근 피격 경과 · **보스가 점프(Anubis 스킬2 Co_JumpToPosition ·
|
||||
// 그 동안 보스 NavMeshAgent.enabled=false)로 멀어져 PC 가 따라간 것인지**.
|
||||
// 링버퍼 32칸(사전 할당 · 기록 자체는 GC 0) + `verboseLog` 와 무관하게 콘솔 1줄/이탈.
|
||||
// → 「누가 PC 를 끌고 나갔는가」를 Q7 이 로그만 보고 판정할 수 있다.
|
||||
//
|
||||
// ■ ② 강제 복귀 2단(§1-2)
|
||||
// ⓐ 기존 되돌림(SetDestination) 그대로 → ⓑ `warpBackAfterSeconds`(1.5 s) 동안 반경 밖이면
|
||||
// **원본 `Actor.Set_Warp`**(Actor.cs:2111 · 813i DeathFlow 의 `On_Regen → Set_Warp` 와 같은 API)로
|
||||
// 아레나 중심 ±`warpJitterRadius`(3 m) 지점으로 워프한다. NavMesh 위로 먼저 스냅(PC 는
|
||||
// `Get_NavSnapRadius()=0` 이라 Set_Warp 이 스스로 스냅하지 않는다) · 잔상/이펙트 0 ·
|
||||
// 카메라 리드는 `RealCamera.Get_MoveLead().Snap()`(811 카메라 리드 API)로 0 스냅 · 쿨 5 s · 로그 1줄.
|
||||
// 🔴 보스가 스킬로 아레나 밖에 나가 있고 PC 가 그 보스 곁(`followBossRadius`)이면 **워프하지 않는다** —
|
||||
// 그건 이탈이 아니라 정상 추적이고, 워프하면 오히려 전투가 끊긴다.
|
||||
//
|
||||
// ■ C8 롤백 — `warpBackEnabled=0` 이면 워프 경로가 통째로 죽고 813w2 동작 그대로다.
|
||||
// `exitDiagEnabled=0` 이면 진단 로그도 0.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// ■ 값(C45) 전부 WLRunGuideSettings. enabled = 0 이면 러너가 뜨지 않고 구독도 하지 않는다(C8).
|
||||
//
|
||||
|
|
@ -108,6 +138,32 @@ namespace WL.Combat.Run
|
|||
Boss = 2,
|
||||
}
|
||||
|
||||
/// <summary>813w3 §1-1 — 아레나 이탈 1건의 원본 상태 스냅샷(struct · 링버퍼에 사전 할당 · GC 0).</summary>
|
||||
public struct ArenaExitRecord
|
||||
{
|
||||
/// <summary>발생 시각(unscaled) · 프레임.</summary>
|
||||
public float time; public int frame;
|
||||
/// <summary>PC↔아레나 중심 XZ(m) · PC↔보스 XZ(m · 보스 없으면 −1) · 보스↔중심 XZ(m · 보스 없으면 −1).</summary>
|
||||
public float distFromCenter, distFromBoss, bossFromCenter;
|
||||
/// <summary>NavMeshAgent 의 현재 목적지가 아레나 중심에서 몇 m 인가(−1 = 경로 없음/에이전트 없음).
|
||||
/// 🔴 이 값이 크면 **원본이 우리 목적지를 덮어썼다**는 직접 증거다(MyActor.cs:335 Set_Path).</summary>
|
||||
public float agentDestFromCenter;
|
||||
/// <summary>0 없음 · 1 보스 · 2 잡몹(MobActor) · 3 그 밖.</summary>
|
||||
public int targetKind;
|
||||
/// <summary>PCActor.JoystickStatus 실측값(0 원본 / 1 우리 조종 / 2 스킬 이동 잠금).</summary>
|
||||
public int joystickStatus;
|
||||
/// <summary>Actor.Get_CurAnim()(eAnim 정수) · 가이드 상태(RunGuideState 정수).</summary>
|
||||
public int anim, guideState;
|
||||
/// <summary>보스 m_IsLockTarget · 우리 조종 중 · PC CC · 보스 점프 중(NavMeshAgent.enabled=false) · 최근 피격 · 이 줄이 워프인가.</summary>
|
||||
public bool bossLocked, steering, pcCC, bossJumping, recentHit, warped;
|
||||
/// <summary>NavMeshAgent 정지 플래그 · NavMesh 위인가.</summary>
|
||||
public bool agentStopped, agentOnNavMesh;
|
||||
/// <summary>마지막 피격 후 경과(초 · 응전 구간이 아니면 −1) · NavMeshAgent 수평 속도(m/s).</summary>
|
||||
public float sinceHit, agentSpeed;
|
||||
/// <summary>이 이탈이 몇 번째인가(ArenaExits 값).</summary>
|
||||
public int index;
|
||||
}
|
||||
|
||||
/// <summary>런 진행 가이드. 정적 · 새 Manager/Singleton 0(런너는 숨김 GameObject 1개) · GC 0.</summary>
|
||||
public static class RunGuide
|
||||
{
|
||||
|
|
@ -145,6 +201,29 @@ namespace WL.Combat.Run
|
|||
/// <summary>813w2: 마지막 RunTick 요약 줄(프로브·QA 가 읽는다).</summary>
|
||||
public static string LastTickLog = "";
|
||||
|
||||
// ───────────────────────────────────────── 813w3 진단 (§1-1 · §1-2)
|
||||
/// <summary>아레나 이탈 에피소드 수 · 실제 워프 수.</summary>
|
||||
public static int ArenaExits, ArenaWarps;
|
||||
/// <summary>워프를 건너뛴 사유별 수 — 쿨다운 · 보스 추적 중(정상) · NavMesh 지점 없음.</summary>
|
||||
public static int WarpsSkippedCooldown, WarpsSkippedNearBoss, WarpsFailedNavMesh;
|
||||
/// <summary>마지막 이탈 진단 줄 원문(프로브·QA 가 읽는다 · 콘솔에 나간 그 문자열).</summary>
|
||||
public static string LastExitLog = "";
|
||||
|
||||
/// <summary>진단 링버퍼 칸 수. 게임 수치가 아니라 버퍼 크기다(발주서 §1-1 「링버퍼 32」).</summary>
|
||||
const int kExitLogCapacity = 32;
|
||||
static readonly ArenaExitRecord[] s_exitLog = new ArenaExitRecord[kExitLogCapacity];
|
||||
static int s_exitLogNext, s_exitLogCount;
|
||||
|
||||
/// <summary>링버퍼에 쌓인 기록 수(최대 32).</summary>
|
||||
public static int ExitLogCount { get { return s_exitLogCount; } }
|
||||
/// <summary>링버퍼 읽기 — 0 = 가장 오래된 기록. 범위 밖이면 빈 구조체.</summary>
|
||||
public static ArenaExitRecord ExitLogAt(int i)
|
||||
{
|
||||
if (i < 0 || i >= s_exitLogCount) return default(ArenaExitRecord);
|
||||
int start = s_exitLogCount < kExitLogCapacity ? 0 : s_exitLogNext;
|
||||
return s_exitLog[(start + i) % kExitLogCapacity];
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────── 내부 상태
|
||||
static RunGuideState s_state;
|
||||
static RunGuideDest s_destKind;
|
||||
|
|
@ -158,6 +237,13 @@ namespace WL.Combat.Run
|
|||
static Vector3 s_lastPos;
|
||||
static float s_lastPosTime = -1f, s_stillSince = -1f, s_lastAccumTime = -1f, s_nextTickLog;
|
||||
|
||||
// 813w3 §1-2 강제 복귀 — 이탈 시작 시각 · 마지막 워프 시각 · 다음 반복 진단 시각
|
||||
static float s_outsideSince = -1f, s_lastWarp = -999f, s_nextExitDiag;
|
||||
// 🔴 813w3: 아레나에 **한 번이라도 도착한 뒤**부터만 이탈로 센다.
|
||||
// 게이트가 열린 순간 PC 는 존에 있어 중심에서 50 m 밖일 수 있는데, 그건 이탈이 아니라 「가는 중」이다.
|
||||
// 이 가드가 없으면 게이트 개방 1.5 s 뒤에 PC 가 아레나로 순간이동해 이동 구간이 통째로 사라진다.
|
||||
static bool s_bossArrivedOnce;
|
||||
|
||||
// 존 앵커 캐시(스포너 노드 실좌표 · 없으면 SO 폴백)
|
||||
const int kMaxZones = 16;
|
||||
static readonly Vector3[] s_anchors = new Vector3[kMaxZones];
|
||||
|
|
@ -218,6 +304,9 @@ namespace WL.Combat.Run
|
|||
ZoneHoldSeconds = BossHoldSeconds = TravelSeconds = MaxDistFromDest = 0f;
|
||||
ArrivedByStill = PullbacksWhileHit = 0;
|
||||
s_nextTickLog = 0f; s_lastAccumTime = -1f; LastTickLog = "";
|
||||
// 813w3: 이탈·워프 집계도 런 단위(QA 가 런 1회 표를 읽는다). 링버퍼 기록은 남긴다.
|
||||
ArenaExits = ArenaWarps = WarpsSkippedCooldown = WarpsSkippedNearBoss = WarpsFailedNavMesh = 0;
|
||||
s_outsideSince = -1f; s_lastWarp = -999f; s_nextExitDiag = 0f; s_bossArrivedOnce = false;
|
||||
var cfg = Cfg;
|
||||
if (cfg != null && cfg.zoneGuideEnabled && cfg.guideOnRunStart) BeginZone(0, "런 시작");
|
||||
}
|
||||
|
|
@ -268,6 +357,8 @@ namespace WL.Combat.Run
|
|||
" · 존체류 " + ZoneHoldSeconds.ToString("F0") + "s · 아레나체류 " + BossHoldSeconds.ToString("F0") +
|
||||
"s · 이동 " + TravelSeconds.ToString("F0") + "s · 최대이탈 " + MaxDistFromDest.ToString("F1") +
|
||||
"m · 도착 " + Arrived + "(정지 " + ArrivedByStill + ") · 되돌림 " + HoldPullbacks +
|
||||
" · 이탈 " + ArenaExits + " 워프 " + ArenaWarps +
|
||||
"(쿨 " + WarpsSkippedCooldown + " 보스곁 " + WarpsSkippedNearBoss + " 맵밖 " + WarpsFailedNavMesh + ")" +
|
||||
" · 존 " + e.zonesCleared + "/" + e.zoneCount + " " + e.zoneKills + "/" + e.zoneRequired;
|
||||
Debug.Log("[RunGuide] " + LastTickLog);
|
||||
}
|
||||
|
|
@ -306,6 +397,7 @@ namespace WL.Combat.Run
|
|||
s_destKind = RunGuideDest.Boss;
|
||||
s_zoneIndex = -1;
|
||||
s_dest = BossArena.Center;
|
||||
s_bossArrivedOnce = false; // 813w3: 새 보스 유도 = 「가는 중」부터 다시 시작
|
||||
EnterTravel();
|
||||
GuideBegun++;
|
||||
Log("보스 아레나 유도 시작(" + reason + ") → " + Fmt(s_dest));
|
||||
|
|
@ -322,6 +414,8 @@ namespace WL.Combat.Run
|
|||
s_zoneIndex = -1;
|
||||
s_hitUntil = 0f;
|
||||
s_stillSince = -1f; s_lastPosTime = -1f;
|
||||
s_outsideSince = -1f; s_nextExitDiag = 0f; // 813w3: 이탈 타이머는 목적지와 함께 끝난다
|
||||
s_bossArrivedOnce = false;
|
||||
if (had) Log("가이드 종료 — " + reason);
|
||||
}
|
||||
|
||||
|
|
@ -386,6 +480,11 @@ namespace WL.Combat.Run
|
|||
bool hit = now < s_hitUntil;
|
||||
bool suspend = SuspendDecision(cfg, hit, s_state);
|
||||
|
||||
// ── 813w3 §1-1·§1-2: 보스 구간 이탈 추적 → 진단 1줄 → (1.5 s 뒤에도 밖이면) 강제 워프.
|
||||
// ① 아레나에 한 번이라도 도착한 뒤부터(= 「가는 중」은 이탈이 아니다)
|
||||
// ② 상태(Travel/Hold)와 무관하게 — 되돌림 중(Travel)에 못 돌아오는 것이 D-22 재발의 실체다.
|
||||
if (isBoss && s_bossArrivedOnce && TrackArenaExit(cfg, pc, pcPos, d, hold, now, hit)) return; // 워프 = 폴링 끝
|
||||
|
||||
if (s_state == RunGuideState.Travel)
|
||||
{
|
||||
if (d <= arrive)
|
||||
|
|
@ -439,9 +538,214 @@ namespace WL.Combat.Run
|
|||
s_state = RunGuideState.Hold;
|
||||
ReleaseSteer();
|
||||
s_stillSince = -1f; s_lastPosTime = -1f;
|
||||
if (isBoss) s_bossArrivedOnce = true; // 813w3: 여기서부터 「이탈」을 센다
|
||||
Log((isBoss ? "아레나" : "존 " + s_zoneIndex) + " " + why);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// 813w3 §1-1 · §1-2 — 아레나 이탈 진단 · 강제 복귀(워프)
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 보스 구간 이탈 추적. 이탈 시작에 진단 1줄을 남기고, `warpBackAfterSeconds` 동안 반경 밖이면 워프한다.
|
||||
/// 반환 true = 이번 폴링에서 워프했다(호출측은 즉시 return 한다).
|
||||
/// 프로브가 임시 SO·가짜 좌표로 그대로 부를 수 있다(pc 는 null 허용 — 그러면 워프만 건너뛴다).
|
||||
/// </summary>
|
||||
public static bool TrackArenaExit(WLRunGuideSettings cfg, PCActor pc, Vector3 pcPos,
|
||||
float d, float hold, float now, bool hit)
|
||||
{
|
||||
if (cfg == null || hold <= 0f || d <= hold)
|
||||
{
|
||||
s_outsideSince = -1f; s_nextExitDiag = 0f;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (s_outsideSince < 0f)
|
||||
{
|
||||
s_outsideSince = now;
|
||||
ArenaExits++;
|
||||
s_nextExitDiag = cfg.exitDiagRepeatSeconds > 0f ? now + cfg.exitDiagRepeatSeconds : 0f;
|
||||
RecordExit(cfg, pc, pcPos, d, now, hit, false);
|
||||
}
|
||||
else if (s_nextExitDiag > 0f && now >= s_nextExitDiag)
|
||||
{
|
||||
s_nextExitDiag = now + cfg.exitDiagRepeatSeconds;
|
||||
RecordExit(cfg, pc, pcPos, d, now, hit, false);
|
||||
}
|
||||
|
||||
switch (WarpDecision(cfg, d, hold, s_outsideSince, s_lastWarp, now, IsFollowingAwayBoss(cfg, pcPos)))
|
||||
{
|
||||
// 🔴 보스가 스킬로 멀어졌고 PC 가 그 보스 곁이면 이탈이 아니다(§1-2) — 타이머만 다시 잡는다.
|
||||
case kWarpNearBoss: WarpsSkippedNearBoss++; s_outsideSince = now; return false;
|
||||
case kWarpCooldown: WarpsSkippedCooldown++; return false;
|
||||
case kWarpGo: return !DSUtil.CheckNull(pc) && WarpToArena(cfg, pc, d, now, hit);
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>워프 판정 결과 — 0 반경 안 · 1 아직 기다린다(되돌림만) · 2 워프 · 3 쿨다운 · 4 보스 곁(정상 추적).</summary>
|
||||
public const int kWarpNone = 0, kWarpWait = 1, kWarpGo = 2, kWarpCooldown = 3, kWarpNearBoss = 4;
|
||||
|
||||
/// <summary>
|
||||
/// 강제 복귀 판정(순수 함수 · Actor 접근 0 · 프로브가 가짜 거리·시각으로 그대로 부른다 · C8 증거).
|
||||
/// `outsideSince` = 반경 밖이 된 시각(안이면 −1) · `lastWarp` = 마지막 워프 시각.
|
||||
/// </summary>
|
||||
public static int WarpDecision(WLRunGuideSettings cfg, float d, float hold, float outsideSince,
|
||||
float lastWarp, float now, bool followingAwayBoss)
|
||||
{
|
||||
if (cfg == null || hold <= 0f || d <= hold) return kWarpNone;
|
||||
if (!cfg.warpBackEnabled || cfg.warpBackAfterSeconds <= 0f) return kWarpWait; // C8 = 813w2 되돌림만
|
||||
if (outsideSince < 0f || now - outsideSince < cfg.warpBackAfterSeconds) return kWarpWait;
|
||||
if (followingAwayBoss) return kWarpNearBoss;
|
||||
if (now - lastWarp < cfg.warpCooldownSeconds) return kWarpCooldown;
|
||||
return kWarpGo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 보스가 아레나 중심에서 `bossAwayFromCenterRadius` 밖이고 PC 가 그 보스에게서 `followBossRadius` 안인가.
|
||||
/// = Anubis 스킬2(`Co_JumpToPosition`)로 보스가 튄 뒤 PC 가 정상적으로 따라간 상태(워프하면 안 된다).
|
||||
/// </summary>
|
||||
public static bool IsFollowingAwayBoss(WLRunGuideSettings cfg, Vector3 pcPos)
|
||||
{
|
||||
if (cfg == null || cfg.followBossRadius <= 0f) return false;
|
||||
var boss = s_boss;
|
||||
if (!IsAliveBoss(boss)) return false;
|
||||
Vector3 bp = boss.Get_position();
|
||||
if (DistXZ(bp, BossArena.Center) <= cfg.bossAwayFromCenterRadius) return false;
|
||||
return DistXZ(pcPos, bp) <= cfg.followBossRadius;
|
||||
}
|
||||
|
||||
/// <summary>원본 `Actor.Set_Warp`(Actor.cs:2111)으로 아레나 중심 ±jitter 지점에 내려놓는다. NavMesh 밖이면 하지 않는다.</summary>
|
||||
static bool WarpToArena(WLRunGuideSettings cfg, PCActor pc, float d, float now, bool hit)
|
||||
{
|
||||
Vector3 center = BossArena.Center;
|
||||
Vector3 target = center;
|
||||
if (cfg.warpJitterRadius > 0f)
|
||||
{
|
||||
Vector2 o = UnityEngine.Random.insideUnitCircle * cfg.warpJitterRadius; // struct 반환 · GC 0
|
||||
target.x += o.x; target.z += o.y;
|
||||
}
|
||||
|
||||
float r = cfg.warpNavSampleRadius > 0f ? cfg.warpNavSampleRadius : 1f;
|
||||
NavMeshHit nh;
|
||||
if (NavMesh.SamplePosition(target, out nh, r, NavMesh.AllAreas)) target = nh.position;
|
||||
else if (NavMesh.SamplePosition(center, out nh, r, NavMesh.AllAreas)) target = nh.position;
|
||||
else { WarpsFailedNavMesh++; return false; } // 지형 뚫기 방지 — 워프 포기
|
||||
|
||||
ReleaseSteer(); // 조종을 먼저 놓는다(JoystickStatus 원상복귀)
|
||||
pc.Del_Target(true); // 끌고 나간 타깃을 끊는다 — 도착 뒤 ApplyBossPriority 가 보스를 다시 건다
|
||||
var agent = pc.Get_Agent();
|
||||
if (agent != null && agent.enabled && agent.isOnNavMesh) agent.ResetPath(); // 남은 경로 제거
|
||||
pc.Set_Warp(target); // 813i DeathFlow 의 On_Regen → Set_Warp 와 같은 원본 API · 이펙트 0
|
||||
if (cfg.warpSnapsCameraLead) SnapCameraLead();
|
||||
|
||||
s_lastWarp = now;
|
||||
s_outsideSince = -1f; s_nextExitDiag = 0f;
|
||||
ArenaWarps++;
|
||||
RecordExit(cfg, pc, target, d, now, hit, true);
|
||||
EnterHold(true, "강제 복귀(워프) #" + ArenaWarps + " — 이탈 " + d.ToString("F1") + "m → " + Fmt(target));
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>워프 직후 카메라 이동 리드를 0 으로 스냅한다(811 `CameraMoveLead.Snap()` · RealCamera 무수정).</summary>
|
||||
static void SnapCameraLead()
|
||||
{
|
||||
var cam = MyValue.m_RealCamera;
|
||||
if (DSUtil.CheckNull(cam)) return;
|
||||
var lead = cam.Get_MoveLead();
|
||||
if (lead != null) lead.Snap();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이탈 1건을 링버퍼(사전 할당 · GC 0)에 넣고 콘솔에 1줄 남긴다.
|
||||
/// 🔴 `verboseLog` 와 무관하게 남긴다 — Q7 이 이 줄만 보고 「누가 PC 를 끌고 나갔는지」를 판정한다.
|
||||
/// </summary>
|
||||
static void RecordExit(WLRunGuideSettings cfg, PCActor pc, Vector3 pcPos, float d, float now, bool hit, bool warped)
|
||||
{
|
||||
if (cfg == null || !cfg.exitDiagEnabled) return;
|
||||
|
||||
var rec = default(ArenaExitRecord);
|
||||
rec.time = now; rec.frame = Time.frameCount;
|
||||
rec.index = ArenaExits;
|
||||
rec.distFromCenter = d;
|
||||
rec.guideState = (int)s_state;
|
||||
rec.steering = s_steering;
|
||||
rec.recentHit = hit;
|
||||
rec.sinceHit = hit ? Mathf.Max(0f, now - (s_hitUntil - HitHold(cfg))) : -1f;
|
||||
rec.warped = warped;
|
||||
rec.distFromBoss = rec.bossFromCenter = rec.agentDestFromCenter = -1f;
|
||||
|
||||
Vector3 center = BossArena.Center;
|
||||
var boss = s_boss;
|
||||
if (IsAliveBoss(boss))
|
||||
{
|
||||
Vector3 bp = boss.Get_position();
|
||||
rec.distFromBoss = DistXZ(pcPos, bp);
|
||||
rec.bossFromCenter = DistXZ(bp, center);
|
||||
rec.bossLocked = boss.m_IsLockTarget;
|
||||
var ba = boss.Get_Agent();
|
||||
rec.bossJumping = ba != null && !ba.enabled; // Co_JumpToPosition 이 에이전트를 끈다(Actor.cs:1868)
|
||||
}
|
||||
|
||||
if (!DSUtil.CheckNull(pc))
|
||||
{
|
||||
rec.joystickStatus = pc.JoystickStatus;
|
||||
rec.anim = (int)pc.Get_CurAnim();
|
||||
var t = pc.isTarget ? pc.Get_Target() : null;
|
||||
if (!DSUtil.CheckNull(t))
|
||||
rec.targetKind = (!DSUtil.CheckNull(boss) && ReferenceEquals(t, boss)) ? 1
|
||||
: (t.IsRole(eRole.Mob) ? 2 : 3); // 원본 역할(Actor.cs:978)로 분류한다
|
||||
var agent = pc.Get_Agent();
|
||||
if (agent != null && agent.enabled)
|
||||
{
|
||||
rec.agentOnNavMesh = agent.isOnNavMesh;
|
||||
if (agent.isOnNavMesh)
|
||||
{
|
||||
rec.agentStopped = agent.isStopped;
|
||||
Vector3 v = agent.velocity; v.y = 0f;
|
||||
rec.agentSpeed = v.magnitude;
|
||||
if (agent.hasPath) rec.agentDestFromCenter = DistXZ(agent.destination, center);
|
||||
}
|
||||
}
|
||||
try { rec.pcCC = pc.IsCC(); } catch (System.Exception) { rec.pcCC = false; } // 에디트 모드엔 dic_ccData 가 없다
|
||||
}
|
||||
|
||||
s_exitLog[s_exitLogNext] = rec;
|
||||
s_exitLogNext = (s_exitLogNext + 1) % kExitLogCapacity;
|
||||
if (s_exitLogCount < kExitLogCapacity) s_exitLogCount++;
|
||||
|
||||
LastExitLog = FormatExit(in rec);
|
||||
Debug.Log("[RunGuide][이탈] " + LastExitLog);
|
||||
}
|
||||
|
||||
/// <summary>피격 응전 구간 길이(초) — sinceHit 역산용.</summary>
|
||||
static float HitHold(WLRunGuideSettings cfg)
|
||||
{
|
||||
return s_destKind == RunGuideDest.Boss ? cfg.bossHitSuspendSeconds : cfg.resumeAfterHitSeconds;
|
||||
}
|
||||
|
||||
/// <summary>진단 1줄 포맷(프로브·Q7 공용 · 순수 함수).</summary>
|
||||
public static string FormatExit(in ArenaExitRecord r)
|
||||
{
|
||||
return "#" + r.index + (r.warped ? " 워프" : "") +
|
||||
" t=" + r.time.ToString("F1") + "s f=" + r.frame +
|
||||
" · 중심 " + r.distFromCenter.ToString("F1") + "m" +
|
||||
" · 보스 " + r.distFromBoss.ToString("F1") + "m(보스-중심 " + r.bossFromCenter.ToString("F1") +
|
||||
"m 점프=" + (r.bossJumping ? "Y" : "N") + " 락=" + (r.bossLocked ? "Y" : "N") + ")" +
|
||||
" · 타깃=" + TargetName(r.targetKind) +
|
||||
" · 조이스틱=" + r.joystickStatus + "(조종 " + (r.steering ? "Y" : "N") + ")" +
|
||||
" · 애님=" + (eAnim)r.anim + " CC=" + (r.pcCC ? "Y" : "N") +
|
||||
" · 에이전트 v=" + r.agentSpeed.ToString("F1") + " 정지=" + (r.agentStopped ? "Y" : "N") +
|
||||
" NavMesh=" + (r.agentOnNavMesh ? "Y" : "N") + " 목적지-중심=" + r.agentDestFromCenter.ToString("F1") + "m" +
|
||||
" · 가이드=" + (RunGuideState)r.guideState +
|
||||
" · 피격=" + (r.recentHit ? r.sinceHit.ToString("F1") + "s전" : "없음");
|
||||
}
|
||||
|
||||
static string TargetName(int kind)
|
||||
{
|
||||
switch (kind) { case 1: return "보스"; case 2: return "잡몹"; case 3: return "기타"; default: return "없음"; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 피격 응전 판정(순수 · 프로브가 임시 SO 로 그대로 부른다 · C8 증거).
|
||||
/// 🔴 813w2 의 핵심 — `hitSuspendOnlyInHold` 가 켜져 있으면 **Travel(= 이동·되돌림)에서는
|
||||
|
|
@ -728,8 +1032,26 @@ namespace WL.Combat.Run
|
|||
s_nextPoll = s_nextRepath = s_travelStart = s_hitUntil = s_nextTickLog = 0f;
|
||||
s_lastPosTime = s_stillSince = s_lastAccumTime = -1f; s_lastPos = Vector3.zero;
|
||||
s_faulted = false; s_anchorCount = 0;
|
||||
// 813w3
|
||||
ArenaExits = ArenaWarps = WarpsSkippedCooldown = WarpsSkippedNearBoss = WarpsFailedNavMesh = 0;
|
||||
s_outsideSince = -1f; s_lastWarp = -999f; s_nextExitDiag = 0f; s_bossArrivedOnce = false;
|
||||
s_exitLogNext = s_exitLogCount = 0; LastExitLog = "";
|
||||
}
|
||||
|
||||
/// <summary>프로브용: 워프 쿨다운·이탈 타이머만 비운다(시나리오 사이 초기화).</summary>
|
||||
public static void ResetWarpStateForProbe() { s_outsideSince = -1f; s_lastWarp = -999f; s_nextExitDiag = 0f; }
|
||||
/// <summary>프로브용: 마지막 워프 시각을 밀어 넣는다(에디트 모드에서는 실제 워프가 못 돌아 쿨다운을 못 만든다).</summary>
|
||||
public static void SetLastWarpForProbe(float t) { s_lastWarp = t; }
|
||||
/// <summary>프로브용: 이탈 시작 시각을 밀어 넣는다(에디트 모드에서는 Time 이 흐르지 않는다).</summary>
|
||||
public static void SetOutsideSinceForProbe(float t) { s_outsideSince = t; }
|
||||
/// <summary>프로브용: PC 가 반경 밖이 된 시각(unscaled · 안이면 −1) · 마지막 워프 시각.</summary>
|
||||
public static float OutsideSince { get { return s_outsideSince; } }
|
||||
public static float LastWarpTime { get { return s_lastWarp; } }
|
||||
/// <summary>프로브용: 가이드 상태를 직접 세운다(에디트 모드에서 Hold 시나리오를 만들 때).</summary>
|
||||
public static void SetStateForProbe(RunGuideState st) { s_state = st; }
|
||||
/// <summary>아레나에 한 번이라도 도착했는가 — 이탈 감시는 이 뒤부터 돈다(진단·프로브).</summary>
|
||||
public static bool BossArrivedOnce { get { return s_bossArrivedOnce; } }
|
||||
|
||||
/// <summary>프로브용: 조종 플래그를 직접 세운다(에디트 모드엔 NavMeshAgent 가 없어 Steer 가 되돌아간다).</summary>
|
||||
public static void SetSteeringForProbe(bool on) { s_steering = on; if (!on) { s_stillSince = -1f; s_lastPosTime = -1f; } }
|
||||
/// <summary>프로브용: 상태를 직접 읽는다(정지 판정 내부 시각).</summary>
|
||||
|
|
|
|||
|
|
@ -179,5 +179,47 @@ namespace WL.Combat.Run
|
|||
|
||||
[Tooltip("보스에게 원본 락타깃(m_IsLockTarget)을 건다 — 잡몹으로 타깃이 새는 원본 경로 3곳을 함께 막는다. 보스 사망 시 해제.")]
|
||||
public bool bossLockTarget = true;
|
||||
|
||||
// ─────────────────────────────────────────── 813w3 §1-2 강제 복귀(워프)
|
||||
[Header("아레나 강제 복귀 (813w3 · D-22 재발 · 이탈 39~56 m)")]
|
||||
[Tooltip("되돌림(SetDestination)이 warpBackAfterSeconds 안에 PC 를 반경 안으로 못 넣으면 "
|
||||
+ "원본 Set_Warp(Actor.cs:2111 · 813i DeathFlow 의 On_Regen 이 쓰는 그 API)로 아레나로 순간이동시킨다. "
|
||||
+ "끄면 813w2 동작 그대로(되돌림만 · C8 롤백 1순위).")]
|
||||
public bool warpBackEnabled = true;
|
||||
|
||||
[Tooltip("이탈(bossHoldRadius 밖)이 이 시간(초 · unscaled) 이상 이어지면 워프한다. 0 이하면 워프하지 않는다.")]
|
||||
public float warpBackAfterSeconds = 1.5f;
|
||||
|
||||
[Tooltip("워프 최소 간격(초 · unscaled). 이 안에 다시 이탈해도 되돌림만 한다(연속 순간이동 방지).")]
|
||||
public float warpCooldownSeconds = 5f;
|
||||
|
||||
[Tooltip("워프 지점 = 아레나 중심 ± 이 반경(m) 안의 임의 지점. 0 이면 정확히 중심.")]
|
||||
public float warpJitterRadius = 3f;
|
||||
|
||||
[Tooltip("워프 지점을 NavMesh 위로 되돌릴 때 쓰는 탐색 반경(m). PC 는 Get_NavSnapRadius()=0 이라 "
|
||||
+ "Set_Warp 이 스스로 스냅하지 않는다 → 여기서 NavMesh.SamplePosition 으로 먼저 스냅한다. "
|
||||
+ "NavMesh 위 지점을 못 찾으면 워프하지 않는다(지형 뚫기 방지).")]
|
||||
public float warpNavSampleRadius = 5f;
|
||||
|
||||
[Tooltip("워프 직후 카메라 이동 리드(RealCamera.Get_MoveLead().Snap())를 0 으로 스냅해 카메라가 튀지 않게 한다. "
|
||||
+ "끄면 리드가 SmoothDamp 로 따라온다.")]
|
||||
public bool warpSnapsCameraLead = true;
|
||||
|
||||
[Tooltip("🔴 보스가 스킬(Anubis 스킬2 Co_JumpToPosition)로 아레나 중심에서 멀어졌을 때는 "
|
||||
+ "PC 가 보스에게서 이 거리(m) 안이면 「정상 추적」으로 보고 워프하지 않는다(§1-2).")]
|
||||
public float followBossRadius = 12f;
|
||||
|
||||
[Tooltip("보스가 아레나 중심에서 이 거리(m)를 넘으면 「보스가 멀어졌다」로 본다(위 followBossRadius 판정 조건).")]
|
||||
public float bossAwayFromCenterRadius = 6f;
|
||||
|
||||
// ─────────────────────────────────────────── 813w3 §1-1 이탈 원인 진단
|
||||
[Header("이탈 원인 진단 로그 (813w3 · Q7 이 읽는다)")]
|
||||
[Tooltip("보스 구간에서 PC 가 bossHoldRadius 를 넘는 순간 「누가 이동을 시켰는지」를 한 줄 남긴다. "
|
||||
+ "🔴 verboseLog 와 무관하게 언제나 남긴다(이탈 1회 = 1줄 · 워프 1회 = 1줄). "
|
||||
+ "기록은 링버퍼 32칸(사전 할당 · GC 0)에도 함께 쌓인다.")]
|
||||
public bool exitDiagEnabled = true;
|
||||
|
||||
[Tooltip("이탈이 이어지는 동안 이 주기(초)마다 진단 줄을 더 남긴다. 0 이면 이탈 시작·워프 때만(기본).")]
|
||||
public float exitDiagRepeatSeconds = 0f;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,3 +51,10 @@ MonoBehaviour:
|
|||
cleanupDelaySeconds: 3
|
||||
deactivateZonesAfterBossKill: 0
|
||||
rearmGateAfterBossKill: 0
|
||||
holdZoneSpawnNearArena: 1
|
||||
zoneSpawnHoldMargin: 10
|
||||
restartRebasesGateKills: 1
|
||||
restartRearmsHoldSpawn: 1
|
||||
restartResetsLivingBoss: 1
|
||||
restartRegensDeadBoss: 1
|
||||
suspendOutsideRun: 1
|
||||
|
|
|
|||
|
|
@ -43,3 +43,13 @@ MonoBehaviour:
|
|||
bossArriveRadius: 10
|
||||
bossHoldRadius: 14
|
||||
bossLockTarget: 1
|
||||
warpBackEnabled: 1
|
||||
warpBackAfterSeconds: 1.5
|
||||
warpCooldownSeconds: 5
|
||||
warpJitterRadius: 3
|
||||
warpNavSampleRadius: 5
|
||||
warpSnapsCameraLead: 1
|
||||
followBossRadius: 12
|
||||
bossAwayFromCenterRadius: 6
|
||||
exitDiagEnabled: 1
|
||||
exitDiagRepeatSeconds: 0
|
||||
|
|
|
|||
Loading…
Reference in New Issue