[WL-813w2] 런 가이드 2차: 도착 판정 · 보스 홀드 · 군중 유지 · 리쉬 축소 (#813)
Q5 §4 실측(존 Arrived=0 · 아레나 이탈 46.3 m · 되돌림 3~4 vs 리셋 15~19)의 원인은 RunGuide.Poll 이 피격 중단에서 통째로 return 한 것(구 RunGuide.cs:291) — 그 return 이 도착 판정(구 :303)과 이탈 되돌림(구 :327) 위에 있었다. - D-24: 도착 판정·이탈 되돌림을 피격과 무관하게 매 폴링 실행 + 정지 판정(0.5 s · 0.3 m/s) 도착 - D-22: 되돌림 즉시 · 되돌림 중 타깃 해제 · 보스 재타깃은 Hold 에서만(락은 상시 유지) - D-19: 존 반경(18 m) 이탈 즉시 복귀 · guideLeashRadius 12 → 14 - §1-4: WLBossArenaSettings.leashHealPercent 0.15 → 0.05 (쿨 10 s 유지) - RunTick 로그(존 체류·이동 초·최대 이탈) · 신규 SO 9필드 · C8 스위치 4개 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
7494047e50
commit
5e97e26a91
|
|
@ -0,0 +1,89 @@
|
|||
# WL813w2 Probe 2026-09-09 13:25:14 (edit mode · Play 0 · 로그인 0)
|
||||
playMode=False
|
||||
|
||||
── ⓪ 준비
|
||||
[PASS] 가짜 메인 PC IsMainPC()=true · MyValue.MyPC 등록
|
||||
[PASS] RunGuide.Active(에셋 · enabled · 런 축)
|
||||
Agent=없음(에디트 모드 · Steer 는 여기서 되돌아간다)
|
||||
|
||||
── ① 에셋 · SOT (813w2 신규 9필드 · C45)
|
||||
[PASS] Resources.Load("WL/WLRunGuideSettings")
|
||||
[813w 값] zoneArriveRadius=6 zoneHoldRadius=18 guideLeashRadius=14 bossArriveRadius=10 bossHoldRadius=14 pollSeconds=0.1 resumeAfterHitSeconds=3 maxTravelSeconds=60
|
||||
[813w2 신규] hitSuspendOnlyInHold=True bossHitSuspendSeconds=0.5 arriveStillEnabled=True arriveStillSeconds=0.5 arriveStillSpeed=0.3 arriveStillMaxDistance=0 pullbackReleasesTarget=True bossRetargetOnlyInHold=True runTickLogSeconds=10
|
||||
[PASS] hitSuspendOnlyInHold=1 — 피격 중단이 도착·되돌림을 막지 않는다(D-24·D-22 원인 차단)
|
||||
[PASS] 정지 판정 도착 on(0.5s · 0.3m/s)
|
||||
[PASS] 되돌림 중 타깃 해제 + 보스 재타깃은 Hold 에서만
|
||||
[PASS] 보스 피격 중단(0.5s) < 존 피격 중단(3s) — Q5 D-22 처방
|
||||
[PASS] guideLeashRadius(14) < zoneHoldRadius(18) — 존 안의 적은 버리지 않는다(D-19)
|
||||
[PASS] WLBossArenaSettings 로드
|
||||
leashHealPercent=0.05 leashHealCooldownSeconds=10 arenaRadius=20 playerLeaveMargin=4 playerLeaveGraceSeconds=1.5
|
||||
[PASS] 리쉬 회복 0.15 → **0.05**(§1-4)
|
||||
[PASS] 쿨다운 10 s 유지(§1-4)
|
||||
[PASS] bossHoldRadius(14) < 리쉬 발동선(24)
|
||||
|
||||
── ② D-24 도착 판정 (Q5 §4-2 「존 Arrived=0 · State=Travel 고착」)
|
||||
목적지=(147.0,2.6,30.0) · 시작 상태=Travel
|
||||
[PASS] 피격 중 · 40 m — Travel 유지(도착 아님)
|
||||
[PASS] 🔴 **피격 중에도 도착 → Hold**(813w = Travel 고착 · D-24 원인)
|
||||
[PASS] Arrived=1 (실측 1) · y 차이 25 m 는 무시(XZ 평면 판정)
|
||||
LastLog=존 1 도착 — 1.0m · 자동전투 재개
|
||||
[PASS] 정지 판정: 0.0s=False 0.3s=False 0.6s=True (arriveStillSeconds=0.5)
|
||||
[PASS] 다시 움직이면 정지 타이머 리셋(still=-1.0)
|
||||
[PASS] 정지 도착은 목적지 반경 안에서만 인정(12 m ok · 40 m 거부)
|
||||
[PASS] maxTravelSeconds(60s) 초과 → Hold · GaveUp=1(이탈 감시는 유지)
|
||||
|
||||
── ③ D-22 보스 홀드 실효화 (Q5 실측 이탈 46.3 m · 되돌림 3~4회 vs 리셋 15~19회)
|
||||
아레나 중심=(139.0,0.8,-20.0) · [PASS] 도착(Hold)
|
||||
[PASS] 이동(Travel) 중에는 보스 재타깃 0 — 우리 목적지를 덮어쓰지 않는다
|
||||
[PASS] Hold — 타깃 = 보스(BossRetargets=1)
|
||||
[PASS] 원본 락타깃 m_IsLockTarget=true(BossLocks=1)
|
||||
[PASS] 🔴 **피격 중에도 이탈 16.0m 에서 즉시 되돌림**(813w = 되돌림 0 · D-22 원인)
|
||||
[PASS] HoldPullbacks=1 · 그중 피격 중=1
|
||||
[PASS] 되돌림 중 타깃 해제(보스 타깃도 놓는다 · pullbackReleasesTarget)
|
||||
[PASS] 그래도 보스 락은 유지 → 잡몹이 타깃을 가져갈 수 없다
|
||||
[PASS] 최대 이탈 실측 16.0m (리쉬 발동선 24 m 미만)
|
||||
LastLog=아레나 이탈 16.0m — 다시 유도(피격 중)
|
||||
[PASS] 복귀 → Hold
|
||||
[PASS] Hold 복귀 즉시 타깃 = 보스로 복원
|
||||
|
||||
── ④ 보스 생존 중 잡몹 타깃 금지 (§1-2)
|
||||
[PASS] 잡몹이 타깃을 가져간 상황을 만든다(__WL813w2_Mob)
|
||||
[PASS] 다음 폴링(0.1 s)에 보스로 복귀 · BossRetargets 2 → 3
|
||||
[PASS] 가이드 종료 시 보스 락 해제(원본 타깃 선택 복귀)
|
||||
|
||||
── ⑤ D-19 군중 유지 — 존 안에서는 존 반경 밖으로 나가지 않는다
|
||||
[PASS] 앵커 위 = Hold(자동전투 재개)
|
||||
[PASS] 반경 안(12m < guideLeashRadius 14) 타깃은 유지 — 원본이 계속 싸운다
|
||||
[PASS] 반경 밖 타깃은 무시(Del_Target · FarTargetIgnored 0 → 1)
|
||||
[PASS] 피격 중에는 먼 타깃도 유지 = 즉시 응전(이탈 되돌림은 그대로 돈다)
|
||||
[PASS] 존 이탈 23m — 피격 중에도 즉시 되돌림(HoldPullbacks=1)
|
||||
체류 계측: 존체류=0.00s · 이동=0.00s · 최대이탈=23.0m (에디트 모드는 Time 이 멈춰 있어 초는 0 · 인게임에서 RunTick 로그로 남는다)
|
||||
|
||||
── ⑥ 보스 구간 240 s 시뮬 — 리쉬 리셋 0 (§1-4)
|
||||
드리프트 4 m/s · 되돌림 5 m/s · 240 s · 판정 800회
|
||||
[PASS] 최대 이탈 14.4 m < 리쉬 발동선 24 m (Q5 실측 46.3 m)
|
||||
[PASS] 🔴 **리쉬 리셋 0회**(Q5 실측 15~19회)
|
||||
[PASS] 대조군(되돌림 0) 리셋 30회 — 판정기가 실제로 작동한다
|
||||
|
||||
── ⑦ 리쉬 회복 축소 0.15 → 0.05 · 쿨다운 10 s 유지 (§1-4)
|
||||
[PASS] 1회차 = 부분 회복(+5%)
|
||||
[PASS] 4 s 뒤 = 쿨다운으로 건너뜀
|
||||
[PASS] 11 s 뒤 = 다시 회복
|
||||
최악 회복 총량(240 s · 쿨다운마다) = 120% (0.15 였다면 360%)
|
||||
[PASS] 리셋이 남더라도 회복 총량이 보스 HP 1.5배 미만
|
||||
[PASS] C8: percent>=1 → 813d 만피 리셋 그대로
|
||||
|
||||
── ⑧ GC 0 (정상 틱 경로)
|
||||
[PASS] ForceTick ×100 할당 = 0 B
|
||||
[PASS] 대조군(64 KB 할당) = 69632 B — 측정기가 살아 있다
|
||||
|
||||
── ⑨ C8 — 813w2 스위치를 끄면 813w 동작 그대로 · 전체를 끄면 무동작
|
||||
[PASS] 813w(hitSuspendOnlyInHold=0): Travel=True Hold=True → **이동 중에도 멈춘다 = 도착 판정·되돌림이 폴링에서 빠진다**(D-24·D-22 원인)
|
||||
[PASS] 813w2(=1): Travel=False Hold=True → 이동·되돌림은 안 멈추고, 제자리 응전만 남는다
|
||||
[PASS] 안 맞으면 언제나 false
|
||||
[PASS] arriveStillEnabled=0 → 정지 판정 도착 없음(반경 판정만 = 813w)
|
||||
[PASS] RuntimeDisabled=1 → 틱 ×10 에 상태 불변(Travel) · 조종 0 · 타깃 무수정
|
||||
[PASS] JoystickStatus=0(원본 자동전투 100%)
|
||||
[PASS] 실에셋 무수정 확인 — hitSuspendOnlyInHold=True arriveStillEnabled=True pullbackReleasesTarget=True
|
||||
|
||||
RESULT PASS
|
||||
|
|
@ -0,0 +1,524 @@
|
|||
// WL813w2_Probe.cs — #813w2(런 가이드 2차: 도착 판정 D-24 · 보스 홀드 D-22 · 군중 유지 D-19 · 리쉬 축소) 프로브
|
||||
// 에디트 모드 전용 · Play 불필요 · 로그인 0 · 씬/에셋 무수정(임시 GameObject·임시 SO 는 즉시 파괴/회수)
|
||||
//
|
||||
// 상주 에디터:
|
||||
// unity command run_script --project-path <wt> --file AgentScripts/WL813w2_Probe.cs --entry WL813w2_Probe.RunAll --timeout 300
|
||||
// 결과: Console + AgentScripts/WL813w2_PROBE.txt (RESULT PASS / RESULT FAIL n)
|
||||
//
|
||||
// 무엇을 증명하나 (발주서 WL-813w2 §1-1~§1-5 · 근거 = qa/W2-5_재채점_v3.md §4)
|
||||
// ① 에셋·SOT — 813w2 신규 9필드 · 값 · 리쉬 0.05 · bossHoldRadius < 리쉬 발동선
|
||||
// ② D-24 도착 — 🔴 **피격 중에도 도착 판정이 돈다**(813w 은 여기서 통째로 return 했다) ·
|
||||
// XZ 평면 판정(y 차이 무시) · 정지 판정 도착 · maxTravelSeconds 초과 → Hold
|
||||
// ③ D-22 보스홀드 — 🔴 **피격 중에도 이탈 되돌림이 즉시** · 되돌림 중 타깃 해제 ·
|
||||
// 보스 재타깃은 Hold 에서만(락은 이동 중에도 유지)
|
||||
// ④ 잡몹 타깃 금지 — 보스 생존 중 Hold 에서 잡몹 타깃 → 다음 폴링에 보스로 복귀
|
||||
// ⑤ D-19 군중 유지 — 존 Hold 에서 zoneHoldRadius 밖 = 즉시 되돌림 · 반경 안 타깃은 유지(교전 계속)
|
||||
// ⑥ 리쉬 0 시뮬 — 보스 구간 240 s 를 0.1 s 스텝으로 돌려 BossArena.EvaluateLeash 리셋 **0** 확인
|
||||
// ⑦ 리쉬 회복 — EvaluateLeashHeal 판정(부분 → 쿨다운 → 쿨다운 뒤 재회복) · 0.05
|
||||
// ⑧ GC 0 — ForceTick ×100 이 0 B(대조군으로 측정기 동작 확인)
|
||||
// ⑨ C8 롤백 — 813w2 스위치 4개를 끄면 813w 동작(피격 중 도착·되돌림 0) 그대로 · RuntimeDisabled → 무동작
|
||||
//
|
||||
// 에디트 모드 한계(보고서 「미확인」)
|
||||
// · NavMesh/NavMeshAgent 가 없어 Steer() 는 에이전트 검사에서 되돌아간다 → 실이동은 못 잰다.
|
||||
// 상태 전이·판정은 PC transform 을 직접 옮겨 검증한다(원본 Get_position() 이 transform 이다).
|
||||
// · Time.unscaledTime 이 스크립트 실행 중에는 멈춰 있다 → 시간 의존 판정은 순수 함수
|
||||
// (RunGuide.StepStillDetect · BossArena.EvaluateLeash/EvaluateLeashHeal)에 가짜 시각을 먹여 검증한다.
|
||||
// 그래서 「피격 중」 상태는 프로브 내내 유지된다 = ②③ 이 노린 조건 그대로다.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using WL.Combat.Boss;
|
||||
using WL.Combat.Run;
|
||||
|
||||
public static class WL813w2_Probe
|
||||
{
|
||||
const string OutCommit = "AgentScripts/WL813w2_PROBE.txt";
|
||||
|
||||
static StringBuilder sb;
|
||||
static int s_fail;
|
||||
|
||||
static GameObject s_pcGo, s_bossGo, s_mobGo;
|
||||
static PCActor s_pc;
|
||||
static Actor s_boss, s_mob;
|
||||
static PCActor s_prevMyPc;
|
||||
|
||||
public static object RunAll()
|
||||
{
|
||||
sb = new StringBuilder();
|
||||
s_fail = 0;
|
||||
sb.AppendLine("# WL813w2 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_Arrive();
|
||||
Sec3_BossHold();
|
||||
Sec4_NoMobTarget();
|
||||
Sec5_ZoneHold();
|
||||
Sec6_LeashZeroSim();
|
||||
Sec7_LeashHeal();
|
||||
Sec8_GC();
|
||||
Sec9_C8();
|
||||
}
|
||||
catch (Exception ex) { L("[FAIL] 예외 — " + ex.GetType().Name + ": " + ex.Message); L(ex.StackTrace); s_fail++; }
|
||||
finally { Teardown(); }
|
||||
|
||||
L("");
|
||||
L(s_fail == 0 ? "RESULT PASS" : "RESULT FAIL " + s_fail);
|
||||
var text = sb.ToString();
|
||||
try { File.WriteAllText(OutCommit, text, new UTF8Encoding(false)); } catch { }
|
||||
Debug.Log(text);
|
||||
return text;
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────── 유틸
|
||||
static void L(string m) { sb.AppendLine(m); }
|
||||
static string P(bool ok) { if (!ok) s_fail++; return ok ? "[PASS]" : "[FAIL]"; }
|
||||
static long Alloc() { return GC.GetTotalMemory(false); }
|
||||
static string V(Vector3 v) { return "(" + v.x.ToString("F1") + "," + v.y.ToString("F1") + "," + v.z.ToString("F1") + ")"; }
|
||||
static float DXZ(Vector3 a, Vector3 b) { float dx = a.x - b.x, dz = a.z - b.z; return Mathf.Sqrt(dx * dx + dz * dz); }
|
||||
|
||||
static void Setup()
|
||||
{
|
||||
s_prevMyPc = MyValue.MyPC;
|
||||
|
||||
s_pcGo = new GameObject("__WL813w2_PC");
|
||||
s_pc = s_pcGo.AddComponent<PCActor>();
|
||||
s_pc.m_Role = eRole.PC; s_pc.m_SubRole = eSubRol.None;
|
||||
MyValue.MyPC = s_pc;
|
||||
|
||||
s_bossGo = new GameObject("__WL813w2_Boss");
|
||||
s_boss = s_bossGo.AddComponent<Actor>();
|
||||
s_boss.m_Role = eRole.Mob; s_boss.m_SubRole = eSubRol.Boss;
|
||||
|
||||
s_mobGo = new GameObject("__WL813w2_Mob");
|
||||
s_mob = s_mobGo.AddComponent<Actor>();
|
||||
s_mob.m_Role = eRole.Mob; s_mob.m_SubRole = eSubRol.None;
|
||||
|
||||
RunGuide.ResetDiagnostics();
|
||||
WLRunGuideSettings.RuntimeDisabled = false;
|
||||
|
||||
L("");
|
||||
L("── ⓪ 준비");
|
||||
L(" " + P(s_pc.IsMainPC()) + " 가짜 메인 PC IsMainPC()=true · MyValue.MyPC 등록");
|
||||
L(" " + P(RunGuide.Active) + " RunGuide.Active(에셋 · enabled · 런 축)");
|
||||
L(" Agent=" + (s_pc.Get_Agent() == null ? "없음(에디트 모드 · Steer 는 여기서 되돌아간다)" : "있음"));
|
||||
}
|
||||
|
||||
static void Teardown()
|
||||
{
|
||||
try
|
||||
{
|
||||
RunGuide.StopGuide("probe teardown");
|
||||
RunGuide.ResetDiagnostics();
|
||||
RunGuide.SetSteeringForProbe(false);
|
||||
WLRunGuideSettings.RuntimeDisabled = false;
|
||||
MyValue.MyPC = s_prevMyPc;
|
||||
Kill(s_pcGo, s_bossGo, s_mobGo);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
static void Kill(params GameObject[] gos)
|
||||
{
|
||||
for (int i = 0; i < gos.Length; i++)
|
||||
if (gos[i] != null) UnityEngine.Object.DestroyImmediate(gos[i]);
|
||||
}
|
||||
|
||||
static void MovePc(Vector3 p) { s_pcGo.transform.position = p; }
|
||||
|
||||
// ───────────────────────────────────────── ① 에셋 · SOT
|
||||
static void Sec1_Asset()
|
||||
{
|
||||
L("");
|
||||
L("── ① 에셋 · SOT (813w2 신규 9필드 · C45)");
|
||||
var c = WLRunGuideSettings.Instance;
|
||||
L(" " + P(c != null) + " Resources.Load(\"" + WLRunGuideSettings.ResourcesPath + "\")");
|
||||
if (c == null) return;
|
||||
|
||||
L(" [813w 값] zoneArriveRadius=" + c.zoneArriveRadius + " zoneHoldRadius=" + c.zoneHoldRadius +
|
||||
" guideLeashRadius=" + c.guideLeashRadius + " bossArriveRadius=" + c.bossArriveRadius +
|
||||
" bossHoldRadius=" + c.bossHoldRadius + " pollSeconds=" + c.pollSeconds +
|
||||
" resumeAfterHitSeconds=" + c.resumeAfterHitSeconds + " maxTravelSeconds=" + c.maxTravelSeconds);
|
||||
L(" [813w2 신규] hitSuspendOnlyInHold=" + c.hitSuspendOnlyInHold +
|
||||
" bossHitSuspendSeconds=" + c.bossHitSuspendSeconds +
|
||||
" arriveStillEnabled=" + c.arriveStillEnabled + " arriveStillSeconds=" + c.arriveStillSeconds +
|
||||
" arriveStillSpeed=" + c.arriveStillSpeed + " arriveStillMaxDistance=" + c.arriveStillMaxDistance +
|
||||
" pullbackReleasesTarget=" + c.pullbackReleasesTarget +
|
||||
" bossRetargetOnlyInHold=" + c.bossRetargetOnlyInHold + " runTickLogSeconds=" + c.runTickLogSeconds);
|
||||
|
||||
L(" " + P(c.hitSuspendOnlyInHold) + " hitSuspendOnlyInHold=1 — 피격 중단이 도착·되돌림을 막지 않는다(D-24·D-22 원인 차단)");
|
||||
L(" " + P(c.arriveStillEnabled && c.arriveStillSeconds > 0f) + " 정지 판정 도착 on(" + c.arriveStillSeconds + "s · " + c.arriveStillSpeed + "m/s)");
|
||||
L(" " + P(c.pullbackReleasesTarget && c.bossRetargetOnlyInHold) + " 되돌림 중 타깃 해제 + 보스 재타깃은 Hold 에서만");
|
||||
L(" " + P(c.bossHitSuspendSeconds < c.resumeAfterHitSeconds) + " 보스 피격 중단(" + c.bossHitSuspendSeconds +
|
||||
"s) < 존 피격 중단(" + c.resumeAfterHitSeconds + "s) — Q5 D-22 처방");
|
||||
L(" " + P(c.guideLeashRadius < c.zoneHoldRadius) + " guideLeashRadius(" + c.guideLeashRadius +
|
||||
") < zoneHoldRadius(" + c.zoneHoldRadius + ") — 존 안의 적은 버리지 않는다(D-19)");
|
||||
|
||||
var b = WLBossArenaSettings.Instance;
|
||||
L(" " + P(b != null) + " WLBossArenaSettings 로드");
|
||||
if (b == null) return;
|
||||
L(" leashHealPercent=" + b.leashHealPercent + " leashHealCooldownSeconds=" + b.leashHealCooldownSeconds +
|
||||
" arenaRadius=" + b.arenaRadius + " playerLeaveMargin=" + b.playerLeaveMargin +
|
||||
" playerLeaveGraceSeconds=" + b.playerLeaveGraceSeconds);
|
||||
L(" " + P(Mathf.Approximately(b.leashHealPercent, 0.05f)) + " 리쉬 회복 0.15 → **0.05**(§1-4)");
|
||||
L(" " + P(Mathf.Approximately(b.leashHealCooldownSeconds, 10f)) + " 쿨다운 10 s 유지(§1-4)");
|
||||
float leashLine = b.arenaRadius + b.playerLeaveMargin;
|
||||
L(" " + P(c.bossHoldRadius < leashLine) + " bossHoldRadius(" + c.bossHoldRadius + ") < 리쉬 발동선(" + leashLine + ")");
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────── ② D-24 도착 판정
|
||||
static void Sec2_Arrive()
|
||||
{
|
||||
L("");
|
||||
L("── ② D-24 도착 판정 (Q5 §4-2 「존 Arrived=0 · State=Travel 고착」)");
|
||||
var c = WLRunGuideSettings.Instance;
|
||||
if (c == null) return;
|
||||
|
||||
Vector3 anchor;
|
||||
if (!RunGuide.TryGetAnchor(c, 1, out anchor)) { L(" [FAIL] 존1 앵커 없음"); s_fail++; return; }
|
||||
|
||||
// ── ②-1 피격 중에도 도착 판정이 돈다 (813w 은 여기서 return 했다)
|
||||
RunGuide.ResetDiagnostics();
|
||||
MovePc(anchor + new Vector3(40f, 0f, 0f));
|
||||
RunGuide.BeginZone(1, "probe");
|
||||
L(" 목적지=" + V(RunGuide.Destination) + " · 시작 상태=" + RunGuide.State);
|
||||
|
||||
RunGuide.SimulateHit(); // 에디트 모드에선 Time 이 멈춰 있어 「피격 중」이 계속된다
|
||||
RunGuide.ForceTick();
|
||||
bool hitStillTravel = RunGuide.State == RunGuideState.Travel;
|
||||
L(" " + P(hitStillTravel) + " 피격 중 · 40 m — Travel 유지(도착 아님)");
|
||||
|
||||
// 앵커 위로 옮긴다(y 는 크게 다르게 둔다 = XZ 판정 확인)
|
||||
MovePc(new Vector3(anchor.x + 1.0f, anchor.y + 25f, anchor.z));
|
||||
RunGuide.ForceTick();
|
||||
L(" " + P(RunGuide.State == RunGuideState.Hold) + " 🔴 **피격 중에도 도착 → Hold**(813w = Travel 고착 · D-24 원인)");
|
||||
L(" " + P(RunGuide.Arrived == 1) + " Arrived=1 (실측 " + RunGuide.Arrived + ") · y 차이 25 m 는 무시(XZ 평면 판정)");
|
||||
L(" LastLog=" + RunGuide.LastLog);
|
||||
|
||||
// ── ②-2 정지 판정 도착 (순수 함수 · 가짜 시각)
|
||||
float still = -1f;
|
||||
bool a0 = RunGuide.StepStillDetect(c, 0.01f, 0.1f, 10.0f, ref still); // 정지 시작
|
||||
bool a1 = RunGuide.StepStillDetect(c, 0.01f, 0.1f, 10.3f, ref still); // 0.3 s — 아직
|
||||
bool a2 = RunGuide.StepStillDetect(c, 0.01f, 0.1f, 10.6f, ref still); // 0.6 s — 도착
|
||||
L(" " + P(!a0 && !a1 && a2) + " 정지 판정: 0.0s=" + a0 + " 0.3s=" + a1 + " 0.6s=" + a2 +
|
||||
" (arriveStillSeconds=" + c.arriveStillSeconds + ")");
|
||||
bool a3 = RunGuide.StepStillDetect(c, 2.0f, 0.1f, 10.7f, ref still); // 20 m/s = 이동 중
|
||||
L(" " + P(!a3 && still < 0f) + " 다시 움직이면 정지 타이머 리셋(still=" + still.ToString("F1") + ")");
|
||||
L(" " + P(RunGuide.WithinStillRange(c, 12f, 18f) && !RunGuide.WithinStillRange(c, 40f, 18f)) +
|
||||
" 정지 도착은 목적지 반경 안에서만 인정(12 m ok · 40 m 거부)");
|
||||
|
||||
// ── ②-3 maxTravelSeconds 초과 → Hold(이탈 감시 유지)
|
||||
RunGuide.ResetDiagnostics();
|
||||
MovePc(anchor + new Vector3(40f, 0f, 0f));
|
||||
RunGuide.BeginZone(1, "probe timeout");
|
||||
RunGuide.SetTravelStartForProbe(Time.unscaledTime - (c.maxTravelSeconds + 1f));
|
||||
RunGuide.ForceTick();
|
||||
L(" " + P(RunGuide.State == RunGuideState.Hold && RunGuide.GaveUp == 1) +
|
||||
" maxTravelSeconds(" + c.maxTravelSeconds + "s) 초과 → Hold · GaveUp=" + RunGuide.GaveUp + "(이탈 감시는 유지)");
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────── ③ D-22 보스 홀드
|
||||
static void Sec3_BossHold()
|
||||
{
|
||||
L("");
|
||||
L("── ③ D-22 보스 홀드 실효화 (Q5 실측 이탈 46.3 m · 되돌림 3~4회 vs 리셋 15~19회)");
|
||||
var c = WLRunGuideSettings.Instance;
|
||||
if (c == null) return;
|
||||
|
||||
RunGuide.ResetDiagnostics();
|
||||
Vector3 center = BossArena.Center;
|
||||
s_bossGo.transform.position = center;
|
||||
RunGuide.SetBossForProbe(s_boss);
|
||||
|
||||
MovePc(center);
|
||||
RunGuide.BeginBoss("probe");
|
||||
RunGuide.ForceTick(); // 중심 = 도착(이 틱에서는 아직 Travel → 재타깃 없음)
|
||||
L(" 아레나 중심=" + V(center) + " · " + P(RunGuide.State == RunGuideState.Hold) + " 도착(Hold)");
|
||||
L(" " + P(RunGuide.BossRetargets == 0) + " 이동(Travel) 중에는 보스 재타깃 0 — 우리 목적지를 덮어쓰지 않는다");
|
||||
|
||||
RunGuide.ForceTick(); // Hold 에서 첫 폴링 → 보스 타깃
|
||||
bool lockedInHold = !DSUtil.CheckNull(s_pc.Get_Target()) && ReferenceEquals(s_pc.Get_Target(), s_boss);
|
||||
L(" " + P(lockedInHold) + " Hold — 타깃 = 보스(BossRetargets=" + RunGuide.BossRetargets + ")");
|
||||
L(" " + P(s_boss.m_IsLockTarget) + " 원본 락타깃 m_IsLockTarget=true(BossLocks=" + RunGuide.BossLocks + ")");
|
||||
|
||||
// 🔴 피격 중에 아레나 밖으로 밀려난다 → 즉시 되돌림이 걸려야 한다
|
||||
RunGuide.SimulateHit();
|
||||
MovePc(center + new Vector3(c.bossHoldRadius + 2f, 0f, 0f)); // 16 m — 홀드(14) 밖 · 리쉬선(24) 안
|
||||
RunGuide.ForceTick();
|
||||
L(" " + P(RunGuide.State == RunGuideState.Travel) + " 🔴 **피격 중에도 이탈 " +
|
||||
(c.bossHoldRadius + 2f).ToString("F1") + "m 에서 즉시 되돌림**(813w = 되돌림 0 · D-22 원인)");
|
||||
L(" " + P(RunGuide.HoldPullbacks == 1 && RunGuide.PullbacksWhileHit == 1) +
|
||||
" HoldPullbacks=" + RunGuide.HoldPullbacks + " · 그중 피격 중=" + RunGuide.PullbacksWhileHit);
|
||||
L(" " + P(!s_pc.isTarget) + " 되돌림 중 타깃 해제(보스 타깃도 놓는다 · pullbackReleasesTarget)");
|
||||
L(" " + P(s_boss.m_IsLockTarget) + " 그래도 보스 락은 유지 → 잡몹이 타깃을 가져갈 수 없다");
|
||||
L(" " + P(RunGuide.MaxDistFromDest >= c.bossHoldRadius) + " 최대 이탈 실측 " +
|
||||
RunGuide.MaxDistFromDest.ToString("F1") + "m (리쉬 발동선 24 m 미만)");
|
||||
L(" LastLog=" + RunGuide.LastLog);
|
||||
|
||||
// 되돌아오면 다시 Hold + 보스 타깃 복귀
|
||||
MovePc(center + new Vector3(2f, 0f, 0f));
|
||||
RunGuide.ForceTick();
|
||||
L(" " + P(RunGuide.State == RunGuideState.Hold) + " 복귀 → Hold");
|
||||
RunGuide.ForceTick();
|
||||
L(" " + P(ReferenceEquals(s_pc.Get_Target(), s_boss)) + " Hold 복귀 즉시 타깃 = 보스로 복원");
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────── ④ 보스 생존 중 잡몹 타깃 금지
|
||||
static void Sec4_NoMobTarget()
|
||||
{
|
||||
L("");
|
||||
L("── ④ 보스 생존 중 잡몹 타깃 금지 (§1-2)");
|
||||
Vector3 center = BossArena.Center;
|
||||
s_mobGo.transform.position = center + new Vector3(3f, 0f, 0f);
|
||||
|
||||
// Hold 상태에서 원본 경로가 잡몹으로 타깃을 바꿔치기한 상황을 만든다
|
||||
s_pc.Change_Target(true, s_mob);
|
||||
bool stolen = ReferenceEquals(s_pc.Get_Target(), s_mob);
|
||||
int before = RunGuide.BossRetargets;
|
||||
RunGuide.ForceTick();
|
||||
bool back = ReferenceEquals(s_pc.Get_Target(), s_boss);
|
||||
L(" " + P(stolen) + " 잡몹이 타깃을 가져간 상황을 만든다(" + s_mob.name + ")");
|
||||
L(" " + P(back && RunGuide.BossRetargets == before + 1) +
|
||||
" 다음 폴링(0.1 s)에 보스로 복귀 · BossRetargets " + before + " → " + RunGuide.BossRetargets);
|
||||
|
||||
// 보스가 죽으면 락·우선 타깃을 놓는다(원본 자동전투 복귀)
|
||||
s_boss.m_IsLockTarget = true;
|
||||
RunGuide.StopGuide("probe boss done");
|
||||
L(" " + P(!s_boss.m_IsLockTarget) + " 가이드 종료 시 보스 락 해제(원본 타깃 선택 복귀)");
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────── ⑤ D-19 군중 유지 (존 반경 이탈 0)
|
||||
static void Sec5_ZoneHold()
|
||||
{
|
||||
L("");
|
||||
L("── ⑤ D-19 군중 유지 — 존 안에서는 존 반경 밖으로 나가지 않는다");
|
||||
var c = WLRunGuideSettings.Instance;
|
||||
if (c == null) return;
|
||||
|
||||
Vector3 anchor;
|
||||
if (!RunGuide.TryGetAnchor(c, 1, out anchor)) { L(" [FAIL] 앵커 없음"); s_fail++; return; }
|
||||
|
||||
RunGuide.ResetDiagnostics();
|
||||
MovePc(anchor);
|
||||
RunGuide.BeginZone(1, "probe zone hold");
|
||||
RunGuide.ForceTick();
|
||||
L(" " + P(RunGuide.State == RunGuideState.Hold) + " 앵커 위 = Hold(자동전투 재개)");
|
||||
|
||||
// 존 반경 안의 적은 유지한다(교전 계속 = 화면 안 적 수)
|
||||
RunGuide.ClearHitSuspendForProbe();
|
||||
s_mobGo.transform.position = anchor + new Vector3(c.guideLeashRadius - 2f, 0f, 0f);
|
||||
s_pc.Change_Target(true, s_mob);
|
||||
RunGuide.ForceTick();
|
||||
L(" " + P(ReferenceEquals(s_pc.Get_Target(), s_mob)) + " 반경 안(" + (c.guideLeashRadius - 2f).ToString("F0") +
|
||||
"m < guideLeashRadius " + c.guideLeashRadius + ") 타깃은 유지 — 원본이 계속 싸운다");
|
||||
|
||||
// 반경 밖 적은 무시한다(따라 나가지 않는다)
|
||||
s_mobGo.transform.position = anchor + new Vector3(c.guideLeashRadius + 6f, 0f, 0f);
|
||||
s_pc.Change_Target(true, s_mob);
|
||||
int fi = RunGuide.FarTargetIgnored;
|
||||
RunGuide.ForceTick();
|
||||
L(" " + P(!s_pc.isTarget && RunGuide.FarTargetIgnored == fi + 1) +
|
||||
" 반경 밖 타깃은 무시(Del_Target · FarTargetIgnored " + fi + " → " + RunGuide.FarTargetIgnored + ")");
|
||||
|
||||
// 피격 중에는 먼 타깃 무시를 쉬어 준다 = 나를 때린 적에게 즉시 응전(반경 밖으로는 어차피 못 나간다)
|
||||
s_pc.Change_Target(true, s_mob);
|
||||
RunGuide.SimulateHit();
|
||||
RunGuide.ForceTick();
|
||||
L(" " + P(ReferenceEquals(s_pc.Get_Target(), s_mob)) +
|
||||
" 피격 중에는 먼 타깃도 유지 = 즉시 응전(이탈 되돌림은 그대로 돈다)");
|
||||
|
||||
// 존 반경 밖으로 밀려나면 피격 중이라도 즉시 되돌림
|
||||
MovePc(anchor + new Vector3(c.zoneHoldRadius + 5f, 0f, 0f));
|
||||
RunGuide.ForceTick();
|
||||
L(" " + P(RunGuide.State == RunGuideState.Travel && RunGuide.HoldPullbacks == 1) +
|
||||
" 존 이탈 " + (c.zoneHoldRadius + 5f).ToString("F0") + "m — 피격 중에도 즉시 되돌림(HoldPullbacks=" +
|
||||
RunGuide.HoldPullbacks + ")");
|
||||
L(" 체류 계측: 존체류=" + RunGuide.ZoneHoldSeconds.ToString("F2") + "s · 이동=" +
|
||||
RunGuide.TravelSeconds.ToString("F2") + "s · 최대이탈=" + RunGuide.MaxDistFromDest.ToString("F1") +
|
||||
"m (에디트 모드는 Time 이 멈춰 있어 초는 0 · 인게임에서 RunTick 로그로 남는다)");
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────── ⑥ 리쉬 0 시뮬 (보스 구간 240 s)
|
||||
static void Sec6_LeashZeroSim()
|
||||
{
|
||||
L("");
|
||||
L("── ⑥ 보스 구간 240 s 시뮬 — 리쉬 리셋 0 (§1-4)");
|
||||
var c = WLRunGuideSettings.Instance;
|
||||
var b = WLBossArenaSettings.Instance;
|
||||
if (c == null || b == null) return;
|
||||
|
||||
// 813w2 홀드 규칙만으로 PC 궤적을 만든다(NavMesh 없이 순수 계산):
|
||||
// Hold = 원본 자동전투가 잡몹 쪽으로 끌고 나간다 → 바깥으로 drift
|
||||
// Travel(되돌림) = 우리가 중심으로 민다 → 안쪽으로 pull
|
||||
// 실측 근거: Q5 는 「되돌림이 3~4회뿐」이라 drift 가 46 m 까지 갔다. 되돌림이 매 폴링 걸리면
|
||||
// PC 는 bossHoldRadius 근처에서 톱니 모양으로 붙잡힌다 → 리쉬 발동선 24 m 에 닿지 않는다.
|
||||
const float step = 0.1f; // 폴링 주기
|
||||
const float drift = 4.0f; // 잡몹 추격으로 바깥으로 끌리는 속도(m/s · 보수적으로 크게)
|
||||
const float pull = 5.0f; // 되돌림 이동 속도(m/s · PC 이동속도 하한)
|
||||
float d = 0f, maxD = 0f;
|
||||
bool traveling = false;
|
||||
int resets = 0, ticks = 0;
|
||||
BossArena.ResetLeashState();
|
||||
Vector3 center = BossArena.Center;
|
||||
|
||||
for (float t = 0f; t < 240f; t += step)
|
||||
{
|
||||
// 813w2 상태 기계와 같은 규칙
|
||||
if (!traveling && d > c.bossHoldRadius) traveling = true; // 이탈 → 되돌림(피격 무관)
|
||||
else if (traveling && d <= c.bossArriveRadius) traveling = false; // 도착 → Hold
|
||||
|
||||
d += (traveling ? -pull : drift) * step;
|
||||
if (d < 0f) d = 0f;
|
||||
if (d > maxD) maxD = d;
|
||||
|
||||
// 0.25 s 마다 813d 리쉬 판정(보스는 중심 · PC 는 d 만큼 떨어져 있다)
|
||||
if (((int)Mathf.Round(t / step)) % 3 != 0) continue;
|
||||
ticks++;
|
||||
int v = BossArena.EvaluateLeash(center, center + new Vector3(d, 0f, 0f), true, center, t);
|
||||
if (v != BossArena.kLeashNone) resets++;
|
||||
}
|
||||
|
||||
L(" 드리프트 " + drift + " m/s · 되돌림 " + pull + " m/s · 240 s · 판정 " + ticks + "회");
|
||||
L(" " + P(maxD < b.arenaRadius + b.playerLeaveMargin) + " 최대 이탈 " + maxD.ToString("F1") +
|
||||
" m < 리쉬 발동선 " + (b.arenaRadius + b.playerLeaveMargin) + " m (Q5 실측 46.3 m)");
|
||||
L(" " + P(resets == 0) + " 🔴 **리쉬 리셋 " + resets + "회**(Q5 실측 15~19회)");
|
||||
|
||||
// 대조군: 813w 동작(되돌림이 거의 안 걸린다) → 리셋이 실제로 난다 = 시뮬레이터가 살아 있다는 증거
|
||||
BossArena.ResetLeashState();
|
||||
int resets813w = 0; float d2 = 0f;
|
||||
for (float t = 0f; t < 240f; t += step)
|
||||
{
|
||||
d2 += drift * step; // 되돌림 없음
|
||||
if (((int)Mathf.Round(t / step)) % 3 != 0) continue;
|
||||
if (BossArena.EvaluateLeash(center, center + new Vector3(d2, 0f, 0f), true, center, t) != BossArena.kLeashNone)
|
||||
{ resets813w++; d2 = 0f; } // 리셋 = 보스 워프(PC 는 그대로 = 거리 0 로 근사)
|
||||
}
|
||||
L(" " + P(resets813w > 0) + " 대조군(되돌림 0) 리셋 " + resets813w + "회 — 판정기가 실제로 작동한다");
|
||||
BossArena.ResetLeashState();
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────── ⑦ 리쉬 회복 판정
|
||||
static void Sec7_LeashHeal()
|
||||
{
|
||||
L("");
|
||||
L("── ⑦ 리쉬 회복 축소 0.15 → 0.05 · 쿨다운 10 s 유지 (§1-4)");
|
||||
var tmp = ScriptableObject.CreateInstance<WLBossArenaSettings>();
|
||||
try
|
||||
{
|
||||
var real = WLBossArenaSettings.Instance;
|
||||
tmp.resetHealsBoss = true;
|
||||
tmp.leashHealPercent = real != null ? real.leashHealPercent : 0.05f;
|
||||
tmp.leashHealCooldownSeconds = real != null ? real.leashHealCooldownSeconds : 10f;
|
||||
|
||||
BossArena.ResetLeashState();
|
||||
int v1 = BossArena.EvaluateLeashHeal(tmp, 100f);
|
||||
int v2 = BossArena.EvaluateLeashHeal(tmp, 104f); // 쿨다운 안
|
||||
int v3 = BossArena.EvaluateLeashHeal(tmp, 111f); // 쿨다운 뒤
|
||||
L(" " + P(v1 == BossArena.kHealPartial) + " 1회차 = 부분 회복(+" + (tmp.leashHealPercent * 100f).ToString("F0") + "%)");
|
||||
L(" " + P(v2 == BossArena.kHealCooldown) + " 4 s 뒤 = 쿨다운으로 건너뜀");
|
||||
L(" " + P(v3 == BossArena.kHealPartial) + " 11 s 뒤 = 다시 회복");
|
||||
|
||||
// 240 s 동안 최악(매 10 s 리쉬)이라도 회복 총량은 24 × 5 % = 120 % → 0.15 였다면 360 %
|
||||
float worst = (240f / tmp.leashHealCooldownSeconds) * tmp.leashHealPercent;
|
||||
L(" 최악 회복 총량(240 s · 쿨다운마다) = " + (worst * 100f).ToString("F0") +
|
||||
"% (0.15 였다면 " + ((240f / tmp.leashHealCooldownSeconds) * 0.15f * 100f).ToString("F0") + "%)");
|
||||
L(" " + P(worst < 1.5f) + " 리셋이 남더라도 회복 총량이 보스 HP 1.5배 미만");
|
||||
|
||||
tmp.leashHealPercent = 1f;
|
||||
BossArena.ResetLeashState();
|
||||
L(" " + P(BossArena.EvaluateLeashHeal(tmp, 200f) == BossArena.kHealFull) + " C8: percent>=1 → 813d 만피 리셋 그대로");
|
||||
}
|
||||
finally { UnityEngine.Object.DestroyImmediate(tmp); BossArena.ResetLeashState(); }
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────── ⑧ GC
|
||||
static void Sec8_GC()
|
||||
{
|
||||
L("");
|
||||
L("── ⑧ GC 0 (정상 틱 경로)");
|
||||
var c = WLRunGuideSettings.Instance;
|
||||
Vector3 anchor;
|
||||
if (c == null || !RunGuide.TryGetAnchor(c, 1, out anchor)) return;
|
||||
|
||||
RunGuide.ResetDiagnostics();
|
||||
MovePc(anchor);
|
||||
RunGuide.BeginZone(1, "probe gc");
|
||||
for (int i = 0; i < 5; i++) RunGuide.ForceTick();
|
||||
|
||||
GC.Collect(); GC.WaitForPendingFinalizers();
|
||||
long a0 = Alloc();
|
||||
for (int i = 0; i < 100; i++) RunGuide.ForceTick();
|
||||
long a1 = Alloc();
|
||||
L(" " + P(a1 - a0 <= 0) + " ForceTick ×100 할당 = " + (a1 - a0) + " B");
|
||||
|
||||
long b0 = Alloc();
|
||||
var junk = new byte[64 * 1024];
|
||||
junk[0] = 1;
|
||||
long b1 = Alloc();
|
||||
L(" " + P(b1 - b0 > 0) + " 대조군(64 KB 할당) = " + (b1 - b0) + " B — 측정기가 살아 있다");
|
||||
RunGuide.StopGuide("probe gc done");
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────── ⑨ C8 롤백
|
||||
static void Sec9_C8()
|
||||
{
|
||||
L("");
|
||||
L("── ⑨ C8 — 813w2 스위치를 끄면 813w 동작 그대로 · 전체를 끄면 무동작");
|
||||
var c = WLRunGuideSettings.Instance;
|
||||
Vector3 anchor;
|
||||
if (c == null || !RunGuide.TryGetAnchor(c, 1, out anchor)) return;
|
||||
|
||||
// ⑨-1 피격 응전 판정 — 실에셋을 건드리지 않고 임시 SO 로 두 동작을 나란히 확인한다
|
||||
// (Poll 이 부르는 것과 **같은 함수** RunGuide.SuspendDecision 이다)
|
||||
var tmp = ScriptableObject.CreateInstance<WLRunGuideSettings>();
|
||||
try
|
||||
{
|
||||
tmp.hitSuspendOnlyInHold = false; // 813w
|
||||
bool w_travel = RunGuide.SuspendDecision(tmp, true, RunGuideState.Travel);
|
||||
bool w_hold = RunGuide.SuspendDecision(tmp, true, RunGuideState.Hold);
|
||||
tmp.hitSuspendOnlyInHold = true; // 813w2
|
||||
bool w2_travel = RunGuide.SuspendDecision(tmp, true, RunGuideState.Travel);
|
||||
bool w2_hold = RunGuide.SuspendDecision(tmp, true, RunGuideState.Hold);
|
||||
bool w2_nohit = RunGuide.SuspendDecision(tmp, false, RunGuideState.Hold);
|
||||
|
||||
L(" " + P(w_travel && w_hold) + " 813w(hitSuspendOnlyInHold=0): Travel=" + w_travel + " Hold=" + w_hold +
|
||||
" → **이동 중에도 멈춘다 = 도착 판정·되돌림이 폴링에서 빠진다**(D-24·D-22 원인)");
|
||||
L(" " + P(!w2_travel && w2_hold) + " 813w2(=1): Travel=" + w2_travel + " Hold=" + w2_hold +
|
||||
" → 이동·되돌림은 안 멈추고, 제자리 응전만 남는다");
|
||||
L(" " + P(!w2_nohit) + " 안 맞으면 언제나 false");
|
||||
|
||||
tmp.arriveStillEnabled = false;
|
||||
float stillOff = -1f;
|
||||
bool off = RunGuide.StepStillDetect(tmp, 0f, 0.1f, 50f, ref stillOff) ||
|
||||
RunGuide.StepStillDetect(tmp, 0f, 0.1f, 60f, ref stillOff);
|
||||
L(" " + P(!off) + " arriveStillEnabled=0 → 정지 판정 도착 없음(반경 판정만 = 813w)");
|
||||
}
|
||||
finally { UnityEngine.Object.DestroyImmediate(tmp); }
|
||||
|
||||
// ⑨-2 RuntimeDisabled → 아무 일도 하지 않는다
|
||||
RunGuide.ResetDiagnostics();
|
||||
MovePc(anchor + new Vector3(40f, 0f, 0f));
|
||||
RunGuide.BeginZone(1, "probe c8-2");
|
||||
var st = RunGuide.State;
|
||||
WLRunGuideSettings.RuntimeDisabled = true;
|
||||
try
|
||||
{
|
||||
s_pc.Change_Target(true, s_mob);
|
||||
for (int i = 0; i < 10; i++) RunGuide.ForceTick();
|
||||
L(" " + P(RunGuide.State == st && !RunGuide.Steering && ReferenceEquals(s_pc.Get_Target(), s_mob)) +
|
||||
" RuntimeDisabled=1 → 틱 ×10 에 상태 불변(" + RunGuide.State + ") · 조종 0 · 타깃 무수정");
|
||||
L(" " + P(s_pc.JoystickStatus == 0) + " JoystickStatus=0(원본 자동전투 100%)");
|
||||
}
|
||||
finally { WLRunGuideSettings.RuntimeDisabled = false; RunGuide.StopGuide("probe c8 done"); }
|
||||
|
||||
// 실에셋은 프로브가 한 번도 쓰지 않았다(전부 임시 SO · 읽기만)
|
||||
L(" " + P(c.hitSuspendOnlyInHold && c.arriveStillEnabled && c.pullbackReleasesTarget) +
|
||||
" 실에셋 무수정 확인 — hitSuspendOnlyInHold=" + c.hitSuspendOnlyInHold +
|
||||
" arriveStillEnabled=" + c.arriveStillEnabled + " pullbackReleasesTarget=" + c.pullbackReleasesTarget);
|
||||
}
|
||||
}
|
||||
|
|
@ -490,7 +490,8 @@ namespace WL.Combat.Boss
|
|||
/// <summary>
|
||||
/// 보스를 아레나 중심으로 되돌린다.
|
||||
/// 🔴 WL-813w §1-3 (Q4b D-11): 예전에는 매번 `On_Regen(true, 1)` = **만피 복구**였고 한 런에서 69회
|
||||
/// 일어나 보스 HP 가 한 칸도 줄지 않았다. 이제 기본은 `leashHealPercent`(0.15) **부분 회복**이고
|
||||
/// 일어나 보스 HP 가 한 칸도 줄지 않았다. 이제 기본은 `leashHealPercent`(813w2 §1-4 로 **0.05**)
|
||||
/// **부분 회복**이고
|
||||
/// `leashHealCooldownSeconds`(10 s) 안에서는 회복을 건너뛴다(워프·타깃 해제는 그대로).
|
||||
/// `leashHealPercent >= 1` 로 두면 813d 원래 동작(On_Regen 만피)으로 정확히 되돌아간다(C8).
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -165,8 +165,9 @@ namespace WL.Combat.Boss
|
|||
|
||||
// ── WL-813w §1-3 (Q4b D-11): 리쉬 1회당 만피 복구가 69회 = 보스 HP 가 절대 안 준다 → 부분 회복으로 완화
|
||||
[Tooltip("리쉬 리셋 때의 회복량 = 최대 HP 대비 비율(Actor.Heal(rate) · 누적 회복). "
|
||||
+ "🔴 1 이상이면 813d 원래 동작(On_Regen 만피 리셋 = C8 롤백). 0 이면 회복 없이 워프만.")]
|
||||
public float leashHealPercent = 0.15f;
|
||||
+ "🔴 1 이상이면 813d 원래 동작(On_Regen 만피 리셋 = C8 롤백). 0 이면 회복 없이 워프만. "
|
||||
+ "🔴 813w2 §1-4: Q5 실측(런당 리셋 15~19회 · 회복 8~9회)에서 0.15 는 깎은 HP 를 되돌렸다 → 0.05.")]
|
||||
public float leashHealPercent = 0.05f;
|
||||
|
||||
[Tooltip("부분 회복의 최소 간격(초 · unscaled). 이 안에 리쉬가 다시 걸리면 워프·타깃 해제만 하고 회복은 건너뛴다(최대 1회/10 s).")]
|
||||
public float leashHealCooldownSeconds = 10f;
|
||||
|
|
|
|||
|
|
@ -33,12 +33,47 @@
|
|||
//
|
||||
// ■ 가이드 상태
|
||||
// Off 목적지 없음 — 아무 일도 하지 않는다(원본 100%).
|
||||
// Travel 목적지로 이동 중 — 조종 ON · 타깃 해제(보스는 예외).
|
||||
// Travel 목적지로 이동 중 — 조종 ON · 타깃 해제.
|
||||
// Hold 도착 — 조종 OFF(원본 자동전투가 그 존/아레나에서 싸운다).
|
||||
// 다만 ① 목적지에서 guideLeashRadius 밖의 타깃은 무시(Del_Target)
|
||||
// ② PC 가 holdRadius 밖으로 밀려나면 Travel 로 되돌아가 끌어온다
|
||||
// → 존1 로 되돌아가는 것(D-13)과 아레나 이탈(D-11)이 여기서 막힌다.
|
||||
// 피격 resumeAfterHitSeconds 동안 가이드를 멈추고 원본에 넘긴다(즉시 응전 · 발주서 §1-1).
|
||||
// 피격 resumeAfterHitSeconds 동안 가이드를 멈추고 원본에 넘긴다(즉시 응전).
|
||||
//
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// WL-813w2 (2026-09-09 · PD 지시 #813) — 근거 = qa/W2-5_재채점_v3.md §4 런 4회 실측
|
||||
//
|
||||
// 🔴 실측 결함과 그 원인(한 곳이다)
|
||||
// · D-24 존 유도의 「도착」이 런 4회 내내 한 번도 안 잡혔다(`Arrived=0` · State=Travel 고착).
|
||||
// · D-22 보스 홀드 14 m 가 안 지켜져 PC 가 아레나에서 **최대 46.3 m** 이탈 →
|
||||
// 813d 리쉬(24 m)가 런당 **15~19회** 걸려 깎은 HP 를 되돌린다.
|
||||
// 되돌림(HoldPullbacks)은 런당 **3~4회**뿐 — 되돌림 검사 자체가 안 돈 것이다.
|
||||
// · D-19 화면 안 적 수 18 → 2. PC 가 존 반경 밖을 떠돈다.
|
||||
//
|
||||
// 원인 = **813w 의 Poll 이 피격 중단(`now < s_hitUntil`)에서 통째로 return** 했다(구 RunGuide.cs:291).
|
||||
// 그 return 이 ① 도착 판정(구 :303) ② 이탈 되돌림(구 :327) **위**에 있었다.
|
||||
// 잡몹 밭에서는 `resumeAfterHitSeconds`(3 s)가 매 피격마다 갱신돼 사실상 상시 참이므로
|
||||
// **도착 판정도 되돌림도 폴링에서 실행되지 않았다.** (HoldPullbacks 3~4 ≪ ResetCount 15~19 이
|
||||
// 그 증거다 — 이 두 값은 같은 함수의 같은 조건에서 나온다.)
|
||||
//
|
||||
// ■ 수리 (값은 전부 SO · 구조 변경 없음)
|
||||
// ① 순서 교정 — 도착 판정·이탈 되돌림은 **피격 중단과 무관하게 매 폴링 실행**한다.
|
||||
// 피격 중단은 이제 **Hold(= 목적지 반경 안)에서 조종을 놓는 것**만 뜻한다
|
||||
// (`hitSuspendOnlyInHold`). 이동·되돌림은 맞아도 멈추지 않는다 = 「이동 중 잡몹 무시」.
|
||||
// ② 정지 판정 도착 — XZ 거리 판정에 더해, 조종 중인데 `arriveStillSeconds`(0.5 s) 동안
|
||||
// `arriveStillSpeed`(0.3 m/s) 미만이면 **그 자리를 도착으로 확정**한다(목적지 반경 안일 때만).
|
||||
// NavMesh 높이·stoppingDistance·지형 때문에 반경 안으로 못 붙는 D-24 경로를 여기서 닫는다.
|
||||
// `maxTravelSeconds` 초과도 Hold 로 떨어뜨린다(이탈 감시는 계속 · 813w 와 동일).
|
||||
// ③ 되돌림 중 타깃 해제 — 이동 중에는 **보스 타깃도** 놓는다(`pullbackReleasesTarget`).
|
||||
// 타깃이 남아 있으면 원본 `Play_Run() → Set_Path(타깃)`(MyActor.cs:335)이 매 폴링
|
||||
// 우리 목적지를 덮어써 두 이동이 싸운다. 락(`m_IsLockTarget`)은 유지하므로
|
||||
// **보스 생존 중 잡몹이 타깃이 되는 일은 없다**(재타깃은 Hold 에서 · `bossRetargetOnlyInHold`).
|
||||
// ④ 진행 로그 — `RunEvents.RunTick` 구독으로 존 체류 초·이동 초·최대 이탈 m 를 남긴다
|
||||
// (`runTickLogSeconds` · QA 가 이 줄로 존 도착 시각표를 적는다).
|
||||
//
|
||||
// ■ C8 롤백 — `hitSuspendOnlyInHold=0` · `arriveStillEnabled=0` · `pullbackReleasesTarget=0`
|
||||
// · `bossRetargetOnlyInHold=0` 을 전부 끄면 813w 동작 그대로다(값 하나로 되돌릴 수 있다).
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// ■ 값(C45) 전부 WLRunGuideSettings. enabled = 0 이면 러너가 뜨지 않고 구독도 하지 않는다(C8).
|
||||
//
|
||||
|
|
@ -98,9 +133,17 @@ namespace WL.Combat.Run
|
|||
// ───────────────────────────────────────── 진단 카운터
|
||||
public static int GuideBegun, Arrived, GaveUp, FarTargetIgnored, HoldPullbacks,
|
||||
Repaths, BossRetargets, BossLocks, HitSuspends, AnchorScans;
|
||||
/// <summary>813w2: 정지 판정으로 확정한 도착 수(Arrived 에도 함께 센다) · 피격 중에도 실행한 되돌림 수.</summary>
|
||||
public static int ArrivedByStill, PullbacksWhileHit;
|
||||
/// <summary>813w2: 목적지에서 잰 최대 이탈 거리(m · Q5 D-22 의 「46.3 m」에 대응하는 실측값).</summary>
|
||||
public static float MaxDistFromDest;
|
||||
/// <summary>813w2: 존 체류 초 · 보스(아레나) 체류 초 · 이동(Travel) 초 — RunTick 로그와 QA 시각표용.</summary>
|
||||
public static float ZoneHoldSeconds, BossHoldSeconds, TravelSeconds;
|
||||
public static string LastLog = "";
|
||||
/// <summary>마지막으로 무시한 타깃 이름(진단).</summary>
|
||||
public static string LastIgnoredTarget = "";
|
||||
/// <summary>813w2: 마지막 RunTick 요약 줄(프로브·QA 가 읽는다).</summary>
|
||||
public static string LastTickLog = "";
|
||||
|
||||
// ───────────────────────────────────────── 내부 상태
|
||||
static RunGuideState s_state;
|
||||
|
|
@ -111,6 +154,10 @@ namespace WL.Combat.Run
|
|||
static bool s_steering, s_subscribed, s_faulted;
|
||||
static Actor s_boss, s_lockedBoss;
|
||||
|
||||
// 813w2 §1-1 정지 판정 · §1-3 체류 계측
|
||||
static Vector3 s_lastPos;
|
||||
static float s_lastPosTime = -1f, s_stillSince = -1f, s_lastAccumTime = -1f, s_nextTickLog;
|
||||
|
||||
// 존 앵커 캐시(스포너 노드 실좌표 · 없으면 SO 폴백)
|
||||
const int kMaxZones = 16;
|
||||
static readonly Vector3[] s_anchors = new Vector3[kMaxZones];
|
||||
|
|
@ -124,6 +171,7 @@ namespace WL.Combat.Run
|
|||
static readonly CombatHandler<BossStartedEvent> h_bossStarted = OnBossStarted;
|
||||
static readonly CombatHandler<RunResult> h_runEnded = OnRunEnded;
|
||||
static readonly CombatHandler<DamagedEvent> h_damaged = OnDamaged;
|
||||
static readonly CombatHandler<RunTickEvent> h_runTick = OnRunTick; // 813w2 §1-3 진행 로그
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// 구독
|
||||
|
|
@ -139,6 +187,7 @@ namespace WL.Combat.Run
|
|||
RunEvents.BossGateOpened.Add(h_gate);
|
||||
RunEvents.BossStarted.Add(h_bossStarted);
|
||||
RunEvents.RunEnded.Add(h_runEnded);
|
||||
RunEvents.RunTick.Add(h_runTick);
|
||||
CombatEvents.Damaged.Add(h_damaged);
|
||||
}
|
||||
|
||||
|
|
@ -152,6 +201,7 @@ namespace WL.Combat.Run
|
|||
RunEvents.BossGateOpened.Remove(h_gate);
|
||||
RunEvents.BossStarted.Remove(h_bossStarted);
|
||||
RunEvents.RunEnded.Remove(h_runEnded);
|
||||
RunEvents.RunTick.Remove(h_runTick);
|
||||
CombatEvents.Damaged.Remove(h_damaged);
|
||||
}
|
||||
|
||||
|
|
@ -164,6 +214,10 @@ namespace WL.Combat.Run
|
|||
StopGuide("런 시작");
|
||||
s_boss = null;
|
||||
s_anchorCount = 0; // 맵이 바뀌었을 수 있다 — 앵커를 다시 읽는다
|
||||
// 813w2 §1-3: 런마다 체류·이탈 계측을 새로 잡는다(런 간 누적 금지 — QA 가 런 1회 표를 읽는다).
|
||||
ZoneHoldSeconds = BossHoldSeconds = TravelSeconds = MaxDistFromDest = 0f;
|
||||
ArrivedByStill = PullbacksWhileHit = 0;
|
||||
s_nextTickLog = 0f; s_lastAccumTime = -1f; LastTickLog = "";
|
||||
var cfg = Cfg;
|
||||
if (cfg != null && cfg.zoneGuideEnabled && cfg.guideOnRunStart) BeginZone(0, "런 시작");
|
||||
}
|
||||
|
|
@ -189,15 +243,35 @@ namespace WL.Combat.Run
|
|||
static void OnDamaged(in DamagedEvent e)
|
||||
{
|
||||
var cfg = Cfg;
|
||||
if (cfg == null || cfg.resumeAfterHitSeconds <= 0f) return;
|
||||
if (cfg == null) return;
|
||||
if (s_destKind == RunGuideDest.None) return;
|
||||
// 813w2 §1-2: 보스 구간은 응전 중단을 짧게 둔다(Q5 D-22 처방 「피격 중단을 보스전에서는 짧게」).
|
||||
float hold = s_destKind == RunGuideDest.Boss ? cfg.bossHitSuspendSeconds : cfg.resumeAfterHitSeconds;
|
||||
if (hold <= 0f) return;
|
||||
var pc = MyValue.MyPC;
|
||||
if (DSUtil.CheckNull(pc) || !ReferenceEquals(e.victim, pc)) return;
|
||||
bool wasFree = s_hitUntil <= Time.unscaledTime;
|
||||
s_hitUntil = Time.unscaledTime + cfg.resumeAfterHitSeconds;
|
||||
s_hitUntil = Time.unscaledTime + hold;
|
||||
if (wasFree) HitSuspends++;
|
||||
}
|
||||
|
||||
/// <summary>813w2 §1-3 — 존 체류/이동 초를 주기적으로 한 줄 남긴다(QA 시각표 · GC 는 로그를 실제로 낼 때만).</summary>
|
||||
static void OnRunTick(in RunTickEvent e)
|
||||
{
|
||||
var cfg = Cfg;
|
||||
if (cfg == null || cfg.runTickLogSeconds <= 0f) return;
|
||||
if (e.elapsedSec < s_nextTickLog) return;
|
||||
s_nextTickLog = e.elapsedSec + cfg.runTickLogSeconds;
|
||||
|
||||
LastTickLog = "t=" + e.elapsedSec.ToString("F0") + "s · " + s_state +
|
||||
(s_destKind == RunGuideDest.Zone ? " 존" + s_zoneIndex : s_destKind == RunGuideDest.Boss ? " 보스" : " -") +
|
||||
" · 존체류 " + ZoneHoldSeconds.ToString("F0") + "s · 아레나체류 " + BossHoldSeconds.ToString("F0") +
|
||||
"s · 이동 " + TravelSeconds.ToString("F0") + "s · 최대이탈 " + MaxDistFromDest.ToString("F1") +
|
||||
"m · 도착 " + Arrived + "(정지 " + ArrivedByStill + ") · 되돌림 " + HoldPullbacks +
|
||||
" · 존 " + e.zonesCleared + "/" + e.zoneCount + " " + e.zoneKills + "/" + e.zoneRequired;
|
||||
Debug.Log("[RunGuide] " + LastTickLog);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// 목적지 지정
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
|
@ -247,6 +321,7 @@ namespace WL.Combat.Run
|
|||
s_state = RunGuideState.Off;
|
||||
s_zoneIndex = -1;
|
||||
s_hitUntil = 0f;
|
||||
s_stillSince = -1f; s_lastPosTime = -1f;
|
||||
if (had) Log("가이드 종료 — " + reason);
|
||||
}
|
||||
|
||||
|
|
@ -255,6 +330,7 @@ namespace WL.Combat.Run
|
|||
s_state = RunGuideState.Travel;
|
||||
s_travelStart = Time.unscaledTime;
|
||||
s_nextRepath = 0f;
|
||||
s_stillSince = -1f; s_lastPosTime = -1f; // 813w2: 정지 판정을 새로 잡는다
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
|
@ -277,76 +353,176 @@ namespace WL.Combat.Run
|
|||
catch (System.Exception ex) { s_faulted = true; ReleaseSteer(); Log("중단(예외) — " + ex.Message); }
|
||||
}
|
||||
|
||||
// 🔴 813w2 §1-1·§1-2 — 이 함수의 **순서**가 D-24 · D-22 의 원인이었다.
|
||||
// 813w 은 피격 중단(`now < s_hitUntil`)에서 통째로 return 해 도착 판정과 이탈 되돌림을
|
||||
// 건너뛰었다. 이제 ① 목적지·거리 계산 ② 도착 판정 ③ 이탈 되돌림은 **항상** 실행하고,
|
||||
// 피격 중단은 **Hold 에서 조종을 놓는 것**만 뜻한다(`hitSuspendOnlyInHold`).
|
||||
static void Poll(WLRunGuideSettings cfg, float now)
|
||||
{
|
||||
var pc = MyValue.MyPC;
|
||||
if (DSUtil.CheckNull(pc) || pc.IsDead()) { ReleaseSteer(); return; }
|
||||
if (DSUtil.CheckNull(pc) || pc.IsDead()) { ReleaseSteer(); s_lastPosTime = -1f; return; }
|
||||
|
||||
if (s_destKind == RunGuideDest.None) { ReleaseSteer(); return; }
|
||||
|
||||
// ── 보스 우선 타깃(발주서 §1-2) — 조종/도착 상태와 무관하게 보스가 살아 있으면 건다
|
||||
if (s_destKind == RunGuideDest.Boss) ApplyBossPriority(cfg, pc);
|
||||
|
||||
// ── 피격 → 즉시 응전(가이드 일시 중단)
|
||||
if (now < s_hitUntil) { ReleaseSteer(); return; }
|
||||
|
||||
if (s_destKind == RunGuideDest.Boss) s_dest = BossArena.Center;
|
||||
bool isBoss = s_destKind == RunGuideDest.Boss;
|
||||
if (isBoss) s_dest = BossArena.Center;
|
||||
|
||||
Vector3 pcPos = pc.Get_position();
|
||||
float d = DistXZ(pcPos, s_dest);
|
||||
bool isBoss = s_destKind == RunGuideDest.Boss;
|
||||
float arrive = isBoss ? cfg.bossArriveRadius : cfg.zoneArriveRadius;
|
||||
float hold = isBoss ? cfg.bossHoldRadius : cfg.zoneHoldRadius;
|
||||
if (d > MaxDistFromDest) MaxDistFromDest = d;
|
||||
|
||||
// ── 체류/이동 초 누적(§1-3 · RunTick 로그가 읽는다)
|
||||
AccumDwell(now, isBoss);
|
||||
|
||||
// ── 정지 판정용 이동량(조종 중일 때만 센다 — 원본이 싸우는 동안의 정지는 「도착」이 아니다)
|
||||
bool still = UpdateStill(cfg, pcPos, now);
|
||||
|
||||
// ── 보스 우선 타깃 — 락은 언제나 유지 · 재타깃은 Hold 에서(이동 중 타깃은 우리 목적지를 덮어쓴다)
|
||||
if (isBoss) ApplyBossPriority(cfg, pc);
|
||||
|
||||
// ── 피격 응전: Hold 에서만 건다(이동·되돌림은 맞아도 멈추지 않는다 = 「이동 중 잡몹 무시」)
|
||||
bool hit = now < s_hitUntil;
|
||||
bool suspend = SuspendDecision(cfg, hit, s_state);
|
||||
|
||||
if (s_state == RunGuideState.Travel)
|
||||
{
|
||||
if (d <= arrive)
|
||||
{
|
||||
s_state = RunGuideState.Hold;
|
||||
ReleaseSteer();
|
||||
Arrived++;
|
||||
Log((isBoss ? "아레나" : "존 " + s_zoneIndex) + " 도착 — " + d.ToString("F1") + "m · 자동전투 재개");
|
||||
EnterHold(isBoss, "도착 — " + d.ToString("F1") + "m · 자동전투 재개");
|
||||
return;
|
||||
}
|
||||
// 813w2 §1-1: 반경 안으로 못 붙어도 「멈춰 섰으면」 도착으로 확정한다(D-24).
|
||||
if (still && WithinStillRange(cfg, d, hold))
|
||||
{
|
||||
Arrived++; ArrivedByStill++;
|
||||
EnterHold(isBoss, "정지 도착 — " + d.ToString("F1") + "m · " +
|
||||
cfg.arriveStillSeconds.ToString("F1") + "s 정지 · 자동전투 재개");
|
||||
return;
|
||||
}
|
||||
if (cfg.maxTravelSeconds > 0f && now - s_travelStart >= cfg.maxTravelSeconds)
|
||||
{
|
||||
s_state = RunGuideState.Hold; // 포기하되 이탈 감시는 남긴다
|
||||
ReleaseSteer();
|
||||
GaveUp++;
|
||||
Log((isBoss ? "아레나" : "존 " + s_zoneIndex) + " 이동 포기 — " +
|
||||
cfg.maxTravelSeconds.ToString("F0") + "s 초과(남은 " + d.ToString("F1") + "m)");
|
||||
EnterHold(isBoss, "이동 포기 — " + cfg.maxTravelSeconds.ToString("F0") +
|
||||
"s 초과(남은 " + d.ToString("F1") + "m) · 이탈 감시는 유지");
|
||||
return;
|
||||
}
|
||||
ClearTravelTarget(pc);
|
||||
if (suspend) { ReleaseSteer(); return; } // 813w 롤백 경로에서만 여기로 온다
|
||||
ClearTravelTarget(cfg, pc);
|
||||
Steer(cfg, pc, now);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Hold: 조종은 놓고, 먼 타깃만 무시 · 이탈하면 다시 끌어온다
|
||||
IgnoreFarTarget(cfg, pc);
|
||||
// ── Hold: 조종은 놓고, 먼 타깃만 무시 · 이탈하면 **피격 중이라도** 다시 끌어온다
|
||||
// 피격 중(suspend)에는 「먼 타깃 무시」를 쉬어 준다 = 나를 때린 적에게 즉시 응전한다.
|
||||
// 그래도 이탈 되돌림은 그대로 도므로 존/아레나 반경 밖으로는 못 나간다(D-19 · D-22).
|
||||
if (!suspend) IgnoreFarTarget(cfg, pc);
|
||||
if (hold > 0f && d > hold)
|
||||
{
|
||||
EnterTravel();
|
||||
HoldPullbacks++;
|
||||
Log((isBoss ? "아레나" : "존 " + s_zoneIndex) + " 이탈 " + d.ToString("F1") + "m — 다시 유도");
|
||||
ClearTravelTarget(pc);
|
||||
if (hit) PullbacksWhileHit++;
|
||||
Log((isBoss ? "아레나" : "존 " + s_zoneIndex) + " 이탈 " + d.ToString("F1") + "m — 다시 유도" +
|
||||
(hit ? "(피격 중)" : ""));
|
||||
ClearTravelTarget(cfg, pc);
|
||||
Steer(cfg, pc, now);
|
||||
return;
|
||||
}
|
||||
ReleaseSteer();
|
||||
}
|
||||
|
||||
/// <summary>Travel → Hold 로 확정한다(도착 · 정지 도착 · 이동 포기 공통).</summary>
|
||||
static void EnterHold(bool isBoss, string why)
|
||||
{
|
||||
s_state = RunGuideState.Hold;
|
||||
ReleaseSteer();
|
||||
s_stillSince = -1f; s_lastPosTime = -1f;
|
||||
Log((isBoss ? "아레나" : "존 " + s_zoneIndex) + " " + why);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 피격 응전 판정(순수 · 프로브가 임시 SO 로 그대로 부른다 · C8 증거).
|
||||
/// 🔴 813w2 의 핵심 — `hitSuspendOnlyInHold` 가 켜져 있으면 **Travel(= 이동·되돌림)에서는
|
||||
/// 피격이 가이드를 멈추지 못한다.** 813w 은 상태와 무관하게 멈췄고, 그 때문에 도착 판정(D-24)과
|
||||
/// 이탈 되돌림(D-22)이 폴링에서 통째로 빠졌다.
|
||||
/// </summary>
|
||||
public static bool SuspendDecision(WLRunGuideSettings cfg, bool hit, RunGuideState state)
|
||||
{
|
||||
if (!hit || cfg == null) return false;
|
||||
return !cfg.hitSuspendOnlyInHold || state == RunGuideState.Hold;
|
||||
}
|
||||
|
||||
/// <summary>정지 판정으로 도착을 인정할 거리인가 — 기본은 목적지 반경(holdRadius) 안(프로브가 그대로 부른다).</summary>
|
||||
public static bool WithinStillRange(WLRunGuideSettings cfg, float d, float hold)
|
||||
{
|
||||
float lim = cfg.arriveStillMaxDistance > 0f ? cfg.arriveStillMaxDistance : hold;
|
||||
return lim <= 0f || d <= lim;
|
||||
}
|
||||
|
||||
/// <summary>조종 중 XZ 속도가 arriveStillSpeed 미만인 상태가 arriveStillSeconds 이상 이어졌는가.</summary>
|
||||
static bool UpdateStill(WLRunGuideSettings cfg, Vector3 pcPos, float now)
|
||||
{
|
||||
if (s_state != RunGuideState.Travel || !s_steering)
|
||||
{ s_stillSince = -1f; s_lastPosTime = -1f; s_lastPos = pcPos; return false; }
|
||||
|
||||
if (s_lastPosTime < 0f) { s_lastPos = pcPos; s_lastPosTime = now; s_stillSince = -1f; return false; }
|
||||
|
||||
float dt = now - s_lastPosTime;
|
||||
float moved = DistXZ(pcPos, s_lastPos);
|
||||
if (dt > 0f) { s_lastPos = pcPos; s_lastPosTime = now; }
|
||||
return StepStillDetect(cfg, moved, dt, now, ref s_stillSince);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 정지 판정 1스텝(순수 · Actor 접근 0). 프로브가 가짜 이동량·시각으로 그대로 부를 수 있다.
|
||||
/// moved = 지난 스텝 동안의 XZ 이동량(m) · dt = 그 간격(초) · stillSince = 정지 시작 시각(없으면 −1).
|
||||
/// </summary>
|
||||
public static bool StepStillDetect(WLRunGuideSettings cfg, float moved, float dt, float now, ref float stillSince)
|
||||
{
|
||||
if (cfg == null || !cfg.arriveStillEnabled || cfg.arriveStillSeconds <= 0f) { stillSince = -1f; return false; }
|
||||
if (dt > 0f)
|
||||
{
|
||||
if (moved / dt < cfg.arriveStillSpeed) { if (stillSince < 0f) stillSince = now; }
|
||||
else stillSince = -1f;
|
||||
}
|
||||
return stillSince >= 0f && now - stillSince >= cfg.arriveStillSeconds;
|
||||
}
|
||||
|
||||
/// <summary>존 체류·아레나 체류·이동 초를 누적한다(§1-3 QA 시각표).</summary>
|
||||
static void AccumDwell(float now, bool isBoss)
|
||||
{
|
||||
if (s_lastAccumTime >= 0f)
|
||||
{
|
||||
float dt = now - s_lastAccumTime;
|
||||
if (dt > 0f && dt < 1f) // 프레임 튐·일시정지 구간은 버린다
|
||||
{
|
||||
if (s_state == RunGuideState.Travel) TravelSeconds += dt;
|
||||
else if (s_state == RunGuideState.Hold) { if (isBoss) BossHoldSeconds += dt; else ZoneHoldSeconds += dt; }
|
||||
}
|
||||
}
|
||||
s_lastAccumTime = now;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// 조종 · 타깃
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>이동 중에는 타깃을 놓는다(보스가 목적지일 때 보스는 예외 — 그쪽으로 가는 것이 목적이다).</summary>
|
||||
static void ClearTravelTarget(PCActor pc)
|
||||
/// <summary>
|
||||
/// 이동·되돌림 중에는 타깃을 놓는다.
|
||||
/// 🔴 813w2 §1-2: 보스 타깃도 놓는다(`pullbackReleasesTarget`). 타깃이 남아 있으면 원본
|
||||
/// `Play_Run() → Set_Path(타깃)`(MyActor.cs:335)이 매 폴링 우리 목적지를 덮어써 두 이동이 싸운다.
|
||||
/// 보스 락(`m_IsLockTarget`)은 유지되므로 잡몹이 타깃을 가져가지 못한다 — 도착(Hold)하면
|
||||
/// `ApplyBossPriority` 가 즉시 보스를 다시 건다.
|
||||
/// </summary>
|
||||
static void ClearTravelTarget(WLRunGuideSettings cfg, PCActor pc)
|
||||
{
|
||||
if (!pc.isTarget) return;
|
||||
var t = pc.Get_Target();
|
||||
if (DSUtil.CheckNull(t)) return;
|
||||
if (s_destKind == RunGuideDest.Boss && !DSUtil.CheckNull(s_boss) && ReferenceEquals(t, s_boss)) return;
|
||||
if (!cfg.pullbackReleasesTarget &&
|
||||
s_destKind == RunGuideDest.Boss && !DSUtil.CheckNull(s_boss) && ReferenceEquals(t, s_boss)) return;
|
||||
LastIgnoredTarget = t.name;
|
||||
pc.Del_Target(true);
|
||||
FarTargetIgnored++;
|
||||
|
|
@ -383,11 +559,15 @@ namespace WL.Combat.Run
|
|||
s_steering = true;
|
||||
}
|
||||
|
||||
pc.WL_PlayRunNow(); // 타깃이 없으므로 경로를 건드리지 않는다(MyActor.cs:335)
|
||||
// 🔴 813w2: 타깃이 남아 있으면 Play_Run() 이 `Set_Path(타깃)`(MyActor.cs:335)으로 경로를 덮어쓴다.
|
||||
// 보통은 바로 위 ClearTravelTarget 이 타깃을 비웠으므로 그런 일이 없지만, 원본이 같은 프레임에
|
||||
// 다시 잡는 경우가 있어 **타깃이 있으면 이 폴링에서 목적지를 다시 지정**해 우리 쪽이 이기게 한다.
|
||||
bool targeted = pc.isTarget;
|
||||
pc.WL_PlayRunNow();
|
||||
agent.isStopped = false;
|
||||
agent.stoppingDistance = cfg.guideStopDistance;
|
||||
|
||||
if (now >= s_nextRepath)
|
||||
if (targeted || now >= s_nextRepath)
|
||||
{
|
||||
s_nextRepath = now + Mathf.Max(0f, cfg.repathIntervalSeconds);
|
||||
agent.speed = pc.Get_MoveSpeed();
|
||||
|
|
@ -426,6 +606,11 @@ namespace WL.Combat.Run
|
|||
BossLocks++;
|
||||
}
|
||||
|
||||
// 813w2 §1-2: 재타깃은 Hold(아레나 안)에서만. 이동·되돌림 중에 보스를 타깃으로 걸면
|
||||
// 원본 Play_Run() 이 Set_Path(보스)로 우리 목적지를 덮어써 되돌림이 무력해진다.
|
||||
// 락은 위에서 계속 유지되므로 「보스 생존 중 잡몹 타깃」은 이동 중에도 생기지 않는다.
|
||||
if (cfg.bossRetargetOnlyInHold && s_state != RunGuideState.Hold) return;
|
||||
|
||||
if (!ReferenceEquals(pc.Get_Target(), boss))
|
||||
{
|
||||
pc.Change_Target(true, boss); // 아웃라인까지 원본 경로 그대로(Actor.cs:988)
|
||||
|
|
@ -534,19 +719,33 @@ namespace WL.Combat.Run
|
|||
{
|
||||
GuideBegun = Arrived = GaveUp = FarTargetIgnored = HoldPullbacks = 0;
|
||||
Repaths = BossRetargets = BossLocks = HitSuspends = AnchorScans = 0;
|
||||
LastLog = LastIgnoredTarget = "";
|
||||
ArrivedByStill = PullbacksWhileHit = 0;
|
||||
MaxDistFromDest = ZoneHoldSeconds = BossHoldSeconds = TravelSeconds = 0f;
|
||||
LastLog = LastIgnoredTarget = LastTickLog = "";
|
||||
ReleaseSteer(); ReleaseBossLock();
|
||||
s_state = RunGuideState.Off; s_destKind = RunGuideDest.None; s_zoneIndex = -1;
|
||||
s_dest = Vector3.zero; s_boss = null;
|
||||
s_nextPoll = s_nextRepath = s_travelStart = s_hitUntil = 0f;
|
||||
s_nextPoll = s_nextRepath = s_travelStart = s_hitUntil = s_nextTickLog = 0f;
|
||||
s_lastPosTime = s_stillSince = s_lastAccumTime = -1f; s_lastPos = Vector3.zero;
|
||||
s_faulted = false; s_anchorCount = 0;
|
||||
}
|
||||
|
||||
/// <summary>프로브용: 조종 플래그를 직접 세운다(에디트 모드엔 NavMeshAgent 가 없어 Steer 가 되돌아간다).</summary>
|
||||
public static void SetSteeringForProbe(bool on) { s_steering = on; if (!on) { s_stillSince = -1f; s_lastPosTime = -1f; } }
|
||||
/// <summary>프로브용: 상태를 직접 읽는다(정지 판정 내부 시각).</summary>
|
||||
public static float StillSince { get { return s_stillSince; } }
|
||||
|
||||
/// <summary>프로브용: 러너 없이 1회 폴링(강제 틱).</summary>
|
||||
public static void ForceTick() { s_nextPoll = 0f; Tick(); }
|
||||
|
||||
/// <summary>프로브용: 피격 응전 구간을 직접 건다.</summary>
|
||||
public static void SimulateHit() { var c = Cfg; if (c != null) { s_hitUntil = Time.unscaledTime + c.resumeAfterHitSeconds; HitSuspends++; } }
|
||||
public static void SimulateHit()
|
||||
{
|
||||
var c = Cfg; if (c == null) return;
|
||||
float hold = s_destKind == RunGuideDest.Boss ? c.bossHitSuspendSeconds : c.resumeAfterHitSeconds;
|
||||
if (hold <= 0f) hold = 0.01f; // 프로브가 「피격 중」 상태를 만들 수 있게 최소값
|
||||
s_hitUntil = Time.unscaledTime + hold; HitSuspends++;
|
||||
}
|
||||
|
||||
/// <summary>프로브용: 피격 응전 구간을 즉시 푼다.</summary>
|
||||
public static void ClearHitSuspendForProbe() { s_hitUntil = 0f; }
|
||||
|
|
@ -554,6 +753,9 @@ namespace WL.Combat.Run
|
|||
/// <summary>프로브용: 보스 참조를 직접 넣는다.</summary>
|
||||
public static void SetBossForProbe(Actor boss) { s_boss = boss; }
|
||||
|
||||
/// <summary>프로브용: 이동 시작 시각을 밀어 넣는다(에디트 모드에서는 Time 이 흐르지 않아 maxTravelSeconds 를 못 넘긴다).</summary>
|
||||
public static void SetTravelStartForProbe(float t) { s_travelStart = t; }
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// 런너 (숨김 GameObject 1개 · 플레이 모드 전용)
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ namespace WL.Combat.Run
|
|||
public bool guideOnRunStart = false;
|
||||
|
||||
[Tooltip("존 앵커에 이 거리(m · XZ) 안으로 들어오면 「도착」 = 조종을 놓고 원본 자동전투에 넘긴다.")]
|
||||
public float zoneArriveRadius = 3f;
|
||||
public float zoneArriveRadius = 6f;
|
||||
|
||||
[Tooltip("도착 뒤에도 PC 가 앵커에서 이 거리(m) 밖으로 밀려나면 다시 끌어온다(존1 로 되돌아가는 것을 막는다). 0 이하면 유지 안 함.")]
|
||||
public float zoneHoldRadius = 18f;
|
||||
|
|
@ -101,8 +101,9 @@ namespace WL.Combat.Run
|
|||
// ─────────────────────────────────────────── 공통 조종 규칙
|
||||
[Header("조종 규칙 (원본 타깃/이동 API 만 쓴다)")]
|
||||
[Tooltip("가이드 중 PC 의 타깃이 목적지(앵커/아레나 중심)에서 이 거리(m) 밖이면 무시한다(Del_Target). "
|
||||
+ "이 안의 적은 원본 자동전투가 그대로 잡는다.")]
|
||||
public float guideLeashRadius = 12f;
|
||||
+ "이 안의 적은 원본 자동전투가 그대로 잡는다. "
|
||||
+ "🔴 813w2: zoneHoldRadius(18) 보다 작게 두되 너무 작으면 존 안의 적을 스스로 버려 화면 내 적이 줄어든다(D-19).")]
|
||||
public float guideLeashRadius = 14f;
|
||||
|
||||
[Tooltip("조종 중 NavMeshAgent.stoppingDistance(m). 목적지 코앞에서 멈춰 서서 도착 판정이 안 나는 것을 막는다.")]
|
||||
public float guideStopDistance = 0.5f;
|
||||
|
|
@ -113,9 +114,54 @@ namespace WL.Combat.Run
|
|||
[Tooltip("PC 가 맞으면 이 시간(초) 동안 가이드를 멈추고 즉시 응전한다(원본 자동전투가 잡는다). 0 이면 응전 중단 없음.")]
|
||||
public float resumeAfterHitSeconds = 3f;
|
||||
|
||||
[Tooltip("한 목적지에 이 시간(초) 안에 도착 못 하면 가이드를 포기한다(경로 불가·지형 끼임 보호). 0 이하면 무제한.")]
|
||||
[Tooltip("한 목적지에 이 시간(초) 안에 도착 못 하면 가이드를 포기한다(경로 불가·지형 끼임 보호). 0 이하면 무제한. "
|
||||
+ "포기해도 상태는 Hold 로 떨어져 이탈 감시(되돌림)는 계속한다.")]
|
||||
public float maxTravelSeconds = 60f;
|
||||
|
||||
// ─────────────────────────────────────────── 813w2 §1-1 · §1-2 (D-24 · D-22 공통 원인)
|
||||
[Header("피격 응전 범위 (813w2 · D-22/D-24 공통 원인)")]
|
||||
[Tooltip("🔴 813w 는 피격 중단이 「도착 판정·이탈 되돌림」까지 통째로 막았다(RunGuide.cs:291). "
|
||||
+ "켜면 피격 중단은 Hold(= 목적지 반경 안)에서만 걸고, 이동·되돌림 중에는 무시한다. "
|
||||
+ "끄면 813w 그대로(C8 롤백 · Q5 실측 = 존 Arrived 0 · 아레나 이탈 46 m).")]
|
||||
public bool hitSuspendOnlyInHold = true;
|
||||
|
||||
[Tooltip("보스 Hold 에서의 피격 중단(초). 존은 resumeAfterHitSeconds 를 쓴다. "
|
||||
+ "0 이면 보스전에서는 피격 중단 없음(되돌림·보스 타깃이 항상 우선).")]
|
||||
public float bossHitSuspendSeconds = 0.5f;
|
||||
|
||||
// ─────────────────────────────────────────── 813w2 §1-1 도착 판정(정지 판정)
|
||||
[Header("도착 판정 보강 (813w2 · D-24 · Q5 §4-2 「존 Arrived=0」)")]
|
||||
[Tooltip("도착 반경 안으로 못 들어가도, 조종 중 PC 가 사실상 멈춰 서면 도착으로 확정한다 "
|
||||
+ "(NavMesh 높이·stoppingDistance·지형 때문에 반경 안으로 못 붙는 경우). 끄면 반경 판정만.")]
|
||||
public bool arriveStillEnabled = true;
|
||||
|
||||
[Tooltip("「멈춰 섰다」로 보는 시간(초 · unscaled). 이 시간 내내 arriveStillSpeed 미만이면 도착.")]
|
||||
public float arriveStillSeconds = 0.5f;
|
||||
|
||||
[Tooltip("「멈춰 섰다」로 보는 XZ 속도(m/s). 이 값 미만이면 정지로 센다.")]
|
||||
public float arriveStillSpeed = 0.3f;
|
||||
|
||||
[Tooltip("정지 판정으로 도착을 확정할 수 있는 최대 거리(m). 목적지 반경(holdRadius) 안일 때만 인정한다 — "
|
||||
+ "0 이면 holdRadius 를 그대로 쓴다. 훨씬 멀리서 끼인 경우는 maxTravelSeconds 가 처리한다.")]
|
||||
public float arriveStillMaxDistance = 0f;
|
||||
|
||||
// ─────────────────────────────────────────── 813w2 §1-2 되돌림
|
||||
[Header("되돌림 (813w2 · D-22 · 이탈 46 m)")]
|
||||
[Tooltip("이동·되돌림 중에는 보스 타깃도 놓는다. "
|
||||
+ "🔴 이유: 타깃이 있으면 원본 Play_Run() 이 매 폴링 Set_Path(타깃)(MyActor.cs:335)으로 "
|
||||
+ "우리 목적지를 덮어써 두 이동이 싸운다. 끄면 813w 그대로(보스 타깃 유지).")]
|
||||
public bool pullbackReleasesTarget = true;
|
||||
|
||||
[Tooltip("보스 재타깃(Change_Target)은 Hold 에서만 한다(락 m_IsLockTarget 은 언제나 유지). "
|
||||
+ "끄면 이동 중에도 매 폴링 보스로 되돌린다(= 813w · pullbackReleasesTarget 와 서로 싸운다).")]
|
||||
public bool bossRetargetOnlyInHold = true;
|
||||
|
||||
// ─────────────────────────────────────────── 813w2 §1-3 진행 로그
|
||||
[Header("진행 로그 (813w2 · D-19 · QA 시각표)")]
|
||||
[Tooltip("RunTick 마다 「존 체류 초 · 이동 초 · 목적지 거리 · 최대 이탈」 한 줄을 남기는 주기(초). "
|
||||
+ "0 이하면 남기지 않는다. QA 가 존 도착 시각표를 이 줄로 적는다.")]
|
||||
public float runTickLogSeconds = 10f;
|
||||
|
||||
// ─────────────────────────────────────────── 보스
|
||||
[Header("보스 (D-11 · 아레나 이탈 → 리쉬 만피 리셋)")]
|
||||
[Tooltip("끄면 게이트 개방·보스 등장 때 아레나로 유도하지 않는다.")]
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ MonoBehaviour:
|
|||
playerLeaveGraceSeconds: 1.5
|
||||
resetHealsBoss: 1
|
||||
resetHealRate: 1
|
||||
leashHealPercent: 0.15
|
||||
leashHealPercent: 0.05
|
||||
leashHealCooldownSeconds: 10
|
||||
bossNameFromTable: 1
|
||||
suppressRegenAfterBossKill: 1
|
||||
|
|
|
|||
|
|
@ -17,18 +17,27 @@ MonoBehaviour:
|
|||
pollSeconds: 0.1
|
||||
zoneGuideEnabled: 1
|
||||
guideOnRunStart: 0
|
||||
zoneArriveRadius: 3
|
||||
zoneArriveRadius: 6
|
||||
zoneHoldRadius: 18
|
||||
zoneAnchorFallbacks:
|
||||
- {x: 115, y: 0.39, z: -22}
|
||||
- {x: 147, y: 2.59, z: 30}
|
||||
- {x: 71, y: -0.29, z: 18}
|
||||
- {x: 159, y: 0.88, z: -22}
|
||||
guideLeashRadius: 12
|
||||
guideLeashRadius: 14
|
||||
guideStopDistance: 0.5
|
||||
repathIntervalSeconds: 0.5
|
||||
resumeAfterHitSeconds: 3
|
||||
maxTravelSeconds: 60
|
||||
hitSuspendOnlyInHold: 1
|
||||
bossHitSuspendSeconds: 0.5
|
||||
arriveStillEnabled: 1
|
||||
arriveStillSeconds: 0.5
|
||||
arriveStillSpeed: 0.3
|
||||
arriveStillMaxDistance: 0
|
||||
pullbackReleasesTarget: 1
|
||||
bossRetargetOnlyInHold: 1
|
||||
runTickLogSeconds: 10
|
||||
bossGuideEnabled: 1
|
||||
bossPriorityEnabled: 1
|
||||
bossArriveRadius: 10
|
||||
|
|
|
|||
Loading…
Reference in New Issue