[WL-813l2] 전투력 토스트 수리: LevelUp 페이로드 기반 (#813)
This commit is contained in:
parent
f08bce97dc
commit
f1b550466e
|
|
@ -0,0 +1,67 @@
|
||||||
|
# WL813l2 Probe 2026-09-09 14:41:18 (edit mode · Play 0 · 로그인 0)
|
||||||
|
playMode=False
|
||||||
|
|
||||||
|
── 준비
|
||||||
|
[OK] 구독 ON · MyValue.bMyPC=False(월드 PC 없음 = 로비·에디트 경로)
|
||||||
|
|
||||||
|
── ⓪ 설정 에셋 · 성장 입력 값 전수 (C45 · 코드 상수 0)
|
||||||
|
[OK] WLPowerDeltaSettings 로드
|
||||||
|
[OK] Enabled · enabled_=True
|
||||||
|
성장 입력: useLevelUpPayload=True readGrowthFromServerData=True statPointWeight=1 atkGain=1 hpGain=22 perLevel=5 levelWeight=0 levelBase=1
|
||||||
|
기존 산식: atk=BaseDmg(폴백 ATK) hp=MaxHP hpWeight=0.1 powerScale=1 최소 표시 0.5% suppressToastOnLevelUp=True
|
||||||
|
[OK] statPointsPerLevel=5 (원본 실측 ServerClass.cs:2770 Get_Point() = (Lv−1)*5 − 사용분)
|
||||||
|
[OK] 포인트 환산 ATK +1 / MaxHP +22 (원본 실측 MyValue.cs:335-336 dic_MainStatMultiplier[STR] = ATK 1 · MaxHP 22)
|
||||||
|
|
||||||
|
── ① 🔴 D-28 원인 실증 — 원본 스탯은 레벨업으로 안 움직인다 · 움직이는 것은 미사용 포인트뿐
|
||||||
|
Lv 1 → 16 · 스탯 포인트 사용 0 · 장비 변화 0 일 때 원본이 주는 것:
|
||||||
|
[OK] Get_Point() 0 → 75 (= (Lv−1)*5 · 미사용 포인트만 늘어난다)
|
||||||
|
[OK] SD_PC.Get_Stat(STR) 0 → 0 (= **사용한** 포인트만 셈 · table_classconfig.Get_PureStat:246 이 읽는 값) → ActorStatInfo 는 그대로
|
||||||
|
⇒ `ActorStatInfo.Reset`(MyValue.cs:286-453)은 SD_PC.Lv 를 한 번도 안 읽는다 = 813l 이 폴링해도 값이 상수
|
||||||
|
(Q6 D-28 `PollCount 1573` 인데 `LastPower 52.000000` 불변 · 레벨 1→16)
|
||||||
|
[OK] ApplyGrowth ↔ 원본 Get_Point() 대조 — 내 값 70 · 원본 70 · level=16 (사용 5점 반영)
|
||||||
|
[OK] 813l 산식 회귀 4건(성장 입력 0) — 200.00 / 210.00 / 230.00 / 486.00 (기대 200 / 210 / 230 / 486)
|
||||||
|
[OK] 포인트 5점 → 216.00 (기대 216.00 = 105 + 111.0)
|
||||||
|
[OK] 포인트 10점 → 232.00 (기대 232.00 · 레벨당 +16.0)
|
||||||
|
[OK] 레벨업 1회 증가율 8.00% (최소 표시 0.5% 를 넘는다)
|
||||||
|
[OK] 포인트 사용 전/후 연속 — 미사용 216.00 ↔ 사용 후 216.00 (statPointWeight=1 의 뜻)
|
||||||
|
|
||||||
|
── ② 813k GrowthEvents.LevelUp 실 Dispatch — prev ATK 100 → new 112 (발주서 §1-3)
|
||||||
|
dispatch GrowthEvents.LevelUp 6→7 pt=30 · 구독자=1
|
||||||
|
[OK] 수신 1회 · 소스 "levelUp(813k 페이로드)"
|
||||||
|
[OK] 페이로드 전투력 prev=280.00 new=308.00 (기대 280.00 / 308.00 · 손계산)
|
||||||
|
[OK] 🔴 LastPower 0 → 308.00 (D-28 = 여기가 52 로 고정이었다)
|
||||||
|
[OK] 레벨업 프레임 토스트 생략(Suppressed=1 Toast=0) — 같은 값을 팝업 줄이 보여준다
|
||||||
|
[OK] 팝업 줄 "전투력 280 → 308 (+10.0%)" (기대 280 → 308 · +10.0%)
|
||||||
|
[OK] 토스트 1회 · 문구 "전투력 +10.0%" (기대 "전투력 +10.0%")
|
||||||
|
[OK] useLevelUpPayload=0 → 소스 "levelUp(스탯 읽기 실패)" · 토스트 0회 · LastPower 없음(= 813l 원래 경로)
|
||||||
|
|
||||||
|
── ③ 🔴 D-28 재현 · 수리 — 스탯이 **하나도 안 바뀌는** 원본 기본 동작(813k 보고 ①)에서
|
||||||
|
dispatch GrowthEvents.LevelUp 7→8 pt=35 · 구독자=1 · 스탯 3종 prev == new (원본 기본값)
|
||||||
|
[OK] 전투력 296.00 → 312.00 (기대 296.00 → 312.00)
|
||||||
|
[OK] ✅ 수리 후 — 토스트 1회 "전투력 +5.4%"
|
||||||
|
[OK] ❌ 재현(statPointWeight=0) — 레벨 7→8→9 인데 전투력 200.00 불변 · 토스트 0회 = Q6 D-28 그대로
|
||||||
|
[OK] 다단(5→8 · 3레벨) prev 포인트 역산 → prev 전투력 264.00 (기대 264.00 = 포인트 20점)
|
||||||
|
|
||||||
|
── ④ 레벨업 팝업 전투력 줄 — 813y StatPopup 왕복(구독 → Dispatch → 줄)
|
||||||
|
init: 구독 ON · box pos=(0.00, 180.00) size=(1080.00, 296.00) rows=3 power=on unitsPerPx=1.0000 titleFont=72.0 edge[LiberationSans SDF Material (WL813y Edge) shader=TextMeshPro/Mobile/Distance Field outlineW=0.260 outlineC=0D0805FF underlay=000000BF off(0.45,-0.45) soft=0.20 UNDERLAY_ON=True]
|
||||||
|
[OK] StatPopup.PowerLineOn
|
||||||
|
[OK] StatPopup 구독=True · GrowthEvents.LevelUp 구독자=2(StatPopup + PowerDeltaToast)
|
||||||
|
팝업 표시: rows=1 level=10 text="LEVEL 10 / +5 포인트 <size=70%>(보유 45)</size> / 전투력 328 → 344 (+4.9%)"
|
||||||
|
[OK] 🔴 팝업 전투력 줄 "전투력 328 → 344 (+4.9%)" (기대 328 → 344 · +4.9%)
|
||||||
|
[OK] 중복 표시 0 — Suppressed=1 Toast=0
|
||||||
|
[OK] 프레임 스탬프 prev=328.00 new=344.00 → 팝업/토스트가 같은 값을 본다(구독 순서 무관)
|
||||||
|
|
||||||
|
── ⑤ GC 0 (폴링 · 페이로드 · 포인트 산식)
|
||||||
|
[OK] Tick × 1000 → GetTotalMemory 0 B · MonoUsedSize 0 B (폴링 2회)
|
||||||
|
[OK] ApplyGrowth × 1000 = 0 B ↔ 원본 Get_Point() × 1000 = 0 B (원본은 LINQ Select+Sum · sink=138000)
|
||||||
|
레벨업 Dispatch × 1000 — 헬퍼만 126976 B · 구독 포함 176128 B → 내 경로 몫 49152 B / 1000회 (813l 때부터 있던 진단 문자열 · **프레임 경로 아님**)
|
||||||
|
대조군 new object() × 1000 = 24576 B (0 이 아니어야 측정이 살아 있다) · keep=1000
|
||||||
|
|
||||||
|
── ⑥ C8 롤백
|
||||||
|
[OK] Enabled=false
|
||||||
|
[OK] 레벨업 무시(LevelUpSeen=0 Toast=0)
|
||||||
|
[OK] BuildStatPopupLine()=null
|
||||||
|
[OK] 해제 후 Enabled=true
|
||||||
|
[OK] statPointWeight=0 · levelWeight=0 → 레벨 16 · 포인트 75 여도 200.00 (813l 원래 산식과 동일 = 값 하나로 되돌린다)
|
||||||
|
|
||||||
|
RESULT PASS
|
||||||
|
|
@ -0,0 +1,426 @@
|
||||||
|
// WL813l2_Probe.cs — #813l2(Q6 D-28 「전투력 토스트가 한 번도 안 뜸 · LastPower 52 불변」 수리) 에디트 모드 프로브
|
||||||
|
// 에디트 모드 전용 · Play 불필요 · 로그인 0 · 씬 무수정(임시 GameObject 는 즉시 파괴) · 에셋 쓰기 0
|
||||||
|
//
|
||||||
|
// 상주 에디터:
|
||||||
|
// unity command run_script --project-path <wt> --file AgentScripts/WL813l2_Probe.cs --entry WL813l2_Probe.RunAll --timeout 600
|
||||||
|
// 결과: Console + AgentScripts/WL813l2_PROBE.txt (RESULT PASS / RESULT FAIL n)
|
||||||
|
//
|
||||||
|
// 검사 (발주서 WL-813l2 §1)
|
||||||
|
// ⓪ 설정 에셋 · 새 성장 입력 값 전수
|
||||||
|
// ① 원인 실증 — 원본 스탯은 레벨업으로 안 움직인다 / 미사용 포인트만 움직인다(원본 Get_Point() 와 대조)
|
||||||
|
// ② 813k 실 Dispatch — LevelUp(prev ATK 100 → new 112) → 토스트 "+N %" · LastPower 변화
|
||||||
|
// ③ 🔴 D-28 재현·수리 — 스탯 prev == new(원본 기본 동작)인데도 표시된다 / statPointWeight=0 이면 다시 0회
|
||||||
|
// ④ 팝업 전투력 줄 — 813y StatPopup 에 「전투력 A → B (+N %)」 실제로 붙는가(레벨업 프레임 스탬프)
|
||||||
|
// ⑤ GC 0 — Tick × 1000 · 페이로드 Dispatch × 1000 · ApplyGrowth × 1000(원본 LINQ Get_Point 대조군)
|
||||||
|
// ⑥ C8 — enabled_=0 → 표시 0 / statPointWeight=0·levelWeight=0 → 813l 원래 산식 그대로
|
||||||
|
//
|
||||||
|
// 에디트 모드 한계(보고서 「미확인」)
|
||||||
|
// · 진짜 PC 는 ServerData 없이 m_Stat 이 안 만들어진다(813l 보고 §5-3) → 실전 전투력 자릿수는 여전히 미확인.
|
||||||
|
// · Canvas 가 없어 UnitsPerPx = 1(px == 유닛).
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.UI;
|
||||||
|
using TMPro;
|
||||||
|
using WL.Combat.Growth;
|
||||||
|
using WL.UI;
|
||||||
|
|
||||||
|
public static class WL813l2_Probe
|
||||||
|
{
|
||||||
|
const string OutCommit = "AgentScripts/WL813l2_PROBE.txt";
|
||||||
|
|
||||||
|
static StringBuilder sb;
|
||||||
|
static int s_fail;
|
||||||
|
|
||||||
|
static GameObject s_canvasGo;
|
||||||
|
static StatPopup s_popup;
|
||||||
|
static PCActor s_prevMyPc;
|
||||||
|
static bool s_prevBMyPc;
|
||||||
|
|
||||||
|
// 에셋 값 임시 변경분 원복용(에셋 저장 0)
|
||||||
|
static bool s_prevSuppress;
|
||||||
|
static float s_prevPointWeight, s_prevLevelWeight;
|
||||||
|
static bool s_prevUsePayload;
|
||||||
|
|
||||||
|
public static object RunAll()
|
||||||
|
{
|
||||||
|
sb = new StringBuilder();
|
||||||
|
s_fail = 0;
|
||||||
|
sb.AppendLine("# WL813l2 Probe " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " (edit mode · Play 0 · 로그인 0)");
|
||||||
|
sb.AppendLine("playMode=" + Application.isPlaying);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Setup();
|
||||||
|
Sec0_Asset();
|
||||||
|
Sec1_Cause();
|
||||||
|
Sec2_Payload();
|
||||||
|
Sec3_D28();
|
||||||
|
Sec4_PopupLine();
|
||||||
|
Sec5_Gc();
|
||||||
|
Sec6_Rollback();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
s_fail++;
|
||||||
|
L("EXCEPTION " + ex);
|
||||||
|
}
|
||||||
|
finally { Teardown(); }
|
||||||
|
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine(s_fail == 0 ? "RESULT PASS" : "RESULT FAIL " + s_fail);
|
||||||
|
string text = sb.ToString();
|
||||||
|
try { File.WriteAllText(Path.Combine(Directory.GetCurrentDirectory(), OutCommit), text, new UTF8Encoding(true)); } catch { }
|
||||||
|
Debug.Log(text);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void L(string s) { sb.AppendLine(s); }
|
||||||
|
static string P(bool ok) { if (!ok) s_fail++; return ok ? "[OK]" : "[FAIL]"; }
|
||||||
|
static bool Near(double a, double b) { return Math.Abs(a - b) < 0.01d; }
|
||||||
|
|
||||||
|
// ───────────────────────────────────────── 준비
|
||||||
|
static void Setup()
|
||||||
|
{
|
||||||
|
s_prevMyPc = MyValue.MyPC;
|
||||||
|
s_prevBMyPc = MyValue.bMyPC;
|
||||||
|
MyValue.MyPC = null; // 에디트 모드 = 월드에 PC 없음(페이로드만으로 도는 경로)
|
||||||
|
|
||||||
|
WLPowerDeltaSettings.RuntimeDisabled = false;
|
||||||
|
WLPowerDeltaSettings.ClearCache();
|
||||||
|
var s = WLPowerDeltaSettings.Instance;
|
||||||
|
if (s != null)
|
||||||
|
{
|
||||||
|
s_prevSuppress = s.suppressToastOnLevelUp;
|
||||||
|
s_prevPointWeight = s.statPointWeight;
|
||||||
|
s_prevLevelWeight = s.levelWeight;
|
||||||
|
s_prevUsePayload = s.useLevelUpPayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
PowerDeltaToast.ResetDiagnostics();
|
||||||
|
PowerDeltaToast.Subscribe(false);
|
||||||
|
PowerDeltaToast.Subscribe(true);
|
||||||
|
|
||||||
|
L("");
|
||||||
|
L("── 준비");
|
||||||
|
L(" " + P(PowerDeltaToast.Subscribed) + " 구독 ON · MyValue.bMyPC=" + MyValue.bMyPC + "(월드 PC 없음 = 로비·에디트 경로)");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Teardown()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var s = WLPowerDeltaSettings.Instance;
|
||||||
|
if (s != null)
|
||||||
|
{
|
||||||
|
s.suppressToastOnLevelUp = s_prevSuppress;
|
||||||
|
s.statPointWeight = s_prevPointWeight;
|
||||||
|
s.levelWeight = s_prevLevelWeight;
|
||||||
|
s.useLevelUpPayload = s_prevUsePayload;
|
||||||
|
}
|
||||||
|
if (s_popup != null) s_popup.Subscribe(false);
|
||||||
|
PowerDeltaToast.Subscribe(false);
|
||||||
|
PowerDeltaToast.ResetDiagnostics();
|
||||||
|
WLPowerDeltaSettings.RuntimeDisabled = false;
|
||||||
|
MyValue.MyPC = s_prevMyPc;
|
||||||
|
MyValue.bMyPC = s_prevBMyPc;
|
||||||
|
if (s_canvasGo != null) UnityEngine.Object.DestroyImmediate(s_canvasGo);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────────────────────────────────────── ⓪ 에셋
|
||||||
|
static void Sec0_Asset()
|
||||||
|
{
|
||||||
|
L("");
|
||||||
|
L("── ⓪ 설정 에셋 · 성장 입력 값 전수 (C45 · 코드 상수 0)");
|
||||||
|
var s = WLPowerDeltaSettings.Instance;
|
||||||
|
L(" " + P(s != null) + " WLPowerDeltaSettings 로드");
|
||||||
|
if (s == null) return;
|
||||||
|
L(" " + P(WLPowerDeltaSettings.Enabled) + " Enabled · enabled_=" + s.enabled_);
|
||||||
|
L(" 성장 입력: useLevelUpPayload=" + s.useLevelUpPayload + " readGrowthFromServerData=" + s.readGrowthFromServerData +
|
||||||
|
" statPointWeight=" + s.statPointWeight + " atkGain=" + s.statPointAtkGain + " hpGain=" + s.statPointHpGain +
|
||||||
|
" perLevel=" + s.statPointsPerLevel + " levelWeight=" + s.levelWeight + " levelBase=" + s.levelBase);
|
||||||
|
L(" 기존 산식: atk=" + s.atkStat + "(폴백 " + s.atkFallbackStat + ") hp=" + s.hpStat +
|
||||||
|
" hpWeight=" + s.hpWeight + " powerScale=" + s.powerScale + " 최소 표시 " + s.minPercentToShow + "%" +
|
||||||
|
" suppressToastOnLevelUp=" + s.suppressToastOnLevelUp);
|
||||||
|
L(" " + P(s.statPointsPerLevel == 5) + " statPointsPerLevel=5 (원본 실측 ServerClass.cs:2770 Get_Point() = (Lv−1)*5 − 사용분)");
|
||||||
|
L(" " + P(Near(s.statPointAtkGain, 1d) && Near(s.statPointHpGain, 22d))
|
||||||
|
+ " 포인트 환산 ATK +" + s.statPointAtkGain + " / MaxHP +" + s.statPointHpGain +
|
||||||
|
" (원본 실측 MyValue.cs:335-336 dic_MainStatMultiplier[STR] = ATK 1 · MaxHP 22)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────────────────────────────────────── ① 원인 실증
|
||||||
|
static void Sec1_Cause()
|
||||||
|
{
|
||||||
|
L("");
|
||||||
|
L("── ① 🔴 D-28 원인 실증 — 원본 스탯은 레벨업으로 안 움직인다 · 움직이는 것은 미사용 포인트뿐");
|
||||||
|
var s = WLPowerDeltaSettings.Instance;
|
||||||
|
|
||||||
|
// (a) 원본 규약 대조 — SD_PC 를 직접 만들어 레벨만 올려 본다(원본 API 만 · 쓰기 0)
|
||||||
|
var pc1 = new SD_PC(); // Lv=1 · Stat={0,0,0,0}
|
||||||
|
var pc16 = new SD_PC(); pc16.Lv = 16; // 레벨만 1 → 16
|
||||||
|
L(" Lv 1 → 16 · 스탯 포인트 사용 0 · 장비 변화 0 일 때 원본이 주는 것:");
|
||||||
|
L(" " + P(pc1.Get_Point() == 0 && pc16.Get_Point() == 75)
|
||||||
|
+ " Get_Point() " + pc1.Get_Point() + " → " + pc16.Get_Point() + " (= (Lv−1)*5 · 미사용 포인트만 늘어난다)");
|
||||||
|
L(" " + P(pc1.Get_Stat(eStat.STR) == 0 && pc16.Get_Stat(eStat.STR) == 0)
|
||||||
|
+ " SD_PC.Get_Stat(STR) " + pc1.Get_Stat(eStat.STR) + " → " + pc16.Get_Stat(eStat.STR) +
|
||||||
|
" (= **사용한** 포인트만 셈 · table_classconfig.Get_PureStat:246 이 읽는 값) → ActorStatInfo 는 그대로");
|
||||||
|
L(" ⇒ `ActorStatInfo.Reset`(MyValue.cs:286-453)은 SD_PC.Lv 를 한 번도 안 읽는다 = 813l 이 폴링해도 값이 상수");
|
||||||
|
L(" (Q6 D-28 `PollCount 1573` 인데 `LastPower 52.000000` 불변 · 레벨 1→16)");
|
||||||
|
|
||||||
|
// (b) 내 포인트 산식이 원본 Get_Point() 와 값이 같은가(GC 0 판 · LINQ 없음)
|
||||||
|
var pcx = new SD_PC(); pcx.Lv = 16; pcx.Stat[0] = 3; pcx.Stat[1] = 2; // 5점 사용
|
||||||
|
WLPowerMeter.Sample g = default(WLPowerMeter.Sample);
|
||||||
|
g.aspd = s.attackSpeedBase;
|
||||||
|
WLPowerMeter.ApplyGrowth(pcx, s, ref g);
|
||||||
|
L(" " + P((int)g.statPoints == pcx.Get_Point() && (int)g.level == 16)
|
||||||
|
+ " ApplyGrowth ↔ 원본 Get_Point() 대조 — 내 값 " + g.statPoints.ToString("F0") +
|
||||||
|
" · 원본 " + pcx.Get_Point() + " · level=" + g.level.ToString("F0") + " (사용 5점 반영)");
|
||||||
|
|
||||||
|
// (c) 813l 원래 산식 회귀 — 성장 입력 0 이면 값이 그대로여야 한다
|
||||||
|
double p1 = WLPowerMeter.Compute(100d, 0d, 0d, 1d, 1000d, s);
|
||||||
|
double p2 = WLPowerMeter.Compute(100d, 0.2d, 0.5d, 1d, 1000d, s);
|
||||||
|
double p3 = WLPowerMeter.Compute(100d, 0d, 0d, 1.3d, 1000d, s);
|
||||||
|
double p4 = WLPowerMeter.Compute(200d, 0.2d, 0.5d, 1.3d, 2000d, s);
|
||||||
|
L(" " + P(Near(p1, 200d) && Near(p2, 210d) && Near(p3, 230d) && Near(p4, 486d))
|
||||||
|
+ " 813l 산식 회귀 4건(성장 입력 0) — " + p1.ToString("F2") + " / " + p2.ToString("F2") + " / " +
|
||||||
|
p3.ToString("F2") + " / " + p4.ToString("F2") + " (기대 200 / 210 / 230 / 486)");
|
||||||
|
|
||||||
|
// (d) 성장 입력이 붙으면 값이 움직인다 — 손계산 대조
|
||||||
|
// (100 + 5×1×1) × 1 × 1 + (1000 + 5×1×22) × 0.1 = 105 + 111.0 = 216.0
|
||||||
|
double g5 = WLPowerMeter.Compute(100d, 0d, 0d, 1d, 1000d, 2d, 5d, s);
|
||||||
|
L(" " + P(Near(g5, 216d)) + " 포인트 5점 → " + g5.ToString("F2") + " (기대 216.00 = 105 + 111.0)");
|
||||||
|
double g10 = WLPowerMeter.Compute(100d, 0d, 0d, 1d, 1000d, 3d, 10d, s);
|
||||||
|
L(" " + P(Near(g10, 232d)) + " 포인트 10점 → " + g10.ToString("F2") + " (기대 232.00 · 레벨당 +16.0)");
|
||||||
|
L(" " + P(WLPowerMeter.PercentDelta(p1, g5) > 7.9d && WLPowerMeter.PercentDelta(p1, g5) < 8.1d)
|
||||||
|
+ " 레벨업 1회 증가율 " + WLPowerMeter.PercentDelta(p1, g5).ToString("F2") + "% (최소 표시 " + s.minPercentToShow + "% 를 넘는다)");
|
||||||
|
|
||||||
|
// (e) 포인트를 실제로 찍어도 전투력이 튀지 않는다(잠재 → 실현 연속)
|
||||||
|
// 5점 미사용: (100+5)×1 + (1000+110)×0.1 = 216.0
|
||||||
|
// 5점 사용 후: atk 100+5 · hp 1000+110 · 포인트 0 → 216.0
|
||||||
|
double spent = WLPowerMeter.Compute(105d, 0d, 0d, 1d, 1110d, 2d, 0d, s);
|
||||||
|
L(" " + P(Near(spent, g5)) + " 포인트 사용 전/후 연속 — 미사용 " + g5.ToString("F2") +
|
||||||
|
" ↔ 사용 후 " + spent.ToString("F2") + " (statPointWeight=1 의 뜻)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────────────────────────────────────── ② 813k 실 Dispatch (스탯이 실제로 바뀐 경우)
|
||||||
|
static void Sec2_Payload()
|
||||||
|
{
|
||||||
|
L("");
|
||||||
|
L("── ② 813k GrowthEvents.LevelUp 실 Dispatch — prev ATK 100 → new 112 (발주서 §1-3)");
|
||||||
|
var s = WLPowerDeltaSettings.Instance;
|
||||||
|
PowerDeltaToast.ResetDiagnostics();
|
||||||
|
|
||||||
|
// (a) 에셋 기본(suppressToastOnLevelUp=1) — 팝업 줄이 표시하므로 토스트는 생략된다
|
||||||
|
s.suppressToastOnLevelUp = true;
|
||||||
|
string d1 = PowerDeltaToast.RaiseFakeLevelUpForProbe(6, 7, 30,
|
||||||
|
eStat.ATK, 100d, 112d, eStat.MaxHP, 1000d, 1000d, eStat.MaxMP, 200d, 200d);
|
||||||
|
L(" " + d1);
|
||||||
|
// 기대: new = (112+30)×1×1 + (1000+30×22)×0.1 = 142 + 166 = 308
|
||||||
|
// prev= (100+25)×1×1 + (1000+25×22)×0.1 = 125 + 155 = 280 (prev 포인트 = 30 − 5)
|
||||||
|
L(" " + P(PowerDeltaToast.LevelUpSeen == 1 && PowerDeltaToast.PayloadUsed == 1)
|
||||||
|
+ " 수신 " + PowerDeltaToast.LevelUpSeen + "회 · 소스 \"" + PowerDeltaToast.LastPowerSource + "\"");
|
||||||
|
L(" " + P(Near(PowerDeltaToast.LastPayloadPrev, 280d) && Near(PowerDeltaToast.LastPayloadNew, 308d))
|
||||||
|
+ " 페이로드 전투력 prev=" + PowerDeltaToast.LastPayloadPrev.ToString("F2") +
|
||||||
|
" new=" + PowerDeltaToast.LastPayloadNew.ToString("F2") + " (기대 280.00 / 308.00 · 손계산)");
|
||||||
|
L(" " + P(PowerDeltaToast.HasPower && Near(PowerDeltaToast.LastPower, 308d))
|
||||||
|
+ " 🔴 LastPower 0 → " + PowerDeltaToast.LastPower.ToString("F2") + " (D-28 = 여기가 52 로 고정이었다)");
|
||||||
|
L(" " + P(PowerDeltaToast.SuppressedByPopup == 1 && PowerDeltaToast.PowerToastCount == 0)
|
||||||
|
+ " 레벨업 프레임 토스트 생략(Suppressed=" + PowerDeltaToast.SuppressedByPopup +
|
||||||
|
" Toast=" + PowerDeltaToast.PowerToastCount + ") — 같은 값을 팝업 줄이 보여준다");
|
||||||
|
string line = PowerDeltaToast.BuildStatPopupLine();
|
||||||
|
L(" " + P(line != null && line.Contains("280") && line.Contains("308") && line.Contains("10.0"))
|
||||||
|
+ " 팝업 줄 \"" + line + "\" (기대 280 → 308 · +10.0%)");
|
||||||
|
|
||||||
|
// (b) 토스트 자체 — 생략 스위치를 끄고 같은 페이로드를 다시 흘린다
|
||||||
|
PowerDeltaToast.ResetDiagnostics();
|
||||||
|
s.suppressToastOnLevelUp = false;
|
||||||
|
PowerDeltaToast.RaiseFakeLevelUpForProbe(6, 7, 30,
|
||||||
|
eStat.ATK, 100d, 112d, eStat.MaxHP, 1000d, 1000d, eStat.MaxMP, 200d, 200d);
|
||||||
|
L(" " + P(PowerDeltaToast.PowerToastCount == 1 && PowerDeltaToast.LastText.Contains("10.0"))
|
||||||
|
+ " 토스트 " + PowerDeltaToast.PowerToastCount + "회 · 문구 \"" + PowerDeltaToast.LastText + "\" (기대 \"전투력 +10.0%\")");
|
||||||
|
s.suppressToastOnLevelUp = s_prevSuppress;
|
||||||
|
|
||||||
|
// (c) 페이로드를 끄면(=813l 원래 동작) 에디트 모드에서는 읽을 스탯이 없어 표시 0 — D-28 의 모양
|
||||||
|
PowerDeltaToast.ResetDiagnostics();
|
||||||
|
s.useLevelUpPayload = false;
|
||||||
|
PowerDeltaToast.RaiseFakeLevelUpForProbe(6, 7, 30,
|
||||||
|
eStat.ATK, 100d, 112d, eStat.MaxHP, 1000d, 1000d, eStat.MaxMP, 200d, 200d);
|
||||||
|
L(" " + P(PowerDeltaToast.PayloadUsed == 0 && PowerDeltaToast.PowerToastCount == 0 && !PowerDeltaToast.HasPower)
|
||||||
|
+ " useLevelUpPayload=0 → 소스 \"" + PowerDeltaToast.LastPowerSource + "\" · 토스트 " +
|
||||||
|
PowerDeltaToast.PowerToastCount + "회 · LastPower 없음(= 813l 원래 경로)");
|
||||||
|
s.useLevelUpPayload = s_prevUsePayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────────────────────────────────────── ③ D-28 재현 · 수리
|
||||||
|
static void Sec3_D28()
|
||||||
|
{
|
||||||
|
L("");
|
||||||
|
L("── ③ 🔴 D-28 재현 · 수리 — 스탯이 **하나도 안 바뀌는** 원본 기본 동작(813k 보고 ①)에서");
|
||||||
|
var s = WLPowerDeltaSettings.Instance;
|
||||||
|
|
||||||
|
// (a) 수리 후 — 스탯 prev == new 인데도 표시된다(포인트가 늘었으니까)
|
||||||
|
PowerDeltaToast.ResetDiagnostics();
|
||||||
|
s.suppressToastOnLevelUp = false;
|
||||||
|
string d = PowerDeltaToast.RaiseFakeLevelUpForProbe(7, 8, 35,
|
||||||
|
eStat.MaxHP, 1000d, 1000d, eStat.MaxMP, 200d, 200d, eStat.ATK, 100d, 100d);
|
||||||
|
L(" " + d + " · 스탯 3종 prev == new (원본 기본값)");
|
||||||
|
// 기대: new = (100+35)×1 + (1000+35×22)×0.1 = 135 + 177 = 312
|
||||||
|
// prev= (100+30)×1 + (1000+30×22)×0.1 = 130 + 166 = 296 → +5.41%
|
||||||
|
L(" " + P(Near(PowerDeltaToast.LastPayloadPrev, 296d) && Near(PowerDeltaToast.LastPayloadNew, 312d))
|
||||||
|
+ " 전투력 " + PowerDeltaToast.LastPayloadPrev.ToString("F2") + " → " +
|
||||||
|
PowerDeltaToast.LastPayloadNew.ToString("F2") + " (기대 296.00 → 312.00)");
|
||||||
|
L(" " + P(PowerDeltaToast.PowerToastCount == 1 && PowerDeltaToast.LastText.Contains("5.4"))
|
||||||
|
+ " ✅ 수리 후 — 토스트 " + PowerDeltaToast.PowerToastCount + "회 \"" + PowerDeltaToast.LastText + "\"");
|
||||||
|
|
||||||
|
// (b) 수리 전 모양 — 성장 입력을 0 으로 두면 그대로 D-28(값 불변 · 표시 0)
|
||||||
|
PowerDeltaToast.ResetDiagnostics();
|
||||||
|
s.statPointWeight = 0f; s.levelWeight = 0f;
|
||||||
|
PowerDeltaToast.RaiseFakeLevelUpForProbe(7, 8, 35,
|
||||||
|
eStat.MaxHP, 1000d, 1000d, eStat.MaxMP, 200d, 200d, eStat.ATK, 100d, 100d);
|
||||||
|
double p0 = PowerDeltaToast.LastPayloadPrev, n0 = PowerDeltaToast.LastPayloadNew;
|
||||||
|
PowerDeltaToast.RaiseFakeLevelUpForProbe(8, 9, 40,
|
||||||
|
eStat.MaxHP, 1000d, 1000d, eStat.MaxMP, 200d, 200d, eStat.ATK, 100d, 100d);
|
||||||
|
L(" " + P(Near(p0, n0) && Near(PowerDeltaToast.LastPayloadNew, n0) && PowerDeltaToast.PowerToastCount == 0)
|
||||||
|
+ " ❌ 재현(statPointWeight=0) — 레벨 7→8→9 인데 전투력 " + p0.ToString("F2") + " 불변 · 토스트 " +
|
||||||
|
PowerDeltaToast.PowerToastCount + "회 = Q6 D-28 그대로");
|
||||||
|
s.statPointWeight = s_prevPointWeight; s.levelWeight = s_prevLevelWeight;
|
||||||
|
s.suppressToastOnLevelUp = s_prevSuppress;
|
||||||
|
|
||||||
|
// (c) 다단 레벨업(levelsGained > 1) 도 prev 포인트를 제대로 역산하는가
|
||||||
|
PowerDeltaToast.ResetDiagnostics();
|
||||||
|
PowerDeltaToast.RaiseFakeLevelUpForProbe(5, 8, 35,
|
||||||
|
eStat.MaxHP, 1000d, 1000d, eStat.MaxMP, 200d, 200d, eStat.ATK, 100d, 100d);
|
||||||
|
// prev 포인트 = 35 − 3×5 = 20 → prev = (100+20)×1 + (1000+440)×0.1 = 120 + 144 = 264
|
||||||
|
L(" " + P(Near(PowerDeltaToast.LastPayloadPrev, 264d))
|
||||||
|
+ " 다단(5→8 · 3레벨) prev 포인트 역산 → prev 전투력 " + PowerDeltaToast.LastPayloadPrev.ToString("F2") +
|
||||||
|
" (기대 264.00 = 포인트 20점)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────────────────────────────────────── ④ 팝업 줄 (813y StatPopup 왕복)
|
||||||
|
static void Sec4_PopupLine()
|
||||||
|
{
|
||||||
|
L("");
|
||||||
|
L("── ④ 레벨업 팝업 전투력 줄 — 813y StatPopup 왕복(구독 → Dispatch → 줄)");
|
||||||
|
var s = WLPowerDeltaSettings.Instance;
|
||||||
|
|
||||||
|
s_canvasGo = new GameObject("__WL813l2_Canvas", typeof(Canvas), typeof(CanvasScaler));
|
||||||
|
s_canvasGo.layer = 5;
|
||||||
|
var croot = (RectTransform)s_canvasGo.transform;
|
||||||
|
croot.sizeDelta = new Vector2(1080f, 1920f);
|
||||||
|
|
||||||
|
var prt = new GameObject("WL_StatPopup", typeof(RectTransform));
|
||||||
|
prt.layer = 5;
|
||||||
|
prt.transform.SetParent(croot, false);
|
||||||
|
s_popup = prt.AddComponent<StatPopup>();
|
||||||
|
L(" init: " + s_popup.Initialize());
|
||||||
|
L(" " + P(StatPopup.PowerLineOn) + " StatPopup.PowerLineOn");
|
||||||
|
L(" " + P(s_popup.Subscribed) + " StatPopup 구독=" + s_popup.Subscribed +
|
||||||
|
" · GrowthEvents.LevelUp 구독자=" + GrowthEvents.LevelUp.Count + "(StatPopup + PowerDeltaToast)");
|
||||||
|
|
||||||
|
PowerDeltaToast.ResetDiagnostics();
|
||||||
|
PowerDeltaToast.RaiseFakeLevelUpForProbe(9, 10, 45,
|
||||||
|
eStat.MaxHP, 1000d, 1000d, eStat.MaxMP, 200d, 200d, eStat.ATK, 100d, 100d);
|
||||||
|
// new = (100+45) + (1000+990)×0.1 = 145 + 199 = 344 · prev = (100+40) + (1000+880)×0.1 = 140 + 188 = 328
|
||||||
|
L(" 팝업 표시: rows=" + s_popup.LastRowCount + " level=" + s_popup.LastLevel +
|
||||||
|
" text=\"" + s_popup.LastText + "\"");
|
||||||
|
L(" " + P(!string.IsNullOrEmpty(s_popup.LastPowerLine) &&
|
||||||
|
s_popup.LastPowerLine.Contains("328") && s_popup.LastPowerLine.Contains("344"))
|
||||||
|
+ " 🔴 팝업 전투력 줄 \"" + s_popup.LastPowerLine + "\" (기대 328 → 344 · +4.9%)");
|
||||||
|
L(" " + P(PowerDeltaToast.SuppressedByPopup + PowerDeltaToast.PowerToastCount == 1)
|
||||||
|
+ " 중복 표시 0 — Suppressed=" + PowerDeltaToast.SuppressedByPopup + " Toast=" + PowerDeltaToast.PowerToastCount);
|
||||||
|
|
||||||
|
// 구독 순서 무관 — StatPopup 이 먼저 구독돼 있어도 같은 값이 나온다(프레임 스탬프)
|
||||||
|
L(" " + P(PowerDeltaToast.LastPayloadPrev > 0d) + " 프레임 스탬프 prev=" +
|
||||||
|
PowerDeltaToast.LastPayloadPrev.ToString("F2") + " new=" + PowerDeltaToast.LastPayloadNew.ToString("F2") +
|
||||||
|
" → 팝업/토스트가 같은 값을 본다(구독 순서 무관)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────────────────────────────────────── ⑤ GC
|
||||||
|
static void Sec5_Gc()
|
||||||
|
{
|
||||||
|
L("");
|
||||||
|
L("── ⑤ GC 0 (폴링 · 페이로드 · 포인트 산식)");
|
||||||
|
var s = WLPowerDeltaSettings.Instance;
|
||||||
|
PowerDeltaToast.ResetDiagnostics();
|
||||||
|
PowerDeltaToast.Observe(1000d, 0f, "base");
|
||||||
|
|
||||||
|
long a0 = Gc(), b0 = Prof();
|
||||||
|
for (int i = 0; i < 1000; i++) PowerDeltaToast.Tick(3000f + i * 0.001f, 0.001f);
|
||||||
|
long a1 = Gc(), b1 = Prof();
|
||||||
|
L(" " + P(a1 - a0 <= 0 && b1 - b0 <= 0) + " Tick × 1000 → GetTotalMemory " + (a1 - a0) +
|
||||||
|
" B · MonoUsedSize " + (b1 - b0) + " B (폴링 " + PowerDeltaToast.PollCount + "회)");
|
||||||
|
|
||||||
|
// 내 포인트 산식(for 루프 · 할당 0) vs 원본 Get_Point()(LINQ Select+Sum · 할당 있음)
|
||||||
|
var pcx = new SD_PC(); pcx.Lv = 30; pcx.Stat[0] = 7;
|
||||||
|
WLPowerMeter.Sample g = default(WLPowerMeter.Sample);
|
||||||
|
g.aspd = s.attackSpeedBase;
|
||||||
|
WLPowerMeter.ApplyGrowth(pcx, s, ref g); // 워밍업
|
||||||
|
long c0 = Gc();
|
||||||
|
for (int i = 0; i < 1000; i++) WLPowerMeter.ApplyGrowth(pcx, s, ref g);
|
||||||
|
long c1 = Gc();
|
||||||
|
int sink = 0;
|
||||||
|
long d0 = Gc();
|
||||||
|
for (int i = 0; i < 1000; i++) sink += pcx.Get_Point();
|
||||||
|
long d1 = Gc();
|
||||||
|
L(" " + P(c1 - c0 <= 0) + " ApplyGrowth × 1000 = " + (c1 - c0) + " B ↔ 원본 Get_Point() × 1000 = " +
|
||||||
|
(d1 - d0) + " B (원본은 LINQ Select+Sum · sink=" + sink + ")");
|
||||||
|
|
||||||
|
// 레벨업 1회당 비용 — 프레임 경로가 아니라 **이벤트 1회** 경로다.
|
||||||
|
// 기준선(A) = 프로브 헬퍼의 반환 문자열만(구독 off) · 측정(B) = 거기에 내 페이로드 경로가 얹힌 것.
|
||||||
|
if (s_popup != null) s_popup.Subscribe(false); // 813y 팝업 비용은 이 측정에서 뺀다
|
||||||
|
PowerDeltaToast.Subscribe(false);
|
||||||
|
long e0 = Gc();
|
||||||
|
for (int i = 0; i < 1000; i++)
|
||||||
|
PowerDeltaToast.RaiseFakeLevelUpForProbe(7, 8, 35, eStat.MaxHP, 1000d, 1000d,
|
||||||
|
eStat.MaxMP, 200d, 200d, eStat.ATK, 100d, 100d);
|
||||||
|
long e1 = Gc();
|
||||||
|
PowerDeltaToast.Subscribe(true);
|
||||||
|
PowerDeltaToast.ResetDiagnostics();
|
||||||
|
long f0 = Gc();
|
||||||
|
for (int i = 0; i < 1000; i++)
|
||||||
|
PowerDeltaToast.RaiseFakeLevelUpForProbe(7, 8, 35, eStat.MaxHP, 1000d, 1000d,
|
||||||
|
eStat.MaxMP, 200d, 200d, eStat.ATK, 100d, 100d);
|
||||||
|
long f1 = Gc();
|
||||||
|
L(" 레벨업 Dispatch × 1000 — 헬퍼만 " + (e0 == e1 ? "0" : (e1 - e0).ToString()) + " B · 구독 포함 " + (f1 - f0) +
|
||||||
|
" B → 내 경로 몫 " + ((f1 - f0) - (e1 - e0)) + " B / 1000회 (813l 때부터 있던 진단 문자열 · **프레임 경로 아님**)");
|
||||||
|
|
||||||
|
long h0 = Gc();
|
||||||
|
var keep = new object[1000];
|
||||||
|
for (int i = 0; i < 1000; i++) keep[i] = new object();
|
||||||
|
long h1 = Gc();
|
||||||
|
L(" 대조군 new object() × 1000 = " + (h1 - h0) + " B (0 이 아니어야 측정이 살아 있다) · keep=" + keep.Length);
|
||||||
|
PowerDeltaToast.ResetDiagnostics();
|
||||||
|
}
|
||||||
|
|
||||||
|
static long Gc() { return GC.GetTotalMemory(false); }
|
||||||
|
static long Prof() { return UnityEngine.Profiling.Profiler.GetMonoUsedSizeLong(); }
|
||||||
|
|
||||||
|
// ───────────────────────────────────────── ⑥ C8
|
||||||
|
static void Sec6_Rollback()
|
||||||
|
{
|
||||||
|
L("");
|
||||||
|
L("── ⑥ C8 롤백");
|
||||||
|
var s = WLPowerDeltaSettings.Instance;
|
||||||
|
PowerDeltaToast.ResetDiagnostics();
|
||||||
|
WLPowerDeltaSettings.RuntimeDisabled = true;
|
||||||
|
L(" " + P(!WLPowerDeltaSettings.Enabled) + " Enabled=false");
|
||||||
|
PowerDeltaToast.RaiseFakeLevelUpForProbe(7, 8, 35, eStat.MaxHP, 1000d, 1000d,
|
||||||
|
eStat.MaxMP, 200d, 200d, eStat.ATK, 100d, 100d);
|
||||||
|
L(" " + P(PowerDeltaToast.LevelUpSeen == 0 && PowerDeltaToast.PowerToastCount == 0)
|
||||||
|
+ " 레벨업 무시(LevelUpSeen=" + PowerDeltaToast.LevelUpSeen + " Toast=" + PowerDeltaToast.PowerToastCount + ")");
|
||||||
|
L(" " + P(PowerDeltaToast.BuildStatPopupLine() == null) + " BuildStatPopupLine()=null");
|
||||||
|
WLPowerDeltaSettings.RuntimeDisabled = false;
|
||||||
|
L(" " + P(WLPowerDeltaSettings.Enabled) + " 해제 후 Enabled=true");
|
||||||
|
|
||||||
|
// 성장 입력만 끄면 813l 원래 산식 그대로(= 이번 변경 전 동작)
|
||||||
|
s.statPointWeight = 0f; s.levelWeight = 0f;
|
||||||
|
double r1 = WLPowerMeter.Compute(100d, 0d, 0d, 1d, 1000d, 16d, 75d, s);
|
||||||
|
s.statPointWeight = s_prevPointWeight; s.levelWeight = s_prevLevelWeight;
|
||||||
|
L(" " + P(Near(r1, 200d)) + " statPointWeight=0 · levelWeight=0 → 레벨 16 · 포인트 75 여도 " +
|
||||||
|
r1.ToString("F2") + " (813l 원래 산식과 동일 = 값 하나로 되돌린다)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,10 +14,33 @@
|
||||||
// · Gameplay(`Assets/WL/Combat/**`) 수정 0 — 811b `CombatEvents` · 813k `GrowthEvents` · 813p `RunEvents` **구독만**.
|
// · Gameplay(`Assets/WL/Combat/**`) 수정 0 — 811b `CombatEvents` · 813k `GrowthEvents` · 813p `RunEvents` **구독만**.
|
||||||
// · 프리팹 diff 0 — 토스트 노드는 런타임 생성(`HideFlags.DontSave`) · 813q `RunHud` 선례.
|
// · 프리팹 diff 0 — 토스트 노드는 런타임 생성(`HideFlags.DontSave`) · 813q `RunHud` 선례.
|
||||||
//
|
//
|
||||||
|
// ■ 🔴 WL-813l2 — Q6 D-28(레벨 1→16 인데 `LastPower 52.000000` 불변 · 토스트 0회) 원인과 수리
|
||||||
|
// 원인(실측 · 「추정」 아님):
|
||||||
|
// · `ActorStatInfo.Reset`(`Assets/Script/Util/MyValue.cs:286-453`)이 **`SD_PC.Lv` 를 한 번도 안 읽는다.**
|
||||||
|
// PC 스탯은 `table_classconfig.Get_PureStat`(`Table/PC/table_classconfig.cs:232-248`
|
||||||
|
// = 보유 직업 기본치 + **사용한** 포인트 `SD_PC.Get_Stat`) + 장비·각성·초월·유물에서만 만들어진다.
|
||||||
|
// · 레벨업(`Server/ServerClass.cs:2725-2739 Add_Exp`)은 `++Lv` 뿐 — 스탯은 안 건드리고
|
||||||
|
// `Get_Point()`(`:2770` = `(Lv−1)*5 − 사용분`)의 **미사용 포인트만** 늘린다.
|
||||||
|
// 포인트를 실제로 찍는 곳은 수동 UI `PCInfoStatMgrUI.cs:81` 하나뿐이다(자동 배분 0).
|
||||||
|
// → 813l 이 레벨업 때도 `ActorStatInfo` 를 **다시 읽기만** 해서(`Observe(sm.power)`) 값이 상수 = 표시 0회.
|
||||||
|
// 폴링은 살아 있었다(Q6 `PollCount 1573`) — **읽는 값이 안 움직인 것**이다.
|
||||||
|
// 수리(발주서 WL-813l2 §1-2):
|
||||||
|
// ① 레벨업 경로는 813k `GrowthEvents.LevelUp` **페이로드**(prev/new 스탯 3종 · prevLevel/newLevel ·
|
||||||
|
// statPointTotal)로 성장 전/후 전투력을 만든다 — 813y `StatPopup` 이 읽는 것과 **같은 값**이다.
|
||||||
|
// ② 산식 입력에 **미사용 스탯 포인트**를 넣는다. 원본 실측 환산(주스탯 1점 → `ATK +1`·`MaxHP +22` ·
|
||||||
|
// `MyValue.cs:335-336`)을 그대로 써서, 포인트를 찍어도 전투력이 튀지 않는다(잠재→실현 연속).
|
||||||
|
// 가중치·환산·레벨 기여는 전부 SO(`statPointWeight`·`statPointAtkGain`·`statPointHpGain`·`levelWeight`).
|
||||||
|
// ③ 장비/강화 경로는 그대로 폴링이다 — 실측으로 **폴링이 옳다**는 것을 확인했다(아래 항).
|
||||||
|
//
|
||||||
// ■ 🔴 장비/강화 변경 이벤트는 원본에 없다(실측)
|
// ■ 🔴 장비/강화 변경 이벤트는 원본에 없다(실측)
|
||||||
// `ServerClass.Set_Equip`(`Assets/Script/Server/ServerClass.cs:2329`)·`ActorStatInfo.Reset` 어디에도
|
// `ServerClass.Set_Equip`(`Assets/Script/Server/ServerClass.cs:2329`)·`ActorStatInfo.Reset` 어디에도
|
||||||
// 구독 가능한 신호가 없고, 원본 파일 수정은 금지다. → 발주서 §1-2 대안대로 **폴링**한다
|
// 구독 가능한 신호가 없고, 원본 파일 수정은 금지다. → 발주서 §1-2 대안대로 **폴링**한다
|
||||||
// (`pollIntervalSeconds` · 기본 0.5 s). 폴링 경로는 값 비교뿐이라 **할당 0**(GC 0) — 변화가 있을 때만 문자열이 난다.
|
// (`pollIntervalSeconds` · 기본 0.5 s). 폴링 경로는 값 비교뿐이라 **할당 0**(GC 0) — 변화가 있을 때만 문자열이 난다.
|
||||||
|
// 🔴 813l2 재실측 — **폴링이 실제로 잡는다**: 장비 착용·강화·각성·초월·유물·스탯 포인트 사용이 전부
|
||||||
|
// `MyValue.Get_ActorStatInfoorNull(eRole.PC)`(예: `UI/Eqiupment/EquipmentPopup_Obtain.cs:85` ·
|
||||||
|
// `EquipmentItemPopup.cs:77` · `EquipmentUI.cs:96` · `PCInfoStatMgrUI.cs:85`)를 부르고, 그게
|
||||||
|
// `MyValue.cs:122` 에서 **공유 캐시 인스턴스**(`dic_ActorStatInfo[(PC,"0",0)]`)에 `Reset()` 을 건다.
|
||||||
|
// 인게임 PC 도 `Actor.cs:322` 로 **같은 인스턴스**를 물고 있다 → 다음 폴링(≤0.5 s)에 값이 바뀐다.
|
||||||
//
|
//
|
||||||
// ■ C8 롤백 — `WLPowerDeltaSettings.enabled_ = 0`(또는 `RuntimeDisabled`) → 구독 0 · 폴링 0 · 노드 0 · 표시 0.
|
// ■ C8 롤백 — `WLPowerDeltaSettings.enabled_ = 0`(또는 `RuntimeDisabled`) → 구독 0 · 폴링 0 · 노드 0 · 표시 0.
|
||||||
//
|
//
|
||||||
|
|
@ -50,17 +73,38 @@ namespace WL.UI
|
||||||
public struct Sample
|
public struct Sample
|
||||||
{
|
{
|
||||||
public double atk, critRate, critDamage, aspd, hp, power;
|
public double atk, critRate, critDamage, aspd, hp, power;
|
||||||
|
/// <summary>WL-813l2 성장 입력 — 레벨(`SD_PC.Lv`)과 미사용 스탯 포인트(`SD_PC.Get_Point()`).</summary>
|
||||||
|
public double level, statPoints;
|
||||||
|
/// <summary>공격력이 1순위 스탯(BaseDmg)이 아니라 폴백(ATK)에서 왔는가 — 페이로드 환산 방식이 갈린다.</summary>
|
||||||
|
public bool atkFromFallback;
|
||||||
public bool valid;
|
public bool valid;
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return "atk=" + atk.ToString("F1") + " cri=" + critRate.ToString("F3") +
|
return "atk=" + atk.ToString("F1") + " cri=" + critRate.ToString("F3") +
|
||||||
" criDmg=" + critDamage.ToString("F3") + " aspd=" + aspd.ToString("F3") +
|
" criDmg=" + critDamage.ToString("F3") + " aspd=" + aspd.ToString("F3") +
|
||||||
" hp=" + hp.ToString("F1") + " → power=" + power.ToString("F1");
|
" hp=" + hp.ToString("F1") + " lv=" + level.ToString("F0") +
|
||||||
|
" pt=" + statPoints.ToString("F0") + " → power=" + power.ToString("F1");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>순수 함수 — 스탯 5개와 계수로 전투력을 만든다(Unity 객체 0 · 프로브가 그대로 검증한다).</summary>
|
/// <summary>순수 함수(성장 입력 없음) — 813l 원래 산식. 아래 8인자 판에 0/0 을 넘긴다.</summary>
|
||||||
public static double Compute(double atk, double critRate, double critDamage, double aspd, double hp, WLPowerDeltaSettings s)
|
public static double Compute(double atk, double critRate, double critDamage, double aspd, double hp, WLPowerDeltaSettings s)
|
||||||
|
{
|
||||||
|
return Compute(atk, critRate, critDamage, aspd, hp, 0d, 0d, s);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 순수 함수 — 스탯 5개 + 성장 입력 2개(레벨 · 미사용 스탯 포인트)와 계수로 전투력을 만든다
|
||||||
|
/// (Unity 객체 0 · 프로브가 그대로 검증한다).
|
||||||
|
///
|
||||||
|
/// 🔴 WL-813l2 — 미사용 스탯 포인트를 넣는 이유는 **원본이 레벨업으로 스탯을 안 올리기 때문**이다
|
||||||
|
/// (실측 근거는 파일 머리 주석). 포인트를 「아직 안 쓴 힘」으로 보고 원본 실측 환산
|
||||||
|
/// (주스탯 1점 → `ATK +1` · `MaxHP +22` · `MyValue.cs:335-336`)으로 산식에 그대로 넣으면,
|
||||||
|
/// 포인트를 실제로 찍는 순간(포인트 −1 · 스탯 +1/+22) 전투력이 **튀지 않는다**(잠재→실현 연속).
|
||||||
|
/// `statPointWeight = 0` 이면 813l 원래 산식과 완전히 같다(C8).
|
||||||
|
/// </summary>
|
||||||
|
public static double Compute(double atk, double critRate, double critDamage, double aspd, double hp,
|
||||||
|
double level, double statPoints, WLPowerDeltaSettings s)
|
||||||
{
|
{
|
||||||
if (s == null) return 0d;
|
if (s == null) return 0d;
|
||||||
double crit = s.useCritDamage ? critRate * critDamage : critRate;
|
double crit = s.useCritDamage ? critRate * critDamage : critRate;
|
||||||
|
|
@ -68,27 +112,55 @@ namespace WL.UI
|
||||||
double aspdTerm = 1d + s.attackSpeedWeight * (aspd - s.attackSpeedBase);
|
double aspdTerm = 1d + s.attackSpeedWeight * (aspd - s.attackSpeedBase);
|
||||||
if (critTerm < 0d) critTerm = 0d;
|
if (critTerm < 0d) critTerm = 0d;
|
||||||
if (aspdTerm < 0d) aspdTerm = 0d;
|
if (aspdTerm < 0d) aspdTerm = 0d;
|
||||||
double p = atk * critTerm * aspdTerm + hp * s.hpWeight;
|
double pts = statPoints > 0d ? statPoints * s.statPointWeight : 0d;
|
||||||
|
if (pts < 0d) pts = 0d;
|
||||||
|
double p = (atk + pts * s.statPointAtkGain) * critTerm * aspdTerm
|
||||||
|
+ (hp + pts * s.statPointHpGain) * s.hpWeight;
|
||||||
|
double lv = level - s.levelBase;
|
||||||
|
if (lv > 0d) p += lv * s.levelWeight;
|
||||||
return p * s.powerScale;
|
return p * s.powerScale;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>원본 스탯 묶음에서 전투력을 읽는다(쓰기 0). 스탯이 없으면 false.</summary>
|
/// <summary>원본 스탯 묶음에서 전투력을 읽는다(쓰기 0). 스탯이 없으면 false. 성장 입력은 0 = 스탯만.</summary>
|
||||||
public static bool TryRead(ActorStatInfo stat, WLPowerDeltaSettings s, out Sample o)
|
public static bool TryRead(ActorStatInfo stat, WLPowerDeltaSettings s, out Sample o)
|
||||||
{
|
{
|
||||||
o = default(Sample);
|
o = default(Sample);
|
||||||
if (stat == null || s == null) return false;
|
if (stat == null || s == null) return false;
|
||||||
double atk = stat.Get_Stat(s.atkStat);
|
double atk = stat.Get_Stat(s.atkStat);
|
||||||
if (atk <= 0d) atk = stat.Get_Stat(s.atkFallbackStat);
|
if (atk <= 0d) { atk = stat.Get_Stat(s.atkFallbackStat); o.atkFromFallback = true; }
|
||||||
o.atk = atk;
|
o.atk = atk;
|
||||||
o.critRate = stat.Get_Stat(s.critRateStat);
|
o.critRate = stat.Get_Stat(s.critRateStat);
|
||||||
o.critDamage = s.useCritDamage ? stat.Get_Stat(s.critDamageStat) : 0d;
|
o.critDamage = s.useCritDamage ? stat.Get_Stat(s.critDamageStat) : 0d;
|
||||||
o.aspd = stat.Get_Stat(s.attackSpeedStat);
|
o.aspd = stat.Get_Stat(s.attackSpeedStat);
|
||||||
o.hp = stat.Get_Stat(s.hpStat);
|
o.hp = stat.Get_Stat(s.hpStat);
|
||||||
o.power = Compute(o.atk, o.critRate, o.critDamage, o.aspd, o.hp, s);
|
o.power = Compute(o.atk, o.critRate, o.critDamage, o.aspd, o.hp, o.level, o.statPoints, s);
|
||||||
o.valid = true;
|
o.valid = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// WL-813l2 — 서버 데이터에서 성장 입력(레벨 · 미사용 스탯 포인트)을 채우고 전투력을 다시 만든다.
|
||||||
|
/// 🔴 **할당 0**: 원본 `SD_PC.Get_Point()`(`ServerClass.cs:2769-2770`)는 LINQ(`Select`+`Sum`)라
|
||||||
|
/// 폴링마다 가비지가 난다 → **같은 식을 for 루프로 다시 센다**(값 동일 · GC 0).
|
||||||
|
/// 「레벨당 5포인트」는 코드 상수가 아니라 SO(`statPointsPerLevel` · 원본 실측 5)다.
|
||||||
|
/// </summary>
|
||||||
|
public static void ApplyGrowth(SD_PC pc, WLPowerDeltaSettings s, ref Sample o)
|
||||||
|
{
|
||||||
|
if (s == null) return;
|
||||||
|
if (pc != null && s.readGrowthFromServerData)
|
||||||
|
{
|
||||||
|
int lv = (int)pc.Lv;
|
||||||
|
o.level = lv;
|
||||||
|
int used = 0;
|
||||||
|
var lst = pc.Stat;
|
||||||
|
// ObscuredInt 는 int 와 양방향 암시 변환이 있어 연산자가 모호해진다 → 명시 캐스트로 고정.
|
||||||
|
if (lst != null) for (int i = 0; i < lst.Count; i++) used += (int)lst[i];
|
||||||
|
int pts = (lv - 1) * s.statPointsPerLevel - used;
|
||||||
|
o.statPoints = pts > 0 ? pts : 0;
|
||||||
|
}
|
||||||
|
o.power = Compute(o.atk, o.critRate, o.critDamage, o.aspd, o.hp, o.level, o.statPoints, s);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>메인 PC 의 현재 전투력. PC 나 스탯이 없으면 false(로비·에디트 모드).</summary>
|
/// <summary>메인 PC 의 현재 전투력. PC 나 스탯이 없으면 false(로비·에디트 모드).</summary>
|
||||||
public static bool TryReadMainPc(out Sample o)
|
public static bool TryReadMainPc(out Sample o)
|
||||||
{
|
{
|
||||||
|
|
@ -98,7 +170,12 @@ namespace WL.UI
|
||||||
if (!MyValue.bMyPC) return false;
|
if (!MyValue.bMyPC) return false;
|
||||||
var pc = MyValue.MyPC;
|
var pc = MyValue.MyPC;
|
||||||
if (pc == null) return false;
|
if (pc == null) return false;
|
||||||
return TryRead(pc.Get_StatInfo(), s, out o);
|
if (!TryRead(pc.Get_StatInfo(), s, out o)) return false;
|
||||||
|
// 🔴 `ServerInfo.Ins` 는 게터가 GameObject 를 새로 만든다(부작용 · `Server/ServerInfo.cs:19`) →
|
||||||
|
// 쓰지 않는다. PC 가 이미 물고 있는 `ServerData` 만 읽는다(`Actor.cs:987` · 읽기 전용).
|
||||||
|
var sd = pc.Get_ServerData();
|
||||||
|
ApplyGrowth(sd != null ? sd.PC : null, s, ref o);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>증가율(%) — prev 가 0 이하면 0(나눗셈 방지).</summary>
|
/// <summary>증가율(%) — prev 가 0 이하면 0(나눗셈 방지).</summary>
|
||||||
|
|
@ -154,8 +231,10 @@ namespace WL.UI
|
||||||
private static float s_lastAnnounceAt = -999f;
|
private static float s_lastAnnounceAt = -999f;
|
||||||
private static int s_suppressToastFrame = -1; // 레벨업 프레임 — 팝업이 전투력 줄을 이미 보여 준다
|
private static int s_suppressToastFrame = -1; // 레벨업 프레임 — 팝업이 전투력 줄을 이미 보여 준다
|
||||||
|
|
||||||
// 레벨업 프레임의 「성장 직전 전투력」 — 813y StatPopup 이 어떤 순서로 구독됐든 같은 값을 보게 한다.
|
// 레벨업 프레임의 「성장 직전/직후 전투력」 — 813y StatPopup 이 어떤 순서로 구독됐든 같은 값을 보게 한다.
|
||||||
|
// 🔴 813l2: 직후 값도 못박는다 — 팝업이 그때 `ActorStatInfo` 를 다시 읽으면 레벨업 분이 안 보인다(D-28).
|
||||||
private static double s_growthPrevPower;
|
private static double s_growthPrevPower;
|
||||||
|
private static double s_growthNewPower;
|
||||||
private static int s_growthPrevFrame = -1;
|
private static int s_growthPrevFrame = -1;
|
||||||
|
|
||||||
// ── 처치 타수 계측 ───────────────────────────────────────────────────
|
// ── 처치 타수 계측 ───────────────────────────────────────────────────
|
||||||
|
|
@ -173,6 +252,11 @@ namespace WL.UI
|
||||||
public static int PollCount, ObserveCount, SkippedSmall, SkippedDown, MergedByCooldown, SuppressedByPopup;
|
public static int PollCount, ObserveCount, SkippedSmall, SkippedDown, MergedByCooldown, SuppressedByPopup;
|
||||||
public static string LastText = "";
|
public static string LastText = "";
|
||||||
public static string LastCompareText = "";
|
public static string LastCompareText = "";
|
||||||
|
// WL-813l2 진단 — 레벨업 전투력이 어느 소스에서 왔는가(페이로드/스탯 재읽기/실패) · 그때의 prev·new.
|
||||||
|
public static string LastPowerSource = "";
|
||||||
|
public static double LastPayloadPrev, LastPayloadNew;
|
||||||
|
public static double LastLevelSeen, LastStatPointsSeen;
|
||||||
|
public static int PayloadUsed, PayloadMissed;
|
||||||
public static double LastPower { get { return s_lastPower; } }
|
public static double LastPower { get { return s_lastPower; } }
|
||||||
public static bool HasPower { get { return s_hasLastPower; } }
|
public static bool HasPower { get { return s_hasLastPower; } }
|
||||||
public static bool Subscribed { get { return s_subscribed; } }
|
public static bool Subscribed { get { return s_subscribed; } }
|
||||||
|
|
@ -384,7 +468,12 @@ namespace WL.UI
|
||||||
s_nextPoll = now + Mathf.Max(0.05f, s.pollIntervalSeconds);
|
s_nextPoll = now + Mathf.Max(0.05f, s.pollIntervalSeconds);
|
||||||
PollCount++;
|
PollCount++;
|
||||||
WLPowerMeter.Sample sm;
|
WLPowerMeter.Sample sm;
|
||||||
if (WLPowerMeter.TryReadMainPc(out sm)) Observe(sm.power, now, "poll");
|
if (WLPowerMeter.TryReadMainPc(out sm))
|
||||||
|
{
|
||||||
|
// 진단용 대입뿐 — 문자열·박싱 0(GC 0 유지).
|
||||||
|
LastLevelSeen = sm.level; LastStatPointsSeen = sm.statPoints;
|
||||||
|
Observe(sm.power, now, "poll");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ② 하한·쿨다운으로 미뤄 둔 변화 마감(기준값과 마지막 관측값이 다르면 다시 본다)
|
// ② 하한·쿨다운으로 미뤄 둔 변화 마감(기준값과 마지막 관측값이 다르면 다시 본다)
|
||||||
|
|
@ -472,19 +561,107 @@ namespace WL.UI
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 813k 레벨업 ──────────────────────────────────────────────────────
|
// ── 813k 레벨업 (🔴 WL-813l2 = D-28 수리 지점) ───────────────────────
|
||||||
private static void OnLevelUp(in PlayerLevelUpEvent e)
|
private static void OnLevelUp(in PlayerLevelUpEvent e)
|
||||||
{
|
{
|
||||||
if (!WLPowerDeltaSettings.Enabled) return;
|
if (!WLPowerDeltaSettings.Enabled) return;
|
||||||
LevelUpSeen++;
|
LevelUpSeen++;
|
||||||
|
var s = St;
|
||||||
float now = Time.unscaledTime;
|
float now = Time.unscaledTime;
|
||||||
// 팝업 줄이 읽을 「성장 직전 전투력」을 이 프레임에 못박는다(구독 순서 무관 · 아래 BuildStatPopupLine).
|
|
||||||
if (s_hasLastPower) { s_growthPrevPower = s_baseline; s_growthPrevFrame = Time.frameCount; }
|
|
||||||
s_suppressToastFrame = Time.frameCount; // 이 프레임의 전투력 토스트는 팝업 줄에 양보한다
|
s_suppressToastFrame = Time.frameCount; // 이 프레임의 전투력 토스트는 팝업 줄에 양보한다
|
||||||
// 레벨업은 「성장 이벤트」다 — 스탯이 그대로여도(원본 기본 동작 · 813k 보고 ①) 타수 창은 새로 연다.
|
// 레벨업은 「성장 이벤트」다 — 스탯이 그대로여도(원본 기본 동작 · 813k 보고 ①) 타수 창은 새로 연다.
|
||||||
MarkGrowth(now, "levelUp " + e.prevLevel + "→" + e.newLevel);
|
MarkGrowth(now, "levelUp " + e.prevLevel + "→" + e.newLevel);
|
||||||
WLPowerMeter.Sample sm;
|
|
||||||
if (WLPowerMeter.TryReadMainPc(out sm)) Observe(sm.power, now, "levelUp");
|
double prevP, newP;
|
||||||
|
string src;
|
||||||
|
if (s != null && s.useLevelUpPayload && TryPayloadPower(s, in e, out prevP, out newP))
|
||||||
|
{
|
||||||
|
PayloadUsed++;
|
||||||
|
src = "levelUp(813k 페이로드)";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 폴백 = 813l 원래 동작. 🔴 원본은 레벨업으로 스탯을 안 올리므로 여기로 떨어지면 값이 안 움직인다(D-28).
|
||||||
|
WLPowerMeter.Sample sm;
|
||||||
|
if (!WLPowerMeter.TryReadMainPc(out sm))
|
||||||
|
{
|
||||||
|
PayloadMissed++; LastPowerSource = "levelUp(스탯 읽기 실패)";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PayloadMissed++;
|
||||||
|
newP = sm.power;
|
||||||
|
prevP = s_hasLastPower ? s_baseline : newP;
|
||||||
|
src = "levelUp(스탯 재읽기)";
|
||||||
|
}
|
||||||
|
LastPowerSource = src; LastPayloadPrev = prevP; LastPayloadNew = newP;
|
||||||
|
LastLevelSeen = e.newLevel; LastStatPointsSeen = e.statPointTotal;
|
||||||
|
|
||||||
|
// 기준값이 아직 없으면 페이로드의 prev 로 연다 — 첫 레벨업이 「기준값 세우기」로 삼켜지지 않게.
|
||||||
|
if (!s_hasLastPower) { s_lastPower = prevP; s_baseline = prevP; s_hasLastPower = true; }
|
||||||
|
|
||||||
|
// 팝업 줄이 읽을 값을 이 프레임에 못박는다(813y StatPopup 과 구독 순서 무관 · 아래 BuildStatPopupLine).
|
||||||
|
// prev 는 「마지막으로 **표시한** 값」(s_baseline)이라 토스트와 이중 계상이 나지 않는다.
|
||||||
|
s_growthPrevPower = s_baseline;
|
||||||
|
s_growthNewPower = newP;
|
||||||
|
s_growthPrevFrame = Time.frameCount;
|
||||||
|
|
||||||
|
Observe(newP, now, src);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 🔴 WL-813l2 — 813k `LevelUp` 페이로드로 성장 전/후 전투력을 만든다(D-28 수리의 핵심).
|
||||||
|
/// 레벨업 순간 `ActorStatInfo` 를 다시 읽으면 **prev == new** 다(원본이 레벨업으로 스탯을 안 올린다).
|
||||||
|
/// 그래서 ① 스탯 3종은 페이로드의 `prevStatX`/`newStatX`(= 813y `StatPopup` 이 읽는 바로 그 값)
|
||||||
|
/// ② 레벨은 `prevLevel`/`newLevel` ③ 미사용 포인트는 `statPointTotal` 과 그 역산값을 쓴다.
|
||||||
|
/// </summary>
|
||||||
|
private static bool TryPayloadPower(WLPowerDeltaSettings s, in PlayerLevelUpEvent e, out double prevP, out double newP)
|
||||||
|
{
|
||||||
|
prevP = 0d; newP = 0d;
|
||||||
|
WLPowerMeter.Sample cur;
|
||||||
|
if (!WLPowerMeter.TryReadMainPc(out cur))
|
||||||
|
{
|
||||||
|
// 월드에 PC 가 없다(로비·오프라인 보상·에디트 모드) — 페이로드만으로 만든다.
|
||||||
|
cur = default(WLPowerMeter.Sample);
|
||||||
|
cur.aspd = s.attackSpeedBase; // 기준 배율 = 공속 기여 0
|
||||||
|
}
|
||||||
|
// 산식이 BaseDmg(1순위)를 쓰는 중인가 — 페이로드가 ATK 를 줄 때 환산 방식이 갈린다.
|
||||||
|
bool atkPrimary = cur.valid && !cur.atkFromFallback && cur.atk > 0d;
|
||||||
|
|
||||||
|
var nw = cur; var pv = cur;
|
||||||
|
ApplyPayloadStat(s, ref nw, e.statAKind, e.newStatA, e.newStatA, atkPrimary);
|
||||||
|
ApplyPayloadStat(s, ref nw, e.statBKind, e.newStatB, e.newStatB, atkPrimary);
|
||||||
|
ApplyPayloadStat(s, ref nw, e.statCKind, e.newStatC, e.newStatC, atkPrimary);
|
||||||
|
ApplyPayloadStat(s, ref pv, e.statAKind, e.prevStatA, e.newStatA, atkPrimary);
|
||||||
|
ApplyPayloadStat(s, ref pv, e.statBKind, e.prevStatB, e.newStatB, atkPrimary);
|
||||||
|
ApplyPayloadStat(s, ref pv, e.statCKind, e.prevStatC, e.newStatC, atkPrimary);
|
||||||
|
|
||||||
|
// 「레벨업 직전 미사용 포인트」 역산 — 원본 Get_Point() = (Lv−1)*5 − 사용분 이므로
|
||||||
|
// 사용분이 그대로면 오른 레벨 수 × 레벨당 포인트만큼 적었다.
|
||||||
|
int gained = e.newLevel - e.prevLevel; if (gained < 0) gained = 0;
|
||||||
|
int per = s.statPointsPerLevel > 0 ? s.statPointsPerLevel : 0;
|
||||||
|
int prevPts = e.statPointTotal - gained * per; if (prevPts < 0) prevPts = 0;
|
||||||
|
|
||||||
|
newP = WLPowerMeter.Compute(nw.atk, nw.critRate, nw.critDamage, nw.aspd, nw.hp, e.newLevel, e.statPointTotal, s);
|
||||||
|
prevP = WLPowerMeter.Compute(pv.atk, pv.critRate, pv.critDamage, pv.aspd, pv.hp, e.prevLevel, prevPts, s);
|
||||||
|
return newP > 0d || prevP > 0d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>페이로드 스탯 1개를 산식 입력에 얹는다. 산식에 없는 스탯(예 MaxMP)은 그냥 버린다.</summary>
|
||||||
|
private static void ApplyPayloadStat(WLPowerDeltaSettings s, ref WLPowerMeter.Sample o,
|
||||||
|
eStat kind, double v, double refNew, bool atkPrimary)
|
||||||
|
{
|
||||||
|
if (kind == s.hpStat) { o.hp = v; return; }
|
||||||
|
if (kind == s.critRateStat) { o.critRate = v; return; }
|
||||||
|
if (kind == s.critDamageStat) { if (s.useCritDamage) o.critDamage = v; return; }
|
||||||
|
if (kind == s.attackSpeedStat) { o.aspd = v; return; }
|
||||||
|
if (kind == s.atkStat) { o.atk = v; return; }
|
||||||
|
if (kind == s.atkFallbackStat)
|
||||||
|
{
|
||||||
|
// 산식이 BaseDmg(= ATK × ATK_Multiplier × DMG_Multiplier · 실측 MyValue.cs:384-386)를 쓰는 중이면
|
||||||
|
// 배율을 모르는 채 ATK 로 덮어쓸 수 없다 → 같은 비율로 환산한다.
|
||||||
|
if (!atkPrimary) o.atk = v;
|
||||||
|
else if (refNew > 0d) o.atk = o.atk * (v / refNew);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 처치 타수 계측 ───────────────────────────────────────────────────
|
// ── 처치 타수 계측 ───────────────────────────────────────────────────
|
||||||
|
|
@ -595,13 +772,20 @@ namespace WL.UI
|
||||||
{
|
{
|
||||||
var s = St;
|
var s = St;
|
||||||
if (s == null || !WLPowerDeltaSettings.Enabled || !s.statPopupLineEnabled) return null;
|
if (s == null || !WLPowerDeltaSettings.Enabled || !s.statPopupLineEnabled) return null;
|
||||||
WLPowerMeter.Sample sm;
|
// 이 프레임에 레벨업 스냅샷이 찍혔으면 **그 값만** 쓴다(PowerDeltaToast 가 먼저 구독됐든 StatPopup 이 먼저든 같다).
|
||||||
if (!WLPowerMeter.TryReadMainPc(out sm)) return null;
|
// 🔴 813l2: 여기서 `ActorStatInfo` 를 다시 읽으면 안 된다 — 원본은 레벨업으로 스탯을 안 올려서
|
||||||
double now = sm.power;
|
// prev == new 가 되고 팝업 줄이 「변화 없음」으로 굳는다(D-28 과 같은 뿌리).
|
||||||
|
bool stamped = s_growthPrevFrame == Time.frameCount;
|
||||||
|
double now;
|
||||||
|
if (stamped) now = s_growthNewPower;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
WLPowerMeter.Sample sm;
|
||||||
|
if (!WLPowerMeter.TryReadMainPc(out sm)) return null;
|
||||||
|
now = sm.power;
|
||||||
|
}
|
||||||
if (now <= 0d) return null;
|
if (now <= 0d) return null;
|
||||||
// 이 프레임에 레벨업 스냅샷이 찍혔으면 그것을 쓴다(PowerDeltaToast 가 먼저 구독됐든 StatPopup 이 먼저든 같은 값).
|
double prev = stamped ? s_growthPrevPower : (s_hasLastPower ? s_baseline : now);
|
||||||
double prev = s_growthPrevFrame == Time.frameCount ? s_growthPrevPower
|
|
||||||
: (s_hasLastPower ? s_baseline : now);
|
|
||||||
double pct = WLPowerMeter.PercentDelta(prev, now);
|
double pct = WLPowerMeter.PercentDelta(prev, now);
|
||||||
if (prev <= 0d || System.Math.Abs(pct) < 0.005d)
|
if (prev <= 0d || System.Math.Abs(pct) < 0.005d)
|
||||||
return DSUtil.Format(s.statPopupLineFormatNoChange, WLPowerMeter.Format(now));
|
return DSUtil.Format(s.statPopupLineFormatNoChange, WLPowerMeter.Format(now));
|
||||||
|
|
@ -617,8 +801,10 @@ namespace WL.UI
|
||||||
LevelUpSeen = PowerToastCount = CompareToastCount = ZoneClearedSeen = 0;
|
LevelUpSeen = PowerToastCount = CompareToastCount = ZoneClearedSeen = 0;
|
||||||
PollCount = ObserveCount = SkippedSmall = SkippedDown = MergedByCooldown = SuppressedByPopup = 0;
|
PollCount = ObserveCount = SkippedSmall = SkippedDown = MergedByCooldown = SuppressedByPopup = 0;
|
||||||
LastText = ""; LastCompareText = "";
|
LastText = ""; LastCompareText = "";
|
||||||
|
LastPowerSource = ""; LastPayloadPrev = LastPayloadNew = 0d;
|
||||||
|
LastLevelSeen = LastStatPointsSeen = 0d; PayloadUsed = PayloadMissed = 0;
|
||||||
s_lastPower = 0d; s_baseline = 0d; s_hasLastPower = false;
|
s_lastPower = 0d; s_baseline = 0d; s_hasLastPower = false;
|
||||||
s_growthPrevPower = 0d; s_growthPrevFrame = -1; s_suppressToastFrame = -1;
|
s_growthPrevPower = 0d; s_growthNewPower = 0d; s_growthPrevFrame = -1; s_suppressToastFrame = -1;
|
||||||
s_lastAnnounceAt = -999f; s_nextPoll = 0f;
|
s_lastAnnounceAt = -999f; s_nextPoll = 0f;
|
||||||
s_curHits = s_curAttacks = s_curKills = 0; s_windowStart = 0f;
|
s_curHits = s_curAttacks = s_curKills = 0; s_windowStart = 0f;
|
||||||
s_beforeRatio = 0f; s_hasBefore = false; s_armed = false; s_armFrom = s_armTo = 0; s_armedAt = 0f;
|
s_beforeRatio = 0f; s_hasBefore = false; s_armed = false; s_armFrom = s_armTo = 0; s_armedAt = 0f;
|
||||||
|
|
@ -647,6 +833,32 @@ namespace WL.UI
|
||||||
return "hits=" + s_curHits + " attacks=" + s_curAttacks + " kills=" + s_curKills;
|
return "hits=" + s_curHits + " attacks=" + s_curAttacks + " kills=" + s_curKills;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 프로브 전용(WL-813l2) — 813k `GrowthEvents.LevelUp` 을 **실제로 Dispatch** 한다(구독 경로 그대로).
|
||||||
|
/// 813y `StatPopup.RaiseFakeGrowthLevelUp` 은 스탯 3종이 고정이라 「prev ATK 100 → new 112」를 못 만든다.
|
||||||
|
/// </summary>
|
||||||
|
public static string RaiseFakeLevelUpForProbe(int prevLevel, int newLevel, int statPointTotal,
|
||||||
|
eStat kindA, double prevA, double newA,
|
||||||
|
eStat kindB, double prevB, double newB,
|
||||||
|
eStat kindC, double prevC, double newC)
|
||||||
|
{
|
||||||
|
var e = new PlayerLevelUpEvent
|
||||||
|
{
|
||||||
|
data = null, pc = null,
|
||||||
|
prevLevel = prevLevel, newLevel = newLevel,
|
||||||
|
levelsGained = Mathf.Max(1, newLevel - prevLevel),
|
||||||
|
statPointTotal = statPointTotal,
|
||||||
|
statAKind = kindA, prevStatA = prevA, newStatA = newA,
|
||||||
|
statBKind = kindB, prevStatB = prevB, newStatB = newB,
|
||||||
|
statCKind = kindC, prevStatC = prevC, newStatC = newC,
|
||||||
|
hpBefore = 0d, maxHp = 0d, mpBefore = 0d, maxMp = 0d,
|
||||||
|
position = Vector3.zero, time = Time.unscaledTime, frame = Time.frameCount,
|
||||||
|
};
|
||||||
|
GrowthEvents.LevelUp.Dispatch(in e);
|
||||||
|
return "dispatch GrowthEvents.LevelUp " + prevLevel + "→" + newLevel +
|
||||||
|
" pt=" + statPointTotal + " · 구독자=" + GrowthEvents.LevelUp.Count;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>프로브 전용 — 813p `RunEvents.ZoneCleared` 를 가짜로 흘린다.</summary>
|
/// <summary>프로브 전용 — 813p `RunEvents.ZoneCleared` 를 가짜로 흘린다.</summary>
|
||||||
public static string RaiseFakeZoneClearedForProbe(int zoneIndex)
|
public static string RaiseFakeZoneClearedForProbe(int zoneIndex)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,14 @@ MonoBehaviour:
|
||||||
hpStat: 10
|
hpStat: 10
|
||||||
hpWeight: 0.1
|
hpWeight: 0.1
|
||||||
powerScale: 1
|
powerScale: 1
|
||||||
|
useLevelUpPayload: 1
|
||||||
|
readGrowthFromServerData: 1
|
||||||
|
statPointWeight: 1
|
||||||
|
statPointAtkGain: 1
|
||||||
|
statPointHpGain: 22
|
||||||
|
statPointsPerLevel: 5
|
||||||
|
levelWeight: 0
|
||||||
|
levelBase: 1
|
||||||
toastEnabled: 1
|
toastEnabled: 1
|
||||||
hudParentPath: IngameUIs/WL_HUD
|
hudParentPath: IngameUIs/WL_HUD
|
||||||
toastCenterOffsetPx: 430
|
toastCenterOffsetPx: 430
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,40 @@ namespace WL.UI
|
||||||
[Tooltip("최종 전투력에 곱하는 표시 배율(자릿수 조정용). 1 = 산식 그대로.")]
|
[Tooltip("최종 전투력에 곱하는 표시 배율(자릿수 조정용). 1 = 산식 그대로.")]
|
||||||
public float powerScale = 1f;
|
public float powerScale = 1f;
|
||||||
|
|
||||||
|
// ── 성장 입력 (WL-813l2 · Q6 D-28) ────────────────────────────────────
|
||||||
|
// 🔴 실측: 원본은 **레벨업으로 스탯을 1도 올리지 않는다**.
|
||||||
|
// · `ActorStatInfo.Reset`(`Assets/Script/Util/MyValue.cs:286-453`)은 `SD_PC.Lv` 를 한 번도 읽지 않는다.
|
||||||
|
// · PC 스탯은 `table_classconfig.Get_PureStat`(`Table/PC/table_classconfig.cs:232-248`
|
||||||
|
// = 보유 직업 기본치 + **사용한** 스탯 포인트 `SD_PC.Get_Stat`) + 장비·각성·초월·유물에서만 온다.
|
||||||
|
// · 레벨업은 `SD_PC.Get_Point()`(`Server/ServerClass.cs:2770` = `(Lv−1)*5 − 사용분`)로
|
||||||
|
// **미사용 스탯 포인트만** 늘린다. 포인트를 찍는 곳은 `PCInfoStatMgrUI.cs:81` 수동 UI 하나뿐이다.
|
||||||
|
// → 그래서 813l 이 `ActorStatInfo` 만 보면 레벨 1→16 에도 전투력이 상수다(Q6 D-28 `LastPower 52` 불변).
|
||||||
|
// 발주서 WL-813l2 §1-2 대로 **813k `LevelUp` 페이로드(prev/new 스탯 · statPointTotal)** 를 입력으로 쓴다.
|
||||||
|
[Header("성장 입력 (WL-813l2 · D-28) — 🔴 원본은 레벨업으로 스탯을 안 올린다(위 주석 = 실측)")]
|
||||||
|
[Tooltip("레벨업 경로에서 813k GrowthEvents.LevelUp 페이로드(prev/new 스탯 · prevLevel/newLevel · statPointTotal)로 전투력을 만든다. 끄면 813l 원래 동작(레벨업 때 ActorStatInfo 재읽기 = D-28 재발).")]
|
||||||
|
public bool useLevelUpPayload = true;
|
||||||
|
|
||||||
|
[Tooltip("폴링 경로에서 SD_PC 의 레벨·미사용 스탯 포인트를 읽는다(장비/강화 갱신과 같은 주기). 끄면 성장 입력 0.")]
|
||||||
|
public bool readGrowthFromServerData = true;
|
||||||
|
|
||||||
|
[Tooltip("미사용 스탯 포인트를 얼마나 「이미 얻은 힘」으로 셀지(0~1). 1 = 찍은 것과 똑같이 센다 → 포인트를 실제로 찍어도 전투력이 튀지 않는다(잠재→실현 연속). 0 = 813l 원래 산식 그대로(= D-28 재발).")]
|
||||||
|
public float statPointWeight = 1f;
|
||||||
|
|
||||||
|
[Tooltip("스탯 포인트 1점의 공격력 환산. 원본 실측 = 1 (MyValue.cs:335/343/351 · 주스탯 1점 → ATK += 1).")]
|
||||||
|
public float statPointAtkGain = 1f;
|
||||||
|
|
||||||
|
[Tooltip("스탯 포인트 1점의 체력 환산. 원본 실측 = 22 (MyValue.cs:336 · STR 1점 → MaxHP += 22). DEX/INT 주스탯 직업은 이 몫이 없다 = 「추정」.")]
|
||||||
|
public float statPointHpGain = 22f;
|
||||||
|
|
||||||
|
[Tooltip("레벨 1당 주는 스탯 포인트. 원본 실측 = 5 (ServerClass.cs:2770 Get_Point() = (Lv−1)*5 − 사용분). 페이로드에서 「레벨업 직전 포인트」를 역산할 때도 쓴다.")]
|
||||||
|
public int statPointsPerLevel = 5;
|
||||||
|
|
||||||
|
[Tooltip("레벨 1당 전투력 기여(스탯·포인트와 별개의 보정). 기본 0 = 끔 — 켜면 포인트를 다 쓴 뒤에도 레벨만으로 전투력이 오른다.")]
|
||||||
|
public float levelWeight = 0f;
|
||||||
|
|
||||||
|
[Tooltip("레벨 기여의 기준 레벨(이 레벨을 빼서 증가분을 만든다). 원본 시작 레벨 1.")]
|
||||||
|
public int levelBase = 1;
|
||||||
|
|
||||||
// ── 전투력 % 토스트 ───────────────────────────────────────────────────
|
// ── 전투력 % 토스트 ───────────────────────────────────────────────────
|
||||||
[Header("전투력 % 토스트")]
|
[Header("전투력 % 토스트")]
|
||||||
[Tooltip("끄면 토스트만 표시 안 함(팝업 줄·타수 비교는 그대로).")]
|
[Tooltip("끄면 토스트만 표시 안 함(팝업 줄·타수 비교는 그대로).")]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue