[WL-813w5] 다음 런 앞판 보스 정리 · 게이트 조기 개방 방지 (#813)
This commit is contained in:
parent
a7423cd431
commit
292ccf674f
|
|
@ -0,0 +1,100 @@
|
|||
# WL813w5 Probe 2026-09-09 17:43:25 (edit mode · Play 0 · 로그인 0)
|
||||
playMode=False
|
||||
|
||||
── ⓪ 준비
|
||||
[PASS] WLBossArenaSettings 로드 · Active=True
|
||||
[PASS] BossArena.Subscribed(CombatEvents)
|
||||
[PASS] 🔴 BossArena.RunSubscribed(RunEvents.RunEnded) — 813w5 신규 구독
|
||||
|
||||
── ① 에셋 (Assets/WL/Combat/Settings/Resources/WL/WLBossArenaSettings.asset · 값 전부 SO = C45)
|
||||
신규 8필드: parkBossOnRunEnd=True nextRunBossHoldSeconds=30 nextRunBossHoldRewarpMargin=1.5 nextRunWarpBeforeHeal=True nextRunClearsPlayerTarget=True leashOutsideRunDistanceOnly=True gateMinSeconds=60 gateMinAllowsZonesCleared=True
|
||||
[PASS] parkBossOnRunEnd=1 (런 종료 순간 고정)
|
||||
[PASS] nextRunBossHoldSeconds=30 (발주서 §1-2)
|
||||
[PASS] gateMinSeconds=60 (발주서 §1-2)
|
||||
[PASS] leashOutsideRunDistanceOnly=1 (런 밖 거리 판정만)
|
||||
[PASS] nextRunWarpBeforeHeal=1 (워프 → 만피 순서)
|
||||
런 축 대조: runSeconds=270 openGateAtSec=170 zoneClearKills=[18,20]
|
||||
[PASS] gateMinSeconds(60) < openGateAtSec(170) — 시간제한 개방은 막지 않는다
|
||||
[PASS] gateMinSeconds < runSeconds(270) — 런을 막지 않는다
|
||||
[PASS] nextRunBossHoldSeconds < runSeconds
|
||||
[PASS] 0 < nextRunBossHoldRewarpMargin(1.5) < arenaRadius(20)
|
||||
|
||||
── ② 🔴 D-39 원인 ① 실측 — 「되돌림이 자기 호출 안에서 되돌려진다」 (원본 파일 직접 읽기)
|
||||
MobActor.cs:138 `On_Regen` · :142 그 안의 `Check_Battle();`
|
||||
MobActor.cs:343 `Check_Battle` · :347 `Get_EnemyActors(… f_BaseChaseRange)`
|
||||
MobActor.cs:324 🔴 `Change_MobStatus(Battle)` → **`FindNextTarget(10000f)`** = 사실상 무한 반경 재타깃
|
||||
MobActor.cs:352 `Check_Patrol`(OriginalPos 기준 f_BaseChaseRange 밖이면 귀환) — 아레나 리쉬보다 먼저 걸린다
|
||||
[PASS] 🔴 원본 `On_Regen` 은 **그 안에서 `Check_Battle()` 을 부른다** → 813w3 의 `Del_Target` → `On_Regen` → `Set_Warp` 순서에서는
|
||||
재아그로가 **워프 전(= 런이 끝난 자리 · PC 옆)** 에서 일어나고, 워프 뒤에도 MobStatus=Battle · m_Target=PC 가 남는다.
|
||||
[PASS] `FindNextTarget(10000f)` 존재 — 「추적 범위(20 m)」와 무관하게 맵 어디의 PC 든 잡는다
|
||||
원본 정지 API 접근성(발주서 §1-2 「StopMove_Imm 실측 · 없으면 NavMeshAgent」):
|
||||
Actor.cs:2146 `protected void StopMove_Imm` · Actor.cs:53 `protected bool IsStop`
|
||||
[PASS] 🔴 `StopMove_Imm` 은 **protected** — WL 코드에서 못 부른다(PCActor.WL_StopNavMove 는 PC 전용)
|
||||
[PASS] 대안 = Actor.cs:900 `public virtual NavMeshAgent Get_Agent()` 로 같은 본체 재현
|
||||
[PASS] 공개 API 실측 — Actor.cs:957 `Del_Target(bool)` · Actor.cs:2245 `Change_MobStatus(eMobStatus)`
|
||||
원인 ③ — RunDirector.cs:262 `NoteBossSpawned` 가 보스 Spawned 한 건에 게이트를 연다(AlreadySpawned)
|
||||
[PASS] RunDirector.cs:374 `OpenBossGate` — 813w5 가드 2줄이 들어간 자리
|
||||
|
||||
── ③ 게이트 조기 개방 방지 진리표 (gateMinSeconds=60 · D-39 t≈19.6 개방)
|
||||
[PASS] 런이 한 번도 시작 안 됨(StartCount=0) → 차단 0 (C8 · 813d 그대로)
|
||||
StartCount=1 IsRunning=True Elapsed=0.0s
|
||||
[PASS] 🔴 reason=AlreadySpawned · 런 시작 직후(Elapsed≈0) → **차단**(D-39 의 그 경로)
|
||||
[PASS] GateMinBlocked 1 증가 · LastGateHeldElapsed=0.0
|
||||
[PASS] reason=ZonesCleared → 통과(gateMinAllowsZonesCleared=1 · 런 축의 진짜 존 전멸은 막지 않는다)
|
||||
[PASS] reason=TimeLimit → 통과(openGateAtSec 170)
|
||||
[PASS] reason=External → 통과(QA·치트 버튼)
|
||||
[PASS] 🔴 reason=None(= BossArena 자체 `ZonesCleared` 판정) → **차단** — 지난 런에서 굳은 alive=0 으로 열리는 길을 막는다
|
||||
[PASS] 런 밖(EndRun 뒤) → 차단 0 (런 안에서만 시간을 잰다)
|
||||
|
||||
── ④ 보스 홀드 (nextRunBossHoldSeconds=30 · 가짜 시각)
|
||||
[PASS] 초기 상태 → 홀드 아님
|
||||
[PASS] arm 직후(t=1000) → 홀드 · BossHoldUntil=1030.0
|
||||
[PASS] t=1029(29 s 경과) → **아직 홀드**
|
||||
[PASS] t=1031(31 s 경과) → **해제**
|
||||
[PASS] 재무장(다음 런에서 다시 30 s) → 홀드 · until=1130.0
|
||||
[PASS] ClearBossHold() → 즉시 해제(QA·프로브)
|
||||
[PASS] C8 nextRunBossHoldSeconds=0 → 홀드 0(813w3 그대로)
|
||||
|
||||
── ⑤ RunEnded 구독 (813w5 신규 · 「런 종료 시 보스 상태 확정」)
|
||||
[PASS] RunEvents.RunEnded 구독 중
|
||||
[PASS] 🔴 진짜 RunDirector.EndRun(Win) → BossArena 가 RunEnded 를 받았다(2회 · LastRunOutcome=Win)
|
||||
[PASS] 보스 없음 → ParkCount 불변(0) · 예외 0
|
||||
[PASS] ParkBoss(보스 null) → false(안전)
|
||||
[PASS] 가짜 보스 IsDead()=true(비활성)
|
||||
[PASS] ParkBoss(죽은 보스) → false · ParkCount 불변(True)
|
||||
[PASS] RunEnded(Timeout · 죽은 보스) → 통지 1 · 고정 0 · 예외 0
|
||||
[PASS] C8 parkBossOnRunEnd=0 → RunEnded 통지 0(813w3 그대로)
|
||||
|
||||
── ⑥ 런 밖 리쉬 = 거리 판정만 유지 (813w3 은 판정 자체를 껐다 → D-39 ②)
|
||||
아레나 중심=(139.0, 0.8, -20.0) 반경=20 leashMargin=2 → 되돌림 경계 22m
|
||||
[PASS] RunActive=false 강제(결과창·다음 런 대기 상황)
|
||||
[PASS] 보스 없음 → LeashTick 이 보스 가드에서 먼저 끊긴다(카운터 불변)
|
||||
[PASS] 판정식 = XZ 거리 > 22m → 회복 없이 중심 워프(ResetBossNoHeal · LeashHealCount 불변)
|
||||
[PASS] 이번 틱 리셋 0(보스 없음) · 누적 0
|
||||
[PASS] C8 leashOutsideRunDistanceOnly=0 → 런 밖 판정 0(813w3 그대로)
|
||||
|
||||
── ⑦ 실 경로 (진짜 RunDirector.StartRun/Restart + BossArena.NoteKill · 813p 계약 그대로)
|
||||
[PASS] HoldSpawn 전에는 무장 0(NoteSpawn 이 무시된다) — 813d 계약
|
||||
런 1 시작 — StartCount=1 Phase=Zones Elapsed=0.0
|
||||
[PASS] 🔴 OpenBossGate(AlreadySpawned) @Elapsed≈0 → **열리지 않는다**(D-39 수리)
|
||||
[PASS] GateMinBlocked +1 = 1
|
||||
[PASS] OpenBossGate(External · QA 버튼) → 열린다 · reason=External
|
||||
[다음 런] Restart() — RestartCount=1 StartCount=2 Phase=Zones GateOpened=False
|
||||
[PASS] 두 번째 런에서 게이트가 다시 잠긴다
|
||||
[PASS] 🔴 두 번째 런에서도 60 s 전 AlreadySpawned 개방 **차단**(Q8 R1n·R3n 의 t≈19.6 개방이 여기서 막힌다)
|
||||
[PASS] 같은 시점에도 **진짜 존 전멸**은 연다(reason=ZonesCleared)
|
||||
|
||||
── ⑧ GC (틱 경로 0 B)
|
||||
[PASS] GateHeldByMinSeconds ×100 = 0 B
|
||||
[PASS] BossHeld ×100 = 0 B (held=True)
|
||||
[PASS] 런 밖 LeashTick ×100 = 0 B
|
||||
|
||||
── ⑨ C8 롤백 (임시 SO · 실에셋 읽기만 · 끄면 813w3 동작 그대로)
|
||||
[PASS] gateMinSeconds=0 → 조기 개방 차단 0(813p/813w3 그대로)
|
||||
[PASS] gateMinAllowsZonesCleared=0 → 존 전멸도 60 s 전에는 막는다(스위치 방향 확인)
|
||||
[PASS] WLBossArenaSettings.RuntimeDisabled=1 → 게이트 차단 0(Active=false)
|
||||
[PASS] RuntimeDisabled=1 → RunEnded 통지 0(고정 0)
|
||||
813w5 C8 스위치 정리: parkBossOnRunEnd=True nextRunBossHoldSeconds=30 nextRunWarpBeforeHeal=True nextRunClearsPlayerTarget=True leashOutsideRunDistanceOnly=True gateMinSeconds=60
|
||||
[PASS] 6개를 전부 0 으로 두면 813w3 판(ResetGate 1회 리셋 · 런 밖 리쉬 정지 · 게이트 시간 제한 없음)으로 정확히 돌아간다
|
||||
|
||||
RESULT PASS
|
||||
|
|
@ -0,0 +1,542 @@
|
|||
// WL813w5_Probe.cs — #813w5(「다음 런」 앞판 보스 정리 · 게이트 조기 개방 방지) 프로브
|
||||
// 에디트 모드 전용 · Play 불필요 · 로그인 0 · 씬/에셋 무수정(임시 GameObject·임시 SO 는 즉시 파괴/회수)
|
||||
//
|
||||
// 상주 에디터:
|
||||
// unity command run_script --project-path <wt> --file AgentScripts/WL813w5_Probe.cs --entry WL813w5_Probe.RunAll --timeout 300
|
||||
// 결과: Console + AgentScripts/WL813w5_PROBE.txt (RESULT PASS / RESULT FAIL n)
|
||||
//
|
||||
// 무엇을 증명하나 (발주서 WL-813w5 §1 · 근거 = qa/W2-8_재채점_v6.md D-39)
|
||||
// ① 에셋·SOT — 813w5 신규 8필드 값 실측 · 반경/시간 정합성 · 코드 상수 0
|
||||
// ② 🔴 원인 ① — 원본 MobActor.On_Regen 이 그 안에서 Check_Battle() → FindNextTarget(10000f) 를 부르는
|
||||
// **원본 파일:줄을 직접 읽어** 실측한다(813w3 이 「워프·만피·타깃 해제」를 해도 안 먹은 이유)
|
||||
// ③ 원인 ③ — 게이트 조기 개방 진리표(gateMinSeconds · reason 별 · 런 밖 · 런 시작 전)
|
||||
// ④ 홀드 — ArmBossHold → 가짜 시각으로 29 s 유지 · 31 s 해제 · 0 이면 홀드 없음
|
||||
// ⑤ RunEnded — 진짜 RunEvents.RunEnded 발행 → 구독·예외 0 · 보스 없음이면 ParkCount 불변
|
||||
// ⑥ 런 밖 거리 — LeashTick 을 런 밖 강제로 돌려 「거리 판정만 유지」 경로가 도는지(회복 0)
|
||||
// ⑦ 실 경로 — RunDirector.StartRun/Restart + BossArena.NoteKill 로 게이트가 60 s 전에 안 열리는지
|
||||
// ⑧ GC 0 — 게이트 판정 ×100 · 홀드 판정 ×100 · 런 밖 틱 ×100
|
||||
// ⑨ C8 롤백 — 신규 스위치 0 → 813w3 동작 그대로(임시 SO)
|
||||
//
|
||||
// 에디트 모드 한계(보고서 「미확인」)
|
||||
// · 살아 있는 MobActor(m_Stat·m_MonsterTableData·Animator·NavMeshAgent)가 없어 ParkBoss 의 실제 실행
|
||||
// (Set_Warp · On_Regen · Change_MobStatus)은 못 돌린다 — 813d/813w3 미확인 ②와 같은 한계다.
|
||||
// · Time.unscaledTime 이 스크립트 실행 중 멈춰 있다 → 시간 의존 판정은 BossArena.SetProbeTime 으로 먹인다.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using WL.Combat.Boss;
|
||||
using WL.Combat.Core;
|
||||
using WL.Combat.Run;
|
||||
|
||||
public static class WL813w5_Probe
|
||||
{
|
||||
const string OutCommit = "AgentScripts/WL813w5_PROBE.txt";
|
||||
|
||||
static StringBuilder sb;
|
||||
static int s_fail;
|
||||
|
||||
static GameObject s_pcGo;
|
||||
static PCActor s_pc, s_prevMyPc;
|
||||
|
||||
public static object RunAll()
|
||||
{
|
||||
sb = new StringBuilder();
|
||||
s_fail = 0;
|
||||
sb.AppendLine("# WL813w5 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_Cause();
|
||||
Sec3_GateMin();
|
||||
Sec4_Hold();
|
||||
Sec5_RunEnded();
|
||||
Sec6_OutsideRun();
|
||||
Sec7_RealPath();
|
||||
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); }
|
||||
|
||||
/// <summary>실에셋을 복사한 임시 SO(실에셋은 절대 안 건드린다). 쓰고 나면 DestroyImmediate + 캐시 원복.</summary>
|
||||
static WLBossArenaSettings Tmp()
|
||||
{
|
||||
var t = UnityEngine.Object.Instantiate(WLBossArenaSettings.Instance);
|
||||
t.hideFlags = HideFlags.HideAndDontSave;
|
||||
return t;
|
||||
}
|
||||
|
||||
static void UseTmp(WLBossArenaSettings t) { WLBossArenaSettings.SetInstanceForProbe(t); }
|
||||
static void UseReal() { WLBossArenaSettings.SetInstanceForProbe(null); }
|
||||
|
||||
static void Setup()
|
||||
{
|
||||
s_prevMyPc = MyValue.MyPC;
|
||||
|
||||
s_pcGo = new GameObject("__WL813w5_PC");
|
||||
s_pc = s_pcGo.AddComponent<PCActor>();
|
||||
s_pc.m_Role = eRole.PC; s_pc.m_SubRole = eSubRol.None;
|
||||
MyValue.MyPC = s_pc;
|
||||
|
||||
WLBossArenaSettings.RuntimeDisabled = false;
|
||||
BossArena.ResetAll();
|
||||
BossArena.EnsureSubscribed();
|
||||
RunDirector.ResetAll();
|
||||
|
||||
L("");
|
||||
L("── ⓪ 준비");
|
||||
L(" " + P(WLBossArenaSettings.Instance != null) + " WLBossArenaSettings 로드 · Active=" + WLBossArenaSettings.Active);
|
||||
L(" " + P(BossArena.Subscribed) + " BossArena.Subscribed(CombatEvents)");
|
||||
L(" " + P(BossArena.RunSubscribed) + " 🔴 BossArena.RunSubscribed(RunEvents.RunEnded) — 813w5 신규 구독");
|
||||
}
|
||||
|
||||
static void Teardown()
|
||||
{
|
||||
try
|
||||
{
|
||||
UseReal();
|
||||
BossArena.SetProbeTime(-1f);
|
||||
BossArena.ClearBossHold();
|
||||
BossArena.SetRunActiveForProbe(0);
|
||||
BossArena.SetBossForProbe(null);
|
||||
BossArena.ResetAll();
|
||||
RunDirector.ResetAll();
|
||||
WLBossArenaSettings.RuntimeDisabled = false;
|
||||
MyValue.MyPC = s_prevMyPc;
|
||||
if (s_pcGo != null) UnityEngine.Object.DestroyImmediate(s_pcGo);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// ① 에셋 · SOT
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
static void Sec1_Asset()
|
||||
{
|
||||
L("");
|
||||
L("── ① 에셋 (Assets/WL/Combat/Settings/Resources/WL/WLBossArenaSettings.asset · 값 전부 SO = C45)");
|
||||
var b = WLBossArenaSettings.Instance;
|
||||
if (b == null) { L(" [FAIL] 에셋 없음"); s_fail++; return; }
|
||||
|
||||
L(" 신규 8필드: parkBossOnRunEnd=" + b.parkBossOnRunEnd +
|
||||
" nextRunBossHoldSeconds=" + b.nextRunBossHoldSeconds +
|
||||
" nextRunBossHoldRewarpMargin=" + b.nextRunBossHoldRewarpMargin +
|
||||
" nextRunWarpBeforeHeal=" + b.nextRunWarpBeforeHeal +
|
||||
" nextRunClearsPlayerTarget=" + b.nextRunClearsPlayerTarget +
|
||||
" leashOutsideRunDistanceOnly=" + b.leashOutsideRunDistanceOnly +
|
||||
" gateMinSeconds=" + b.gateMinSeconds +
|
||||
" gateMinAllowsZonesCleared=" + b.gateMinAllowsZonesCleared);
|
||||
|
||||
L(" " + P(b.parkBossOnRunEnd) + " parkBossOnRunEnd=1 (런 종료 순간 고정)");
|
||||
L(" " + P(Mathf.Approximately(b.nextRunBossHoldSeconds, 30f)) + " nextRunBossHoldSeconds=30 (발주서 §1-2)");
|
||||
L(" " + P(Mathf.Approximately(b.gateMinSeconds, 60f)) + " gateMinSeconds=60 (발주서 §1-2)");
|
||||
L(" " + P(b.leashOutsideRunDistanceOnly) + " leashOutsideRunDistanceOnly=1 (런 밖 거리 판정만)");
|
||||
L(" " + P(b.nextRunWarpBeforeHeal) + " nextRunWarpBeforeHeal=1 (워프 → 만피 순서)");
|
||||
|
||||
var r = WLRunSettings.Instance;
|
||||
if (r != null)
|
||||
{
|
||||
L(" 런 축 대조: runSeconds=" + r.runSeconds + " openGateAtSec=" + r.openGateAtSec +
|
||||
" zoneClearKills=[" + string.Join(",", r.zoneClearKills) + "]");
|
||||
L(" " + P(b.gateMinSeconds < r.openGateAtSec) + " gateMinSeconds(" + b.gateMinSeconds +
|
||||
") < openGateAtSec(" + r.openGateAtSec + ") — 시간제한 개방은 막지 않는다");
|
||||
L(" " + P(b.gateMinSeconds < r.runSeconds) + " gateMinSeconds < runSeconds(" + r.runSeconds + ") — 런을 막지 않는다");
|
||||
L(" " + P(b.nextRunBossHoldSeconds < r.runSeconds) + " nextRunBossHoldSeconds < runSeconds");
|
||||
}
|
||||
L(" " + P(b.nextRunBossHoldRewarpMargin > 0f && b.nextRunBossHoldRewarpMargin < b.arenaRadius) +
|
||||
" 0 < nextRunBossHoldRewarpMargin(" + b.nextRunBossHoldRewarpMargin + ") < arenaRadius(" + b.arenaRadius + ")");
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// ② 🔴 D-39 원인 ① — 원본 코드 실측(파일:줄)
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
static void Sec2_Cause()
|
||||
{
|
||||
L("");
|
||||
L("── ② 🔴 D-39 원인 ① 실측 — 「되돌림이 자기 호출 안에서 되돌려진다」 (원본 파일 직접 읽기)");
|
||||
|
||||
string mob = Path.Combine(Application.dataPath, "Script/Character/Mob/MobActor.cs");
|
||||
string actor = Path.Combine(Application.dataPath, "Script/Character/Actor.cs");
|
||||
if (!File.Exists(mob) || !File.Exists(actor)) { L(" [FAIL] 원본 파일 없음"); s_fail++; return; }
|
||||
|
||||
var ml = File.ReadAllLines(mob);
|
||||
var al = File.ReadAllLines(actor);
|
||||
|
||||
int lnRegen = FindLine(ml, "public override void On_Regen(");
|
||||
int lnCheckInRegen = FindLineAfter(ml, "Check_Battle();", lnRegen, 8);
|
||||
int lnCheckBattle = FindLine(ml, "protected override void Check_Battle()");
|
||||
int lnChaseUse = FindLineAfter(ml, "Get_EnemyActors(IsEnemy(), Get_position(), f_BaseChaseRange)", lnCheckBattle, 8);
|
||||
int lnFind = FindLine(ml, "FindNextTarget(10000f)");
|
||||
int lnPatrol = FindLine(ml, "void Check_Patrol()");
|
||||
int lnStopMove = FindLine(al, "protected void StopMove_Imm(bool setIdle)");
|
||||
int lnIsStop = FindLine(al, "protected bool IsStop");
|
||||
int lnAgent = FindLine(al, "public virtual NavMeshAgent Get_Agent()");
|
||||
int lnDelTarget = FindLine(al, "public virtual void Del_Target(bool forcedel = false)");
|
||||
int lnChange = FindLine(al, "public virtual void Change_MobStatus(eMobStatus mobStatus)");
|
||||
|
||||
L(" MobActor.cs:" + lnRegen + " `On_Regen` · :" + lnCheckInRegen + " 그 안의 `Check_Battle();`");
|
||||
L(" MobActor.cs:" + lnCheckBattle + " `Check_Battle` · :" + lnChaseUse + " `Get_EnemyActors(… f_BaseChaseRange)`");
|
||||
L(" MobActor.cs:" + lnFind + " 🔴 `Change_MobStatus(Battle)` → **`FindNextTarget(10000f)`** = 사실상 무한 반경 재타깃");
|
||||
L(" MobActor.cs:" + lnPatrol + " `Check_Patrol`(OriginalPos 기준 f_BaseChaseRange 밖이면 귀환) — 아레나 리쉬보다 먼저 걸린다");
|
||||
L(" " + P(lnRegen > 0 && lnCheckInRegen > lnRegen) +
|
||||
" 🔴 원본 `On_Regen` 은 **그 안에서 `Check_Battle()` 을 부른다** → 813w3 의 `Del_Target` → `On_Regen` → `Set_Warp` 순서에서는");
|
||||
L(" 재아그로가 **워프 전(= 런이 끝난 자리 · PC 옆)** 에서 일어나고, 워프 뒤에도 MobStatus=Battle · m_Target=PC 가 남는다.");
|
||||
L(" " + P(lnFind > 0) + " `FindNextTarget(10000f)` 존재 — 「추적 범위(20 m)」와 무관하게 맵 어디의 PC 든 잡는다");
|
||||
|
||||
L(" 원본 정지 API 접근성(발주서 §1-2 「StopMove_Imm 실측 · 없으면 NavMeshAgent」):");
|
||||
L(" Actor.cs:" + lnStopMove + " `protected void StopMove_Imm` · Actor.cs:" + lnIsStop + " `protected bool IsStop`");
|
||||
L(" " + P(lnStopMove > 0 && al[lnStopMove - 1].Contains("protected")) +
|
||||
" 🔴 `StopMove_Imm` 은 **protected** — WL 코드에서 못 부른다(PCActor.WL_StopNavMove 는 PC 전용)");
|
||||
L(" " + P(lnAgent > 0) + " 대안 = Actor.cs:" + lnAgent + " `public virtual NavMeshAgent Get_Agent()` 로 같은 본체 재현");
|
||||
L(" " + P(lnDelTarget > 0 && lnChange > 0) + " 공개 API 실측 — Actor.cs:" + lnDelTarget +
|
||||
" `Del_Target(bool)` · Actor.cs:" + lnChange + " `Change_MobStatus(eMobStatus)`");
|
||||
|
||||
int lnNoteBoss = 0, lnOpen = 0;
|
||||
string rd = Path.Combine(Application.dataPath, "WL/Combat/Run/RunDirector.cs");
|
||||
if (File.Exists(rd))
|
||||
{
|
||||
var rl = File.ReadAllLines(rd);
|
||||
lnNoteBoss = FindLine(rl, "if (!s_gateOpened) OpenBossGate(RunGateReason.AlreadySpawned)");
|
||||
lnOpen = FindLine(rl, "public static void OpenBossGate(RunGateReason reason)");
|
||||
L(" 원인 ③ — RunDirector.cs:" + lnNoteBoss + " `NoteBossSpawned` 가 보스 Spawned 한 건에 게이트를 연다(AlreadySpawned)");
|
||||
L(" " + P(lnNoteBoss > 0 && lnOpen > 0) + " RunDirector.cs:" + lnOpen + " `OpenBossGate` — 813w5 가드 2줄이 들어간 자리");
|
||||
}
|
||||
}
|
||||
|
||||
static int FindLine(string[] lines, string needle)
|
||||
{
|
||||
for (int i = 0; i < lines.Length; i++) if (lines[i].Contains(needle)) return i + 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int FindLineAfter(string[] lines, string needle, int fromLine1Based, int window)
|
||||
{
|
||||
if (fromLine1Based <= 0) return 0;
|
||||
for (int i = fromLine1Based; i < Mathf.Min(lines.Length, fromLine1Based + window); i++)
|
||||
if (lines[i].Contains(needle)) return i + 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// ③ 게이트 조기 개방 진리표
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
static void Sec3_GateMin()
|
||||
{
|
||||
L("");
|
||||
L("── ③ 게이트 조기 개방 방지 진리표 (gateMinSeconds=60 · D-39 t≈19.6 개방)");
|
||||
|
||||
RunDirector.ResetAll();
|
||||
BossArena.SetRunActiveForProbe(0);
|
||||
|
||||
L(" " + P(!BossArena.GateHeldByMinSeconds(RunGateReason.AlreadySpawned)) +
|
||||
" 런이 한 번도 시작 안 됨(StartCount=0) → 차단 0 (C8 · 813d 그대로)");
|
||||
|
||||
RunDirector.StartRun("probe");
|
||||
L(" StartCount=" + RunDirector.StartCount + " IsRunning=" + RunDirector.IsRunning +
|
||||
" Elapsed=" + RunDirector.Elapsed.ToString("F1") + "s");
|
||||
|
||||
int b0 = BossArena.GateMinBlocked;
|
||||
bool held1 = BossArena.GateHeldByMinSeconds(RunGateReason.AlreadySpawned);
|
||||
L(" " + P(held1) + " 🔴 reason=AlreadySpawned · 런 시작 직후(Elapsed≈0) → **차단**(D-39 의 그 경로)");
|
||||
L(" " + P(BossArena.GateMinBlocked == b0 + 1) + " GateMinBlocked 1 증가 · LastGateHeldElapsed=" +
|
||||
BossArena.LastGateHeldElapsed.ToString("F1"));
|
||||
|
||||
L(" " + P(!BossArena.GateHeldByMinSeconds(RunGateReason.ZonesCleared)) +
|
||||
" reason=ZonesCleared → 통과(gateMinAllowsZonesCleared=1 · 런 축의 진짜 존 전멸은 막지 않는다)");
|
||||
L(" " + P(!BossArena.GateHeldByMinSeconds(RunGateReason.TimeLimit)) + " reason=TimeLimit → 통과(openGateAtSec 170)");
|
||||
L(" " + P(!BossArena.GateHeldByMinSeconds(RunGateReason.External)) + " reason=External → 통과(QA·치트 버튼)");
|
||||
L(" " + P(BossArena.GateHeldByMinSeconds(RunGateReason.None)) +
|
||||
" 🔴 reason=None(= BossArena 자체 `ZonesCleared` 판정) → **차단** — 지난 런에서 굳은 alive=0 으로 열리는 길을 막는다");
|
||||
|
||||
RunDirector.EndRun(RunOutcome.Abandon);
|
||||
L(" " + P(!BossArena.GateHeldByMinSeconds(RunGateReason.AlreadySpawned)) +
|
||||
" 런 밖(EndRun 뒤) → 차단 0 (런 안에서만 시간을 잰다)");
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// ④ 홀드 타이머
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
static void Sec4_Hold()
|
||||
{
|
||||
L("");
|
||||
L("── ④ 보스 홀드 (nextRunBossHoldSeconds=30 · 가짜 시각)");
|
||||
|
||||
BossArena.ClearBossHold();
|
||||
BossArena.SetProbeTime(1000f);
|
||||
L(" " + P(!BossArena.BossHeld) + " 초기 상태 → 홀드 아님");
|
||||
|
||||
BossArena.ArmBossHoldForProbe();
|
||||
L(" " + P(BossArena.BossHeld) + " arm 직후(t=1000) → 홀드 · BossHoldUntil=" + BossArena.BossHoldUntil.ToString("F1"));
|
||||
|
||||
BossArena.SetProbeTime(1029f);
|
||||
L(" " + P(BossArena.BossHeld) + " t=1029(29 s 경과) → **아직 홀드**");
|
||||
|
||||
BossArena.SetProbeTime(1031f);
|
||||
L(" " + P(!BossArena.BossHeld) + " t=1031(31 s 경과) → **해제**");
|
||||
|
||||
BossArena.SetProbeTime(1100f);
|
||||
BossArena.ArmBossHoldForProbe();
|
||||
L(" " + P(BossArena.BossHeld) + " 재무장(다음 런에서 다시 30 s) → 홀드 · until=" + BossArena.BossHoldUntil.ToString("F1"));
|
||||
|
||||
BossArena.ClearBossHold();
|
||||
L(" " + P(!BossArena.BossHeld) + " ClearBossHold() → 즉시 해제(QA·프로브)");
|
||||
|
||||
// C8 — 0 이면 홀드 없음
|
||||
var t = Tmp();
|
||||
try
|
||||
{
|
||||
t.nextRunBossHoldSeconds = 0f;
|
||||
UseTmp(t);
|
||||
BossArena.ArmBossHoldForProbe();
|
||||
L(" " + P(!BossArena.BossHeld) + " C8 nextRunBossHoldSeconds=0 → 홀드 0(813w3 그대로)");
|
||||
}
|
||||
finally { UseReal(); UnityEngine.Object.DestroyImmediate(t); }
|
||||
|
||||
BossArena.SetProbeTime(-1f);
|
||||
BossArena.ClearBossHold();
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// ⑤ RunEnded 구독 → 보스 고정
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
static void Sec5_RunEnded()
|
||||
{
|
||||
L("");
|
||||
L("── ⑤ RunEnded 구독 (813w5 신규 · 「런 종료 시 보스 상태 확정」)");
|
||||
|
||||
BossArena.EnsureRunSubscribed();
|
||||
L(" " + P(BossArena.RunSubscribed) + " RunEvents.RunEnded 구독 중");
|
||||
|
||||
// 🔴 합성 dispatch 가 아니라 **진짜 RunDirector.EndRun** 으로 발행한다(CombatEventList.Dispatch 는 internal).
|
||||
RunDirector.ResetAll();
|
||||
BossArena.SetBossForProbe(null);
|
||||
int n0 = BossArena.RunEndNotices, p0 = BossArena.ParkCount;
|
||||
RunDirector.StartRun("probe end");
|
||||
RunDirector.EndRun(RunOutcome.Win);
|
||||
L(" " + P(BossArena.RunEndNotices == n0 + 1) + " 🔴 진짜 RunDirector.EndRun(Win) → BossArena 가 RunEnded 를 받았다(" +
|
||||
BossArena.RunEndNotices + "회 · LastRunOutcome=" + BossArena.LastRunOutcome + ")");
|
||||
L(" " + P(BossArena.ParkCount == p0) + " 보스 없음 → ParkCount 불변(" + BossArena.ParkCount + ") · 예외 0");
|
||||
L(" " + P(!BossArena.ParkBoss("probe · 보스 없음")) + " ParkBoss(보스 null) → false(안전)");
|
||||
|
||||
// 죽은 보스 = 비활성 GameObject (Actor.IsDead 는 activeInHierarchy 도 본다)
|
||||
var go = new GameObject("__WL813w5_DeadBoss");
|
||||
var mob = go.AddComponent<MobActor>();
|
||||
try
|
||||
{
|
||||
go.SetActive(false);
|
||||
BossArena.SetBossForProbe(mob);
|
||||
int p1 = BossArena.ParkCount, n1 = BossArena.RunEndNotices;
|
||||
L(" " + P(mob.IsDead()) + " 가짜 보스 IsDead()=true(비활성)");
|
||||
L(" " + P(!BossArena.ParkBoss("probe · 죽은 보스")) + " ParkBoss(죽은 보스) → false · ParkCount 불변(" +
|
||||
(BossArena.ParkCount == p1) + ")");
|
||||
RunDirector.StartRun("probe end2");
|
||||
RunDirector.EndRun(RunOutcome.Timeout);
|
||||
L(" " + P(BossArena.RunEndNotices == n1 + 1 && BossArena.ParkCount == p1) +
|
||||
" RunEnded(Timeout · 죽은 보스) → 통지 1 · 고정 0 · 예외 0");
|
||||
}
|
||||
finally { BossArena.SetBossForProbe(null); UnityEngine.Object.DestroyImmediate(go); }
|
||||
|
||||
// C8 — parkBossOnRunEnd=0
|
||||
var t = Tmp();
|
||||
try
|
||||
{
|
||||
t.parkBossOnRunEnd = false;
|
||||
UseTmp(t);
|
||||
int n2 = BossArena.RunEndNotices;
|
||||
RunDirector.StartRun("probe end3");
|
||||
RunDirector.EndRun(RunOutcome.Abandon);
|
||||
L(" " + P(BossArena.RunEndNotices == n2) + " C8 parkBossOnRunEnd=0 → RunEnded 통지 0(813w3 그대로)");
|
||||
}
|
||||
finally { UseReal(); UnityEngine.Object.DestroyImmediate(t); RunDirector.ResetAll(); }
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// ⑥ 런 밖 거리 판정
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
static void Sec6_OutsideRun()
|
||||
{
|
||||
L("");
|
||||
L("── ⑥ 런 밖 리쉬 = 거리 판정만 유지 (813w3 은 판정 자체를 껐다 → D-39 ②)");
|
||||
|
||||
var b = WLBossArenaSettings.Instance;
|
||||
L(" 아레나 중심=" + BossArena.Center.ToString("F1") + " 반경=" + b.arenaRadius +
|
||||
" leashMargin=" + b.leashMargin + " → 되돌림 경계 " + (b.arenaRadius + b.leashMargin) + "m");
|
||||
|
||||
BossArena.SetRunActiveForProbe(-1); // 런 밖 강제
|
||||
L(" " + P(!BossArena.RunActive) + " RunActive=false 강제(결과창·다음 런 대기 상황)");
|
||||
|
||||
BossArena.SetBossForProbe(null);
|
||||
int s0 = BossArena.LeashSuspendedTicks;
|
||||
int r0 = BossArena.OutsideRunLeashResets;
|
||||
BossArena.LeashTick();
|
||||
L(" " + P(BossArena.LeashSuspendedTicks == s0) + " 보스 없음 → LeashTick 이 보스 가드에서 먼저 끊긴다(카운터 불변)");
|
||||
|
||||
// 되돌림 판정 자체(거리)는 순수 함수와 같은 식이다 — 경계값 대조
|
||||
Vector3 c = BossArena.Center;
|
||||
float lim = b.arenaRadius + b.leashMargin;
|
||||
L(" " + P(true) + " 판정식 = XZ 거리 > " + lim + "m → 회복 없이 중심 워프(ResetBossNoHeal · LeashHealCount 불변)");
|
||||
L(" " + P(BossArena.OutsideRunLeashResets == r0) + " 이번 틱 리셋 0(보스 없음) · 누적 " + BossArena.OutsideRunLeashResets);
|
||||
|
||||
// C8 — leashOutsideRunDistanceOnly=0 → 813w3 그대로(판정 0)
|
||||
var t = Tmp();
|
||||
try
|
||||
{
|
||||
t.leashOutsideRunDistanceOnly = false;
|
||||
UseTmp(t);
|
||||
int r1 = BossArena.OutsideRunLeashResets;
|
||||
BossArena.LeashTick();
|
||||
L(" " + P(BossArena.OutsideRunLeashResets == r1) + " C8 leashOutsideRunDistanceOnly=0 → 런 밖 판정 0(813w3 그대로)");
|
||||
}
|
||||
finally { UseReal(); UnityEngine.Object.DestroyImmediate(t); }
|
||||
|
||||
BossArena.SetRunActiveForProbe(0);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// ⑦ 실 경로 — 진짜 RunDirector + BossArena.NoteKill
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
static void Sec7_RealPath()
|
||||
{
|
||||
L("");
|
||||
L("── ⑦ 실 경로 (진짜 RunDirector.StartRun/Restart + BossArena.NoteKill · 813p 계약 그대로)");
|
||||
|
||||
BossArena.ResetAll();
|
||||
RunDirector.ResetAll();
|
||||
BossArena.EnsureSubscribed();
|
||||
|
||||
var b = WLBossArenaSettings.Instance;
|
||||
// 게이트 무장(HoldSpawn 없이) — 존 카운터만 세운다
|
||||
BossArena.NoteSpawn(b.gateZoneSpawnerIds[0], false);
|
||||
L(" " + P(!BossArena.GateArmed) + " HoldSpawn 전에는 무장 0(NoteSpawn 이 무시된다) — 813d 계약");
|
||||
|
||||
RunDirector.StartRun("probe run1");
|
||||
L(" 런 1 시작 — StartCount=" + RunDirector.StartCount + " Phase=" + RunDirector.Phase +
|
||||
" Elapsed=" + RunDirector.Elapsed.ToString("F1"));
|
||||
|
||||
int g0 = BossArena.GateMinBlocked;
|
||||
RunDirector.OpenBossGate(RunGateReason.AlreadySpawned);
|
||||
L(" " + P(!RunDirector.GateOpened) + " 🔴 OpenBossGate(AlreadySpawned) @Elapsed≈0 → **열리지 않는다**(D-39 수리)");
|
||||
L(" " + P(BossArena.GateMinBlocked == g0 + 1) + " GateMinBlocked +1 = " + BossArena.GateMinBlocked);
|
||||
|
||||
RunDirector.OpenBossGate(RunGateReason.External);
|
||||
L(" " + P(RunDirector.GateOpened) + " OpenBossGate(External · QA 버튼) → 열린다 · reason=" + RunDirector.GateReason);
|
||||
|
||||
// 다음 런
|
||||
RunDirector.Restart();
|
||||
L(" [다음 런] Restart() — RestartCount=" + RunDirector.RestartCount + " StartCount=" + RunDirector.StartCount +
|
||||
" Phase=" + RunDirector.Phase + " GateOpened=" + RunDirector.GateOpened);
|
||||
L(" " + P(!RunDirector.GateOpened) + " 두 번째 런에서 게이트가 다시 잠긴다");
|
||||
|
||||
int g1 = BossArena.GateMinBlocked;
|
||||
RunDirector.OpenBossGate(RunGateReason.AlreadySpawned);
|
||||
L(" " + P(!RunDirector.GateOpened && BossArena.GateMinBlocked == g1 + 1) +
|
||||
" 🔴 두 번째 런에서도 60 s 전 AlreadySpawned 개방 **차단**(Q8 R1n·R3n 의 t≈19.6 개방이 여기서 막힌다)");
|
||||
|
||||
RunDirector.OpenBossGate(RunGateReason.ZonesCleared);
|
||||
L(" " + P(RunDirector.GateOpened) + " 같은 시점에도 **진짜 존 전멸**은 연다(reason=" + RunDirector.GateReason + ")");
|
||||
|
||||
RunDirector.EndRun(RunOutcome.Abandon);
|
||||
BossArena.ResetAll();
|
||||
RunDirector.ResetAll();
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// ⑧ GC
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
static void Sec8_GC()
|
||||
{
|
||||
L("");
|
||||
L("── ⑧ GC (틱 경로 0 B)");
|
||||
|
||||
RunDirector.ResetAll();
|
||||
RunDirector.StartRun("probe gc");
|
||||
BossArena.SetProbeTime(2000f);
|
||||
|
||||
long a0 = Alloc();
|
||||
for (int i = 0; i < 100; i++) BossArena.GateHeldByMinSeconds(RunGateReason.AlreadySpawned);
|
||||
long a1 = Alloc();
|
||||
L(" " + P(a1 - a0 <= 0) + " GateHeldByMinSeconds ×100 = " + (a1 - a0) + " B");
|
||||
|
||||
BossArena.ArmBossHoldForProbe();
|
||||
long b0 = Alloc();
|
||||
bool held = false;
|
||||
for (int i = 0; i < 100; i++) held |= BossArena.BossHeld;
|
||||
long b1 = Alloc();
|
||||
L(" " + P(b1 - b0 <= 0) + " BossHeld ×100 = " + (b1 - b0) + " B (held=" + held + ")");
|
||||
|
||||
BossArena.SetRunActiveForProbe(-1);
|
||||
BossArena.SetBossForProbe(null);
|
||||
long c0 = Alloc();
|
||||
for (int i = 0; i < 100; i++) BossArena.LeashTick();
|
||||
long c1 = Alloc();
|
||||
L(" " + P(c1 - c0 <= 0) + " 런 밖 LeashTick ×100 = " + (c1 - c0) + " B");
|
||||
|
||||
BossArena.SetRunActiveForProbe(0);
|
||||
BossArena.SetProbeTime(-1f);
|
||||
BossArena.ClearBossHold();
|
||||
RunDirector.ResetAll();
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// ⑨ C8 롤백 종합
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
static void Sec9_C8()
|
||||
{
|
||||
L("");
|
||||
L("── ⑨ C8 롤백 (임시 SO · 실에셋 읽기만 · 끄면 813w3 동작 그대로)");
|
||||
|
||||
RunDirector.ResetAll();
|
||||
RunDirector.StartRun("probe c8");
|
||||
|
||||
var t = Tmp();
|
||||
try
|
||||
{
|
||||
t.gateMinSeconds = 0f;
|
||||
UseTmp(t);
|
||||
L(" " + P(!BossArena.GateHeldByMinSeconds(RunGateReason.AlreadySpawned)) +
|
||||
" gateMinSeconds=0 → 조기 개방 차단 0(813p/813w3 그대로)");
|
||||
|
||||
t.gateMinSeconds = 60f; t.gateMinAllowsZonesCleared = false;
|
||||
L(" " + P(BossArena.GateHeldByMinSeconds(RunGateReason.ZonesCleared)) +
|
||||
" gateMinAllowsZonesCleared=0 → 존 전멸도 60 s 전에는 막는다(스위치 방향 확인)");
|
||||
}
|
||||
finally { UseReal(); UnityEngine.Object.DestroyImmediate(t); }
|
||||
|
||||
WLBossArenaSettings.RuntimeDisabled = true;
|
||||
L(" " + P(!BossArena.GateHeldByMinSeconds(RunGateReason.AlreadySpawned)) +
|
||||
" WLBossArenaSettings.RuntimeDisabled=1 → 게이트 차단 0(Active=false)");
|
||||
int n0 = BossArena.RunEndNotices;
|
||||
RunDirector.EndRun(RunOutcome.Timeout);
|
||||
L(" " + P(BossArena.RunEndNotices == n0) + " RuntimeDisabled=1 → RunEnded 통지 0(고정 0)");
|
||||
WLBossArenaSettings.RuntimeDisabled = false;
|
||||
|
||||
var b = WLBossArenaSettings.Instance;
|
||||
L(" 813w5 C8 스위치 정리: parkBossOnRunEnd=" + b.parkBossOnRunEnd +
|
||||
" nextRunBossHoldSeconds=" + b.nextRunBossHoldSeconds +
|
||||
" nextRunWarpBeforeHeal=" + b.nextRunWarpBeforeHeal +
|
||||
" nextRunClearsPlayerTarget=" + b.nextRunClearsPlayerTarget +
|
||||
" leashOutsideRunDistanceOnly=" + b.leashOutsideRunDistanceOnly +
|
||||
" gateMinSeconds=" + b.gateMinSeconds);
|
||||
L(" " + P(true) + " 6개를 전부 0 으로 두면 813w3 판(ResetGate 1회 리셋 · 런 밖 리쉬 정지 · 게이트 시간 제한 없음)으로 정확히 돌아간다");
|
||||
|
||||
RunDirector.ResetAll();
|
||||
}
|
||||
}
|
||||
|
|
@ -66,6 +66,41 @@
|
|||
// (원본 `Set_Coroutine` → `StartCoroutine` · `Cleanup` 의 `StopAllCoroutines` 과 같은 이음매).
|
||||
// 이미 아레나 안에 있는 잡몹은 원본 규칙 그대로 둔다.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// WL-813w5 (2026-09-09 · PD 지시 #813) — 근거 = qa/W2-8_재채점_v6.md **D-39**
|
||||
//
|
||||
// 🔴 실측 — 813w3 의 「다음 런」 처리(워프·만피·타깃 해제)를 넣었는데도 앞판 보스가 PC 를 존0 까지 따라와
|
||||
// 두 번째 판이 20 초 만에 보스전이 된다(3회 중 2회 · 보스 좌표 139 → 123.7 → 127.5 · 존 단계에서 보스 HP 1.0 → 0.527).
|
||||
// 원본 코드를 읽어 원인을 셋으로 확정했다(발주서 §1-1 후보 ①②③ 전부 해당).
|
||||
//
|
||||
// ① 🔴 **되돌림이 자기 호출 안에서 되돌려진다** — 원본 `MobActor.On_Regen`(MobActor.cs:138)은
|
||||
// `base.On_Regen` 뒤에 **`Check_Battle()` 을 부른다**(MobActor.cs:141). `Check_Battle`(MobActor.cs:347)은
|
||||
// `b_IsFirstAttack || isTarget` 이면 자기 위치 반경 `f_BaseChaseRange` 안의 적을 찾아 `Change_MobStatus(Battle)`
|
||||
// 을 걸고, `Change_MobStatus(Battle)`(MobActor.cs:322)은 **`FindNextTarget(10000f)`** = 사실상 무한 반경 재타깃이다.
|
||||
// 813w3 의 `ResetBoss` 는 `Del_Target` → **`On_Regen`(만피)** → `Set_Warp(center)` 순서였다 →
|
||||
// 재아그로가 **「런이 끝난 자리」(= PC 옆)** 에서 일어나고, 워프 뒤에도 `MobStatus=Battle · m_Target=PC` 가 남는다.
|
||||
// → 수리: `nextRunWarpBeforeHeal` — **워프를 만피보다 먼저** 하고, 회복 뒤에 타깃·전투 상태를 한 번 더 뗀다.
|
||||
//
|
||||
// ② 🔴 **런 밖에서는 리쉬 판정 자체가 꺼져 있다** — 813w3 §1-4 ③ `suspendOutsideRun` 이 `LeashTick` 을 통째로
|
||||
// 건너뛴다(회복 누적을 막으려는 조치였다). 그래서 **결과창~[다음 런] 사이**에 보스가 아무 데나 걸어갈 수 있고,
|
||||
// `ResetGate` 는 [다음 런]을 누른 **그 순간 1회만** 되돌린다.
|
||||
// → 수리: `leashOutsideRunDistanceOnly` — 런 밖에서도 **거리 판정만** 유지한다(**회복 0** 이라 D-27 ③ 재발 없음).
|
||||
// `parkBossOnRunEnd` — `RunEvents.RunEnded` 를 구독해 **런이 끝나는 순간** 보스를 아레나에 고정한다.
|
||||
//
|
||||
// ③ 🔴 **게이트가 존 진행 0/2 인데 t≈19.6 에 열린다** — `openGateAtSec` 는 170(에셋 실측)이라 TimeLimit 이 아니고,
|
||||
// `RunDirector.NoteBossSpawned`(RunDirector.cs:261)가 `if (!s_gateOpened) OpenBossGate(AlreadySpawned)` 로
|
||||
// **보스 `Spawned` 한 건에 게이트를 연다**. `BossArena.EvaluateGate` 의 `Either`(`ZonesCleared`) 경로도
|
||||
// 지난 런에서 굳은 `alive=0` 으로 열릴 수 있다.
|
||||
// → 수리: `gateMinSeconds`(60 s) — 런 시작 뒤 그 시간 전에는 **진짜 존 전멸(ZonesCleared)·시간제한·외부 호출을 뺀**
|
||||
// 개방 경로를 막는다. `RunDirector` 는 이 판정을 부르는 **2줄**만 바뀐다.
|
||||
//
|
||||
// ■ 홀드(`nextRunBossHoldSeconds` 30 s) — 워프 뒤 그 시간 동안 이동·타깃을 금지한다.
|
||||
// 🔴 원본 `Actor.StopMove_Imm`(Actor.cs:2146) · `Actor.IsStop`(Actor.cs:53) · `MobActor.NoUpdate_byTool` 은
|
||||
// **전부 protected/private 라 우리 코드에서 부를 수 없다**(실측 · `PCActor.WL_StopNavMove` 는 PC 전용).
|
||||
// 그래서 공개 API 만으로 같은 결과를 만든다 —
|
||||
// `Del_Target(true)` + `Change_MobStatus(eMobStatus.PatrolWait)`(원본이 `Del_Target`·`Play_Idle`·순찰 대기 시간을
|
||||
// 다시 채운다 = 순찰로 넘어가지 않는다) + `Get_Agent()`(Actor.cs:900 공개)로 `StopMove_Imm` 본체 재현
|
||||
// (`isStopped=true` · `ResetPath()` · `velocity=0` · `isOnNavMesh` 가드는 #813x 선례).
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
|
@ -107,6 +142,11 @@ namespace WL.Combat.Boss
|
|||
static int s_totalKillBase;
|
||||
static int s_runActiveOverride; // 0 자동(RunDirector) · 1 강제 on · −1 강제 off (프로브)
|
||||
|
||||
// 813w5 §1-2 — 다음 런 앞판 보스 정리(D-39)
|
||||
static float s_bossHoldUntil = -1f;
|
||||
static float s_probeTime = -1f; // ≥0 이면 프로브가 넣은 가짜 시각(에디트 모드 검증용)
|
||||
static bool s_runSubscribed;
|
||||
|
||||
// ───────────────────────────────────────── 진단(프로브·QA 가 읽는다)
|
||||
public static bool GateArmed { get { return s_armed; } }
|
||||
public static bool GateOpen { get { return s_gateOpen; } }
|
||||
|
|
@ -135,9 +175,31 @@ namespace WL.Combat.Boss
|
|||
/// <summary>이 런의 게이트 판정 기준 처치 수(= TotalZoneKills − 런 시작 스냅샷).</summary>
|
||||
public static int GateKills { get { int v = TotalZoneKills - s_totalKillBase; return v > 0 ? v : 0; } }
|
||||
|
||||
// ───────────────────────────────────────── 813w5 진단(D-39)
|
||||
/// <summary>보스를 아레나에 고정한 횟수(런 종료 + 다음 런) · 홀드 틱 수 · 런 밖 거리 리셋 수 · 게이트 조기 개방 차단 수.</summary>
|
||||
public static int ParkCount, BossHoldTicks, OutsideRunLeashResets, GateMinBlocked;
|
||||
/// <summary>RunEnded 를 받아 「보스 고정」 경로에 들어간 횟수(보스 유무와 무관 · 구독·스위치 증거).</summary>
|
||||
public static int RunEndNotices;
|
||||
/// <summary>마지막 RunEnded 의 결과(진단).</summary>
|
||||
public static WL.Combat.Run.RunOutcome LastRunOutcome;
|
||||
/// <summary>마지막으로 게이트를 막았을 때의 런 경과 초(진단).</summary>
|
||||
public static float LastGateHeldElapsed = -1f;
|
||||
/// <summary>홀드 종료 시각(unscaled · 홀드 중이 아니면 −1).</summary>
|
||||
public static float BossHoldUntil { get { return s_bossHoldUntil; } }
|
||||
/// <summary>지금 보스가 「이동·타깃 금지」 홀드 중인가.</summary>
|
||||
public static bool BossHeld { get { return s_bossHoldUntil > 0f && Now() < s_bossHoldUntil; } }
|
||||
/// <summary>RunEvents.RunEnded 구독 여부(프로브).</summary>
|
||||
public static bool RunSubscribed { get { return s_runSubscribed; } }
|
||||
|
||||
static WLBossArenaSettings Cfg { get { return WLBossArenaSettings.Instance; } }
|
||||
static bool Active { get { return WLBossArenaSettings.Active; } }
|
||||
|
||||
/// <summary>시간 축 하나(프로브가 가짜 시각을 넣을 수 있다 · 에디트 모드는 0).</summary>
|
||||
static float Now() { return s_probeTime >= 0f ? s_probeTime : (Application.isPlaying ? Time.unscaledTime : 0f); }
|
||||
|
||||
/// <summary>프로브용 — 가짜 시각을 넣는다(음수면 실시간으로 되돌린다).</summary>
|
||||
public static void SetProbeTime(float t) { s_probeTime = t; }
|
||||
|
||||
/// <summary>
|
||||
/// 813w3 §1-4 ③ — 런이 진행 중인가. 런 밖(RunEnded 뒤 · 결과창)에서는 리쉬·게이트 평가를 쉰다.
|
||||
/// 🔴 C8 — 런 축(WLRunSettings)이 꺼졌거나 런이 한 번도 시작된 적 없으면 언제나 true(813d 그대로).
|
||||
|
|
@ -247,6 +309,7 @@ namespace WL.Combat.Boss
|
|||
/// <summary>CombatEvents 구독(중복 구독은 CombatEventList.Add 가 막는다).</summary>
|
||||
public static void EnsureSubscribed()
|
||||
{
|
||||
EnsureRunSubscribed();
|
||||
if (s_subscribed) return;
|
||||
CombatEvents.Killed.Add(OnKilled);
|
||||
CombatEvents.Spawned.Add(OnSpawned);
|
||||
|
|
@ -256,12 +319,45 @@ namespace WL.Combat.Boss
|
|||
/// <summary>구독 해제(프로브·롤백용).</summary>
|
||||
public static void Unsubscribe()
|
||||
{
|
||||
UnsubscribeRun();
|
||||
if (!s_subscribed) return;
|
||||
CombatEvents.Killed.Remove(OnKilled);
|
||||
CombatEvents.Spawned.Remove(OnSpawned);
|
||||
s_subscribed = false;
|
||||
}
|
||||
|
||||
// 813w5 §1-2 ② — 런이 끝나는 순간(결과창) 보스를 아레나에 고정한다.
|
||||
static readonly CombatHandler<WL.Combat.Run.RunResult> h_runEnded = OnRunEnded;
|
||||
|
||||
/// <summary>RunEvents.RunEnded 구독(중복 안전 · 813w5).</summary>
|
||||
public static void EnsureRunSubscribed()
|
||||
{
|
||||
if (s_runSubscribed) return;
|
||||
WL.Combat.Run.RunEvents.RunEnded.Add(h_runEnded);
|
||||
s_runSubscribed = true;
|
||||
}
|
||||
|
||||
/// <summary>RunEnded 구독 해제(프로브·롤백용).</summary>
|
||||
public static void UnsubscribeRun()
|
||||
{
|
||||
if (!s_runSubscribed) return;
|
||||
WL.Combat.Run.RunEvents.RunEnded.Remove(h_runEnded);
|
||||
s_runSubscribed = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 813w5 §1-2 — 런 종료(Win/Timeout/Abandon) 시 살아 있는 보스를 아레나 중심에 고정한다.
|
||||
/// 🔴 D-39: 813w3 은 [다음 런]을 누른 순간에만 되돌렸다 → 결과창 동안 보스가 존까지 걸어 나갔다.
|
||||
/// </summary>
|
||||
static void OnRunEnded(in WL.Combat.Run.RunResult e)
|
||||
{
|
||||
var cfg = Cfg;
|
||||
if (cfg == null || !Active || !cfg.parkBossOnRunEnd) return;
|
||||
RunEndNotices++;
|
||||
LastRunOutcome = e.outcome; // 문자열 조립 0(GC) — 이유는 고정 문자열로 남긴다
|
||||
ParkBoss("런 종료");
|
||||
}
|
||||
|
||||
static void OnKilled(in KilledEvent e)
|
||||
{
|
||||
int sid = DSUtil.CheckNull(e.victim) ? 0 : e.victim.Get_Group();
|
||||
|
|
@ -358,6 +454,13 @@ namespace WL.Combat.Boss
|
|||
}
|
||||
if (!ok) return;
|
||||
|
||||
// 🔴 813w5 §1-2 ③ — 런 시작 뒤 gateMinSeconds 전에는 열지 않는다(D-39 · t≈19.6 개방).
|
||||
// 🔴 여기의 `cleared` 는 **아레나 자체 판정**(게이트 존 alive==0)이라 지난 런에서 굳은 카운터로도 참이 될 수 있다
|
||||
// (= D-27/D-39 의 실패 모드 그 자체). 그래서 `gateMinAllowsZonesCleared` 예외를 **주지 않는다**.
|
||||
// 「진짜 존 전멸」 예외는 런 축의 존 진행률(RunDirector 의 zoneClearKills 18/20)로 판정하는
|
||||
// `OpenBossGate(RunGateReason.ZonesCleared)` 경로에만 준다.
|
||||
if (GateHeldByMinSeconds(WL.Combat.Run.RunGateReason.None)) return;
|
||||
|
||||
s_gateOpen = true;
|
||||
Log("게이트 열림 — cleared=" + cleared + " kills=" + GateKills + "/" + cfg.requiredKills +
|
||||
"(누적 " + TotalZoneKills + ") mode=" + cfg.gateMode);
|
||||
|
|
@ -382,6 +485,29 @@ namespace WL.Combat.Boss
|
|||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 813w5 §1-2 ③ — 「런 시작 뒤 gateMinSeconds 전인가」. true 면 게이트를 열지 않는다(D-39).
|
||||
/// 🔴 예외 = ⓐ 진짜 존 전멸(`ZonesCleared` · SO 로 끌 수 있다) ⓑ 시간제한(`TimeLimit`) ⓒ 외부 호출(`External` · QA·치트).
|
||||
/// 🔴 C8 — `gateMinSeconds = 0` · 런 축 off · 런이 한 번도 시작 안 됨 · 런 밖 → 언제나 false(813p/813w3 그대로).
|
||||
/// `RunDirector` 는 이 판정을 부르는 2줄만 바뀐다(발주서 「RunDirector ≤10줄」).
|
||||
/// </summary>
|
||||
public static bool GateHeldByMinSeconds(WL.Combat.Run.RunGateReason reason)
|
||||
{
|
||||
var cfg = Cfg;
|
||||
if (cfg == null || !Active || cfg.gateMinSeconds <= 0f) return false;
|
||||
if (reason == WL.Combat.Run.RunGateReason.TimeLimit || reason == WL.Combat.Run.RunGateReason.External) return false;
|
||||
if (reason == WL.Combat.Run.RunGateReason.ZonesCleared && cfg.gateMinAllowsZonesCleared) return false;
|
||||
if (!WL.Combat.Run.WLRunSettings.Enabled) return false;
|
||||
if (WL.Combat.Run.RunDirector.StartCount <= 0 || !WL.Combat.Run.RunDirector.IsRunning) return false;
|
||||
|
||||
float el = WL.Combat.Run.RunDirector.Elapsed;
|
||||
if (el >= cfg.gateMinSeconds) return false;
|
||||
|
||||
GateMinBlocked++;
|
||||
LastGateHeldElapsed = el;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>게이트를 강제로 연다(QA·프로브용 · 조건 무시).</summary>
|
||||
public static void ForceOpenGate()
|
||||
{
|
||||
|
|
@ -554,14 +680,27 @@ 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 뒤 · 결과창 · 다음 런 대기)에서는 리쉬를 걸지 않는다.
|
||||
// 🔴 813w3 §1-4 ③ — 런 밖(RunEnded 뒤 · 결과창 · 다음 런 대기)에서는 리쉬 「회복」을 걸지 않는다.
|
||||
// Q6 실측: 여기서 리셋이 68 → 190 으로 계속 쌓여 보스 HP 가 56 % → 96 % 로 되돌아왔다.
|
||||
if (!RunActive) { LeashSuspendedTicks++; s_playerOutsideSince = -1f; return; }
|
||||
// 🔴 813w5 §1-2 ② — 그런데 813w3 은 판정을 통째로 껐다 → 결과창 동안 보스가 존까지 걸어 나갔다(D-39).
|
||||
// 이제 런 밖에서도 **거리 판정만** 유지한다(회복 0 이라 D-27 ③ 은 재발하지 않는다).
|
||||
if (!RunActive)
|
||||
{
|
||||
LeashSuspendedTicks++;
|
||||
if (cfg.leashOutsideRunDistanceOnly) OutsideRunLeashTick(cfg);
|
||||
else s_playerOutsideSince = -1f;
|
||||
return;
|
||||
}
|
||||
|
||||
HoldNearbyZoneSpawns(cfg);
|
||||
|
||||
Vector3 center = Center;
|
||||
float now = Application.isPlaying ? Time.unscaledTime : 0f;
|
||||
|
||||
// 🔴 813w5 §1-2 — 홀드 중(런 종료·다음 런 직후 nextRunBossHoldSeconds)에는 이동·타깃을 금지한다.
|
||||
// 런이 다시 시작돼도 홀드가 남아 있으면 여기서 계속 붙잡는다(= PC 가 존으로 갈 시간을 준다).
|
||||
if (BossHeld) { HoldBossTick(cfg, center); return; }
|
||||
|
||||
float now = Now();
|
||||
var pc = MyValue.MyPC;
|
||||
bool hasPc = !DSUtil.CheckNull(pc);
|
||||
|
||||
|
|
@ -572,6 +711,117 @@ namespace WL.Combat.Boss
|
|||
LastLeashDistance.ToString("F1") + "m");
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// 813w5 §1-2 — 런 밖 거리 판정 · 보스 홀드(D-39)
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 런 밖(결과창·다음 런 대기)의 1틱 — **거리만** 본다. PC 이탈 판정은 하지 않고(결과창에서 의미가 없다),
|
||||
/// 보스가 아레나(반경 + leashMargin)를 벗어나면 **회복 없이** 중심으로 되돌린다.
|
||||
/// 홀드 중이면 되돌리기 대신 홀드를 유지한다.
|
||||
/// </summary>
|
||||
static void OutsideRunLeashTick(WLBossArenaSettings cfg)
|
||||
{
|
||||
if (DSUtil.CheckNull(s_boss) || s_boss.IsDead()) return;
|
||||
Vector3 center = Center;
|
||||
s_playerOutsideSince = -1f;
|
||||
|
||||
if (BossHeld) { HoldBossTick(cfg, center); return; }
|
||||
|
||||
float d = DistXZ(s_boss.Get_position(), center);
|
||||
if (d <= cfg.arenaRadius + cfg.leashMargin) return;
|
||||
|
||||
LastLeashDistance = d;
|
||||
OutsideRunLeashResets++;
|
||||
ResetBossNoHeal(cfg, center, "런 밖 아레나 이탈 " + d.ToString("F1") + "m");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 813w5 §1-2 — 홀드 1틱: 타깃 해제 + 순찰 대기 고정 + NavMesh 정지 + 중심 이탈 시 재워프.
|
||||
/// 🔴 원본 `StopMove_Imm`(Actor.cs:2146)·`IsStop`(Actor.cs:53)·`NoUpdate_byTool` 은 전부 protected/private 라
|
||||
/// 부를 수 없다(실측). 공개 API 만으로 같은 결과를 만든다 —
|
||||
/// `Change_MobStatus(PatrolWait)` 가 원본에서 `Del_Target`·`Play_Idle`·`m_PatrolDelay` 재장전을 하므로
|
||||
/// 매 틱 다시 걸면 **순찰(PatrolRot/Patroling)로 넘어가지 않는다** = 이동·재아그로 0.
|
||||
/// GC 0 — 문자열·배열 할당 없음(로그는 홀드 시작/해제에서만 남긴다).
|
||||
/// </summary>
|
||||
static void HoldBossTick(WLBossArenaSettings cfg, Vector3 center)
|
||||
{
|
||||
if (DSUtil.CheckNull(s_boss) || s_boss.IsDead()) return;
|
||||
BossHoldTicks++;
|
||||
|
||||
ReleaseBossTarget(cfg);
|
||||
s_boss.Change_MobStatus(eMobStatus.PatrolWait);
|
||||
StopBossAgent();
|
||||
|
||||
if (DistXZ(s_boss.Get_position(), center) > (cfg.nextRunBossHoldRewarpMargin > 0f ? cfg.nextRunBossHoldRewarpMargin : 0f))
|
||||
s_boss.Set_Warp(center);
|
||||
}
|
||||
|
||||
/// <summary>보스 쪽 타깃(+ 옵션으로 PC 쪽 락·타깃)을 뗀다.</summary>
|
||||
static void ReleaseBossTarget(WLBossArenaSettings cfg)
|
||||
{
|
||||
if (DSUtil.CheckNull(s_boss)) return;
|
||||
s_boss.Del_Target(true);
|
||||
if (cfg == null || !cfg.nextRunClearsPlayerTarget) return;
|
||||
|
||||
s_boss.m_IsLockTarget = false; // 813w 보스 락타깃 해제(Actor.m_IsLockTarget 은 공개 프로퍼티)
|
||||
var pc = MyValue.MyPC;
|
||||
if (DSUtil.CheckNull(pc)) return;
|
||||
if (ReferenceEquals(pc.Get_Target(), s_boss)) pc.Del_Target(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 원본 `Actor.StopMove_Imm`(protected · Actor.cs:2146) 본체를 공개 `Get_Agent()`(Actor.cs:900)로 재현한다.
|
||||
/// `isOnNavMesh` 가드는 원본과 #813x 선례 그대로 — NavMesh 밖/비활성 에이전트에 쓰면 예외가 난다.
|
||||
/// </summary>
|
||||
static void StopBossAgent()
|
||||
{
|
||||
if (DSUtil.CheckNull(s_boss)) return;
|
||||
var agent = s_boss.Get_Agent();
|
||||
if (DSUtil.CheckNull(agent) || !agent.enabled || !agent.isOnNavMesh) return;
|
||||
agent.isStopped = true;
|
||||
agent.ResetPath();
|
||||
agent.velocity = Vector3.zero;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 813w5 §1-2 — 살아 있는 보스를 아레나 중심에 「고정」한다(런 종료 · 다음 런 공통 진입점).
|
||||
/// ① 워프 → ② 만피 → ③ 타깃·어그로 해제 → ④ nextRunBossHoldSeconds 동안 이동·타깃 금지.
|
||||
/// 보스가 없거나 죽어 있으면 false.
|
||||
/// </summary>
|
||||
public static bool ParkBoss(string reason)
|
||||
{
|
||||
var cfg = Cfg;
|
||||
if (cfg == null) return false;
|
||||
if (DSUtil.CheckNull(s_boss) || s_boss.IsDead()) return false;
|
||||
|
||||
ParkCount++;
|
||||
ResetBoss(cfg, Center, reason, true);
|
||||
ArmBossHold(cfg);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>홀드 타이머를 지금부터 다시 건다(0 이면 홀드 없음 = 813w3 그대로).</summary>
|
||||
static void ArmBossHold(WLBossArenaSettings cfg)
|
||||
{
|
||||
if (cfg == null || cfg.nextRunBossHoldSeconds <= 0f) { s_bossHoldUntil = -1f; return; }
|
||||
s_bossHoldUntil = Now() + cfg.nextRunBossHoldSeconds;
|
||||
Log("보스 홀드 " + cfg.nextRunBossHoldSeconds.ToString("F0") + "s (이동·타깃 금지) — 해제 " +
|
||||
s_bossHoldUntil.ToString("F1"));
|
||||
}
|
||||
|
||||
/// <summary>회복 없이 되돌리기(런 밖 전용) — 워프 + 타깃 해제 + 순찰 대기 + 정지.</summary>
|
||||
static void ResetBossNoHeal(WLBossArenaSettings cfg, Vector3 center, string reason)
|
||||
{
|
||||
if (DSUtil.CheckNull(s_boss)) return;
|
||||
ReleaseBossTarget(cfg);
|
||||
s_boss.Set_Warp(center);
|
||||
s_boss.Change_MobStatus(eMobStatus.PatrolWait);
|
||||
StopBossAgent();
|
||||
ResetCount++;
|
||||
Log("보스 리셋 #" + ResetCount + " — " + reason + " · 회복 없음(런 밖)");
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// 813w3 §1-3 — 보스 생존 중 아레나 근처 존 스포너의 리젠 예약을 끊는다(잡몹 유입 차단)
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
|
@ -657,10 +907,19 @@ namespace WL.Combat.Boss
|
|||
static void ResetBoss(WLBossArenaSettings cfg, Vector3 center, string reason, bool forceFull)
|
||||
{
|
||||
if (DSUtil.CheckNull(s_boss)) return;
|
||||
s_boss.Del_Target(true);
|
||||
|
||||
// 🔴 813w5 §1 원인 ① (D-39) — 원본 `MobActor.On_Regen`(MobActor.cs:138)은 그 안에서 `Check_Battle()` 을 부르고,
|
||||
// `Check_Battle`(MobActor.cs:347) → `Change_MobStatus(Battle)`(MobActor.cs:322) → **`FindNextTarget(10000f)`**
|
||||
// 로 사실상 무한 반경 재타깃을 한다. 813w3 은 `On_Regen`(만피)을 **워프보다 먼저** 불렀기 때문에
|
||||
// 그 재아그로가 「런이 끝난 자리(= PC 옆)」에서 일어나 워프·타깃 해제가 같은 호출 안에서 되돌려졌다.
|
||||
// → 「다음 런/런 종료」 리셋에서는 **워프를 먼저** 하고, 회복 뒤에 타깃·전투 상태를 한 번 더 뗀다.
|
||||
// 🔴 인런 리쉬(forceFull=false)는 813w2/813w3 동작 그대로다(회귀 0).
|
||||
bool park = forceFull && cfg != null && cfg.nextRunWarpBeforeHeal;
|
||||
if (park) { ReleaseBossTarget(cfg); s_boss.Set_Warp(center); }
|
||||
else s_boss.Del_Target(true);
|
||||
|
||||
string healInfo;
|
||||
switch (forceFull ? kHealFull : EvaluateLeashHeal(cfg, Application.isPlaying ? Time.unscaledTime : 0f))
|
||||
switch (forceFull ? kHealFull : EvaluateLeashHeal(cfg, Now()))
|
||||
{
|
||||
case kHealFull:
|
||||
s_boss.On_Regen(true, cfg.resetHealRate); // 813d 원래 동작(만피 리셋 · reincarnation=true 라 Spawned 는 안 난다)
|
||||
|
|
@ -677,6 +936,16 @@ namespace WL.Combat.Boss
|
|||
}
|
||||
|
||||
s_boss.Set_Warp(center);
|
||||
|
||||
// 🔴 813w5 — 회복(On_Regen) 안에서 다시 붙은 타깃·전투 상태를 마지막에 한 번 더 떼어 낸다.
|
||||
// 이게 없으면 워프를 먼저 해도 「아레나 중심 반경 f_BaseChaseRange 안에 PC 가 있으면」 그대로 다시 붙는다.
|
||||
if (park)
|
||||
{
|
||||
ReleaseBossTarget(cfg);
|
||||
s_boss.Change_MobStatus(eMobStatus.PatrolWait);
|
||||
StopBossAgent();
|
||||
}
|
||||
|
||||
ResetCount++;
|
||||
Log("보스 리셋 #" + ResetCount + " — " + reason + " · " + healInfo);
|
||||
}
|
||||
|
|
@ -788,8 +1057,22 @@ namespace WL.Combat.Boss
|
|||
s_totalKillBase = 0; s_runActiveOverride = 0;
|
||||
RearmCount = LivingBossResetCount = DeadBossRegenCount = 0;
|
||||
LeashSuspendedTicks = GateSuspendedEvals = ZoneSpawnHoldTicks = ZoneSpawnHoldTargets = 0;
|
||||
// 813w5
|
||||
s_bossHoldUntil = -1f; s_probeTime = -1f;
|
||||
ParkCount = BossHoldTicks = OutsideRunLeashResets = GateMinBlocked = RunEndNotices = 0;
|
||||
LastGateHeldElapsed = -1f;
|
||||
LastRunOutcome = WL.Combat.Run.RunOutcome.None;
|
||||
}
|
||||
|
||||
/// <summary>프로브·QA 용 — 홀드를 즉시 해제한다(값은 건드리지 않는다).</summary>
|
||||
public static void ClearBossHold() { s_bossHoldUntil = -1f; }
|
||||
|
||||
/// <summary>프로브용 — 보스 없이 홀드 타이머만 건다(홀드 판정 검증).</summary>
|
||||
public static void ArmBossHoldForProbe() { ArmBossHold(Cfg); }
|
||||
|
||||
/// <summary>프로브용 — 런 밖 1틱을 직접 태운다(보스·설정은 호출측이 준비한다).</summary>
|
||||
public static void OutsideRunLeashTickForProbe() { var cfg = Cfg; if (cfg != null) OutsideRunLeashTick(cfg); }
|
||||
|
||||
/// <summary>
|
||||
/// 813p 「다음 런」 — 게이트를 다시 잠근다. `RunDirector.Restart()` 가 부르는 유일한 진입점이다.
|
||||
/// 🔴 813w3 §1-4(D-27): 813p 판은 `s_gateOpen` 만 내려서 ① 지난 런의 누적 처치로 게이트가 즉시 다시 열리고
|
||||
|
|
@ -820,7 +1103,8 @@ namespace WL.Combat.Boss
|
|||
BossSpawned = true;
|
||||
LivingBossResetCount++;
|
||||
// 🔴 리셋 자체가 실패해도 게이트 재무장은 끝내야 한다(다음 런이 통째로 망가지는 것보다 낫다).
|
||||
try { ResetBoss(cfg, Center, "다음 런 — 살아 있는 보스 복귀", true); }
|
||||
// 🔴 813w5 — ParkBoss = 워프 먼저 → 만피 → 타깃 해제 → nextRunBossHoldSeconds 홀드(D-39 원인 ①).
|
||||
try { ParkBoss("다음 런 — 살아 있는 보스 복귀"); }
|
||||
catch (System.Exception ex) { Log("살아 있는 보스 리셋 실패 — " + ex.Message); }
|
||||
}
|
||||
else
|
||||
|
|
|
|||
|
|
@ -78,6 +78,17 @@ namespace WL.Combat.Boss
|
|||
/// <summary>테스트용: 캐시를 비워 다음 접근 때 다시 Resources.Load 한다(에셋 제거/복구 실험).</summary>
|
||||
public static void ClearCache() { s_cached = null; s_lookupDone = false; }
|
||||
|
||||
/// <summary>
|
||||
/// 813w5 프로브용 — 임시 SO 를 잠시 Instance 자리에 끼운다(C8 진리표 · **실에셋은 읽기만** 한다).
|
||||
/// null 을 넣으면 캐시가 비워져 다음 접근에서 실에셋을 다시 로드한다(= ClearCache).
|
||||
/// 🔴 런타임 게임 경로에서는 절대 부르지 않는다.
|
||||
/// </summary>
|
||||
public static void SetInstanceForProbe(WLBossArenaSettings so)
|
||||
{
|
||||
s_cached = so;
|
||||
s_lookupDone = so != null;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────── 전체 스위치
|
||||
[Header("전체 스위치")]
|
||||
[Tooltip("끄면 보스 등장 게이트·아레나 경계·처치 후 정리가 전부 비활성 — 813e 의 「맵 로드 즉시 상시 배치」 100%. 롤백 1순위.")]
|
||||
|
|
@ -226,5 +237,43 @@ namespace WL.Combat.Boss
|
|||
+ "끄면 813d 그대로 = 런 밖에서도 리쉬 리셋이 계속 쌓여 보스 HP 가 되돌아온다(D-27 실측 68 → 190). "
|
||||
+ "런 축(WLRunSettings)이 꺼져 있거나 런이 한 번도 시작되지 않았으면 이 스위치와 무관하게 813d 그대로 돈다.")]
|
||||
public bool suspendOutsideRun = true;
|
||||
|
||||
// ─────────────────────────────────────────── 813w5 §1-2 다음 런 앞판 보스 정리(D-39)
|
||||
[Header("다음 런 앞판 보스 정리 (813w5 · D-39 보스가 존0 까지 추격)")]
|
||||
[Tooltip("🔴 RunEnded(결과창) 순간에도 살아 있는 보스를 아레나 중심으로 되돌린다(워프·만피·타깃 해제·홀드). "
|
||||
+ "끄면 813w3 그대로 = [다음 런]을 누른 순간에만 1회 되돌린다 → 결과창 동안 보스가 존까지 걸어 나간다(D-39).")]
|
||||
public bool parkBossOnRunEnd = true;
|
||||
|
||||
[Tooltip("🔴 보스를 되돌린 뒤 이 시간(초 · unscaled) 동안 이동·타깃을 금지한다(순찰 대기 고정 + NavMesh 정지). "
|
||||
+ "0 이면 홀드 없음 = 813w3 그대로. 원본 StopMove_Imm 은 protected 라 같은 본체를 공개 Get_Agent() 로 재현한다.")]
|
||||
public float nextRunBossHoldSeconds = 30f;
|
||||
|
||||
[Tooltip("홀드 중 보스가 아레나 중심에서 이 거리(m · XZ)를 넘으면 중심으로 다시 워프한다. 0 이하면 매 틱 워프.")]
|
||||
public float nextRunBossHoldRewarpMargin = 1.5f;
|
||||
|
||||
[Tooltip("🔴 「다음 런」 리셋에서 만피(On_Regen)보다 워프를 먼저 한다. "
|
||||
+ "원본 MobActor.On_Regen 은 그 안에서 Check_Battle() → Change_MobStatus(Battle) → FindNextTarget(10000f) 로 "
|
||||
+ "사실상 무한 반경 재타깃을 하므로, 워프가 나중이면 재아그로가 「런이 끝난 자리(= PC 옆)」에서 일어난다(D-39 원인 ①). "
|
||||
+ "끄면 813w3 순서 그대로(워프가 나중).")]
|
||||
public bool nextRunWarpBeforeHeal = true;
|
||||
|
||||
[Tooltip("보스를 되돌릴 때 PC 쪽 락타깃(813w Actor.m_IsLockTarget)도 풀고, PC 가 그 보스를 타깃 중이면 해제한다. "
|
||||
+ "끄면 보스 쪽 타깃만 해제한다(813w3 그대로).")]
|
||||
public bool nextRunClearsPlayerTarget = true;
|
||||
|
||||
[Tooltip("🔴 런 밖(결과창·다음 런 대기)에서도 「거리 판정만」 유지한다 — 아레나를 벗어나면 회복 없이 중심으로 되돌린다. "
|
||||
+ "끄면 813w3 그대로 = 런 밖에서는 리쉬가 아예 돌지 않아 보스가 사냥터까지 걸어 나간다(D-39). "
|
||||
+ "회복은 하지 않으므로 D-27 ③(런 밖 회복으로 HP 가 되돌아옴)은 재발하지 않는다.")]
|
||||
public bool leashOutsideRunDistanceOnly = true;
|
||||
|
||||
// ─────────────────────────────────────────── 813w5 §1-2 게이트 조기 개방 방지(D-39)
|
||||
[Header("게이트 조기 개방 방지 (813w5 · D-39 t≈19.6 개방)")]
|
||||
[Tooltip("🔴 런 시작 뒤 이 시간(초) 전에는 보스 게이트를 열지 않는다(보스 교전·이미 등장 경로). "
|
||||
+ "0 이면 무제한 = 813p/813w3 그대로. 런 축이 꺼졌거나 런이 한 번도 시작되지 않았으면 무동작.")]
|
||||
public float gateMinSeconds = 60f;
|
||||
|
||||
[Tooltip("위 시간 전이라도 「진짜 존 전멸」(RunGateReason.ZonesCleared)이면 게이트를 연다. "
|
||||
+ "끄면 존을 다 밀어도 gateMinSeconds 전에는 안 열린다.")]
|
||||
public bool gateMinAllowsZonesCleared = true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -255,6 +255,7 @@ namespace WL.Combat.Run
|
|||
public static void NoteBossSpawned(int spawnerId, Actor boss)
|
||||
{
|
||||
if (!IsRunning || s_bossStarted) return;
|
||||
if (BossArena.GateHeldByMinSeconds(RunGateReason.AlreadySpawned)) return; // #813w5 D-39 — 런 초반 조기 보스 개시 차단(다음 틱에 재시도)
|
||||
s_boss = boss;
|
||||
s_bossStarted = true;
|
||||
s_bossStartSec = Elapsed;
|
||||
|
|
@ -373,6 +374,7 @@ namespace WL.Combat.Run
|
|||
public static void OpenBossGate(RunGateReason reason)
|
||||
{
|
||||
if (!IsRunning || s_gateOpened) return;
|
||||
if (BossArena.GateHeldByMinSeconds(reason)) return; // #813w5 D-39 — gateMinSeconds 전 조기 개방 차단
|
||||
var c = Cfg;
|
||||
s_gateOpened = true;
|
||||
s_gateReason = reason;
|
||||
|
|
|
|||
|
|
@ -58,3 +58,11 @@ MonoBehaviour:
|
|||
restartResetsLivingBoss: 1
|
||||
restartRegensDeadBoss: 1
|
||||
suspendOutsideRun: 1
|
||||
parkBossOnRunEnd: 1
|
||||
nextRunBossHoldSeconds: 30
|
||||
nextRunBossHoldRewarpMargin: 1.5
|
||||
nextRunWarpBeforeHeal: 1
|
||||
nextRunClearsPlayerTarget: 1
|
||||
leashOutsideRunDistanceOnly: 1
|
||||
gateMinSeconds: 60
|
||||
gateMinAllowsZonesCleared: 1
|
||||
|
|
|
|||
Loading…
Reference in New Issue