[WL-813w7] 다음 런 존 재무장 · 결과창 중 PC 정지 (#813)
This commit is contained in:
parent
562732edd7
commit
018c43a95f
|
|
@ -0,0 +1,95 @@
|
||||||
|
# WL813w7 Probe 2026-09-09 20:49:10 (edit mode · Play 0 · 로그인 0)
|
||||||
|
playMode=False
|
||||||
|
|
||||||
|
── ⓪ 준비
|
||||||
|
[PASS] WLBossArenaSettings 로드 · Active=True
|
||||||
|
[PASS] WLRunGuideSettings 로드 · Enabled=True
|
||||||
|
[PASS] WLRunSettings 로드 · Enabled=True
|
||||||
|
[PASS] RunGuide.Subscribed(RunEvents)
|
||||||
|
[PASS] HoldSpawn(FieldBossData) → 게이트 무장(armed=True open=False)
|
||||||
|
|
||||||
|
── ① 에셋 (813w7 신규 12필드 · 값 전부 SO = C45)
|
||||||
|
RunGuide: holdPcOnRunEnd=True runEndHoldMaxSeconds=120 runEndHoldClearsTarget=True runEndHoldStopsAgent=True
|
||||||
|
[PASS] holdPcOnRunEnd=1 (발주서 §1-2 ① 기본값)
|
||||||
|
[PASS] runEndHoldMaxSeconds>0 (결과창을 닫고 방치해도 스스로 풀린다)
|
||||||
|
[PASS] runEndHoldClearsTarget=1 (자동 스킬 루프 차단 · PCActor.cs:93 isTarget 조건)
|
||||||
|
[PASS] runEndHoldStopsAgent=1 (잔여 경로 제거)
|
||||||
|
BossArena: holdZoneSpawnOnlyAfterGate=True holdZoneSpawnSkipsRunZones=True rearmZoneSpawnsEnabled=True rearmZoneEmptySeconds=8 rearmZoneCheckSeconds=1 rearmZoneRegenMax=15 rearmZoneRunZonesOnly=True rearmZoneSpawnsOnRestart=True
|
||||||
|
[PASS] holdZoneSpawnOnlyAfterGate=1 (차단을 보스전으로 좁힌다)
|
||||||
|
[PASS] holdZoneSpawnSkipsRunZones=1 (런 사냥 존은 대상 제외)
|
||||||
|
[PASS] rearmZoneSpawnsEnabled=1
|
||||||
|
[PASS] rearmZoneSpawnsOnRestart=1 ([다음 런] 즉시 1회)
|
||||||
|
[PASS] rearmZoneRegenMax>0 (테이블 실패 시 폴백·상한)
|
||||||
|
[PASS] 🔴 rearmZoneEmptySeconds(8s) > 원본 리듬 5s — 멀쩡한 스포너와 겹치지 않는다(이중 스폰 방지)
|
||||||
|
[PASS] 감시 주기(1s) < 임계(8s)
|
||||||
|
[PASS] 813w3 holdZoneSpawnNearArena=1 유지(차단 경로 자체는 그대로 · 조건만 좁혔다)
|
||||||
|
|
||||||
|
── ② 🔴 D-45 원인 산식 (Q10: R1n 0/18 · R2n 1/18 PC (112.93,0.06,−16.60) 고정 무피해 · R3n 14/18 정지)
|
||||||
|
아레나 중심 (139.00, 0.80, -20.00) · 차단 반경 = arenaRadius 20 + zoneSpawnHoldMargin 10 = 30 m
|
||||||
|
존0(id 813001) 앵커 (115.00, 0.39, -22.00) → 중심 24.1 m · 차단 사정권 안 🔴
|
||||||
|
존1(id 813002) 앵커 (147.00, 2.59, 30.00) → 중심 50.6 m · 차단 사정권 밖
|
||||||
|
[PASS] 🔴 존0 이 차단 사정권 안(24.1 m ≤ 30 m) = D-45 의 직접 원인
|
||||||
|
[PASS] Q10 R2n PC 좌표 (112.93, 0.06, -16.60) ↔ 존0 앵커 5.8 m ≤ zoneArriveRadius 6 = **도착(Hold)했는데 때릴 몹이 없었다**
|
||||||
|
차단 방식 = MobControlMgr.StopAllCoroutines() → PhaseAllKill 의 `killmobcount = 0`(MobControlMgr.cs:218)이
|
||||||
|
함께 끊겨 카운터가 n_MaxMonsterCount 에 굳는다 → `==` 판정(:214)이 두 번 다시 참이 안 된다 = 영구 소진.
|
||||||
|
(MyCoroutine.Set_Coroutine = StartCoroutine(MyCoroutine.cs) 이므로 StopAllCoroutines 에 걸린다 · 실측)
|
||||||
|
|
||||||
|
── ③ 수리 ① 결과창 중 PC 정지 (D-45 원인 = 결과창 100 s 사이 z0 처치 26 → 47)
|
||||||
|
[PASS] 가짜 RunEnded(Win) → 정지 시작(RunEndHeld=True RunEndHolds=1)
|
||||||
|
[PASS] 가이드는 꺼진다(State=Off Dest=None) — 813w6 계약 유지
|
||||||
|
[PASS] 🔴 정지 틱 → JoystickStatus=1(1 = 원본 자동 교전 접근 루프 차단 · PCActor.cs:139) Steering=True
|
||||||
|
[PASS] 🔴 정지 틱 → 타깃 해제(isTarget=False RunEndTargetClears=1) = 자동 스킬 루프(PCActor.cs:93)도 돌지 않는다
|
||||||
|
[PASS] 정지가 이어진다(틱 3 · 상한 전)
|
||||||
|
[PASS] 🔴 가짜 RunStarted("restart") → 정지 해제(RunEndHeld=False 해제 1 사유 '런 시작')
|
||||||
|
[PASS] 🔴 조종 원복 JoystickStatus=0(0 = 원본 그대로) Steering=False
|
||||||
|
[PASS] 813w4 재유도는 그대로 산다(RestartGuides=1) — 정지 해제가 유도보다 앞
|
||||||
|
[PASS] 🔴 상한 만료 자동 해제(199 s 유지 → 201 s 해제 · 사유 '상한 만료')
|
||||||
|
[PASS] 만료 해제에서도 조종 원복(JoystickStatus=0)
|
||||||
|
[PASS] JoystickStatus=2(스킬 이동 잠금)는 건드리지 않는다(=2 Steering=False)
|
||||||
|
|
||||||
|
── ④ 수리 ②-a 리젠 차단 판정(ShouldHoldZoneSpawn · 순수 함수)
|
||||||
|
[PASS] IsRunZoneSpawner(813001)=true (런 사냥 존)
|
||||||
|
[PASS] IsRunZoneSpawner(813005)=false (런 밖 존)
|
||||||
|
[PASS] 🔴 게이트 **닫힘** + 런 밖 존 → 차단 안 함(= 813w6 이 만든 「다음 런 내내 차단」이 사라진다)
|
||||||
|
[PASS] 🔴 게이트 열림 + **런 사냥 존** → 차단 안 함(D-45 재발 차단)
|
||||||
|
[PASS] 게이트 열림 + 런 밖 존 → 차단(813w3 잡몹 유입 차단 의도 유지)
|
||||||
|
[PASS] C8: 두 스위치 0 → 게이트 전 + 런 존도 차단 = 813w3 동작 그대로
|
||||||
|
[PASS] C8: holdZoneSpawnNearArena=0 → 언제나 차단 0
|
||||||
|
|
||||||
|
── ⑤ 수리 ②-b 존 재무장(원본 리셋 API 없음 → MobActor.On_Regen = MobControlMgr.cs:221 과 같은 호출)
|
||||||
|
감시 대상 2개: 813001 / 813002
|
||||||
|
[PASS] 대상 = WLRunSettings.zoneSpawnerIds 전부(rearmZoneRunZonesOnly=1)
|
||||||
|
[PASS] 첫 대상 = 존0(813001)
|
||||||
|
[PASS] WaveSize(813001)=15 (테이블 n_MaxMonsterCount → 없으면 rearmZoneRegenMax 15 · 상한도 그 값)
|
||||||
|
[PASS] alive=1 → 빈 시각 미기록(-1)
|
||||||
|
[PASS] alive=0 → 빈 시각 100 기록
|
||||||
|
[PASS] 임계(8s) 전 → 트리거 없음(빈 시각 그대로 100)
|
||||||
|
[PASS] 🔴 임계 경과 → 재무장 트리거(빈 시각이 108.5 로 갱신 = RearmZone 호출)
|
||||||
|
(에디트 모드는 ActorInfo.Ins 가 없어 실제 On_Regen 은 0마리 — 「언제 트리거되는가」까지 증명 · §미확인)
|
||||||
|
[PASS] 런 밖(결과창)에서는 감시 정지(빈 시각 108.5 불변)
|
||||||
|
[PASS] RearmZoneSpawns() 진입점 동작(재등장 0마리 · 에디트 모드라 0 · 목록 2개 재구축)
|
||||||
|
|
||||||
|
── ⑥ 수리 ③ 런 밖 처치가 존 카운터에 안 실린다(Q10: 결과창 동안 z0 26 → 47)
|
||||||
|
[PASS] BossArena 누적은 계속 센다(1 → 22) = 813p 계약(리셋 금지) 유지
|
||||||
|
[PASS] 런 밖 게이트 평가는 건너뛴다(GateSuspendedEvals 0 → 21)
|
||||||
|
[PASS] 🔴 다음 런 시작 직후 존0 진행 = 0/18 — 런 밖 21처치가 한 건도 안 실렸다(StartRun 스냅샷 차감)
|
||||||
|
[PASS] 런 안 처치 1건은 정상 반영(1/18)
|
||||||
|
|
||||||
|
── ⑦ GC 0 (틱 경로 · 각 100회)
|
||||||
|
[PASS] 정지 틱 ×100 = 0 B
|
||||||
|
[PASS] 빈 존 감시 ×100 = 0 B
|
||||||
|
[PASS] 차단 판정 ×100 = 0 B (acc=False)
|
||||||
|
[PASS] 런 존 판정 ×100 = 0 B (acc=False)
|
||||||
|
|
||||||
|
── ⑧ C8 롤백 (신규 스위치 0 → 813w6 동작 그대로)
|
||||||
|
[PASS] holdPcOnRunEnd=0 → 정지 0(RunEndHeld=False RunEndHolds=0)
|
||||||
|
[PASS] 조종도 잡지 않는다(JoystickStatus=0) = 813w6 그대로
|
||||||
|
[PASS] 정지 중 스위치 off → 즉시 해제(사유 '스위치 off')
|
||||||
|
[PASS] rearmZoneSpawnsEnabled=0 → 감시 무동작(빈 시각 -1)
|
||||||
|
[PASS] RearmZoneSpawns() 도 0
|
||||||
|
[PASS] rearmZoneSpawnsOnRestart=0 → [다음 런] 즉시 재무장 0
|
||||||
|
[PASS] RunGuide RuntimeDisabled → Active=false
|
||||||
|
[PASS] Tick 이 정지를 풀고 원본으로 돌려준다(RunEndHeld=False)
|
||||||
|
[PASS] BossArena RuntimeDisabled → 감시 무동작
|
||||||
|
|
||||||
|
RESULT PASS
|
||||||
|
|
@ -0,0 +1,588 @@
|
||||||
|
// WL813w7_Probe.cs — #813w7(다음 런 존 재무장 · 결과창 중 PC 정지 D-45) 프로브
|
||||||
|
// 에디트 모드 전용 · Play 불필요 · 로그인 0 · 씬/에셋 무수정(임시 GameObject·임시 SO 는 즉시 파괴/회수)
|
||||||
|
//
|
||||||
|
// 상주 에디터:
|
||||||
|
// unity command run_script --project-path <wt> --file AgentScripts/WL813w7_Probe.cs --entry WL813w7_Probe.RunAll --timeout 300
|
||||||
|
// 결과: Console + AgentScripts/WL813w7_PROBE.txt (RESULT PASS / RESULT FAIL n)
|
||||||
|
//
|
||||||
|
// 무엇을 증명하나 (발주서 WL-813w7 §1 · 근거 = qa/W2-10_재채점_v8.md D-45)
|
||||||
|
// ① 에셋·SOT — 813w7 신규 12필드 값 실측 · 이중 스폰 방지 부등식 · 코드 상수 0
|
||||||
|
// ② 원인 산식 — 존0 앵커 ↔ 아레나 중심 24.1 m ≤ arenaRadius+zoneSpawnHoldMargin 30 m
|
||||||
|
// = 813w3 리젠 차단의 사정권 안이다(D-45 의 직접 원인 · 값으로 재현)
|
||||||
|
// ③ 수리 ① — 가짜 `RunEnded` → PC 조종 잡힘(JoystickStatus 1) · 타깃 해제 ·
|
||||||
|
// 가짜 `RunStarted("restart")` → 조종 원복(0) · 상한 만료 자동 해제
|
||||||
|
// ④ 수리 ②-a — 리젠 차단 판정(ShouldHoldZoneSpawn) 진리표: 게이트 전 0 · 런 존 0 · 그 밖 1
|
||||||
|
// ⑤ 수리 ②-b — 빈 존 감시 타이밍(8 s) · Restart 즉시 재무장 · 1웨이브 마리 수(WaveSize)
|
||||||
|
// ⑥ 수리 ③ — 런 밖 처치는 다음 런 존 진행에 안 센다(StartRun 스냅샷 차감)
|
||||||
|
// ⑦ GC 0 — 정지 틱 ×100 · 빈 존 감시 ×100 · 차단 판정 ×100 · 런 존 판정 ×100
|
||||||
|
// ⑧ C8 롤백 — 신규 스위치 0 → 813w6 동작 그대로 · RuntimeDisabled → 무동작
|
||||||
|
//
|
||||||
|
// 에디트 모드 한계(보고서 「미확인」)
|
||||||
|
// · `ActorInfo.Ins` 도 `table_monsterappear.Ins` 도 없다(플레이 모드 전용) → 실제 `On_Regen()` 재무장은
|
||||||
|
// 못 돌린다. 「언제 재무장을 트리거하는가」와 「1웨이브 마리 수 산정」까지를 증명한다.
|
||||||
|
// · `Time.unscaledTime` 이 스크립트 실행 중 멈춰 있다 → 시간 판정은 전부 인자로 먹인다.
|
||||||
|
// · PC 에 살아 있는 타깃(m_Target)을 붙일 수 없다(Set_Outline 이 렌더러를 만진다) → 타깃 해제는
|
||||||
|
// 「isTarget 플래그가 내려가는가」로 증명한다.
|
||||||
|
|
||||||
|
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 WL813w7_Probe
|
||||||
|
{
|
||||||
|
const string OutCommit = "AgentScripts/WL813w7_PROBE.txt";
|
||||||
|
|
||||||
|
static StringBuilder sb;
|
||||||
|
static int s_fail;
|
||||||
|
|
||||||
|
static GameObject s_pcGo, s_fbdGo;
|
||||||
|
static PCActor s_pc, s_prevMyPc;
|
||||||
|
static FieldBossData s_fbd;
|
||||||
|
|
||||||
|
public static object RunAll()
|
||||||
|
{
|
||||||
|
sb = new StringBuilder();
|
||||||
|
s_fail = 0;
|
||||||
|
sb.AppendLine("# WL813w7 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_RunEndHold();
|
||||||
|
Sec4_HoldDecision();
|
||||||
|
Sec5_ZoneRearm();
|
||||||
|
Sec6_OutsideRunKills();
|
||||||
|
Sec7_GC();
|
||||||
|
Sec8_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 WLBossArenaSettings TmpB()
|
||||||
|
{
|
||||||
|
var t = UnityEngine.Object.Instantiate(WLBossArenaSettings.Instance);
|
||||||
|
t.hideFlags = HideFlags.HideAndDontSave;
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
static WLRunGuideSettings TmpG()
|
||||||
|
{
|
||||||
|
var t = UnityEngine.Object.Instantiate(WLRunGuideSettings.Instance);
|
||||||
|
t.hideFlags = HideFlags.HideAndDontSave;
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
static void UseTmpB(WLBossArenaSettings t) { WLBossArenaSettings.SetInstanceForProbe(t); }
|
||||||
|
static void UseRealB() { WLBossArenaSettings.SetInstanceForProbe(null); }
|
||||||
|
static void UseTmpG(WLRunGuideSettings t) { WLRunGuideSettings.SetInstanceForProbe(t); }
|
||||||
|
static void UseRealG() { WLRunGuideSettings.SetInstanceForProbe(null); }
|
||||||
|
|
||||||
|
static float DistXZ(Vector3 a, Vector3 b)
|
||||||
|
{
|
||||||
|
float dx = a.x - b.x, dz = a.z - b.z;
|
||||||
|
return Mathf.Sqrt(dx * dx + dz * dz);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Setup()
|
||||||
|
{
|
||||||
|
s_prevMyPc = MyValue.MyPC;
|
||||||
|
|
||||||
|
s_pcGo = new GameObject("__WL813w7_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;
|
||||||
|
WLRunGuideSettings.RuntimeDisabled = false;
|
||||||
|
BossArena.ResetAll();
|
||||||
|
BossArena.EnsureSubscribed();
|
||||||
|
RunDirector.ResetAll();
|
||||||
|
RunGuide.EnsureSubscribed();
|
||||||
|
RunGuide.ResetDiagnostics();
|
||||||
|
|
||||||
|
// 게이트 무장 — 원본 훅과 같은 진입점(HoldSpawn)으로 Arm 한다(s_zones 에 존 id 가 들어간다).
|
||||||
|
var cfg = WLBossArenaSettings.Instance;
|
||||||
|
s_fbdGo = new GameObject("__WL813w7_FBD");
|
||||||
|
s_fbd = s_fbdGo.AddComponent<FieldBossData>();
|
||||||
|
s_fbd.n_SpawnerId = cfg != null ? cfg.bossSpawnerId : 813010;
|
||||||
|
s_fbd.n_MonsterID = cfg != null ? cfg.bossMonsterId : 10006;
|
||||||
|
bool held = BossArena.HoldSpawn(s_fbd, NoopResume);
|
||||||
|
|
||||||
|
L("");
|
||||||
|
L("── ⓪ 준비");
|
||||||
|
L(" " + P(WLBossArenaSettings.Instance != null) + " WLBossArenaSettings 로드 · Active=" + WLBossArenaSettings.Active);
|
||||||
|
L(" " + P(WLRunGuideSettings.Instance != null) + " WLRunGuideSettings 로드 · Enabled=" + WLRunGuideSettings.Enabled);
|
||||||
|
L(" " + P(WLRunSettings.Instance != null) + " WLRunSettings 로드 · Enabled=" + WLRunSettings.Enabled);
|
||||||
|
L(" " + P(RunGuide.Subscribed) + " RunGuide.Subscribed(RunEvents)");
|
||||||
|
L(" " + P(held && BossArena.GateArmed && !BossArena.GateOpen) +
|
||||||
|
" HoldSpawn(FieldBossData) → 게이트 무장(armed=" + BossArena.GateArmed + " open=" + BossArena.GateOpen + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void NoopResume() { }
|
||||||
|
|
||||||
|
static void Teardown()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
UseRealB(); UseRealG();
|
||||||
|
BossArena.SetProbeTime(-1f);
|
||||||
|
BossArena.SetRunActiveForProbe(0);
|
||||||
|
BossArena.SetBossForProbe(null);
|
||||||
|
BossArena.ResetAll();
|
||||||
|
RunGuide.EndRunEndHold("teardown");
|
||||||
|
RunGuide.ResetDiagnostics();
|
||||||
|
RunDirector.ResetAll();
|
||||||
|
WLBossArenaSettings.RuntimeDisabled = false;
|
||||||
|
WLRunGuideSettings.RuntimeDisabled = false;
|
||||||
|
MyValue.MyPC = s_prevMyPc;
|
||||||
|
if (s_pcGo != null) UnityEngine.Object.DestroyImmediate(s_pcGo);
|
||||||
|
if (s_fbdGo != null) UnityEngine.Object.DestroyImmediate(s_fbdGo);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
|
// ① 에셋 · SOT (C45 — 값은 전부 .asset)
|
||||||
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
|
static void Sec1_Asset()
|
||||||
|
{
|
||||||
|
L("");
|
||||||
|
L("── ① 에셋 (813w7 신규 12필드 · 값 전부 SO = C45)");
|
||||||
|
|
||||||
|
var g = WLRunGuideSettings.Instance;
|
||||||
|
var b = WLBossArenaSettings.Instance;
|
||||||
|
if (g == null || b == null) { L(" [FAIL] 에셋 없음"); s_fail++; return; }
|
||||||
|
|
||||||
|
L(" RunGuide: holdPcOnRunEnd=" + g.holdPcOnRunEnd +
|
||||||
|
" runEndHoldMaxSeconds=" + g.runEndHoldMaxSeconds +
|
||||||
|
" runEndHoldClearsTarget=" + g.runEndHoldClearsTarget +
|
||||||
|
" runEndHoldStopsAgent=" + g.runEndHoldStopsAgent);
|
||||||
|
L(" " + P(g.holdPcOnRunEnd) + " holdPcOnRunEnd=1 (발주서 §1-2 ① 기본값)");
|
||||||
|
L(" " + P(g.runEndHoldMaxSeconds > 0f) + " runEndHoldMaxSeconds>0 (결과창을 닫고 방치해도 스스로 풀린다)");
|
||||||
|
L(" " + P(g.runEndHoldClearsTarget) + " runEndHoldClearsTarget=1 (자동 스킬 루프 차단 · PCActor.cs:93 isTarget 조건)");
|
||||||
|
L(" " + P(g.runEndHoldStopsAgent) + " runEndHoldStopsAgent=1 (잔여 경로 제거)");
|
||||||
|
|
||||||
|
L(" BossArena: holdZoneSpawnOnlyAfterGate=" + b.holdZoneSpawnOnlyAfterGate +
|
||||||
|
" holdZoneSpawnSkipsRunZones=" + b.holdZoneSpawnSkipsRunZones +
|
||||||
|
" rearmZoneSpawnsEnabled=" + b.rearmZoneSpawnsEnabled +
|
||||||
|
" rearmZoneEmptySeconds=" + b.rearmZoneEmptySeconds +
|
||||||
|
" rearmZoneCheckSeconds=" + b.rearmZoneCheckSeconds +
|
||||||
|
" rearmZoneRegenMax=" + b.rearmZoneRegenMax +
|
||||||
|
" rearmZoneRunZonesOnly=" + b.rearmZoneRunZonesOnly +
|
||||||
|
" rearmZoneSpawnsOnRestart=" + b.rearmZoneSpawnsOnRestart);
|
||||||
|
L(" " + P(b.holdZoneSpawnOnlyAfterGate) + " holdZoneSpawnOnlyAfterGate=1 (차단을 보스전으로 좁힌다)");
|
||||||
|
L(" " + P(b.holdZoneSpawnSkipsRunZones) + " holdZoneSpawnSkipsRunZones=1 (런 사냥 존은 대상 제외)");
|
||||||
|
L(" " + P(b.rearmZoneSpawnsEnabled) + " rearmZoneSpawnsEnabled=1");
|
||||||
|
L(" " + P(b.rearmZoneSpawnsOnRestart) + " rearmZoneSpawnsOnRestart=1 ([다음 런] 즉시 1회)");
|
||||||
|
L(" " + P(b.rearmZoneRegenMax > 0) + " rearmZoneRegenMax>0 (테이블 실패 시 폴백·상한)");
|
||||||
|
|
||||||
|
// 🔴 이중 스폰 방지 부등식 — 원본 리듬 = 시체 3.0 s(Actor.cs:631) + f_SpawnerDelay 2.0 s(813e2 표 A) = 5.0 s
|
||||||
|
const float kOriginalRegenRhythm = 5.0f; // 게임 수치가 아니라 813e2 실측 리듬(검증 기준값)
|
||||||
|
L(" " + P(b.rearmZoneEmptySeconds > kOriginalRegenRhythm) +
|
||||||
|
" 🔴 rearmZoneEmptySeconds(" + b.rearmZoneEmptySeconds + "s) > 원본 리듬 " + kOriginalRegenRhythm +
|
||||||
|
"s — 멀쩡한 스포너와 겹치지 않는다(이중 스폰 방지)");
|
||||||
|
L(" " + P(b.rearmZoneCheckSeconds > 0f && b.rearmZoneCheckSeconds < b.rearmZoneEmptySeconds) +
|
||||||
|
" 감시 주기(" + b.rearmZoneCheckSeconds + "s) < 임계(" + b.rearmZoneEmptySeconds + "s)");
|
||||||
|
L(" " + P(b.holdZoneSpawnNearArena) + " 813w3 holdZoneSpawnNearArena=1 유지(차단 경로 자체는 그대로 · 조건만 좁혔다)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
|
// ② 원인 산식 — 존0 이 리젠 차단 사정권 안이다
|
||||||
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
|
static void Sec2_Cause()
|
||||||
|
{
|
||||||
|
L("");
|
||||||
|
L("── ② 🔴 D-45 원인 산식 (Q10: R1n 0/18 · R2n 1/18 PC (112.93,0.06,−16.60) 고정 무피해 · R3n 14/18 정지)");
|
||||||
|
|
||||||
|
var b = WLBossArenaSettings.Instance;
|
||||||
|
var g = WLRunGuideSettings.Instance;
|
||||||
|
var rs = WLRunSettings.Instance;
|
||||||
|
if (b == null || g == null || rs == null) { L(" [FAIL] 에셋 없음"); s_fail++; return; }
|
||||||
|
|
||||||
|
Vector3 center = b.arenaCenter;
|
||||||
|
float lim = b.arenaRadius + b.zoneSpawnHoldMargin;
|
||||||
|
L(" 아레나 중심 " + center.ToString("F2") + " · 차단 반경 = arenaRadius " + b.arenaRadius +
|
||||||
|
" + zoneSpawnHoldMargin " + b.zoneSpawnHoldMargin + " = " + lim.ToString("F0") + " m");
|
||||||
|
|
||||||
|
var fb = g.zoneAnchorFallbacks;
|
||||||
|
for (int i = 0; i < rs.zoneSpawnerIds.Length; i++)
|
||||||
|
{
|
||||||
|
if (fb == null || i >= fb.Length) continue;
|
||||||
|
float d = DistXZ(fb[i], center);
|
||||||
|
L(" 존" + i + "(id " + rs.zoneSpawnerIds[i] + ") 앵커 " + fb[i].ToString("F2") +
|
||||||
|
" → 중심 " + d.ToString("F1") + " m · 차단 사정권 " + (d <= lim ? "안 🔴" : "밖"));
|
||||||
|
}
|
||||||
|
|
||||||
|
float d0 = fb != null && fb.Length > 0 ? DistXZ(fb[0], center) : -1f;
|
||||||
|
L(" " + P(d0 >= 0f && d0 <= lim) +
|
||||||
|
" 🔴 존0 이 차단 사정권 안(" + d0.ToString("F1") + " m ≤ " + lim.ToString("F0") + " m) = D-45 의 직접 원인");
|
||||||
|
|
||||||
|
// PC 고정 좌표(Q10 R2n 실측)가 존0 앵커에 도착한 자리인가 = 「PC 는 존에 있는데 몹이 없다」
|
||||||
|
var pcSeen = new Vector3(112.93f, 0.06f, -16.60f);
|
||||||
|
float dPc = fb != null && fb.Length > 0 ? DistXZ(pcSeen, fb[0]) : -1f;
|
||||||
|
L(" " + P(dPc >= 0f && dPc <= g.zoneArriveRadius) +
|
||||||
|
" Q10 R2n PC 좌표 " + pcSeen.ToString("F2") + " ↔ 존0 앵커 " + dPc.ToString("F1") +
|
||||||
|
" m ≤ zoneArriveRadius " + g.zoneArriveRadius + " = **도착(Hold)했는데 때릴 몹이 없었다**");
|
||||||
|
|
||||||
|
L(" 차단 방식 = MobControlMgr.StopAllCoroutines() → PhaseAllKill 의 `killmobcount = 0`(MobControlMgr.cs:218)이");
|
||||||
|
L(" 함께 끊겨 카운터가 n_MaxMonsterCount 에 굳는다 → `==` 판정(:214)이 두 번 다시 참이 안 된다 = 영구 소진.");
|
||||||
|
L(" (MyCoroutine.Set_Coroutine = StartCoroutine(MyCoroutine.cs) 이므로 StopAllCoroutines 에 걸린다 · 실측)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
|
// ③ 수리 ① — 결과창 중 PC 정지
|
||||||
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
|
static void Sec3_RunEndHold()
|
||||||
|
{
|
||||||
|
L("");
|
||||||
|
L("── ③ 수리 ① 결과창 중 PC 정지 (D-45 원인 = 결과창 100 s 사이 z0 처치 26 → 47)");
|
||||||
|
|
||||||
|
var g = WLRunGuideSettings.Instance;
|
||||||
|
RunGuide.ResetDiagnostics();
|
||||||
|
s_pc.Set_JoystickStatus(3);
|
||||||
|
s_pc.isTarget = false;
|
||||||
|
|
||||||
|
int holds0 = RunGuide.RunEndHolds;
|
||||||
|
RunGuide.ProbeRunEnded(RunOutcome.Win);
|
||||||
|
RunGuide.SetRunEndHoldUntilForProbe(-1f); // 에디트 모드는 Time 이 안 흘러 상한 판정을 따로 검증한다(아래 ⓑ)
|
||||||
|
L(" " + P(RunGuide.RunEndHeld && RunGuide.RunEndHolds == holds0 + 1) +
|
||||||
|
" 가짜 RunEnded(Win) → 정지 시작(RunEndHeld=" + RunGuide.RunEndHeld + " RunEndHolds=" + RunGuide.RunEndHolds + ")");
|
||||||
|
L(" " + P(RunGuide.State == RunGuideState.Off && RunGuide.DestKind == RunGuideDest.None) +
|
||||||
|
" 가이드는 꺼진다(State=" + RunGuide.State + " Dest=" + RunGuide.DestKind + ") — 813w6 계약 유지");
|
||||||
|
|
||||||
|
// 정지 1틱 — 조종을 잡고 타깃을 뗀다
|
||||||
|
s_pc.isTarget = true;
|
||||||
|
int clears0 = RunGuide.RunEndTargetClears;
|
||||||
|
RunGuide.RunEndHoldTick(g, s_pc, 100f);
|
||||||
|
L(" " + P(s_pc.JoystickStatus == 1 && RunGuide.Steering) +
|
||||||
|
" 🔴 정지 틱 → JoystickStatus=" + s_pc.JoystickStatus + "(1 = 원본 자동 교전 접근 루프 차단 · PCActor.cs:139) Steering=" + RunGuide.Steering);
|
||||||
|
L(" " + P(!s_pc.isTarget && RunGuide.RunEndTargetClears == clears0 + 1) +
|
||||||
|
" 🔴 정지 틱 → 타깃 해제(isTarget=" + s_pc.isTarget + " RunEndTargetClears=" + RunGuide.RunEndTargetClears +
|
||||||
|
") = 자동 스킬 루프(PCActor.cs:93)도 돌지 않는다");
|
||||||
|
|
||||||
|
int ticks0 = RunGuide.RunEndHoldTicks;
|
||||||
|
RunGuide.RunEndHoldTick(g, s_pc, 101f);
|
||||||
|
RunGuide.RunEndHoldTick(g, s_pc, 102f);
|
||||||
|
L(" " + P(RunGuide.RunEndHoldTicks == ticks0 + 2 && RunGuide.RunEndHeld) +
|
||||||
|
" 정지가 이어진다(틱 " + RunGuide.RunEndHoldTicks + " · 상한 전)");
|
||||||
|
|
||||||
|
// 복귀 ⓐ — 다음 런 시작(RunDirector.Restart → StartRun("restart"))
|
||||||
|
int rel0 = RunGuide.RunEndHoldReleases;
|
||||||
|
RunGuide.ProbeRunStarted(2, "restart");
|
||||||
|
L(" " + P(!RunGuide.RunEndHeld && RunGuide.RunEndHoldReleases == rel0 + 1) +
|
||||||
|
" 🔴 가짜 RunStarted(\"restart\") → 정지 해제(RunEndHeld=" + RunGuide.RunEndHeld +
|
||||||
|
" 해제 " + RunGuide.RunEndHoldReleases + " 사유 '" + RunGuide.LastRunEndHoldReason + "')");
|
||||||
|
L(" " + P(s_pc.JoystickStatus == 0 && !RunGuide.Steering) +
|
||||||
|
" 🔴 조종 원복 JoystickStatus=" + s_pc.JoystickStatus + "(0 = 원본 그대로) Steering=" + RunGuide.Steering);
|
||||||
|
L(" " + P(RunGuide.RestartGuides >= 1) +
|
||||||
|
" 813w4 재유도는 그대로 산다(RestartGuides=" + RunGuide.RestartGuides + ") — 정지 해제가 유도보다 앞");
|
||||||
|
|
||||||
|
// 복귀 ⓑ — 상한 만료(결과창을 [닫기]로 닫고 런을 다시 시작하지 않는 경우)
|
||||||
|
RunGuide.StopGuide("프로브");
|
||||||
|
RunGuide.BeginRunEndHold("상한 시험");
|
||||||
|
RunGuide.SetRunEndHoldUntilForProbe(200f);
|
||||||
|
RunGuide.RunEndHoldTick(g, s_pc, 199f);
|
||||||
|
bool stillHeld = RunGuide.RunEndHeld;
|
||||||
|
RunGuide.RunEndHoldTick(g, s_pc, 201f);
|
||||||
|
L(" " + P(stillHeld && !RunGuide.RunEndHeld) +
|
||||||
|
" 🔴 상한 만료 자동 해제(199 s 유지 → 201 s 해제 · 사유 '" + RunGuide.LastRunEndHoldReason + "')");
|
||||||
|
L(" " + P(s_pc.JoystickStatus == 0) + " 만료 해제에서도 조종 원복(JoystickStatus=" + s_pc.JoystickStatus + ")");
|
||||||
|
|
||||||
|
// 이미 스킬 이동 잠금(2)이면 건드리지 않는다
|
||||||
|
RunGuide.ResetDiagnostics();
|
||||||
|
s_pc.Set_JoystickStatus(2);
|
||||||
|
RunGuide.BeginRunEndHold("잠금 시험");
|
||||||
|
RunGuide.SetRunEndHoldUntilForProbe(-1f);
|
||||||
|
RunGuide.RunEndHoldTick(g, s_pc, 300f);
|
||||||
|
L(" " + P(s_pc.JoystickStatus == 2 && !RunGuide.Steering) +
|
||||||
|
" JoystickStatus=2(스킬 이동 잠금)는 건드리지 않는다(=" + s_pc.JoystickStatus + " Steering=" + RunGuide.Steering + ")");
|
||||||
|
RunGuide.EndRunEndHold("프로브 정리");
|
||||||
|
s_pc.Set_JoystickStatus(3);
|
||||||
|
RunGuide.ResetDiagnostics();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
|
// ④ 수리 ②-a — 리젠 차단 판정 진리표
|
||||||
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
|
static void Sec4_HoldDecision()
|
||||||
|
{
|
||||||
|
L("");
|
||||||
|
L("── ④ 수리 ②-a 리젠 차단 판정(ShouldHoldZoneSpawn · 순수 함수)");
|
||||||
|
|
||||||
|
var rs = WLRunSettings.Instance;
|
||||||
|
int runZone = rs != null && rs.zoneSpawnerIds != null && rs.zoneSpawnerIds.Length > 0 ? rs.zoneSpawnerIds[0] : 813001;
|
||||||
|
int other = 813005;
|
||||||
|
|
||||||
|
L(" " + P(BossArena.IsRunZoneSpawner(runZone)) + " IsRunZoneSpawner(" + runZone + ")=true (런 사냥 존)");
|
||||||
|
L(" " + P(!BossArena.IsRunZoneSpawner(other)) + " IsRunZoneSpawner(" + other + ")=false (런 밖 존)");
|
||||||
|
|
||||||
|
var t = TmpB();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
UseTmpB(t);
|
||||||
|
t.holdZoneSpawnNearArena = true;
|
||||||
|
t.holdZoneSpawnOnlyAfterGate = true;
|
||||||
|
t.holdZoneSpawnSkipsRunZones = true;
|
||||||
|
|
||||||
|
L(" " + P(!BossArena.ShouldHoldZoneSpawn(t, other, false)) +
|
||||||
|
" 🔴 게이트 **닫힘** + 런 밖 존 → 차단 안 함(= 813w6 이 만든 「다음 런 내내 차단」이 사라진다)");
|
||||||
|
L(" " + P(!BossArena.ShouldHoldZoneSpawn(t, runZone, true)) +
|
||||||
|
" 🔴 게이트 열림 + **런 사냥 존** → 차단 안 함(D-45 재발 차단)");
|
||||||
|
L(" " + P(BossArena.ShouldHoldZoneSpawn(t, other, true)) +
|
||||||
|
" 게이트 열림 + 런 밖 존 → 차단(813w3 잡몹 유입 차단 의도 유지)");
|
||||||
|
|
||||||
|
// C8 — 두 스위치를 끄면 813w3 그대로(보스 생존 = 상시 차단)
|
||||||
|
t.holdZoneSpawnOnlyAfterGate = false;
|
||||||
|
t.holdZoneSpawnSkipsRunZones = false;
|
||||||
|
L(" " + P(BossArena.ShouldHoldZoneSpawn(t, runZone, false)) +
|
||||||
|
" C8: 두 스위치 0 → 게이트 전 + 런 존도 차단 = 813w3 동작 그대로");
|
||||||
|
|
||||||
|
t.holdZoneSpawnNearArena = false;
|
||||||
|
L(" " + P(!BossArena.ShouldHoldZoneSpawn(t, other, true)) + " C8: holdZoneSpawnNearArena=0 → 언제나 차단 0");
|
||||||
|
}
|
||||||
|
finally { UseRealB(); UnityEngine.Object.DestroyImmediate(t); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
|
// ⑤ 수리 ②-b — 빈 존 감시 · 재무장
|
||||||
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
|
static void Sec5_ZoneRearm()
|
||||||
|
{
|
||||||
|
L("");
|
||||||
|
L("── ⑤ 수리 ②-b 존 재무장(원본 리셋 API 없음 → MobActor.On_Regen = MobControlMgr.cs:221 과 같은 호출)");
|
||||||
|
|
||||||
|
var b = WLBossArenaSettings.Instance;
|
||||||
|
var rs = WLRunSettings.Instance;
|
||||||
|
int z0 = rs != null && rs.zoneSpawnerIds != null && rs.zoneSpawnerIds.Length > 0 ? rs.zoneSpawnerIds[0] : 813001;
|
||||||
|
|
||||||
|
// 감시 목록 — 런 존만
|
||||||
|
BossArena.RebuildRearmZonesForProbe();
|
||||||
|
L(" 감시 대상 " + BossArena.RearmZoneCount + "개: " +
|
||||||
|
(BossArena.RearmZoneCount > 0 ? BossArena.RearmZoneIdAt(0).ToString() : "-") +
|
||||||
|
(BossArena.RearmZoneCount > 1 ? " / " + BossArena.RearmZoneIdAt(1) : ""));
|
||||||
|
L(" " + P(rs != null && BossArena.RearmZoneCount == rs.zoneSpawnerIds.Length) +
|
||||||
|
" 대상 = WLRunSettings.zoneSpawnerIds 전부(rearmZoneRunZonesOnly=1)");
|
||||||
|
L(" " + P(BossArena.RearmZoneCount > 0 && BossArena.RearmZoneIdAt(0) == z0) + " 첫 대상 = 존0(" + z0 + ")");
|
||||||
|
|
||||||
|
// 1웨이브 마리 수 — 에디트 모드엔 table_monsterappear 가 없어 SO 폴백을 쓴다
|
||||||
|
int wave = BossArena.WaveSize(b, z0);
|
||||||
|
L(" " + P(wave > 0 && wave <= b.rearmZoneRegenMax) +
|
||||||
|
" WaveSize(" + z0 + ")=" + wave + " (테이블 n_MaxMonsterCount → 없으면 rearmZoneRegenMax " + b.rearmZoneRegenMax + " · 상한도 그 값)");
|
||||||
|
|
||||||
|
// 타이밍 — 살아 있으면 타이머가 안 돈다 / 비면 임계까지 기다렸다 트리거
|
||||||
|
BossArena.SetRunActiveForProbe(1);
|
||||||
|
BossArena.NoteSpawn(z0, false); // alive 1
|
||||||
|
BossArena.ZoneRearmTick(b, 100f);
|
||||||
|
L(" " + P(BossArena.AliveOf(z0) > 0 && BossArena.RearmZoneEmptySinceAt(0) < 0f) +
|
||||||
|
" alive=" + BossArena.AliveOf(z0) + " → 빈 시각 미기록(" + BossArena.RearmZoneEmptySinceAt(0) + ")");
|
||||||
|
|
||||||
|
BossArena.NoteKill(z0, false); // alive 0
|
||||||
|
BossArena.ZoneRearmTick(b, 100f);
|
||||||
|
L(" " + P(BossArena.AliveOf(z0) == 0 && Mathf.Approximately(BossArena.RearmZoneEmptySinceAt(0), 100f)) +
|
||||||
|
" alive=0 → 빈 시각 " + BossArena.RearmZoneEmptySinceAt(0) + " 기록");
|
||||||
|
|
||||||
|
BossArena.ZoneRearmTick(b, 100f + b.rearmZoneEmptySeconds - 1f);
|
||||||
|
L(" " + P(Mathf.Approximately(BossArena.RearmZoneEmptySinceAt(0), 100f)) +
|
||||||
|
" 임계(" + b.rearmZoneEmptySeconds + "s) 전 → 트리거 없음(빈 시각 그대로 " + BossArena.RearmZoneEmptySinceAt(0) + ")");
|
||||||
|
|
||||||
|
float fire = 100f + b.rearmZoneEmptySeconds + 0.5f;
|
||||||
|
BossArena.ZoneRearmTick(b, fire);
|
||||||
|
L(" " + P(Mathf.Approximately(BossArena.RearmZoneEmptySinceAt(0), fire)) +
|
||||||
|
" 🔴 임계 경과 → 재무장 트리거(빈 시각이 " + BossArena.RearmZoneEmptySinceAt(0) + " 로 갱신 = RearmZone 호출)");
|
||||||
|
L(" (에디트 모드는 ActorInfo.Ins 가 없어 실제 On_Regen 은 0마리 — 「언제 트리거되는가」까지 증명 · §미확인)");
|
||||||
|
|
||||||
|
// 런 밖에서는 감시하지 않는다(결과창 동안 몹을 우리가 늘리지 않는다)
|
||||||
|
BossArena.SetRunActiveForProbe(-1);
|
||||||
|
float before = BossArena.RearmZoneEmptySinceAt(0);
|
||||||
|
BossArena.ZoneRearmTick(b, fire + 100f);
|
||||||
|
L(" " + P(Mathf.Approximately(BossArena.RearmZoneEmptySinceAt(0), before)) +
|
||||||
|
" 런 밖(결과창)에서는 감시 정지(빈 시각 " + BossArena.RearmZoneEmptySinceAt(0) + " 불변)");
|
||||||
|
BossArena.SetRunActiveForProbe(1);
|
||||||
|
|
||||||
|
// [다음 런] 즉시 재무장 진입점
|
||||||
|
int n = BossArena.RearmZoneSpawns("프로브 다음 런");
|
||||||
|
L(" " + P(BossArena.RearmZoneCount > 0) +
|
||||||
|
" RearmZoneSpawns() 진입점 동작(재등장 " + n + "마리 · 에디트 모드라 0 · 목록 " + BossArena.RearmZoneCount + "개 재구축)");
|
||||||
|
BossArena.SetRunActiveForProbe(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
|
// ⑥ 수리 ③ — 런 밖 처치는 다음 런 존 진행에 안 센다
|
||||||
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
|
static void Sec6_OutsideRunKills()
|
||||||
|
{
|
||||||
|
L("");
|
||||||
|
L("── ⑥ 수리 ③ 런 밖 처치가 존 카운터에 안 실린다(Q10: 결과창 동안 z0 26 → 47)");
|
||||||
|
|
||||||
|
var rs = WLRunSettings.Instance;
|
||||||
|
int z0 = rs.zoneSpawnerIds[0];
|
||||||
|
|
||||||
|
RunDirector.ResetAll();
|
||||||
|
RunDirector.EnsureSubscribed();
|
||||||
|
BossArena.SetRunActiveForProbe(-1); // 런 밖(결과창)
|
||||||
|
|
||||||
|
int arena0 = BossArena.KillsOf(z0);
|
||||||
|
int gateEvals0 = BossArena.GateSuspendedEvals;
|
||||||
|
for (int i = 0; i < 21; i++) // Q10 실측과 같은 21마리
|
||||||
|
{
|
||||||
|
RunDirector.NoteKill(z0, false); // 런 밖 → IsRunning=false → 무시
|
||||||
|
BossArena.NoteKill(z0, false); // 누적값은 813p 계약대로 계속 센다
|
||||||
|
}
|
||||||
|
L(" " + P(BossArena.KillsOf(z0) == arena0 + 21) +
|
||||||
|
" BossArena 누적은 계속 센다(" + arena0 + " → " + BossArena.KillsOf(z0) + ") = 813p 계약(리셋 금지) 유지");
|
||||||
|
L(" " + P(BossArena.GateSuspendedEvals > gateEvals0) +
|
||||||
|
" 런 밖 게이트 평가는 건너뛴다(GateSuspendedEvals " + gateEvals0 + " → " + BossArena.GateSuspendedEvals + ")");
|
||||||
|
|
||||||
|
BossArena.SetRunActiveForProbe(1);
|
||||||
|
RunDirector.StartRun("probe restart"); // ← 여기서 s_zoneBase 를 다시 잡는다
|
||||||
|
L(" " + P(RunDirector.ZoneProgress(0) == 0) +
|
||||||
|
" 🔴 다음 런 시작 직후 존0 진행 = " + RunDirector.ZoneProgress(0) + "/" + RunDirector.ZoneRequired(0) +
|
||||||
|
" — 런 밖 21처치가 한 건도 안 실렸다(StartRun 스냅샷 차감)");
|
||||||
|
|
||||||
|
RunDirector.NoteKill(z0, false);
|
||||||
|
L(" " + P(RunDirector.ZoneProgress(0) == 1) +
|
||||||
|
" 런 안 처치 1건은 정상 반영(" + RunDirector.ZoneProgress(0) + "/" + RunDirector.ZoneRequired(0) + ")");
|
||||||
|
|
||||||
|
RunDirector.ResetAll();
|
||||||
|
BossArena.SetRunActiveForProbe(0);
|
||||||
|
RunGuide.ResetDiagnostics();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
|
// ⑦ GC 0
|
||||||
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
|
static void Sec7_GC()
|
||||||
|
{
|
||||||
|
L("");
|
||||||
|
L("── ⑦ GC 0 (틱 경로 · 각 100회)");
|
||||||
|
|
||||||
|
var g = WLRunGuideSettings.Instance;
|
||||||
|
var b = WLBossArenaSettings.Instance;
|
||||||
|
var rs = WLRunSettings.Instance;
|
||||||
|
int z0 = rs.zoneSpawnerIds[0];
|
||||||
|
int other = 813005;
|
||||||
|
|
||||||
|
// ⓐ 결과창 정지 틱
|
||||||
|
RunGuide.ResetDiagnostics();
|
||||||
|
s_pc.Set_JoystickStatus(3);
|
||||||
|
RunGuide.BeginRunEndHold("GC");
|
||||||
|
RunGuide.SetRunEndHoldUntilForProbe(-1f);
|
||||||
|
RunGuide.RunEndHoldTick(g, s_pc, 10f); // 워밍업(조종 획득 1회)
|
||||||
|
long a0 = Alloc();
|
||||||
|
for (int i = 0; i < 100; i++) RunGuide.RunEndHoldTick(g, s_pc, 10f + i);
|
||||||
|
long a1 = Alloc();
|
||||||
|
L(" " + P(a1 - a0 <= 0) + " 정지 틱 ×100 = " + (a1 - a0) + " B");
|
||||||
|
RunGuide.EndRunEndHold("GC 끝");
|
||||||
|
s_pc.Set_JoystickStatus(3);
|
||||||
|
|
||||||
|
// ⓑ 빈 존 감시
|
||||||
|
BossArena.SetRunActiveForProbe(1);
|
||||||
|
BossArena.ZoneRearmTick(b, 500f);
|
||||||
|
long b0 = Alloc();
|
||||||
|
for (int i = 0; i < 100; i++) BossArena.ZoneRearmTick(b, 500f + i * 0.01f);
|
||||||
|
long b1 = Alloc();
|
||||||
|
L(" " + P(b1 - b0 <= 0) + " 빈 존 감시 ×100 = " + (b1 - b0) + " B");
|
||||||
|
BossArena.SetRunActiveForProbe(0);
|
||||||
|
|
||||||
|
// ⓒ 차단 판정 · 런 존 판정
|
||||||
|
long c0 = Alloc();
|
||||||
|
bool acc = false;
|
||||||
|
for (int i = 0; i < 100; i++) acc ^= BossArena.ShouldHoldZoneSpawn(b, i % 2 == 0 ? z0 : other, i % 2 == 0);
|
||||||
|
long c1 = Alloc();
|
||||||
|
L(" " + P(c1 - c0 <= 0) + " 차단 판정 ×100 = " + (c1 - c0) + " B (acc=" + acc + ")");
|
||||||
|
|
||||||
|
long d0 = Alloc();
|
||||||
|
for (int i = 0; i < 100; i++) acc ^= BossArena.IsRunZoneSpawner(i % 2 == 0 ? z0 : other);
|
||||||
|
long d1 = Alloc();
|
||||||
|
L(" " + P(d1 - d0 <= 0) + " 런 존 판정 ×100 = " + (d1 - d0) + " B (acc=" + acc + ")");
|
||||||
|
|
||||||
|
RunGuide.ResetDiagnostics();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
|
// ⑧ C8 롤백
|
||||||
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
|
static void Sec8_C8()
|
||||||
|
{
|
||||||
|
L("");
|
||||||
|
L("── ⑧ C8 롤백 (신규 스위치 0 → 813w6 동작 그대로)");
|
||||||
|
|
||||||
|
// ⓐ holdPcOnRunEnd = 0 → 런 종료 정지 경로가 통째로 죽는다
|
||||||
|
var tg = TmpG();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
UseTmpG(tg);
|
||||||
|
tg.holdPcOnRunEnd = false;
|
||||||
|
RunGuide.ResetDiagnostics();
|
||||||
|
s_pc.Set_JoystickStatus(3);
|
||||||
|
RunGuide.ProbeRunEnded(RunOutcome.Timeout);
|
||||||
|
L(" " + P(!RunGuide.RunEndHeld && RunGuide.RunEndHolds == 0) +
|
||||||
|
" holdPcOnRunEnd=0 → 정지 0(RunEndHeld=" + RunGuide.RunEndHeld + " RunEndHolds=" + RunGuide.RunEndHolds + ")");
|
||||||
|
L(" " + P(s_pc.JoystickStatus == 0) + " 조종도 잡지 않는다(JoystickStatus=" + s_pc.JoystickStatus + ") = 813w6 그대로");
|
||||||
|
|
||||||
|
// 정지 중에 스위치를 끄면 그 자리에서 풀린다
|
||||||
|
tg.holdPcOnRunEnd = true;
|
||||||
|
RunGuide.BeginRunEndHold("C8");
|
||||||
|
tg.holdPcOnRunEnd = false;
|
||||||
|
RunGuide.RunEndHoldTick(tg, s_pc, 900f);
|
||||||
|
L(" " + P(!RunGuide.RunEndHeld) + " 정지 중 스위치 off → 즉시 해제(사유 '" + RunGuide.LastRunEndHoldReason + "')");
|
||||||
|
}
|
||||||
|
finally { UseRealG(); UnityEngine.Object.DestroyImmediate(tg); RunGuide.ResetDiagnostics(); }
|
||||||
|
|
||||||
|
// ⓑ rearmZoneSpawnsEnabled = 0 → 감시·재무장 경로가 통째로 죽는다
|
||||||
|
var tb = TmpB();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
UseTmpB(tb);
|
||||||
|
BossArena.SetRunActiveForProbe(1);
|
||||||
|
BossArena.RebuildRearmZonesForProbe();
|
||||||
|
tb.rearmZoneSpawnsEnabled = false;
|
||||||
|
BossArena.ZoneRearmTick(tb, 1000f);
|
||||||
|
L(" " + P(BossArena.RearmZoneEmptySinceAt(0) < 0f) +
|
||||||
|
" rearmZoneSpawnsEnabled=0 → 감시 무동작(빈 시각 " + BossArena.RearmZoneEmptySinceAt(0) + ")");
|
||||||
|
L(" " + P(BossArena.RearmZoneSpawns("C8") == 0) + " RearmZoneSpawns() 도 0");
|
||||||
|
|
||||||
|
tb.rearmZoneSpawnsEnabled = true;
|
||||||
|
tb.rearmZoneSpawnsOnRestart = false;
|
||||||
|
L(" " + P(BossArena.RearmZoneSpawns("C8") == 0) + " rearmZoneSpawnsOnRestart=0 → [다음 런] 즉시 재무장 0");
|
||||||
|
}
|
||||||
|
finally { UseRealB(); BossArena.SetRunActiveForProbe(0); UnityEngine.Object.DestroyImmediate(tb); }
|
||||||
|
|
||||||
|
// ⓒ RuntimeDisabled — 축 전체 off
|
||||||
|
WLRunGuideSettings.RuntimeDisabled = true;
|
||||||
|
RunGuide.ResetDiagnostics();
|
||||||
|
RunGuide.BeginRunEndHold("RuntimeDisabled");
|
||||||
|
RunGuide.ForceTick();
|
||||||
|
L(" " + P(!WLRunGuideSettings.Enabled && !RunGuide.Active) + " RunGuide RuntimeDisabled → Active=false");
|
||||||
|
L(" " + P(!RunGuide.RunEndHeld) + " Tick 이 정지를 풀고 원본으로 돌려준다(RunEndHeld=" + RunGuide.RunEndHeld + ")");
|
||||||
|
WLRunGuideSettings.RuntimeDisabled = false;
|
||||||
|
|
||||||
|
WLBossArenaSettings.RuntimeDisabled = true;
|
||||||
|
var b2 = WLBossArenaSettings.Instance;
|
||||||
|
BossArena.SetRunActiveForProbe(1);
|
||||||
|
BossArena.RebuildRearmZonesForProbe();
|
||||||
|
BossArena.ZoneRearmTick(b2, 2000f);
|
||||||
|
L(" " + P(BossArena.RearmZoneEmptySinceAt(0) < 0f) + " BossArena RuntimeDisabled → 감시 무동작");
|
||||||
|
WLBossArenaSettings.RuntimeDisabled = false;
|
||||||
|
BossArena.SetRunActiveForProbe(0);
|
||||||
|
RunGuide.ResetDiagnostics();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -115,6 +115,24 @@
|
||||||
// → 수리 순서 = **HP 스냅샷(`SnapshotBossHp` · RunDirector.EndRun) → `RunEnded` 발행 → Park**.
|
// → 수리 순서 = **HP 스냅샷(`SnapshotBossHp` · RunDirector.EndRun) → `RunEnded` 발행 → Park**.
|
||||||
// `parkBossOnRunEndDeferred`(기본 on)가 고정을 **발행 뒤(다음 Tick)** 로 미루고, `RunResult.bossHpRatio/bossHp/bossHpMax`
|
// `parkBossOnRunEndDeferred`(기본 on)가 고정을 **발행 뒤(다음 Tick)** 로 미루고, `RunResult.bossHpRatio/bossHp/bossHpMax`
|
||||||
// 가 Park 이전 값을 싣는다. **Park 뒤 만피는 그대로 유지**한다(D-27 ③ 방지 로직 불변).
|
// 가 Park 이전 값을 싣는다. **Park 뒤 만피는 그대로 유지**한다(D-27 ③ 방지 로직 불변).
|
||||||
|
//
|
||||||
|
// ■ WL-813w7 (Q10 D-45 · 2026-09-09) — 813w3 §1-3 의 잡몹 유입 차단이 813w6 과 만나 존을 영구 소진시킨다
|
||||||
|
// 🔴 원인(코드로 확정 · 발주서 §1-1 「원인 실측」)
|
||||||
|
// ⓐ `HoldNearbyZoneSpawns`(이 파일)는 **보스가 살아 있는 동안** 아레나 중심 `arenaRadius + zoneSpawnHoldMargin`
|
||||||
|
// (= 20 + 10 = **30 m**) 안의 존 스포너에 `StopAllCoroutines()` 를 건다.
|
||||||
|
// ⓑ 존0(813001) 앵커 `(115, 0.39, −22)` ↔ 아레나 중심 `(139, 0.8, −20)` = **24.1 m** → **차단 대상이다.**
|
||||||
|
// ⓒ 원본 `MobControlMgr` 의 PhaseAllKill 리젠은 **그 코루틴 안에서** `killmobcount = 0` 을 한다
|
||||||
|
// (MobControlMgr.cs:216-222). 코루틴이 끊기면 `killmobcount` 가 `n_MaxMonsterCount` 에 굳고,
|
||||||
|
// 판정이 `==`(MobControlMgr.cs:214)라 다음 사망부터 Max+1, Max+2… → **두 번 다시 참이 되지 않는다.**
|
||||||
|
// = 그 스포너는 맵 세션이 끝날 때까지 **영구 소진**된다(리셋 API 없음 · 전부 private).
|
||||||
|
// ⓓ 813w6 이 재스폰 보스를 **게이트 개방까지** 살려 두면서, 차단이 「보스전 동안」이 아니라
|
||||||
|
// **다음 런 내내** 걸렸다 → R1n 0/18 · R2n 1/18(PC 는 존0 앵커에서 5.8 m 에 서 있었다) · R3n 14/18 정지.
|
||||||
|
// ■ 수리 ① `holdZoneSpawnOnlyAfterGate`(기본 on) — 차단을 **게이트 개방 뒤**(= 진짜 보스전)로 좁힌다.
|
||||||
|
// ② `holdZoneSpawnSkipsRunZones`(기본 on) — 런이 쓰는 존(WLRunSettings.zoneSpawnerIds)은 아예 대상에서 뺀다.
|
||||||
|
// ③ `rearmZoneSpawnsEnabled` — 존이 `rearmZoneEmptySeconds`(8 s > 원본 리듬 5.0 s) 넘게 비면
|
||||||
|
// **원본과 같은 공개 경로**(`MobActor.On_Regen()` = MobControlMgr.cs:221 과 동일한 호출)로 1웨이브를 되살린다.
|
||||||
|
// `RunDirector.Restart()` 도 즉시 1회 부른다(`rearmZoneSpawnsOnRestart` · 이미 굳은 세션 구제).
|
||||||
|
// ■ C8 롤백 — 위 4개 스위치를 전부 끄면 813w6 동작 그대로다.
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
@ -167,6 +185,13 @@ namespace WL.Combat.Boss
|
||||||
static bool s_parkPending; // RunEnded 발행이 끝난 뒤로 미뤄 둔 고정이 있는가
|
static bool s_parkPending; // RunEnded 발행이 끝난 뒤로 미뤄 둔 고정이 있는가
|
||||||
static float s_parkDueTime = -1f; // 그 고정을 실행할 시각(unscaled · 0 지연이면 다음 Tick)
|
static float s_parkDueTime = -1f; // 그 고정을 실행할 시각(unscaled · 0 지연이면 다음 Tick)
|
||||||
|
|
||||||
|
// 813w7 §1-2 ② — 존 스포너 재무장(D-45). 감시 대상 id 와 「빈 지 얼마나 됐나」를 사전 할당 배열로 든다(GC 0).
|
||||||
|
const int kMaxRearmZones = 16; // 버퍼 크기지 게임 수치가 아니다
|
||||||
|
static readonly int[] s_rearmIds = new int[kMaxRearmZones];
|
||||||
|
static readonly float[] s_rearmEmptySince = new float[kMaxRearmZones];
|
||||||
|
static int s_rearmCount = -1; // −1 = 아직 목록을 안 잡았다
|
||||||
|
static float s_nextZoneRearmCheck;
|
||||||
|
|
||||||
// ───────────────────────────────────────── 진단(프로브·QA 가 읽는다)
|
// ───────────────────────────────────────── 진단(프로브·QA 가 읽는다)
|
||||||
public static bool GateArmed { get { return s_armed; } }
|
public static bool GateArmed { get { return s_armed; } }
|
||||||
public static bool GateOpen { get { return s_gateOpen; } }
|
public static bool GateOpen { get { return s_gateOpen; } }
|
||||||
|
|
@ -192,6 +217,8 @@ namespace WL.Combat.Boss
|
||||||
public static int LeashSuspendedTicks, GateSuspendedEvals;
|
public static int LeashSuspendedTicks, GateSuspendedEvals;
|
||||||
/// <summary>존 스포너 리젠 예약을 끊은 횟수 · 대상 스포너 수(§1-3).</summary>
|
/// <summary>존 스포너 리젠 예약을 끊은 횟수 · 대상 스포너 수(§1-3).</summary>
|
||||||
public static int ZoneSpawnHoldTicks, ZoneSpawnHoldTargets;
|
public static int ZoneSpawnHoldTicks, ZoneSpawnHoldTargets;
|
||||||
|
/// <summary>813w7 §1-2 ②: 차단을 건너뛴 횟수(게이트 전 · 런 존 제외) — D-45 수리가 실제로 도는지의 증거.</summary>
|
||||||
|
public static int ZoneSpawnHoldSkipped;
|
||||||
/// <summary>이 런의 게이트 판정 기준 처치 수(= TotalZoneKills − 런 시작 스냅샷).</summary>
|
/// <summary>이 런의 게이트 판정 기준 처치 수(= TotalZoneKills − 런 시작 스냅샷).</summary>
|
||||||
public static int GateKills { get { int v = TotalZoneKills - s_totalKillBase; return v > 0 ? v : 0; } }
|
public static int GateKills { get { int v = TotalZoneKills - s_totalKillBase; return v > 0 ? v : 0; } }
|
||||||
|
|
||||||
|
|
@ -1021,16 +1048,46 @@ namespace WL.Combat.Boss
|
||||||
{
|
{
|
||||||
if (cfg == null || !Active || !cfg.holdZoneSpawnNearArena || cfg.zoneSpawnHoldMargin < 0f) return;
|
if (cfg == null || !Active || !cfg.holdZoneSpawnNearArena || cfg.zoneSpawnHoldMargin < 0f) return;
|
||||||
if (DSUtil.CheckNull(s_boss) || s_boss.IsDead()) return;
|
if (DSUtil.CheckNull(s_boss) || s_boss.IsDead()) return;
|
||||||
|
|
||||||
|
// 🔴 813w7 §1-2 ②(D-45) — 「보스 생존 중」이 아니라 「게이트 개방 뒤」로 좁힌다.
|
||||||
|
// 813w6 이 재스폰 보스를 **게이트 개방까지** 살려 두면서, 이 차단이 다음 런 내내 걸렸다.
|
||||||
|
// 차단은 `StopAllCoroutines` 라 PhaseAllKill 의 `killmobcount = 0`(MobControlMgr.cs:218)까지 함께
|
||||||
|
// 끊어 카운터를 `n_MaxMonsterCount` 에 굳힌다 → `== 비교`(:214) 가 영영 참이 안 된다(영구 소진).
|
||||||
|
if (cfg.holdZoneSpawnOnlyAfterGate && !s_gateOpen) { ZoneSpawnHoldSkipped++; return; }
|
||||||
|
|
||||||
if (!s_nearZonesScanned) ScanNearZones(cfg);
|
if (!s_nearZonesScanned) ScanNearZones(cfg);
|
||||||
for (int i = 0; i < s_nearZones.Count; i++)
|
for (int i = 0; i < s_nearZones.Count; i++)
|
||||||
{
|
{
|
||||||
var m = s_nearZones[i];
|
var m = s_nearZones[i];
|
||||||
if (DSUtil.CheckNull(m) || !m.Get_LoadComplete()) continue;
|
if (DSUtil.CheckNull(m) || !m.Get_LoadComplete()) continue;
|
||||||
|
// 🔴 런이 쓰는 존(= 다음 런의 사냥터)은 건드리지 않는다 — 여기서 굳으면 D-45 가 그대로 재발한다.
|
||||||
|
if (!ShouldHoldZoneSpawn(cfg, m.SpawnerID, s_gateOpen)) { ZoneSpawnHoldSkipped++; continue; }
|
||||||
m.StopAllCoroutines();
|
m.StopAllCoroutines();
|
||||||
ZoneSpawnHoldTicks++;
|
ZoneSpawnHoldTicks++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 813w7 §1-2 ② — 이 스포너의 리젠 예약을 끊어도 되는가(순수 함수 · Actor 접근 0 · 프로브가 그대로 부른다 · C8 증거).
|
||||||
|
/// 🔴 「끊는다 = 그 스포너의 PhaseAllKill 카운터를 영구히 굳힌다」이므로 판정은 보수적이어야 한다.
|
||||||
|
/// </summary>
|
||||||
|
public static bool ShouldHoldZoneSpawn(WLBossArenaSettings cfg, int spawnerId, bool gateOpen)
|
||||||
|
{
|
||||||
|
if (cfg == null || !cfg.holdZoneSpawnNearArena) return false;
|
||||||
|
if (cfg.holdZoneSpawnOnlyAfterGate && !gateOpen) return false;
|
||||||
|
if (cfg.holdZoneSpawnSkipsRunZones && IsRunZoneSpawner(spawnerId)) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>이 스포너가 런 축(WLRunSettings.zoneSpawnerIds)의 사냥 존인가(순수 · 프로브가 그대로 부른다).</summary>
|
||||||
|
public static bool IsRunZoneSpawner(int spawnerId)
|
||||||
|
{
|
||||||
|
var rs = WL.Combat.Run.WLRunSettings.Instance;
|
||||||
|
if (rs == null || rs.zoneSpawnerIds == null) return false;
|
||||||
|
for (int i = 0; i < rs.zoneSpawnerIds.Length; i++) if (rs.zoneSpawnerIds[i] == spawnerId) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>대상 존 스포너를 1회 훑는다(런당 몇 회 · 매 틱 아님).</summary>
|
/// <summary>대상 존 스포너를 1회 훑는다(런당 몇 회 · 매 틱 아님).</summary>
|
||||||
static void ScanNearZones(WLBossArenaSettings cfg)
|
static void ScanNearZones(WLBossArenaSettings cfg)
|
||||||
{
|
{
|
||||||
|
|
@ -1053,6 +1110,171 @@ namespace WL.Combat.Boss
|
||||||
Log("존 스포너 유입 차단 대상 " + s_nearZones.Count + "개(중심 " + center.ToString("F1") + " 반경 " + lim.ToString("F0") + "m)");
|
Log("존 스포너 유입 차단 대상 " + s_nearZones.Count + "개(중심 " + center.ToString("F1") + " 반경 " + lim.ToString("F0") + "m)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
|
// 813w7 §1-2 ② — 존 스포너 재무장 (D-45 · 다음 런 사냥터가 비어 있다)
|
||||||
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// 🔴 원본에 「스포너 리셋 API」는 없다(실측) — `MobControlMgr` 의 공개 멤버는
|
||||||
|
// `SpawnerID` · `MobGenRange` · `text_lv` · `Get_LoadComplete()` 뿐이고,
|
||||||
|
// `killmobcount` · `orderIndex` · `dic_actors` 는 전부 private 이다.
|
||||||
|
// 그래서 **원본이 스스로 쓰는 공개 경로**를 그대로 다시 태운다 —
|
||||||
|
// 813e2 표 A 의 `MobControlMgr.cs:220-221` `dic_actors[orderIndex][i].On_Regen()` 와
|
||||||
|
// 같은 호출을, 그 스포너에 등록된 비활성 몹(`ActorInfo.Get_Actors`)에 대해 한다.
|
||||||
|
// `MobActor.On_Regen()` 은 원본 그대로 `Set_Warp(OriginalPos)` + 스폰 이펙트 +
|
||||||
|
// `CombatEvents.RaiseSpawned` 까지 하므로 존 카운터(z.alive/z.spawns)도 정상으로 붙는다.
|
||||||
|
//
|
||||||
|
// 🔴 이중 스폰 방지 = `rearmZoneEmptySeconds`(기본 8 s) > 원본 리듬 5.0 s(시체 3.0 + 리젠 2.0 · 813e2 표 A).
|
||||||
|
// 스포너가 멀쩡하면 5.0 s 에 스스로 되살아나 alive > 0 이 되므로 이 경로는 아예 들어오지 않는다.
|
||||||
|
|
||||||
|
/// <summary>감시 대상 존 id 목록을 다시 잡는다(런 존만 / 아레나 존 전체 · SO).</summary>
|
||||||
|
static void BuildRearmZones(WLBossArenaSettings cfg)
|
||||||
|
{
|
||||||
|
s_rearmCount = 0;
|
||||||
|
int[] src = null;
|
||||||
|
if (cfg.rearmZoneRunZonesOnly)
|
||||||
|
{
|
||||||
|
var rs = WL.Combat.Run.WLRunSettings.Instance;
|
||||||
|
if (rs != null) src = rs.zoneSpawnerIds;
|
||||||
|
}
|
||||||
|
if (src == null) src = cfg.zoneSpawnerIds;
|
||||||
|
if (src == null) return;
|
||||||
|
|
||||||
|
for (int i = 0; i < src.Length && s_rearmCount < kMaxRearmZones; i++)
|
||||||
|
{
|
||||||
|
s_rearmIds[s_rearmCount] = src[i];
|
||||||
|
s_rearmEmptySince[s_rearmCount] = -1f;
|
||||||
|
s_rearmCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 빈 존 감시 1틱 — 살아 있는 몹이 0 인 상태가 `rearmZoneEmptySeconds` 이상이면 그 존을 재무장한다.
|
||||||
|
/// 🔴 폴링 자체는 카운터만 본다 = 할당 0. 실제 재무장(드문 경로)만 원본 등록부를 훑는다.
|
||||||
|
/// 프로브가 가짜 시각으로 그대로 부를 수 있다.
|
||||||
|
/// </summary>
|
||||||
|
public static void ZoneRearmTick(WLBossArenaSettings cfg, float now)
|
||||||
|
{
|
||||||
|
if (cfg == null || !Active || !cfg.rearmZoneSpawnsEnabled || !s_armed) return;
|
||||||
|
if (cfg.rearmZoneEmptySeconds <= 0f) return;
|
||||||
|
if (!RunActive) return; // 결과창·다음 런 대기 중에는 손대지 않는다
|
||||||
|
if (s_rearmCount < 0) BuildRearmZones(cfg);
|
||||||
|
|
||||||
|
for (int i = 0; i < s_rearmCount; i++)
|
||||||
|
{
|
||||||
|
if (AliveOf(s_rearmIds[i]) > 0) { s_rearmEmptySince[i] = -1f; continue; }
|
||||||
|
if (s_rearmEmptySince[i] < 0f) { s_rearmEmptySince[i] = now; continue; }
|
||||||
|
if (now - s_rearmEmptySince[i] < cfg.rearmZoneEmptySeconds) continue;
|
||||||
|
s_rearmEmptySince[i] = now; // 성공·실패와 무관하게 다음 주기를 다시 잰다
|
||||||
|
RearmZone(cfg, s_rearmIds[i], "빈 존 " + cfg.rearmZoneEmptySeconds.ToString("F0") + "s");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 대상 존을 전부 즉시 1회 재무장한다(「다음 런」 진입점 · `RunDirector.Restart()` 가 부른다).
|
||||||
|
/// 이미 살아 있는 몹이 있으면 그 존은 건너뛴다.
|
||||||
|
/// </summary>
|
||||||
|
public static int RearmZoneSpawns(string reason)
|
||||||
|
{
|
||||||
|
var cfg = Cfg;
|
||||||
|
if (cfg == null || !Active || !cfg.rearmZoneSpawnsEnabled || !cfg.rearmZoneSpawnsOnRestart) return 0;
|
||||||
|
BuildRearmZones(cfg); // 런 존 목록이 바뀌었을 수 있다
|
||||||
|
s_nextZoneRearmCheck = 0f;
|
||||||
|
|
||||||
|
int total = 0;
|
||||||
|
for (int i = 0; i < s_rearmCount; i++)
|
||||||
|
{
|
||||||
|
s_rearmEmptySince[i] = Now();
|
||||||
|
if (AliveOf(s_rearmIds[i]) > 0) continue;
|
||||||
|
total += RearmZone(cfg, s_rearmIds[i], reason);
|
||||||
|
}
|
||||||
|
if (total > 0) Log("존 재무장(" + reason + ") — " + total + "마리 재등장");
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 존 하나를 재무장한다 — 그 스포너의 **비활성** 몹을 1웨이브분만큼 `On_Regen()` 한다.
|
||||||
|
/// 반환 = 실제로 되살린 마리 수. 원본 등록부(`ActorInfo`)가 없거나 대상이 없으면 0.
|
||||||
|
/// </summary>
|
||||||
|
public static int RearmZone(WLBossArenaSettings cfg, int spawnerId, string reason)
|
||||||
|
{
|
||||||
|
if (cfg == null || !Application.isPlaying) return 0;
|
||||||
|
var info = ActorInfo.Ins;
|
||||||
|
if (DSUtil.CheckNull(info)) return 0;
|
||||||
|
|
||||||
|
int want = WaveSize(cfg, spawnerId);
|
||||||
|
if (want <= 0) return 0;
|
||||||
|
|
||||||
|
// 🔴 `ActorInfo.Get_Actors` 는 dic_actor[role][spawnerId] 를 직접 인덱싱한다 —
|
||||||
|
// 등록이 없는 스포너면 KeyNotFoundException 이 난다. 재무장 실패가 런을 죽이면 안 되므로 감싼다.
|
||||||
|
List<Actor> actors = null;
|
||||||
|
int done = 0;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
actors = info.Get_Actors(eRole.Mob, spawnerId, true);
|
||||||
|
if (actors == null) return 0;
|
||||||
|
|
||||||
|
// 이미 살아 있는 개체가 있으면(카운터 드리프트) 재무장하지 않는다 — 이중 스폰 방지.
|
||||||
|
for (int i = 0; i < actors.Count; i++)
|
||||||
|
{
|
||||||
|
var a = actors[i];
|
||||||
|
if (!DSUtil.CheckNull(a) && a.isActiveAndEnabled && !a.IsDead()) return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < actors.Count && done < want; i++)
|
||||||
|
{
|
||||||
|
var mob = actors[i] as MobActor;
|
||||||
|
if (DSUtil.CheckNull(mob) || mob.isActiveAndEnabled) continue;
|
||||||
|
mob.On_Regen(); // 원본 웨이브와 같은 호출(MobControlMgr.cs:221) — 좌표·이펙트·Spawned 이벤트까지 원본 그대로
|
||||||
|
done++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (System.Exception ex) { Log("존 " + spawnerId + " 재무장 실패 — " + ex.Message); }
|
||||||
|
finally { if (actors != null) info.Release_Actors(actors); }
|
||||||
|
|
||||||
|
if (done > 0)
|
||||||
|
{
|
||||||
|
ZoneRearmCount++;
|
||||||
|
ZoneRearmMobs += done;
|
||||||
|
LastRearmZoneId = spawnerId;
|
||||||
|
Log("존 " + spawnerId + " 재무장(" + reason + ") — " + done + "/" + want + "마리");
|
||||||
|
}
|
||||||
|
return done;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 1웨이브 마리 수 — 원본 테이블 `n_MaxMonsterCount`(= `MobControlMgr` 이 쓰는 그 값)를 읽고,
|
||||||
|
/// 못 읽으면 SO `rearmZoneRegenMax`. SO 값은 언제나 상한으로 쓴다(안전).
|
||||||
|
/// </summary>
|
||||||
|
public static int WaveSize(WLBossArenaSettings cfg, int spawnerId)
|
||||||
|
{
|
||||||
|
int cap = cfg != null && cfg.rearmZoneRegenMax > 0 ? cfg.rearmZoneRegenMax : 0;
|
||||||
|
int n = cap;
|
||||||
|
// 🔴 table_monsterappear.Get_Data_orNull 은 dic_Data[id] 직접 인덱싱이라 없는 행이면 던진다.
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var tbl = table_monsterappear.Ins;
|
||||||
|
if (!DSUtil.CheckNull(tbl))
|
||||||
|
{
|
||||||
|
var row = tbl.Get_Data_orNull(spawnerId);
|
||||||
|
if (row != null && row.n_MaxMonsterCount > 0) n = row.n_MaxMonsterCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (System.Exception) { n = cap; }
|
||||||
|
if (cap > 0 && n > cap) n = cap;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>813w7 진단 — 재무장을 실행한 존 수 · 되살린 총 마리 수 · 마지막 대상 존 id.</summary>
|
||||||
|
public static int ZoneRearmCount, ZoneRearmMobs;
|
||||||
|
public static int LastRearmZoneId;
|
||||||
|
/// <summary>감시 대상 존 수(진단 · 아직 목록을 안 잡았으면 −1).</summary>
|
||||||
|
public static int RearmZoneCount { get { return s_rearmCount; } }
|
||||||
|
/// <summary>감시 대상 i 의 스포너 id · 「비었다」로 잡힌 시각(unscaled · 안 비었으면 −1).</summary>
|
||||||
|
public static int RearmZoneIdAt(int i) { return i >= 0 && i < s_rearmCount ? s_rearmIds[i] : 0; }
|
||||||
|
public static float RearmZoneEmptySinceAt(int i) { return i >= 0 && i < s_rearmCount ? s_rearmEmptySince[i] : -1f; }
|
||||||
|
/// <summary>프로브용 — 감시 목록을 다시 잡는다.</summary>
|
||||||
|
public static void RebuildRearmZonesForProbe() { var c = Cfg; if (c != null) BuildRearmZones(c); }
|
||||||
|
|
||||||
/// <summary>마지막 부분 회복 시각(unscaled · 쿨다운 판정).</summary>
|
/// <summary>마지막 부분 회복 시각(unscaled · 쿨다운 판정).</summary>
|
||||||
static float s_lastLeashHeal = -999f;
|
static float s_lastLeashHeal = -999f;
|
||||||
/// <summary>리쉬 회복을 실제로 넣은 횟수 · 쿨다운으로 건너뛴 횟수(진단).</summary>
|
/// <summary>리쉬 회복을 실제로 넣은 횟수 · 쿨다운으로 건너뛴 횟수(진단).</summary>
|
||||||
|
|
@ -1208,6 +1430,14 @@ namespace WL.Combat.Boss
|
||||||
// 🔴 813w6 §1-2 (D-44) — RunEnded 발행이 끝난 뒤로 미뤄 둔 고정을 여기서 실행한다.
|
// 🔴 813w6 §1-2 (D-44) — RunEnded 발행이 끝난 뒤로 미뤄 둔 고정을 여기서 실행한다.
|
||||||
if (s_parkPending) PumpDeferredPark();
|
if (s_parkPending) PumpDeferredPark();
|
||||||
|
|
||||||
|
// 🔴 813w7 §1-2 ② (D-45) — 빈 존 감시(자체 주기 · 리쉬 주기와 독립).
|
||||||
|
if (cfg.rearmZoneSpawnsEnabled && now >= s_nextZoneRearmCheck)
|
||||||
|
{
|
||||||
|
s_nextZoneRearmCheck = now + Mathf.Max(0.1f, cfg.rearmZoneCheckSeconds);
|
||||||
|
try { ZoneRearmTick(cfg, now); }
|
||||||
|
catch (System.Exception ex) { Log("존 재무장 감시 실패 — " + ex.Message); }
|
||||||
|
}
|
||||||
|
|
||||||
if (now < s_nextLeashCheck) return;
|
if (now < s_nextLeashCheck) return;
|
||||||
s_nextLeashCheck = now + Mathf.Max(0f, cfg.leashCheckIntervalSeconds);
|
s_nextLeashCheck = now + Mathf.Max(0f, cfg.leashCheckIntervalSeconds);
|
||||||
try { LeashTick(); }
|
try { LeashTick(); }
|
||||||
|
|
@ -1245,6 +1475,10 @@ namespace WL.Combat.Boss
|
||||||
s_totalKillBase = 0; s_runActiveOverride = 0;
|
s_totalKillBase = 0; s_runActiveOverride = 0;
|
||||||
RearmCount = LivingBossResetCount = DeadBossRegenCount = 0;
|
RearmCount = LivingBossResetCount = DeadBossRegenCount = 0;
|
||||||
LeashSuspendedTicks = GateSuspendedEvals = ZoneSpawnHoldTicks = ZoneSpawnHoldTargets = 0;
|
LeashSuspendedTicks = GateSuspendedEvals = ZoneSpawnHoldTicks = ZoneSpawnHoldTargets = 0;
|
||||||
|
// 813w7
|
||||||
|
ZoneSpawnHoldSkipped = ZoneRearmCount = ZoneRearmMobs = LastRearmZoneId = 0;
|
||||||
|
s_rearmCount = -1; s_nextZoneRearmCheck = 0f;
|
||||||
|
for (int i = 0; i < kMaxRearmZones; i++) s_rearmEmptySince[i] = -1f;
|
||||||
// 813w5
|
// 813w5
|
||||||
s_bossHoldUntil = -1f; s_probeTime = -1f;
|
s_bossHoldUntil = -1f; s_probeTime = -1f;
|
||||||
ParkCount = BossHoldTicks = OutsideRunLeashResets = GateMinBlocked = RunEndNotices = 0;
|
ParkCount = BossHoldTicks = OutsideRunLeashResets = GateMinBlocked = RunEndNotices = 0;
|
||||||
|
|
@ -1283,6 +1517,7 @@ namespace WL.Combat.Boss
|
||||||
s_lastLeashHeal = -999f;
|
s_lastLeashHeal = -999f;
|
||||||
s_nearZonesScanned = false; // 맵/스포너가 바뀌었을 수 있다 — 다시 훑는다
|
s_nearZonesScanned = false; // 맵/스포너가 바뀌었을 수 있다 — 다시 훑는다
|
||||||
s_parkPending = false; s_parkDueTime = -1f; // 813w6 — 여기서 보스를 직접 처리하므로 미뤄 둔 고정은 버린다
|
s_parkPending = false; s_parkDueTime = -1f; // 813w6 — 여기서 보스를 직접 처리하므로 미뤄 둔 고정은 버린다
|
||||||
|
s_rearmCount = -1; // 813w7 — 존 감시 목록도 다시 잡는다(런 존이 바뀌었을 수 있다)
|
||||||
|
|
||||||
// ① 게이트 조기 개방(D-27) — 이 런 기준선으로 재스냅샷
|
// ① 게이트 조기 개방(D-27) — 이 런 기준선으로 재스냅샷
|
||||||
if (cfg != null && cfg.restartRebasesGateKills) RebaseGateCounters();
|
if (cfg != null && cfg.restartRebasesGateKills) RebaseGateCounters();
|
||||||
|
|
|
||||||
|
|
@ -308,5 +308,46 @@ namespace WL.Combat.Boss
|
||||||
[Tooltip("🔴 런 종료 순간(Park 이전)의 보스 HP 를 RunResult.bossHpRatio/bossHp/bossHpMax 에 담는다. "
|
[Tooltip("🔴 런 종료 순간(Park 이전)의 보스 HP 를 RunResult.bossHpRatio/bossHp/bossHpMax 에 담는다. "
|
||||||
+ "끄면 −1/0/0 = 813p 계약 그대로(필드는 있지만 비어 있다).")]
|
+ "끄면 −1/0/0 = 813p 계약 그대로(필드는 있지만 비어 있다).")]
|
||||||
public bool snapshotBossHpOnRunEnd = true;
|
public bool snapshotBossHpOnRunEnd = true;
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────── 813w7 §1-2 ② 존 스포너 재무장(D-45)
|
||||||
|
[Header("존 스포너 재무장 (813w7 · D-45 다음 런 사냥터가 비어 있다)")]
|
||||||
|
[Tooltip("🔴 존 스포너 리젠 차단(holdZoneSpawnNearArena)을 **게이트가 열린 뒤**(= 진짜 보스전)에만 한다. "
|
||||||
|
+ "813w3 은 「보스가 살아 있는 동안」이 조건이었는데, 813w6 이 재스폰 보스를 게이트 개방까지 살려 두면서 "
|
||||||
|
+ "다음 런 내내 차단이 걸렸다 → 존0 이 통째로 얼어붙는다(D-45 · R2n 1/18). "
|
||||||
|
+ "끄면 813w3 그대로(보스 생존 = 상시 차단).")]
|
||||||
|
public bool holdZoneSpawnOnlyAfterGate = true;
|
||||||
|
|
||||||
|
[Tooltip("🔴 리젠 차단 대상에서 **런이 쓰는 존**(WLRunSettings.zoneSpawnerIds)은 뺀다. "
|
||||||
|
+ "차단은 원본 `MobControlMgr` 의 예약 코루틴을 StopAllCoroutines 로 끊는 방식인데, "
|
||||||
|
+ "PhaseAllKill 은 그 코루틴 안에서 `killmobcount = 0` 을 하므로(MobControlMgr.cs:218) "
|
||||||
|
+ "끊기면 카운터가 `n_MaxMonsterCount` 에 굳어 `== 비교`(:214)가 두 번 다시 참이 되지 않는다 = 영구 소진. "
|
||||||
|
+ "끄면 813w3 그대로(런 존도 차단 대상).")]
|
||||||
|
public bool holdZoneSpawnSkipsRunZones = true;
|
||||||
|
|
||||||
|
[Tooltip("🔴 존 스포너가 비어 있는 채로 굳으면 WL 이 원본과 같은 경로(MobActor.On_Regen)로 한 웨이브를 다시 깨운다. "
|
||||||
|
+ "원본 리셋 API 가 없어(MobControlMgr 공개 멤버 = SpawnerID·MobGenRange·text_lv·Get_LoadComplete 뿐) "
|
||||||
|
+ "813e2 PhaseAllKill 웨이브(:220-221 `dic_actors[orderIndex][i].On_Regen()`)와 같은 호출로 대신한다. "
|
||||||
|
+ "끄면 재무장 경로가 통째로 죽는다(C8).")]
|
||||||
|
public bool rearmZoneSpawnsEnabled = true;
|
||||||
|
|
||||||
|
[Tooltip("존의 살아 있는 몹이 0 인 상태가 이 시간(초 · unscaled) 이상 이어지면 재무장한다. "
|
||||||
|
+ "🔴 원본 리듬(시체 3.0 s + f_SpawnerDelay 2.0 s = 5.0 s · 813e2 표 A)보다 **길게** 둘 것 — "
|
||||||
|
+ "짧으면 멀쩡한 스포너의 웨이브와 겹쳐 이중 스폰이 된다.")]
|
||||||
|
public float rearmZoneEmptySeconds = 8f;
|
||||||
|
|
||||||
|
[Tooltip("빈 존 감시 주기(초 · unscaled). 이 주기의 폴링 자체는 할당 0(카운터만 본다).")]
|
||||||
|
public float rearmZoneCheckSeconds = 1f;
|
||||||
|
|
||||||
|
[Tooltip("한 번에 되살릴 최대 마리 수. 기본은 테이블 `n_MaxMonsterCount`(= 1웨이브)를 쓰고, "
|
||||||
|
+ "테이블을 못 읽으면 이 값을 쓴다. 테이블 값이 이 값보다 크면 이 값으로 자른다(안전 상한).")]
|
||||||
|
public int rearmZoneRegenMax = 15;
|
||||||
|
|
||||||
|
[Tooltip("재무장 대상 = 런이 쓰는 존(WLRunSettings.zoneSpawnerIds)만. "
|
||||||
|
+ "끄면 이 에셋의 zoneSpawnerIds 전체(게이트 존 포함)를 대상으로 한다.")]
|
||||||
|
public bool rearmZoneRunZonesOnly = true;
|
||||||
|
|
||||||
|
[Tooltip("🔴 [다음 런](RunDirector.Restart)에서 대기 없이 즉시 1회 재무장한다 — 앞 런에서 이미 굳은 스포너를 "
|
||||||
|
+ "다음 런 시작 시점에 되살린다(D-45 의 직접 처방). 끄면 위의 빈 존 감시로만 되살아난다.")]
|
||||||
|
public bool rearmZoneSpawnsOnRestart = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -523,6 +523,7 @@ namespace WL.Combat.Run
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (c.restartRearmsBossGate) BossArena.ResetGate(); // 존 누적 카운터는 건드리지 않는다
|
if (c.restartRearmsBossGate) BossArena.ResetGate(); // 존 누적 카운터는 건드리지 않는다
|
||||||
|
BossArena.RearmZoneSpawns("다음 런"); // 🔴 #813w7 D-45 — 굳은 존 스포너를 1웨이브 되살린다(SO 로 끈다)
|
||||||
if (c.restartResetsPotions) PotionUse.ResetRun("run restart");
|
if (c.restartResetsPotions) PotionUse.ResetRun("run restart");
|
||||||
}
|
}
|
||||||
catch (System.Exception ex) { Log("재시작 일부 실패 — " + ex.Message); }
|
catch (System.Exception ex) { Log("재시작 일부 실패 — " + ex.Message); }
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,24 @@
|
||||||
// ■ C8 롤백 — `warpBackEnabled=0` 이면 워프 경로가 통째로 죽고 813w2 동작 그대로다.
|
// ■ C8 롤백 — `warpBackEnabled=0` 이면 워프 경로가 통째로 죽고 813w2 동작 그대로다.
|
||||||
// `exitDiagEnabled=0` 이면 진단 로그도 0.
|
// `exitDiagEnabled=0` 이면 진단 로그도 0.
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// WL-813w7 (2026-09-09 · PD 지시 #813) — 근거 = qa/W2-10_재채점_v8.md **D-45**
|
||||||
|
//
|
||||||
|
// 🔴 실측 결함 — 「다음 런」의 사냥터가 비어 있다(R1n 0/18 · R2n 1/18 · R3n 14/18 정지).
|
||||||
|
// R2n 은 PC 좌표 `(112.93, 0.06, −16.60)` = 존0 앵커 `(115, 0.39, −22)` 에서 5.8 m,
|
||||||
|
// 즉 **PC 는 존0 에 도착해 Hold 로 서 있는데 때릴 몹이 없다**(HP 310/310 무피해).
|
||||||
|
//
|
||||||
|
// ■ 이 파일이 맡은 몫 = ① 결과창 동안 PC 를 세운다(§1-2 ①)
|
||||||
|
// 813w6 까지는 `OnRunEnded` 가 `StopGuide` 만 했다 = 가이드를 끄고 **원본 자동전투 100%** 로 돌려줬다.
|
||||||
|
// Q10 실측: 결과창을 100 초 띄워 둔 사이 존0 처치가 **26 → 47**(21마리)로 늘었다.
|
||||||
|
// 이제 런이 끝나면 다음 런이 시작될 때까지(또는 `runEndHoldMaxSeconds`) PC 를 붙잡는다 —
|
||||||
|
// ⓐ `Set_JoystickStatus(1)` → PCActor.cs:139 이 `base.Update()`(자동 교전 접근 루프)를 건너뛴다
|
||||||
|
// ⓑ `Del_Target(true)` 매 폴링 → PCActor.cs:93 자동 스킬 루프는 `isTarget` 이 있어야만 돈다
|
||||||
|
// ⓒ `NavMeshAgent.ResetPath()` → 런 종료 순간의 잔여 경로로 계속 걸어가지 않는다
|
||||||
|
// 복귀 = 다음 `RunStarted`(= `RunDirector.Restart()`) 또는 상한 만료 또는 PC 사망.
|
||||||
|
// 🔴 UI 0 — RunGuide 는 결과창을 보지 않는다. 「결과창 닫힘」은 상한으로만 판정한다(§4 미확인).
|
||||||
|
//
|
||||||
|
// ■ C8 롤백 — `holdPcOnRunEnd=0` 이면 이 경로가 통째로 죽고 813w6 동작 그대로다.
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
//
|
//
|
||||||
// ■ 값(C45) 전부 WLRunGuideSettings. enabled = 0 이면 러너가 뜨지 않고 구독도 하지 않는다(C8).
|
// ■ 값(C45) 전부 WLRunGuideSettings. enabled = 0 이면 러너가 뜨지 않고 구독도 하지 않는다(C8).
|
||||||
//
|
//
|
||||||
|
|
@ -217,6 +235,16 @@ namespace WL.Combat.Run
|
||||||
/// <summary>마지막 이탈 진단 줄 원문(프로브·QA 가 읽는다 · 콘솔에 나간 그 문자열).</summary>
|
/// <summary>마지막 이탈 진단 줄 원문(프로브·QA 가 읽는다 · 콘솔에 나간 그 문자열).</summary>
|
||||||
public static string LastExitLog = "";
|
public static string LastExitLog = "";
|
||||||
|
|
||||||
|
// ───────────────────────────────────────── 813w7 진단 (D-45 · §1-2 ①)
|
||||||
|
/// <summary>813w7 §1-2 ①: 런 종료 정지를 건 횟수 · 푼 횟수 · 정지 폴링 틱 수 · 정지 중 뗀 타깃 수.</summary>
|
||||||
|
public static int RunEndHolds, RunEndHoldReleases, RunEndHoldTicks, RunEndTargetClears;
|
||||||
|
/// <summary>지금 「결과창 중 PC 정지」 상태인가(프로브·QA 가 읽는다).</summary>
|
||||||
|
public static bool RunEndHeld { get { return s_runEndHold; } }
|
||||||
|
/// <summary>정지 상한 만료 시각(unscaled · 무제한이면 −1).</summary>
|
||||||
|
public static float RunEndHoldUntil { get { return s_runEndHoldUntil; } }
|
||||||
|
/// <summary>마지막으로 정지를 푼 사유(진단).</summary>
|
||||||
|
public static string LastRunEndHoldReason = "";
|
||||||
|
|
||||||
/// <summary>진단 링버퍼 칸 수. 게임 수치가 아니라 버퍼 크기다(발주서 §1-1 「링버퍼 32」).</summary>
|
/// <summary>진단 링버퍼 칸 수. 게임 수치가 아니라 버퍼 크기다(발주서 §1-1 「링버퍼 32」).</summary>
|
||||||
const int kExitLogCapacity = 32;
|
const int kExitLogCapacity = 32;
|
||||||
static readonly ArenaExitRecord[] s_exitLog = new ArenaExitRecord[kExitLogCapacity];
|
static readonly ArenaExitRecord[] s_exitLog = new ArenaExitRecord[kExitLogCapacity];
|
||||||
|
|
@ -257,6 +285,10 @@ namespace WL.Combat.Run
|
||||||
static float s_idleSince = -1f, s_lastStallGuide = -999f;
|
static float s_idleSince = -1f, s_lastStallGuide = -999f;
|
||||||
static int s_lastRestartCount;
|
static int s_lastRestartCount;
|
||||||
|
|
||||||
|
// 813w7 §1-2 ① 결과창 중 PC 정지 — 정지 중인가 · 상한 만료 시각(−1 = 무제한)
|
||||||
|
static bool s_runEndHold;
|
||||||
|
static float s_runEndHoldUntil = -1f;
|
||||||
|
|
||||||
// 813w4 §1-2 엘리트 등록부 — 사전 할당 배열(GC 0 · 버퍼 크기지 게임 수치가 아니다)
|
// 813w4 §1-2 엘리트 등록부 — 사전 할당 배열(GC 0 · 버퍼 크기지 게임 수치가 아니다)
|
||||||
const int kMaxElites = 16;
|
const int kMaxElites = 16;
|
||||||
static readonly Actor[] s_elites = new Actor[kMaxElites];
|
static readonly Actor[] s_elites = new Actor[kMaxElites];
|
||||||
|
|
@ -321,6 +353,8 @@ namespace WL.Combat.Run
|
||||||
|
|
||||||
static void OnRunStarted(in RunStartedEvent e)
|
static void OnRunStarted(in RunStartedEvent e)
|
||||||
{
|
{
|
||||||
|
// 🔴 813w7 §1-2 ① — 런이 다시 시작됐다 = 결과창이 끝났다. PC 정지를 먼저 푼다(StopGuide 보다 앞).
|
||||||
|
EndRunEndHold("런 시작");
|
||||||
StopGuide("런 시작");
|
StopGuide("런 시작");
|
||||||
s_boss = null;
|
s_boss = null;
|
||||||
s_anchorCount = 0; // 맵이 바뀌었을 수 있다 — 앵커를 다시 읽는다
|
s_anchorCount = 0; // 맵이 바뀌었을 수 있다 — 앵커를 다시 읽는다
|
||||||
|
|
@ -403,7 +437,9 @@ namespace WL.Combat.Run
|
||||||
BeginBoss("보스 등장");
|
BeginBoss("보스 등장");
|
||||||
}
|
}
|
||||||
|
|
||||||
static void OnRunEnded(in RunResult e) { StopGuide("런 종료"); }
|
// 🔴 813w7 §1-2 ① (D-45) — 813w6 까지는 여기서 가이드만 껐다 = 결과창 동안 원본 자동전투 100%.
|
||||||
|
// Q10 실측: 결과창 100 s 사이 존0 처치 26 → 47 → 다음 런 시작 시 사냥터가 비었다.
|
||||||
|
static void OnRunEnded(in RunResult e) { StopGuide("런 종료"); BeginRunEndHold("런 종료"); }
|
||||||
|
|
||||||
static void OnDamaged(in DamagedEvent e)
|
static void OnDamaged(in DamagedEvent e)
|
||||||
{
|
{
|
||||||
|
|
@ -463,6 +499,9 @@ namespace WL.Combat.Run
|
||||||
"(쿨 " + WarpsSkippedCooldown + " 보스곁 " + WarpsSkippedNearBoss + " 맵밖 " + WarpsFailedNavMesh + ")" +
|
"(쿨 " + WarpsSkippedCooldown + " 보스곁 " + WarpsSkippedNearBoss + " 맵밖 " + WarpsFailedNavMesh + ")" +
|
||||||
// 813w4: 「다음 런」 재유도 · 정지 감시 · 엘리트 우선 타깃(D-34 · D-20 을 이 줄로 판정한다)
|
// 813w4: 「다음 런」 재유도 · 정지 감시 · 엘리트 우선 타깃(D-34 · D-20 을 이 줄로 판정한다)
|
||||||
" · 재유도 " + RestartGuides + "/정지 " + StallGuides +
|
" · 재유도 " + RestartGuides + "/정지 " + StallGuides +
|
||||||
|
// 813w7 §1-2 ①: 결과창 중 PC 정지(D-45) — QA 가 이 값으로 「빈 사냥터」 수리를 판정한다
|
||||||
|
" · 결과정지 " + RunEndHolds + "→" + RunEndHoldReleases +
|
||||||
|
"(틱 " + RunEndHoldTicks + " 타깃 " + RunEndTargetClears + ")" +
|
||||||
" · 엘리트 " + EliteCount + "(고정 " + (DSUtil.CheckNull(s_lockedElite) ? "-" : LastEliteName) +
|
" · 엘리트 " + EliteCount + "(고정 " + (DSUtil.CheckNull(s_lockedElite) ? "-" : LastEliteName) +
|
||||||
" 락 " + EliteLocks + " 재타깃 " + EliteRetargets + " 해제 " + EliteReleases + ")" +
|
" 락 " + EliteLocks + " 재타깃 " + EliteRetargets + " 해제 " + EliteReleases + ")" +
|
||||||
" · 존 " + e.zonesCleared + "/" + e.zoneCount + " " + e.zoneKills + "/" + e.zoneRequired;
|
" · 존 " + e.zonesCleared + "/" + e.zoneCount + " " + e.zoneKills + "/" + e.zoneRequired;
|
||||||
|
|
@ -542,7 +581,7 @@ namespace WL.Combat.Run
|
||||||
internal static void Tick()
|
internal static void Tick()
|
||||||
{
|
{
|
||||||
if (s_faulted) return;
|
if (s_faulted) return;
|
||||||
if (!Active) { if (s_steering) ReleaseSteer(); return; }
|
if (!Active) { if (s_runEndHold) EndRunEndHold("가이드 off"); else if (s_steering) ReleaseSteer(); return; }
|
||||||
|
|
||||||
var cfg = Cfg;
|
var cfg = Cfg;
|
||||||
float now = Time.unscaledTime;
|
float now = Time.unscaledTime;
|
||||||
|
|
@ -563,6 +602,10 @@ namespace WL.Combat.Run
|
||||||
var pc = MyValue.MyPC;
|
var pc = MyValue.MyPC;
|
||||||
if (DSUtil.CheckNull(pc) || pc.IsDead()) { ReleaseSteer(); s_lastPosTime = -1f; return; }
|
if (DSUtil.CheckNull(pc) || pc.IsDead()) { ReleaseSteer(); s_lastPosTime = -1f; return; }
|
||||||
|
|
||||||
|
// 🔴 813w7 §1-2 ①(D-45) — 결과창 동안 PC 정지. 목적지 판정보다 **앞**에 둔다
|
||||||
|
// (이 구간에는 목적지가 없고, 원본 자동전투가 존 몹을 소진하는 것을 막는 것이 전부다).
|
||||||
|
if (s_runEndHold) { RunEndHoldTick(cfg, pc, now); return; }
|
||||||
|
|
||||||
// 🔴 813w4 §1-1(D-34) 안전망 — 목적지가 없는 동안에도 「런은 도는데 PC 가 멈춰 있는지」를 본다.
|
// 🔴 813w4 §1-1(D-34) 안전망 — 목적지가 없는 동안에도 「런은 도는데 PC 가 멈춰 있는지」를 본다.
|
||||||
if (s_destKind == RunGuideDest.None && TrackIdleStall(cfg, pc, pc.Get_position(), now)) return;
|
if (s_destKind == RunGuideDest.None && TrackIdleStall(cfg, pc, pc.Get_position(), now)) return;
|
||||||
|
|
||||||
|
|
@ -654,6 +697,80 @@ namespace WL.Combat.Run
|
||||||
Log((isBoss ? "아레나" : "존 " + s_zoneIndex) + " " + why);
|
Log((isBoss ? "아레나" : "존 " + s_zoneIndex) + " " + why);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
|
// 813w7 §1-2 ① — 결과창 중 PC 정지 (D-45)
|
||||||
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 런 종료(결과창) 정지를 건다. 다음 `RunStarted`·상한 만료·가이드 off 에서 풀린다.
|
||||||
|
/// `holdPcOnRunEnd` 가 꺼져 있으면 아무 일도 하지 않는다(C8 = 813w6 그대로).
|
||||||
|
/// 프로브가 직접 부를 수 있다.
|
||||||
|
/// </summary>
|
||||||
|
public static void BeginRunEndHold(string reason)
|
||||||
|
{
|
||||||
|
var cfg = Cfg;
|
||||||
|
if (cfg == null || !cfg.holdPcOnRunEnd) return;
|
||||||
|
if (s_runEndHold) return; // 중복 안전(RunEnded 는 런당 1회지만 Abandon 경로가 겹칠 수 있다)
|
||||||
|
s_runEndHold = true;
|
||||||
|
s_runEndHoldUntil = cfg.runEndHoldMaxSeconds > 0f ? Time.unscaledTime + cfg.runEndHoldMaxSeconds : -1f;
|
||||||
|
RunEndHolds++;
|
||||||
|
Log("결과창 중 PC 정지 시작(" + reason + ") — 상한 " +
|
||||||
|
(s_runEndHoldUntil > 0f ? cfg.runEndHoldMaxSeconds.ToString("F0") + "s" : "무제한"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>런 종료 정지를 푼다(조종 원복 포함). 프로브가 직접 부를 수 있다.</summary>
|
||||||
|
public static void EndRunEndHold(string reason)
|
||||||
|
{
|
||||||
|
if (!s_runEndHold) { s_runEndHoldUntil = -1f; return; }
|
||||||
|
s_runEndHold = false;
|
||||||
|
s_runEndHoldUntil = -1f;
|
||||||
|
LastRunEndHoldReason = reason;
|
||||||
|
RunEndHoldReleases++;
|
||||||
|
|
||||||
|
// 🔴 정지 중 세워 둔 에이전트를 반드시 다시 풀어 준다 — 안 그러면 다음 런에서 PC 가 못 걷는다.
|
||||||
|
var pc0 = MyValue.MyPC;
|
||||||
|
if (!DSUtil.CheckNull(pc0))
|
||||||
|
{
|
||||||
|
var ag = pc0.Get_Agent();
|
||||||
|
if (ag != null && ag.enabled && ag.isOnNavMesh) ag.isStopped = false;
|
||||||
|
}
|
||||||
|
ReleaseSteer(); // JoystickStatus 를 우리가 1 로 만든 경우에만 3(=0)으로 되돌린다
|
||||||
|
Log("결과창 중 PC 정지 해제 — " + reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 정지 1틱 — 조종을 잡고(자동 교전 접근 루프 차단) 타깃을 떼고(자동 스킬 차단) 잔여 경로를 지운다.
|
||||||
|
/// 🔴 GC 0 — 문자열·배열 할당 없음(로그는 시작·해제에서만).
|
||||||
|
/// 프로브가 가짜 시각으로 그대로 부를 수 있다(pc 는 살아 있는 참조여야 한다).
|
||||||
|
/// </summary>
|
||||||
|
public static void RunEndHoldTick(WLRunGuideSettings cfg, PCActor pc, float now)
|
||||||
|
{
|
||||||
|
if (cfg == null || !cfg.holdPcOnRunEnd) { EndRunEndHold("스위치 off"); return; }
|
||||||
|
if (s_runEndHoldUntil > 0f && now >= s_runEndHoldUntil) { EndRunEndHold("상한 만료"); return; }
|
||||||
|
if (DSUtil.CheckNull(pc)) return;
|
||||||
|
|
||||||
|
RunEndHoldTicks++;
|
||||||
|
|
||||||
|
// ⓐ 자동 교전 접근 루프 차단(PCActor.cs:139) — 우리가 0 → 1 로 만든 경우에만.
|
||||||
|
AcquireSteer(pc);
|
||||||
|
|
||||||
|
// ⓑ 자동 스킬 루프 차단(PCActor.cs:93 `isTarget` 조건) — 매 폴링 뗀다.
|
||||||
|
if (cfg.runEndHoldClearsTarget && pc.isTarget)
|
||||||
|
{
|
||||||
|
var t = pc.Get_Target();
|
||||||
|
if (!DSUtil.CheckNull(t)) { LastIgnoredTarget = t.name; pc.Del_Target(true); }
|
||||||
|
else pc.isTarget = false; // 참조 없이 플래그만 남은 상태 — Del_Target 은 여기서 m_Target 을 NRE 로 만진다
|
||||||
|
RunEndTargetClears++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ⓒ 런 종료 순간의 잔여 경로로 계속 걸어가지 않게 한다.
|
||||||
|
if (!cfg.runEndHoldStopsAgent) return;
|
||||||
|
var agent = pc.Get_Agent();
|
||||||
|
if (agent == null || !agent.enabled || !agent.isOnNavMesh) return;
|
||||||
|
if (agent.hasPath) agent.ResetPath();
|
||||||
|
agent.isStopped = true;
|
||||||
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
// 813w4 §1-1 — 정지 감시(D-34 안전망)
|
// 813w4 §1-1 — 정지 감시(D-34 안전망)
|
||||||
// ─────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -1004,11 +1121,7 @@ namespace WL.Combat.Run
|
||||||
if (anim == eAnim.Dodge || anim >= eAnim.Attack || pc.IsCC()) return; // 공격·스킬·회피·CC 는 원본 우선
|
if (anim == eAnim.Dodge || anim >= eAnim.Attack || pc.IsCC()) return; // 공격·스킬·회피·CC 는 원본 우선
|
||||||
if (pc.JoystickStatus == 2) return; // 스킬 이동 잠금(Whirlwind 등)
|
if (pc.JoystickStatus == 2) return; // 스킬 이동 잠금(Whirlwind 등)
|
||||||
|
|
||||||
if (!s_steering && pc.JoystickStatus == 0)
|
AcquireSteer(pc);
|
||||||
{
|
|
||||||
pc.Set_JoystickStatus(1); // 원본 자동 교전 접근 루프를 비켜 세운다(PCActor.cs:139)
|
|
||||||
s_steering = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🔴 813w2: 타깃이 남아 있으면 Play_Run() 이 `Set_Path(타깃)`(MyActor.cs:335)으로 경로를 덮어쓴다.
|
// 🔴 813w2: 타깃이 남아 있으면 Play_Run() 이 `Set_Path(타깃)`(MyActor.cs:335)으로 경로를 덮어쓴다.
|
||||||
// 보통은 바로 위 ClearTravelTarget 이 타깃을 비웠으므로 그런 일이 없지만, 원본이 같은 프레임에
|
// 보통은 바로 위 ClearTravelTarget 이 타깃을 비웠으므로 그런 일이 없지만, 원본이 같은 프레임에
|
||||||
|
|
@ -1027,6 +1140,19 @@ namespace WL.Combat.Run
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 조종을 잡는다 — 원본 자동 교전 접근 루프를 비켜 세운다(PCActor.cs:139).
|
||||||
|
/// 🔴 우리가 **0 → 1** 로 만든 경우에만 잡는다. 2(Whirlwind 등 스킬 이동 잠금)는 건드리지 않는다.
|
||||||
|
/// 813w7 §1-2 ①: 이동(Steer)과 결과창 정지(RunEndHoldTick)가 같은 진입점을 쓴다.
|
||||||
|
/// </summary>
|
||||||
|
static void AcquireSteer(PCActor pc)
|
||||||
|
{
|
||||||
|
if (s_steering || DSUtil.CheckNull(pc)) return;
|
||||||
|
if (pc.JoystickStatus != 0) return;
|
||||||
|
pc.Set_JoystickStatus(1);
|
||||||
|
s_steering = true;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>조종을 놓는다(우리가 1 로 만든 경우에만 0 으로 되돌린다).</summary>
|
/// <summary>조종을 놓는다(우리가 1 로 만든 경우에만 0 으로 되돌린다).</summary>
|
||||||
static void ReleaseSteer()
|
static void ReleaseSteer()
|
||||||
{
|
{
|
||||||
|
|
@ -1267,6 +1393,21 @@ namespace WL.Combat.Run
|
||||||
for (int i = 0; i < kMaxElites; i++) s_elites[i] = null;
|
for (int i = 0; i < kMaxElites; i++) s_elites[i] = null;
|
||||||
s_idleSince = -1f; s_lastStallGuide = -999f; s_idlePos = Vector3.zero;
|
s_idleSince = -1f; s_lastStallGuide = -999f; s_idlePos = Vector3.zero;
|
||||||
s_lastRestartCount = RunDirector.RestartCount;
|
s_lastRestartCount = RunDirector.RestartCount;
|
||||||
|
// 813w7
|
||||||
|
EndRunEndHold("진단 초기화");
|
||||||
|
RunEndHolds = RunEndHoldReleases = RunEndHoldTicks = RunEndTargetClears = 0;
|
||||||
|
LastRunEndHoldReason = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 813w7 프로브 API
|
||||||
|
/// <summary>프로브용 — 정지 상한 만료 시각을 직접 밀어 넣는다(에디트 모드에서는 Time 이 흐르지 않는다).</summary>
|
||||||
|
public static void SetRunEndHoldUntilForProbe(float t) { s_runEndHoldUntil = t; }
|
||||||
|
/// <summary>프로브용 — 진짜 RunEnded 페이로드로 핸들러를 그대로 태운다(Play 불필요).</summary>
|
||||||
|
public static void ProbeRunEnded(RunOutcome outcome)
|
||||||
|
{
|
||||||
|
var e = default(RunResult);
|
||||||
|
e.outcome = outcome; e.time = Time.unscaledTime; e.frame = Time.frameCount;
|
||||||
|
OnRunEnded(in e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 813w4 프로브 API
|
// ── 813w4 프로브 API
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,17 @@ namespace WL.Combat.Run
|
||||||
/// <summary>테스트용: 캐시를 비워 다음 접근 때 다시 Resources.Load 한다.</summary>
|
/// <summary>테스트용: 캐시를 비워 다음 접근 때 다시 Resources.Load 한다.</summary>
|
||||||
public static void ClearCache() { s_cached = null; s_lookupDone = false; }
|
public static void ClearCache() { s_cached = null; s_lookupDone = false; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 813w7 프로브용 — 임시 SO 를 잠시 Instance 자리에 끼운다(C8 진리표 · **실에셋은 읽기만** 한다).
|
||||||
|
/// null 을 넣으면 캐시가 비워져 다음 접근에서 실에셋을 다시 로드한다(= ClearCache).
|
||||||
|
/// 🔴 런타임 게임 경로에서는 절대 부르지 않는다(WLBossArenaSettings.SetInstanceForProbe 선례 그대로).
|
||||||
|
/// </summary>
|
||||||
|
public static void SetInstanceForProbe(WLRunGuideSettings so)
|
||||||
|
{
|
||||||
|
s_cached = so;
|
||||||
|
s_lookupDone = so != null;
|
||||||
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────── 전체 스위치
|
// ─────────────────────────────────────────── 전체 스위치
|
||||||
[Header("전체 스위치")]
|
[Header("전체 스위치")]
|
||||||
[Tooltip("끄면 존 이동 가이드·보스 우선 타깃이 전부 비활성 — 원본 자동전투 100%(Q4b 상태 그대로). 롤백 1순위.")]
|
[Tooltip("끄면 존 이동 가이드·보스 우선 타깃이 전부 비활성 — 원본 자동전투 100%(Q4b 상태 그대로). 롤백 1순위.")]
|
||||||
|
|
@ -263,5 +274,27 @@ namespace WL.Combat.Run
|
||||||
|
|
||||||
[Tooltip("이탈이 이어지는 동안 이 주기(초)마다 진단 줄을 더 남긴다. 0 이면 이탈 시작·워프 때만(기본).")]
|
[Tooltip("이탈이 이어지는 동안 이 주기(초)마다 진단 줄을 더 남긴다. 0 이면 이탈 시작·워프 때만(기본).")]
|
||||||
public float exitDiagRepeatSeconds = 0f;
|
public float exitDiagRepeatSeconds = 0f;
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────── 813w7 §1-2 ① 결과창 중 PC 정지(D-45)
|
||||||
|
[Header("결과창 중 PC 정지 (813w7 · D-45 결과창 동안 PC 가 존 몹을 소진)")]
|
||||||
|
[Tooltip("🔴 런이 끝나면(RunEnded) 다음 런이 시작될 때까지 PC 를 세운다 — 813w 과 같은 "
|
||||||
|
+ "Set_JoystickStatus(1)(PCActor.cs:139 이 base.Update() = 자동 교전 접근 루프를 통째로 건너뛴다) + "
|
||||||
|
+ "타깃 해제(PCActor.cs:93 자동 스킬 루프는 isTarget 이 있어야만 돈다). "
|
||||||
|
+ "Q10 D-45 실측: 결과창 100 초 동안 z0 처치가 26 → 47 로 늘어 다음 런의 사냥터가 비었다. "
|
||||||
|
+ "끄면 813w6 그대로(런 종료 = 가이드만 끄고 원본 자동전투 100%).")]
|
||||||
|
public bool holdPcOnRunEnd = true;
|
||||||
|
|
||||||
|
[Tooltip("정지 상한(초 · unscaled). 결과창을 [닫기]로 닫고 런을 다시 시작하지 않는 경우 PC 가 영영 굳지 않게 "
|
||||||
|
+ "이 시간이 지나면 스스로 푼다. 0 이면 무제한(다음 RunStarted 까지). "
|
||||||
|
+ "🔴 RunGuide 는 UI 를 보지 않는다(UI 0) — 「결과창 닫힘」은 이 상한과 다음 런 시작으로만 판정한다.")]
|
||||||
|
public float runEndHoldMaxSeconds = 120f;
|
||||||
|
|
||||||
|
[Tooltip("정지 중 매 폴링마다 PC 타깃을 뗀다(Actor.Del_Target(true)). "
|
||||||
|
+ "🔴 이것이 꺼지면 조이스틱 잠금만으로는 자동 스킬(PCActor.cs:93)이 계속 돌아 몹이 계속 죽는다.")]
|
||||||
|
public bool runEndHoldClearsTarget = true;
|
||||||
|
|
||||||
|
[Tooltip("정지 중 NavMeshAgent 의 남은 경로를 지운다(ResetPath · isStopped). 런 종료 순간의 잔여 경로로 "
|
||||||
|
+ "PC 가 계속 걸어가는 것을 막는다. 끄면 좌표만 그대로 두고 조종만 잡는다.")]
|
||||||
|
public bool runEndHoldStopsAgent = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,3 +72,11 @@ MonoBehaviour:
|
||||||
parkBossOnRunEndDeferred: 1
|
parkBossOnRunEndDeferred: 1
|
||||||
parkBossOnRunEndDelaySeconds: 0
|
parkBossOnRunEndDelaySeconds: 0
|
||||||
snapshotBossHpOnRunEnd: 1
|
snapshotBossHpOnRunEnd: 1
|
||||||
|
holdZoneSpawnOnlyAfterGate: 1
|
||||||
|
holdZoneSpawnSkipsRunZones: 1
|
||||||
|
rearmZoneSpawnsEnabled: 1
|
||||||
|
rearmZoneEmptySeconds: 8
|
||||||
|
rearmZoneCheckSeconds: 1
|
||||||
|
rearmZoneRegenMax: 15
|
||||||
|
rearmZoneRunZonesOnly: 1
|
||||||
|
rearmZoneSpawnsOnRestart: 1
|
||||||
|
|
|
||||||
|
|
@ -62,3 +62,7 @@ MonoBehaviour:
|
||||||
bossAwayFromCenterRadius: 6
|
bossAwayFromCenterRadius: 6
|
||||||
exitDiagEnabled: 1
|
exitDiagEnabled: 1
|
||||||
exitDiagRepeatSeconds: 0
|
exitDiagRepeatSeconds: 0
|
||||||
|
holdPcOnRunEnd: 1
|
||||||
|
runEndHoldMaxSeconds: 120
|
||||||
|
runEndHoldClearsTarget: 1
|
||||||
|
runEndHoldStopsAgent: 1
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue