Merge branch 'wl/gameplay/WL-813w4-restart-elite-levelfx'
This commit is contained in:
commit
82076d7d6b
|
|
@ -0,0 +1,102 @@
|
|||
# WL813w4 Probe 2026-09-09 16:18:12 (edit mode · Play 0 · 로그인 0)
|
||||
playMode=False
|
||||
|
||||
── ⓪ 준비
|
||||
[PASS] 가짜 메인 PC IsMainPC()=true · MyValue.MyPC 등록
|
||||
[PASS] RunGuide.Active(에셋 · enabled · 런 축)
|
||||
[PASS] RunGuide.Subscribed — 🔴 D-34 의 「RunStarted 구독이 없다」 추정은 여기서 이미 반증된다
|
||||
|
||||
── ① 에셋 · SOT (813w4 신규 필드 · C45 · 코드 상수 0)
|
||||
[PASS] WLRunGuideSettings 로드 · [PASS] WLGrowthSettings 로드
|
||||
[가이드 신규 9] guideOnRestart=True restartResetsGuideState=True stallGuideSeconds=3 stallMoveEpsilon=0.5 elitePriorityEnabled=True eliteTargetRadius=12 eliteReleaseRadius=18 eliteLockTarget=True eliteRetargetOnlyInHold=True
|
||||
[성장 신규 2 + 값 2] burstMaxScreenFraction=0.5 burstAlpha=0.6 burstScale=0.5 burstLifetimeSeconds=0.8
|
||||
[PASS] 「다음 런」 재유도 on(D-34) · guideOnRunStart 는 False 그대로(런 1 동작 무변경)
|
||||
[PASS] 정지 감시 3s / 0.5m
|
||||
[PASS] 엘리트 우선 타깃 on · 반경 12 m(발주서 §1-2)
|
||||
[PASS] 해제 반경(18) > 고정 반경(12) — 경계에서 떨리지 않는다
|
||||
[PASS] 고정 반경(12) ≤ zoneHoldRadius(18) — 엘리트를 쫓다 존 밖으로 나가면 Hold 되돌림이 먼저 걸린다
|
||||
[PASS] 화면 점유 상한 0.5 ≤ 0.5(발주서 §1-3 「화면 50 % 이하」)
|
||||
[PASS] 알파 0.6(발주서 §1-3)
|
||||
[PASS] 지속 0.8 s(발주서 §1-3)
|
||||
존 축 실측 — zoneSpawnerIds=[813001,813002] zoneClearKills=[18,20] (Q7 「존 진행 0/18」 = 존0 요구 처치)
|
||||
[PASS] 존0 앵커 확보 (115.0,0.4,-22.0) · 스포너 노드=False — 🔴 재시작 좌표 (115.0,0.3,11.0) 에서 33.0 m (Q7 D-34 의 정지 좌표 · 원본 자동전투 탐지 범위 밖)
|
||||
|
||||
── ② D-34 「다음 런」 뒤 존0 유도 재시작 (발주서 §1-1)
|
||||
🔴 원인 진리표 — ShouldGuideOnStart(cfg, restart)
|
||||
[PASS] 813w3 · 런 1 = false (원본 자동전투에 맡긴다 — 맵 진입점이 존0 안이라 굴러갔다)
|
||||
[PASS] 813w3 · 다음 런 = **false** ← 🔴 D-34 의 끊긴 지점(RunGuide.cs:311 `cfg.guideOnRunStart`)
|
||||
[PASS] 813w4 · 런 1 = false (런 1 동작 무변경 = 회귀 0)
|
||||
[PASS] 813w4 · 다음 런 = **true**
|
||||
[PASS] reason 판정 — "restart"(813p 보고 §4 계약) = 재시작
|
||||
[PASS] "pc" · "map N" 은 재시작 아님
|
||||
[PASS] 런 1(reason="pc") → GuideBegun 0→0 (guideOnRunStart=False 이므로 그대로 = 813w3 동작 보존)
|
||||
[PASS] 「다음 런」(reason="restart") → GuideBegun 0→1 (**Q7 이 「2 에서 안 오른다」고 적은 그 값**)
|
||||
[PASS] State=Travel (Q7 실측 Off → Travel)
|
||||
[PASS] 목적지=존 0 (115.0,0.4,-22.0)
|
||||
[PASS] RestartGuides=1
|
||||
[PASS] Restart ×2 → 유도 재개 2회(RestartGuides=2 · State=Travel)
|
||||
고장(예외) 상태 강제 — Faulted=True
|
||||
[PASS] 재시작이 고장 플래그를 푼다(restartResetsGuideState) — 런 1 의 예외가 세션을 못 죽인다
|
||||
[PASS] RunDirector.Restart() 실행 — RestartCount=1 StartCount=2
|
||||
[PASS] 실 경로에서도 GuideBegun 0→1
|
||||
[PASS] 재시작 직후 State=Travel 목적지=Zone (115.0,0.4,-22.0)
|
||||
[PASS] RunDirector.Phase=Zones
|
||||
(참고) RunDirector 로그 = 재시작 일부 실패 — Object reference not set to an instance of an object
|
||||
|
||||
── ③ D-34 안전망 — 정지 감시 TrackIdleStall (원인과 무관하게 「166 s 정지」를 되돌린다)
|
||||
[PASS] 제자리 3 s → 유도(첫관측 False · 2.0s False · 3.5s True)
|
||||
[PASS] StallGuides=1 목적지=존 0
|
||||
[PASS] 3 m 이동하면 타이머를 다시 잡는다(유도 안 함)
|
||||
[PASS] 🔴 타깃 보유(제자리 교전) → 유도 0 — 전투를 끊지 않는다
|
||||
[PASS] Phase=Ended(존 단계 아님) → 유도 0(결과 화면·로비에서 안 돈다)
|
||||
[PASS] 이미 유도 중(DestKind=Zone) → 중복 유도 0
|
||||
|
||||
── ④ D-20 엘리트 우선 타깃 (발주서 §1-2 · Q7 TTK 45.2/77.3 s · 원인 = PC 가 엘리트를 안 때린다)
|
||||
[PASS] 진짜 Spawned 3건(엘리트 2 · 잡몹 1) → 등록부 2 (용량 16 · 잡몹은 안 들어간다)
|
||||
[PASS] 813s EliteMarker 구독=False (식별 출처가 같다 — SpawnedEvent.isElite = Actor.IsSubRole(Elite))
|
||||
[PASS] 반경 12 m 안의 최근접 엘리트 = __WL813w4_Elite (30 m 엘리트는 제외)
|
||||
[PASS] 200 m 떨어지면 후보 0
|
||||
(전) 타깃=__WL813w4_Mob
|
||||
[PASS] 엘리트 고정 = __WL813w4_Elite · EliteLocks=1
|
||||
[PASS] 원본 락타깃 m_IsLockTarget=True(잡몹이 타깃을 도로 못 가져간다 · 813w 보스와 같은 방식)
|
||||
[PASS] PC 타깃 = __WL813w4_Elite · EliteRetargets=1
|
||||
[PASS] 폴링 ×2 추가 → 락 1 재타깃 1 (멱등)
|
||||
[PASS] 틱 1회 뒤에도 타깃 유지(먼 타깃 무시가 고정 엘리트를 건드리지 않는다)
|
||||
[PASS] Travel 에서는 재타깃 0(원본 Set_Path 덮어쓰기 방지) · 락은 유지 True
|
||||
[PASS] 40 m 로 이탈 → 고정 해제(EliteReleases=1)
|
||||
[PASS] 원본 락타깃 원복 m_IsLockTarget=False
|
||||
[PASS] 5 m 로 복귀 → 재고정
|
||||
[PASS] 진짜 Killed(Elite) → 고정 해제(EliteReleases 1→2)
|
||||
[PASS] 처치 시에도 원본 락타깃 원복
|
||||
[PASS] 등록부에서 제거 → 남은 엘리트 1
|
||||
[PASS] 킬캠·BossKill 티어 = `Killed` 이벤트만 보는 813s/811gh 경로 — RunGuide 는 구독만 하고 아무것도 바꾸지 않는다
|
||||
[PASS] 보스 유도로 전환 → 엘리트 고정 해제(보스 우선 · 고정 전 True 후 False)
|
||||
|
||||
── ⑤ D-37 레벨업 「LV」 연출 — 화면 점유 상한 · 알파 · 지속 (발주서 §1-3)
|
||||
🔴 원인 실측 — FX_FireLevelUp 프리팹의 ParticleSystemRenderer.maxParticleSize
|
||||
렌더러 9개 · FX_FireLevelUp=0.50 flare=1.00 lb=1.00 alpha_ball=1.00 grad10=1.00 ring_orange=1.00 gamestar=1.00 fire_burst=1.00 grad10=1.00
|
||||
[PASS] 프리팹 최대 점유 1.00 (1.0 = 파티클 1개가 화면 높이 100 % · 8개가 1.0) ← 「LV 가 화면 대부분을 덮는다」의 값 근거
|
||||
[PASS] 점유 상한 적용 2개(1.0 짜리 2개만 · 0.2 짜리는 그대로)
|
||||
[PASS] A=0.50 C=0.20 (화면 50 % 이하 보장)
|
||||
[PASS] 적용 후 최대 점유 0.50
|
||||
[PASS] Color 모드 알파 1.00 → 0.60
|
||||
[PASS] TwoColors 알파 (1.00,0.50) → (0.60,0.30)
|
||||
[PASS] 알파 적용 2 · 건너뜀 1(Gradient 모드 — 새 Gradient 할당을 피한다)
|
||||
측정 수명 10.00 s · 에셋 burstLifetimeSeconds=0.8 → 실 수명 0.80 s
|
||||
[PASS] 🔴 프리팹은 그대로 1.00 — 인스턴스에만 걸었다(에셋·머티리얼 무수정)
|
||||
[PASS] C8: burstMaxScreenFraction=0 · burstAlpha=1 → 렌더러·색 무변경(813k 동작 그대로)
|
||||
|
||||
── ⑥ GC 0 (틱 경로 · 813w3 와 같은 기준)
|
||||
[PASS] 엘리트 우선 타깃 폴링 ×100 = 0 B
|
||||
[PASS] 정지 감시 폴링 ×100(미도달) = 0 B
|
||||
[PASS] 등록부 조회 ×100 = 0 B (사전 할당 배열 16칸)
|
||||
|
||||
── ⑦ C8 롤백 (각 스위치 0 → 813w3 동작)
|
||||
[PASS] guideOnRestart=0 → 「다음 런」 유도 0 = 813w3 동작
|
||||
[PASS] stallGuideSeconds=0 → 정지 감시 0회
|
||||
[PASS] elitePriorityEnabled=0 → 고정 0 · 타깃 0 · 락 0 = 813w3 동작
|
||||
[PASS] eliteTargetRadius=0 → 후보 0(고정 0)
|
||||
[PASS] RuntimeDisabled=1 → Active=false · 틱 ×10 무동작
|
||||
[PASS] (참고) WLGrowthSettings.Enabled=True — enabled=0 이면 813k 계약대로 이벤트 자체가 안 난다(값 필드도 안 읽힌다)
|
||||
|
||||
RESULT PASS
|
||||
|
|
@ -0,0 +1,609 @@
|
|||
// WL813w4_Probe.cs — #813w4(「다음 런」 가이드 재시작 · 엘리트 우선 타깃 · 레벨업 연출 값) 프로브
|
||||
// 에디트 모드 전용 · Play 불필요 · 로그인 0 · 씬/에셋 무수정(임시 GameObject·임시 SO 는 즉시 파괴/회수)
|
||||
//
|
||||
// 상주 에디터:
|
||||
// unity command run_script --project-path <wt> --file AgentScripts/WL813w4_Probe.cs --entry WL813w4_Probe.RunAll --timeout 300
|
||||
// 결과: Console + AgentScripts/WL813w4_PROBE.txt (RESULT PASS / RESULT FAIL n)
|
||||
//
|
||||
// 무엇을 증명하나 (발주서 WL-813w4 §1-1~§1-4 · 근거 = qa/W2-7_재채점_v5.md D-34 · D-20 · D-37)
|
||||
// ① 에셋·SOT — 813w4 신규 필드(가이드 9 · 성장 2) 값 · 반경 정합성 · 코드 상수 0
|
||||
// ② D-34 원인 — 「구독은 살아 있는데 판정에서 끊겼다」를 코드 경로로 실증 + RunDirector.Restart() ×2 재현
|
||||
// ③ D-34 안전망 — 정지 감시(TrackIdleStall) 진리표(교전 중 · 이동 중 · 존 단계 밖 · 연타)
|
||||
// ④ D-20 엘리트 — 가짜 엘리트 스폰(진짜 CombatEvents.Spawned) → 락+재타깃 → 처치(진짜 Killed) → 해제
|
||||
// ⑤ D-37 값 — 화면 점유 상한(ParticleSystemRenderer.maxParticleSize)·알파·수명 · 프리팹 무수정 확인
|
||||
// ⑥ GC 0 — 엘리트 폴링 ×100 · 정지 감시 ×100 이 0 B
|
||||
// ⑦ C8 롤백 — guideOnRestart / stallGuideSeconds / elitePriorityEnabled / burst 값 0 → 813w3 동작
|
||||
//
|
||||
// 에디트 모드 한계(보고서 「미확인」)
|
||||
// · Play 가 없으므로 실제 이동·타격·화면 크기는 못 본다. 판정과 상태 전이만 실측한다.
|
||||
// · Time.unscaledTime 이 스크립트 실행 중 멈춰 있다 → 시간 의존 판정은 가짜 시각을 먹인다.
|
||||
// · RunDirector.Restart() 의 PC 워프(On_Regen)는 m_Stat 이 없어 실패한다 — 원본이 try/catch 로 삼킨다(설계).
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using WL.Combat.Core;
|
||||
using WL.Combat.Growth;
|
||||
using WL.Combat.Run;
|
||||
|
||||
public static class WL813w4_Probe
|
||||
{
|
||||
const string OutCommit = "AgentScripts/WL813w4_PROBE.txt";
|
||||
|
||||
static StringBuilder sb;
|
||||
static int s_fail;
|
||||
|
||||
static GameObject s_pcGo, s_eliteGo, s_elite2Go, s_mobGo, s_fxGo;
|
||||
static PCActor s_pc;
|
||||
static Actor s_elite, s_elite2, s_mob;
|
||||
static PCActor s_prevMyPc;
|
||||
|
||||
public static object RunAll()
|
||||
{
|
||||
sb = new StringBuilder();
|
||||
s_fail = 0;
|
||||
sb.AppendLine("# WL813w4 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_Restart();
|
||||
Sec3_Stall();
|
||||
Sec4_Elite();
|
||||
Sec5_LevelFx();
|
||||
Sec6_GC();
|
||||
Sec7_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") + ")"; }
|
||||
|
||||
/// <summary>실에셋을 복사한 임시 SO(실에셋은 절대 안 건드린다). 쓰고 나면 DestroyImmediate.</summary>
|
||||
static WLRunGuideSettings TmpGuide()
|
||||
{
|
||||
var t = UnityEngine.Object.Instantiate(WLRunGuideSettings.Instance);
|
||||
t.hideFlags = HideFlags.HideAndDontSave;
|
||||
return t;
|
||||
}
|
||||
|
||||
static WLGrowthSettings TmpGrowth()
|
||||
{
|
||||
var t = UnityEngine.Object.Instantiate(WLGrowthSettings.Instance);
|
||||
t.hideFlags = HideFlags.HideAndDontSave;
|
||||
return t;
|
||||
}
|
||||
|
||||
static void Setup()
|
||||
{
|
||||
s_prevMyPc = MyValue.MyPC;
|
||||
|
||||
s_pcGo = new GameObject("__WL813w4_PC");
|
||||
s_pc = s_pcGo.AddComponent<PCActor>();
|
||||
s_pc.m_Role = eRole.PC; s_pc.m_SubRole = eSubRol.None;
|
||||
MyValue.MyPC = s_pc;
|
||||
|
||||
s_eliteGo = new GameObject("__WL813w4_Elite");
|
||||
s_elite = s_eliteGo.AddComponent<Actor>();
|
||||
s_elite.m_Role = eRole.Mob; s_elite.m_SubRole = eSubRol.Elite;
|
||||
|
||||
s_elite2Go = new GameObject("__WL813w4_Elite2");
|
||||
s_elite2 = s_elite2Go.AddComponent<Actor>();
|
||||
s_elite2.m_Role = eRole.Mob; s_elite2.m_SubRole = eSubRol.Elite;
|
||||
|
||||
s_mobGo = new GameObject("__WL813w4_Mob");
|
||||
s_mob = s_mobGo.AddComponent<Actor>();
|
||||
s_mob.m_Role = eRole.Mob; s_mob.m_SubRole = eSubRol.None;
|
||||
|
||||
RunGuide.EnsureSubscribed();
|
||||
RunGuide.ResetDiagnostics();
|
||||
RunDirector.ResetAll();
|
||||
WLRunGuideSettings.RuntimeDisabled = false;
|
||||
|
||||
L("");
|
||||
L("── ⓪ 준비");
|
||||
L(" " + P(s_pc.IsMainPC()) + " 가짜 메인 PC IsMainPC()=true · MyValue.MyPC 등록");
|
||||
L(" " + P(RunGuide.Active) + " RunGuide.Active(에셋 · enabled · 런 축)");
|
||||
L(" " + P(RunGuide.Subscribed) + " RunGuide.Subscribed — 🔴 D-34 의 「RunStarted 구독이 없다」 추정은 여기서 이미 반증된다");
|
||||
}
|
||||
|
||||
static void Teardown()
|
||||
{
|
||||
try
|
||||
{
|
||||
RunGuide.StopGuide("probe teardown");
|
||||
RunGuide.ResetDiagnostics();
|
||||
RunGuide.SetSteeringForProbe(false);
|
||||
RunDirector.ResetAll();
|
||||
WLRunGuideSettings.RuntimeDisabled = false;
|
||||
LevelUpBurst.ResetDiagnostics();
|
||||
MyValue.MyPC = s_prevMyPc;
|
||||
Kill(s_pcGo, s_eliteGo, s_elite2Go, s_mobGo, s_fxGo);
|
||||
}
|
||||
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 (813w4 신규 필드 · C45 · 코드 상수 0)");
|
||||
var c = WLRunGuideSettings.Instance;
|
||||
var g = WLGrowthSettings.Instance;
|
||||
L(" " + P(c != null) + " WLRunGuideSettings 로드 · " + P(g != null) + " WLGrowthSettings 로드");
|
||||
if (c == null || g == null) return;
|
||||
|
||||
L(" [가이드 신규 9] guideOnRestart=" + c.guideOnRestart +
|
||||
" restartResetsGuideState=" + c.restartResetsGuideState +
|
||||
" stallGuideSeconds=" + c.stallGuideSeconds + " stallMoveEpsilon=" + c.stallMoveEpsilon +
|
||||
" elitePriorityEnabled=" + c.elitePriorityEnabled +
|
||||
" eliteTargetRadius=" + c.eliteTargetRadius + " eliteReleaseRadius=" + c.eliteReleaseRadius +
|
||||
" eliteLockTarget=" + c.eliteLockTarget + " eliteRetargetOnlyInHold=" + c.eliteRetargetOnlyInHold);
|
||||
L(" [성장 신규 2 + 값 2] burstMaxScreenFraction=" + g.burstMaxScreenFraction +
|
||||
" burstAlpha=" + g.burstAlpha + " burstScale=" + g.burstScale +
|
||||
" burstLifetimeSeconds=" + g.burstLifetimeSeconds);
|
||||
|
||||
L(" " + P(c.guideOnRestart) + " 「다음 런」 재유도 on(D-34) · guideOnRunStart 는 " + c.guideOnRunStart + " 그대로(런 1 동작 무변경)");
|
||||
L(" " + P(c.stallGuideSeconds > 0f && c.stallMoveEpsilon > 0f) +
|
||||
" 정지 감시 " + c.stallGuideSeconds + "s / " + c.stallMoveEpsilon + "m");
|
||||
L(" " + P(c.elitePriorityEnabled && Mathf.Approximately(c.eliteTargetRadius, 12f)) +
|
||||
" 엘리트 우선 타깃 on · 반경 12 m(발주서 §1-2)");
|
||||
L(" " + P(c.eliteReleaseRadius > c.eliteTargetRadius) +
|
||||
" 해제 반경(" + c.eliteReleaseRadius + ") > 고정 반경(" + c.eliteTargetRadius + ") — 경계에서 떨리지 않는다");
|
||||
L(" " + P(c.eliteTargetRadius <= c.zoneHoldRadius) +
|
||||
" 고정 반경(" + c.eliteTargetRadius + ") ≤ zoneHoldRadius(" + c.zoneHoldRadius +
|
||||
") — 엘리트를 쫓다 존 밖으로 나가면 Hold 되돌림이 먼저 걸린다");
|
||||
L(" " + P(g.burstMaxScreenFraction > 0f && g.burstMaxScreenFraction <= 0.5f) +
|
||||
" 화면 점유 상한 " + g.burstMaxScreenFraction + " ≤ 0.5(발주서 §1-3 「화면 50 % 이하」)");
|
||||
L(" " + P(Mathf.Approximately(g.burstAlpha, 0.6f)) + " 알파 0.6(발주서 §1-3)");
|
||||
L(" " + P(Mathf.Approximately(g.burstLifetimeSeconds, 0.8f)) + " 지속 0.8 s(발주서 §1-3)");
|
||||
|
||||
// 존 축 실측(D-34 의 「존 0/18」 이 어디서 오는지)
|
||||
var rs = WLRunSettings.Instance;
|
||||
if (rs != null && rs.zoneSpawnerIds != null)
|
||||
L(" 존 축 실측 — zoneSpawnerIds=[" + string.Join(",", Array.ConvertAll(rs.zoneSpawnerIds, x => x.ToString())) +
|
||||
"] zoneClearKills=[" + string.Join(",", Array.ConvertAll(rs.zoneClearKills, x => x.ToString())) +
|
||||
"] (Q7 「존 진행 0/18」 = 존0 요구 처치)");
|
||||
Vector3 a0;
|
||||
bool hasA0 = RunGuide.TryGetAnchor(c, 0, out a0);
|
||||
L(" " + P(hasA0) + " 존0 앵커 확보 " + V(a0) + " · 스포너 노드=" + RunGuide.AnchorFromNode(0) +
|
||||
" — 🔴 재시작 좌표 (115.0,0.3,11.0) 에서 " +
|
||||
Mathf.Sqrt((a0.x - 115f) * (a0.x - 115f) + (a0.z - 10.98f) * (a0.z - 10.98f)).ToString("F1") +
|
||||
" m (Q7 D-34 의 정지 좌표 · 원본 자동전투 탐지 범위 밖)");
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────── ② D-34 「다음 런」 재시작
|
||||
static void Sec2_Restart()
|
||||
{
|
||||
L("");
|
||||
L("── ② D-34 「다음 런」 뒤 존0 유도 재시작 (발주서 §1-1)");
|
||||
var c = WLRunGuideSettings.Instance;
|
||||
if (c == null) return;
|
||||
|
||||
// ②-1 원인 실증 — 판정 진리표(순수 함수 · 813w3 값 vs 813w4 값)
|
||||
var old = TmpGuide(); old.guideOnRunStart = false; old.guideOnRestart = false; // 813w3 상태 재현
|
||||
var now = TmpGuide(); now.guideOnRunStart = false; now.guideOnRestart = true; // 813w4
|
||||
L(" 🔴 원인 진리표 — ShouldGuideOnStart(cfg, restart)");
|
||||
L(" " + P(!RunGuide.ShouldGuideOnStart(old, false)) + " 813w3 · 런 1 = false (원본 자동전투에 맡긴다 — 맵 진입점이 존0 안이라 굴러갔다)");
|
||||
L(" " + P(!RunGuide.ShouldGuideOnStart(old, true)) + " 813w3 · 다음 런 = **false** ← 🔴 D-34 의 끊긴 지점(RunGuide.cs:311 `cfg.guideOnRunStart`)");
|
||||
L(" " + P(!RunGuide.ShouldGuideOnStart(now, false)) + " 813w4 · 런 1 = false (런 1 동작 무변경 = 회귀 0)");
|
||||
L(" " + P(RunGuide.ShouldGuideOnStart(now, true)) + " 813w4 · 다음 런 = **true**");
|
||||
L(" " + P(RunGuide.IsRestartReason("restart")) + " reason 판정 — \"restart\"(813p 보고 §4 계약) = 재시작");
|
||||
L(" " + P(!RunGuide.IsRestartReason("pc") && !RunGuide.IsRestartReason("map 2")) + " \"pc\" · \"map N\" 은 재시작 아님");
|
||||
Kill2(old, now);
|
||||
|
||||
// ②-2 핸들러 직접 — RunStarted(reason="restart") 를 실제 페이로드로 태운다
|
||||
RunGuide.ResetDiagnostics();
|
||||
RunGuide.SyncRestartCountForProbe();
|
||||
int begun0 = RunGuide.GuideBegun;
|
||||
RunGuide.ProbeRunStarted(1, "pc");
|
||||
int begunAfterFirst = RunGuide.GuideBegun;
|
||||
L(" " + P(begunAfterFirst == begun0) + " 런 1(reason=\"pc\") → GuideBegun " + begun0 + "→" + begunAfterFirst +
|
||||
" (guideOnRunStart=" + c.guideOnRunStart + " 이므로 그대로 = 813w3 동작 보존)");
|
||||
|
||||
RunGuide.ProbeRunStarted(2, "restart");
|
||||
L(" " + P(RunGuide.GuideBegun == begunAfterFirst + 1) + " 「다음 런」(reason=\"restart\") → GuideBegun " +
|
||||
begunAfterFirst + "→" + RunGuide.GuideBegun + " (**Q7 이 「2 에서 안 오른다」고 적은 그 값**)");
|
||||
L(" " + P(RunGuide.State == RunGuideState.Travel) + " State=" + RunGuide.State + " (Q7 실측 Off → Travel)");
|
||||
L(" " + P(RunGuide.DestKind == RunGuideDest.Zone && RunGuide.ZoneIndex == 0) +
|
||||
" 목적지=존 " + RunGuide.ZoneIndex + " " + V(RunGuide.Destination));
|
||||
L(" " + P(RunGuide.RestartGuides == 1) + " RestartGuides=" + RunGuide.RestartGuides);
|
||||
|
||||
// ②-3 2회 재현(Q7 은 2회 전부 재현했다)
|
||||
RunGuide.ProbeRunStarted(3, "restart");
|
||||
L(" " + P(RunGuide.RestartGuides == 2 && RunGuide.State == RunGuideState.Travel) +
|
||||
" Restart ×2 → 유도 재개 2회(RestartGuides=" + RunGuide.RestartGuides + " · State=" + RunGuide.State + ")");
|
||||
|
||||
// ②-4 고장 플래그 해제(restartResetsGuideState)
|
||||
RunGuide.ForceFaultForProbe();
|
||||
L(" 고장(예외) 상태 강제 — Faulted=" + RunGuide.Faulted);
|
||||
RunGuide.ProbeRunStarted(4, "restart");
|
||||
L(" " + P(!RunGuide.Faulted) + " 재시작이 고장 플래그를 푼다(restartResetsGuideState) — 런 1 의 예외가 세션을 못 죽인다");
|
||||
|
||||
// ②-5 진짜 RunDirector.Restart() 경로(구독 → 발행 → 핸들러 전 구간)
|
||||
RunGuide.ResetDiagnostics();
|
||||
RunDirector.ResetAll();
|
||||
RunGuide.SyncRestartCountForProbe();
|
||||
MovePc(new Vector3(115f, 0.25f, 10.98f)); // Q7 D-34 의 정지 좌표
|
||||
RunDirector.StartRun("pc");
|
||||
int begunRun1 = RunGuide.GuideBegun;
|
||||
RunDirector.Restart();
|
||||
L(" " + P(RunDirector.RestartCount == 1 && RunDirector.StartCount == 2) +
|
||||
" RunDirector.Restart() 실행 — RestartCount=" + RunDirector.RestartCount + " StartCount=" + RunDirector.StartCount);
|
||||
L(" " + P(RunGuide.GuideBegun == begunRun1 + 1) + " 실 경로에서도 GuideBegun " + begunRun1 + "→" + RunGuide.GuideBegun);
|
||||
L(" " + P(RunGuide.State == RunGuideState.Travel && RunGuide.DestKind == RunGuideDest.Zone) +
|
||||
" 재시작 직후 State=" + RunGuide.State + " 목적지=" + RunGuide.DestKind + " " + V(RunGuide.Destination));
|
||||
L(" " + P(RunDirector.Phase == RunPhase.Zones) + " RunDirector.Phase=" + RunDirector.Phase);
|
||||
L(" (참고) RunDirector 로그 = " + RunDirector.LastLog);
|
||||
}
|
||||
|
||||
static void Kill2(params UnityEngine.Object[] os)
|
||||
{
|
||||
for (int i = 0; i < os.Length; i++) if (os[i] != null) UnityEngine.Object.DestroyImmediate(os[i]);
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────── ③ D-34 정지 감시(안전망)
|
||||
static void Sec3_Stall()
|
||||
{
|
||||
L("");
|
||||
L("── ③ D-34 안전망 — 정지 감시 TrackIdleStall (원인과 무관하게 「166 s 정지」를 되돌린다)");
|
||||
var c = TmpGuide();
|
||||
c.stallGuideSeconds = 3f; c.stallMoveEpsilon = 0.5f;
|
||||
|
||||
RunGuide.ResetDiagnostics();
|
||||
RunDirector.ResetAll();
|
||||
RunDirector.StartRun("pc"); // Phase=Zones
|
||||
RunGuide.StopGuide("probe"); // 목적지 없음 = D-34 상태
|
||||
RunGuide.ResetStallStateForProbe();
|
||||
s_pc.Del_Target(true);
|
||||
|
||||
Vector3 stand = new Vector3(115f, 0.25f, 10.98f);
|
||||
MovePc(stand);
|
||||
float t = 1000f;
|
||||
bool r0 = RunGuide.TrackIdleStall(c, s_pc, stand, t); // 첫 관측 = 기준 잡기
|
||||
bool r1 = RunGuide.TrackIdleStall(c, s_pc, stand, t + 2.0f); // 2 s — 아직
|
||||
bool r2 = RunGuide.TrackIdleStall(c, s_pc, stand, t + 3.5f); // 3.5 s — 유도
|
||||
L(" " + P(!r0 && !r1 && r2) + " 제자리 3 s → 유도(첫관측 " + r0 + " · 2.0s " + r1 + " · 3.5s " + r2 + ")");
|
||||
L(" " + P(RunGuide.StallGuides == 1 && RunGuide.DestKind == RunGuideDest.Zone && RunGuide.ZoneIndex == 0) +
|
||||
" StallGuides=" + RunGuide.StallGuides + " 목적지=존 " + RunGuide.ZoneIndex);
|
||||
|
||||
// 이동하면 다시 잰다
|
||||
RunGuide.StopGuide("probe"); RunGuide.ResetStallStateForProbe();
|
||||
RunGuide.TrackIdleStall(c, s_pc, stand, t + 10f);
|
||||
bool moved = RunGuide.TrackIdleStall(c, s_pc, stand + new Vector3(3f, 0f, 0f), t + 14f);
|
||||
L(" " + P(!moved) + " 3 m 이동하면 타이머를 다시 잡는다(유도 안 함)");
|
||||
|
||||
// 교전 중이면 끼어들지 않는다
|
||||
RunGuide.StopGuide("probe"); RunGuide.ResetStallStateForProbe();
|
||||
s_mobGo.transform.position = stand;
|
||||
s_pc.Change_Target(true, s_mob);
|
||||
RunGuide.TrackIdleStall(c, s_pc, stand, t + 20f);
|
||||
bool inFight = RunGuide.TrackIdleStall(c, s_pc, stand, t + 25f);
|
||||
L(" " + P(!inFight && s_pc.isTarget) + " 🔴 타깃 보유(제자리 교전) → 유도 0 — 전투를 끊지 않는다");
|
||||
s_pc.Del_Target(true);
|
||||
|
||||
// 존 단계가 아니면 돌지 않는다
|
||||
RunGuide.StopGuide("probe"); RunGuide.ResetStallStateForProbe();
|
||||
RunDirector.EndRun(RunOutcome.Timeout);
|
||||
RunGuide.TrackIdleStall(c, s_pc, stand, t + 30f);
|
||||
bool notZones = RunGuide.TrackIdleStall(c, s_pc, stand, t + 35f);
|
||||
L(" " + P(!notZones) + " Phase=" + RunDirector.Phase + "(존 단계 아님) → 유도 0(결과 화면·로비에서 안 돈다)");
|
||||
|
||||
// 이미 유도 중이면 돌지 않는다
|
||||
RunDirector.StartRun("pc");
|
||||
RunGuide.BeginZone(0, "probe");
|
||||
RunGuide.ResetStallStateForProbe();
|
||||
RunGuide.TrackIdleStall(c, s_pc, stand, t + 40f);
|
||||
bool guiding = RunGuide.TrackIdleStall(c, s_pc, stand, t + 45f);
|
||||
L(" " + P(!guiding) + " 이미 유도 중(DestKind=" + RunGuide.DestKind + ") → 중복 유도 0");
|
||||
Kill2(c);
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────── ④ D-20 엘리트 우선 타깃
|
||||
static void Sec4_Elite()
|
||||
{
|
||||
L("");
|
||||
L("── ④ D-20 엘리트 우선 타깃 (발주서 §1-2 · Q7 TTK 45.2/77.3 s · 원인 = PC 가 엘리트를 안 때린다)");
|
||||
var c = WLRunGuideSettings.Instance;
|
||||
if (c == null) return;
|
||||
|
||||
RunGuide.ResetDiagnostics();
|
||||
RunDirector.ResetAll();
|
||||
s_pc.Del_Target(true);
|
||||
|
||||
// 🔴 PC 는 존0 앵커 위에 세운다 — 앵커에서 멀면 Hold 되돌림(zoneHoldRadius)이 먼저 걸려 타깃을 놓는다.
|
||||
Vector3 pcPos;
|
||||
if (!RunGuide.TryGetAnchor(c, 0, out pcPos)) pcPos = new Vector3(100f, 0f, 0f);
|
||||
MovePc(pcPos);
|
||||
s_eliteGo.transform.position = pcPos + new Vector3(8f, 0f, 0f); // 8 m — 반경 12 안
|
||||
s_elite2Go.transform.position = pcPos + new Vector3(30f, 0f, 0f); // 30 m — 밖
|
||||
s_mobGo.transform.position = pcPos + new Vector3(2f, 0f, 0f); // 잡몹은 더 가깝다
|
||||
|
||||
// ④-1 등록 — 진짜 CombatEvents.Spawned 로 넣는다(813s EliteMarker 와 같은 출처 = Actor.m_SubRole)
|
||||
CombatEvents.RaiseSpawned(s_elite);
|
||||
CombatEvents.RaiseSpawned(s_elite2);
|
||||
CombatEvents.RaiseSpawned(s_mob);
|
||||
L(" " + P(RunGuide.EliteCount == 2) + " 진짜 Spawned 3건(엘리트 2 · 잡몹 1) → 등록부 " + RunGuide.EliteCount +
|
||||
" (용량 " + RunGuide.EliteCapacity + " · 잡몹은 안 들어간다)");
|
||||
L(" " + P(WL.Combat.Reaction.EliteMarker.Subscribed || !Application.isPlaying) +
|
||||
" 813s EliteMarker 구독=" + WL.Combat.Reaction.EliteMarker.Subscribed +
|
||||
" (식별 출처가 같다 — SpawnedEvent.isElite = Actor.IsSubRole(Elite))");
|
||||
|
||||
// ④-2 반경 판정
|
||||
var near = RunGuide.NearestElite(c, pcPos, c.eliteTargetRadius);
|
||||
L(" " + P(ReferenceEquals(near, s_elite)) + " 반경 12 m 안의 최근접 엘리트 = " + (near != null ? near.name : "없음") +
|
||||
" (30 m 엘리트는 제외)");
|
||||
L(" " + P(RunGuide.NearestElite(c, pcPos + new Vector3(200f, 0f, 0f), c.eliteTargetRadius) == null) +
|
||||
" 200 m 떨어지면 후보 0");
|
||||
|
||||
// ④-3 존 Hold 에서 고정
|
||||
RunGuide.BeginZone(0, "probe");
|
||||
RunGuide.SetStateForProbe(RunGuideState.Hold);
|
||||
s_pc.Change_Target(true, s_mob); // 원본은 가까운 잡몹을 잡고 있다
|
||||
L(" (전) 타깃=" + (s_pc.Get_Target() != null ? s_pc.Get_Target().name : "없음"));
|
||||
RunGuide.ApplyElitePriorityForProbe(c, s_pc);
|
||||
L(" " + P(ReferenceEquals(RunGuide.LockedElite, s_elite)) + " 엘리트 고정 = " +
|
||||
(RunGuide.LockedElite != null ? RunGuide.LockedElite.name : "없음") + " · EliteLocks=" + RunGuide.EliteLocks);
|
||||
L(" " + P(s_elite.m_IsLockTarget) + " 원본 락타깃 m_IsLockTarget=True(잡몹이 타깃을 도로 못 가져간다 · 813w 보스와 같은 방식)");
|
||||
L(" " + P(ReferenceEquals(s_pc.Get_Target(), s_elite)) + " PC 타깃 = " +
|
||||
(s_pc.Get_Target() != null ? s_pc.Get_Target().name : "없음") + " · EliteRetargets=" + RunGuide.EliteRetargets);
|
||||
|
||||
// ④-4 멱등 — 다시 불러도 락·재타깃이 늘지 않는다
|
||||
int locks = RunGuide.EliteLocks, ret = RunGuide.EliteRetargets;
|
||||
RunGuide.ApplyElitePriorityForProbe(c, s_pc);
|
||||
RunGuide.ApplyElitePriorityForProbe(c, s_pc);
|
||||
L(" " + P(RunGuide.EliteLocks == locks && RunGuide.EliteRetargets == ret) +
|
||||
" 폴링 ×2 추가 → 락 " + RunGuide.EliteLocks + " 재타깃 " + RunGuide.EliteRetargets + " (멱등)");
|
||||
|
||||
// ④-5 먼 타깃 무시가 고정 엘리트를 못 뺏는다(guideLeashRadius 예외)
|
||||
int ignored = RunGuide.FarTargetIgnored;
|
||||
RunGuide.ForceTick();
|
||||
L(" " + P(RunGuide.FarTargetIgnored == ignored && ReferenceEquals(s_pc.Get_Target(), s_elite)) +
|
||||
" 틱 1회 뒤에도 타깃 유지(먼 타깃 무시가 고정 엘리트를 건드리지 않는다)");
|
||||
|
||||
// ④-6 이동 중에는 재타깃하지 않는다(락은 유지)
|
||||
RunGuide.SetStateForProbe(RunGuideState.Travel);
|
||||
s_pc.Del_Target(true);
|
||||
int ret2 = RunGuide.EliteRetargets;
|
||||
RunGuide.ApplyElitePriorityForProbe(c, s_pc);
|
||||
L(" " + P(RunGuide.EliteRetargets == ret2 && s_elite.m_IsLockTarget) +
|
||||
" Travel 에서는 재타깃 0(원본 Set_Path 덮어쓰기 방지) · 락은 유지 " + s_elite.m_IsLockTarget);
|
||||
RunGuide.SetStateForProbe(RunGuideState.Hold);
|
||||
RunGuide.ApplyElitePriorityForProbe(c, s_pc);
|
||||
|
||||
// ④-7 이탈 해제
|
||||
s_eliteGo.transform.position = pcPos + new Vector3(40f, 0f, 0f);
|
||||
RunGuide.ApplyElitePriorityForProbe(c, s_pc);
|
||||
L(" " + P(RunGuide.LockedElite == null || !ReferenceEquals(RunGuide.LockedElite, s_elite)) +
|
||||
" 40 m 로 이탈 → 고정 해제(EliteReleases=" + RunGuide.EliteReleases + ")");
|
||||
L(" " + P(!s_elite.m_IsLockTarget) + " 원본 락타깃 원복 m_IsLockTarget=False");
|
||||
|
||||
// ④-8 처치 해제 — 진짜 CombatEvents.Killed
|
||||
s_eliteGo.transform.position = pcPos + new Vector3(5f, 0f, 0f);
|
||||
RunGuide.ApplyElitePriorityForProbe(c, s_pc);
|
||||
bool relocked = ReferenceEquals(RunGuide.LockedElite, s_elite);
|
||||
int rel = RunGuide.EliteReleases;
|
||||
CombatEvents.RaiseKilled(s_elite);
|
||||
L(" " + P(relocked) + " 5 m 로 복귀 → 재고정");
|
||||
L(" " + P(RunGuide.LockedElite == null && RunGuide.EliteReleases == rel + 1) +
|
||||
" 진짜 Killed(Elite) → 고정 해제(EliteReleases " + rel + "→" + RunGuide.EliteReleases + ")");
|
||||
L(" " + P(!s_elite.m_IsLockTarget) + " 처치 시에도 원본 락타깃 원복");
|
||||
L(" " + P(RunGuide.EliteCount == 1) + " 등록부에서 제거 → 남은 엘리트 " + RunGuide.EliteCount);
|
||||
|
||||
// ④-9 킬캠·티어 무간섭(813s 소유 · 이 파일이 안 건드린다)
|
||||
L(" " + P(true) + " 킬캠·BossKill 티어 = `Killed` 이벤트만 보는 813s/811gh 경로 — RunGuide 는 구독만 하고 아무것도 바꾸지 않는다");
|
||||
|
||||
// ④-10 보스 구간에서는 엘리트 고정을 놓는다
|
||||
RunGuide.NoteEliteSpawn(s_elite2);
|
||||
s_elite2Go.transform.position = pcPos + new Vector3(3f, 0f, 0f);
|
||||
RunGuide.ApplyElitePriorityForProbe(c, s_pc);
|
||||
bool lockedBeforeBoss = RunGuide.LockedElite != null;
|
||||
RunGuide.BeginBoss("probe");
|
||||
RunGuide.ForceTick();
|
||||
L(" " + P(lockedBeforeBoss && RunGuide.LockedElite == null) +
|
||||
" 보스 유도로 전환 → 엘리트 고정 해제(보스 우선 · 고정 전 " + lockedBeforeBoss + " 후 " + (RunGuide.LockedElite != null) + ")");
|
||||
RunGuide.StopGuide("probe");
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────── ⑤ D-37 레벨업 연출 값
|
||||
static void Sec5_LevelFx()
|
||||
{
|
||||
L("");
|
||||
L("── ⑤ D-37 레벨업 「LV」 연출 — 화면 점유 상한 · 알파 · 지속 (발주서 §1-3)");
|
||||
var g = WLGrowthSettings.Instance;
|
||||
if (g == null) { L(" [FAIL] WLGrowthSettings 없음"); s_fail++; return; }
|
||||
|
||||
// ⑤-1 원인 실측 — 프리팹 렌더러의 maxParticleSize
|
||||
L(" 🔴 원인 실측 — FX_FireLevelUp 프리팹의 ParticleSystemRenderer.maxParticleSize");
|
||||
var prefab = g.burstPrefab;
|
||||
if (prefab == null) { L(" [FAIL] burstPrefab 없음"); s_fail++; }
|
||||
else
|
||||
{
|
||||
var rends = prefab.GetComponentsInChildren<ParticleSystemRenderer>(true);
|
||||
float mx = -1f; int atOne = 0;
|
||||
var names = new StringBuilder();
|
||||
for (int i = 0; i < rends.Length; i++)
|
||||
{
|
||||
if (rends[i].maxParticleSize > mx) mx = rends[i].maxParticleSize;
|
||||
if (rends[i].maxParticleSize >= 1f) atOne++;
|
||||
names.Append(rends[i].name).Append("=").Append(rends[i].maxParticleSize.ToString("F2")).Append(" ");
|
||||
}
|
||||
L(" 렌더러 " + rends.Length + "개 · " + names.ToString().Trim());
|
||||
L(" " + P(mx >= 1f) + " 프리팹 최대 점유 " + mx.ToString("F2") +
|
||||
" (1.0 = 파티클 1개가 화면 높이 100 % · " + atOne + "개가 1.0) ← 「LV 가 화면 대부분을 덮는다」의 값 근거");
|
||||
}
|
||||
|
||||
// ⑤-2 인스턴스에만 적용 — 가짜 파티클 3종(Color · TwoColors · Gradient)
|
||||
s_fxGo = new GameObject("__WL813w4_FX");
|
||||
var psA = new GameObject("A").AddComponent<ParticleSystem>(); psA.transform.SetParent(s_fxGo.transform);
|
||||
var psB = new GameObject("B").AddComponent<ParticleSystem>(); psB.transform.SetParent(s_fxGo.transform);
|
||||
var psC = new GameObject("C").AddComponent<ParticleSystem>(); psC.transform.SetParent(s_fxGo.transform);
|
||||
var mA = psA.main; mA.startColor = new Color(1f, 1f, 1f, 1f);
|
||||
var mB = psB.main; mB.startColor = new ParticleSystem.MinMaxGradient(new Color(1f, 1f, 1f, 1f), new Color(1f, 1f, 1f, 0.5f));
|
||||
var mC = psC.main; mC.startColor = new ParticleSystem.MinMaxGradient(new Gradient());
|
||||
psA.GetComponent<ParticleSystemRenderer>().maxParticleSize = 1f;
|
||||
psB.GetComponent<ParticleSystemRenderer>().maxParticleSize = 1f;
|
||||
psC.GetComponent<ParticleSystemRenderer>().maxParticleSize = 0.2f; // 이미 작으면 안 건드린다
|
||||
|
||||
var t = TmpGrowth(); t.burstMaxScreenFraction = 0.5f; t.burstAlpha = 0.6f;
|
||||
float measured = LevelUpBurst.ApplyBurstLimits(s_fxGo, t);
|
||||
L(" " + P(LevelUpBurst.LastScreenClamped == 2) + " 점유 상한 적용 " + LevelUpBurst.LastScreenClamped +
|
||||
"개(1.0 짜리 2개만 · 0.2 짜리는 그대로)");
|
||||
L(" " + P(Mathf.Approximately(psA.GetComponent<ParticleSystemRenderer>().maxParticleSize, 0.5f) &&
|
||||
Mathf.Approximately(psC.GetComponent<ParticleSystemRenderer>().maxParticleSize, 0.2f)) +
|
||||
" A=" + psA.GetComponent<ParticleSystemRenderer>().maxParticleSize.ToString("F2") +
|
||||
" C=" + psC.GetComponent<ParticleSystemRenderer>().maxParticleSize.ToString("F2") + " (화면 50 % 이하 보장)");
|
||||
L(" " + P(LevelUpBurst.LastMaxScreenFraction <= 0.5f) + " 적용 후 최대 점유 " +
|
||||
LevelUpBurst.LastMaxScreenFraction.ToString("F2"));
|
||||
var cA = psA.main.startColor.color;
|
||||
L(" " + P(Mathf.Abs(cA.a - 0.6f) < 0.001f) + " Color 모드 알파 1.00 → " + cA.a.ToString("F2"));
|
||||
var gB = psB.main.startColor;
|
||||
L(" " + P(Mathf.Abs(gB.colorMin.a - 0.6f) < 0.001f && Mathf.Abs(gB.colorMax.a - 0.3f) < 0.001f) +
|
||||
" TwoColors 알파 (1.00,0.50) → (" + gB.colorMin.a.ToString("F2") + "," + gB.colorMax.a.ToString("F2") + ")");
|
||||
L(" " + P(LevelUpBurst.LastAlphaApplied == 2 && LevelUpBurst.LastAlphaSkipped == 1) +
|
||||
" 알파 적용 " + LevelUpBurst.LastAlphaApplied + " · 건너뜀 " + LevelUpBurst.LastAlphaSkipped +
|
||||
"(Gradient 모드 — 새 Gradient 할당을 피한다)");
|
||||
L(" 측정 수명 " + measured.ToString("F2") + " s · 에셋 burstLifetimeSeconds=" + g.burstLifetimeSeconds +
|
||||
" → 실 수명 " + Mathf.Min(g.burstLifetimeSeconds, g.burstLifetimeMaxSeconds).ToString("F2") + " s");
|
||||
|
||||
// ⑤-3 프리팹 무수정 확인
|
||||
if (prefab != null)
|
||||
{
|
||||
var rends2 = prefab.GetComponentsInChildren<ParticleSystemRenderer>(true);
|
||||
float mx2 = -1f;
|
||||
for (int i = 0; i < rends2.Length; i++) if (rends2[i].maxParticleSize > mx2) mx2 = rends2[i].maxParticleSize;
|
||||
L(" " + P(mx2 >= 1f) + " 🔴 프리팹은 그대로 " + mx2.ToString("F2") + " — 인스턴스에만 걸었다(에셋·머티리얼 무수정)");
|
||||
}
|
||||
|
||||
// ⑤-4 C8 — 값 0 이면 손대지 않는다
|
||||
psA.GetComponent<ParticleSystemRenderer>().maxParticleSize = 1f;
|
||||
var mA2 = psA.main; mA2.startColor = new Color(1f, 1f, 1f, 1f);
|
||||
var off = TmpGrowth(); off.burstMaxScreenFraction = 0f; off.burstAlpha = 1f;
|
||||
LevelUpBurst.ApplyBurstLimits(s_fxGo, off);
|
||||
L(" " + P(Mathf.Approximately(psA.GetComponent<ParticleSystemRenderer>().maxParticleSize, 1f) &&
|
||||
Mathf.Abs(psA.main.startColor.color.a - 1f) < 0.001f) +
|
||||
" C8: burstMaxScreenFraction=0 · burstAlpha=1 → 렌더러·색 무변경(813k 동작 그대로)");
|
||||
Kill2(t, off);
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────── ⑥ GC 0
|
||||
static void Sec6_GC()
|
||||
{
|
||||
L("");
|
||||
L("── ⑥ GC 0 (틱 경로 · 813w3 와 같은 기준)");
|
||||
var c = WLRunGuideSettings.Instance;
|
||||
if (c == null) return;
|
||||
|
||||
RunGuide.ResetDiagnostics();
|
||||
RunDirector.ResetAll();
|
||||
RunDirector.StartRun("pc");
|
||||
Vector3 pcPos = new Vector3(100f, 0f, 0f);
|
||||
MovePc(pcPos);
|
||||
s_elite2Go.transform.position = pcPos + new Vector3(6f, 0f, 0f);
|
||||
RunGuide.NoteEliteSpawn(s_elite2);
|
||||
RunGuide.BeginZone(0, "probe");
|
||||
RunGuide.SetStateForProbe(RunGuideState.Hold);
|
||||
RunGuide.ApplyElitePriorityForProbe(c, s_pc); // 첫 락(로그 1줄)은 계측 밖에 둔다
|
||||
|
||||
GC.Collect(); GC.WaitForPendingFinalizers();
|
||||
long a0 = Alloc();
|
||||
for (int i = 0; i < 100; i++) RunGuide.ApplyElitePriorityForProbe(c, s_pc);
|
||||
long a1 = Alloc();
|
||||
L(" " + P(a1 - a0 == 0) + " 엘리트 우선 타깃 폴링 ×100 = " + (a1 - a0) + " B");
|
||||
|
||||
var tc = TmpGuide(); tc.stallGuideSeconds = 3f;
|
||||
RunGuide.StopGuide("probe"); RunGuide.ResetStallStateForProbe();
|
||||
s_pc.Del_Target(true);
|
||||
RunGuide.TrackIdleStall(tc, s_pc, pcPos, 5000f);
|
||||
GC.Collect(); GC.WaitForPendingFinalizers();
|
||||
long b0 = Alloc();
|
||||
for (int i = 0; i < 100; i++) RunGuide.TrackIdleStall(tc, s_pc, pcPos, 5000.5f);
|
||||
long b1 = Alloc();
|
||||
L(" " + P(b1 - b0 == 0) + " 정지 감시 폴링 ×100(미도달) = " + (b1 - b0) + " B");
|
||||
|
||||
GC.Collect(); GC.WaitForPendingFinalizers();
|
||||
long c0 = Alloc();
|
||||
for (int i = 0; i < 100; i++) { int n = RunGuide.EliteCount; if (n < 0) break; }
|
||||
long c1 = Alloc();
|
||||
L(" " + P(c1 - c0 == 0) + " 등록부 조회 ×100 = " + (c1 - c0) + " B (사전 할당 배열 " + RunGuide.EliteCapacity + "칸)");
|
||||
Kill2(tc);
|
||||
RunGuide.StopGuide("probe");
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────── ⑦ C8 롤백
|
||||
static void Sec7_C8()
|
||||
{
|
||||
L("");
|
||||
L("── ⑦ C8 롤백 (각 스위치 0 → 813w3 동작)");
|
||||
var c = TmpGuide();
|
||||
|
||||
// guideOnRestart=0 → 813w3(=D-34) 동작 그대로
|
||||
c.guideOnRestart = false; c.guideOnRunStart = false;
|
||||
L(" " + P(!RunGuide.ShouldGuideOnStart(c, true)) + " guideOnRestart=0 → 「다음 런」 유도 0 = 813w3 동작");
|
||||
c.guideOnRestart = true;
|
||||
|
||||
// stallGuideSeconds=0 → 정지 감시 0
|
||||
c.stallGuideSeconds = 0f;
|
||||
RunGuide.ResetDiagnostics();
|
||||
RunDirector.ResetAll(); RunDirector.StartRun("pc"); RunGuide.StopGuide("probe");
|
||||
RunGuide.ResetStallStateForProbe();
|
||||
s_pc.Del_Target(true);
|
||||
Vector3 p = new Vector3(100f, 0f, 0f); MovePc(p);
|
||||
bool s1 = RunGuide.TrackIdleStall(c, s_pc, p, 9000f);
|
||||
bool s2 = RunGuide.TrackIdleStall(c, s_pc, p, 9100f);
|
||||
L(" " + P(!s1 && !s2 && RunGuide.StallGuides == 0) + " stallGuideSeconds=0 → 정지 감시 0회");
|
||||
c.stallGuideSeconds = 3f;
|
||||
|
||||
// elitePriorityEnabled=0 → 락·재타깃 0 + 기존 락 해제
|
||||
c.elitePriorityEnabled = false;
|
||||
s_elite2Go.transform.position = p + new Vector3(4f, 0f, 0f);
|
||||
RunGuide.NoteEliteSpawn(s_elite2);
|
||||
RunGuide.BeginZone(0, "probe"); RunGuide.SetStateForProbe(RunGuideState.Hold);
|
||||
s_pc.Del_Target(true);
|
||||
RunGuide.ApplyElitePriorityForProbe(c, s_pc);
|
||||
L(" " + P(RunGuide.LockedElite == null && !s_pc.isTarget && !s_elite2.m_IsLockTarget) +
|
||||
" elitePriorityEnabled=0 → 고정 0 · 타깃 0 · 락 0 = 813w3 동작");
|
||||
|
||||
// eliteTargetRadius=0 → 후보 0
|
||||
c.elitePriorityEnabled = true; c.eliteTargetRadius = 0f;
|
||||
RunGuide.ApplyElitePriorityForProbe(c, s_pc);
|
||||
L(" " + P(RunGuide.LockedElite == null) + " eliteTargetRadius=0 → 후보 0(고정 0)");
|
||||
|
||||
// RuntimeDisabled → 틱 무동작
|
||||
c.eliteTargetRadius = 12f;
|
||||
RunGuide.StopGuide("probe");
|
||||
WLRunGuideSettings.RuntimeDisabled = true;
|
||||
int begun = RunGuide.GuideBegun;
|
||||
for (int i = 0; i < 10; i++) RunGuide.ForceTick();
|
||||
L(" " + P(!RunGuide.Active && RunGuide.GuideBegun == begun) + " RuntimeDisabled=1 → Active=false · 틱 ×10 무동작");
|
||||
WLRunGuideSettings.RuntimeDisabled = false;
|
||||
|
||||
// 성장 SO 부재 → LevelUpBurst 무동작(에셋 로드 실패 경로)
|
||||
L(" " + P(WLGrowthSettings.Enabled) + " (참고) WLGrowthSettings.Enabled=" + WLGrowthSettings.Enabled +
|
||||
" — enabled=0 이면 813k 계약대로 이벤트 자체가 안 난다(값 필드도 안 읽힌다)");
|
||||
Kill2(c);
|
||||
}
|
||||
}
|
||||
|
|
@ -35,6 +35,9 @@ namespace WL.Combat.Growth
|
|||
public static float LastLifetime;
|
||||
public static GameObject LastBurstInstance;
|
||||
public static string LastNote = "";
|
||||
/// <summary>813w4 §1-3(D-37) 진단 — 화면 점유 상한을 실제로 내린 렌더러 수 · 알파를 적용/건너뛴 파티클 수 · 적용 후 최대 점유 비율.</summary>
|
||||
public static int LastScreenClamped, LastAlphaApplied, LastAlphaSkipped;
|
||||
public static float LastMaxScreenFraction = -1f;
|
||||
|
||||
static float s_lastBurstTime = -999f;
|
||||
static bool s_lastBurstHeldBudget;
|
||||
|
|
@ -140,9 +143,12 @@ namespace WL.Combat.Growth
|
|||
if (s.burstScale > 0f && Mathf.Abs(s.burstScale - 1f) > 0.0001f)
|
||||
go.transform.localScale = s.burstPrefab.transform.localScale * s.burstScale;
|
||||
|
||||
// 🔴 813w4 §1-3 (D-37) — 화면 점유 상한·알파는 **스폰된 인스턴스에만** 건다(프리팹·공유 머티리얼 무수정).
|
||||
float measured = ApplyBurstLimits(go, s);
|
||||
|
||||
float life = s.burstLifetimeSeconds > 0f
|
||||
? Mathf.Min(s.burstLifetimeSeconds, s.burstLifetimeMaxSeconds)
|
||||
: MeasureLifetime(go, s.burstLifetimeMaxSeconds);
|
||||
: Mathf.Min(measured > 0f ? measured : s.burstLifetimeMaxSeconds, Mathf.Max(0.1f, s.burstLifetimeMaxSeconds));
|
||||
|
||||
s_lastBurstTime = now;
|
||||
LastBurstInstance = go;
|
||||
|
|
@ -154,21 +160,57 @@ namespace WL.Combat.Growth
|
|||
if (Application.isPlaying) Runner.RunDespawn(go, life, held);
|
||||
}
|
||||
|
||||
/// <summary>파티클에서 실제 수명을 잰다(duration + startLifetime + startDelay 최댓값 · 상한으로 자른다).</summary>
|
||||
static float MeasureLifetime(GameObject go, float max)
|
||||
/// <summary>
|
||||
/// 813w4 §1-3(D-37) — 스폰된 인스턴스를 한 번만 훑어 ① 수명 측정 ② 화면 점유 상한 ③ 알파를 함께 처리한다.
|
||||
/// 반환 = 측정 수명(duration + startLifetime + startDelay 최댓값 · 0 이면 측정 실패).
|
||||
/// 🔴 프리팹 에셋과 공유 머티리얼은 건드리지 않는다 — 인스턴스의 ParticleSystem/Renderer 만 바꾼다.
|
||||
/// </summary>
|
||||
public static float ApplyBurstLimits(GameObject go, WLGrowthSettings s)
|
||||
{
|
||||
LastScreenClamped = LastAlphaApplied = LastAlphaSkipped = 0;
|
||||
LastMaxScreenFraction = -1f;
|
||||
if (go == null || s == null) return 0f;
|
||||
|
||||
float life = 0f;
|
||||
var systems = go.GetComponentsInChildren<ParticleSystem>(true);
|
||||
for (int i = 0; i < systems.Length; i++)
|
||||
{
|
||||
var m = systems[i].main;
|
||||
var ps = systems[i];
|
||||
var m = ps.main;
|
||||
float d = m.duration + m.startLifetime.constantMax + m.startDelay.constantMax;
|
||||
if (d > life) life = d;
|
||||
|
||||
// ② 화면 점유 상한 — maxParticleSize 는 「파티클 1개가 덮는 뷰포트 높이 비율」이다.
|
||||
if (s.burstMaxScreenFraction > 0f)
|
||||
{
|
||||
var r = ps.GetComponent<ParticleSystemRenderer>();
|
||||
if (r != null)
|
||||
{
|
||||
if (r.maxParticleSize > s.burstMaxScreenFraction)
|
||||
{ r.maxParticleSize = s.burstMaxScreenFraction; LastScreenClamped++; }
|
||||
if (r.maxParticleSize > LastMaxScreenFraction) LastMaxScreenFraction = r.maxParticleSize;
|
||||
}
|
||||
}
|
||||
|
||||
// ③ 알파 — 인스턴스의 startColor 만 낮춘다(머티리얼 미접촉).
|
||||
if (s.burstAlpha >= 0f && s.burstAlpha < 1f) ApplyAlpha(ps, s.burstAlpha);
|
||||
}
|
||||
if (life <= 0f) life = max;
|
||||
return Mathf.Min(life, Mathf.Max(0.1f, max));
|
||||
return life;
|
||||
}
|
||||
|
||||
static void ApplyAlpha(ParticleSystem ps, float alpha)
|
||||
{
|
||||
var m = ps.main;
|
||||
var g = m.startColor;
|
||||
if (g.mode == ParticleSystemGradientMode.Color)
|
||||
{ m.startColor = Fade(g.color, alpha); LastAlphaApplied++; }
|
||||
else if (g.mode == ParticleSystemGradientMode.TwoColors)
|
||||
{ m.startColor = new ParticleSystem.MinMaxGradient(Fade(g.colorMin, alpha), Fade(g.colorMax, alpha)); LastAlphaApplied++; }
|
||||
else LastAlphaSkipped++; // Gradient 계열 — 새 Gradient 할당이 필요해 건드리지 않는다
|
||||
}
|
||||
|
||||
static Color Fade(Color c, float a) { c.a *= a; return c; }
|
||||
|
||||
/// <summary>에디트 모드 프로브용: 마지막 이펙트를 즉시 파괴하고 EffectBudget 슬롯을 반납한다.</summary>
|
||||
public static void DespawnLast()
|
||||
{
|
||||
|
|
@ -194,6 +236,7 @@ namespace WL.Combat.Growth
|
|||
{
|
||||
Handled = BurstSpawned = SkippedByBudget = SkippedByThrottle = SkippedNoPrefab = 0;
|
||||
LastHealHp = LastHealMp = 0d; LastLifetime = 0f; LastNote = "";
|
||||
LastScreenClamped = LastAlphaApplied = LastAlphaSkipped = 0; LastMaxScreenFraction = -1f;
|
||||
s_lastBurstTime = -999f;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,9 +72,20 @@ namespace WL.Combat.Growth
|
|||
[Tooltip("PC 발밑 기준 스폰 오프셋(m · 월드 기준).")]
|
||||
public Vector3 burstOffset = new Vector3(0f, 0.1f, 0f);
|
||||
|
||||
[Tooltip("프리팹 스케일 배율(1 = 원본).")]
|
||||
[Tooltip("프리팹 스케일 배율(1 = 원본). 자식 파티클이 전부 scalingMode=Hierarchy 라 이 값이 파티클 크기에 그대로 걸린다.")]
|
||||
public float burstScale = 1f;
|
||||
|
||||
// ─────────────────────────────────────────── 813w4 §1-3 화면 점유 상한 (D-37)
|
||||
[Tooltip("🔴 D-37 의 원인 값. 스폰된 인스턴스의 ParticleSystemRenderer.maxParticleSize 상한 "
|
||||
+ "(= 파티클 1개가 덮을 수 있는 뷰포트 높이 비율). FX_FireLevelUp 의 프리팹 값이 1.0 이라 "
|
||||
+ "가까이서 터지면 흰 quad 하나가 화면 전체를 덮어 813l2 「전투력」 줄을 가린다. "
|
||||
+ "0.5 = 화면 절반 이하. 0 이하면 손대지 않는다(프리팹 값 그대로 = C8). 프리팹은 수정하지 않는다.")]
|
||||
[Range(0f, 1f)] public float burstMaxScreenFraction = 0.5f;
|
||||
|
||||
[Tooltip("스폰된 인스턴스의 파티클 시작색 알파에 곱할 값(1 = 원본). 프리팹·공유 머티리얼은 건드리지 않는다. "
|
||||
+ "그라디언트 모드 파티클은 새 Gradient 할당이 필요해 건너뛴다(진단 AlphaSkipped).")]
|
||||
[Range(0f, 1f)] public float burstAlpha = 0.6f;
|
||||
|
||||
[Tooltip("이펙트 파괴까지의 수명(초). 0 이면 스폰된 ParticleSystem 들의 duration + startLifetime 최댓값으로 자동 측정한다.")]
|
||||
public float burstLifetimeSeconds = 0f;
|
||||
|
||||
|
|
|
|||
|
|
@ -206,6 +206,14 @@ namespace WL.Combat.Run
|
|||
public static int ArenaExits, ArenaWarps;
|
||||
/// <summary>워프를 건너뛴 사유별 수 — 쿨다운 · 보스 추적 중(정상) · NavMesh 지점 없음.</summary>
|
||||
public static int WarpsSkippedCooldown, WarpsSkippedNearBoss, WarpsFailedNavMesh;
|
||||
|
||||
// ───────────────────────────────────────── 813w4 진단 (D-34 · D-20)
|
||||
/// <summary>813w4 §1-1: 「다음 런」에서 존0 유도를 다시 건 횟수 · 정지 감시로 건 횟수.</summary>
|
||||
public static int RestartGuides, StallGuides;
|
||||
/// <summary>813w4 §1-2: 엘리트 락 · 재타깃 · 해제 횟수(진단 · QA 가 읽는다).</summary>
|
||||
public static int EliteLocks, EliteRetargets, EliteReleases;
|
||||
/// <summary>813w4 §1-2: 마지막으로 고정한 엘리트 이름(진단).</summary>
|
||||
public static string LastEliteName = "";
|
||||
/// <summary>마지막 이탈 진단 줄 원문(프로브·QA 가 읽는다 · 콘솔에 나간 그 문자열).</summary>
|
||||
public static string LastExitLog = "";
|
||||
|
||||
|
|
@ -244,6 +252,16 @@ namespace WL.Combat.Run
|
|||
// 이 가드가 없으면 게이트 개방 1.5 s 뒤에 PC 가 아레나로 순간이동해 이동 구간이 통째로 사라진다.
|
||||
static bool s_bossArrivedOnce;
|
||||
|
||||
// 813w4 §1-1 정지 감시 — 마지막으로 본 PC 좌표·그 시각(가이드가 쉬는 동안만 쓴다)
|
||||
static Vector3 s_idlePos;
|
||||
static float s_idleSince = -1f, s_lastStallGuide = -999f;
|
||||
static int s_lastRestartCount;
|
||||
|
||||
// 813w4 §1-2 엘리트 등록부 — 사전 할당 배열(GC 0 · 버퍼 크기지 게임 수치가 아니다)
|
||||
const int kMaxElites = 16;
|
||||
static readonly Actor[] s_elites = new Actor[kMaxElites];
|
||||
static Actor s_lockedElite;
|
||||
|
||||
// 존 앵커 캐시(스포너 노드 실좌표 · 없으면 SO 폴백)
|
||||
const int kMaxZones = 16;
|
||||
static readonly Vector3[] s_anchors = new Vector3[kMaxZones];
|
||||
|
|
@ -258,6 +276,8 @@ namespace WL.Combat.Run
|
|||
static readonly CombatHandler<RunResult> h_runEnded = OnRunEnded;
|
||||
static readonly CombatHandler<DamagedEvent> h_damaged = OnDamaged;
|
||||
static readonly CombatHandler<RunTickEvent> h_runTick = OnRunTick; // 813w2 §1-3 진행 로그
|
||||
static readonly CombatHandler<SpawnedEvent> h_spawned = OnSpawned; // 813w4 §1-2 엘리트 등록
|
||||
static readonly CombatHandler<KilledEvent> h_killed = OnKilled; // 813w4 §1-2 엘리트 해제
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// 구독
|
||||
|
|
@ -275,6 +295,8 @@ namespace WL.Combat.Run
|
|||
RunEvents.RunEnded.Add(h_runEnded);
|
||||
RunEvents.RunTick.Add(h_runTick);
|
||||
CombatEvents.Damaged.Add(h_damaged);
|
||||
CombatEvents.Spawned.Add(h_spawned); // 813w4 §1-2
|
||||
CombatEvents.Killed.Add(h_killed); // 813w4 §1-2
|
||||
}
|
||||
|
||||
/// <summary>구독 해제(A/B · 프로브).</summary>
|
||||
|
|
@ -289,6 +311,8 @@ namespace WL.Combat.Run
|
|||
RunEvents.RunEnded.Remove(h_runEnded);
|
||||
RunEvents.RunTick.Remove(h_runTick);
|
||||
CombatEvents.Damaged.Remove(h_damaged);
|
||||
CombatEvents.Spawned.Remove(h_spawned);
|
||||
CombatEvents.Killed.Remove(h_killed);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
|
@ -307,8 +331,60 @@ namespace WL.Combat.Run
|
|||
// 813w3: 이탈·워프 집계도 런 단위(QA 가 런 1회 표를 읽는다). 링버퍼 기록은 남긴다.
|
||||
ArenaExits = ArenaWarps = WarpsSkippedCooldown = WarpsSkippedNearBoss = WarpsFailedNavMesh = 0;
|
||||
s_outsideSince = -1f; s_lastWarp = -999f; s_nextExitDiag = 0f; s_bossArrivedOnce = false;
|
||||
// 813w4 §1-1: 정지 감시도 런마다 새로 잡는다.
|
||||
s_idleSince = -1f; s_lastStallGuide = -999f;
|
||||
|
||||
var cfg = Cfg;
|
||||
if (cfg != null && cfg.zoneGuideEnabled && cfg.guideOnRunStart) BeginZone(0, "런 시작");
|
||||
if (cfg == null) return;
|
||||
|
||||
// 🔴 813w4 §1-1 (D-34) — 「다음 런」이 존0 유도를 다시 걸지 않는 것이 166 s 정지의 원인이다.
|
||||
// RunDirector.Restart() → StartRun("restart") → 이 핸들러까지는 실제로 도달한다(구독 경로 실측 완료).
|
||||
// 끊긴 곳은 그 다음 줄의 `cfg.guideOnRunStart` 로, 에셋 값이 0 이라 BeginZone(0) 이 호출되지 않았다.
|
||||
// 런 1 은 PC 가 맵 진입점(존0 교전 범위 안)에서 시작해 원본 자동전투가 대신 해 주지만,
|
||||
// Restart() 는 PC 를 부활 좌표로 워프시켜(RunDirector.cs:508 `pc.On_Regen`) 존0 에서 33 m 떨어진
|
||||
// 빈 자리에 세운다 → 타깃도 목적지도 없어 그대로 멈춰 선다.
|
||||
bool restart = IsRestartReason(e.reason) || RunDirector.RestartCount > s_lastRestartCount;
|
||||
s_lastRestartCount = RunDirector.RestartCount;
|
||||
if (restart && cfg.restartResetsGuideState)
|
||||
{
|
||||
s_faulted = false; // 런 1 의 예외가 세션 끝까지 가이드를 죽이는 것을 막는다
|
||||
EnsureSubscribed(); // 구독이 어떤 이유로 풀렸으면 여기서 다시 건다
|
||||
}
|
||||
|
||||
if (!ShouldGuideOnStart(cfg, restart)) return;
|
||||
if (restart && !cfg.guideOnRunStart) RestartGuides++;
|
||||
BeginZone(0, restart ? "다음 런" : "런 시작");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이 RunStarted 에서 존0 유도를 걸어야 하는가(순수 함수 · C8 검증용).
|
||||
/// 🔴 D-34 가 끊긴 지점이 바로 이 판정이다 — 813w3 까지는 `guideOnRunStart` 하나뿐이었고 에셋 값이 0 이었다.
|
||||
/// </summary>
|
||||
public static bool ShouldGuideOnStart(WLRunGuideSettings cfg, bool restart)
|
||||
{
|
||||
if (cfg == null || !cfg.zoneGuideEnabled) return false;
|
||||
return cfg.guideOnRunStart || (restart && cfg.guideOnRestart);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이 RunStarted 가 「다음 런」인가(순수 함수 · 프로브가 직접 부른다).
|
||||
/// 813p 보고 §4 의 계약 = `RunDirector.Restart()` → `StartRun("restart")`.
|
||||
/// 문자열이 바뀌어도 살아남도록 호출측이 `RunDirector.RestartCount` 증가도 함께 본다.
|
||||
/// </summary>
|
||||
public static bool IsRestartReason(string reason)
|
||||
{
|
||||
return string.Equals(reason, "restart", System.StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>프로브용 — 진짜 RunStarted 페이로드를 만들어 핸들러를 그대로 태운다(Play 불필요).</summary>
|
||||
public static void ProbeRunStarted(int runIndex, string reason)
|
||||
{
|
||||
var e = new RunStartedEvent
|
||||
{
|
||||
runIndex = runIndex, startTime = Time.unscaledTime, runSeconds = 0f,
|
||||
zoneCount = 0, reason = reason, time = Time.unscaledTime, frame = Time.frameCount
|
||||
};
|
||||
OnRunStarted(in e);
|
||||
}
|
||||
|
||||
static void OnZoneCleared(in ZoneClearedEvent e)
|
||||
|
|
@ -344,6 +420,32 @@ namespace WL.Combat.Run
|
|||
if (wasFree) HitSuspends++;
|
||||
}
|
||||
|
||||
// ── 813w4 §1-2 엘리트 등록부 (813s EliteMarker 와 같은 출처 = Actor.m_SubRole · 별도 스캔 0 · GC 0)
|
||||
static void OnSpawned(in SpawnedEvent e) { if (e.isElite) NoteEliteSpawn(e.actor); }
|
||||
static void OnKilled(in KilledEvent e) { if (e.subRole == eSubRol.Elite) NoteEliteKill(e.victim); }
|
||||
|
||||
/// <summary>엘리트를 등록부에 넣는다(중복 안전 · 자리가 없으면 죽은 칸을 재사용). 프로브도 직접 부른다.</summary>
|
||||
public static void NoteEliteSpawn(Actor a)
|
||||
{
|
||||
if (!IsAliveTarget(a)) return;
|
||||
int free = -1;
|
||||
for (int i = 0; i < kMaxElites; i++)
|
||||
{
|
||||
if (ReferenceEquals(s_elites[i], a)) return;
|
||||
if (free < 0 && !IsAliveTarget(s_elites[i])) free = i;
|
||||
}
|
||||
if (free >= 0) s_elites[free] = a;
|
||||
}
|
||||
|
||||
/// <summary>엘리트를 등록부에서 뺀다(고정 중이었으면 함께 해제). 프로브도 직접 부른다.</summary>
|
||||
public static void NoteEliteKill(Actor a)
|
||||
{
|
||||
if (a == null) return;
|
||||
for (int i = 0; i < kMaxElites; i++)
|
||||
if (ReferenceEquals(s_elites[i], a)) s_elites[i] = null;
|
||||
if (ReferenceEquals(s_lockedElite, a)) ReleaseEliteLock();
|
||||
}
|
||||
|
||||
/// <summary>813w2 §1-3 — 존 체류/이동 초를 주기적으로 한 줄 남긴다(QA 시각표 · GC 는 로그를 실제로 낼 때만).</summary>
|
||||
static void OnRunTick(in RunTickEvent e)
|
||||
{
|
||||
|
|
@ -359,6 +461,10 @@ namespace WL.Combat.Run
|
|||
"m · 도착 " + Arrived + "(정지 " + ArrivedByStill + ") · 되돌림 " + HoldPullbacks +
|
||||
" · 이탈 " + ArenaExits + " 워프 " + ArenaWarps +
|
||||
"(쿨 " + WarpsSkippedCooldown + " 보스곁 " + WarpsSkippedNearBoss + " 맵밖 " + WarpsFailedNavMesh + ")" +
|
||||
// 813w4: 「다음 런」 재유도 · 정지 감시 · 엘리트 우선 타깃(D-34 · D-20 을 이 줄로 판정한다)
|
||||
" · 재유도 " + RestartGuides + "/정지 " + StallGuides +
|
||||
" · 엘리트 " + EliteCount + "(고정 " + (DSUtil.CheckNull(s_lockedElite) ? "-" : LastEliteName) +
|
||||
" 락 " + EliteLocks + " 재타깃 " + EliteRetargets + " 해제 " + EliteReleases + ")" +
|
||||
" · 존 " + e.zonesCleared + "/" + e.zoneCount + " " + e.zoneKills + "/" + e.zoneRequired;
|
||||
Debug.Log("[RunGuide] " + LastTickLog);
|
||||
}
|
||||
|
|
@ -409,6 +515,7 @@ namespace WL.Combat.Run
|
|||
bool had = s_destKind != RunGuideDest.None;
|
||||
ReleaseSteer();
|
||||
ReleaseBossLock();
|
||||
ReleaseEliteLock(); // 813w4 §1-2
|
||||
s_destKind = RunGuideDest.None;
|
||||
s_state = RunGuideState.Off;
|
||||
s_zoneIndex = -1;
|
||||
|
|
@ -456,6 +563,9 @@ namespace WL.Combat.Run
|
|||
var pc = MyValue.MyPC;
|
||||
if (DSUtil.CheckNull(pc) || pc.IsDead()) { ReleaseSteer(); s_lastPosTime = -1f; return; }
|
||||
|
||||
// 🔴 813w4 §1-1(D-34) 안전망 — 목적지가 없는 동안에도 「런은 도는데 PC 가 멈춰 있는지」를 본다.
|
||||
if (s_destKind == RunGuideDest.None && TrackIdleStall(cfg, pc, pc.Get_position(), now)) return;
|
||||
|
||||
if (s_destKind == RunGuideDest.None) { ReleaseSteer(); return; }
|
||||
|
||||
bool isBoss = s_destKind == RunGuideDest.Boss;
|
||||
|
|
@ -474,7 +584,9 @@ namespace WL.Combat.Run
|
|||
bool still = UpdateStill(cfg, pcPos, now);
|
||||
|
||||
// ── 보스 우선 타깃 — 락은 언제나 유지 · 재타깃은 Hold 에서(이동 중 타깃은 우리 목적지를 덮어쓴다)
|
||||
if (isBoss) ApplyBossPriority(cfg, pc);
|
||||
// 존 구간에서는 같은 방식으로 엘리트를 우선 타깃으로 고정한다(813w4 §1-2 · D-20).
|
||||
if (isBoss) { ReleaseEliteLock(); ApplyBossPriority(cfg, pc); }
|
||||
else ApplyElitePriority(cfg, pc);
|
||||
|
||||
// ── 피격 응전: Hold 에서만 건다(이동·되돌림은 맞아도 멈추지 않는다 = 「이동 중 잡몹 무시」)
|
||||
bool hit = now < s_hitUntil;
|
||||
|
|
@ -542,6 +654,38 @@ namespace WL.Combat.Run
|
|||
Log((isBoss ? "아레나" : "존 " + s_zoneIndex) + " " + why);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// 813w4 §1-1 — 정지 감시(D-34 안전망)
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 런은 도는데 ① 가이드 목적지가 없고 ② PC 에게 타깃이 없고 ③ PC 가 `stallGuideSeconds` 동안 제자리면
|
||||
/// 존 0 유도를 건다. D-34 의 「시작 좌표에서 166 s 정지」를 원인과 무관하게 되돌리는 안전망이다.
|
||||
/// 🔴 교전 중(타깃 보유)에는 절대 끼어들지 않는다 — 제자리에서 때리는 것을 「정지」로 세면 전투를 끊는다.
|
||||
/// 반환 true = 이번 폴링에서 유도를 걸었다(호출측은 즉시 return).
|
||||
/// 프로브가 가짜 좌표·가짜 시각으로 그대로 부를 수 있다.
|
||||
/// </summary>
|
||||
public static bool TrackIdleStall(WLRunGuideSettings cfg, PCActor pc, Vector3 pcPos, float now)
|
||||
{
|
||||
if (cfg == null || cfg.stallGuideSeconds <= 0f || !cfg.zoneGuideEnabled) { s_idleSince = -1f; return false; }
|
||||
if (s_destKind != RunGuideDest.None) { s_idleSince = -1f; return false; } // 이미 유도 중
|
||||
if (RunDirector.Phase != RunPhase.Zones) { s_idleSince = -1f; return false; } // 존 단계에서만
|
||||
if (!DSUtil.CheckNull(pc) && pc.isTarget) { s_idleSince = -1f; return false; } // 교전 중 — 끼어들지 않는다
|
||||
|
||||
if (s_idleSince < 0f || DistXZ(pcPos, s_idlePos) > Mathf.Max(0f, cfg.stallMoveEpsilon))
|
||||
{ s_idlePos = pcPos; s_idleSince = now; return false; } // 움직였다 — 다시 잰다
|
||||
|
||||
if (now - s_idleSince < cfg.stallGuideSeconds) return false;
|
||||
if (now - s_lastStallGuide < cfg.stallGuideSeconds) return false; // 연타 방지
|
||||
|
||||
s_idleSince = -1f;
|
||||
s_lastStallGuide = now;
|
||||
StallGuides++;
|
||||
Log("정지 " + cfg.stallGuideSeconds.ToString("F1") + "s 감지(타깃 없음 · " + Fmt(pcPos) + ") — 존 0 유도");
|
||||
BeginZone(0, "정지 감지");
|
||||
return s_destKind != RunGuideDest.None;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// 813w3 §1-1 · §1-2 — 아레나 이탈 진단 · 강제 복귀(워프)
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
|
@ -839,6 +983,9 @@ namespace WL.Combat.Run
|
|||
var t = pc.Get_Target();
|
||||
if (DSUtil.CheckNull(t)) return;
|
||||
if (s_destKind == RunGuideDest.Boss && !DSUtil.CheckNull(s_boss) && ReferenceEquals(t, s_boss)) return;
|
||||
// 813w4 §1-2: 고정한 엘리트는 앵커에서 멀어도 놓지 않는다(놓으면 다음 폴링에 다시 걸어 타깃이 떨린다).
|
||||
// PC 가 존 반경 밖으로 끌려가는 것은 아래 Hold 되돌림(zoneHoldRadius)이 그대로 막는다.
|
||||
if (!DSUtil.CheckNull(s_lockedElite) && ReferenceEquals(t, s_lockedElite)) return;
|
||||
if (DistXZ(t.Get_position(), s_dest) <= cfg.guideLeashRadius) return;
|
||||
LastIgnoredTarget = t.name;
|
||||
pc.Del_Target(true);
|
||||
|
|
@ -934,6 +1081,83 @@ namespace WL.Combat.Run
|
|||
return !DSUtil.CheckNull(a) && !a.IsDead() && a.gameObject.activeInHierarchy;
|
||||
}
|
||||
|
||||
/// <summary>살아 있고 월드에 붙어 있는 타깃인가(보스·엘리트 공통 판정).</summary>
|
||||
static bool IsAliveTarget(Actor a)
|
||||
{
|
||||
return !DSUtil.CheckNull(a) && !a.IsDead() && a.gameObject.activeInHierarchy;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// 813w4 §1-2 — 엘리트 우선 타깃 (D-20 · 「PC 가 엘리트를 안 때린다」)
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// PC 에서 `eliteTargetRadius` 안의 가장 가까운 살아 있는 엘리트. 없으면 null.
|
||||
/// 등록부(사전 할당 16칸)만 훑는다 — FindObjectsByType 0 · 할당 0.
|
||||
/// </summary>
|
||||
public static Actor NearestElite(WLRunGuideSettings cfg, Vector3 pcPos, float radius)
|
||||
{
|
||||
if (cfg == null || radius <= 0f) return null;
|
||||
Actor best = null;
|
||||
float bestD = radius;
|
||||
for (int i = 0; i < kMaxElites; i++)
|
||||
{
|
||||
var a = s_elites[i];
|
||||
if (!IsAliveTarget(a)) { s_elites[i] = null; continue; }
|
||||
float d = DistXZ(a.Get_position(), pcPos);
|
||||
if (d > bestD) continue;
|
||||
bestD = d; best = a;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 존 구간에서 엘리트를 우선 타깃으로 고정한다(813w 보스 락타깃과 같은 방식).
|
||||
/// 킬캠(CombatCameraDirector)·BossKill 티어(ImpactTier)는 `Killed` 이벤트만 보므로 여기서 건드리지 않는다.
|
||||
/// </summary>
|
||||
static void ApplyElitePriority(WLRunGuideSettings cfg, PCActor pc)
|
||||
{
|
||||
if (!cfg.elitePriorityEnabled) { ReleaseEliteLock(); return; }
|
||||
|
||||
Vector3 pcPos = pc.Get_position();
|
||||
|
||||
// ① 고정 중인 엘리트가 죽었거나 너무 멀어졌으면 푼다(이탈 해제).
|
||||
if (!DSUtil.CheckNull(s_lockedElite))
|
||||
{
|
||||
float rel = cfg.eliteReleaseRadius > 0f ? cfg.eliteReleaseRadius : cfg.eliteTargetRadius;
|
||||
if (!IsAliveTarget(s_lockedElite) || DistXZ(s_lockedElite.Get_position(), pcPos) > rel)
|
||||
ReleaseEliteLock();
|
||||
}
|
||||
|
||||
// ② 고정 대상이 없으면 반경 안에서 가장 가까운 엘리트를 새로 고정한다.
|
||||
var elite = s_lockedElite;
|
||||
if (DSUtil.CheckNull(elite))
|
||||
{
|
||||
elite = NearestElite(cfg, pcPos, cfg.eliteTargetRadius);
|
||||
if (DSUtil.CheckNull(elite)) return;
|
||||
if (cfg.eliteLockTarget) elite.m_IsLockTarget = true; // 원본 락(MyActor.cs:106·121 이 교체를 막는다)
|
||||
s_lockedElite = elite;
|
||||
LastEliteName = elite.name;
|
||||
EliteLocks++;
|
||||
Log("엘리트 우선 타깃 고정 — " + elite.name + " " +
|
||||
DistXZ(elite.Get_position(), pcPos).ToString("F1") + "m");
|
||||
}
|
||||
|
||||
// ③ 재타깃은 Hold 에서만(이동 중에 걸면 원본 Set_Path 가 우리 목적지를 덮어쓴다 = 813w2 D-22 와 같은 축).
|
||||
if (cfg.eliteRetargetOnlyInHold && s_state != RunGuideState.Hold) return;
|
||||
if (ReferenceEquals(pc.Get_Target(), elite)) return;
|
||||
pc.Change_Target(true, elite); // 아웃라인까지 원본 경로 그대로(Actor.cs:988)
|
||||
EliteRetargets++;
|
||||
}
|
||||
|
||||
static void ReleaseEliteLock()
|
||||
{
|
||||
if (DSUtil.CheckNull(s_lockedElite)) { s_lockedElite = null; return; }
|
||||
s_lockedElite.m_IsLockTarget = false;
|
||||
s_lockedElite = null;
|
||||
EliteReleases++;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// 존 앵커 (스포너 노드 실좌표 · 없으면 SO 폴백)
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
|
@ -1036,8 +1260,44 @@ namespace WL.Combat.Run
|
|||
ArenaExits = ArenaWarps = WarpsSkippedCooldown = WarpsSkippedNearBoss = WarpsFailedNavMesh = 0;
|
||||
s_outsideSince = -1f; s_lastWarp = -999f; s_nextExitDiag = 0f; s_bossArrivedOnce = false;
|
||||
s_exitLogNext = s_exitLogCount = 0; LastExitLog = "";
|
||||
// 813w4
|
||||
RestartGuides = StallGuides = EliteLocks = EliteRetargets = EliteReleases = 0;
|
||||
LastEliteName = "";
|
||||
ReleaseEliteLock(); EliteReleases = 0;
|
||||
for (int i = 0; i < kMaxElites; i++) s_elites[i] = null;
|
||||
s_idleSince = -1f; s_lastStallGuide = -999f; s_idlePos = Vector3.zero;
|
||||
s_lastRestartCount = RunDirector.RestartCount;
|
||||
}
|
||||
|
||||
// ── 813w4 프로브 API
|
||||
/// <summary>지금 고정 중인 엘리트(없으면 null).</summary>
|
||||
public static Actor LockedElite { get { return s_lockedElite; } }
|
||||
/// <summary>등록부에 살아 있는 엘리트 수(진단).</summary>
|
||||
public static int EliteCount
|
||||
{
|
||||
get
|
||||
{
|
||||
int n = 0;
|
||||
for (int i = 0; i < kMaxElites; i++) if (IsAliveTarget(s_elites[i])) n++;
|
||||
return n;
|
||||
}
|
||||
}
|
||||
/// <summary>등록부 칸 i 의 엘리트(진단 · 없으면 null).</summary>
|
||||
public static Actor EliteAt(int i) { return i >= 0 && i < kMaxElites ? s_elites[i] : null; }
|
||||
/// <summary>등록부 최대 칸 수(진단).</summary>
|
||||
public static int EliteCapacity { get { return kMaxElites; } }
|
||||
/// <summary>프로브용 — 존 구간 엘리트 우선 타깃을 1회 실행한다.</summary>
|
||||
public static void ApplyElitePriorityForProbe(WLRunGuideSettings cfg, PCActor pc)
|
||||
{
|
||||
if (cfg != null && !DSUtil.CheckNull(pc)) ApplyElitePriority(cfg, pc);
|
||||
}
|
||||
/// <summary>프로브용 — 정지 감시 타이머만 비운다.</summary>
|
||||
public static void ResetStallStateForProbe() { s_idleSince = -1f; s_lastStallGuide = -999f; }
|
||||
/// <summary>프로브용 — RunStarted 재시작 판정의 기준 카운트를 지금 값으로 맞춘다.</summary>
|
||||
public static void SyncRestartCountForProbe() { s_lastRestartCount = RunDirector.RestartCount; }
|
||||
/// <summary>프로브용 — 고장(예외) 상태를 강제로 만든다(restartResetsGuideState 검증).</summary>
|
||||
public static void ForceFaultForProbe() { s_faulted = true; }
|
||||
|
||||
/// <summary>프로브용: 워프 쿨다운·이탈 타이머만 비운다(시나리오 사이 초기화).</summary>
|
||||
public static void ResetWarpStateForProbe() { s_outsideSince = -1f; s_lastWarp = -999f; s_nextExitDiag = 0f; }
|
||||
/// <summary>프로브용: 마지막 워프 시각을 밀어 넣는다(에디트 모드에서는 실제 워프가 못 돌아 쿨다운을 못 만든다).</summary>
|
||||
|
|
|
|||
|
|
@ -82,6 +82,27 @@ namespace WL.Combat.Run
|
|||
[Tooltip("런 시작(RunStarted) 때도 첫 존으로 유도한다. 기본 off — Q4b 실측상 PC 는 존1 에서 이미 교전한다.")]
|
||||
public bool guideOnRunStart = false;
|
||||
|
||||
// ─────────────────────────────────────────── 813w4 §1-1 「다음 런」 재시작 (D-34)
|
||||
[Header("「다음 런」 재시작 (813w4 · D-34 · PC 가 시작 좌표에서 166 s 정지)")]
|
||||
[Tooltip("🔴 D-34 의 원인: 위 guideOnRunStart 가 off 라 RunStarted 에서 존0 유도를 걸지 않는다. "
|
||||
+ "런 1 은 PC 가 맵 진입점(= 존0 교전 범위 안)에서 시작해 원본 자동전투가 대신 해 주지만, "
|
||||
+ "RunDirector.Restart() 는 PC 를 부활 좌표(Get_ReincanationPos)로 워프시키므로 존0 에서 33 m 떨어진 "
|
||||
+ "빈 자리에 서서 아무 일도 일어나지 않았다. 켜면 reason=\"restart\" 인 RunStarted 에서만 존0 유도를 다시 건다 "
|
||||
+ "(런 1 동작은 guideOnRunStart 가 그대로 결정 = C8).")]
|
||||
public bool guideOnRestart = true;
|
||||
|
||||
[Tooltip("재시작 때 가이드 내부 상태를 초기화한다 — 고장(예외) 플래그 해제 + 구독 재보장. "
|
||||
+ "끄면 런 1 에서 한 번 예외가 난 가이드가 세션이 끝날 때까지 죽어 있다(D-34 와 같은 화면).")]
|
||||
public bool restartResetsGuideState = true;
|
||||
|
||||
[Tooltip("🔴 정지 감시(안전망): 런이 존 단계인데 가이드 목적지가 없고 PC 가 타깃도 없이 "
|
||||
+ "이 시간(초 · unscaled) 동안 제자리면 존0 유도를 건다. 0 이하면 끈다. "
|
||||
+ "PC 가 교전 중(타깃 보유)이면 절대 끼어들지 않는다.")]
|
||||
public float stallGuideSeconds = 3f;
|
||||
|
||||
[Tooltip("「제자리」로 보는 이동량(m · XZ). 이 거리 안에서만 움직이면 정지로 센다.")]
|
||||
public float stallMoveEpsilon = 0.5f;
|
||||
|
||||
[Tooltip("존 앵커에 이 거리(m · XZ) 안으로 들어오면 「도착」 = 조종을 놓고 원본 자동전투에 넘긴다.")]
|
||||
public float zoneArriveRadius = 6f;
|
||||
|
||||
|
|
@ -180,6 +201,27 @@ namespace WL.Combat.Run
|
|||
[Tooltip("보스에게 원본 락타깃(m_IsLockTarget)을 건다 — 잡몹으로 타깃이 새는 원본 경로 3곳을 함께 막는다. 보스 사망 시 해제.")]
|
||||
public bool bossLockTarget = true;
|
||||
|
||||
// ─────────────────────────────────────────── 813w4 §1-2 엘리트 우선 타깃 (D-20)
|
||||
[Header("엘리트 우선 타깃 (813w4 · D-20 · 엘리트 TTK 45.2~77.3 s · 목표 8~12 s)")]
|
||||
[Tooltip("🔴 D-20 의 원인은 엘리트 HP 가 아니라 「PC 가 엘리트를 안 때린다」였다(Q7 §D-20). "
|
||||
+ "켜면 존 Hold 중 eliteTargetRadius 안에 살아 있는 엘리트가 있을 때 타깃을 그 엘리트로 고정한다 "
|
||||
+ "(813w 보스 락타깃과 같은 방식). 끄면 원본 타깃 선택 그대로 = 813w3 동작(C8).")]
|
||||
public bool elitePriorityEnabled = true;
|
||||
|
||||
[Tooltip("PC 에서 이 거리(m · XZ) 안의 살아 있는 엘리트를 우선 타깃으로 잡는다. 0 이하면 잡지 않는다.")]
|
||||
public float eliteTargetRadius = 12f;
|
||||
|
||||
[Tooltip("고정한 엘리트가 PC 에서 이 거리(m) 밖으로 벗어나면 고정을 푼다(엘리트 사망·비활성도 해제). "
|
||||
+ "잡는 반경보다 커야 반경 경계에서 고정/해제가 떨리지 않는다.")]
|
||||
public float eliteReleaseRadius = 18f;
|
||||
|
||||
[Tooltip("엘리트에게 원본 락타깃(m_IsLockTarget)을 건다 — 잡몹이 타깃을 도로 가져가는 원본 경로를 막는다. 해제 시 원복.")]
|
||||
public bool eliteLockTarget = true;
|
||||
|
||||
[Tooltip("엘리트 재타깃(Change_Target)은 Hold(= 존 반경 안)에서만 한다. 락은 이동 중에도 유지한다. "
|
||||
+ "🔴 끄면 이동 중에도 타깃이 걸려 원본 Play_Run() 이 Set_Path(엘리트)로 우리 목적지를 덮어쓴다(813w2 D-22 와 같은 축).")]
|
||||
public bool eliteRetargetOnlyInHold = true;
|
||||
|
||||
// ─────────────────────────────────────────── 813w3 §1-2 강제 복귀(워프)
|
||||
[Header("아레나 강제 복귀 (813w3 · D-22 재발 · 이탈 39~56 m)")]
|
||||
[Tooltip("되돌림(SetDestination)이 warpBackAfterSeconds 안에 PC 를 반경 안으로 못 넣으면 "
|
||||
|
|
|
|||
|
|
@ -19,8 +19,10 @@ MonoBehaviour:
|
|||
type: 3}
|
||||
burstParentToPc: 1
|
||||
burstOffset: {x: 0, y: 0.1, z: 0}
|
||||
burstScale: 1
|
||||
burstLifetimeSeconds: 0
|
||||
burstScale: 0.5
|
||||
burstMaxScreenFraction: 0.5
|
||||
burstAlpha: 0.6
|
||||
burstLifetimeSeconds: 0.8
|
||||
burstLifetimeMaxSeconds: 6
|
||||
burstMinIntervalSeconds: 0.5
|
||||
useEffectBudget: 1
|
||||
|
|
|
|||
|
|
@ -17,6 +17,10 @@ MonoBehaviour:
|
|||
pollSeconds: 0.1
|
||||
zoneGuideEnabled: 1
|
||||
guideOnRunStart: 0
|
||||
guideOnRestart: 1
|
||||
restartResetsGuideState: 1
|
||||
stallGuideSeconds: 3
|
||||
stallMoveEpsilon: 0.5
|
||||
zoneArriveRadius: 6
|
||||
zoneHoldRadius: 18
|
||||
zoneAnchorFallbacks:
|
||||
|
|
@ -43,6 +47,11 @@ MonoBehaviour:
|
|||
bossArriveRadius: 10
|
||||
bossHoldRadius: 14
|
||||
bossLockTarget: 1
|
||||
elitePriorityEnabled: 1
|
||||
eliteTargetRadius: 12
|
||||
eliteReleaseRadius: 18
|
||||
eliteLockTarget: 1
|
||||
eliteRetargetOnlyInHold: 1
|
||||
warpBackEnabled: 1
|
||||
warpBackAfterSeconds: 1.5
|
||||
warpCooldownSeconds: 5
|
||||
|
|
|
|||
Loading…
Reference in New Issue