Merge branch 'wl/gameplay/WL-811s-effect-scale-pass'

This commit is contained in:
깃 관리자 2026-09-10 01:15:50 +09:00
commit a3a5aedb55
11 changed files with 1948 additions and 36 deletions

View File

@ -0,0 +1,201 @@
=== WL-811s MEASURE · 2026-09-10 01:01:55 (에디트 모드 · Play 0) ===
[A] 액터 높이 (프리팹 렌더러 바운드 · 원 크기 → × 테이블 배율)
kind name rawH(m) rawD(m) tableScale 실높이(m) 실지름(m)
PC Ai01 (10101/10102/10501/10502) 1.702 1.305 0.70 1.191 0.913
PC Ai02 1.611 1.305 0.70 1.127 0.913
PC Eri01 1.689 1.305 0.70 1.182 0.913
존1 Mob/Batty_A 0.358 0.624 1.50 0.537 0.936
존1 Mob/Batty_D 0.358 0.624 1.50 0.537 0.936
존2 Mob/Porin_A 0.491 0.526 1.30 0.639 0.684
존2 Mob/Porin_E 0.539 0.526 1.30 0.701 0.684
존3 Mob/Racco_A 0.582 0.762 1.10 0.640 0.838
존3 Mob/Rabby_Brown 0.385 0.545 1.50 0.577 0.817
존4 Mob/Devilu_A 0.556 0.704 1.20 0.668 0.845
존4 Mob/Devilu_C 0.612 0.704 1.20 0.734 0.845
엘리트1 Elite/Aspethis_D 0.691 0.559 2.30 1.590 1.286
엘리트2 Elite/Cute_Crab_C_Blue 0.376 0.617 2.50 0.941 1.542
엘리트3 Elite/Beeto_A 0.526 0.596 3.00 1.579 1.787
엘리트4 Elite/Mummy_King 0.591 0.401 2.30 1.360 0.921
보스 FieldBoss/Anubis 2.085 1.771 1.50 3.128 2.656
엘리트 배수(WLEliteSettings.scaleMultiplier) = 1.30 → 위 「엘리트*」 실높이 × 1.30
PC 기준 높이 = 1.191 m
[B] 이펙트 원 크기 (프리팹 localScale 포함 · Simulate t 스윕 · 파티클 실좌표 GetParticles)
prefab baseScale ps pMax diaP90(m) diaMax(m) hgt(m) note
Effect_Berserk 1.000 4 19 1.430 3.084 3.340
Effect_PCDash 1.000 4 10 2.036 3.076 2.726
Effect_Howling 1.000 11 20 5.702 10.632 6.765
Effect_Casting_Energyball 1.000 6 15 2.143 2.541 3.824
Effect_Casting_ManaShield 1.000 12 38 2.650 5.000 6.683
Effect_Casting_Meteor 1.000 7 93 1.802 2.747 2.607
Effect_Slash_10101_1 1.000 8 20 3.890 7.790 7.200
Effect_Slash_10101_2 1.000 8 20 5.332 8.108 7.200
Effect_Slash_10101_3 1.000 5 20 14.741 20.928 9.628
Effect_WLSwingArc 1.000 5 15 4.801 5.103 2.808
Effect_Slash_10102_1 1.000 6 20 3.874 6.582 3.704
Effect_Slash_10102_2 1.000 6 20 4.431 4.533 5.912
Effect_SwordShield 1.000 1 1 10.200 15.095 10.200
Effect_STR_EXTRA_SPLASH 1.000 7 20 10.680 20.865 10.221
Effect_Slash_10501_1 1.000 6 12 5.333 6.062 4.520
Effect_Slash_10501_2 1.000 6 12 5.653 6.461 4.522
Effect_Slash_10501_3 1.000 5 20 14.741 20.928 9.628
Effect_Heal 1.000 8 18 2.171 3.512 2.802
Effect_Slash_10502_1 1.000 5 10 4.828 5.472 4.364
Effect_Slash_10502_2 1.000 5 10 5.195 5.641 4.364
Effect_Aggro 1.000 1 3 0.454 0.454 0.454
ShineBulletFlash 1.000 4 11 2.157 5.000 5.000
Effect_Hit_10101 1.000 5 20 3.530 5.216 2.277
Effect_Hit_10102 1.000 6 5 1.150 1.492 1.492
Effect_Hit_10501 1.000 6 20 8.826 20.865 7.670
Effect_Hit_SweepAttack 1.000 6 10 1.961 2.847 2.847
Effect_Hit_Skill_EnergySlashFlash 1.000 8 37 4.925 16.424 16.424
Effect_Hit_Energyball 1.000 4 6 2.705 6.517 6.517
WindBullet_Hit 1.000 5 30 2.840 5.084 5.638
WaveShotHit 1.000 5 100 1.134 3.013 3.000
ShineBulletHit 1.000 5 11 2.700 7.000 7.000
Effect_Hit_Skill_CrossSlash 1.000 5 17 0.638 1.199 1.147
Effect_Hit_Skill_SpinSlash 1.000 3 6 0.463 0.648 0.647
Effect_Stun 1.000 5 10 5.776 6.840 6.802
Effect_Poison 1.000 5 41 1.866 3.130 2.095
Effect_Burn 1.000 10 20 3.151 5.000 5.000
Effect_Blind 1.000 0 0 0.000 0.000 0.000 nops,noparticle
Effect_Curse 1.000 7 30 4.000 5.363 4.989
Effect_Slow 1.000 9 7 3.000 5.011 5.884
Effect_Freezing 1.000 7 24 4.549 4.950 5.515
Effect_Frost 1.000 9 20 7.595 20.865 7.670
Effect_HPBurn 1.000 3 12 3.436 4.998 4.998
Effect_ManaBurn 1.000 4 50 2.355 3.915 3.666
Effect_SilenceCurse 1.000 9 13 3.351 4.936 4.777
Effect_Blood 1.000 5 59 7.199 20.299 7.487
Effect_DefenceDecrease 1.000 4 4 4.226 4.893 8.215
Effect_Barrier 1.000 4 2 22.000 22.000 22.000
Effect_ManaShield 1.000 9 14 1.395 20.000 20.000
Effect_Cure 1.000 4 20 5.249 10.433 4.532
Effect_SpeedUp 1.000 10 7 3.000 5.011 6.857
Effect_StatUp 1.000 4 20 2.231 2.248 8.763
Effect_Reincarnation 1.000 28 40 3.372 4.909 5.948
Effect_VampirHeal 1.000 4 25 8.000 20.299 3.180
Effect_Hit_10201 1.000 8 10 4.421 5.486 5.486
Effect_Hit_10202 1.000 3 10 1.327 1.329 1.329
Effect_Hit_10301 1.000 3 3 1.345 1.505 2.591
Effect_Hit_10302 1.500 5 16 2.167 2.970 2.580
Effect_Hit_10303 1.000 8 22 1.519 2.431 2.552
Effect_Hit_10401 1.000 3 16 1.136 1.726 0.830
Effect_Hit_10402 1.000 4 16 1.818 2.766 1.374
Effect_Hit_10502 1.000 6 10 2.847 3.254 3.087
Effect_Hit_10601 1.000 7 15 2.488 3.425 3.355
Effect_Hit_IceBolt 1.000 7 21 2.961 7.000 7.000
Effect_Hit_Fireball 1.000 12 35 3.503 5.395 7.106
Effect_Hit_Firebolt 1.000 8 15 1.933 2.500 2.500
Effect_Hit_Electronic 1.000 7 15 1.919 2.500 2.504
Effect_Hit_Bead 1.000 7 30 1.205 2.400 1.594
Effect_Hit_BlackHole 1.000 9 20 10.757 22.952 8.449
Effect_Hit_AnubisSkill1 1.000 3 15 1.484 2.047 2.207
Effect_Hit_AnubisSkill2 1.000 9 10 1.587 2.774 2.774
Effect_Hit_BrotherPenguin_Attack 1.000 6 9 2.112 3.421 4.070
Effect_Hit_FlowerDryad_Attack 1.000 4 37 0.933 1.253 1.206
Effect_Hit_IceGolem_Attack 1.000 4 8 1.013 1.497 3.694
Effect_Hit_Shaga_Attack 1.000 5 20 1.385 1.887 2.531
Effect_Hit_Mob_HellfireFlames 1.000 8 20 1.991 2.500 2.500
Effect_Hit_RookieArcherSkill1 1.000 9 11 1.800 3.821 3.185
Effect_Hit_SummonGolem 1.000 6 12 1.163 2.100 2.100
Effect_Hit_SummonWorm 1.000 6 10 1.164 1.750 2.531
Effect_Hit_Skill_DaggerThrow 1.000 5 37 2.590 5.761 8.645
Effect_Hit_Skill_Frozen 1.000 7 20 6.000 20.299 3.229
[C] 카메라 (Main Camera.prefab fov 60.0 · RealCamera yaw 347.21 pan 0.00 · 811o 오버라이드 h 76.0 d 3.00 up 2.40)
camPos(타겟 원점 기준) = (-2.120, 4.585, -4.211) · 거리 6.576 m · 내림각 44.2°
세로 1080×1920 (aspect 0.5625) · 1 m 구가 PC 몸 중심(y=0.60)에 있을 때 세로 점유 = 19.4 %
[C2] 현재 16행 슬롯 4 본체의 화면 세로 점유 (현재 scale · 앵커 Mid = PC 몸 중심)
skill prefab scale dia0(m) dia*(m) 세로점유%
702006 Effect_WLSwingArc 0.58 4.801 2.770 53.7
702009 Effect_STR_EXTRA_SPLASH 0.23 10.680 2.435 47.2
702005 Effect_Slash_10501_3 0.16 14.741 2.344 45.5
702012 ShineBulletFlash 1.00 2.157 2.150 41.7
콘솔 error/exception = 0
=== WL-811s VERIFY · 2026-09-10 01:03:02 (에디트 모드 · Play 0) ===
PASS SO 로드 · spectacle=True effectScale=True rules=57
[G] 새 에셋 guid → 경로(충돌 0 확인)
PASS guid 8115a0c3 · Assets/WL/Combat/Reaction/WLEffectScaleSettings.cs
PASS guid 8115b1d4 · Assets/WL/Combat/Reaction/EffectScaleGuard.cs
PASS guid 8115c2e5 · Assets/WL/Combat/Reaction/EffectScaleGuardRunner.cs
PASS guid 8115d3f6 · Assets/WL/Combat/Settings/Resources/WL/WLEffectScaleSettings.asset
[D] 화면 점유 (fov 60 · 카메라 거리 6.58 m · 1080×1920)
세로 50 % 가 되는 지름 = 2.577 m (= 슬롯 4 본체 캡)
[D2] 16행 전/후 (scale · dia0 = 프리팹 원 지름 실측 · after = dia0 × new)
skill slot label phase prefab dia0 before after 지름before 지름after 세로%before 세로%after
703101 1 연속 베기 body Effect_Slash_10101_1 3.890 1.200 0.459 4.668 1.786 90.6 34.6
703101 1 연속 베기 impact Effect_Hit_10101 3.530 1.000 0.270 3.530 0.953 68.5 18.5
703102 2 회심의 일격 charge Effect_Berserk 1.430 1.000 0.999 1.430 1.429 26.0 25.9
703102 2 회심의 일격 body Effect_Slash_10101_2 5.332 1.300 0.335 6.931 1.786 134.5 34.6
703102 2 회심의 일격 impact Effect_Hit_10101 3.530 1.200 0.270 4.236 0.953 82.2 18.5
703103 3 돌진 베기 charge Effect_PCDash 2.036 1.000 0.702 2.036 1.429 37.0 26.0
703103 3 돌진 베기 body Effect_Slash_10101_3 14.741 1.400 0.121 20.638 1.784 867.8 34.6
703103 3 돌진 베기 impact Effect_Hit_SweepAttack 1.961 1.000 0.486 1.961 0.953 38.0 18.5
702006 4 돌개 바람 charge Effect_Howling 5.702 1.200 0.251 6.842 1.431 124.2 26.0
702006 4 돌개 바람 body Effect_WLSwingArc 4.801 1.500 0.483 7.202 2.319 139.7 45.0
702006 4 돌개 바람 impact WindBullet_Hit 2.840 1.200 0.335 3.408 0.951 66.1 18.5
703106 1 무자비한 일격 body Effect_Slash_10102_1 3.874 1.200 0.461 4.649 1.786 90.2 34.6
703106 1 무자비한 일격 impact Effect_Hit_10102 1.150 1.000 0.829 1.150 0.953 22.3 18.5
703105 2 검기 분출 charge Effect_Casting_Energyball 2.143 1.000 0.667 2.143 1.430 38.9 26.0
703105 2 검기 분출 body Effect_Slash_10102_2 4.431 1.300 0.403 5.761 1.786 111.8 34.6
703105 2 검기 분출 impact Effect_Hit_Skill_EnergySlashFlash 4.925 1.000 0.193 4.925 0.950 95.5 18.4
703104 3 칼날 방패 charge Effect_Casting_ManaShield 2.650 1.000 0.539 2.650 1.428 48.1 25.9
703104 3 칼날 방패 body Effect_SwordShield 10.200 1.350 0.175 13.770 1.785 329.6 34.6
702009 4 마나 폭발 charge Effect_Casting_Meteor 1.802 1.200 0.793 2.163 1.429 39.3 26.0
702009 4 마나 폭발 body Effect_STR_EXTRA_SPLASH 10.680 1.600 0.217 17.088 2.318 499.3 45.0
702009 4 마나 폭발 impact Effect_Hit_Energyball 2.705 1.200 0.352 3.247 0.952 63.0 18.5
703502 1 방패 치기 body Effect_Slash_10501_1 5.333 1.200 0.335 6.400 1.787 124.2 34.7
703502 1 방패 치기 impact Effect_Hit_10501 8.826 1.000 0.108 8.826 0.953 801.3 18.5
703503 2 강력한 일격 charge Effect_PCDash 2.036 1.000 0.702 2.036 1.429 37.0 26.0
703503 2 강력한 일격 body Effect_Slash_10501_2 5.653 1.400 0.316 7.915 1.786 143.7 32.4
703503 2 강력한 일격 impact Effect_Hit_SweepAttack 1.961 1.000 0.486 1.961 0.953 38.0 18.5
703501 3 간식 먹기 body Effect_Heal 2.171 1.300 0.823 2.823 1.787 54.8 34.7
702005 4 휘몰아치는 파도 charge Effect_Howling 5.702 1.200 0.251 6.842 1.431 124.2 26.0
702005 4 휘몰아치는 파도 body Effect_Slash_10501_3 14.741 1.800 0.157 26.535 2.314 1269.4 44.9
702005 4 휘몰아치는 파도 impact WaveShotHit 1.134 1.200 0.840 1.361 0.953 26.4 18.5
703505 1 십자 가르기 body Effect_Slash_10502_1 4.828 1.200 0.370 5.793 1.786 112.4 34.7
703505 1 십자 가르기 impact Effect_Hit_Skill_CrossSlash 0.638 1.000 1.000 0.638 0.638 12.4 12.4
703506 2 회전 베기 body Effect_Slash_10502_2 5.195 1.500 0.344 7.793 1.787 151.2 34.7
703506 2 회전 베기 impact Effect_Hit_Skill_SpinSlash 0.463 1.000 1.000 0.463 0.463 9.0 9.0
703504 3 도발 charge Effect_Howling 5.702 1.000 0.251 5.702 1.431 103.5 26.0
703504 3 도발 body Effect_Aggro 0.454 1.300 1.300 0.590 0.590 11.4 11.4
702012 4 심판의 빛 charge Effect_Casting_Meteor 1.802 1.200 0.793 2.163 1.429 39.3 26.0
702012 4 심판의 빛 body ShineBulletFlash 2.157 1.500 1.075 3.235 2.319 62.8 45.0
702012 4 심판의 빛 impact ShineBulletHit 2.700 1.200 0.353 3.240 0.953 62.9 18.5
[D3] 슬롯 4 본체 캡 판정 (≤ 50 %)
PASS 슬롯4 702006 Effect_WLSwingArc · 지름 2.319 m · 세로 45.0 %
PASS 슬롯4 702009 Effect_STR_EXTRA_SPLASH · 지름 2.318 m · 세로 45.0 %
PASS 슬롯4 702005 Effect_Slash_10501_3 · 지름 2.314 m · 세로 44.9 %
PASS 슬롯4 702012 ShineBulletFlash · 지름 2.319 m · 세로 45.0 %
[E] EffectScaleGuard (가짜 풀 루트 = InGameInfo.tf_Effects · 진짜 이펙트 프리팹 인스턴스)
PASS 규칙 매칭 · Effect_Stun → rule 0 (scale 0.124) · Effect_AnubisCharging → -1(없음 = 원본)
PASS ① 활성 인스턴스 배율 적용 · Effect_Stun 1.000 → 0.1240 (기대 0.1240)
PASS ② 규칙 없는 것은 무변경 · Effect_AnubisCharging 1.0000 (원본 1.0000)
PASS ③ 비활성은 아직 미적용 · Effect_Poison 1.0000
PASS ④ 멱등(3회 더 틱) · scale 0.124000 · Applied 2 → 2
PASS ⑤ 반납(SetActive false) → 원 스케일 복귀 · 1.0000 (원본 1.0000)
PASS ⑥ 재활성 → 다시 적용 · 0.1240
PASS ⑦ 나중에 활성된 인스턴스도 적용 · Effect_Poison → 0.3830
[E2] scaleByTargetHeight (대상 액터 실높이로 재계산)
PASS 리플렉션 필드 존재 · Actor=True Actor.tf_Top=True TurnOff_GO.m_target2=True
PASS Effect_Stun 에 TurnOff_GO 존재 · ok
PASS ⑧ 대상 높이 기반 재계산 · 몹 높이 0.537 m × 0.6 ÷ 5.776 = 0.0558 → scale 0.0558 · 지름 0.322 m · HeightResolved 1
PASS ⑨ 대상 없으면 고정 배율로 폴백 · scale 0.1240 · HeightMissed 0 → 1
[F] GC · C8
PASS ⑩ GC 0 (Tick ×200,000 · 워밍 후) · GetAllocatedBytesForCurrentThread Δ 0 B · MonoUsed Δ 0 B
PASS ⑪ C8 (enabled = 0) → 전량 원복 · stun 1.0000 · hit 1.0000 · ActiveScaled 0
PASS ⑫ C8 상태에서 무동작 · Applied 6 → 6
카운터: Sweep 200012 · Tracked 4 · Ruled 3 · Applied 6 · Restored 6 · Skipped 0
RESULT PASS 24/24 · 콘솔 error/exception = 0

View File

@ -0,0 +1,667 @@
// PD 지시 #813 · 발주서 WL-811s — 이펙트 크기 1차 조정 검증 프로브 (에디트 모드 전용 · Play 0 · 로그인 0 · 에셋 무수정)
//
// 실행 (CLAUDE.md §1: using 금지 · 네임스페이스 풀어 쓰기)
// unity command run_script --file AgentScripts/WL811s_Probe.cs --entry WL811s_Probe.Measure --timeout 1800
// unity command run_script --file AgentScripts/WL811s_Probe.cs --entry WL811s_Probe.Verify --timeout 1800
//
// Measure — 값을 정하기 위한 실측(코드 수정 전)
// A. 액터 높이 : PC 프리팹 렌더러 바운드 × ClassConfig.f_Scale · 존 몹 × MonsterList.f_DefaultScale ·
// 엘리트 × WLEliteSettings.scaleMultiplier · 보스 Anubis
// B. 이펙트 원 크기 : 프리팹 인스턴스화 → ParticleSystem.Simulate(t, withChildren, restart) 를 t 스윕하며
// 모든 렌더러 월드 바운드 합집합의 최대 수평 지름/높이 (= 프리팹 자체 localScale 포함한 「원 크기」)
// C. 화면 점유 : Main Camera 프리팹 fov + RealCamera 궤도식(Quaternion.Euler(h,yaw,pan) * Vector3.one)에
// 811o 오버라이드(h 76 · d 3 · up 2.4)를 넣어 카메라를 세우고 1080×1920 뷰포트에서 세로 점유율
//
// Verify — 코드/값을 넣은 뒤의 검증
// D. 16행 전/후 표 · 슬롯 4 화면 점유 ≤ 50 %
// E. EffectScaleGuard : 가짜 풀 루트에 진짜 이펙트 인스턴스를 넣고 활성/비활성 → 배율 적용·원복·멱등 · 규칙 매칭
// F. GC 0 · C8(enabled=0 → 무동작 · 스케일 원본)
//
// 에디트 모드 주의: 프로브가 만드는 오브젝트는 전부 HideAndDontSave + DestroyImmediate · 어떤 에셋도 쓰지 않는다.
public static class WL811s_Probe
{
const string kOutDir = "AgentScripts/staging/WL811s";
const string kMeasureOut = "AgentScripts/staging/WL811s/MEASURE.txt";
const string kVerifyOut = "AgentScripts/staging/WL811s/VERIFY.txt";
const string kPcPrefab = "Assets/Res_Addr/PC/Ai01.prefab";
const string kCamPrefab = "Assets/ResWork/Prefabs/Ingame/Main Camera.prefab";
const string kEffectDir = "Assets/Res_Addr/Effect/";
static System.Text.StringBuilder s_sb = new System.Text.StringBuilder(1 << 16);
static void L(string s) { s_sb.Append(s).Append('\n'); }
static System.Collections.Generic.List<string> s_err = new System.Collections.Generic.List<string>();
static bool s_hooked;
static void OnLog(string cond, string stack, UnityEngine.LogType t)
{
if (t == UnityEngine.LogType.Error || t == UnityEngine.LogType.Exception || t == UnityEngine.LogType.Assert)
s_err.Add(t + " | " + cond);
}
static void Hook() { if (!s_hooked) { UnityEngine.Application.logMessageReceived += OnLog; s_hooked = true; } }
static void Unhook() { if (s_hooked) { UnityEngine.Application.logMessageReceived -= OnLog; s_hooked = false; } }
static void Save(string path)
{
System.IO.Directory.CreateDirectory(kOutDir);
System.IO.File.WriteAllText(path, s_sb.ToString(), new System.Text.UTF8Encoding(false));
}
// ─────────────────────────────────────────────────────────────── 공용 측정
/// <summary>프리팹을 숨은 인스턴스로 띄운다(원 localScale 유지 · 원점 · 무회전).</summary>
static UnityEngine.GameObject Spawn(string path)
{
var pf = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>(path);
if (pf == null) return null;
var go = UnityEngine.Object.Instantiate(pf);
go.hideFlags = UnityEngine.HideFlags.HideAndDontSave;
go.transform.position = UnityEngine.Vector3.zero;
go.transform.rotation = UnityEngine.Quaternion.identity;
go.transform.localScale = pf.transform.localScale;
return go;
}
/// <summary>활성 여부와 무관하게 모든 렌더러의 월드 바운드 합집합. 없으면 false.</summary>
static bool RendererBounds(UnityEngine.GameObject go, out UnityEngine.Bounds b, out int count)
{
b = new UnityEngine.Bounds();
count = 0;
var rs = go.GetComponentsInChildren<UnityEngine.Renderer>(true);
for (int i = 0; i < rs.Length; i++)
{
var r = rs[i];
if (r == null) continue;
var rb = r.bounds;
if (rb.size.sqrMagnitude <= 0.0000001f) continue;
if (count == 0) b = rb; else b.Encapsulate(rb);
count++;
}
return count > 0;
}
static UnityEngine.ParticleSystem.Particle[] s_buf = new UnityEngine.ParticleSystem.Particle[8192];
static System.Collections.Generic.List<float> s_radii = new System.Collections.Generic.List<float>(1 << 14);
/// <summary>파티클 실좌표(GetParticles)로 원점 기준 반경 분포를 만든다. 렌더러 바운드(=Unity 보수 컬링 바운드)는 쓰지 않는다.
/// diaMax = 전 파티클 최대 지름 · diaP90 = 90 백분위 지름(스트레이 1개에 흔들리지 않는 「덩치」) · hgt = y 폭.</summary>
static void MeasureEffect(string prefabName, out float diaMax, out float diaP90, out float hgt, out float baseScale, out int psCount, out int pCount, out string note)
{
diaMax = 0f; diaP90 = 0f; hgt = 0f; baseScale = 1f; psCount = 0; pCount = 0; note = "";
var go = Spawn(kEffectDir + prefabName + ".prefab");
if (go == null) { note = "noprefab"; return; }
try
{
baseScale = go.transform.localScale.x;
var pss = go.GetComponentsInChildren<UnityEngine.ParticleSystem>(true);
psCount = pss.Length;
// 🔴 재현성: 자동 랜덤 시드를 끄고 고정 시드를 준다(안 하면 같은 프리팹이 실행마다 ±16 % 다르게 나온다).
for (int i = 0; i < pss.Length; i++)
{
var ps = pss[i]; if (ps == null) continue;
try { ps.useAutoRandomSeed = false; ps.randomSeed = 8115u; } catch (System.Exception) { }
}
go.SetActive(true);
s_radii.Clear();
float yMin = 1e9f, yMax = -1e9f;
// 파티클이 아닌 메시(예: Effect_WLSwingArc 의 슬래시 메시)는 실 바운드가 정확하다
var mrs = go.GetComponentsInChildren<UnityEngine.MeshRenderer>(true);
for (int i = 0; i < mrs.Length; i++)
{
var mr = mrs[i];
if (mr == null || mr.GetComponent<UnityEngine.ParticleSystem>() != null) continue;
var mb = mr.bounds;
if (mb.size.sqrMagnitude <= 0.0000001f) continue;
float rr = new UnityEngine.Vector2(UnityEngine.Mathf.Abs(mb.center.x) + mb.extents.x, UnityEngine.Mathf.Abs(mb.center.z) + mb.extents.z).magnitude;
s_radii.Add(rr);
if (mb.min.y < yMin) yMin = mb.min.y;
if (mb.max.y > yMax) yMax = mb.max.y;
}
if (pss.Length > 0)
{
float[] ts = new float[] { 0.05f, 0.1f, 0.15f, 0.2f, 0.3f, 0.4f, 0.5f, 0.7f, 1.0f, 1.3f, 1.6f, 2.0f };
for (int k = 0; k < ts.Length; k++)
{
for (int i = 0; i < pss.Length; i++)
{
var ps = pss[i];
if (ps == null) continue;
if (ps.transform.parent != null && ps.transform.parent.GetComponentInParent<UnityEngine.ParticleSystem>() != null) continue;
try { ps.Simulate(ts[k], true, true, false); } catch (System.Exception) { }
}
for (int i = 0; i < pss.Length; i++)
{
var ps = pss[i];
if (ps == null) continue;
int n = 0;
try { n = ps.GetParticles(s_buf); } catch (System.Exception) { continue; }
if (n <= 0) continue;
if (n > pCount) pCount = n;
bool local = ps.main.simulationSpace == UnityEngine.ParticleSystemSimulationSpace.Local;
float ls = UnityEngine.Mathf.Abs(ps.transform.lossyScale.x); if (ls <= 0f) ls = 1f;
for (int q = 0; q < n; q++)
{
var p = s_buf[q];
var pos = local ? ps.transform.TransformPoint(p.position) : p.position;
float sz = p.GetCurrentSize(ps) * (local ? ls : 1f);
float rr = new UnityEngine.Vector2(pos.x, pos.z).magnitude + sz * 0.5f;
s_radii.Add(rr);
if (pos.y - sz * 0.5f < yMin) yMin = pos.y - sz * 0.5f;
if (pos.y + sz * 0.5f > yMax) yMax = pos.y + sz * 0.5f;
}
}
}
for (int i = 0; i < pss.Length; i++) { var ps = pss[i]; if (ps != null) { try { ps.Stop(true, UnityEngine.ParticleSystemStopBehavior.StopEmittingAndClear); } catch (System.Exception) { } } }
}
else note = "nops";
if (s_radii.Count == 0) { note = (note.Length > 0 ? note + "," : "") + "noparticle"; return; }
s_radii.Sort();
diaMax = s_radii[s_radii.Count - 1] * 2f;
int idx = UnityEngine.Mathf.Clamp(UnityEngine.Mathf.RoundToInt((s_radii.Count - 1) * 0.90f), 0, s_radii.Count - 1);
diaP90 = s_radii[idx] * 2f;
hgt = (yMax > yMin) ? (yMax - yMin) : 0f;
}
finally { UnityEngine.Object.DestroyImmediate(go); }
}
// ─────────────────────────────────────────────────────────────── A. 액터 높이
class ActorRow { public string kind, name, path; public float tableScale; public float rawH, rawD; public bool ok; }
static System.Collections.Generic.List<ActorRow> MeasureActors()
{
var list = new System.Collections.Generic.List<ActorRow>();
// PC (ClassConfig.f_Scale = 0.7 · 4 근접 클래스 공통 · 프리팹은 코스튬 테이블이 고른다 → 기본 Ai01)
list.Add(new ActorRow { kind = "PC", name = "Ai01 (10101/10102/10501/10502)", path = kPcPrefab, tableScale = 0.7f });
list.Add(new ActorRow { kind = "PC", name = "Ai02", path = "Assets/Res_Addr/PC/Ai02.prefab", tableScale = 0.7f });
list.Add(new ActorRow { kind = "PC", name = "Eri01", path = "Assets/Res_Addr/PC/Eri01.prefab", tableScale = 0.7f });
// 존 몹 (813e 813001~813004)
AddMob(list, "존1", "Mob/Batty_A", 1.5f); AddMob(list, "존1", "Mob/Batty_D", 1.5f);
AddMob(list, "존2", "Mob/Porin_A", 1.3f); AddMob(list, "존2", "Mob/Porin_E", 1.3f);
AddMob(list, "존3", "Mob/Racco_A", 1.1f); AddMob(list, "존3", "Mob/Rabby_Brown", 1.5f);
AddMob(list, "존4", "Mob/Devilu_A", 1.2f); AddMob(list, "존4", "Mob/Devilu_C", 1.2f);
// 엘리트 (813s · × scaleMultiplier)
AddMob(list, "엘리트1", "Elite/Aspethis_D", 2.3f); AddMob(list, "엘리트2", "Elite/Cute_Crab_C_Blue", 2.5f);
AddMob(list, "엘리트3", "Elite/Beeto_A", 3.0f); AddMob(list, "엘리트4", "Elite/Mummy_King", 2.3f);
// 보스
AddMob(list, "보스", "FieldBoss/Anubis", 1.5f);
for (int i = 0; i < list.Count; i++)
{
var r = list[i];
var go = Spawn(r.path);
if (go == null) { r.ok = false; continue; }
try
{
go.transform.localScale = UnityEngine.Vector3.one; // 원 크기(테이블 배율 전) 를 잰다
UnityEngine.Bounds b; int c;
if (RendererBounds(go, out b, out c)) { r.rawH = b.size.y; r.rawD = UnityEngine.Mathf.Max(b.size.x, b.size.z); r.ok = true; }
}
finally { UnityEngine.Object.DestroyImmediate(go); }
}
return list;
}
static void AddMob(System.Collections.Generic.List<ActorRow> l, string kind, string rel, float sc)
{
l.Add(new ActorRow { kind = kind, name = rel, path = "Assets/Res_Addr/Mobs/" + rel + ".prefab", tableScale = sc });
}
// ─────────────────────────────────────────────────────────────── C. 카메라 · 화면 점유
class CamInfo { public float fov, h, dist, up, yaw, pan; public UnityEngine.Vector3 camPos; public UnityEngine.Vector3 lookAt; }
static CamInfo BuildCam()
{
var ci = new CamInfo { fov = 60f, h = 76f, dist = 3f, up = 2.4f, yaw = 347.21f, pan = 0f };
var cam = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>(kCamPrefab);
if (cam != null)
{
var c = cam.GetComponent<UnityEngine.Camera>();
if (c != null) ci.fov = c.fieldOfView;
var rc = cam.GetComponent<RealCamera>();
if (rc != null)
{
var t = typeof(RealCamera);
var fy = t.GetField("rotateAround", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (fy != null) ci.yaw = (float)fy.GetValue(rc);
var fp = t.GetField("cameraPan", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (fp != null) ci.pan = (float)fp.GetValue(rc);
}
}
var st = WL.CameraFraming.WLCameraFramingSettings.Instance;
if (st != null && st.framingEnabled) { ci.h = st.cameraHeight; ci.dist = st.distance; ci.up = st.distanceUp; }
// RealCamera 궤도식 그대로: rotateVector = Quaternion.Euler(h, yaw, pan) * Vector3.one
var rot = UnityEngine.Quaternion.Euler(ci.h, ci.yaw, ci.pan);
var rv = rot * UnityEngine.Vector3.one;
ci.camPos = UnityEngine.Vector3.up * ci.up - rv * ci.dist; // 타겟 = 원점
ci.lookAt = UnityEngine.Vector3.zero;
return ci;
}
/// <summary>원점(PC 발밑)에 지름 dia · 높이 hgt 의 구/원기둥이 있을 때 세로 화면 점유율(0~1).</summary>
static float ScreenFraction(CamInfo ci, float dia, float hgt, float anchorY)
{
var go = new UnityEngine.GameObject("__wl811s_cam");
go.hideFlags = UnityEngine.HideFlags.HideAndDontSave;
try
{
var cam = go.AddComponent<UnityEngine.Camera>();
cam.enabled = false;
cam.fieldOfView = ci.fov;
cam.aspect = 1080f / 1920f;
cam.nearClipPlane = 0.1f; cam.farClipPlane = 1000f;
go.transform.position = ci.camPos;
go.transform.LookAt(ci.lookAt);
float r = dia * 0.5f;
float yc = anchorY;
float minY = 1e9f, maxY = -1e9f;
for (int i = 0; i < 8; i++)
{
float a = i * (UnityEngine.Mathf.PI * 2f / 8f);
float x = UnityEngine.Mathf.Cos(a) * r, z = UnityEngine.Mathf.Sin(a) * r;
for (int k = 0; k < 2; k++)
{
float y = yc + (k == 0 ? -hgt * 0.5f : hgt * 0.5f);
var vp = cam.WorldToViewportPoint(new UnityEngine.Vector3(x, y, z));
if (vp.z <= 0f) continue;
if (vp.y < minY) minY = vp.y;
if (vp.y > maxY) maxY = vp.y;
}
}
if (maxY < minY) return 0f;
return maxY - minY;
}
finally { UnityEngine.Object.DestroyImmediate(go); }
}
// ─────────────────────────────────────────────────────────────── Measure 엔트리
public static string Measure()
{
s_sb.Length = 0; s_err.Clear(); Hook();
L("=== WL-811s MEASURE · " + System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " (에디트 모드 · Play 0) ===");
// A
L("");
L("[A] 액터 높이 (프리팹 렌더러 바운드 · 원 크기 → × 테이블 배율)");
L("kind\tname\trawH(m)\trawD(m)\ttableScale\t실높이(m)\t실지름(m)");
var actors = MeasureActors();
float pcH = 0f;
for (int i = 0; i < actors.Count; i++)
{
var r = actors[i];
if (!r.ok) { L(r.kind + "\t" + r.name + "\tMISSING"); continue; }
float h = r.rawH * r.tableScale, d = r.rawD * r.tableScale;
L(r.kind + "\t" + r.name + "\t" + r.rawH.ToString("F3") + "\t" + r.rawD.ToString("F3") + "\t" + r.tableScale.ToString("F2")
+ "\t" + h.ToString("F3") + "\t" + d.ToString("F3"));
if (r.kind == "PC" && pcH <= 0f) pcH = h;
}
float eliteMul = 1.3f;
var est = WL.Combat.Reaction.WLEliteSettings.Instance;
if (est != null) eliteMul = est.scaleMultiplier;
L("엘리트 배수(WLEliteSettings.scaleMultiplier) = " + eliteMul.ToString("F2") + " → 위 「엘리트*」 실높이 × " + eliteMul.ToString("F2"));
L("PC 기준 높이 = " + pcH.ToString("F3") + " m");
// B
L("");
L("[B] 이펙트 원 크기 (프리팹 localScale 포함 · Simulate t 스윕 · 파티클 실좌표 GetParticles)");
L("prefab\tbaseScale\tps\tpMax\tdiaP90(m)\tdiaMax(m)\thgt(m)\tnote");
string[] names = EffectNames();
var dict = new System.Collections.Generic.Dictionary<string, float>();
for (int i = 0; i < names.Length; i++)
{
float diaMax, diaP90, hgt, bs; int ps, pc; string note;
MeasureEffect(names[i], out diaMax, out diaP90, out hgt, out bs, out ps, out pc, out note);
dict[names[i]] = diaP90;
L(names[i] + "\t" + bs.ToString("F3") + "\t" + ps + "\t" + pc + "\t" + diaP90.ToString("F3") + "\t" + diaMax.ToString("F3") + "\t" + hgt.ToString("F3") + "\t" + note);
}
// C
L("");
var ci = BuildCam();
L("[C] 카메라 (Main Camera.prefab fov " + ci.fov.ToString("F1") + " · RealCamera yaw " + ci.yaw.ToString("F2") + " pan " + ci.pan.ToString("F2")
+ " · 811o 오버라이드 h " + ci.h.ToString("F1") + " d " + ci.dist.ToString("F2") + " up " + ci.up.ToString("F2") + ")");
L("camPos(타겟 원점 기준) = " + ci.camPos.ToString("F3") + " · 거리 " + ci.camPos.magnitude.ToString("F3")
+ " m · 내림각 " + (UnityEngine.Mathf.Asin(UnityEngine.Mathf.Clamp(ci.camPos.y / UnityEngine.Mathf.Max(0.0001f, ci.camPos.magnitude), -1f, 1f)) * UnityEngine.Mathf.Rad2Deg).ToString("F1") + "°");
L("세로 1080×1920 (aspect " + (1080f / 1920f).ToString("F4") + ") · 1 m 구가 PC 몸 중심(y=" + (pcH * 0.5f).ToString("F2") + ")에 있을 때 세로 점유 = "
+ (ScreenFraction(ci, 1f, 1f, pcH * 0.5f) * 100f).ToString("F1") + " %");
L("");
L("[C2] 현재 16행 슬롯 4 본체의 화면 세로 점유 (현재 scale · 앵커 Mid = PC 몸 중심)");
L("skill\tprefab\tscale\tdia0(m)\tdia*(m)\t세로점유%");
var so = WL.Combat.Reaction.WLSkillSpectacleSettings.Instance;
if (so != null && so.rows != null)
{
for (int i = 0; i < so.rows.Length; i++)
{
var row = so.rows[i];
if (row.slot != 4) continue;
string p = row.body.prefab;
float d0 = 0f; dict.TryGetValue(p, out d0);
float sc = row.body.scale > 0f ? row.body.scale : 1f;
float d = d0 * sc;
L(row.skillId + "\t" + p + "\t" + sc.ToString("F2") + "\t" + d0.ToString("F3") + "\t" + d.ToString("F3") + "\t"
+ (ScreenFraction(ci, d, d, pcH * 0.5f) * 100f).ToString("F1"));
}
}
else L("WLSkillSpectacleSettings 로드 실패");
L("");
L("콘솔 error/exception = " + s_err.Count);
for (int i = 0; i < s_err.Count && i < 10; i++) L(" " + s_err[i]);
Unhook();
Save(kMeasureOut);
return "MEASURE done · rows=" + actors.Count + " effects=" + names.Length + " err=" + s_err.Count + " → " + kMeasureOut;
}
// ─────────────────────────────────────────────────────────────── Verify 엔트리
// 발주 전 16행 값(main @ 22832ff5d 실측) — (skillId, charge, body, impact) · 1 = 그 단 없음
static float[][] Before()
{
return new float[][] {
new float[]{703101,-1f,1.2f,1.0f}, new float[]{703102,1.0f,1.3f,1.2f},
new float[]{703103,1.0f,1.4f,1.0f}, new float[]{702006,1.2f,1.5f,1.2f},
new float[]{703106,-1f,1.2f,1.0f}, new float[]{703105,1.0f,1.3f,1.0f},
new float[]{703104,1.0f,1.35f,-1f}, new float[]{702009,1.2f,1.6f,1.2f},
new float[]{703502,-1f,1.2f,1.0f}, new float[]{703503,1.0f,1.4f,1.0f},
new float[]{703501,-1f,1.3f,-1f}, new float[]{702005,1.2f,1.8f,1.2f},
new float[]{703505,-1f,1.2f,1.0f}, new float[]{703506,-1f,1.5f,1.0f},
new float[]{703504,1.0f,1.3f,-1f}, new float[]{702012,1.2f,1.5f,1.2f},
};
}
static UnityEngine.GameObject s_fakeInfoGo, s_poolRoot;
static InGameInfo s_prevInfo;
static void PoolBegin()
{
s_prevInfo = InGameInfo.Ins;
s_fakeInfoGo = new UnityEngine.GameObject("__wl811s_info");
s_fakeInfoGo.hideFlags = UnityEngine.HideFlags.HideAndDontSave;
s_poolRoot = new UnityEngine.GameObject("__wl811s_pool");
s_poolRoot.hideFlags = UnityEngine.HideFlags.HideAndDontSave;
var info = s_fakeInfoGo.AddComponent<InGameInfo>(); // 에디트 모드라 Awake 미실행 = 부작용 0
info.tf_Effects = s_poolRoot.transform;
InGameInfo.Ins = info;
}
static void PoolEnd()
{
InGameInfo.Ins = s_prevInfo;
if (s_poolRoot != null) UnityEngine.Object.DestroyImmediate(s_poolRoot);
if (s_fakeInfoGo != null) UnityEngine.Object.DestroyImmediate(s_fakeInfoGo);
s_poolRoot = null; s_fakeInfoGo = null;
}
static UnityEngine.GameObject PoolChild(string prefabName, bool active)
{
var pf = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>(kEffectDir + prefabName + ".prefab");
if (pf == null) return null;
var go = UnityEngine.Object.Instantiate(pf, s_poolRoot.transform);
go.name = prefabName + "(Clone)"; // 원본 풀과 같은 이름 규약
go.hideFlags = UnityEngine.HideFlags.HideAndDontSave;
go.transform.localScale = pf.transform.localScale;
go.SetActive(active);
return go;
}
static float s_tick;
static void Tick() { s_tick += 1f; WL.Combat.Reaction.EffectScaleGuard.Tick(s_tick); }
static int s_pass, s_fail;
static void Chk(string name, bool ok, string detail)
{
if (ok) s_pass++; else s_fail++;
L((ok ? "PASS " : "🔴FAIL ") + name + " · " + detail);
}
static bool Near(float a, float b, float eps) { return UnityEngine.Mathf.Abs(a - b) <= eps; }
public static string Verify()
{
s_sb.Length = 0; s_err.Clear(); s_pass = 0; s_fail = 0; Hook();
L("=== WL-811s VERIFY · " + System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " (에디트 모드 · Play 0) ===");
UnityEditor.AssetDatabase.Refresh(); // 디스크에서 손으로 고친 .asset 을 다시 읽는다
UnityEngine.Resources.UnloadUnusedAssets();
WL.Combat.Reaction.WLSkillSpectacleSettings.ClearCache();
WL.Combat.Reaction.WLEffectScaleSettings.ClearCache();
var sk = WL.Combat.Reaction.WLSkillSpectacleSettings.Instance;
var es = WL.Combat.Reaction.WLEffectScaleSettings.Instance;
Chk("SO 로드", sk != null && es != null, "spectacle=" + (sk != null) + " effectScale=" + (es != null) + " rules=" + (es != null && es.rules != null ? es.rules.Length : -1));
// G. guid
L("");
L("[G] 새 에셋 guid → 경로(충돌 0 확인)");
string[][] gp = new string[][] {
new string[]{"8115a0c3e7d24b6f9a1c8e5d3b7f2049","Assets/WL/Combat/Reaction/WLEffectScaleSettings.cs"},
new string[]{"8115b1d4f8e35c709b2d9f6e4c803150","Assets/WL/Combat/Reaction/EffectScaleGuard.cs"},
new string[]{"8115c2e509f46d81ac3ea07f5d914261","Assets/WL/Combat/Reaction/EffectScaleGuardRunner.cs"},
new string[]{"8115d3f61a057e92bd4fb1806ea25372","Assets/WL/Combat/Settings/Resources/WL/WLEffectScaleSettings.asset"},
};
for (int i = 0; i < gp.Length; i++)
{
string got = UnityEditor.AssetDatabase.GUIDToAssetPath(gp[i][0]);
Chk("guid " + gp[i][0].Substring(0, 8), got == gp[i][1], got);
}
// D. 16행 전/후 · 화면 점유
var ci = BuildCam();
float pcH = 1.191f;
// 세로 50 % 가 되는 지름(이분 탐색 · 원 산식 그대로)
float lo = 0.1f, hi = 20f;
for (int i = 0; i < 40; i++) { float mid = (lo + hi) * 0.5f; if (ScreenFraction(ci, mid, mid, pcH * 0.5f) < 0.5f) lo = mid; else hi = mid; }
float capDia = (lo + hi) * 0.5f;
L("");
L("[D] 화면 점유 (fov " + ci.fov.ToString("F0") + " · 카메라 거리 " + ci.camPos.magnitude.ToString("F2") + " m · 1080×1920)");
L("세로 50 % 가 되는 지름 = " + capDia.ToString("F3") + " m (= 슬롯 4 본체 캡)");
var before = Before();
L("");
L("[D2] 16행 전/후 (scale · dia0 = 프리팹 원 지름 실측 · after = dia0 × new)");
L("skill\tslot\tlabel\tphase\tprefab\tdia0\tbefore\tafter\t지름before\t지름after\t세로%before\t세로%after");
var dia = new System.Collections.Generic.Dictionary<string, float>();
for (int i = 0; sk != null && sk.rows != null && i < sk.rows.Length; i++)
{
var row = sk.rows[i];
float[] b = null;
for (int k = 0; k < before.Length; k++) if ((int)before[k][0] == row.skillId) { b = before[k]; break; }
for (int ph = 0; ph < 3; ph++)
{
var v = ph == 0 ? row.charge : (ph == 1 ? row.body : row.impact);
if (string.IsNullOrEmpty(v.prefab)) continue;
float d0;
if (!dia.TryGetValue(v.prefab, out d0))
{
float dm, dp, hg, bs; int ps, pc; string nt;
MeasureEffect(v.prefab, out dm, out dp, out hg, out bs, out ps, out pc, out nt);
d0 = dp; dia[v.prefab] = d0;
}
float ob = b != null ? b[ph + 1] : -1f;
float ns = v.scale > 0f ? v.scale : 1f;
float anchor = v.anchor == eEffectLocation.Bottom ? 0f : (v.anchor == eEffectLocation.Top ? pcH : pcH * 0.5f);
L(row.skillId + "\t" + row.slot + "\t" + row.label + "\t" + (ph == 0 ? "charge" : (ph == 1 ? "body" : "impact")) + "\t" + v.prefab
+ "\t" + d0.ToString("F3") + "\t" + (ob < 0f ? "?" : ob.ToString("F3")) + "\t" + ns.ToString("F3")
+ "\t" + (ob < 0f ? "?" : (d0 * ob).ToString("F3")) + "\t" + (d0 * ns).ToString("F3")
+ "\t" + (ob < 0f ? "?" : (ScreenFraction(ci, d0 * ob, d0 * ob, anchor) * 100f).ToString("F1"))
+ "\t" + (ScreenFraction(ci, d0 * ns, d0 * ns, anchor) * 100f).ToString("F1"));
}
}
L("");
L("[D3] 슬롯 4 본체 캡 판정 (≤ 50 %)");
for (int i = 0; sk != null && sk.rows != null && i < sk.rows.Length; i++)
{
var row = sk.rows[i];
if (row.slot != 4 || string.IsNullOrEmpty(row.body.prefab)) continue;
float d0 = dia.ContainsKey(row.body.prefab) ? dia[row.body.prefab] : 0f;
float d = d0 * (row.body.scale > 0f ? row.body.scale : 1f);
float f = ScreenFraction(ci, d, d, pcH * 0.5f);
Chk("슬롯4 " + row.skillId + " " + row.body.prefab, f <= 0.5f + 0.005f,
"지름 " + d.ToString("F3") + " m · 세로 " + (f * 100f).ToString("F1") + " %");
}
// E. 가드
L("");
L("[E] EffectScaleGuard (가짜 풀 루트 = InGameInfo.tf_Effects · 진짜 이펙트 프리팹 인스턴스)");
WL.Combat.Reaction.EffectScaleGuard.ResetDiagnostics();
WL.Combat.Reaction.WLEffectScaleSettings.RuntimeOverride = 1;
bool prevByHeight = es != null && es.scaleByTargetHeight;
if (es != null) es.scaleByTargetHeight = false;
PoolBegin();
try
{
var stun = PoolChild("Effect_Stun", true);
var hit = PoolChild("Effect_Hit_10101", true);
var none = PoolChild("Effect_AnubisCharging", true); // 보스 패턴 = 규칙 없음 = 원본 유지
var poison = PoolChild("Effect_Poison", false); // 비활성으로 시작
var origStun = stun.transform.localScale;
var origHit = hit.transform.localScale;
var origNone = none.transform.localScale;
var origPoison = poison.transform.localScale;
int rStun = es.FindRule("Effect_Stun(Clone)", WL.Combat.Reaction.WLEffectScaleSettings.CoreLength("Effect_Stun(Clone)"));
int rNone = es.FindRule("Effect_AnubisCharging(Clone)", WL.Combat.Reaction.WLEffectScaleSettings.CoreLength("Effect_AnubisCharging(Clone)"));
Chk("규칙 매칭", rStun >= 0 && rNone < 0, "Effect_Stun → rule " + rStun + " (scale " + (rStun >= 0 ? es.rules[rStun].scale.ToString("F3") : "-") + ") · Effect_AnubisCharging → " + rNone + "(없음 = 원본)");
Tick();
float fStun = es.rules[rStun].scale;
Chk("① 활성 인스턴스 배율 적용", Near(stun.transform.localScale.x, origStun.x * fStun, 0.0005f),
"Effect_Stun " + origStun.x.ToString("F3") + " → " + stun.transform.localScale.x.ToString("F4") + " (기대 " + (origStun.x * fStun).ToString("F4") + ")");
Chk("② 규칙 없는 것은 무변경", Near(none.transform.localScale.x, origNone.x, 0.0001f),
"Effect_AnubisCharging " + none.transform.localScale.x.ToString("F4") + " (원본 " + origNone.x.ToString("F4") + ")");
Chk("③ 비활성은 아직 미적용", Near(poison.transform.localScale.x, origPoison.x, 0.0001f), "Effect_Poison " + poison.transform.localScale.x.ToString("F4"));
int ap1 = WL.Combat.Reaction.EffectScaleGuard.AppliedCount;
var afterFirst = stun.transform.localScale;
Tick(); Tick(); Tick();
Chk("④ 멱등(3회 더 틱)", Near(stun.transform.localScale.x, afterFirst.x, 0.000001f) && WL.Combat.Reaction.EffectScaleGuard.AppliedCount == ap1,
"scale " + stun.transform.localScale.x.ToString("F6") + " · Applied " + ap1 + " → " + WL.Combat.Reaction.EffectScaleGuard.AppliedCount);
stun.SetActive(false); Tick();
Chk("⑤ 반납(SetActive false) → 원 스케일 복귀", Near(stun.transform.localScale.x, origStun.x, 0.0001f),
stun.transform.localScale.x.ToString("F4") + " (원본 " + origStun.x.ToString("F4") + ")");
stun.SetActive(true); Tick();
Chk("⑥ 재활성 → 다시 적용", Near(stun.transform.localScale.x, origStun.x * fStun, 0.0005f), stun.transform.localScale.x.ToString("F4"));
poison.SetActive(true); Tick();
int rP = es.FindRule("Effect_Poison(Clone)", WL.Combat.Reaction.WLEffectScaleSettings.CoreLength("Effect_Poison(Clone)"));
Chk("⑦ 나중에 활성된 인스턴스도 적용", rP >= 0 && Near(poison.transform.localScale.x, origPoison.x * es.rules[rP].scale, 0.0005f),
"Effect_Poison → " + poison.transform.localScale.x.ToString("F4"));
// 대상 높이 옵션
L("");
L("[E2] scaleByTargetHeight (대상 액터 실높이로 재계산)");
var mobPf = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>("Assets/Res_Addr/Mobs/Mob/Batty_A.prefab");
var mobGo = mobPf != null ? UnityEngine.Object.Instantiate(mobPf) : null;
if (mobGo != null)
{
mobGo.hideFlags = UnityEngine.HideFlags.HideAndDontSave;
mobGo.transform.position = UnityEngine.Vector3.zero;
var actor = mobGo.GetComponent<Actor>();
var fiTop = typeof(Actor).GetField("tf_Top", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
var fiT2 = typeof(TurnOff_GO).GetField("m_target2", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
Chk("리플렉션 필드 존재", actor != null && fiTop != null && fiT2 != null,
"Actor=" + (actor != null) + " Actor.tf_Top=" + (fiTop != null) + " TurnOff_GO.m_target2=" + (fiT2 != null));
if (actor != null && fiTop != null && fiT2 != null)
{
var topGo = new UnityEngine.GameObject("HUD_Top"); topGo.hideFlags = UnityEngine.HideFlags.HideAndDontSave;
topGo.transform.SetParent(mobGo.transform, false);
topGo.transform.position = new UnityEngine.Vector3(0f, 0.537f, 0f); // 존1 Batty 실높이(MEASURE [A])
fiTop.SetValue(actor, topGo.transform);
var off = stun.GetComponent<TurnOff_GO>();
Chk("Effect_Stun 에 TurnOff_GO 존재", off != null, off != null ? "ok" : "없음");
if (off != null)
{
fiT2.SetValue(off, actor);
es.scaleByTargetHeight = true;
stun.SetActive(false); Tick(); stun.SetActive(true); Tick();
float expect = UnityEngine.Mathf.Clamp((0.537f * es.rules[rStun].targetRatio) / es.rules[rStun].baseDiameter, es.minScale, es.maxScale);
Chk("⑧ 대상 높이 기반 재계산", Near(stun.transform.localScale.x, origStun.x * expect, 0.0005f),
"몹 높이 0.537 m × " + es.rules[rStun].targetRatio + " ÷ " + es.rules[rStun].baseDiameter + " = " + expect.ToString("F4")
+ " → scale " + stun.transform.localScale.x.ToString("F4") + " · 지름 " + (es.rules[rStun].baseDiameter * expect).ToString("F3") + " m"
+ " · HeightResolved " + WL.Combat.Reaction.EffectScaleGuard.HeightResolved);
fiT2.SetValue(off, null);
int miss0 = WL.Combat.Reaction.EffectScaleGuard.HeightMissed;
stun.SetActive(false); Tick(); stun.SetActive(true); Tick();
Chk("⑨ 대상 없으면 고정 배율로 폴백", Near(stun.transform.localScale.x, origStun.x * fStun, 0.0005f) && WL.Combat.Reaction.EffectScaleGuard.HeightMissed > miss0,
"scale " + stun.transform.localScale.x.ToString("F4") + " · HeightMissed " + miss0 + " → " + WL.Combat.Reaction.EffectScaleGuard.HeightMissed);
es.scaleByTargetHeight = false;
}
}
UnityEngine.Object.DestroyImmediate(mobGo);
}
else Chk("몹 프리팹 로드", false, "Batty_A 없음");
// F. GC
L("");
L("[F] GC · C8");
stun.SetActive(true); hit.SetActive(true); Tick();
long m0 = System.GC.GetAllocatedBytesForCurrentThread();
long mono0 = UnityEngine.Profiling.Profiler.GetMonoUsedSizeLong();
for (int i = 0; i < 200000; i++) Tick();
long dAlloc = System.GC.GetAllocatedBytesForCurrentThread() - m0;
long dMono = UnityEngine.Profiling.Profiler.GetMonoUsedSizeLong() - mono0;
Chk("⑩ GC 0 (Tick ×200,000 · 워밍 후)", dAlloc == 0, "GetAllocatedBytesForCurrentThread Δ " + dAlloc + " B · MonoUsed Δ " + dMono + " B");
// C8
WL.Combat.Reaction.WLEffectScaleSettings.RuntimeOverride = -1;
Tick();
Chk("⑪ C8 (enabled = 0) → 전량 원복", Near(stun.transform.localScale.x, origStun.x, 0.0001f) && Near(hit.transform.localScale.x, origHit.x, 0.0001f)
&& WL.Combat.Reaction.EffectScaleGuard.ActiveScaled == 0,
"stun " + stun.transform.localScale.x.ToString("F4") + " · hit " + hit.transform.localScale.x.ToString("F4") + " · ActiveScaled " + WL.Combat.Reaction.EffectScaleGuard.ActiveScaled);
int ap2 = WL.Combat.Reaction.EffectScaleGuard.AppliedCount;
Tick(); Tick();
Chk("⑫ C8 상태에서 무동작", WL.Combat.Reaction.EffectScaleGuard.AppliedCount == ap2, "Applied " + ap2 + " → " + WL.Combat.Reaction.EffectScaleGuard.AppliedCount);
L("카운터: Sweep " + WL.Combat.Reaction.EffectScaleGuard.SweepCount + " · Tracked " + WL.Combat.Reaction.EffectScaleGuard.TrackedCount
+ " · Ruled " + WL.Combat.Reaction.EffectScaleGuard.RuledCount + " · Applied " + WL.Combat.Reaction.EffectScaleGuard.AppliedCount
+ " · Restored " + WL.Combat.Reaction.EffectScaleGuard.RestoredCount + " · Skipped " + WL.Combat.Reaction.EffectScaleGuard.SkippedCount);
}
finally
{
WL.Combat.Reaction.WLEffectScaleSettings.RuntimeOverride = 0;
if (es != null) es.scaleByTargetHeight = prevByHeight;
WL.Combat.Reaction.EffectScaleGuard.ResetDiagnostics();
PoolEnd();
}
L("");
L("RESULT " + (s_fail == 0 ? "PASS" : "FAIL") + " " + s_pass + "/" + (s_pass + s_fail) + " · 콘솔 error/exception = " + s_err.Count);
for (int i = 0; i < s_err.Count && i < 10; i++) L(" " + s_err[i]);
Unhook();
Save(kVerifyOut);
return "VERIFY " + (s_fail == 0 ? "PASS" : "FAIL") + " " + s_pass + "/" + (s_pass + s_fail) + " err=" + s_err.Count + " → " + kVerifyOut;
}
static string[] EffectNames()
{
return new string[]
{
// 16행(811i 매핑) — 차지/본체/착탄
"Effect_Berserk","Effect_PCDash","Effect_Howling","Effect_Casting_Energyball","Effect_Casting_ManaShield","Effect_Casting_Meteor",
"Effect_Slash_10101_1","Effect_Slash_10101_2","Effect_Slash_10101_3","Effect_WLSwingArc",
"Effect_Slash_10102_1","Effect_Slash_10102_2","Effect_SwordShield","Effect_STR_EXTRA_SPLASH",
"Effect_Slash_10501_1","Effect_Slash_10501_2","Effect_Slash_10501_3","Effect_Heal",
"Effect_Slash_10502_1","Effect_Slash_10502_2","Effect_Aggro","ShineBulletFlash",
"Effect_Hit_10101","Effect_Hit_10102","Effect_Hit_10501","Effect_Hit_SweepAttack","Effect_Hit_Skill_EnergySlashFlash",
"Effect_Hit_Energyball","WindBullet_Hit","WaveShotHit","ShineBulletHit","Effect_Hit_Skill_CrossSlash","Effect_Hit_Skill_SpinSlash",
// 원본 풀 상태이상
"Effect_Stun","Effect_Poison","Effect_Burn","Effect_Blind","Effect_Curse","Effect_Slow","Effect_Freezing","Effect_Frost",
"Effect_HPBurn","Effect_ManaBurn","Effect_SilenceCurse","Effect_Blood","Effect_DefenceDecrease","Effect_Barrier","Effect_ManaShield",
"Effect_Cure","Effect_SpeedUp","Effect_StatUp","Effect_Reincarnation","Effect_VampirHeal",
// 원본 풀 히트(클래스 · 몹 · 보스)
"Effect_Hit_10201","Effect_Hit_10202","Effect_Hit_10301","Effect_Hit_10302","Effect_Hit_10303","Effect_Hit_10401","Effect_Hit_10402",
"Effect_Hit_10502","Effect_Hit_10601","Effect_Hit_IceBolt","Effect_Hit_Fireball","Effect_Hit_Firebolt","Effect_Hit_Electronic",
"Effect_Hit_Bead","Effect_Hit_BlackHole","Effect_Hit_AnubisSkill1","Effect_Hit_AnubisSkill2",
"Effect_Hit_BrotherPenguin_Attack","Effect_Hit_FlowerDryad_Attack","Effect_Hit_IceGolem_Attack","Effect_Hit_Shaga_Attack",
"Effect_Hit_Mob_HellfireFlames","Effect_Hit_RookieArcherSkill1","Effect_Hit_SummonGolem","Effect_Hit_SummonWorm",
"Effect_Hit_Skill_DaggerThrow","Effect_Hit_Skill_Frozen"
};
}
}

View File

@ -0,0 +1,336 @@
// ─────────────────────────────────────────────────────────────────────────────
// EffectScaleGuard.cs — 원본 풀이 스폰하는 상태이상·히트 이펙트의 크기 가드(PD 지시 #813 · 발주서 WL-811s §1-3)
//
// ■ 무엇을 하나
// 원본 `InGameInfo.Show_Effect` 가 쓰는 이펙트 풀(`dic_str_Effect` · 전부 `tf_Effects` 의 직속 자식)을
// LateUpdate 에서 훑어, **활성화된 인스턴스**에 규칙 배율을 적용한다.
// 적용 = `localScale = 원 스케일 × 배율` **대입**(누적 곱이 아니다 → 몇 번 불려도 같은 값 = 멱등)
// 원복 = 원본 `TurnOff_GO.Off_Imm` 이 `SetActive(false)` 로 반납하면 원 스케일 대입
//
// ■ 왜 스폰 훅이 아니라 풀 스윕인가 (813m3 가 같은 이유로 같은 방식을 썼다)
// `Effect_Stun`(Actor.cs:1793) · `Effect_Burn`(Skill_Golem.cs:54) · SkillTypeConfig 의 상태이상 ·
// `ProjectileBase`(:365) 의 히트 이펙트는 **전부 원본 코드가 스폰**한다. 원본은 1줄도 고칠 수 없으므로
// (발주서 §2) 스폰 지점이 아니라 **풀 루트**를 본다. WL 스폰 지점용 공개 API `Apply(GameObject)` 도 함께 둔다.
//
// ■ 왜 LateUpdate 인가
// 원본 스폰은 Update·코루틴에서 일어난다. LateUpdate 는 그 뒤 · 렌더 앞이라 **활성화된 그 프레임에** 적용된다
// (Update 에서 돌면 스폰 순서에 따라 1프레임 원본 크기로 그려질 수 있다 = 과대 이펙트가 한 번 번쩍인다).
//
// ■ GC 0
// `Transform.GetChild` · `GetInstanceID` · `activeSelf` · `Dictionary<int,int>` 조회는 전부 비할당.
// 유일한 할당 = **처음 보는 인스턴스 1개당 `GameObject.name` 1회**(풀은 인스턴스를 재사용하므로 워밍 후 0 B).
//
// ■ C8 롤백: `WLEffectScaleSettings.enabled = 0`(또는 RuntimeOverride < 0) → 다음 틱에 전량 원복 후 무동작.
//
// 🔴 원본 프리팹/머티리얼/셰이더 무수정 — 런타임 인스턴스의 localScale 만 만진다.
// 🔴 Debug.LogError/LogException 금지(813z).
// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것.
// ─────────────────────────────────────────────────────────────────────────────
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
namespace WL.Combat.Reaction
{
/// <summary>원본 이펙트 풀 인스턴스에 크기 배율을 걸고 반납 시 되돌린다. 정적 · 멱등 · GC 0.</summary>
public static class EffectScaleGuard
{
// ── 진단(프로브·QA 가 읽는다)
/// <summary>스윕 횟수 · 추적 중인 인스턴스 수 · 규칙이 붙은 인스턴스 수.</summary>
public static int SweepCount, TrackedCount, RuledCount;
/// <summary>배율을 건 횟수 · 되돌린 횟수 · 슬롯 부족으로 건너뛴 수.</summary>
public static int AppliedCount, RestoredCount, SkippedCount;
/// <summary>현재 배율이 걸려 있는 인스턴스 수.</summary>
public static int ActiveScaled;
/// <summary>대상 액터 높이로 배율을 다시 계산한 횟수 · 대상을 못 찾아 고정 배율로 떨어진 횟수.</summary>
public static int HeightResolved, HeightMissed;
/// <summary>마지막으로 배율을 건 이름·배율(표본).</summary>
public static string LastName = "";
public static float LastFactor;
// ── 추적 슬롯(사전 할당 · 병렬 배열)
static int[] s_id;
static Transform[] s_tf;
static Vector3[] s_orig;
static float[] s_factor; // 0 = 규칙 없음(손대지 않는다)
static int[] s_rule; // 1 = 규칙 없음
static bool[] s_applied;
static TurnOff_GO[] s_off;
static bool[] s_offResolved;
static int s_count;
static int s_capacity;
static readonly Dictionary<int, int> s_map = new Dictionary<int, int>(512);
static float s_nextSweep;
static bool s_sceneHooked;
// 원본 private 필드 리플렉션(대상 높이 옵션 전용 · 캐시 1회 · 참조형이라 박싱 없음)
static FieldInfo s_fiTarget2, s_fiTfTop;
static bool s_reflectDone;
static WLEffectScaleSettings St { get { return WLEffectScaleSettings.Instance; } }
// ───────────────────────────────────────── 틱
/// <summary>러너가 LateUpdate 마다 부른다. C8·주기 판정은 여기서 한다.</summary>
public static void Tick(float unscaledNow)
{
var st = St;
if (st == null || !WLEffectScaleSettings.Enabled)
{
if (ActiveScaled > 0) RestoreAll(); // C8: 끄면 다음 틱에 전량 원복
return;
}
if (unscaledNow < s_nextSweep) return;
s_nextSweep = unscaledNow + Mathf.Max(0f, st.sweepIntervalSeconds);
EnsureCapacity(st);
HookScene();
SweepCount++;
int budget = Mathf.Max(1, st.maxNewPerSweep);
var info = InGameInfo.Ins;
if (info != null)
{
if (st.sweepIngameEffects) budget = SweepRoot(info.tf_Effects, st, budget);
if (st.sweepIngameObjs) budget = SweepRoot(info.tf_Objs, st, budget);
}
if (st.sweepSkillSpectacle && SkillSpectacleRunner.Exists)
{
var runner = SkillSpectacleRunner.Ensure(); // Exists 가 true 라 새로 만들지 않는다
if (runner != null)
{
budget = SweepRoot(runner.LiveRoot, st, budget);
budget = SweepRoot(runner.PoolRoot, st, budget);
}
}
if (st.sweepMainPc)
{
var pc = MyValue.MyPC;
if (pc != null) budget = SweepRoot(pc.transform, st, budget);
}
}
/// <summary>루트의 직속 자식을 훑는다. 처음 보는 것은 등록(예산 소모)하고, 아는 것은 활성/비활성만 반영한다.</summary>
static int SweepRoot(Transform root, WLEffectScaleSettings st, int budget)
{
if (root == null) return budget;
int n = root.childCount;
for (int i = 0; i < n; i++)
{
var child = root.GetChild(i);
if (child == null) continue;
int id = child.GetInstanceID();
int slot;
if (!s_map.TryGetValue(id, out slot))
{
if (budget <= 0) continue;
budget--;
slot = Register(child, id, st);
if (slot < 0) continue;
}
UpdateSlot(slot, st);
}
return budget;
}
/// <summary>처음 보는 인스턴스를 등록한다(이름 1회 읽기 = 유일한 할당). 슬롯이 없으면 1.</summary>
static int Register(Transform t, int id, WLEffectScaleSettings st)
{
if (s_count >= s_capacity)
{
Compact();
if (s_count >= s_capacity) { SkippedCount++; return -1; }
}
int slot = s_count++;
s_id[slot] = id;
s_tf[slot] = t;
s_orig[slot] = t.localScale;
s_applied[slot] = false;
s_off[slot] = null;
s_offResolved[slot] = false;
string nm = t.name; // ← 인스턴스당 1회
int core = WLEffectScaleSettings.CoreLength(nm);
int r = st.FindRule(nm, core);
s_rule[slot] = r;
s_factor[slot] = r >= 0 ? Mathf.Max(0f, st.rules[r].scale) : 0f;
if (r >= 0) RuledCount++;
s_map[id] = slot;
TrackedCount = s_count;
return slot;
}
/// <summary>활성이면 배율을 대입하고 비활성이면 되돌린다(둘 다 멱등).</summary>
static void UpdateSlot(int slot, WLEffectScaleSettings st)
{
var t = s_tf[slot];
if (t == null) return;
if (s_rule[slot] < 0) return; // 규칙 없음 = 원본 100 %
bool active = t.gameObject.activeSelf;
if (active)
{
if (s_applied[slot]) return;
float f = ResolveFactor(slot, st);
if (f <= 0f) return;
t.localScale = s_orig[slot] * f; // 대입(멱등) — 누적 곱이 아니다
s_applied[slot] = true;
AppliedCount++; ActiveScaled++;
LastName = t.name; LastFactor = f;
if (st.verboseLog) Debug.Log("[WL811s] effectScale " + LastName + " ×" + f.ToString("F3")); // 🔴 Log 만(813z)
}
else if (s_applied[slot])
{
if (st.restoreOnReturn) t.localScale = s_orig[slot];
s_applied[slot] = false;
RestoredCount++; if (ActiveScaled > 0) ActiveScaled--;
}
}
/// <summary>고정 배율 · (옵션) 대상 액터 높이 기반 재계산.</summary>
static float ResolveFactor(int slot, WLEffectScaleSettings st)
{
int r = s_rule[slot];
float f = s_factor[slot];
if (!st.scaleByTargetHeight || r < 0) return f;
var rule = st.rules[r];
if (!rule.useTargetHeight || rule.baseDiameter <= 0f || rule.targetRatio <= 0f) return f;
float h;
if (!TryTargetHeight(slot, out h) || h <= 0f) { HeightMissed++; return f; }
HeightResolved++;
float byHeight = (h * rule.targetRatio) / rule.baseDiameter;
return Mathf.Clamp(byHeight, Mathf.Max(0.0001f, st.minScale), Mathf.Max(st.minScale, st.maxScale));
}
/// <summary>이 이펙트가 따라다니는 대상 액터의 실높이(발밑→머리). 원본 private 필드라 리플렉션(캐시 1회).</summary>
static bool TryTargetHeight(int slot, out float height)
{
height = 0f;
if (!s_offResolved[slot])
{
var tf = s_tf[slot];
s_off[slot] = tf != null ? tf.GetComponent<TurnOff_GO>() : null;
s_offResolved[slot] = true;
}
var off = s_off[slot];
if (off == null) return false;
if (!s_reflectDone)
{
s_reflectDone = true;
s_fiTarget2 = typeof(TurnOff_GO).GetField("m_target2", BindingFlags.Instance | BindingFlags.NonPublic);
s_fiTfTop = typeof(Actor).GetField("tf_Top", BindingFlags.Instance | BindingFlags.NonPublic);
}
if (s_fiTarget2 == null || s_fiTfTop == null) return false;
var actor = s_fiTarget2.GetValue(off) as Actor; // 참조형 필드 → 박싱 없음
if (actor == null) return false;
var top = s_fiTfTop.GetValue(actor) as Transform;
if (top == null) return false;
height = top.position.y - actor.transform.position.y;
return height > 0.01f;
}
// ───────────────────────────────────────── 공개 API · 정리
/// <summary>WL 스폰 지점용(후속 1줄 연결). 스폰 직후 부르면 그 인스턴스를 즉시 등록·적용한다.</summary>
public static void Apply(GameObject go)
{
var st = St;
if (st == null || !WLEffectScaleSettings.Enabled || go == null) return;
EnsureCapacity(st);
var t = go.transform;
int id = t.GetInstanceID();
int slot;
if (!s_map.TryGetValue(id, out slot))
{
slot = Register(t, id, st);
if (slot < 0) return;
}
UpdateSlot(slot, st);
}
/// <summary>적용된 배율을 전부 되돌린다(C8 · 씬 전환 · 프로브).</summary>
public static void RestoreAll()
{
for (int i = 0; i < s_count; i++)
{
if (!s_applied[i]) continue;
var t = s_tf[i];
if (t != null) t.localScale = s_orig[i];
s_applied[i] = false;
RestoredCount++;
}
ActiveScaled = 0;
}
static void EnsureCapacity(WLEffectScaleSettings st)
{
int want = Mathf.Max(32, st.trackCapacity);
if (s_id != null && s_capacity >= want) return;
Array.Resize(ref s_id, want);
Array.Resize(ref s_tf, want);
Array.Resize(ref s_orig, want);
Array.Resize(ref s_factor, want);
Array.Resize(ref s_rule, want);
Array.Resize(ref s_applied, want);
Array.Resize(ref s_off, want);
Array.Resize(ref s_offResolved, want);
s_capacity = want;
}
/// <summary>파괴된 인스턴스 슬롯을 정리한다(슬롯이 꽉 찼을 때 · 씬 전환 뒤).</summary>
static void Compact()
{
int w = 0;
for (int i = 0; i < s_count; i++)
{
if (s_tf[i] == null) continue;
if (w != i)
{
s_id[w] = s_id[i]; s_tf[w] = s_tf[i]; s_orig[w] = s_orig[i];
s_factor[w] = s_factor[i]; s_rule[w] = s_rule[i]; s_applied[w] = s_applied[i];
s_off[w] = s_off[i]; s_offResolved[w] = s_offResolved[i];
}
w++;
}
s_count = w;
s_map.Clear();
for (int i = 0; i < s_count; i++) s_map[s_id[i]] = i;
TrackedCount = s_count;
}
static void HookScene()
{
if (s_sceneHooked) return;
s_sceneHooked = true;
UnityEngine.SceneManagement.SceneManager.activeSceneChanged += OnSceneChanged;
}
static void OnSceneChanged(UnityEngine.SceneManagement.Scene a, UnityEngine.SceneManagement.Scene b)
{
s_map.Clear(); s_count = 0; TrackedCount = 0; ActiveScaled = 0;
}
/// <summary>프로브용: 카운터·추적을 비운다(적용분은 먼저 원복한다).</summary>
public static void ResetDiagnostics()
{
RestoreAll();
s_map.Clear(); s_count = 0; s_nextSweep = 0f;
SweepCount = TrackedCount = RuledCount = 0;
AppliedCount = RestoredCount = SkippedCount = ActiveScaled = 0;
HeightResolved = HeightMissed = 0;
LastName = ""; LastFactor = 0f;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8115b1d4f8e35c709b2d9f6e4c803150
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,42 @@
// ─────────────────────────────────────────────────────────────────────────────
// EffectScaleGuardRunner.cs — 이펙트 크기 가드 틱 러너(숨은 오브젝트 1개 · 코루틴 0)
//
// PD 지시 #813 · 발주서 WL-811s §1-3
//
// · SO 가 꺼져 있으면 **오브젝트를 만들지 않는다**(C8: 꺼짐 = 흔적 0).
// · **LateUpdate** 로 돈다 — 원본 스폰(Update·코루틴) 뒤 · 렌더 앞이라 활성화된 그 프레임에 배율이 걸린다.
// · unscaled 시간으로 판정한다(히트스톱 timeScale 0 중에도 착탄 이펙트가 뜬다).
// · 기존 러너(811def WLReactionRunner · 813m3 NullMaterialGuardRunner)를 건드리지 않는다 = 병합 충돌 0.
//
// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것.
// ─────────────────────────────────────────────────────────────────────────────
using UnityEngine;
namespace WL.Combat.Reaction
{
/// <summary>EffectScaleGuard 전용 러너. DontDestroyOnLoad · LateUpdate 1개.</summary>
public sealed class EffectScaleGuardRunner : MonoBehaviour
{
static EffectScaleGuardRunner s_runner;
public static bool Exists { get { return s_runner != null; } }
public static void Ensure()
{
if (s_runner != null) return;
if (!Application.isPlaying) return;
var go = new GameObject("__WLEffectScaleGuard");
DontDestroyOnLoad(go);
s_runner = go.AddComponent<EffectScaleGuardRunner>();
}
void LateUpdate() { EffectScaleGuard.Tick(Time.unscaledTime); }
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
static void Boot()
{
if (WLEffectScaleSettings.Enabled) Ensure(); // 꺼져 있으면 오브젝트 0(C8)
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8115c2e509f46d81ac3ea07f5d914261
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,197 @@
// ─────────────────────────────────────────────────────────────────────────────
// WLEffectScaleSettings.cs — 원본 풀 스폰 이펙트(상태이상·히트) 크기 값의 단일 출처(SOT · C45)
//
// PD 지시 #813 추가(2026-09-10) 「상태이상 효과(스턴) 크기 및 플레이어 스킬이 몬스터·플레이어 크기에 비해
// 비정상적으로 큼 → 캐릭터/몬스터 크기를 고려해 1차로 조절」 · 발주서 WL-811s §1-3
//
// ■ 왜 필요한가 (전부 실측 · `AgentScripts/staging/WL811s/MEASURE.txt`)
// 액터 실높이 = PC(Ai01 × ClassConfig.f_Scale 0.7) **1.191 m** · 존 잡몹 **0.54~0.73 m** ·
// 엘리트(× 1.3) 1.22~2.07 m · 보스 Anubis(× 1.5) **3.13 m**
// 이펙트 원 지름(프리팹 인스턴스 → ParticleSystem.Simulate t 스윕 → 파티클 실좌표 90 백분위) =
// Effect_Stun **5.58 m**(PC 높이의 4.7배 · 잡몹 높이의 10배) · Effect_Hit_10501 8.84 m ·
// Effect_Barrier 22.0 m … → PD 지적 그대로 「대상보다 이펙트가 크다」가 수치로 확인된다.
//
// ■ 규칙(Lead 제안 · 이 에셋은 그 규칙을 **상한**으로 적용한다 = 하향만 · 이미 작은 이펙트는 그대로)
// 목표 최대 지름 = 기준 높이 × 계수 (상태이상 0.6 · 착탄 0.8) → 배율 = 목표 지름 ÷ 프리팹 원 지름
// 보스 **패턴**(Effect_*Charging · Effect_Anubis_Skill1End 등)은 규칙 없음 = 원본 100 %(발주서 §1-1).
//
// ■ 어떻게 (813m3 NullMaterialGuard 의 「원본 이펙트 풀 루트 스윕」 방식 그대로 · 원본 훅 0)
// 상태이상·히트 이펙트는 전부 원본 `InGameInfo.Show_Effect` → `dic_str_Effect` 풀이 스폰한다(WL 코드가 아니다).
// 그래서 스폰 지점 훅 대신 **풀 루트(`InGameInfo.tf_Effects`)의 직속 자식**을 LateUpdate 에서 훑어
// 활성화된 인스턴스에 `localScale = 원 스케일 × 배율` 을 **대입**(누적 곱이 아니라 대입 = 멱등)하고,
// 원본 `TurnOff_GO.Off_Imm` 이 `SetActive(false)` 로 반납하면 원 스케일로 되돌린다.
//
// 에셋 경로(고정): Assets/WL/Combat/Settings/Resources/WL/WLEffectScaleSettings.asset
// C8 롤백: 에셋이 없거나 enabled = false → 러너도 스윕도 없고 적용분은 전부 원복 = 원본 100 %.
//
// 🔴 원본 프리팹·머티리얼은 고치지 않는다(발주서 §2 금지). 런타임 인스턴스의 localScale 만 만진다.
// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것.
// ─────────────────────────────────────────────────────────────────────────────
using System;
using UnityEngine;
namespace WL.Combat.Reaction
{
/// <summary>인스턴스 이름(끝의 "(Clone)" 제외)과 규칙 문자열을 어떻게 맞추나.</summary>
public enum EffectScaleMatch
{
/// <summary>이름 전체가 같다(기본).</summary>
Exact = 0,
/// <summary>이름이 이 문자열로 시작한다(계열 일괄 규칙).</summary>
Prefix = 1,
/// <summary>이름 안에 이 문자열이 있다.</summary>
Contains = 2
}
[Serializable]
public struct EffectScaleRule
{
[Tooltip("프리팹 이름(Assets/Res_Addr/Effect/<이름>.prefab). 인스턴스는 '<이름>(Clone)' 이다.")]
public string prefab;
[Tooltip("매칭 방식. 규칙은 배열 순서대로 검사해 처음 맞는 것을 쓴다(Exact 를 앞에, Prefix 를 뒤에).")]
public EffectScaleMatch match;
[Tooltip("고정 배율(프리팹 원 localScale × 이 값). 1 = 원본 그대로 · 0 이하 = 이 규칙 무시.")]
public float scale;
[Tooltip("scaleByTargetHeight 가 켜졌고 대상 액터를 찾았을 때 targetRatio·baseDiameter 로 배율을 다시 계산한다.")]
public bool useTargetHeight;
[Tooltip("대상 높이 × 이 계수 = 목표 최대 지름(m). Lead 규칙: 상태이상 0.6 · 착탄 0.8.")]
public float targetRatio;
[Tooltip("프리팹 원 지름 실측(m · 파티클 실좌표 90 백분위 · MEASURE.txt [B]). 0 .")]
public float baseDiameter;
[Tooltip("표시용 메모(데이터 · 코드가 읽지 않는다).")]
public string note;
}
[CreateAssetMenu(fileName = "WLEffectScaleSettings", menuName = "WL/Effect Scale Guard Settings", order = 327)]
public sealed class WLEffectScaleSettings : ScriptableObject
{
public const string ResourcesPath = "WL/WLEffectScaleSettings";
static WLEffectScaleSettings s_cached;
static bool s_lookupDone;
/// <summary>설정 에셋(1회 로드 후 캐시). 없으면 null.</summary>
public static WLEffectScaleSettings Instance
{
get
{
if (!s_lookupDone) { s_cached = Resources.Load<WLEffectScaleSettings>(ResourcesPath); s_lookupDone = true; }
return s_cached;
}
}
/// <summary>QA·A/B 전용 런타임 스위치. 0 = 에셋의 enabled · 1 = 강제 on · -1 = 강제 off.</summary>
public static int RuntimeOverride;
/// <summary>가드가 살아 있는가. false 면 스윕도 러너도 없다(C8).</summary>
public static bool Enabled
{
get
{
var i = Instance;
if (i == null || RuntimeOverride < 0) return false;
return RuntimeOverride > 0 || i.enabled;
}
}
/// <summary>테스트용: 캐시를 비운다.</summary>
public static void ClearCache() { s_cached = null; s_lookupDone = false; }
[Header("전체 스위치")]
[Tooltip("끄면 러너도 스윕도 없고 이미 적용된 배율은 전부 원복된다 = 원본 100 %. C8 롤백 1순위.")]
public bool enabled = true;
[Tooltip("적용·원복을 Console 에 남긴다(인스턴스당 1줄 · GC 발생 · 기본 off).")]
public bool verboseLog = false;
[Header("스윕 (813m3 NullMaterialGuard 와 같은 풀 루트 스윕)")]
[Tooltip("원본 이펙트 풀 `InGameInfo.tf_Effects` 를 훑는다 — 상태이상·히트가 전부 여기다. 끄면 이 발주는 무효.")]
public bool sweepIngameEffects = true;
[Tooltip("원본 오브젝트 풀 `InGameInfo.tf_Objs` 도 훑는다(드랍·813f 등급 빔 · 기본 off).")]
public bool sweepIngameObjs = false;
[Tooltip("811i SkillSpectacle 의 Live/Pool 루트도 훑는다. 🔴 811i 는 이미 행마다 scale 을 곱하므로 " +
"켜면 이중 축소가 된다 — 기본 off 를 유지할 것.")]
public bool sweepSkillSpectacle = false;
[Tooltip("메인 PC 하위를 훑는다(813k 레벨업 폭발이 PC 에 붙는다 · 813w4 가 이미 0.5 로 낮췄으므로 기본 off).")]
public bool sweepMainPc = false;
[Tooltip("스윕 주기(초 · unscaled). 0 = 매 프레임(권장) — 활성화 프레임에 바로 적용해야 1프레임 원본 크기 깜빡임이 없다.")]
public float sweepIntervalSeconds = 0f;
[Tooltip("한 스윕에서 **처음 보는** 인스턴스를 몇 개까지 등록하나(이름 읽기 = 유일한 할당 지점). 남으면 다음 스윕.")]
public int maxNewPerSweep = 24;
[Tooltip("추적 슬롯 수(사전 할당). 넘으면 죽은 인스턴스를 정리하고, 그래도 모자라면 새 인스턴스를 건너뛴다.")]
public int trackCapacity = 512;
[Tooltip("원본 `TurnOff_GO` 가 반납(SetActive(false))할 때 원 스케일로 되돌린다. 끄면 적용된 채로 풀에 남는다(멱등이라 안전하지만 C8 이 더 깔끔해진다).")]
public bool restoreOnReturn = true;
[Header("대상 높이 기반 (옵션 · 기본 off = 아래 rules[].scale )")]
[Tooltip("켜면 이펙트가 붙은 대상 액터의 실높이(발밑→머리)로 배율을 다시 계산한다. " +
"대상을 못 찾으면(위치 지정 스폰 등) 고정 배율로 떨어진다.")]
public bool scaleByTargetHeight = false;
[Tooltip("고정 배율을 역산할 때 쓴 기준 높이(m). 실측 PC(Ai01 × f_Scale 0.7) = 1.191 m.")]
public float referenceHeight = 1.191f;
[Tooltip("대상 높이 기반 계산의 배율 하한(너무 작아져 안 보이는 것 방지).")]
public float minScale = 0.05f;
[Tooltip("대상 높이 기반 계산의 배율 상한(보스처럼 큰 대상에서 원본보다 커지는 것 방지).")]
public float maxScale = 1.5f;
[Header("규칙 (배열 순서대로 검사 · 처음 맞는 것 · 안 맞으면 손대지 않는다 = 원본 100 %)")]
public EffectScaleRule[] rules = new EffectScaleRule[0];
/// <summary>인스턴스 이름으로 규칙을 찾는다(선형 · alloc 0). 없으면 1.</summary>
public int FindRule(string instanceName, int coreLength)
{
if (rules == null || string.IsNullOrEmpty(instanceName)) return -1;
for (int i = 0; i < rules.Length; i++)
{
if (rules[i].scale <= 0f && !rules[i].useTargetHeight) continue;
if (NameMatches(instanceName, coreLength, rules[i].prefab, rules[i].match)) return i;
}
return -1;
}
/// <summary>"<이름>(Clone)" 에서 "(Clone)" 을 뺀 유효 길이(부분 문자열을 만들지 않는다 = alloc 0).</summary>
public static int CoreLength(string n)
{
if (string.IsNullOrEmpty(n)) return 0;
int len = n.Length;
if (len > 7 && string.CompareOrdinal(n, len - 7, "(Clone)", 0, 7) == 0) len -= 7;
return len;
}
/// <summary>부분 문자열 생성 없이 이름을 맞춘다(alloc 0).</summary>
public static bool NameMatches(string n, int coreLength, string pattern, EffectScaleMatch match)
{
if (string.IsNullOrEmpty(pattern) || coreLength <= 0) return false;
int pl = pattern.Length;
if (pl > coreLength) return false;
switch (match)
{
case EffectScaleMatch.Exact:
return pl == coreLength && string.CompareOrdinal(n, 0, pattern, 0, pl) == 0;
case EffectScaleMatch.Prefix:
return string.CompareOrdinal(n, 0, pattern, 0, pl) == 0;
default:
for (int i = 0; i + pl <= coreLength; i++)
if (string.CompareOrdinal(n, i, pattern, 0, pl) == 0) return true;
return false;
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8115a0c3e7d24b6f9a1c8e5d3b7f2049
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,428 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 8115a0c3e7d24b6f9a1c8e5d3b7f2049, type: 3}
m_Name: WLEffectScaleSettings
m_EditorClassIdentifier: Assembly-CSharp::WL.Combat.Reaction.WLEffectScaleSettings
enabled: 1
verboseLog: 0
sweepIngameEffects: 1
sweepIngameObjs: 0
sweepSkillSpectacle: 0
sweepMainPc: 0
sweepIntervalSeconds: 0
maxNewPerSweep: 24
trackCapacity: 512
restoreOnReturn: 1
scaleByTargetHeight: 0
referenceHeight: 1.191
minScale: 0.05
maxScale: 1.5
rules:
- prefab: Effect_Stun
match: 0
scale: 0.124
useTargetHeight: 1
targetRatio: 0.6
baseDiameter: 5.776
note: 상태이상 0.6
- prefab: Effect_Poison
match: 0
scale: 0.383
useTargetHeight: 1
targetRatio: 0.6
baseDiameter: 1.866
note: 상태이상 0.6
- prefab: Effect_Burn
match: 0
scale: 0.227
useTargetHeight: 1
targetRatio: 0.6
baseDiameter: 3.151
note: 상태이상 0.6
- prefab: Effect_Freezing
match: 0
scale: 0.157
useTargetHeight: 1
targetRatio: 0.6
baseDiameter: 4.549
note: 상태이상 0.6
- prefab: Effect_Frost
match: 0
scale: 0.094
useTargetHeight: 1
targetRatio: 0.6
baseDiameter: 7.595
note: 상태이상 0.6
- prefab: Effect_Curse
match: 0
scale: 0.179
useTargetHeight: 1
targetRatio: 0.6
baseDiameter: 4
note: 상태이상 0.6
- prefab: Effect_Slow
match: 0
scale: 0.238
useTargetHeight: 1
targetRatio: 0.6
baseDiameter: 3
note: 상태이상 0.6
- prefab: Effect_SilenceCurse
match: 0
scale: 0.213
useTargetHeight: 1
targetRatio: 0.6
baseDiameter: 3.351
note: 상태이상 0.6
- prefab: Effect_Blood
match: 0
scale: 0.099
useTargetHeight: 1
targetRatio: 0.6
baseDiameter: 7.199
note: 상태이상 0.6
- prefab: Effect_HPBurn
match: 0
scale: 0.208
useTargetHeight: 1
targetRatio: 0.6
baseDiameter: 3.436
note: 상태이상 0.6
- prefab: Effect_ManaBurn
match: 0
scale: 0.303
useTargetHeight: 1
targetRatio: 0.6
baseDiameter: 2.355
note: 상태이상 0.6
- prefab: Effect_DefenceDecrease
match: 0
scale: 0.169
useTargetHeight: 1
targetRatio: 0.6
baseDiameter: 4.226
note: 상태이상 0.6
- prefab: Effect_Barrier
match: 0
scale: 0.032
useTargetHeight: 1
targetRatio: 0.6
baseDiameter: 22
note: 상태이상 0.6
- prefab: Effect_ManaShield
match: 0
scale: 0.512
useTargetHeight: 1
targetRatio: 0.6
baseDiameter: 1.395
note: 상태이상 0.6
- prefab: Effect_Cure
match: 0
scale: 0.136
useTargetHeight: 1
targetRatio: 0.6
baseDiameter: 5.249
note: 상태이상 0.6
- prefab: Effect_SpeedUp
match: 0
scale: 0.238
useTargetHeight: 1
targetRatio: 0.6
baseDiameter: 3
note: 상태이상 0.6
- prefab: Effect_StatUp
match: 0
scale: 0.32
useTargetHeight: 1
targetRatio: 0.6
baseDiameter: 2.231
note: 상태이상 0.6
- prefab: Effect_Reincarnation
match: 0
scale: 0.212
useTargetHeight: 1
targetRatio: 0.6
baseDiameter: 3.372
note: 상태이상 0.6
- prefab: Effect_VampirHeal
match: 0
scale: 0.089
useTargetHeight: 1
targetRatio: 0.6
baseDiameter: 8
note: 상태이상 0.6
- prefab: Effect_Hit_10101
match: 0
scale: 0.27
useTargetHeight: 1
targetRatio: 0.8
baseDiameter: 3.53
note: 착탄 0.8
- prefab: Effect_Hit_10102
match: 0
scale: 0.829
useTargetHeight: 1
targetRatio: 0.8
baseDiameter: 1.15
note: 착탄 0.8
- prefab: Effect_Hit_10201
match: 0
scale: 0.216
useTargetHeight: 1
targetRatio: 0.8
baseDiameter: 4.421
note: 착탄 0.8
- prefab: Effect_Hit_10202
match: 0
scale: 0.718
useTargetHeight: 1
targetRatio: 0.8
baseDiameter: 1.327
note: 착탄 0.8
- prefab: Effect_Hit_10301
match: 0
scale: 0.708
useTargetHeight: 1
targetRatio: 0.8
baseDiameter: 1.345
note: 착탄 0.8
- prefab: Effect_Hit_10302
match: 0
scale: 0.44
useTargetHeight: 1
targetRatio: 0.8
baseDiameter: 2.167
note: 착탄 0.8
- prefab: Effect_Hit_10303
match: 0
scale: 0.627
useTargetHeight: 1
targetRatio: 0.8
baseDiameter: 1.519
note: 착탄 0.8
- prefab: Effect_Hit_10401
match: 0
scale: 0.839
useTargetHeight: 1
targetRatio: 0.8
baseDiameter: 1.136
note: 착탄 0.8
- prefab: Effect_Hit_10402
match: 0
scale: 0.524
useTargetHeight: 1
targetRatio: 0.8
baseDiameter: 1.818
note: 착탄 0.8
- prefab: Effect_Hit_10501
match: 0
scale: 0.108
useTargetHeight: 1
targetRatio: 0.8
baseDiameter: 8.826
note: 착탄 0.8
- prefab: Effect_Hit_10502
match: 0
scale: 0.335
useTargetHeight: 1
targetRatio: 0.8
baseDiameter: 2.847
note: 착탄 0.8
- prefab: Effect_Hit_10601
match: 0
scale: 0.383
useTargetHeight: 1
targetRatio: 0.8
baseDiameter: 2.488
note: 착탄 0.8
- prefab: Effect_Hit_AnubisSkill1
match: 0
scale: 0.642
useTargetHeight: 1
targetRatio: 0.8
baseDiameter: 1.484
note: 착탄 0.8
- prefab: Effect_Hit_AnubisSkill2
match: 0
scale: 0.6
useTargetHeight: 1
targetRatio: 0.8
baseDiameter: 1.587
note: 착탄 0.8
- prefab: Effect_Hit_Bead
match: 0
scale: 0.791
useTargetHeight: 1
targetRatio: 0.8
baseDiameter: 1.205
note: 착탄 0.8
- prefab: Effect_Hit_BlackHole
match: 0
scale: 0.089
useTargetHeight: 1
targetRatio: 0.8
baseDiameter: 10.757
note: 착탄 0.8
- prefab: Effect_Hit_BrotherPenguin_Attack
match: 0
scale: 0.451
useTargetHeight: 1
targetRatio: 0.8
baseDiameter: 2.112
note: 착탄 0.8
- prefab: Effect_Hit_Electronic
match: 0
scale: 0.497
useTargetHeight: 1
targetRatio: 0.8
baseDiameter: 1.919
note: 착탄 0.8
- prefab: Effect_Hit_Energyball
match: 0
scale: 0.352
useTargetHeight: 1
targetRatio: 0.8
baseDiameter: 2.705
note: 착탄 0.8
- prefab: Effect_Hit_Fireball
match: 0
scale: 0.272
useTargetHeight: 1
targetRatio: 0.8
baseDiameter: 3.503
note: 착탄 0.8
- prefab: Effect_Hit_Firebolt
match: 0
scale: 0.493
useTargetHeight: 1
targetRatio: 0.8
baseDiameter: 1.933
note: 착탄 0.8
- prefab: Effect_Hit_FlowerDryad_Attack
match: 0
scale: 1
useTargetHeight: 1
targetRatio: 0.8
baseDiameter: 0.933
note: 착탄 0.8
- prefab: Effect_Hit_IceBolt
match: 0
scale: 0.322
useTargetHeight: 1
targetRatio: 0.8
baseDiameter: 2.961
note: 착탄 0.8
- prefab: Effect_Hit_IceGolem_Attack
match: 0
scale: 0.941
useTargetHeight: 1
targetRatio: 0.8
baseDiameter: 1.013
note: 착탄 0.8
- prefab: Effect_Hit_Mob_HellfireFlames
match: 0
scale: 0.479
useTargetHeight: 1
targetRatio: 0.8
baseDiameter: 1.991
note: 착탄 0.8
- prefab: Effect_Hit_RookieArcherSkill1
match: 0
scale: 0.529
useTargetHeight: 1
targetRatio: 0.8
baseDiameter: 1.8
note: 착탄 0.8
- prefab: Effect_Hit_Shaga_Attack
match: 0
scale: 0.688
useTargetHeight: 1
targetRatio: 0.8
baseDiameter: 1.385
note: 착탄 0.8
- prefab: Effect_Hit_Skill_CrossSlash
match: 0
scale: 1
useTargetHeight: 1
targetRatio: 0.8
baseDiameter: 0.638
note: 착탄 0.8
- prefab: Effect_Hit_Skill_DaggerThrow
match: 0
scale: 0.368
useTargetHeight: 1
targetRatio: 0.8
baseDiameter: 2.59
note: 착탄 0.8
- prefab: Effect_Hit_Skill_EnergySlashFlash
match: 0
scale: 0.193
useTargetHeight: 1
targetRatio: 0.8
baseDiameter: 4.925
note: 착탄 0.8
- prefab: Effect_Hit_Skill_Frozen
match: 0
scale: 0.159
useTargetHeight: 1
targetRatio: 0.8
baseDiameter: 6
note: 착탄 0.8
- prefab: Effect_Hit_Skill_SpinSlash
match: 0
scale: 1
useTargetHeight: 1
targetRatio: 0.8
baseDiameter: 0.463
note: 착탄 0.8
- prefab: Effect_Hit_SummonGolem
match: 0
scale: 0.819
useTargetHeight: 1
targetRatio: 0.8
baseDiameter: 1.163
note: 착탄 0.8
- prefab: Effect_Hit_SummonWorm
match: 0
scale: 0.819
useTargetHeight: 1
targetRatio: 0.8
baseDiameter: 1.164
note: 착탄 0.8
- prefab: Effect_Hit_SweepAttack
match: 0
scale: 0.486
useTargetHeight: 1
targetRatio: 0.8
baseDiameter: 1.961
note: 착탄 0.8
- prefab: ShineBulletHit
match: 0
scale: 0.353
useTargetHeight: 1
targetRatio: 0.8
baseDiameter: 2.7
note: 착탄 0.8
- prefab: WaveShotHit
match: 0
scale: 0.84
useTargetHeight: 1
targetRatio: 0.8
baseDiameter: 1.134
note: 착탄 0.8
- prefab: WindBullet_Hit
match: 0
scale: 0.335
useTargetHeight: 1
targetRatio: 0.8
baseDiameter: 2.84
note: 착탄 0.8

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8115d3f61a057e92bd4fb1806ea25372
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@ -71,7 +71,7 @@ MonoBehaviour:
lifetime: 0
body:
prefab: Effect_Slash_10101_1
scale: 1.2
scale: 0.459
anchor: 1
forwardOffset: 0.6
useCasterRotation: 1
@ -79,7 +79,7 @@ MonoBehaviour:
lifetime: 0
impact:
prefab: Effect_Hit_10101
scale: 1
scale: 0.27
anchor: 1
forwardOffset: 0
useCasterRotation: 0
@ -92,7 +92,7 @@ MonoBehaviour:
label: 회심의 일격
charge:
prefab: Effect_Berserk
scale: 1
scale: 0.999
anchor: 2
forwardOffset: 0
useCasterRotation: 0
@ -100,7 +100,7 @@ MonoBehaviour:
lifetime: 0
body:
prefab: Effect_Slash_10101_2
scale: 1.3
scale: 0.335
anchor: 1
forwardOffset: 0.8
useCasterRotation: 1
@ -108,7 +108,7 @@ MonoBehaviour:
lifetime: 0
impact:
prefab: Effect_Hit_10101
scale: 1.2
scale: 0.27
anchor: 1
forwardOffset: 0
useCasterRotation: 0
@ -121,7 +121,7 @@ MonoBehaviour:
label: 돌진 베기
charge:
prefab: Effect_PCDash
scale: 1
scale: 0.702
anchor: 2
forwardOffset: 0
useCasterRotation: 1
@ -129,7 +129,7 @@ MonoBehaviour:
lifetime: 0
body:
prefab: Effect_Slash_10101_3
scale: 1.4
scale: 0.121
anchor: 1
forwardOffset: 1
useCasterRotation: 1
@ -137,7 +137,7 @@ MonoBehaviour:
lifetime: 0
impact:
prefab: Effect_Hit_SweepAttack
scale: 1
scale: 0.486
anchor: 1
forwardOffset: 0
useCasterRotation: 0
@ -150,7 +150,7 @@ MonoBehaviour:
label: 돌개 바람
charge:
prefab: Effect_Howling
scale: 1.2
scale: 0.251
anchor: 2
forwardOffset: 0
useCasterRotation: 0
@ -158,7 +158,7 @@ MonoBehaviour:
lifetime: 0
body:
prefab: Effect_WLSwingArc
scale: 1.5
scale: 0.483
anchor: 1
forwardOffset: 0.5
useCasterRotation: 1
@ -166,7 +166,7 @@ MonoBehaviour:
lifetime: 0
impact:
prefab: WindBullet_Hit
scale: 1.2
scale: 0.335
anchor: 1
forwardOffset: 0
useCasterRotation: 0
@ -187,7 +187,7 @@ MonoBehaviour:
lifetime: 0
body:
prefab: Effect_Slash_10102_1
scale: 1.2
scale: 0.461
anchor: 1
forwardOffset: 0.6
useCasterRotation: 1
@ -195,7 +195,7 @@ MonoBehaviour:
lifetime: 0
impact:
prefab: Effect_Hit_10102
scale: 1
scale: 0.829
anchor: 1
forwardOffset: 0
useCasterRotation: 0
@ -208,7 +208,7 @@ MonoBehaviour:
label: 검기 분출
charge:
prefab: Effect_Casting_Energyball
scale: 1
scale: 0.667
anchor: 2
forwardOffset: 0
useCasterRotation: 0
@ -216,7 +216,7 @@ MonoBehaviour:
lifetime: 0
body:
prefab: Effect_Slash_10102_2
scale: 1.3
scale: 0.403
anchor: 1
forwardOffset: 0.8
useCasterRotation: 1
@ -224,7 +224,7 @@ MonoBehaviour:
lifetime: 0
impact:
prefab: Effect_Hit_Skill_EnergySlashFlash
scale: 1
scale: 0.193
anchor: 1
forwardOffset: 0
useCasterRotation: 0
@ -237,7 +237,7 @@ MonoBehaviour:
label: 칼날 방패
charge:
prefab: Effect_Casting_ManaShield
scale: 1
scale: 0.539
anchor: 2
forwardOffset: 0
useCasterRotation: 0
@ -245,7 +245,7 @@ MonoBehaviour:
lifetime: 0
body:
prefab: Effect_SwordShield
scale: 1.35
scale: 0.175
anchor: 1
forwardOffset: 0
useCasterRotation: 0
@ -266,7 +266,7 @@ MonoBehaviour:
label: 마나 폭발
charge:
prefab: Effect_Casting_Meteor
scale: 1.2
scale: 0.793
anchor: 2
forwardOffset: 0
useCasterRotation: 0
@ -274,7 +274,7 @@ MonoBehaviour:
lifetime: 0
body:
prefab: Effect_STR_EXTRA_SPLASH
scale: 1.6
scale: 0.217
anchor: 1
forwardOffset: 0
useCasterRotation: 0
@ -282,7 +282,7 @@ MonoBehaviour:
lifetime: 0
impact:
prefab: Effect_Hit_Energyball
scale: 1.2
scale: 0.352
anchor: 1
forwardOffset: 0
useCasterRotation: 0
@ -303,7 +303,7 @@ MonoBehaviour:
lifetime: 0
body:
prefab: Effect_Slash_10501_1
scale: 1.2
scale: 0.335
anchor: 1
forwardOffset: 0.6
useCasterRotation: 1
@ -311,7 +311,7 @@ MonoBehaviour:
lifetime: 0
impact:
prefab: Effect_Hit_10501
scale: 1
scale: 0.108
anchor: 1
forwardOffset: 0
useCasterRotation: 0
@ -324,7 +324,7 @@ MonoBehaviour:
label: 강력한 일격
charge:
prefab: Effect_PCDash
scale: 1
scale: 0.702
anchor: 2
forwardOffset: 0
useCasterRotation: 1
@ -332,7 +332,7 @@ MonoBehaviour:
lifetime: 0
body:
prefab: Effect_Slash_10501_2
scale: 1.4
scale: 0.316
anchor: 2
forwardOffset: 0.8
useCasterRotation: 1
@ -340,7 +340,7 @@ MonoBehaviour:
lifetime: 0
impact:
prefab: Effect_Hit_SweepAttack
scale: 1
scale: 0.486
anchor: 1
forwardOffset: 0
useCasterRotation: 0
@ -361,7 +361,7 @@ MonoBehaviour:
lifetime: 0
body:
prefab: Effect_Heal
scale: 1.3
scale: 0.823
anchor: 1
forwardOffset: 0
useCasterRotation: 0
@ -382,7 +382,7 @@ MonoBehaviour:
label: 휘몰아치는 파도
charge:
prefab: Effect_Howling
scale: 1.2
scale: 0.251
anchor: 2
forwardOffset: 0
useCasterRotation: 0
@ -390,7 +390,7 @@ MonoBehaviour:
lifetime: 0
body:
prefab: Effect_Slash_10501_3
scale: 1.8
scale: 0.157
anchor: 1
forwardOffset: 1
useCasterRotation: 1
@ -398,7 +398,7 @@ MonoBehaviour:
lifetime: 0
impact:
prefab: WaveShotHit
scale: 1.2
scale: 0.84
anchor: 1
forwardOffset: 0
useCasterRotation: 0
@ -419,7 +419,7 @@ MonoBehaviour:
lifetime: 0
body:
prefab: Effect_Slash_10502_1
scale: 1.2
scale: 0.37
anchor: 1
forwardOffset: 0.6
useCasterRotation: 1
@ -448,7 +448,7 @@ MonoBehaviour:
lifetime: 0
body:
prefab: Effect_Slash_10502_2
scale: 1.5
scale: 0.344
anchor: 1
forwardOffset: 0
useCasterRotation: 1
@ -469,7 +469,7 @@ MonoBehaviour:
label: 도발
charge:
prefab: Effect_Howling
scale: 1
scale: 0.251
anchor: 2
forwardOffset: 0
useCasterRotation: 0
@ -498,7 +498,7 @@ MonoBehaviour:
label: 심판의 빛
charge:
prefab: Effect_Casting_Meteor
scale: 1.2
scale: 0.793
anchor: 2
forwardOffset: 0
useCasterRotation: 0
@ -506,7 +506,7 @@ MonoBehaviour:
lifetime: 0
body:
prefab: ShineBulletFlash
scale: 1.5
scale: 1.075
anchor: 1
forwardOffset: 0
useCasterRotation: 0
@ -514,7 +514,7 @@ MonoBehaviour:
lifetime: 0
impact:
prefab: ShineBulletHit
scale: 1.2
scale: 0.353
anchor: 1
forwardOffset: 0
useCasterRotation: 0