Project_WL/Assets/WL/Tools/EffectWarmup.cs

207 lines
11 KiB
C#

// ─────────────────────────────────────────────────────────────────────────────
// EffectWarmup.cs — 맵 진입 시 이펙트 사전 워밍(프레임 분산) · 히치 1차 완화 ⓑ
//
// PD 지시 #813(버벅임) · 발주서 WL-811q §1-2 ⓑ (2026-09-10)
//
// ■ 관측된 원인 (코드·테이블 실측)
// ① 원본은 맵 로드 때 `EffectList.json` 90행을 돌며 **인스턴스 514개**를 미리 만든다
// (`InGameInfo.cs:83-88` · `n_PreLoadCount` 합 = 이펙트 312 + 투사체 202).
// 그래서 `Effect_MonsterSpawn`(프리로드 20)·`Effect_Hit_*`(각 10) 같은 것은 **이미 워밍돼 있다**.
// ② 🔴 그런데 **WL 이 쓰는 원본 풀 이펙트 22종**(검기 21 + `BoomHit`)은 그 표에 **한 종도 없다**(실측).
// → 전투 중 첫 사용에서 `InGameInfo.Get_Effect` → `Load_Effect` → **Addressables 로드 + Instantiate**.
// ③ 🔴 811i 스킬 이펙트 **33종**도 그 표에 없다 — 지금은 **첫 `AttackStarted`** 에 그 클래스 4행이
// 한꺼번에 로드되고(811i §1 ④), 완료 콜백이 몰린 프레임에 워밍 인스턴스가 같이 만들어진다.
//
// ■ 무엇을 하나 (원본 훅 0 · 원본 파일 수정 0)
// 맵 진입 경계(메인 PC 교체 또는 맵 ID 변경 — `AutoCombatOnEnter` 와 **같은 판정**)를 보고,
// `warmStartDelaySeconds` 뒤부터 **프레임당 N 개씩** 나눠서
// ⓐ 811i 행 → `SkillSpectacle.PreloadRows(warmRowsPerFrame, classId)`
// ⓑ 원본 풀 22종 → `InGameInfo.Show_Effect(name, Vector3.one * warmFarDistance)`
// (원본 자신의 사전 로드 `InGameInfo.cs:87` 과 **같은 좌표·같은 경로**. 프리팹의 `TurnOff_GO.OnEnable`
// → `Set_Off()` 가 수명 뒤 스스로 꺼서 풀로 돌려놓는다 = 화면에 보이지 않고 잔류도 없다.)
// ⇒ 첫 전투에서 터지던 로드·Instantiate 를 **로딩 직후 조용한 구간**으로 옮기고, 그 구간에서도 나눈다.
//
// ■ 🔴 안전
// · `warmPoolPrefabs` 의 이름은 프리팹에 `TurnOff_GO` 가 있어야 한다(없으면 원본 `Show_Effect` 가
// `GetComponent<TurnOff_GO>()` null 에서 NRE). **기본 22종은 디스크에서 전수 확인**했다(811q 보고 §4).
// · 워밍은 맵 경계당 1회. 실패해도 런을 죽이지 않게 전체를 try/catch 로 감싸고 한 번 실패하면 그 판은 멈춘다.
// · `Application.isPlaying` 이 아니면 아무것도 하지 않는다(에디트 모드 오염 0 · 프로브는 별도 진입점).
//
// ■ C8 롤백
// `WLHitchSettings.enabled = 0` → 러너 자체가 없다 · `warmOnMapEnter = 0` → 워밍 0
// (그러면 811i 는 예전처럼 첫 `AttackStarted` 에 프리로드하고 22종은 첫 사용에 로드된다 = 100 % 기존 동작).
//
// 🔴 어셈블리 주의: Assets/WL/ 에 .asmdef 를 만들지 말 것.
// 🔴 이 파일은 Debug.LogError / Debug.LogException 을 쓰지 않는다(813z 규칙).
// ─────────────────────────────────────────────────────────────────────────────
using UnityEngine;
using WL.Combat.Reaction;
namespace WL.Tools
{
/// <summary>맵 진입 이펙트 워밍. 정적 · 러너는 HitchRecorderRunner 가 겸한다(새 오브젝트 0).</summary>
public static class EffectWarmup
{
enum Phase { Idle = 0, Waiting = 1, Rows = 2, Pool = 3, Done = 4, Faulted = 5 }
static Phase s_phase;
static PCActor s_lastPc;
static int s_lastMapId = int.MinValue;
static float s_nextPoll, s_startAt;
static int s_poolIndex, s_classId = -1;
// ───────────────────────────────────────── 진단(프로브·레코더가 읽는다)
/// <summary>맵 경계를 본 횟수 · 워밍한 811i 행 수 · 워밍한 원본 풀 이름 수.</summary>
public static int Boundaries, RowsWarmed, PoolWarmed;
/// <summary>워밍 완료(=Done) 도달 횟수 · 실패 횟수.</summary>
public static int Completions, Faults;
/// <summary>레코더가 프레임 델타로 쓰는 진행 총계.</summary>
public static int WarmedTotal { get { return RowsWarmed + PoolWarmed; } }
/// <summary>현재 단계 이름(파일 요약에 그대로 들어간다).</summary>
public static string LastState { get { return s_phase.ToString(); } }
public static string LastLog = "";
static WLHitchSettings St { get { return WLHitchSettings.Instance; } }
// ─────────────────────────────────────────────────────────────────────
// 틱 (HitchRecorderRunner.Update 가 부른다)
// ─────────────────────────────────────────────────────────────────────
/// <summary>1프레임. <paramref name="now"/> = Time.unscaledTime.</summary>
public static void Tick(float now)
{
var st = St;
if (st == null || !WLHitchSettings.Enabled || !st.warmOnMapEnter) return;
if (s_phase == Phase.Faulted) return;
// 🔴 에디트 모드에서는 경계 감시·원본 풀 접근을 하지 않는다(오염 0). 행 워밍만 프로브가 돌릴 수 있다.
try
{
if (now >= s_nextPoll)
{
s_nextPoll = now + Mathf.Max(0.05f, st.warmPollSeconds);
PollBoundary(st, now);
}
switch (s_phase)
{
case Phase.Waiting:
if (now >= s_startAt) { s_phase = Phase.Rows; }
break;
case Phase.Rows:
StepRows(st);
break;
case Phase.Pool:
StepPool(st);
break;
}
}
catch (System.Exception ex)
{
s_phase = Phase.Faulted; Faults++;
LastLog = "중단(예외) — " + ex.Message;
if (st.verboseLog) Debug.Log("[EffectWarmup] " + LastLog); // 🔴 LogError 금지(813z)
}
}
/// <summary>맵 진입 경계 판정 — AutoCombatOnEnter 와 같은 규칙(메인 PC 교체 또는 맵 ID 변경).</summary>
static void PollBoundary(WLHitchSettings st, float now)
{
if (!Application.isPlaying) return; // 에디트 모드 오염 0(프로브는 ForceStart 로 들어온다)
var pc = MyValue.MyPC;
if (DSUtil.CheckNull(pc)) { s_lastPc = null; return; }
// InGameInfo 가 아직 없으면 경계를 「소비」하지 않는다 — 다음 폴링에서 같은 경계를 다시 본다.
var info = InGameInfo.Ins;
if (DSUtil.CheckNull(info)) return;
int map = MyValue.MyChoiceMapData != null ? MyValue.MyChoiceMapData.n_MapID : int.MinValue;
bool pcChanged = !ReferenceEquals(pc, s_lastPc);
bool mapChanged = map != s_lastMapId;
if (!pcChanged && !mapChanged) return;
s_lastPc = pc;
s_lastMapId = map;
Boundaries++;
s_classId = st.warmAllClasses ? -1 : pc.Get_ID();
s_poolIndex = 0;
s_startAt = now + Mathf.Max(0f, st.warmStartDelaySeconds);
s_phase = Phase.Waiting;
LastLog = "맵 진입 경계 — 워밍 예약";
if (st.verboseLog) Debug.Log("[EffectWarmup] " + LastLog + " map=" + map + " class=" + s_classId);
}
/// <summary>811i 행 워밍 1프레임분.</summary>
static void StepRows(WLHitchSettings st)
{
int n = SkillSpectacle.PreloadRows(Mathf.Max(0, st.warmRowsPerFrame), s_classId);
RowsWarmed += n;
if (n == 0 || SkillSpectacle.PreloadRemaining(s_classId) <= 0) s_phase = Phase.Pool;
}
/// <summary>원본 풀 워밍 1프레임분.</summary>
static void StepPool(WLHitchSettings st)
{
var names = st.warmPoolPrefabs;
if (names == null || names.Length == 0) { Finish(st); return; }
if (!Application.isPlaying) { Finish(st); return; } // 에디트 모드는 원본 풀을 만들지 않는다
var info = InGameInfo.Ins;
if (DSUtil.CheckNull(info)) { Finish(st); return; }
int per = st.warmPoolPerFrame;
if (per <= 0) per = names.Length;
Vector3 far = Vector3.one * st.warmFarDistance;
for (int k = 0; k < per && s_poolIndex < names.Length; k++)
{
string name = names[s_poolIndex++];
if (string.IsNullOrEmpty(name)) continue;
// 원본 경로 그대로 — 풀에 없으면 Load_Effect(Addressables) 후 인스턴스 1개가 남는다.
info.Show_Effect(name, far);
PoolWarmed++;
}
if (s_poolIndex >= names.Length) Finish(st);
}
static void Finish(WLHitchSettings st)
{
s_phase = Phase.Done;
Completions++;
LastLog = "워밍 완료 — 행 " + RowsWarmed + " · 풀 " + PoolWarmed;
if (st.verboseLog) Debug.Log("[EffectWarmup] " + LastLog);
}
// ─────────────────────────────────────────────────────────────────────
// 프로브 진입점 · 초기화
// ─────────────────────────────────────────────────────────────────────
/// <summary>프로브용: 경계 폴링을 건너뛰고 지금 바로 워밍을 시작한다(classId &lt; 0 = 전 클래스).</summary>
public static void ForceStart(int classId, float now)
{
s_classId = classId;
s_poolIndex = 0;
s_startAt = now;
s_phase = Phase.Waiting;
Boundaries++;
}
/// <summary>프로브용: 지금 단계(0 Idle · 1 Waiting · 2 Rows · 3 Pool · 4 Done · 5 Faulted).</summary>
public static int PhaseIndex { get { return (int)s_phase; } }
/// <summary>프로브·재시작용.</summary>
public static void ResetAll()
{
s_phase = Phase.Idle;
s_lastPc = null; s_lastMapId = int.MinValue;
s_nextPoll = s_startAt = 0f;
s_poolIndex = 0; s_classId = -1;
Boundaries = RowsWarmed = PoolWarmed = Completions = Faults = 0;
LastLog = "";
}
}
}