[WL-813h] 보스 페이즈·예고·쿨 데이터화 (#813)

- Assets/WL/Combat/Boss/BossEvents.cs (NEW): BossPhase 통지 허브. 코어
  CombatEvents.cs 는 무수정 — 811d~j 와 충돌 방지. 같은 규약(정적 · struct
  페이로드 in 전달 · 사전 할당 구독자 배열 · 예외 격리 · WLCombatCoreSettings
  enabled/RuntimeDisabled 준수). 실측 GC = 1000회 디스패치 0 byte.
- Assets/WL/Combat/Boss/BossPatternTable.cs (NEW): 보스 패턴 JSON 로더.
  스폰(On_Regen) 시 그 Boss_*.cs 가 직접 선언한 public float/int/bool 필드에만
  대입(DeclaredOnly) → Boss_*.cs 수정 0 · 프리팹 값 변경 0. 행이 없으면 무동작.
- Assets/WL/Combat/Settings/Resources/WL/WL813h_BossPattern.json (NEW):
  보스 12행(10001~10012). 813b 실측(프리팹 인스펙터) 값 그대로이며, Anubis(10006)
  의 예고(차징) 3 s → 1.2 s 만 발주서 제안값으로 데이터에서 바꾼다.
  페이즈 임계 기본 [1.0, 0.7, 0.4] · 스킬 해금 임계 기본 0.9 / 0.7(원본과 동일).
- Assets/Script/Character/Mob/BossMobActor.cs: 훅 3줄(로더 적용 1 · 임계 조회 1 ·
  페이즈 통지 1). 원본 라인은 그대로 둬서 C8 시 100% 원본 동작.
- AgentScripts/WL813h_Probe.cs + WL813h_PROBE.txt: 에디트 모드 프로브와 실행 덤프
  (로더 적용 · 행 없음 원복 · 페이즈 3회 · GC 0 · RuntimeDisabled 롤백 전부 PASS).

게이트 A PASS(oneshot · exit 0 · error CS 0줄 · 0.8분) · 콘솔 error 0 · Play 0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
깃 관리자 2026-09-09 00:19:49 +09:00
parent 21c0fae954
commit 2b43190037
10 changed files with 968 additions and 0 deletions

View File

@ -0,0 +1,68 @@
===== WL-813h 보스 패턴 로더 · 페이즈 통지 프로브 =====
playMode=False time=2026-09-09 00:18:21
── ① 로더 상태
코어 Enabled = True
로더 Enabled = True
RowCount = 12
ParseFailed = False
── ① 임계 조회표 (skill1 / skill2 = 해금 임계 · phases = 페이즈 임계)
ID 이름 skill1 skill2 phases 필드수
10001 Lusif 0.9 0.7 [1, 0.7, 0.4] 8
10002 Lusif_B 0.9 0.7 [1, 0.7, 0.4] 7
10003 Mega_Ice_Golem 0.9 0.7 [1, 0.7, 0.4] 11
10004 Minotos 0.9 0.7 [1, 0.7, 0.4] 7
10005 Golem_Child 0.9 0.7 [1, 0.7, 0.4] 8
10006 Anubis 0.9 0.7 [1, 0.7, 0.4] 9
10007 Queen_Undead 0.9 0.7 [1, 0.7, 0.4] 10
10008 Flower Dryad 0.9 0.7 [1, 0.7, 0.4] 15
10009 Angez_B 0.9 0.7 [1, 0.7, 0.4] 11
10010 Angez_C 0.9 0.7 [1, 0.7, 0.4] 9
10011 Brother_Penguin_C 0.9 0.7 [1, 0.7, 0.4] 14
10012 Shaga_B 0.9 0.7 [1, 0.7, 0.4] 12
── ② Anubis(10006) 프리팹 인스턴스에 행 적용 (LoadPrefabContents · 저장 없음)
대상 = Boss_Anubis on 'Anubis'
ApplyById(10006) → 적용 필드 9개 · 미지원 키 0개
필드 프리팹값 → 적용후 (변화)
Skill1CoolTime 18 → 18
Skill1Dmg 2.2 → 2.2
Skill1ChargingTime 3 → 1.2 <<< 변경
Perforation_Line False → False
Skill2CoolTime 15 → 15
Skill2ChargingTime 3 → 1.2 <<< 변경
Skill2AttackDelay 1 → 1
Skill2Dmg 3 → 3
Skill2Speed 15 → 15
── ③ 행 없음(ID 99999) → 값 유지 검증
Revert(10006) = 9개 되돌림 (프리팹 원본 값으로 복구)
ApplyById(99999) = -1 (1 = 무동작) · 변경된 필드 0개
판정 = PASS (행 없으면 프리팹 값 그대로)
── ④ BossPhase 통지 (가짜 HP 100 → 70 → 40 %)
#1 phase 0→1/3 id=10006 hp=100 % 임계=1 spawn=True
#2 phase 1→2/3 id=10006 hp=70 % 임계=0.7 spawn=False
#3 phase 2→3/3 id=10006 hp=40 % 임계=0.4 spawn=False
이벤트 3회 · Raised=3 Dispatched=3 최종 페이즈=3
판정 = PASS (100→70→40 · 3회 · 회복 시 되돌림 없음)
── ④ 스킬 해금 임계 (원본 0.9 / 0.7 · 행이 값을 주면 그 값)
HP 100 % → CanUseSkill1=False CanUseSkill2=False
HP 95 % → CanUseSkill1=False CanUseSkill2=False
HP 90 % → CanUseSkill1=True CanUseSkill2=False
HP 80 % → CanUseSkill1=True CanUseSkill2=False
HP 70 % → CanUseSkill1=True CanUseSkill2=True
HP 50 % → CanUseSkill1=True CanUseSkill2=True
── ⑤ 디스패치 GC (Raise 1000회 · 구독자는 카운터만 = 로그 문자열 할당 배제)
구독자 1명 · 수신 1000회 · 할당 델타 = 0 byte (회당 0 byte)
── ⑥ C8 롤백 (RuntimeDisabled = true → 로더·통지 무동작)
로더 Enabled=False · ApplyById=-1 · 변경 필드=0 · 게이트(HP 10%)=False/False · UpdatePhase=0 · 이벤트=0
판정 = PASS (프리팹 값 · 원본 게이트 · 통지 0)
===== 끝 =====

View File

@ -0,0 +1,260 @@
// PD 지시 #813 · 발주서 WL-813h §1-4 — 보스 패턴 로더 · 페이즈 통지 에디트 모드 프로브 (읽기 전용)
//
// 실행 (CLAUDE.md §1: using 금지 · 네임스페이스 풀어 쓰기 · 첫 Roslyn 컴파일은 --timeout 60)
// unity command run_script --file AgentScripts/WL813h_Probe.cs --entry WL813h_Probe.Run --timeout 180
//
// 무엇을 확인하나 (발주서 §3 ⓒ ⓓ ⓔ)
// ① 로더 상태(JSON 행 수 · 파싱) 와 12행 임계 조회표
// ② Anubis(10006) 프리팹 인스턴스에 행 값이 적용되는지 — 적용 전/후 필드 덤프
// ③ 행 없음(가짜 ID) → 값 무변경(원본 유지)
// ④ BossPhase 통지 — 가짜 HP 100 → 70 → 40 % 에서 이벤트 3회 + 페이로드
// ⑤ 디스패치 GC 할당(1000회 Raise 전후 GC.GetTotalMemory 차)
// ⑥ C8 — RuntimeDisabled = true 면 로더/통지 무동작
//
// Play 하지 않는다. 프리팹은 LoadPrefabContents → (저장 없이) UnloadPrefabContents 라 에셋 무변경.
// 수치는 전부 JSON/프리팹에서 읽는다(C45). 프로브의 반복 횟수·가짜 HP 는 "측정 조건"이지 게임 데이터가 아니다.
public static class WL813h_Probe
{
const string kAnubisPrefab = "Assets/Res_Addr/Mobs/FieldBoss/Anubis.prefab";
const int kAnubisId = 10006;
const int kMissingId = 99999; // JSON 에 없는 ID (행 없음 경로 검증)
const int kGcIterations = 1000; // 측정 조건
const string kLog = "AgentScripts/WL813h_PROBE.txt"; // 발주서 §4 허용 경로(AgentScripts/WL813h_*)
static System.Text.StringBuilder s;
static int s_eventCount;
static string s_eventLog;
public static string Run()
{
s = new System.Text.StringBuilder();
s_eventCount = 0; s_eventLog = "";
L("===== WL-813h 보스 패턴 로더 · 페이즈 통지 프로브 =====");
L("playMode=" + UnityEngine.Application.isPlaying + " time=" + System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
WL.Combat.Core.WLCombatCoreSettings.RuntimeDisabled = false;
WL.Combat.Boss.BossPatternTable.ClearCache();
WL.Combat.Boss.BossEvents.ResetDiagnostics();
Section1_Table();
Section2_Apply(); // ②③ 안에서 ④⑤(페이즈·GC)까지 같은 인스턴스로 이어서 돈다
Section4_Rollback();
L("");
L("===== 끝 =====");
return Flush();
}
// ───────────────────────────────────────── ① 로더 상태 + 임계 조회표
static void Section1_Table()
{
L("");
L("── ① 로더 상태");
L(" 코어 Enabled = " + WL.Combat.Core.WLCombatCoreSettings.Enabled);
L(" 로더 Enabled = " + WL.Combat.Boss.BossPatternTable.Enabled);
L(" RowCount = " + WL.Combat.Boss.BossPatternTable.RowCount);
L(" ParseFailed = " + WL.Combat.Boss.BossPatternTable.ParseFailed +
(WL.Combat.Boss.BossPatternTable.LoadError.Length > 0 ? (" (" + WL.Combat.Boss.BossPatternTable.LoadError + ")") : ""));
L("");
L("── ① 임계 조회표 (skill1 / skill2 = 해금 임계 · phases = 페이즈 임계)");
L(" ID 이름 skill1 skill2 phases 필드수");
var f = WL.Combat.Boss.BossPatternTable.File;
if (f == null || f.rows == null) { L(" (JSON 없음)"); return; }
for (int i = 0; i < f.rows.Count; i++)
{
var r = f.rows[i];
float t1, t2;
WL.Combat.Boss.BossPatternTable.GetSkillThresholds(r.id, out t1, out t2);
var th = WL.Combat.Boss.BossPatternTable.GetPhaseThresholds(r.id);
L(string.Format(" {0,-7} {1,-20} {2,-7} {3,-7} {4,-21} {5}",
r.id, r.name, t1.ToString("0.###"), t2.ToString("0.###"),
"[" + string.Join(", ", System.Array.ConvertAll(th, x => x.ToString("0.##"))) + "]",
r.fields != null ? r.fields.Count : 0));
}
}
// ───────────────────────────────────────── ② 행 적용 · ③ 행 없음
static void Section2_Apply()
{
L("");
L("── ② Anubis(10006) 프리팹 인스턴스에 행 적용 (LoadPrefabContents · 저장 없음)");
var root = UnityEditor.PrefabUtility.LoadPrefabContents(kAnubisPrefab);
if (root == null) { L(" [에러] 프리팹 로드 실패: " + kAnubisPrefab); return; }
try
{
var boss = root.GetComponentInChildren<BossMobActor>(true);
if (boss == null) { L(" [에러] BossMobActor 컴포넌트 없음"); return; }
L(" 대상 = " + boss.GetType().Name + " on '" + boss.gameObject.name + "'");
var before = Dump(boss);
int applied = WL.Combat.Boss.BossPatternTable.ApplyById(boss, kAnubisId);
var after = Dump(boss);
L(" ApplyById(10006) → 적용 필드 " + applied + "개 · 미지원 키 " + WL.Combat.Boss.BossPatternTable.LastUnknownKeys + "개");
L("");
L(" 필드 프리팹값 → 적용후 (변화)");
foreach (var kv in before)
{
string a = after.ContainsKey(kv.Key) ? after[kv.Key] : "?";
L(string.Format(" {0,-24} {1,-9} → {2,-9} {3}", kv.Key, kv.Value, a, kv.Value == a ? "" : "<<< 변경"));
}
L("");
L("── ③ 행 없음(ID " + kMissingId + ") → 값 유지 검증");
int reverted = WL.Combat.Boss.BossPatternTable.Revert(boss, kAnubisId);
L(" Revert(10006) = " + reverted + "개 되돌림 (프리팹 원본 값으로 복구)");
var baseline = Dump(boss);
int applied2 = WL.Combat.Boss.BossPatternTable.ApplyById(boss, kMissingId);
var afterMissing = Dump(boss);
int changed = 0;
foreach (var kv in baseline) if (!afterMissing.ContainsKey(kv.Key) || afterMissing[kv.Key] != kv.Value) changed++;
L(" ApplyById(" + kMissingId + ") = " + applied2 + " (1 = 무동작) · 변경된 필드 " + changed + "개");
L(" 판정 = " + (applied2 == -1 && changed == 0 ? "PASS (행 없으면 프리팹 값 그대로)" : "FAIL"));
// 페이즈 검사는 같은 인스턴스로 이어서 한다.
s_probeBoss = boss;
Section3_PhaseInner();
}
finally
{
s_probeBoss = null;
UnityEditor.PrefabUtility.UnloadPrefabContents(root); // 저장하지 않으므로 프리팹 에셋 무변경
}
}
static BossMobActor s_probeBoss;
// ───────────────────────────────────────── ④ 페이즈 통지 · ⑤ GC
static void Section3_PhaseInner()
{
var boss = s_probeBoss;
L("");
L("── ④ BossPhase 통지 (가짜 HP 100 → 70 → 40 %)");
WL.Combat.Core.CombatHandler<WL.Combat.Boss.BossPhaseEvent> h = OnPhase;
WL.Combat.Boss.BossEvents.BossPhase.Add(h);
WL.Combat.Boss.BossEvents.ResetDiagnostics();
s_eventCount = 0; s_eventLog = "";
try
{
WL.Combat.Boss.BossPatternTable.ApplyById(boss, kAnubisId); // 스폰 = 페이즈 1
WL.Combat.Boss.BossPatternTable.UpdatePhaseById(boss, kAnubisId, 1.00f);
WL.Combat.Boss.BossPatternTable.UpdatePhaseById(boss, kAnubisId, 0.85f);
WL.Combat.Boss.BossPatternTable.UpdatePhaseById(boss, kAnubisId, 0.70f);
WL.Combat.Boss.BossPatternTable.UpdatePhaseById(boss, kAnubisId, 0.55f);
WL.Combat.Boss.BossPatternTable.UpdatePhaseById(boss, kAnubisId, 0.40f);
WL.Combat.Boss.BossPatternTable.UpdatePhaseById(boss, kAnubisId, 0.90f); // 회복 — 되돌아가지 않아야 한다
WL.Combat.Boss.BossPatternTable.UpdatePhaseById(boss, kAnubisId, 0.05f);
L(s_eventLog);
L(" 이벤트 " + s_eventCount + "회 · Raised=" + WL.Combat.Boss.BossEvents.BossPhase.Raised +
" Dispatched=" + WL.Combat.Boss.BossEvents.BossPhase.Dispatched +
" 최종 페이즈=" + WL.Combat.Boss.BossPatternTable.CurrentPhase(boss));
L(" 판정 = " + (s_eventCount == 3 ? "PASS (100→70→40 · 3회 · 회복 시 되돌림 없음)" : "FAIL (기대 3회)"));
L("");
L("── ④ 스킬 해금 임계 (원본 0.9 / 0.7 · 행이 값을 주면 그 값)");
bool c1 = false, c2 = false;
float[] hps = { 1.00f, 0.95f, 0.90f, 0.80f, 0.70f, 0.50f };
for (int i = 0; i < hps.Length; i++)
{
WL.Combat.Boss.BossPatternTable.ApplySkillGatesById(boss, kAnubisId, hps[i], ref c1, ref c2);
L(string.Format(" HP {0,5:P0} → CanUseSkill1={1,-5} CanUseSkill2={2}", hps[i], c1, c2));
}
L("");
L("── ⑤ 디스패치 GC (Raise " + kGcIterations + "회 · 구독자는 카운터만 = 로그 문자열 할당 배제)");
WL.Combat.Boss.BossEvents.BossPhase.Remove(h);
WL.Combat.Core.CombatHandler<WL.Combat.Boss.BossPhaseEvent> hc = OnPhaseCount;
WL.Combat.Boss.BossEvents.BossPhase.Add(hc);
s_gcCount = 0;
System.GC.Collect();
long m0 = System.GC.GetTotalMemory(true);
for (int i = 0; i < kGcIterations; i++)
WL.Combat.Boss.BossEvents.RaiseBossPhase(boss, kAnubisId, 1, 2, 3, 0.7f, 0.7f, false);
long m1 = System.GC.GetTotalMemory(false);
WL.Combat.Boss.BossEvents.BossPhase.Remove(hc);
L(" 구독자 1명 · 수신 " + s_gcCount + "회 · 할당 델타 = " + (m1 - m0) + " byte (회당 " +
((m1 - m0) / (double)kGcIterations).ToString("0.##") + " byte)");
}
finally { WL.Combat.Boss.BossEvents.BossPhase.Remove(h); }
}
static int s_gcCount;
/// GC 측정용 — 할당이 전혀 없는 구독자(문자열 조립 없음).
static void OnPhaseCount(in WL.Combat.Boss.BossPhaseEvent e) { s_gcCount++; }
static void OnPhase(in WL.Combat.Boss.BossPhaseEvent e)
{
s_eventCount++;
s_eventLog += string.Format(" #{0} phase {1}→{2}/{3} id={4} hp={5:P0} 임계={6:0.##} spawn={7}\n",
s_eventCount, e.prevPhase, e.phase, e.phaseCount, e.monsterId, e.hpPercent, e.threshold, e.isSpawn);
}
// ───────────────────────────────────────── ⑥ C8 롤백
static void Section4_Rollback()
{
L("");
L("── ⑥ C8 롤백 (RuntimeDisabled = true → 로더·통지 무동작)");
var root = UnityEditor.PrefabUtility.LoadPrefabContents(kAnubisPrefab);
try
{
var boss = root.GetComponentInChildren<BossMobActor>(true);
WL.Combat.Core.WLCombatCoreSettings.RuntimeDisabled = true;
WL.Combat.Boss.BossEvents.ResetDiagnostics();
var before = Dump(boss);
int applied = WL.Combat.Boss.BossPatternTable.ApplyById(boss, kAnubisId);
var after = Dump(boss);
int changed = 0;
foreach (var kv in before) if (after[kv.Key] != kv.Value) changed++;
bool c1 = false, c2 = false;
WL.Combat.Boss.BossPatternTable.ApplySkillGatesById(boss, kAnubisId, 0.10f, ref c1, ref c2);
int ph = WL.Combat.Boss.BossPatternTable.UpdatePhaseById(boss, kAnubisId, 0.10f);
L(" 로더 Enabled=" + WL.Combat.Boss.BossPatternTable.Enabled + " · ApplyById=" + applied +
" · 변경 필드=" + changed + " · 게이트(HP 10%)=" + c1 + "/" + c2 +
" · UpdatePhase=" + ph + " · 이벤트=" + WL.Combat.Boss.BossEvents.TotalRaised);
L(" 판정 = " + (applied == -1 && changed == 0 && !c1 && !c2 && ph == 0 &&
WL.Combat.Boss.BossEvents.TotalRaised == 0
? "PASS (프리팹 값 · 원본 게이트 · 통지 0)" : "FAIL"));
}
finally
{
UnityEditor.PrefabUtility.UnloadPrefabContents(root);
WL.Combat.Core.WLCombatCoreSettings.RuntimeDisabled = false;
WL.Combat.Boss.BossPatternTable.ClearCache();
}
}
// ───────────────────────────────────────── 유틸
static System.Collections.Generic.Dictionary<string, string> Dump(BossMobActor boss)
{
var d = new System.Collections.Generic.Dictionary<string, string>();
var fields = boss.GetType().GetFields(System.Reflection.BindingFlags.Public |
System.Reflection.BindingFlags.Instance |
System.Reflection.BindingFlags.DeclaredOnly);
for (int i = 0; i < fields.Length; i++)
{
var t = fields[i].FieldType;
if (t == typeof(float) || t == typeof(int) || t == typeof(bool) || t == typeof(string))
d[fields[i].Name] = System.Convert.ToString(fields[i].GetValue(boss));
}
return d;
}
static void L(string line) { s.Append(line).Append("\n"); }
static string Flush()
{
var text = s.ToString();
try
{
var dir = System.IO.Path.GetDirectoryName(kLog);
if (!System.IO.Directory.Exists(dir)) System.IO.Directory.CreateDirectory(dir);
System.IO.File.WriteAllText(kLog, text, new System.Text.UTF8Encoding(false));
}
catch (System.Exception ex) { text += "\n[로그 저장 실패] " + ex.Message; }
return text;
}
}

View File

@ -16,6 +16,7 @@ public class BossMobActor : MobActor
base.On_Regen(reincarnation, healrate);
CanUseSkill1 = CanUseSkill2 = false;
CoolTime_PassiveSkill = 0;
WL.Combat.Boss.BossPatternTable.Apply(this, m_MonsterTableData); // #813h 패턴 데이터 적용 + 페이즈 1 통지 (행 없으면 무동작 = 프리팹 값 · C8)
}
protected override void Update()
@ -81,6 +82,8 @@ public class BossMobActor : MobActor
base.Get_Damage(_dinfo);
if (!CanUseSkill1) CanUseSkill1 = (Get_HP() / Get_MaxHP()) <= 0.9;
if (!CanUseSkill2) CanUseSkill2 = (Get_HP() / Get_MaxHP()) <= 0.7;
WL.Combat.Boss.BossPatternTable.ApplySkillGates(this, m_MonsterTableData, Get_HPPercent(), ref CanUseSkill1, ref CanUseSkill2); // #813h 임계 조회 (행 없으면 위 두 줄 결과 유지 · C8)
WL.Combat.Boss.BossPatternTable.UpdatePhase(this, m_MonsterTableData, Get_HPPercent()); // #813h 페이즈 임계 통과 시 BossPhase 통지
}
protected override void Set_Die()

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: becf3d4af4b59364eac35f55c5bf1919
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,104 @@
// ─────────────────────────────────────────────────────────────────────────────
// BossEvents.cs — WL 전투 액션 · 보스 페이즈 통지 허브(정적)
//
// PD 지시 #813 · 발주서 WL-813h §1-1 (2026-09-09)
//
// ■ 왜 별도 파일인가
// 코어 허브 `Assets/WL/Combat/Core/CombatEvents.cs` 는 811d~j 가 동시에 물고 있어 수정 금지다.
// 보스 페이즈(BossPhase)는 811 12종 이벤트에 없던 새 축이므로 같은 규약으로 이 파일에 따로 둔다.
// · 정적 클래스(새 Manager/Singleton 0) · 구조체 페이로드를 `in` 으로 전달(복사·박싱 0)
// · 구독자 = 사전 할당 배열(코어의 CombatEventList<T> 재사용 — Assembly-CSharp 단일 어셈블리)
// · 구독자 예외는 try/catch 로 격리(코어 CombatEventList.Dispatch 안) → 원본 전투 흐름을 끊지 않는다
// · WLCombatCoreSettings.enabled / RuntimeDisabled 를 그대로 준수(C8 롤백 1순위 스위치 공유)
//
// ■ 발생 지점 (원본 훅은 BossMobActor.cs 1줄뿐 — 실제 판정은 BossPatternTable 안)
// BossMobActor.On_Regen ─ BossPatternTable.Apply → 페이즈 1 진입 통지(스폰)
// BossMobActor.Get_Damage ─ BossPatternTable.UpdatePhase → HP% 가 임계를 내려갈 때만 통지
// 페이즈는 단조 증가(회복해도 되돌아가지 않는다) — 연출·UI 가 페이즈를 왕복하지 않게 한다.
//
// ■ 임계값의 출처 (C45)
// 코드 상수 0. 임계는 `Assets/WL/Combat/Settings/Resources/WL/WL813h_BossPattern.json`
// (보스별 phaseThresholds · 없으면 테이블 기본 [1.0, 0.7, 0.4]).
// JSON 이 없거나 코어가 꺼져 있으면 Raise 자체가 일어나지 않는다 = 원본 100%.
//
// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것(BossMobActor 가 Assembly-CSharp 에 있다).
// ─────────────────────────────────────────────────────────────────────────────
using UnityEngine;
using WL.Combat.Core;
namespace WL.Combat.Boss
{
/// <summary>보스 페이즈 전환 페이로드(발생 시점 스냅샷 · struct · GC 0).</summary>
public struct BossPhaseEvent
{
/// <summary>페이즈가 바뀐 보스(에디트 모드 프로브에서는 프리팹 인스턴스일 수 있다).</summary>
public BossMobActor boss;
/// <summary>MonsterList 의 n_MonsterID (Anubis = 10006).</summary>
public int monsterId;
/// <summary>이전 페이즈(스폰 통지는 0) · 새 페이즈(1부터).</summary>
public int prevPhase, phase;
/// <summary>이 보스의 총 페이즈 수(임계 배열 길이).</summary>
public int phaseCount;
/// <summary>전환 시점의 HP 비율(0~1) · 이 페이즈에 들어오게 한 임계값.</summary>
public float hpPercent, threshold;
/// <summary>스폰(페이즈 1 진입) 통지면 true — 피해로 인한 전환과 구분한다.</summary>
public bool isSpawn;
public float time;
public int frame;
}
/// <summary>보스 이벤트 허브. 정적 · GC 0 · 구독은 BossPhase.Add / Remove.</summary>
public static class BossEvents
{
/// <summary>페이즈 전환 구독자 목록(코어와 같은 구현 · 예외 격리 · Raised/Dispatched 카운터).</summary>
public static readonly CombatEventList<BossPhaseEvent> BossPhase = new CombatEventList<BossPhaseEvent>(4);
/// <summary>코어 활성 여부 = WLCombatCoreSettings.Enabled(에셋 · enabled · RuntimeDisabled).</summary>
public static bool Enabled { get { return WLCombatCoreSettings.Enabled; } }
// ── 진단(프로브가 읽는다)
public static int TotalRaised;
public static string LastEvent = "";
public static float LastEventTime;
public static int LastPhase, LastPrevPhase, LastMonsterId;
public static float LastHpPercent, LastThreshold;
/// <summary>
/// 페이즈 전환 통지. 호출측(BossPatternTable)이 이미 "바뀌었다"를 판정한 뒤에만 부른다.
/// 코어가 꺼져 있으면 즉시 반환(C8) — 구독자도 카운터도 움직이지 않는다.
/// </summary>
public static void RaiseBossPhase(BossMobActor boss, int monsterId, int prevPhase, int phase,
int phaseCount, float hpPercent, float threshold, bool isSpawn)
{
if (!Enabled) return;
var e = new BossPhaseEvent
{
boss = boss, monsterId = monsterId,
prevPhase = prevPhase, phase = phase, phaseCount = phaseCount,
hpPercent = hpPercent, threshold = threshold, isSpawn = isSpawn,
time = Time.unscaledTime, frame = Time.frameCount
};
TotalRaised++;
LastEvent = "BossPhase";
LastEventTime = e.time;
LastPrevPhase = prevPhase; LastPhase = phase; LastMonsterId = monsterId;
LastHpPercent = hpPercent; LastThreshold = threshold;
BossPhase.Dispatch(in e);
var c = WLCombatCoreSettings.Instance;
if (c != null && c.verboseLog)
Debug.Log("[BossEvents] Phase " + prevPhase + "→" + phase + " id=" + monsterId +
" hp=" + hpPercent.ToString("F3") + " th=" + threshold.ToString("F3"));
}
/// <summary>프로브용: 카운터 초기화(구독은 유지).</summary>
public static void ResetDiagnostics()
{
TotalRaised = 0; LastEvent = ""; LastEventTime = 0f;
LastPhase = LastPrevPhase = LastMonsterId = 0;
LastHpPercent = LastThreshold = 0f;
BossPhase.Raised = BossPhase.Dispatched = 0;
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1cd4589f49587da428cae4c43d82488b

View File

@ -0,0 +1,386 @@
// ─────────────────────────────────────────────────────────────────────────────
// BossPatternTable.cs — 보스 패턴(쿨 · 예고(차징) · 피해 배율 · 속도 · 페이즈 임계) 데이터 로더
//
// PD 지시 #813 · 발주서 WL-813h §1-2 (2026-09-09) · 실측 근거 = WL-813b 완료보고 §1
//
// ■ 무엇을 하나 (C45 — 코드 상수 0)
// 힘민지 보스 12종의 튜닝 값은 테이블이 아니라 **프리팹 인스펙터에 직렬화된 public 필드**에 들어 있다
// (813b 실측: 코드 기본값과 4곳 불일치 — Minotos Skill2CoolTime 10, Anubis Skill2Dmg 3, QueenUndead
// Skill2BackDuration 2, FlowerDryad Skill1Heal 0). 12개 프리팹을 손으로 고치는 대신,
// JSON 한 장을 SOT 로 두고 **스폰 시점(On_Regen)에 그 보스의 public 필드에 덮어쓴다**.
// · `Boss_*.cs` 는 한 줄도 고치지 않는다(로더가 리플렉션으로 대입).
// · 행이 있는 보스만 덮어쓴다. 행이 없으면 프리팹 인스펙터 값 그대로 = 변경 0 (C8).
// · 덮어쓰는 필드는 그 Boss_* 클래스가 **직접 선언한**(DeclaredOnly) public float/int/bool 로 제한한다
// → MobActor/Actor 의 공용 필드는 어떤 JSON 키로도 건드릴 수 없다(사고 방지).
//
// ■ 페이즈
// 페이즈 임계도 같은 JSON(`phaseThresholds` · 기본 [1.0, 0.7, 0.4] = 100/70/40 %).
// HP% 가 임계를 내려갈 때 BossEvents.RaiseBossPhase 로 통지한다(단조 증가 · 회복해도 되돌아가지 않는다).
// `BossMobActor.cs:82-83` 의 스킬 해금 임계(원본 0.9 / 0.7)는 `skill1Threshold`/`skill2Threshold` 로
// 보스별 조정이 가능하고, 키가 없으면 테이블 기본값(0.9 / 0.7) = 원본과 동일하다.
//
// ■ 롤백 (C8 · 3중)
// ① `WLCombatCoreSettings` 에셋 없음 / enabled=false / RuntimeDisabled → 로더·통지 전부 무동작
// ② JSON 없음 / 파싱 실패 → 무동작(에러 1줄만 로그) ③ JSON 의 `"enabled": false` → 무동작
// 무동작 = 스킬 해금 임계 0.9/0.7(원본 두 줄이 이미 계산해 둔 값을 그대로 통과) + 프리팹 인스펙터 값.
//
// ■ 성능
// 필드 대입은 스폰 1회(보스 1기 · 필드 ~10개)뿐이고 FieldInfo 는 타입별로 캐시한다.
// 페이즈 판정·통지 경로(Get_Damage)는 배열/구조체만 쓴다 = 할당 0.
//
// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것.
// 🔴 이 JSON 은 butler.xlsm 밖의 **WL 전용 파일**이다(G 폴더 · 추후 테이블 편입 후보).
// ─────────────────────────────────────────────────────────────────────────────
using System;
using System.Collections.Generic;
using System.Reflection;
using Newtonsoft.Json;
using UnityEngine;
using WL.Combat.Core;
namespace WL.Combat.Boss
{
/// <summary>보스 1종의 패턴 행. 값이 없는 키는 건드리지 않는다(프리팹 값 유지).</summary>
[Serializable]
public class BossPatternRow
{
/// <summary>MonsterList.n_MonsterID (10001~10012).</summary>
public int id;
/// <summary>읽는 사람용 이름 · 대상 스크립트 · 메모(로직에 쓰지 않는다).</summary>
public string name, script, note;
/// <summary>페이즈 임계(내림차순 · 예 [1.0, 0.7, 0.4]). null 이면 테이블 기본값.</summary>
public float[] phaseThresholds;
/// <summary>스킬1/스킬2 해금 HP 비율. null 이면 테이블 기본값(원본 0.9 / 0.7).</summary>
public float? skill1Threshold, skill2Threshold;
/// <summary>Boss_*.cs 가 선언한 public 필드 이름 → 값(float · int 는 반올림 · bool 은 0 아니면 true).</summary>
public Dictionary<string, float> fields;
}
/// <summary>JSON 파일 전체.</summary>
[Serializable]
public class BossPatternFile
{
public string _comment, _source, _sot;
/// <summary>이 파일 자체의 스위치(false 면 로더 무동작 = 프리팹 값 · C8).</summary>
public bool enabled = true;
/// <summary>행에 phaseThresholds 가 없을 때 쓰는 기본 임계.</summary>
public float[] defaultPhaseThresholds;
/// <summary>행에 임계가 없을 때 쓰는 스킬 해금 기본값 = 원본 BossMobActor.cs:82-83 값.</summary>
public float defaultSkill1Threshold = 0.9f;
public float defaultSkill2Threshold = 0.7f;
public List<BossPatternRow> rows;
}
/// <summary>보스 패턴 JSON 로더 + 페이즈 판정. 정적 · 새 Manager 0.</summary>
public static class BossPatternTable
{
/// <summary>Resources 경로(확장자 없음) — Assets/WL/Combat/Settings/Resources/WL/WL813h_BossPattern.json</summary>
public const string ResourcesPath = "WL/WL813h_BossPattern";
/// <summary>테이블 기본 임계(JSON 에 defaultPhaseThresholds 가 없을 때) = 100 / 70 / 40 %.</summary>
static readonly float[] kFallbackPhases = { 1f, 0.7f, 0.4f };
/// <summary>임계 비교 허용 오차(부동소수 누적 오차용 · 게임 데이터 아님).</summary>
const float kEpsilon = 1e-4f;
static BossPatternFile s_file;
static bool s_lookupDone;
// ── 진단(프로브가 읽는다)
public static bool ParseFailed;
public static string LoadError = "";
public static int LastAppliedFields = -1, LastAppliedId, LastUnknownKeys;
/// <summary>JSON(1회 로드 후 캐시). 없거나 파싱 실패면 null.</summary>
public static BossPatternFile File
{
get
{
if (!s_lookupDone)
{
s_lookupDone = true;
var ta = Resources.Load<TextAsset>(ResourcesPath);
if (ta == null) { LoadError = "TextAsset not found: " + ResourcesPath; return null; }
try { s_file = JsonConvert.DeserializeObject<BossPatternFile>(ta.text); }
catch (Exception ex)
{
ParseFailed = true; LoadError = ex.Message; s_file = null;
Debug.LogError("[BossPatternTable] JSON 파싱 실패 → 원본 동작 유지(C8): " + ex.Message);
}
}
return s_file;
}
}
/// <summary>로더가 살아 있는가 = 코어 ON · JSON 있음 · JSON enabled.</summary>
public static bool Enabled
{
get { var f = WLCombatCoreSettings.Enabled ? File : null; return f != null && f.enabled && f.rows != null; }
}
/// <summary>행 수(로드 실패 시 0).</summary>
public static int RowCount { get { var f = File; return f != null && f.rows != null ? f.rows.Count : 0; } }
/// <summary>테스트용: 캐시를 비워 다음 접근 때 다시 Resources.Load 한다.</summary>
public static void ClearCache()
{
s_file = null; s_lookupDone = false; ParseFailed = false; LoadError = "";
s_fieldCache.Clear(); s_originals.Clear();
for (int i = 0; i < s_states.Length; i++) s_states[i] = default(PhaseState);
}
/// <summary>보스 ID 의 행(없으면 null). 12행 선형 탐색 — 할당 0.</summary>
public static BossPatternRow Get(int monsterId)
{
var f = WLCombatCoreSettings.Enabled ? File : null;
if (f == null || !f.enabled || f.rows == null) return null;
for (int i = 0; i < f.rows.Count; i++)
if (f.rows[i] != null && f.rows[i].id == monsterId) return f.rows[i];
return null;
}
/// <summary>이 보스의 페이즈 임계(행 없음/키 없음 → 테이블 기본 → [1, 0.7, 0.4]).</summary>
public static float[] GetPhaseThresholds(int monsterId)
{
var row = Get(monsterId);
if (row != null && row.phaseThresholds != null && row.phaseThresholds.Length > 0) return row.phaseThresholds;
var f = File;
if (f != null && f.defaultPhaseThresholds != null && f.defaultPhaseThresholds.Length > 0) return f.defaultPhaseThresholds;
return kFallbackPhases;
}
/// <summary>스킬1/스킬2 해금 임계(행 없음/키 없음 → 원본 0.9 / 0.7).</summary>
public static void GetSkillThresholds(int monsterId, out float t1, out float t2)
{
var f = File;
t1 = f != null ? f.defaultSkill1Threshold : 0.9f;
t2 = f != null ? f.defaultSkill2Threshold : 0.7f;
var row = Get(monsterId);
if (row == null) return;
if (row.skill1Threshold.HasValue) t1 = row.skill1Threshold.Value;
if (row.skill2Threshold.HasValue) t2 = row.skill2Threshold.Value;
}
// ───────────────────────────────────────── 원본 훅이 부르는 3개 (BossMobActor.cs · 각 1줄)
/// <summary>
/// [훅 1/3] BossMobActor.On_Regen — 스폰 시 JSON 행을 이 보스 인스턴스의 public 필드에 적용하고
/// 페이즈 상태를 초기화한 뒤 페이즈 1 진입을 통지한다.
/// 반환 = 적용된 필드 수(코어 off · JSON 없음 · 행 없음이면 -1 = 무동작).
/// </summary>
public static int Apply(BossMobActor boss, MonsterTableData mobtable)
{
return ApplyById(boss, mobtable != null ? mobtable.n_MonsterID : 0);
}
/// <summary>
/// [훅 2/3] BossMobActor.Get_Damage — 스킬 해금 임계를 데이터 값으로 확정한다.
/// 행이 없으면 원본 두 줄이 계산해 둔 값을 그대로 둔다(C8). 행이 있으면 데이터가 최종 권한.
/// </summary>
public static void ApplySkillGates(BossMobActor boss, MonsterTableData mobtable, float hpPercent,
ref bool canUseSkill1, ref bool canUseSkill2)
{
ApplySkillGatesById(boss, mobtable != null ? mobtable.n_MonsterID : 0, hpPercent, ref canUseSkill1, ref canUseSkill2);
}
/// <summary>[훅 3/3] BossMobActor.Get_Damage — HP% 가 페이즈 임계를 내려갔으면 BossPhase 를 통지한다.</summary>
public static void UpdatePhase(BossMobActor boss, MonsterTableData mobtable, float hpPercent)
{
UpdatePhaseById(boss, mobtable != null ? mobtable.n_MonsterID : 0, hpPercent);
}
// ───────────────────────────────────────── ID 직접 지정판 (프로브 · 에디트 모드)
/// <summary>행 값을 보스 인스턴스에 적용. 반환 = 적용된 필드 수(-1 = 무동작).</summary>
public static int ApplyById(BossMobActor boss, int monsterId)
{
LastAppliedId = monsterId; LastAppliedFields = -1; LastUnknownKeys = 0;
if (boss == null) return -1;
var row = Get(monsterId);
if (row == null) return -1; // C8 — 행이 없으면 상태 칸도 쓰지 않는다
ResetState(boss, monsterId); // 풀 재사용 대비 래치 초기화
int applied = 0;
if (row.fields != null && row.fields.Count > 0)
{
var map = FieldsOf(boss.GetType());
CacheOriginals(boss, row, map);
foreach (var kv in row.fields)
{
FieldInfo fi;
if (!map.TryGetValue(kv.Key, out fi)) { LastUnknownKeys++; continue; }
if (SetField(fi, boss, kv.Value)) applied++; else LastUnknownKeys++;
}
}
LastAppliedFields = applied;
// 스폰 = 페이즈 1 진입 통지(HP 는 On_Regen 직후라 만피).
var th = GetPhaseThresholds(monsterId);
int idx = StateIndex(boss, monsterId, true);
s_states[idx].phase = 1;
BossEvents.RaiseBossPhase(boss, monsterId, 0, 1, th.Length, 1f, th.Length > 0 ? th[0] : 1f, true);
return applied;
}
/// <summary>스킬 해금 임계 적용(ID 직접 지정).</summary>
public static void ApplySkillGatesById(BossMobActor boss, int monsterId, float hpPercent,
ref bool canUseSkill1, ref bool canUseSkill2)
{
if (boss == null || float.IsNaN(hpPercent) || float.IsInfinity(hpPercent)) return;
var row = Get(monsterId);
if (row == null) return; // C8 — 원본 0.9 / 0.7 결과를 그대로 둔다
float t1, t2; GetSkillThresholds(monsterId, out t1, out t2);
int idx = StateIndex(boss, monsterId, true);
if (hpPercent <= t1 + kEpsilon) s_states[idx].skill1 = true; // 한 번 열리면 유지(원본과 같은 래치)
if (hpPercent <= t2 + kEpsilon) s_states[idx].skill2 = true;
canUseSkill1 = s_states[idx].skill1;
canUseSkill2 = s_states[idx].skill2;
}
/// <summary>페이즈 판정 + 통지(ID 직접 지정). 반환 = 새 페이즈(변화 없으면 현재 페이즈).</summary>
public static int UpdatePhaseById(BossMobActor boss, int monsterId, float hpPercent)
{
if (boss == null || float.IsNaN(hpPercent) || float.IsInfinity(hpPercent)) return 0;
var row = Get(monsterId);
if (row == null) return 0; // C8 — 행이 없으면 통지도 없다
var th = GetPhaseThresholds(monsterId);
int idx = StateIndex(boss, monsterId, true);
int cur = s_states[idx].phase;
int next = PhaseOf(th, hpPercent);
if (next <= cur) return cur; // 단조 증가 — 회복해도 되돌리지 않는다
s_states[idx].phase = next;
BossEvents.RaiseBossPhase(boss, monsterId, cur, next, th.Length, hpPercent, th[next - 1], false);
return next;
}
/// <summary>HP 비율에 해당하는 페이즈(1부터). 임계는 내림차순 배열.</summary>
public static int PhaseOf(float[] thresholds, float hpPercent)
{
if (thresholds == null || thresholds.Length == 0) return 1;
int p = 1;
for (int i = 0; i < thresholds.Length; i++)
if (hpPercent <= thresholds[i] + kEpsilon) p = i + 1;
return p;
}
/// <summary>현재 페이즈(상태가 없으면 0).</summary>
public static int CurrentPhase(BossMobActor boss)
{
int idx = StateIndex(boss, 0, false);
return idx >= 0 ? s_states[idx].phase : 0;
}
/// <summary>
/// 프로브/롤백용 — Apply 가 덮어쓰기 전에 캐시해 둔 원본(프리팹) 값으로 되돌린다.
/// 반환 = 되돌린 필드 수(캐시 없으면 -1).
/// </summary>
public static int Revert(BossMobActor boss, int monsterId)
{
if (boss == null) return -1;
float[] orig;
if (!s_originals.TryGetValue(monsterId, out orig)) return -1;
var row = Get(monsterId);
if (row == null || row.fields == null) return -1;
var map = FieldsOf(boss.GetType());
int i = 0, n = 0;
foreach (var kv in row.fields)
{
FieldInfo fi;
if (map.TryGetValue(kv.Key, out fi) && i < orig.Length && !float.IsNaN(orig[i]))
if (SetField(fi, boss, orig[i])) n++;
i++;
}
return n;
}
/// <summary>페이즈/해금 래치 초기화(스폰·풀 재사용 시).</summary>
public static void ResetState(BossMobActor boss, int monsterId)
{
int idx = StateIndex(boss, monsterId, true);
s_states[idx].phase = 0; s_states[idx].skill1 = false; s_states[idx].skill2 = false;
}
// ───────────────────────────────────────── 인스턴스별 상태(사전 할당 배열 · 할당 0)
struct PhaseState { public int instanceId, monsterId, phase; public bool skill1, skill2; }
/// <summary>
/// 동시 보스 수 상한. 813b 실측 = Map_C01/C02 에 FieldBossData 9개씩(WL_Nature 는 0) → 여유 16칸.
/// 넘치면 가장 오래된 칸을 재사용한다(페이즈 상태만 잃고 전투는 그대로).
/// </summary>
static readonly PhaseState[] s_states = new PhaseState[16];
static int s_cursor;
static int StateIndex(BossMobActor boss, int monsterId, bool create)
{
if (boss == null) return -1;
int id = boss.GetInstanceID();
for (int i = 0; i < s_states.Length; i++)
if (s_states[i].instanceId == id) return i;
if (!create) return -1;
for (int i = 0; i < s_states.Length; i++)
if (s_states[i].instanceId == 0)
{ s_states[i] = new PhaseState { instanceId = id, monsterId = monsterId }; return i; }
int c = s_cursor; s_cursor = (s_cursor + 1) % s_states.Length;
s_states[c] = new PhaseState { instanceId = id, monsterId = monsterId };
return c;
}
// ───────────────────────────────────────── 리플렉션 캐시
static readonly Dictionary<Type, Dictionary<string, FieldInfo>> s_fieldCache =
new Dictionary<Type, Dictionary<string, FieldInfo>>();
static readonly Dictionary<int, float[]> s_originals = new Dictionary<int, float[]>();
/// <summary>그 Boss_* 클래스가 **직접 선언한** public float/int/bool 필드만(상속 필드는 제외 = 사고 방지).</summary>
static Dictionary<string, FieldInfo> FieldsOf(Type t)
{
Dictionary<string, FieldInfo> map;
if (s_fieldCache.TryGetValue(t, out map)) return map;
map = new Dictionary<string, FieldInfo>();
var fields = t.GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
for (int i = 0; i < fields.Length; i++)
{
var ft = fields[i].FieldType;
if (ft == typeof(float) || ft == typeof(int) || ft == typeof(bool))
map[fields[i].Name] = fields[i];
}
s_fieldCache[t] = map;
return map;
}
static void CacheOriginals(BossMobActor boss, BossPatternRow row, Dictionary<string, FieldInfo> map)
{
if (s_originals.ContainsKey(row.id)) return;
var vals = new float[row.fields.Count];
int i = 0;
foreach (var kv in row.fields)
{
FieldInfo fi;
vals[i++] = map.TryGetValue(kv.Key, out fi) ? GetField(fi, boss) : float.NaN;
}
s_originals[row.id] = vals;
}
static bool SetField(FieldInfo fi, object target, float v)
{
var t = fi.FieldType;
if (t == typeof(float)) { fi.SetValue(target, v); return true; }
if (t == typeof(int)) { fi.SetValue(target, Mathf.RoundToInt(v)); return true; }
if (t == typeof(bool)) { fi.SetValue(target, Mathf.Abs(v) > kEpsilon); return true; }
return false;
}
static float GetField(FieldInfo fi, object target)
{
var t = fi.FieldType;
if (t == typeof(float)) return (float)fi.GetValue(target);
if (t == typeof(int)) return (int)fi.GetValue(target);
if (t == typeof(bool)) return ((bool)fi.GetValue(target)) ? 1f : 0f;
return float.NaN;
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5ead0415803444748bcfed484db1c33a

View File

@ -0,0 +1,128 @@
{
"_comment": "WL-813h 보스 패턴 데이터 (쿨 · 예고(차징) · 피해 배율 · 속도 · 페이즈 임계). 로더 = Assets/WL/Combat/Boss/BossPatternTable.cs. 값은 보스 스폰(On_Regen) 시 그 Boss_*.cs 가 선언한 public 필드에 대입된다. 행이 없는 보스는 프리팹 인스펙터 값 그대로(변경 0 · C8).",
"_source": "WL-813b 실측(프리팹 인스펙터 YAML 값). 프리팹에 직렬화되지 않은 필드는 Boss_*.cs 의 코드 기본값을 그대로 적었다(= 런타임 실효값). 문자열 필드(Skill1Indicator 등)는 로더가 숫자만 다루므로 제외.",
"_sot": "WL 전용 파일 — butler.xlsm(Assets/ResWork/Table/Export) 밖이다. 추후 테이블 편입 후보.",
"_rollback": "이 파일을 지우거나 enabled 를 false 로 두면 로더가 무동작 → 프리팹 값 + 원본 임계 0.9 / 0.7 로 100% 복귀.",
"enabled": true,
"_defaultPhaseThresholds_comment": "페이즈 임계(HP 비율 · 내림차순). 100 / 70 / 40 % = 3페이즈. 보스별로 phaseThresholds 를 적으면 그 값이 우선.",
"defaultPhaseThresholds": [1.0, 0.7, 0.4],
"_defaultSkillThreshold_comment": "스킬 해금 임계 = 원본 BossMobActor.cs:82-83 의 0.9 / 0.7. 보스별 skill1Threshold / skill2Threshold 로 덮어쓸 수 있다.",
"defaultSkill1Threshold": 0.9,
"defaultSkill2Threshold": 0.7,
"rows": [
{
"id": 10001, "name": "Lusif", "script": "Boss_LusifActor",
"note": "813b 실측값 그대로(변경 0).",
"fields": {
"Skill1CoolTime": 8, "Skill1Dmg": 3.5, "Perforation_Line": 0,
"Skill2CoolTime": 1.5, "Skill2Dist": 10, "Skill2ProjectileCount": 5,
"Skill2Delay": 0.8, "Skill2Dmg": 1.5
}
},
{
"id": 10002, "name": "Lusif_B", "script": "Boss_LusifB_Actor",
"note": "813b 실측값 그대로(변경 0).",
"fields": {
"Skill1CoolTime": 12, "Skill1Dmg": 3.5, "Skill1ChargingTime": 4, "Perforation_Line": 0,
"Skill2CoolTime": 20, "Skill2Dmg": 5, "Skill2ChargingTime": 4
}
},
{
"id": 10003, "name": "Mega_Ice_Golem", "script": "Boss_IceGolem",
"note": "813b 실측값 그대로(변경 0). 인디케이터 이름 2종은 문자열이라 로더 대상 밖 = 프리팹 값 유지.",
"fields": {
"Skill1CoolTime": 8, "Skill1TargetRange": 2, "Skill1Speed": 5, "Skill1Dmg": 3.5,
"Skill1StunTime": 2, "Perforation_Line": 0,
"Skill2CoolTime": 10, "Skill2Speed": 7, "Skill2Dmg": 3.5,
"Skill2KnockBackTime": 0.3, "Skill2StunTime": 1
}
},
{
"id": 10004, "name": "Minotos", "script": "Boss_Minotos",
"note": "813b 실측값 그대로(변경 0). Skill2CoolTime 10 은 프리팹 값 — 코드 기본값 25 와 다르다(813b 불일치 1/4).",
"fields": {
"Skill1CoolTime": 15, "Skill1Dmg": 1.2, "Perforation_Line": 0,
"Skill2CoolTime": 10, "Skill2LifeTime": 15, "Skill2MoveSpeed": 0.5, "Skill2AttackSpeed": 0.5
}
},
{
"id": 10005, "name": "Golem_Child", "script": "Boss_GolemChild",
"note": "813b 실측값 그대로(변경 0). DmgAfterSkill2 는 프리팹에 직렬화돼 있지 않아 코드 기본값 2 = 런타임 실효값.",
"fields": {
"Skill1CoolTime": 8, "Skill1Dmg": 3.8, "Skill1AreaDmg": 1.2, "Perforation_Line": 0,
"Skill2CoolTime": 8, "Skill2ChargingTime": 1, "Skill2Dmg": 3, "DmgAfterSkill2": 2
}
},
{
"id": 10006, "name": "Anubis", "script": "Boss_Anubis",
"note": "첫 보스 후보(813b). 예고(차징) 3 s → 1.2 s 는 발주서 WL-813h §1-2 의 제안값이며 이 파일에서만 바뀐다(프리팹 무변경). 나머지는 실측값. Skill2Dmg 3 은 프리팹 값 — 코드 기본값 2.8 과 다르다(813b 불일치 2/4).",
"fields": {
"Skill1CoolTime": 18, "Skill1Dmg": 2.2, "Skill1ChargingTime": 1.2, "Perforation_Line": 0,
"Skill2CoolTime": 15, "Skill2ChargingTime": 1.2, "Skill2AttackDelay": 1,
"Skill2Dmg": 3, "Skill2Speed": 15
}
},
{
"id": 10007, "name": "Queen_Undead", "script": "Boss_QueenUndead",
"note": "813b 실측값 그대로(변경 0). Skill2BackDuration 2 는 프리팹 값 — 코드 기본값 1.5 와 다르다(813b 불일치 3/4).",
"fields": {
"Skill1CoolTime": 6, "Skill1LifeTime": 5, "Skill1Manaburn": 0.02, "Skill1Distance": 15,
"Perforation_Line": 0,
"Skill2CoolTime": 18, "Skill2ChargingTime": 2, "Skill2Dmg": 3.2,
"Skill2BackDist": 7.5, "Skill2BackDuration": 2
}
},
{
"id": 10008, "name": "Flower Dryad", "script": "Boss_FlowerDryad",
"note": "813b 실측값 그대로(변경 0). Skill1Heal 0 은 프리팹 값 — 즉시회복이 꺼져 있다(코드 기본값 0.1 · 813b 불일치 4/4 · 켤지는 PD 결정). Skill1ChargingTime · Skill2AirborneTime 은 프리팹 미직렬화라 코드 기본값이 런타임 실효값.",
"fields": {
"Skill1CoolTime": 20, "Skill1ChargingTime": 3, "Skill1Heal": 0, "Skill1FindDist": 3,
"Skill1LifeTime": 6, "Skill1DelayTime": 1, "Skill1PerHeal": 0.01, "Perforation_Line": 0,
"Skill2CoolTime": 18, "Skill2ChargingTime": 3, "Skill2Dmg": 5.2,
"Skill2PoisonTime": 5, "Skill2PoisonGapTime": 1, "Skill2PoisonDmg": 0.3, "Skill2AirborneTime": 1.5
}
},
{
"id": 10009, "name": "Angez_B", "script": "Boss_Angez_B",
"note": "813b 실측값 그대로(변경 0). 챕터 보스(Map_C01/02 의 isChapterBoss=1).",
"fields": {
"Skill1CoolTime": 20, "Skill1ChargingTime": 3, "Skill1Dmg": 2.8, "Skill1DelayTime": 0.5,
"Skill1Count": 10, "Skill1StunTime": 0.3, "Perforation_Line": 0,
"Skill2CoolTime": 15, "Skill2ChargingTime": 3, "Skill2Dmg": 1.2, "Skill2StunTime": 0.2
}
},
{
"id": 10010, "name": "Angez_C", "script": "Boss_Angez_C",
"note": "813b 실측값 그대로(변경 0). 어느 맵에도 배치돼 있지 않다(813b §1).",
"fields": {
"Skill1CoolTime": 12, "Skill1ChargingTime": 3, "Skill1Dmg": 3.2, "Skill1DelayTime": 0.5,
"Skill1Count": 8, "Perforation_Line": 0,
"Skill2CoolTime": 18, "Skill2ChargingTime": 3, "Skill2Dmg": 1.5
}
},
{
"id": 10011, "name": "Brother_Penguin_C", "script": "Boss_BrotherPenguin",
"note": "813b 실측값 그대로(변경 0). Skill1KnockBackTime · Skill1KnockBackSpeed · Skill2Dmg · Skill2ChargingTime 은 프리팹 미직렬화라 코드 기본값이 런타임 실효값. 어느 맵에도 배치돼 있지 않다.",
"fields": {
"Skill1CoolTime": 12, "Skill1ChargingTime": 3, "Skill1Dmg": 3.2, "Skill1FrostTime": 3,
"Skill1AtkSpd": 0.7, "Skill1MovSpd": 0.7, "Skill1KnockBackTime": 0.5, "Skill1KnockBackSpeed": 4,
"Perforation_Line": 0,
"Skill2CoolTime": 20, "Skill2Dmg": 5, "Skill2LifeTime": 12, "Skill2BarrierRate": 0.25,
"Skill2ChargingTime": 3
}
},
{
"id": 10012, "name": "Shaga_B", "script": "Boss_Shaga",
"note": "813b 실측값 그대로(변경 0). Skill2SummonMobID 176 은 소환몹 ID(MonsterList) — 값을 바꾸면 소환 대상이 바뀐다. 어느 맵에도 배치돼 있지 않다.",
"fields": {
"Skill1CoolTime": 18, "Skill1Dmg": 1.6, "Skill1FrostTime": 6,
"Skill1AtkSpd": 0.5, "Skill1MovSpd": 0.5, "Perforation_Line": 0,
"Skill2CoolTime": 20, "Skill2ChargingTime": 3, "Skill2SummonLifeTime": 12,
"Skill2SummonMobID": 176, "Skill2SummonCount": 2, "Skill2StatRate": 0.5
}
}
]
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 1f4771cdbcdfb5d4a967adf208bdee6b
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: