Merge branch 'wl/gameplay/WL-813i-survival'
This commit is contained in:
commit
e255055800
|
|
@ -0,0 +1,28 @@
|
|||
# WL813i Probe 2026-09-09 02:15:53 (edit mode · Play 0)
|
||||
survival asset=found enabled(disk)=False Enabled=False · gameplay asset=found playerInvincible(disk)=True · core Enabled=True
|
||||
values: mob True×0.5 boss True×0.5 eliteAsBoss=False · potion 3/run 0.4 cd 5 showHeal=True refuseFull=True resetOnMap=True · death display 2 hold=True autoNoListener=True timeout=0 invincible 1.5 heal 1 companions=True
|
||||
[PASS] ① C8 기본: enabled(disk)=false✔ Enabled=false · RuntimeOverride 1→True -1→off True · playerInvincible 유지 True
|
||||
[PASS] ② C8(SO off): TakeDamage→base 1회 dmg=100 flag=True PassDisabled=1 · TryUse=Disabled Changed=0 hp=300 · OnPCDied state=None Died=0
|
||||
[PASS] ③a 잡몹 100→base 50 (기대 50) flagDuring=False flagAfter=True (기대 True) Lifted=1
|
||||
[PASS] ③b 보스 100→base 25 (기대 25) boss=True
|
||||
[PASS] ③c 보스 스위치 off: base dmg=100 (기대 100) flagDuring=True SkippedBossSwitch=1
|
||||
[PASS] ③d 정예: 기본 50 (기대 50) · eliteCountsAsBoss 25 (기대 25)
|
||||
[PASS] ③e 잡몹 스위치 off 통과=True (SkippedMobSwitch=1) · 적 PC 통과=True (PassNotMainPC=1)
|
||||
[PASS] ③f base 예외 후 플래그 복원: threw=True flag=True (기대 True)
|
||||
[PASS] ③g 부활 무적 1.5 s: 창 안 통과(dmg 100 Blocked=1) · t+1.49 True · t+1.5 해제 True → 배율 재적용 True · 다른 PC True
|
||||
[PASS] ③h GC: TakeDamage×1,000,000 = 힙 0 B / mono 0 B / 수집 0회 (기대 0/0/0) · 훅 줄 델리게이트 ×100k(보존) = 7479296 B / mono 7479296 B (≈74 B/피격) · 대조군 new object()×100k(보존) = 1593344 B / mono 1593344 B (감도 True)
|
||||
[PASS] ④a 사용 1: Used hp 300→700.000005960464 (기대 700) remaining=2 cd=5.00 IsReady=False
|
||||
[PASS] ④b 쿨 중 거절=Cooldown · t=4.99 CooldownEnded=0 · t=5.0 CooldownEnded=1 IsReady=True
|
||||
[PASS] ④c 사용 2: Used hp=1000 (기대 1000 클램프) remaining=1 · 만충 거절=FullHp remaining 유지=True
|
||||
[PASS] ④d 사용 3: Used hp=500.000005960464 (기대 500) remaining=0 · 빈 통=Empty Used 이벤트=3 (기대 3)
|
||||
[PASS] ④e 런 리셋: remaining=3 Changed(RunReset)=1 · 사망=Dead · NoPC=NoPC · Changed 총=12 refused=5
|
||||
[PASS] ⑤a 사이클: Dying True · t+1 Dying(remain 1.00) · t+1.99 True · t+2 Awaiting+ReviveRequested(held=False · 에디트 모드 기대 false) True · Revive()=True → None/Revived/무적 1.5 True · 창 True → 1.5 s 뒤 해제 True · LastDeathToRequest=2.00
|
||||
[PASS] ⑤b 구독자 0 자동 부활: state=None revived=2 auto=1
|
||||
[PASS] ⑤c 외부 부활 감지: Awaiting True → external=1 override 미호출 True 무적 True
|
||||
[PASS] ⑤d 조기 응답: Dying 유지 True → 2 s 뒤 부활 True revived=4
|
||||
[PASS] ⑤e 타임아웃 3 s: +2.9 대기 True · +3.0 자동 부활 True · LastRequestToRevive=3.00
|
||||
[PASS] ⑤f 중복 사망 무시 True · Abort True · 적 PC 무시 True · timeScale 무변경 True (1)
|
||||
[PASS] ⑤g 대기 없는 Revive()=false True
|
||||
[PASS] ⑥ PotionUse.Changed 디스패치×1,000,000 = 힙 0 B / mono 0 B / 수집 0회 (기대 0) handler=1000010
|
||||
[PASS] ⑦ 정리: 디스크 에셋 enabled=false 복귀 True · playerInvincible=True · 잔존 오브젝트 0 · timeScale 1(시작 1) · 러너 생성 0 True · MyPC=null True · 씬 '' dirty=False
|
||||
RESULT PASS
|
||||
|
|
@ -0,0 +1,491 @@
|
|||
// WL813i_Probe.cs — #813i(무적 해제 게이트 · 물약 · 사망/부활) 에디트 모드 검증 프로브
|
||||
// 에디트 모드 전용 · Play 불필요 · 로그인 0 · 에셋/씬/TimeManager 무수정(런타임 클론 SO 를 리플렉션으로 주입 · 임시 오브젝트 즉시 파괴)
|
||||
//
|
||||
// 상주 에디터: unity command run_script --project-path <wt> --file AgentScripts/WL813i_Probe.cs --entry WL813i_Probe.RunAll
|
||||
// 결과: Console + AgentScripts/WL813i_PROBE.txt(커밋) + Screenshots_WL/WL813i/probe_<HHmmss>.txt (RESULT PASS / RESULT FAIL n)
|
||||
//
|
||||
// 검사(발주서 WL-813i ⓒ · ⓓ)
|
||||
// ① 에셋: 디스크 enabled=0(C8 기본) · 값 덤프 · RuntimeOverride
|
||||
// ② C8: SO off 면 TakeDamage=base 그대로(피해·플래그 무변경) · TryUse=Disabled · OnPCDied 무시
|
||||
// ③ 무적 해제 게이트: 잡몹 ×0.5 · 보스 ×배율 · 스위치 off 통과 · 정예 취급 · 적 PC 통과 · 호출 중 플래그 false → 뒤 true 복원 · 예외에도 복원 · 부활 무적 창 통과 · GC
|
||||
// ④ 물약: 3회/쿨 5 s/만충 거절/빈 통/사망/NoPC · 잔량·쿨 이벤트 · 런 리셋
|
||||
// ⑤ 사망/부활: 연출 2 s → ReviveRequested → Revive() → Revived + 무적 1.5 s · 구독자 0 자동 부활 · 외부 부활 감지 · 조기 응답 · 타임아웃 · Abort · 중복 사망 · 적 PC
|
||||
// ⑥ 이벤트 GC 0 · ⑦ 정리(클론 파괴 · 캐시 복구 · 씬 dirty)
|
||||
//
|
||||
// 에디트 모드 한계: Time.timeScale 은 읽기만(대입하면 TimeManager.asset 에 적힌다 · 811b-fix 실측) — DeathFlow 가 Application.isPlaying 으로 스스로 막는다.
|
||||
// 부활 실행(On_Regen → LoadMapMgr/ActorInfo)은 에디트 모드에 없어 DeathFlow.ReviveOverride 로 바꿔치기한다(런타임 On_Regen 경로 = 병합 후 QA).
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using WL.Combat.Core;
|
||||
using WL.Combat.Survival;
|
||||
|
||||
public static class WL813i_Probe
|
||||
{
|
||||
const string OutDir = "Screenshots_WL/WL813i";
|
||||
const string OutCommit = "AgentScripts/WL813i_PROBE.txt";
|
||||
const BindingFlags NP = BindingFlags.NonPublic | BindingFlags.Instance;
|
||||
|
||||
static WLSurvivalSettings s_clone;
|
||||
|
||||
public static object RunAll()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("# WL813i Probe " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " (edit mode · Play 0)");
|
||||
int fail = 0;
|
||||
float ts0 = Time.timeScale;
|
||||
try { fail += Asset(sb); } catch (Exception ex) { fail++; sb.AppendLine("[FAIL] ① 예외 " + ex); }
|
||||
try { fail += C8(sb); } catch (Exception ex) { fail++; sb.AppendLine("[FAIL] ② 예외 " + ex); }
|
||||
try { fail += DamageGate(sb); } catch (Exception ex) { fail++; sb.AppendLine("[FAIL] ③ 예외 " + ex); }
|
||||
try { fail += Potion(sb); } catch (Exception ex) { fail++; sb.AppendLine("[FAIL] ④ 예외 " + ex); }
|
||||
try { fail += Death(sb); } catch (Exception ex) { fail++; sb.AppendLine("[FAIL] ⑤ 예외 " + ex); }
|
||||
try { fail += EventGc(sb); } catch (Exception ex) { fail++; sb.AppendLine("[FAIL] ⑥ 예외 " + ex); }
|
||||
try { fail += Cleanup(sb, ts0); } catch (Exception ex) { fail++; sb.AppendLine("[FAIL] ⑦ 예외 " + ex); }
|
||||
sb.AppendLine(fail == 0 ? "RESULT PASS" : "RESULT FAIL " + fail);
|
||||
var s = sb.ToString();
|
||||
Debug.Log(s);
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(OutDir);
|
||||
File.WriteAllText(Path.Combine(OutDir, "probe_" + DateTime.Now.ToString("HHmmss") + ".txt"), s);
|
||||
File.WriteAllText(OutCommit, s);
|
||||
}
|
||||
catch { /* 출력 저장 실패는 판정에 영향 없음 */ }
|
||||
return s;
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────── 도구
|
||||
static string P(bool ok) { return ok ? "[PASS]" : "[FAIL]"; }
|
||||
/// <summary>원본 Heal(float rate) 은 0.4f 를 double 로 넓혀 곱하므로(400.000006) 1e-3 허용.</summary>
|
||||
static bool Near(double a, double b) { return Math.Abs(a - b) < 1e-3; }
|
||||
|
||||
/// <summary>Unity Mono 의 GC.GetAllocatedBytesForCurrentThread 는 상수를 돌려준다(실측 · 대조군 0 B) → GetTotalMemory(4 KB 페이지 단위) + 대조군으로 감도를 보인다(813k 선례).</summary>
|
||||
static long Alloc() { return GC.GetTotalMemory(false); }
|
||||
static long Mono() { return UnityEngine.Profiling.Profiler.GetMonoUsedSizeLong(); }
|
||||
|
||||
/// <summary>디스크 에셋을 건드리지 않는 런타임 클론을 WLSurvivalSettings 캐시에 주입한다(값 변경 실험용).</summary>
|
||||
static WLSurvivalSettings UseClone()
|
||||
{
|
||||
var orig = WLSurvivalSettings.Instance;
|
||||
if (orig == null) return null;
|
||||
if (s_clone == null)
|
||||
{
|
||||
s_clone = UnityEngine.Object.Instantiate(orig);
|
||||
s_clone.hideFlags = HideFlags.HideAndDontSave;
|
||||
s_clone.name = "WLSurvivalSettings(probe clone)";
|
||||
}
|
||||
var fc = typeof(WLSurvivalSettings).GetField("s_cached", BindingFlags.NonPublic | BindingFlags.Static);
|
||||
var fd = typeof(WLSurvivalSettings).GetField("s_lookupDone", BindingFlags.NonPublic | BindingFlags.Static);
|
||||
if (fc == null || fd == null) return null;
|
||||
fc.SetValue(null, s_clone); fd.SetValue(null, true);
|
||||
return s_clone;
|
||||
}
|
||||
|
||||
static void RestoreAsset()
|
||||
{
|
||||
WLSurvivalSettings.ClearCache();
|
||||
WLSurvivalSettings.RuntimeOverride = 0;
|
||||
}
|
||||
|
||||
static PCActor MakePC(string name, double maxHp, double hp, bool enemy = false)
|
||||
{
|
||||
var go = new GameObject("__WL813i_" + name);
|
||||
var pc = go.AddComponent<PCActor>();
|
||||
pc.m_Role = eRole.PC; pc.m_SubRole = eSubRol.None;
|
||||
var stat = new ActorStatInfo(eRole.PC);
|
||||
stat.Set_Stat(eStat.MaxHP, maxHp); stat.Set_Stat(eStat.HP, hp);
|
||||
typeof(Actor).GetField("m_Stat", NP).SetValue(pc, stat);
|
||||
typeof(Actor).GetField("m_Enemy", NP).SetValue(pc, enemy);
|
||||
return pc;
|
||||
}
|
||||
|
||||
static Actor MakeBeater(string name, eSubRol sub)
|
||||
{
|
||||
var go = new GameObject("__WL813i_" + name);
|
||||
var a = go.AddComponent<Actor>();
|
||||
a.m_Role = eRole.Mob; a.m_SubRole = sub;
|
||||
typeof(Actor).GetField("m_Enemy", NP).SetValue(a, true);
|
||||
return a;
|
||||
}
|
||||
|
||||
static void SetDead(Actor a, bool dead) { typeof(Actor).GetField("DeadStatus", NP).SetValue(a, dead); }
|
||||
static bool IsDead(Actor a) { return (bool)typeof(Actor).GetField("DeadStatus", NP).GetValue(a); }
|
||||
static void SetHp(Actor a, double hp) { a.Get_StatInfo().Set_Stat(eStat.HP, hp); }
|
||||
|
||||
static void Kill(params UnityEngine.Object[] objs)
|
||||
{
|
||||
foreach (var o in objs)
|
||||
{
|
||||
if (o == null) continue;
|
||||
var c = o as Component;
|
||||
UnityEngine.Object.DestroyImmediate(c != null ? c.gameObject : o);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 가짜 base.Get_Damage(MyActor 대역): 호출 수 · 호출 시점의 피해값 · 무적 플래그를 기록한다
|
||||
static int s_baseCalls; static double s_baseDamage; static bool s_baseFlag; static bool s_baseThrow;
|
||||
static void FakeBase(DamageInfo d)
|
||||
{
|
||||
s_baseCalls++; s_baseDamage = d != null ? d.Damage : -1d;
|
||||
s_baseFlag = WL.Settings.WLGameplaySettings.PlayerInvincible;
|
||||
if (s_baseThrow) throw new InvalidOperationException("probe: base threw");
|
||||
}
|
||||
static readonly Action<DamageInfo> s_fake = FakeBase;
|
||||
|
||||
static DamageInfo Dmg(Actor beater, double dmg) { var d = new DamageInfo(); d.Beater = beater; d.Damage = dmg; return d; }
|
||||
|
||||
// ───────────────────────────────────────── ① 에셋
|
||||
static int Asset(StringBuilder sb)
|
||||
{
|
||||
RestoreAsset();
|
||||
var st = WLSurvivalSettings.Instance;
|
||||
var gs = WL.Settings.WLGameplaySettings.Instance;
|
||||
sb.AppendLine("survival asset=" + (st != null ? "found" : "NULL") + " enabled(disk)=" + (st != null && st.enabled)
|
||||
+ " Enabled=" + WLSurvivalSettings.Enabled + " · gameplay asset=" + (gs != null ? "found" : "NULL")
|
||||
+ " playerInvincible(disk)=" + (gs != null && gs.playerInvincible) + " · core Enabled=" + WLCombatCoreSettings.Enabled);
|
||||
if (st == null) { sb.AppendLine("[FAIL] ① WLSurvivalSettings.asset 로드 실패"); return 1; }
|
||||
sb.AppendLine(" values: mob " + st.takeMobDamage + "×" + st.mobDamageMultiplier + " boss " + st.takeBossDamage + "×" + st.bossDamageMultiplier
|
||||
+ " eliteAsBoss=" + st.eliteCountsAsBoss + " · potion " + st.potionCountPerRun + "/run " + st.potionHealRate + " cd " + st.potionCooldownSeconds
|
||||
+ " showHeal=" + st.potionShowHealNumber + " refuseFull=" + st.potionRefuseAtFullHp + " resetOnMap=" + st.potionResetOnMapLoad
|
||||
+ " · death display " + st.deathDisplaySeconds + " hold=" + st.holdWorldWhileReviving + " autoNoListener=" + st.autoReviveWhenNoListener
|
||||
+ " timeout=" + st.autoReviveTimeoutSeconds + " invincible " + st.reviveInvincibleSeconds + " heal " + st.reviveHealRate + " companions=" + st.reviveCompanions);
|
||||
bool diskOff = !st.enabled && !WLSurvivalSettings.Enabled;
|
||||
WLSurvivalSettings.RuntimeOverride = 1; bool forceOn = WLSurvivalSettings.Enabled;
|
||||
WLSurvivalSettings.RuntimeOverride = -1; bool forceOff = !WLSurvivalSettings.Enabled;
|
||||
WLSurvivalSettings.RuntimeOverride = 0;
|
||||
bool ok = diskOff && forceOn && forceOff && gs != null && gs.playerInvincible;
|
||||
sb.AppendLine(P(ok) + " ① C8 기본: enabled(disk)=false✔ Enabled=false · RuntimeOverride 1→" + forceOn + " -1→off " + forceOff + " · playerInvincible 유지 " + (gs != null && gs.playerInvincible));
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────── ② C8 통과
|
||||
static int C8(StringBuilder sb)
|
||||
{
|
||||
RestoreAsset();
|
||||
Survival.ResetDiagnostics(); PotionUse.ResetDiagnostics(); DeathFlow.ResetDiagnostics();
|
||||
var pc = MakePC("pc", 1000, 300); var mob = MakeBeater("mob", eSubRol.None);
|
||||
try
|
||||
{
|
||||
s_baseCalls = 0;
|
||||
var d = Dmg(mob, 100);
|
||||
Survival.TakeDamage(pc, d, s_fake);
|
||||
bool a = s_baseCalls == 1 && Math.Abs(s_baseDamage - 100d) < 1e-9 && s_baseFlag && Survival.PassDisabled == 1 && Survival.Applied == 0;
|
||||
var r = PotionUse.TryUse(pc);
|
||||
bool b = r == PotionResult.Disabled && PotionUse.Changed.Raised == 0 && Math.Abs(pc.Get_HP() - 300d) < 1e-9;
|
||||
SetDead(pc, true);
|
||||
DeathFlow.OnPCDied(pc);
|
||||
bool c = DeathFlow.State == DeathState.None && DeathFlow.Died.Raised == 0 && DeathFlow.DeathCount == 0;
|
||||
SetDead(pc, false);
|
||||
bool ok = a && b && c;
|
||||
sb.AppendLine(P(ok) + " ② C8(SO off): TakeDamage→base 1회 dmg=" + s_baseDamage + " flag=" + s_baseFlag + " PassDisabled=" + Survival.PassDisabled
|
||||
+ " · TryUse=" + r + " Changed=" + PotionUse.Changed.Raised + " hp=" + pc.Get_HP() + " · OnPCDied state=" + DeathFlow.State + " Died=" + DeathFlow.Died.Raised);
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
finally { Kill(pc, mob); }
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────── ③ 무적 해제 게이트
|
||||
static int DamageGate(StringBuilder sb)
|
||||
{
|
||||
var st = UseClone(); if (st == null) { sb.AppendLine("[FAIL] ③ 클론 주입 실패"); return 1; }
|
||||
st.enabled = true; st.verboseLog = false;
|
||||
st.takeMobDamage = true; st.mobDamageMultiplier = 0.5f; st.takeBossDamage = true; st.bossDamageMultiplier = 0.25f; st.eliteCountsAsBoss = false;
|
||||
Survival.ResetDiagnostics();
|
||||
var gs = WL.Settings.WLGameplaySettings.Instance;
|
||||
bool flagBefore = WL.Settings.WLGameplaySettings.PlayerInvincible;
|
||||
var pc = MakePC("pc", 1000, 500); var enemyPc = MakePC("enemyPc", 1000, 500, true);
|
||||
var mob = MakeBeater("mob", eSubRol.None); var boss = MakeBeater("boss", eSubRol.Boss); var elite = MakeBeater("elite", eSubRol.Elite);
|
||||
int fails = 0;
|
||||
try
|
||||
{
|
||||
// a) 잡몹 ×0.5 · 호출 중 플래그 false · 뒤 복원
|
||||
s_baseCalls = 0; var d = Dmg(mob, 100); Survival.TakeDamage(pc, d, s_fake);
|
||||
bool a = s_baseCalls == 1 && Math.Abs(s_baseDamage - 50d) < 1e-9 && !s_baseFlag && WL.Settings.WLGameplaySettings.PlayerInvincible == flagBefore
|
||||
&& Survival.Applied == 1 && Survival.Lifted == (flagBefore ? 1 : 0) && !Survival.LastWasBoss;
|
||||
sb.AppendLine(P(a) + " ③a 잡몹 100→base " + s_baseDamage + " (기대 50) flagDuring=" + s_baseFlag + " flagAfter=" + WL.Settings.WLGameplaySettings.PlayerInvincible + " (기대 " + flagBefore + ") Lifted=" + Survival.Lifted);
|
||||
if (!a) fails++;
|
||||
|
||||
// b) 보스 ×0.25
|
||||
s_baseCalls = 0; d = Dmg(boss, 100); Survival.TakeDamage(pc, d, s_fake);
|
||||
bool b = s_baseCalls == 1 && Math.Abs(s_baseDamage - 25d) < 1e-9 && !s_baseFlag && Survival.LastWasBoss && WL.Settings.WLGameplaySettings.PlayerInvincible == flagBefore;
|
||||
sb.AppendLine(P(b) + " ③b 보스 100→base " + s_baseDamage + " (기대 25) boss=" + Survival.LastWasBoss);
|
||||
if (!b) fails++;
|
||||
|
||||
// c) 보스 스위치 off → 원본 그대로(피해 100 · 플래그 true 유지)
|
||||
st.takeBossDamage = false;
|
||||
s_baseCalls = 0; d = Dmg(boss, 100); Survival.TakeDamage(pc, d, s_fake);
|
||||
bool c = s_baseCalls == 1 && Math.Abs(s_baseDamage - 100d) < 1e-9 && s_baseFlag == flagBefore && Survival.SkippedBossSwitch == 1;
|
||||
sb.AppendLine(P(c) + " ③c 보스 스위치 off: base dmg=" + s_baseDamage + " (기대 100) flagDuring=" + s_baseFlag + " SkippedBossSwitch=" + Survival.SkippedBossSwitch);
|
||||
if (!c) fails++;
|
||||
st.takeBossDamage = true;
|
||||
|
||||
// d) 정예: 기본 잡몹 취급(×0.5) · eliteCountsAsBoss → ×0.25
|
||||
s_baseCalls = 0; d = Dmg(elite, 100); Survival.TakeDamage(pc, d, s_fake); double e1 = s_baseDamage;
|
||||
st.eliteCountsAsBoss = true;
|
||||
s_baseCalls = 0; d = Dmg(elite, 100); Survival.TakeDamage(pc, d, s_fake); double e2 = s_baseDamage;
|
||||
st.eliteCountsAsBoss = false;
|
||||
bool dd = Math.Abs(e1 - 50d) < 1e-9 && Math.Abs(e2 - 25d) < 1e-9;
|
||||
sb.AppendLine(P(dd) + " ③d 정예: 기본 " + e1 + " (기대 50) · eliteCountsAsBoss " + e2 + " (기대 25)");
|
||||
if (!dd) fails++;
|
||||
|
||||
// e) 잡몹 스위치 off + 적 PC 통과
|
||||
st.takeMobDamage = false;
|
||||
s_baseCalls = 0; d = Dmg(mob, 100); Survival.TakeDamage(pc, d, s_fake); bool e_mob = s_baseDamage == 100d && Survival.SkippedMobSwitch == 1;
|
||||
st.takeMobDamage = true;
|
||||
s_baseCalls = 0; d = Dmg(mob, 100); Survival.TakeDamage(enemyPc, d, s_fake); bool e_pc = s_baseDamage == 100d && Survival.PassNotMainPC == 1 && s_baseFlag == flagBefore;
|
||||
bool e = e_mob && e_pc;
|
||||
sb.AppendLine(P(e) + " ③e 잡몹 스위치 off 통과=" + e_mob + " (SkippedMobSwitch=" + Survival.SkippedMobSwitch + ") · 적 PC 통과=" + e_pc + " (PassNotMainPC=" + Survival.PassNotMainPC + ")");
|
||||
if (!e) fails++;
|
||||
|
||||
// f) base 예외 → finally 복원
|
||||
s_baseThrow = true; bool threw = false;
|
||||
try { Survival.TakeDamage(pc, Dmg(mob, 100), s_fake); } catch (InvalidOperationException) { threw = true; }
|
||||
s_baseThrow = false;
|
||||
bool f = threw && WL.Settings.WLGameplaySettings.PlayerInvincible == flagBefore;
|
||||
sb.AppendLine(P(f) + " ③f base 예외 후 플래그 복원: threw=" + threw + " flag=" + WL.Settings.WLGameplaySettings.PlayerInvincible + " (기대 " + flagBefore + ")");
|
||||
if (!f) fails++;
|
||||
|
||||
// g) 부활 무적 창(게임 시간 클록)
|
||||
Survival.Tick(10f); Survival.SetReviveInvincible(pc, 1.5f);
|
||||
bool g1 = Survival.IsReviveInvincible(pc) && Math.Abs(Survival.ReviveInvincibleRemaining - 1.5f) < 1e-4f;
|
||||
s_baseCalls = 0; Survival.TakeDamage(pc, Dmg(mob, 100), s_fake);
|
||||
bool g2 = s_baseCalls == 1 && s_baseDamage == 100d && s_baseFlag == flagBefore && Survival.BlockedReviveInvincible == 1;
|
||||
Survival.Tick(11.49f); bool g3 = Survival.IsReviveInvincible(pc);
|
||||
Survival.Tick(11.5f); bool g4 = !Survival.IsReviveInvincible(pc);
|
||||
s_baseCalls = 0; Survival.TakeDamage(pc, Dmg(mob, 100), s_fake); bool g5 = s_baseDamage == 50d;
|
||||
bool g6 = !Survival.IsReviveInvincible(enemyPc);
|
||||
bool g = g1 && g2 && g3 && g4 && g5 && g6;
|
||||
sb.AppendLine(P(g) + " ③g 부활 무적 1.5 s: 창 안 통과(dmg " + (g2 ? "100" : s_baseDamage.ToString()) + " Blocked=" + Survival.BlockedReviveInvincible + ") · t+1.49 " + g3 + " · t+1.5 해제 " + g4 + " → 배율 재적용 " + g5 + " · 다른 PC " + g6);
|
||||
if (!g) fails++;
|
||||
|
||||
// h) GC: 힙 크기 델타는 페이지 단위·수집 시점에 좌우되므로 100만 회(할당 시 32 MB) + GC.CollectionCount 로 판정하고,
|
||||
// 델리게이트·대조군은 배열에 살려 둔 채(100k · 수집 불가) 힙 델타로 개당 비용을 잰다.
|
||||
var dd2 = Dmg(mob, 100);
|
||||
for (int i = 0; i < 20; i++) { dd2.Damage = 100; Survival.TakeDamage(pc, dd2, s_fake); }
|
||||
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
|
||||
int gc0 = GC.CollectionCount(0); long a0 = Alloc(), p0 = Mono();
|
||||
for (int i = 0; i < 1000000; i++) { dd2.Damage = 100; Survival.TakeDamage(pc, dd2, s_fake); }
|
||||
long a1 = Alloc(), p1 = Mono(); int gc1 = GC.CollectionCount(0);
|
||||
const int N = 100000;
|
||||
var keepArr = new Action<DamageInfo>[N];
|
||||
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
|
||||
long b0 = Alloc(), q0 = Mono();
|
||||
for (int i = 0; i < N; i++) keepArr[i] = new Action<DamageInfo>(mob.Get_Damage);
|
||||
long b1 = Alloc(), q1 = Mono();
|
||||
var objArr = new object[N];
|
||||
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
|
||||
long c0 = Alloc(), r0 = Mono();
|
||||
for (int i = 0; i < N; i++) objArr[i] = new object();
|
||||
long c1 = Alloc(), r1 = Mono();
|
||||
bool sensitive = (c1 - c0) > 0L || (r1 - r0) > 0L;
|
||||
bool h = a1 - a0 == 0L && p1 - p0 == 0L && gc1 == gc0 && sensitive && keepArr[N - 1] != null && objArr[N - 1] != null;
|
||||
sb.AppendLine(P(h) + " ③h GC: TakeDamage×1,000,000 = 힙 " + (a1 - a0) + " B / mono " + (p1 - p0) + " B / 수집 " + (gc1 - gc0) + "회 (기대 0/0/0)"
|
||||
+ " · 훅 줄 델리게이트 ×100k(보존) = " + (b1 - b0) + " B / mono " + (q1 - q0) + " B (≈" + (Math.Max(b1 - b0, q1 - q0) / N) + " B/피격)"
|
||||
+ " · 대조군 new object()×100k(보존) = " + (c1 - c0) + " B / mono " + (r1 - r0) + " B (감도 " + sensitive + ")");
|
||||
keepArr = null; objArr = null;
|
||||
if (!h) fails++;
|
||||
}
|
||||
finally { Kill(pc, enemyPc, mob, boss, elite); if (gs != null) gs.playerInvincible = flagBefore; }
|
||||
return fails;
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────── ④ 물약
|
||||
static int Potion(StringBuilder sb)
|
||||
{
|
||||
var st = UseClone(); if (st == null) { sb.AppendLine("[FAIL] ④ 클론 주입 실패"); return 1; }
|
||||
st.enabled = true; st.potionCountPerRun = 3; st.potionHealRate = 0.4f; st.potionCooldownSeconds = 5f; st.potionShowHealNumber = false; st.potionRefuseAtFullHp = true;
|
||||
PotionUse.ResetDiagnostics();
|
||||
int changed = 0, used = 0, cdEnded = 0, refused = 0; PotionResult lastRefuse = PotionResult.Used;
|
||||
CombatHandler<PotionEvent> onChanged = (in PotionEvent e) => { changed++; if (e.change == PotionChange.CooldownEnded) cdEnded++; if (e.change == PotionChange.Refused) { refused++; lastRefuse = e.result; } };
|
||||
CombatHandler<PotionEvent> onUsed = (in PotionEvent e) => { used++; };
|
||||
PotionUse.Changed.Add(onChanged); PotionUse.Used.Add(onUsed);
|
||||
var pc = MakePC("pc", 1000, 300);
|
||||
int fails = 0;
|
||||
try
|
||||
{
|
||||
PotionUse.Tick(0f);
|
||||
bool a0 = PotionUse.Remaining == 3 && PotionUse.Max == 3 && PotionUse.IsReady;
|
||||
var r1 = PotionUse.TryUse(pc);
|
||||
bool a = a0 && r1 == PotionResult.Used && Near(pc.Get_HP(), 700d) && PotionUse.Remaining == 2 && Math.Abs(PotionUse.CooldownRemaining - 5f) < 1e-4f && !PotionUse.IsReady && used == 1 && changed == 1;
|
||||
sb.AppendLine(P(a) + " ④a 사용 1: " + r1 + " hp 300→" + pc.Get_HP() + " (기대 700) remaining=" + PotionUse.Remaining + " cd=" + PotionUse.CooldownRemaining.ToString("F2") + " IsReady=" + PotionUse.IsReady);
|
||||
if (!a) fails++;
|
||||
|
||||
var r2 = PotionUse.TryUse(pc);
|
||||
PotionUse.Tick(4.99f); int cd1 = cdEnded;
|
||||
PotionUse.Tick(5f); int cd2 = cdEnded;
|
||||
bool b = r2 == PotionResult.Cooldown && lastRefuse == PotionResult.Cooldown && cd1 == 0 && cd2 == 1 && PotionUse.IsReady && Near(pc.Get_HP(), 700d);
|
||||
sb.AppendLine(P(b) + " ④b 쿨 중 거절=" + r2 + " · t=4.99 CooldownEnded=" + cd1 + " · t=5.0 CooldownEnded=" + cd2 + " IsReady=" + PotionUse.IsReady);
|
||||
if (!b) fails++;
|
||||
|
||||
var r3 = PotionUse.TryUse(pc); // 700 + 400 → 1000(클램프)
|
||||
bool c = r3 == PotionResult.Used && Near(pc.Get_HP(), 1000d) && PotionUse.Remaining == 1;
|
||||
PotionUse.Tick(10f);
|
||||
var r4 = PotionUse.TryUse(pc); // 만충 거절 · 개수 유지
|
||||
bool c2 = r4 == PotionResult.FullHp && PotionUse.Remaining == 1;
|
||||
sb.AppendLine(P(c && c2) + " ④c 사용 2: " + r3 + " hp=" + pc.Get_HP() + " (기대 1000 클램프) remaining=" + PotionUse.Remaining + " · 만충 거절=" + r4 + " remaining 유지=" + (PotionUse.Remaining == 1));
|
||||
if (!(c && c2)) fails++;
|
||||
|
||||
SetHp(pc, 100);
|
||||
var r5 = PotionUse.TryUse(pc); // 3회째
|
||||
bool d = r5 == PotionResult.Used && Near(pc.Get_HP(), 500d) && PotionUse.Remaining == 0 && !PotionUse.IsReady;
|
||||
PotionUse.Tick(20f);
|
||||
var r6 = PotionUse.TryUse(pc);
|
||||
bool d2 = r6 == PotionResult.Empty && PotionUse.Remaining == 0 && used == 3;
|
||||
sb.AppendLine(P(d && d2) + " ④d 사용 3: " + r5 + " hp=" + pc.Get_HP() + " (기대 500) remaining=" + PotionUse.Remaining + " · 빈 통=" + r6 + " Used 이벤트=" + used + " (기대 3)");
|
||||
if (!(d && d2)) fails++;
|
||||
|
||||
int chBefore = changed;
|
||||
PotionUse.ResetRun("probe");
|
||||
int chReset = changed - chBefore;
|
||||
bool e = PotionUse.Remaining == 3 && PotionUse.IsReady && chReset == 1 && PotionUse.RunResets == 1;
|
||||
SetDead(pc, true); var r7 = PotionUse.TryUse(pc); SetDead(pc, false);
|
||||
var r8 = PotionUse.TryUse(null);
|
||||
bool e2 = r7 == PotionResult.Dead && r8 == PotionResult.NoPC && PotionUse.Remaining == 3;
|
||||
sb.AppendLine(P(e && e2) + " ④e 런 리셋: remaining=" + PotionUse.Remaining + " Changed(RunReset)=" + chReset + " · 사망=" + r7 + " · NoPC=" + r8 + " · Changed 총=" + changed + " refused=" + refused);
|
||||
if (!(e && e2)) fails++;
|
||||
}
|
||||
finally { PotionUse.Changed.Remove(onChanged); PotionUse.Used.Remove(onUsed); Kill(pc); }
|
||||
return fails;
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────── ⑤ 사망/부활
|
||||
static int Death(StringBuilder sb)
|
||||
{
|
||||
var st = UseClone(); if (st == null) { sb.AppendLine("[FAIL] ⑤ 클론 주입 실패"); return 1; }
|
||||
st.enabled = true; st.deathDisplaySeconds = 2f; st.holdWorldWhileReviving = true; st.autoReviveWhenNoListener = true; st.autoReviveTimeoutSeconds = 0f;
|
||||
st.reviveInvincibleSeconds = 1.5f; st.reviveHealRate = 1f; st.reviveCompanions = true;
|
||||
DeathFlow.ResetDiagnostics(); Survival.ResetDiagnostics();
|
||||
int died = 0, requested = 0, revived = 0, revivedExternal = 0, revivedAuto = 0, overrideCalls = 0; bool lastHeld = true; float lastInv = -1f;
|
||||
CombatHandler<DeathEvent> onDied = (in DeathEvent e) => { died++; };
|
||||
CombatHandler<ReviveRequestEvent> onReq = (in ReviveRequestEvent e) => { requested++; lastHeld = e.worldHeld; };
|
||||
CombatHandler<RevivedEvent> onRev = (in RevivedEvent e) => { revived++; if (e.external) revivedExternal++; if (e.auto) revivedAuto++; lastInv = e.invincibleSeconds; };
|
||||
DeathFlow.Died.Add(onDied); DeathFlow.ReviveRequested.Add(onReq); DeathFlow.Revived.Add(onRev);
|
||||
DeathFlow.ReviveOverride = (pc, rate) => { overrideCalls++; SetDead(pc, false); SetHp(pc, pc.Get_MaxHP() * rate); };
|
||||
var pc = MakePC("pc", 1000, 0); var enemyPc = MakePC("enemyPc", 1000, 0, true);
|
||||
float ts = Time.timeScale;
|
||||
int fails = 0;
|
||||
try
|
||||
{
|
||||
// a) 정상 사이클: 사망 → 2 s → 요청 → Revive() → 부활 + 무적 1.5 s
|
||||
Survival.Tick(50f); DeathFlow.Tick(100f); SetDead(pc, true);
|
||||
DeathFlow.OnPCDied(pc);
|
||||
bool a1 = DeathFlow.State == DeathState.Dying && died == 1 && DeathFlow.DeathCount == 1 && DeathFlow.Current == pc;
|
||||
DeathFlow.Tick(101f); bool a2 = DeathFlow.State == DeathState.Dying && requested == 0 && Math.Abs(DeathFlow.DisplayRemaining - 1f) < 1e-4f;
|
||||
DeathFlow.Tick(101.99f); bool a3 = DeathFlow.State == DeathState.Dying && requested == 0;
|
||||
DeathFlow.Tick(102f); bool a4 = DeathFlow.State == DeathState.AwaitingRevive && requested == 1 && !lastHeld && !DeathFlow.WorldHeld && IsDead(pc);
|
||||
bool a5 = DeathFlow.Revive();
|
||||
bool a6 = DeathFlow.State == DeathState.None && revived == 1 && revivedExternal == 0 && revivedAuto == 0 && overrideCalls == 1 && !IsDead(pc) && pc.Get_HP() == 1000d && Math.Abs(lastInv - 1.5f) < 1e-4f;
|
||||
bool a7 = Survival.IsReviveInvincible(pc) && Math.Abs(Survival.ReviveInvincibleRemaining - 1.5f) < 1e-4f;
|
||||
Survival.Tick(51.5f); bool a8 = !Survival.IsReviveInvincible(pc);
|
||||
bool a = a1 && a2 && a3 && a4 && a5 && a6 && a7 && a8;
|
||||
sb.AppendLine(P(a) + " ⑤a 사이클: Dying " + a1 + " · t+1 Dying(remain " + (a2 ? "1.00" : DeathFlow.DisplayRemaining.ToString("F2")) + ") · t+1.99 " + a3 + " · t+2 Awaiting+ReviveRequested(held=" + lastHeld + " · 에디트 모드 기대 false) " + a4
|
||||
+ " · Revive()=" + a5 + " → None/Revived/무적 1.5 " + a6 + " · 창 " + a7 + " → 1.5 s 뒤 해제 " + a8 + " · LastDeathToRequest=" + DeathFlow.LastDeathToRequest.ToString("F2"));
|
||||
if (!a) fails++;
|
||||
|
||||
// b) 구독자 0 → 자동 부활
|
||||
DeathFlow.ReviveRequested.Remove(onReq);
|
||||
SetDead(pc, true); DeathFlow.Tick(200f); DeathFlow.OnPCDied(pc); DeathFlow.Tick(202f);
|
||||
bool b = DeathFlow.State == DeathState.None && revived == 2 && revivedAuto == 1 && !IsDead(pc) && DeathFlow.AutoReviveCount == 1;
|
||||
sb.AppendLine(P(b) + " ⑤b 구독자 0 자동 부활: state=" + DeathFlow.State + " revived=" + revived + " auto=" + revivedAuto);
|
||||
if (!b) fails++;
|
||||
DeathFlow.ReviveRequested.Add(onReq);
|
||||
|
||||
// c) 외부(원본 After_Die)가 먼저 살림 → external
|
||||
SetDead(pc, true); DeathFlow.Tick(300f); DeathFlow.OnPCDied(pc); DeathFlow.Tick(302f);
|
||||
bool c1 = DeathFlow.State == DeathState.AwaitingRevive && requested == 2;
|
||||
SetDead(pc, false); int ov = overrideCalls; DeathFlow.Tick(302.1f);
|
||||
bool c = c1 && DeathFlow.State == DeathState.None && revived == 3 && revivedExternal == 1 && overrideCalls == ov && Survival.IsReviveInvincible(pc);
|
||||
sb.AppendLine(P(c) + " ⑤c 외부 부활 감지: Awaiting " + c1 + " → external=" + revivedExternal + " override 미호출 " + (overrideCalls == ov) + " 무적 " + Survival.IsReviveInvincible(pc));
|
||||
if (!c) fails++;
|
||||
|
||||
// d) 연출 중 조기 응답 → 연출 끝에 부활
|
||||
SetDead(pc, true); DeathFlow.Tick(400f); DeathFlow.OnPCDied(pc); DeathFlow.Tick(400.5f);
|
||||
bool d1 = DeathFlow.Revive() && DeathFlow.State == DeathState.Dying && IsDead(pc);
|
||||
DeathFlow.Tick(402f);
|
||||
bool d = d1 && DeathFlow.State == DeathState.None && revived == 4 && !IsDead(pc) && requested == 3;
|
||||
sb.AppendLine(P(d) + " ⑤d 조기 응답: Dying 유지 " + d1 + " → 2 s 뒤 부활 " + (DeathFlow.State == DeathState.None) + " revived=" + revived);
|
||||
if (!d) fails++;
|
||||
|
||||
// e) 타임아웃 자동 부활
|
||||
st.autoReviveTimeoutSeconds = 3f;
|
||||
SetDead(pc, true); DeathFlow.Tick(500f); DeathFlow.OnPCDied(pc); DeathFlow.Tick(502f);
|
||||
bool e1 = DeathFlow.State == DeathState.AwaitingRevive;
|
||||
DeathFlow.Tick(504.9f); bool e2 = DeathFlow.State == DeathState.AwaitingRevive;
|
||||
DeathFlow.Tick(505f); bool e3 = DeathFlow.State == DeathState.None && revived == 5 && revivedAuto == 2;
|
||||
st.autoReviveTimeoutSeconds = 0f;
|
||||
bool e = e1 && e2 && e3;
|
||||
sb.AppendLine(P(e) + " ⑤e 타임아웃 3 s: +2.9 대기 " + e2 + " · +3.0 자동 부활 " + e3 + " · LastRequestToRevive=" + DeathFlow.LastRequestToRevive.ToString("F2"));
|
||||
if (!e) fails++;
|
||||
|
||||
// f) Abort · 중복 사망 · 적 PC
|
||||
SetDead(pc, true); DeathFlow.Tick(600f); DeathFlow.OnPCDied(pc); int dc = DeathFlow.DeathCount;
|
||||
DeathFlow.OnPCDied(pc); bool f1 = DeathFlow.DeathCount == dc;
|
||||
DeathFlow.Abort("probe"); bool f2 = DeathFlow.State == DeathState.None && DeathFlow.AbortCount == 1 && revived == 5 && IsDead(pc);
|
||||
SetDead(pc, false);
|
||||
SetDead(enemyPc, true); DeathFlow.OnPCDied(enemyPc); bool f3 = DeathFlow.State == DeathState.None && DeathFlow.DeathCount == dc; SetDead(enemyPc, false);
|
||||
bool f4 = Time.timeScale == ts;
|
||||
bool f = f1 && f2 && f3 && f4;
|
||||
sb.AppendLine(P(f) + " ⑤f 중복 사망 무시 " + f1 + " · Abort " + f2 + " · 적 PC 무시 " + f3 + " · timeScale 무변경 " + f4 + " (" + Time.timeScale + ")");
|
||||
if (!f) fails++;
|
||||
|
||||
// g) Revive() with nothing pending → false
|
||||
bool g = !DeathFlow.Revive();
|
||||
sb.AppendLine(P(g) + " ⑤g 대기 없는 Revive()=false " + g);
|
||||
if (!g) fails++;
|
||||
}
|
||||
finally
|
||||
{
|
||||
DeathFlow.Died.Remove(onDied); DeathFlow.ReviveRequested.Remove(onReq); DeathFlow.Revived.Remove(onRev);
|
||||
DeathFlow.ReviveOverride = null; DeathFlow.ResetDiagnostics(); Survival.ResetDiagnostics();
|
||||
Kill(pc, enemyPc);
|
||||
}
|
||||
return fails;
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────── ⑥ 이벤트 GC
|
||||
static int EventGc(StringBuilder sb)
|
||||
{
|
||||
var st = UseClone(); if (st == null) { sb.AppendLine("[FAIL] ⑥ 클론 주입 실패"); return 1; }
|
||||
st.enabled = true; st.verboseLog = false;
|
||||
int n = 0;
|
||||
CombatHandler<PotionEvent> h = (in PotionEvent e) => { n++; };
|
||||
PotionUse.Changed.Add(h);
|
||||
try
|
||||
{
|
||||
for (int i = 0; i < 10; i++) PotionUse.ResetRun("");
|
||||
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
|
||||
int g0 = GC.CollectionCount(0); long a0 = Alloc(), p0 = Mono();
|
||||
for (int i = 0; i < 1000000; i++) PotionUse.ResetRun("");
|
||||
long a1 = Alloc(), p1 = Mono(); int g1 = GC.CollectionCount(0);
|
||||
bool ok = a1 - a0 == 0L && p1 - p0 == 0L && g1 == g0 && n == 1000010;
|
||||
sb.AppendLine(P(ok) + " ⑥ PotionUse.Changed 디스패치×1,000,000 = 힙 " + (a1 - a0) + " B / mono " + (p1 - p0) + " B / 수집 " + (g1 - g0) + "회 (기대 0) handler=" + n);
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
finally { PotionUse.Changed.Remove(h); PotionUse.ResetDiagnostics(); }
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────── ⑦ 정리
|
||||
static int Cleanup(StringBuilder sb, float ts0)
|
||||
{
|
||||
RestoreAsset();
|
||||
if (s_clone != null) { UnityEngine.Object.DestroyImmediate(s_clone); s_clone = null; }
|
||||
Survival.ResetDiagnostics(); PotionUse.ResetDiagnostics(); DeathFlow.ResetDiagnostics();
|
||||
var st = WLSurvivalSettings.Instance;
|
||||
bool assetBack = st != null && !st.enabled && !WLSurvivalSettings.Enabled;
|
||||
bool flag = WL.Settings.WLGameplaySettings.PlayerInvincible;
|
||||
int leftovers = 0;
|
||||
foreach (var go in UnityEngine.Object.FindObjectsByType<GameObject>(FindObjectsInactive.Include, FindObjectsSortMode.None))
|
||||
if (go != null && go.name.StartsWith("__WL813i_")) { leftovers++; UnityEngine.Object.DestroyImmediate(go); }
|
||||
bool dirty = false; string scene = "?";
|
||||
#if UNITY_EDITOR
|
||||
var sc = UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene(); dirty = sc.isDirty; scene = sc.name;
|
||||
#endif
|
||||
bool ok = assetBack && flag && leftovers == 0 && Time.timeScale == ts0 && !SurvivalRunner.Exists && MyValue.MyPC == null;
|
||||
sb.AppendLine(P(ok) + " ⑦ 정리: 디스크 에셋 enabled=false 복귀 " + assetBack + " · playerInvincible=" + flag + " · 잔존 오브젝트 " + leftovers
|
||||
+ " · timeScale " + Time.timeScale + "(시작 " + ts0 + ") · 러너 생성 0 " + !SurvivalRunner.Exists + " · MyPC=null " + (MyValue.MyPC == null) + " · 씬 '" + scene + "' dirty=" + dirty);
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -264,7 +264,7 @@ public class PCActor : MyActor
|
|||
public override void Get_Damage(DamageInfo _dinfo)
|
||||
{
|
||||
//_dinfo.Damage = 1; // ?뚯뒪??: 留덈? ??二쎄쾶
|
||||
base.Get_Damage(_dinfo);
|
||||
WL.Combat.Survival.Survival.TakeDamage(this, _dinfo, base.Get_Damage); // WL #813i 무적 해제 게이트(원본 = base.Get_Damage(_dinfo) · SO off 면 그대로 base 호출)
|
||||
if (!IsEnemy() && _dinfo.Damage >= Get_MaxHP() * 0.5d && !isSurviveTime)
|
||||
StartCoroutine(Co_Survive(5f));
|
||||
}
|
||||
|
|
@ -485,6 +485,7 @@ public class PCActor : MyActor
|
|||
//NewGameUI.Ins.m_GuideQuestUI.Add_GuideQuest(ePurpose.DieOrQuit);
|
||||
DungeonInfo.Ins.Sub_WaveLife();
|
||||
MonsterSimpleInfo.Ins.Set_DelLockTarget(true);
|
||||
WL.Combat.Survival.DeathFlow.OnPCDied(this); // WL #813i 사망 연출 → 부활 요청 → 존 시작점 On_Regen(SO off 면 즉시 반환)
|
||||
}
|
||||
|
||||
protected override IEnumerator Co_ScaleControl(bool returntoOriginalScale, float targetscale, Action actComplete = null)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 8d2f1434ac6b4b71828c7c8301d4cb5a, type: 3}
|
||||
m_Name: WLSurvivalSettings
|
||||
m_EditorClassIdentifier: Assembly-CSharp::WL.Combat.Survival.WLSurvivalSettings
|
||||
enabled: 0
|
||||
verboseLog: 0
|
||||
takeMobDamage: 1
|
||||
mobDamageMultiplier: 0.5
|
||||
takeBossDamage: 1
|
||||
bossDamageMultiplier: 0.5
|
||||
eliteCountsAsBoss: 0
|
||||
potionCountPerRun: 3
|
||||
potionHealRate: 0.4
|
||||
potionCooldownSeconds: 5
|
||||
potionShowHealNumber: 1
|
||||
potionRefuseAtFullHp: 1
|
||||
potionResetOnMapLoad: 1
|
||||
deathDisplaySeconds: 2
|
||||
holdWorldWhileReviving: 1
|
||||
autoReviveWhenNoListener: 1
|
||||
autoReviveTimeoutSeconds: 0
|
||||
reviveInvincibleSeconds: 1.5
|
||||
reviveHealRate: 1
|
||||
reviveCompanions: 1
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: dc47d184d5804340b907753ea1b59004
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: f937213861694ed48d60dc6f8d9475ea
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,197 @@
|
|||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// DeathFlow.cs — PC 사망 → 사망 연출(2 s · 카메라 추가 연출 0 · 디졸브 0 · 조작 차단) → 부활 요청(813j 팝업) → 응답 → 존 시작점 On_Regen → 무적 창
|
||||
//
|
||||
// PD 지시 #813 · 발주서 WL-813i §1-3 · 기준서 v1 §B 요소 8 회복 · §D-1 813i 행 (2026-09-09)
|
||||
//
|
||||
// ■ 원본 실측(Actor.cs)
|
||||
// Cal_Damage :583 HP ≤ 0 → DeadStatus = true → Set_Die :585 → PCActor.Set_Die :481(All_Stop(true) · Sub_WaveLife · DelLockTarget)
|
||||
// Actor.Set_Die :626 → Play_Die · Set_Coroutine(After_Die, 3f)(익명 코루틴 · 게임 시간 · 취소 불가) · 콜라이더/NavMesh off
|
||||
// After_Die :640 → Check_Extra(AfterDie)(PC 는 no-op) → `if (!IsDead()) return;` → PC: On_Regen()(즉시 부활 · 연출 0) + 소환수/펫/일꾼 On_Regen
|
||||
// InGameInfo.Defeat_byMonster :442 = 전부 주석(빈 함수) — 대체하지 않고 이 파일이 새 흐름을 맡는다.
|
||||
// MyActor.On_Regen :388 = base.On_Regen(전회복 · 상태 초기화) + Set_Warp(LoadMapMgr.Get_ReincanationPos) = **존 시작점** ← 부활은 이 공개 API 로.
|
||||
//
|
||||
// ■ 흐름(훅 = PCActor.Set_Die 끝 1줄 `DeathFlow.OnPCDied(this)`)
|
||||
// Dying(deathDisplaySeconds · unscaled) → AwaitingRevive: ReviveRequested 발생 + (holdWorldWhileReviving) Time.timeScale = 0
|
||||
// · 세계를 세우는 이유 ① 팝업 동안 몹이 시체를 두들기지 않게 ② 원본 After_Die(3 s · 게임 시간)가 먼저 부활시키지 못하게(붙잡아 둠).
|
||||
// · 원본 PauseUI 가 같은 방식(timeScale 0 · 복원 = MyValue.Get_GameSpeed())이라 TimeScaleArbiter 는 이걸 외부 일시정지로 관측한다.
|
||||
// 813j 가 Revive() 로 응답 → 세계 복원 → pc.On_Regen(false, reviveHealRate) → 동료 On_Regen → Survival.SetReviveInvincible → Revived.
|
||||
// 이후 원본 After_Die 가 도착해도 `!IsDead()` 로 아무 것도 하지 않는다(실측 :643).
|
||||
// 구독자 0(813j 미병합)이면 autoReviveWhenNoListener 로 즉시 부활 · autoReviveTimeoutSeconds 로 시간 상한 · 원본이 먼저 부활시키면 external=true 로 통보.
|
||||
// ■ 조작 차단 = 원본이 이미 한다(Run_byJoystick/Play_* 의 IsDead 가드 · Set_Die 의 All_Stop(true)) + 세계 정지. 카메라 = 팔로우 대상이 멈추므로 자연 정지(추가 연출 0).
|
||||
// ■ 비활성(WLSurvivalSettings off)이면 OnPCDied 가 즉시 반환 = 원본 After_Die 3 s 자동 부활 100%(C8).
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using WL.Combat.Core;
|
||||
|
||||
namespace WL.Combat.Survival
|
||||
{
|
||||
public enum DeathState { None = 0, Dying = 1, AwaitingRevive = 2 }
|
||||
|
||||
/// <summary>PC 사망 통보(연출 시작 · 813j HUD 가 조작 UI 를 잠글 때).</summary>
|
||||
public struct DeathEvent { public PCActor pc; public Vector3 position; public int deathCount; public float displaySeconds; public float time; public int frame; }
|
||||
/// <summary>부활 요청(813j 부활 팝업이 띄우고 DeathFlow.Revive() 로 응답한다).</summary>
|
||||
public struct ReviveRequestEvent { public PCActor pc; public int deathCount; public bool worldHeld; public float timeoutSeconds; public float time; public int frame; }
|
||||
/// <summary>부활 완료(팝업 닫기 · HUD 잠금 해제 · 무적 연출). external = 원본 After_Die 등 바깥이 먼저 살렸다.</summary>
|
||||
public struct RevivedEvent { public PCActor pc; public Vector3 position; public int deathCount; public float invincibleSeconds; public bool external; public bool auto; public float time; public int frame; }
|
||||
|
||||
public static class DeathFlow
|
||||
{
|
||||
public static readonly CombatEventList<DeathEvent> Died = new CombatEventList<DeathEvent>(4);
|
||||
public static readonly CombatEventList<ReviveRequestEvent> ReviveRequested = new CombatEventList<ReviveRequestEvent>(4);
|
||||
public static readonly CombatEventList<RevivedEvent> Revived = new CombatEventList<RevivedEvent>(4);
|
||||
|
||||
static DeathState s_state;
|
||||
static PCActor s_pc;
|
||||
static float s_now, s_diedAt, s_requestedAt;
|
||||
static bool s_worldHeld, s_acceptEarly;
|
||||
|
||||
// ── 진단(프로브 · 813m 로그가 읽는다)
|
||||
public static int DeathCount, ReviveCount, ExternalReviveCount, AutoReviveCount, AbortCount;
|
||||
public static float LastDeathToRequest, LastRequestToRevive;
|
||||
public static string LastAbortReason = "";
|
||||
|
||||
/// <summary>프로브 전용 — 부활 실행을 바꿔치기한다(에디트 모드에는 LoadMapMgr·ActorInfo 가 없다). 런타임에서는 null 로 둘 것.</summary>
|
||||
public static Action<PCActor, float> ReviveOverride;
|
||||
|
||||
public static DeathState State { get { return s_state; } }
|
||||
public static PCActor Current { get { return s_pc; } }
|
||||
public static bool WorldHeld { get { return s_worldHeld; } }
|
||||
/// <summary>사망 연출 남은 시간(초 · unscaled · 813j 가 카운트다운에 써도 된다).</summary>
|
||||
public static float DisplayRemaining { get { var st = St; return s_state == DeathState.Dying && st != null ? Mathf.Max(0f, st.deathDisplaySeconds - (s_now - s_diedAt)) : 0f; } }
|
||||
|
||||
static WLSurvivalSettings St { get { return WLSurvivalSettings.Instance; } }
|
||||
static bool Verbose { get { var st = St; return st != null && st.verboseLog; } }
|
||||
|
||||
/// <summary>PCActor.Set_Die 훅(1줄 · base.Set_Die 뒤). 비활성 · 메인 PC 아님 · 진행 중이면 무시.</summary>
|
||||
public static void OnPCDied(PCActor pc)
|
||||
{
|
||||
if (!WLSurvivalSettings.Enabled || pc == null || !pc.IsMainPC()) return;
|
||||
if (s_state != DeathState.None) return;
|
||||
Survival.EnsureRunner();
|
||||
if (Application.isPlaying) s_now = Time.unscaledTime; // 에디트 모드(프로브)는 Tick 이 넣은 클록을 쓴다
|
||||
s_pc = pc; s_state = DeathState.Dying; s_diedAt = s_now; s_acceptEarly = false;
|
||||
DeathCount++;
|
||||
var st = St;
|
||||
if (Verbose) Debug.Log("[DeathFlow] died #" + DeathCount + " display=" + st.deathDisplaySeconds.ToString("F1") + "s");
|
||||
var e = new DeathEvent { pc = pc, position = pc.Get_position(), deathCount = DeathCount, displaySeconds = st.deathDisplaySeconds, time = Time.unscaledTime, frame = Time.frameCount };
|
||||
Died.Dispatch(in e);
|
||||
}
|
||||
|
||||
/// <summary>러너(또는 프로브)가 unscaled 시간을 넣는다.</summary>
|
||||
public static void Tick(float unscaledNow)
|
||||
{
|
||||
s_now = unscaledNow;
|
||||
if (s_state == DeathState.None) return;
|
||||
var st = St;
|
||||
if (st == null || !WLSurvivalSettings.Enabled) { Abort("disabled"); return; }
|
||||
if (s_pc == null) { Abort("pc destroyed"); return; }
|
||||
if (!s_pc.IsOnlyDead()) { Finish(true, false); return; } // 바깥(원본 After_Die · 리스폰)이 먼저 살렸다
|
||||
|
||||
if (s_state == DeathState.Dying)
|
||||
{
|
||||
if (s_now - s_diedAt < st.deathDisplaySeconds) return;
|
||||
s_state = DeathState.AwaitingRevive; s_requestedAt = s_now;
|
||||
LastDeathToRequest = s_now - s_diedAt;
|
||||
if (st.holdWorldWhileReviving) HoldWorld();
|
||||
var e = new ReviveRequestEvent { pc = s_pc, deathCount = DeathCount, worldHeld = s_worldHeld, timeoutSeconds = st.autoReviveTimeoutSeconds, time = Time.unscaledTime, frame = Time.frameCount };
|
||||
ReviveRequested.Dispatch(in e);
|
||||
if (Verbose) Debug.Log("[DeathFlow] revive requested listeners=" + ReviveRequested.Count + " held=" + s_worldHeld);
|
||||
if (s_acceptEarly) { Finish(false, false); return; }
|
||||
if (ReviveRequested.Count == 0 && st.autoReviveWhenNoListener) { Finish(false, true); return; }
|
||||
return;
|
||||
}
|
||||
|
||||
// AwaitingRevive
|
||||
if (st.autoReviveTimeoutSeconds > 0f && s_now - s_requestedAt >= st.autoReviveTimeoutSeconds) Finish(false, true);
|
||||
}
|
||||
|
||||
/// <summary>813j 부활 팝업의 응답. AwaitingRevive 면 즉시 부활 · Dying 이면 연출이 끝나는 순간 부활 · None 이면 false.</summary>
|
||||
public static bool Revive()
|
||||
{
|
||||
switch (s_state)
|
||||
{
|
||||
case DeathState.AwaitingRevive: Finish(false, false); return true;
|
||||
case DeathState.Dying: s_acceptEarly = true; return true;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
static void Finish(bool external, bool auto)
|
||||
{
|
||||
var st = St;
|
||||
var pc = s_pc;
|
||||
ReleaseWorld();
|
||||
LastRequestToRevive = s_state == DeathState.AwaitingRevive ? s_now - s_requestedAt : 0f;
|
||||
s_state = DeathState.None; s_pc = null; s_acceptEarly = false;
|
||||
|
||||
float inv = st != null ? st.reviveInvincibleSeconds : 0f;
|
||||
if (!external && pc != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (ReviveOverride != null) ReviveOverride(pc, st.reviveHealRate);
|
||||
else pc.On_Regen(false, st.reviveHealRate); // MyActor.On_Regen → 전회복 + 존 시작점 Set_Warp · PCActor.On_Regen → All_Stop(false) 등
|
||||
if (st.reviveCompanions && ReviveOverride == null) ReviveCompanions(pc);
|
||||
}
|
||||
catch (Exception ex) { Debug.LogException(ex); }
|
||||
}
|
||||
Survival.SetReviveInvincible(pc, inv);
|
||||
ReviveCount++; if (external) ExternalReviveCount++; if (auto) AutoReviveCount++;
|
||||
if (Verbose) Debug.Log("[DeathFlow] revived external=" + external + " auto=" + auto + " invincible=" + inv.ToString("F2") + "s");
|
||||
var e = new RevivedEvent { pc = pc, position = pc != null ? pc.Get_position() : Vector3.zero, deathCount = DeathCount, invincibleSeconds = inv, external = external, auto = auto, time = Time.unscaledTime, frame = Time.frameCount };
|
||||
Revived.Dispatch(in e);
|
||||
}
|
||||
|
||||
/// <summary>원본 After_Die 의 PC 분기 재현(소환수 이동 · 펫 부활 · 일꾼 이동) — 전부 공개 API.</summary>
|
||||
static void ReviveCompanions(PCActor pc)
|
||||
{
|
||||
if (pc.IsEnemy()) return;
|
||||
var pet = pc.Get_MyPet();
|
||||
if (!DSUtil.CheckNull(pet)) pet.On_Regen();
|
||||
var info = ActorInfo.Ins;
|
||||
if (info == null) return;
|
||||
var summ = info.Get_Actor(eRole.Summon);
|
||||
if (!DSUtil.CheckNull(summ)) summ.On_Regen();
|
||||
var worker = info.Get_Actor(eRole.Worker);
|
||||
if (!DSUtil.CheckNull(worker)) worker.On_Regen();
|
||||
}
|
||||
|
||||
/// <summary>흐름 중단(런 경계 · 로비 · PC 소실 · SO off). 세계 정지를 풀고 상태만 비운다(부활 호출 0).</summary>
|
||||
public static void Abort(string reason)
|
||||
{
|
||||
if (s_state == DeathState.None && !s_worldHeld) return;
|
||||
ReleaseWorld();
|
||||
s_state = DeathState.None; s_pc = null; s_acceptEarly = false;
|
||||
AbortCount++; LastAbortReason = reason ?? "";
|
||||
if (Verbose) Debug.Log("[DeathFlow] abort (" + reason + ")");
|
||||
}
|
||||
|
||||
static void HoldWorld()
|
||||
{
|
||||
if (!Application.isPlaying) return; // 에디트 모드에서 timeScale 대입은 TimeManager.asset 에 적힌다(811b-fix 실측) — 프로브 안전
|
||||
if (s_worldHeld) return;
|
||||
Time.timeScale = 0f; // 원본 PauseUI.Set 과 같은 방식
|
||||
s_worldHeld = true;
|
||||
}
|
||||
|
||||
/// <summary>세계 정지 해제(우리가 세웠고 아직 0 일 때만 · 복원값 = 원본 PauseUI 와 같은 MyValue.Get_GameSpeed()).</summary>
|
||||
internal static void ReleaseWorld()
|
||||
{
|
||||
if (!s_worldHeld) return;
|
||||
s_worldHeld = false;
|
||||
if (Time.timeScale == 0f) Time.timeScale = MyValue.Get_GameSpeed();
|
||||
}
|
||||
|
||||
/// <summary>프로브용: 상태·카운터 초기화(구독은 유지 · 세계 정지 해제).</summary>
|
||||
public static void ResetDiagnostics()
|
||||
{
|
||||
ReleaseWorld();
|
||||
s_state = DeathState.None; s_pc = null; s_acceptEarly = false; s_diedAt = s_requestedAt = 0f;
|
||||
DeathCount = ReviveCount = ExternalReviveCount = AutoReviveCount = AbortCount = 0;
|
||||
LastDeathToRequest = LastRequestToRevive = 0f; LastAbortReason = "";
|
||||
Died.Raised = Died.Dispatched = 0; ReviveRequested.Raised = ReviveRequested.Dispatched = 0; Revived.Raised = Revived.Dispatched = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: c5bbc2bf1a7246698cf1bcbae6ef5d7e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// PotionUse.cs — 전투 물약(런당 N개 · 최대 HP 비율 회복 · 쿨다운) — 813j 물약 버튼이 부르는 사용 API + 잔량/쿨 이벤트
|
||||
//
|
||||
// PD 지시 #813 · 발주서 WL-813i §1-2 · 기준서 v1 §B 요소 8 회복(3개/런 · 40 % · 쿨 5 s · Actor.Heal 사용) (2026-09-09)
|
||||
//
|
||||
// ■ 813j(UI) 가 쓰는 API
|
||||
// · 사용 요청: PotionUse.TryUse() → PotionResult (Used 면 회복됨 · 그 외 거절 사유)
|
||||
// · 상태 읽기: Remaining · Max · CooldownRemaining · CooldownTotal · IsReady (매 프레임 읽어도 GC 0)
|
||||
// · 이벤트: PotionUse.Changed(in PotionEvent) — 잔량/쿨 변화 때마다(런 리셋 · 사용 · 쿨 종료 · 거절) · PotionUse.Used — 사용 성공만
|
||||
// · 런 리셋: PotionUse.ResetRun() (813m 런 시작이 부를 수 있다 · potionResetOnMapLoad 면 맵 진입 때 자동)
|
||||
// ■ 회복 = 원본 Actor.Heal(float rate, bool showhud) — 사망·만충이면 원본이 아무 것도 하지 않으므로 사용 전에 거절한다.
|
||||
// ■ 시간 = 게임 시간(Time.time · 러너 Tick) — 일시정지·부활 대기(timeScale 0) 중엔 쿨이 흐르지 않는다.
|
||||
// ■ 비활성(WLSurvivalSettings off)이면 TryUse 는 Disabled 를 돌려주고 이벤트 0 = 원본 100%(원본엔 전투 물약이 없다).
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using WL.Combat.Core;
|
||||
|
||||
namespace WL.Combat.Survival
|
||||
{
|
||||
public enum PotionResult { Used = 0, Disabled = 1, NoPC = 2, Dead = 3, Empty = 4, Cooldown = 5, FullHp = 6 }
|
||||
public enum PotionChange { RunReset = 0, Used = 1, CooldownEnded = 2, Refused = 3 }
|
||||
|
||||
/// <summary>물약 상태 스냅샷(struct · GC 0). Changed/Used 구독자가 받는다.</summary>
|
||||
public struct PotionEvent
|
||||
{
|
||||
public PCActor pc;
|
||||
public PotionChange change;
|
||||
public PotionResult result; // Refused 일 때 사유 · 그 외 Used
|
||||
public int remaining, max;
|
||||
public float cooldownRemaining, cooldownTotal;
|
||||
public double healed, hpAfter, maxHp;
|
||||
public float time;
|
||||
public int frame;
|
||||
}
|
||||
|
||||
public static class PotionUse
|
||||
{
|
||||
/// <summary>잔량/쿨 변화 통지(813j 버튼 갱신). 런 리셋 · 사용 · 쿨 종료 · 거절.</summary>
|
||||
public static readonly CombatEventList<PotionEvent> Changed = new CombatEventList<PotionEvent>(4);
|
||||
/// <summary>사용 성공 통지(연출 · 로그).</summary>
|
||||
public static readonly CombatEventList<PotionEvent> Used = new CombatEventList<PotionEvent>(4);
|
||||
|
||||
static int s_remaining = -1; // -1 = 런 미시작(첫 조회 때 SO 값으로 채운다)
|
||||
static float s_cooldownEnd = -1f;
|
||||
static float s_now;
|
||||
static bool s_cooldownNotified = true;
|
||||
|
||||
// ── 진단(프로브가 읽는다)
|
||||
public static int UsedCount, RefusedCount, RunResets;
|
||||
public static PotionResult LastResult;
|
||||
public static double LastHealed;
|
||||
|
||||
static WLSurvivalSettings St { get { return WLSurvivalSettings.Instance; } }
|
||||
|
||||
/// <summary>남은 개수(런 미시작이면 SO 값).</summary>
|
||||
public static int Remaining { get { EnsureRun(); return s_remaining; } }
|
||||
/// <summary>런당 개수(SO).</summary>
|
||||
public static int Max { get { var st = St; return st != null ? st.potionCountPerRun : 0; } }
|
||||
/// <summary>쿨 남은 시간(초 · 게임 시간).</summary>
|
||||
public static float CooldownRemaining { get { return Mathf.Max(0f, s_cooldownEnd - s_now); } }
|
||||
/// <summary>쿨 전체 길이(SO · 813j 링 비율 = Remaining/Total).</summary>
|
||||
public static float CooldownTotal { get { var st = St; return st != null ? st.potionCooldownSeconds : 0f; } }
|
||||
/// <summary>지금 누르면 쓸 수 있는가(활성 · 잔량 · 쿨). HP 만충·사망은 TryUse 가 거절한다.</summary>
|
||||
public static bool IsReady { get { return WLSurvivalSettings.Enabled && Remaining > 0 && CooldownRemaining <= 0f; } }
|
||||
|
||||
static void EnsureRun()
|
||||
{
|
||||
if (s_remaining >= 0) return;
|
||||
var st = St;
|
||||
s_remaining = st != null ? Mathf.Max(0, st.potionCountPerRun) : 0;
|
||||
s_cooldownEnd = -1f; s_cooldownNotified = true;
|
||||
}
|
||||
|
||||
/// <summary>메인 PC(MyValue.MyPC)에 사용.</summary>
|
||||
public static PotionResult TryUse() { return TryUse(MyValue.MyPC); }
|
||||
|
||||
/// <summary>813j 물약 버튼이 부른다. Used 면 회복됨. 거절이어도 Changed(Refused · 사유) 를 내 UI 가 피드백할 수 있다.</summary>
|
||||
public static PotionResult TryUse(PCActor pc)
|
||||
{
|
||||
if (!WLSurvivalSettings.Enabled) return Finish(PotionResult.Disabled, pc, 0d, false);
|
||||
Survival.EnsureRunner();
|
||||
EnsureRun();
|
||||
if (pc == null) return Finish(PotionResult.NoPC, pc, 0d, true);
|
||||
if (pc.IsDead()) return Finish(PotionResult.Dead, pc, 0d, true);
|
||||
if (s_remaining <= 0) return Finish(PotionResult.Empty, pc, 0d, true);
|
||||
if (s_now < s_cooldownEnd) return Finish(PotionResult.Cooldown, pc, 0d, true);
|
||||
var st = St;
|
||||
double before = pc.Get_HP(), max = pc.Get_MaxHP();
|
||||
if (st.potionRefuseAtFullHp && before >= max) return Finish(PotionResult.FullHp, pc, 0d, true);
|
||||
|
||||
try { pc.Heal(st.potionHealRate, st.potionShowHealNumber); } // 원본 Heal(rate) · HUD 초록 숫자
|
||||
catch (Exception ex) { Debug.LogException(ex); } // HUD 쪽 예외가 물약 상태를 깨지 않게 격리(회복 자체는 HP 갱신 뒤)
|
||||
|
||||
double healed = pc.Get_HP() - before;
|
||||
s_remaining--;
|
||||
s_cooldownEnd = s_now + Mathf.Max(0f, st.potionCooldownSeconds);
|
||||
s_cooldownNotified = st.potionCooldownSeconds <= 0f;
|
||||
UsedCount++; LastHealed = healed;
|
||||
if (st.verboseLog)
|
||||
Debug.Log("[PotionUse] used +" + healed.ToString("F0") + " hp=" + pc.Get_HP().ToString("F0") + "/" + max.ToString("F0")
|
||||
+ " remaining=" + s_remaining + "/" + st.potionCountPerRun + " cd=" + st.potionCooldownSeconds.ToString("F1"));
|
||||
return Finish(PotionResult.Used, pc, healed, true);
|
||||
}
|
||||
|
||||
static PotionResult Finish(PotionResult r, PCActor pc, double healed, bool notify)
|
||||
{
|
||||
LastResult = r;
|
||||
if (r != PotionResult.Used) RefusedCount++;
|
||||
if (!notify) return r;
|
||||
var e = Snapshot(pc, r == PotionResult.Used ? PotionChange.Used : PotionChange.Refused, r, healed);
|
||||
Changed.Dispatch(in e);
|
||||
if (r == PotionResult.Used) Used.Dispatch(in e);
|
||||
return r;
|
||||
}
|
||||
|
||||
static PotionEvent Snapshot(PCActor pc, PotionChange change, PotionResult r, double healed)
|
||||
{
|
||||
var st = St;
|
||||
return new PotionEvent
|
||||
{
|
||||
pc = pc, change = change, result = r,
|
||||
remaining = s_remaining < 0 ? 0 : s_remaining, max = st != null ? st.potionCountPerRun : 0,
|
||||
cooldownRemaining = CooldownRemaining, cooldownTotal = st != null ? st.potionCooldownSeconds : 0f,
|
||||
healed = healed,
|
||||
hpAfter = pc != null ? pc.Get_HP() : 0d, maxHp = pc != null ? pc.Get_MaxHP() : 0d,
|
||||
time = Time.unscaledTime, frame = Time.frameCount
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>런 시작(맵 진입 · 813m) — 잔량을 SO 값으로 · 쿨 해제. 비활성이면 이벤트 0.</summary>
|
||||
public static void ResetRun(string reason = "")
|
||||
{
|
||||
var st = St;
|
||||
s_remaining = st != null ? Mathf.Max(0, st.potionCountPerRun) : 0;
|
||||
s_cooldownEnd = -1f; s_cooldownNotified = true;
|
||||
RunResets++;
|
||||
if (!WLSurvivalSettings.Enabled) return;
|
||||
if (st.verboseLog) Debug.Log("[PotionUse] run reset (" + reason + ") remaining=" + s_remaining);
|
||||
var e = Snapshot(MyValue.MyPC, PotionChange.RunReset, PotionResult.Used, 0d);
|
||||
Changed.Dispatch(in e);
|
||||
}
|
||||
|
||||
/// <summary>러너(또는 프로브)가 게임 시간을 넣는다. 쿨이 끝나는 프레임에 Changed(CooldownEnded) 1회.</summary>
|
||||
public static void Tick(float gameNow)
|
||||
{
|
||||
s_now = gameNow;
|
||||
if (s_cooldownNotified || s_now < s_cooldownEnd) return;
|
||||
s_cooldownNotified = true;
|
||||
if (!WLSurvivalSettings.Enabled) return;
|
||||
var e = Snapshot(MyValue.MyPC, PotionChange.CooldownEnded, PotionResult.Used, 0d);
|
||||
Changed.Dispatch(in e);
|
||||
}
|
||||
|
||||
/// <summary>프로브용: 상태·카운터 초기화(구독은 유지).</summary>
|
||||
public static void ResetDiagnostics()
|
||||
{
|
||||
s_remaining = -1; s_cooldownEnd = -1f; s_cooldownNotified = true;
|
||||
UsedCount = RefusedCount = RunResets = 0; LastResult = PotionResult.Used; LastHealed = 0d;
|
||||
Changed.Raised = Changed.Dispatched = 0; Used.Raised = Used.Dispatched = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5bf46f3924424ef9b33fe31cc528d489
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,220 @@
|
|||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Survival.cs — 무적 해제 게이트(받는 피해 배율 · 잡몹/보스 분리) + 부활 무적 창 + 생존 축 공용 틱 러너
|
||||
//
|
||||
// PD 지시 #813 · 발주서 WL-813i §1-1 · 기준서 v1 §B 요소 8 · §F-2 · 설계안 WL-811a §A 7행 (2026-09-09)
|
||||
//
|
||||
// ■ 왜 PCActor.Get_Damage 훅인가 (실측 · 발주서 "실측 후 최소 침습")
|
||||
// PC 피해 경로 = PCActor.Get_Damage :264 → base = MyActor.Get_Damage :301
|
||||
// → `wlInvincible = IsRole(PC) && WLGameplaySettings.PlayerInvincible` 이면 Actor.Get_Damage(방어 공식 · Cal_Damage · Set_Die)
|
||||
// 를 **아예 부르지 않고** RaiseDamaged(invincible=true) 만 낸다.
|
||||
// · Damaged 이벤트 구독으로는 피해를 넣을 수 없다 — 그 시점엔 이미 base 가 건너뛰어졌고, 다시 Get_Damage 를 부르면 같은 게이트에 막힌다.
|
||||
// · Actor.Cal_Damage 직접 호출은 방어 공식(:400-462) · 관통 · 감쇠 · 마나실드 · CC · HitConfirmed 를 전부 건너뛴다(스탯 의미 상실).
|
||||
// · MyActor.cs(무적 분기) 수정 금지 · WLGameplaySettings.asset 값 변경 금지.
|
||||
// → PCActor.Get_Damage 의 `base.Get_Damage(_dinfo)` 1줄을 `Survival.TakeDamage(this, _dinfo, base.Get_Damage)` 로 바꾼다.
|
||||
// base 메서드 그룹은 C# 이 **비가상**으로 MyActor.Get_Damage 에 묶어 준다. 이 안에서
|
||||
// ① 부활 무적 창이면 원본 그대로(무적 플래그 유지 → 피해 0) ② 잡몹/보스 스위치가 꺼져 있으면 원본 그대로
|
||||
// ③ 아니면 원 피해 × 배율 → WLGameplaySettings.Instance.playerInvincible 을 **이 호출 동안만** false 로 내리고 base 호출 → finally 로 복원.
|
||||
// 비활성(에셋 없음 · enabled=false)이면 base 만 부른다 = 원본 100%(C8).
|
||||
// 비용: 훅 줄이 base 델리게이트 1개를 만든다(피격 1회당 ≈ 64 B · 원본이 피격마다 new DamageInfo 를 만드는 것과 같은 자릿수). 완료보고에 기재.
|
||||
//
|
||||
// ■ 배율은 방어 공식 **전**(원 피해)에 곱한다 — Actor.Get_Damage 뒤에는 PC 에 훅 지점이 없다. 방어 공식은 피해에 선형이라
|
||||
// "받는 피해 0.5" 의 체감은 같고, 고정 피해(ATK_FIXED) 항만 배율 밖이다.
|
||||
//
|
||||
// ■ 러너(SurvivalRunner · 숨은 오브젝트 1개 · DontDestroyOnLoad · 코루틴 없음)는 SO 가 켜져 있을 때만 만든다(C8: 꺼져 있으면 오브젝트 0).
|
||||
// 틱: Survival(게임 시간) · PotionUse(게임 시간) · DeathFlow(unscaled · 세계를 세워도 흘러야 한다) · 런 경계 폴링(맵/모드/PC 변화).
|
||||
//
|
||||
// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace WL.Combat.Survival
|
||||
{
|
||||
/// <summary>무적 해제 게이트 + 부활 무적 창 + 런 경계. 정적 · GC 0(verboseLog 때만 문자열).</summary>
|
||||
public static class Survival
|
||||
{
|
||||
// ── 진단(프로브가 읽는다)
|
||||
public static int Applied; // 배율 적용 후 base 호출(무적 해제) 횟수
|
||||
public static int Lifted; // 그중 playerInvincible 을 실제로 내렸다 되돌린 횟수
|
||||
public static int PassDisabled; // SO off → 원본 그대로
|
||||
public static int PassNotMainPC; // 메인 PC 아님(적 PC 등) → 원본 그대로
|
||||
public static int BlockedReviveInvincible; // 부활 무적 창 → 원본 그대로(피해 0)
|
||||
public static int SkippedMobSwitch, SkippedBossSwitch;
|
||||
public static double LastRawDamage, LastScaledDamage, LastFinalDamage;
|
||||
public static bool LastWasBoss;
|
||||
public static bool LastFlagDuringCall; // base 호출 직전 PlayerInvincible 값(기대 false)
|
||||
|
||||
static float s_now; // 게임 시간(러너 Tick · 프로브는 직접 넣는다)
|
||||
static float s_reviveInvincibleUntil = -1f;
|
||||
static Actor s_invinciblePc;
|
||||
|
||||
/// <summary>마지막 틱의 게임 시간(Time.time · 프로브 클록).</summary>
|
||||
public static float Now { get { return s_now; } }
|
||||
public static float ReviveInvincibleUntil { get { return s_reviveInvincibleUntil; } }
|
||||
public static float ReviveInvincibleRemaining { get { return Mathf.Max(0f, s_reviveInvincibleUntil - s_now); } }
|
||||
|
||||
static WLSurvivalSettings St { get { return WLSurvivalSettings.Instance; } }
|
||||
static bool Verbose { get { var st = St; return st != null && st.verboseLog; } }
|
||||
|
||||
/// <summary>부활 무적 창 안인가(게임 시간 · 813j HP 바 연출이 읽어도 된다).</summary>
|
||||
public static bool IsReviveInvincible(Actor pc)
|
||||
{
|
||||
return pc != null && ReferenceEquals(pc, s_invinciblePc) && s_now < s_reviveInvincibleUntil;
|
||||
}
|
||||
|
||||
/// <summary>DeathFlow 가 부활 직후 부른다. seconds ≤ 0 이면 창 없음.</summary>
|
||||
public static void SetReviveInvincible(Actor pc, float seconds)
|
||||
{
|
||||
if (pc == null || seconds <= 0f) { s_invinciblePc = null; s_reviveInvincibleUntil = -1f; return; }
|
||||
s_invinciblePc = pc;
|
||||
s_reviveInvincibleUntil = s_now + seconds;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PCActor.Get_Damage 훅(1줄). baseCall = base.Get_Damage(MyActor · 비가상). 비활성이면 baseCall 만 = 원본 100%.
|
||||
/// </summary>
|
||||
public static void TakeDamage(PCActor pc, DamageInfo dinfo, Action<DamageInfo> baseCall)
|
||||
{
|
||||
if (baseCall == null) return;
|
||||
if (!WLSurvivalSettings.Enabled || pc == null || dinfo == null) { PassDisabled++; baseCall(dinfo); return; }
|
||||
if (!pc.IsMainPC()) { PassNotMainPC++; baseCall(dinfo); return; }
|
||||
EnsureRunner();
|
||||
|
||||
if (IsReviveInvincible(pc)) { BlockedReviveInvincible++; baseCall(dinfo); return; } // 원본 무적 경로(피해 0 · Damaged(invincible=true))
|
||||
|
||||
var st = St;
|
||||
bool boss = IsBossSource(dinfo, st);
|
||||
if (boss ? !st.takeBossDamage : !st.takeMobDamage)
|
||||
{
|
||||
if (boss) SkippedBossSwitch++; else SkippedMobSwitch++;
|
||||
baseCall(dinfo); // 스위치 off = 그 진영엔 원본 무적 그대로
|
||||
return;
|
||||
}
|
||||
|
||||
double mult = boss ? st.bossDamageMultiplier : st.mobDamageMultiplier;
|
||||
if (mult < 0d) mult = 0d;
|
||||
double raw = dinfo.Damage;
|
||||
dinfo.Damage = raw * mult;
|
||||
LastRawDamage = raw; LastScaledDamage = dinfo.Damage; LastWasBoss = boss;
|
||||
|
||||
var gs = WL.Settings.WLGameplaySettings.Instance;
|
||||
bool lift = gs != null && gs.playerInvincible; // 이미 꺼져 있으면(PD 가 에셋에서 해제) 손대지 않는다
|
||||
if (lift) { gs.playerInvincible = false; Lifted++; }
|
||||
LastFlagDuringCall = WL.Settings.WLGameplaySettings.PlayerInvincible;
|
||||
try { baseCall(dinfo); }
|
||||
finally { if (lift) gs.playerInvincible = true; } // 디스크 값 무변경 · 예외가 나도 되돌린다
|
||||
Applied++;
|
||||
LastFinalDamage = dinfo.Damage; // Actor.Get_Damage 가 dinfo 를 제자리에서 갱신 → 방어 후 최종 피해
|
||||
|
||||
if (Verbose)
|
||||
Debug.Log("[Survival] " + (boss ? "boss" : "mob") + " dmg " + raw.ToString("F0") + " ×" + mult.ToString("F2")
|
||||
+ " → " + LastScaledDamage.ToString("F0") + " → final " + dinfo.Damage.ToString("F0")
|
||||
+ " hp=" + pc.Get_HP().ToString("F0") + "/" + pc.Get_MaxHP().ToString("F0") + (lift ? " (lifted)" : ""));
|
||||
}
|
||||
|
||||
static bool IsBossSource(DamageInfo dinfo, WLSurvivalSettings st)
|
||||
{
|
||||
var beater = dinfo.Beater;
|
||||
if (beater == null) return false; // 가해자 없음(함정 등) = 잡몹 취급
|
||||
if (beater.IsSubRole(eSubRol.Boss)) return true;
|
||||
return st.eliteCountsAsBoss && beater.IsSubRole(eSubRol.Elite);
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────── 틱 · 런 경계
|
||||
|
||||
/// <summary>러너(또는 프로브)가 게임 시간을 넣는다.</summary>
|
||||
public static void Tick(float gameNow) { s_now = gameNow; }
|
||||
|
||||
static PCActor s_lastPc;
|
||||
static int s_lastMapId = int.MinValue;
|
||||
static int s_lastMode = int.MinValue;
|
||||
public static int RunStarts;
|
||||
public static string LastRunReason = "";
|
||||
|
||||
/// <summary>러너가 매 프레임 부른다. 메인 PC 교체 · Stage/Dungeon 맵 진입을 런 시작으로 본다(원본 훅 0 · 폴링).</summary>
|
||||
internal static void PollRunBoundary()
|
||||
{
|
||||
var st = St;
|
||||
if (st == null) return;
|
||||
|
||||
var pc = MyValue.MyPC; // 파괴된 참조는 Unity != 로 걸러진다
|
||||
if (!ReferenceEquals(pc, s_lastPc))
|
||||
{
|
||||
s_lastPc = pc;
|
||||
if (pc != null) OnRunStart("pc");
|
||||
else DeathFlow.Abort("pc lost");
|
||||
}
|
||||
|
||||
var info = InGameInfo.Ins;
|
||||
if (info == null) return;
|
||||
int mode = (int)info.Get_GameMode();
|
||||
int map = MyValue.MyChoiceMapData != null ? MyValue.MyChoiceMapData.n_MapID : -1;
|
||||
if (mode != s_lastMode || map != s_lastMapId)
|
||||
{
|
||||
bool first = s_lastMode == int.MinValue;
|
||||
s_lastMode = mode; s_lastMapId = map;
|
||||
if (first) return; // 부팅 직후 첫 관측은 경계가 아니다
|
||||
var m = (eGameMode)mode;
|
||||
if (m == eGameMode.Lobby) DeathFlow.Abort("lobby");
|
||||
else if ((m == eGameMode.Stage || m == eGameMode.Dungeon) && st.potionResetOnMapLoad) OnRunStart("map " + map);
|
||||
}
|
||||
}
|
||||
|
||||
static void OnRunStart(string reason)
|
||||
{
|
||||
RunStarts++; LastRunReason = reason;
|
||||
DeathFlow.Abort(reason);
|
||||
PotionUse.ResetRun(reason);
|
||||
SetReviveInvincible(null, 0f);
|
||||
if (Verbose) Debug.Log("[Survival] run start (" + reason + ")");
|
||||
}
|
||||
|
||||
internal static void EnsureRunner()
|
||||
{
|
||||
if (Application.isPlaying) SurvivalRunner.Ensure();
|
||||
}
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
||||
static void Boot()
|
||||
{
|
||||
if (WLSurvivalSettings.Enabled) SurvivalRunner.Ensure(); // 꺼져 있으면 오브젝트 0(C8)
|
||||
}
|
||||
|
||||
/// <summary>프로브용: 카운터·창 초기화.</summary>
|
||||
public static void ResetDiagnostics()
|
||||
{
|
||||
Applied = Lifted = PassDisabled = PassNotMainPC = BlockedReviveInvincible = SkippedMobSwitch = SkippedBossSwitch = 0;
|
||||
LastRawDamage = LastScaledDamage = LastFinalDamage = 0d; LastWasBoss = false; LastFlagDuringCall = false;
|
||||
s_reviveInvincibleUntil = -1f; s_invinciblePc = null;
|
||||
RunStarts = 0; LastRunReason = "";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>생존 축 틱 러너(숨은 오브젝트 1개 · DontDestroyOnLoad · 코루틴 없음). 파괴·비활성·앱 종료 시 세계 정지를 반드시 푼다.</summary>
|
||||
public sealed class SurvivalRunner : MonoBehaviour
|
||||
{
|
||||
static SurvivalRunner s_runner;
|
||||
|
||||
public static bool Exists { get { return s_runner != null; } }
|
||||
|
||||
public static void Ensure()
|
||||
{
|
||||
if (s_runner != null) return;
|
||||
var go = new GameObject("__WLSurvival");
|
||||
DontDestroyOnLoad(go);
|
||||
s_runner = go.AddComponent<SurvivalRunner>();
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
Survival.Tick(Time.time);
|
||||
PotionUse.Tick(Time.time);
|
||||
DeathFlow.Tick(Time.unscaledTime);
|
||||
Survival.PollRunBoundary();
|
||||
}
|
||||
|
||||
void OnDisable() { DeathFlow.ReleaseWorld(); }
|
||||
void OnApplicationQuit() { DeathFlow.ReleaseWorld(); }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: dc93f89305b14490ba351c8953e9871f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// WLSurvivalSettings.cs — "위험과 회복"(무적 해제 게이트 · 받는 피해 배율 · 물약 · 사망/부활) 값의 단일 출처(SOT · C45)
|
||||
//
|
||||
// PD 지시 #813 · 발주서 WL-813i §1-4 · 기준서 v1 §B 요소 8 · §D-1 813i 행 · §F-2 (2026-09-09)
|
||||
//
|
||||
// 에셋 경로(고정): Assets/WL/Combat/Settings/Resources/WL/WLSurvivalSettings.asset
|
||||
// → Resources.Load<WLSurvivalSettings>("WL/WLSurvivalSettings") 로 1회 로드 후 캐시(코어 설정 SO 와 같은 관례).
|
||||
// 에셋이 없거나 enabled = false 면 Enabled == false → Survival · PotionUse · DeathFlow 가 아무 것도 하지 않는다
|
||||
// = 원본 + 기존 WL 동작 100%(플레이어 무적 유지 · 물약 없음 · After_Die 3 s 자동 부활). C8 롤백 1순위 스위치.
|
||||
//
|
||||
// 🔴 기본 enabled = false — 813j(물약 버튼 · 부활 팝업 UI)와 **동시 병합**할 때 Lead 가 켠다(기준서 §F-2).
|
||||
// WLGameplaySettings.playerInvincible 은 손대지 않는다 — 이 SO 가 켜지면 Survival.TakeDamage 가 피격 1회 동안만
|
||||
// 그 플래그를 내렸다가 되돌린다(디스크 값 무변경).
|
||||
//
|
||||
// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것(PCActor 가 Assembly-CSharp 에 있다).
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace WL.Combat.Survival
|
||||
{
|
||||
[CreateAssetMenu(fileName = "WLSurvivalSettings", menuName = "WL/Survival Settings", order = 325)]
|
||||
public sealed class WLSurvivalSettings : ScriptableObject
|
||||
{
|
||||
public const string ResourcesPath = "WL/WLSurvivalSettings";
|
||||
|
||||
static WLSurvivalSettings s_cached;
|
||||
static bool s_lookupDone;
|
||||
|
||||
/// <summary>설정 에셋(1회 로드 후 캐시). 없으면 null.</summary>
|
||||
public static WLSurvivalSettings Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!s_lookupDone)
|
||||
{
|
||||
s_cached = Resources.Load<WLSurvivalSettings>(ResourcesPath);
|
||||
s_lookupDone = true;
|
||||
}
|
||||
return s_cached;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// QA·A/B 전용 런타임 스위치(에셋 무수정 · Play 종료 시 도메인 리로드로 자동 0).
|
||||
/// 0 = 에셋의 enabled 를 따른다 · 1 = 에셋이 꺼져 있어도 켠다(813j 병합 전 QA) · -1 = 강제 off.
|
||||
/// </summary>
|
||||
public static int RuntimeOverride;
|
||||
|
||||
/// <summary>생존 축이 살아 있는가 = 에셋 존재 · (enabled 또는 강제 on) · 강제 off 아님. false 면 모든 경로가 원본 동작이다.</summary>
|
||||
public static bool Enabled
|
||||
{
|
||||
get
|
||||
{
|
||||
var i = Instance;
|
||||
if (i == null || RuntimeOverride < 0) return false;
|
||||
return RuntimeOverride > 0 || i.enabled;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>테스트용: 캐시를 비워 다음 접근 때 다시 Resources.Load 한다.</summary>
|
||||
public static void ClearCache() { s_cached = null; s_lookupDone = false; }
|
||||
|
||||
// ─────────────────────────────────────────── 전체 스위치
|
||||
[Header("전체 스위치")]
|
||||
[Tooltip("끄면 무적 해제 · 물약 · 사망/부활 흐름이 전부 비활성 — 플레이어 무적(WLGameplaySettings.playerInvincible) 그대로 · 원본 After_Die 3 s 자동 부활 그대로. 롤백 1순위. 813j UI 와 동시 병합 때 Lead 가 켠다.")]
|
||||
public bool enabled = false;
|
||||
|
||||
[Tooltip("피해 배율 · 물약 · 사망/부활 결정을 Console 에 남긴다(검증용 · 기본 off · 문자열 GC 발생).")]
|
||||
public bool verboseLog = false;
|
||||
|
||||
// ─────────────────────────────────────────── 받는 피해
|
||||
[Header("받는 피해 (기준서 §B 요소 8 위험 · §F-2 · 잡몹/보스 분리)")]
|
||||
[Tooltip("잡몹(Boss 가 아닌 가해자 · 정예 포함)의 피해를 받을지. 끄면 잡몹에게는 원본 무적 그대로.")]
|
||||
public bool takeMobDamage = true;
|
||||
|
||||
[Tooltip("잡몹 피해에 곱하는 받는 피해 배율. 원본 Actor.Get_Damage(방어 공식) 진입 **전** 원 피해에 곱한다. 기준서 제안 0.5 로 시작.")]
|
||||
public float mobDamageMultiplier = 0.5f;
|
||||
|
||||
[Tooltip("보스(eSubRol.Boss 가해자)의 피해를 받을지. 끄면 보스에게는 원본 무적 그대로.")]
|
||||
public bool takeBossDamage = true;
|
||||
|
||||
[Tooltip("보스 피해에 곱하는 받는 피해 배율. 기준서 제안 0.5 로 시작.")]
|
||||
public float bossDamageMultiplier = 0.5f;
|
||||
|
||||
[Tooltip("켜면 정예(eSubRol.Elite) 가해자를 보스 배율·스위치로 다룬다. 기본은 잡몹 취급.")]
|
||||
public bool eliteCountsAsBoss = false;
|
||||
|
||||
// ─────────────────────────────────────────── 물약
|
||||
[Header("물약 (기준서 §B 요소 8 회복 · 3개/런 · 40 % · 쿨 5 s)")]
|
||||
[Tooltip("런(맵 진입)당 물약 개수.")]
|
||||
public int potionCountPerRun = 3;
|
||||
|
||||
[Tooltip("1회 회복량(최대 HP 비율 · Actor.Heal(rate)). 0.4 = 40 %.")]
|
||||
public float potionHealRate = 0.4f;
|
||||
|
||||
[Tooltip("사용 후 재사용 대기(초 · 게임 시간 = 일시정지 중 정지).")]
|
||||
public float potionCooldownSeconds = 5f;
|
||||
|
||||
[Tooltip("회복 수치를 원본 데미지 텍스트(HUDDMGUI · 초록)로 띄운다.")]
|
||||
public bool potionShowHealNumber = true;
|
||||
|
||||
[Tooltip("HP 가 가득 차 있으면 사용을 거절한다(개수 낭비 방지 · Actor.Heal 도 만충이면 아무 것도 안 한다).")]
|
||||
public bool potionRefuseAtFullHp = true;
|
||||
|
||||
[Tooltip("Stage/Dungeon 맵에 들어갈 때(맵 ID·게임 모드 변화) 물약 개수를 런 시작값으로 되돌린다. 끄면 PotionUse.ResetRun() 호출(813m 런 시작)로만 리셋된다.")]
|
||||
public bool potionResetOnMapLoad = true;
|
||||
|
||||
// ─────────────────────────────────────────── 사망 · 부활
|
||||
[Header("사망 · 부활 (기준서 사망 연출 2 s → 부활 요청 → 존 시작점 On_Regen · 부활 후 무적 1.5 s)")]
|
||||
[Tooltip("사망 연출 길이(초 · unscaled). 이 시간이 지나면 ReviveRequested 를 낸다(813j 팝업).")]
|
||||
public float deathDisplaySeconds = 2f;
|
||||
|
||||
[Tooltip("부활 요청을 낸 뒤 응답까지 Time.timeScale = 0 으로 세계를 세운다(원본 PauseUI 선례). 원본 After_Die(사망 3 s 뒤 자동 부활 · 코루틴 · 게임 시간)를 붙잡아 두는 수단이기도 하다 — 끄면 팝업 응답이 3 s 를 넘길 때 원본이 먼저 부활시킨다(external 부활로 통보).")]
|
||||
public bool holdWorldWhileReviving = true;
|
||||
|
||||
[Tooltip("ReviveRequested 구독자가 0 이면(813j 미병합 · QA) 요청 즉시 자동 부활한다. 끄면 구독자가 없어도 기다린다(autoReviveTimeoutSeconds 참고).")]
|
||||
public bool autoReviveWhenNoListener = true;
|
||||
|
||||
[Tooltip("부활 요청 뒤 이 시간(초 · unscaled) 안에 응답이 없으면 자동 부활. 0 이면 무제한 대기.")]
|
||||
public float autoReviveTimeoutSeconds = 0f;
|
||||
|
||||
[Tooltip("부활 직후 무적 시간(초 · 게임 시간). 이 동안 PC 피격은 원본 무적 경로(피해 0 · Damaged(invincible=true) 발생).")]
|
||||
public float reviveInvincibleSeconds = 1.5f;
|
||||
|
||||
[Tooltip("부활 시 회복 비율(On_Regen healrate · 1 = 전회복 · 원본 After_Die 와 동일).")]
|
||||
public float reviveHealRate = 1f;
|
||||
|
||||
[Tooltip("부활 시 소환수·펫·일꾼도 On_Regen 한다(원본 After_Die 의 PC 분기와 동일).")]
|
||||
public bool reviveCompanions = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8d2f1434ac6b4b71828c7c8301d4cb5a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Loading…
Reference in New Issue