From d5d64e3cc39aaf0fa5b5d0c5f02c5b3293215807 Mon Sep 17 00:00:00 2001 From: swrring Date: Thu, 10 Sep 2026 01:04:08 +0900 Subject: [PATCH] =?UTF-8?q?[WL-811s]=20=EC=9D=B4=ED=8E=99=ED=8A=B8=20?= =?UTF-8?q?=ED=81=AC=EA=B8=B0=201=EC=B0=A8=20=EC=A1=B0=EC=A0=95:=20?= =?UTF-8?q?=EC=8A=A4=ED=82=AC=2016=ED=96=89=20=C2=B7=20=EC=83=81=ED=83=9C?= =?UTF-8?q?=EC=9D=B4=EC=83=81/=ED=9E=88=ED=8A=B8=20=ED=92=80=20=EC=8A=A4?= =?UTF-8?q?=EC=BC=80=EC=9D=BC=20=EA=B0=80=EB=93=9C=20(#813)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PD 지시 #813 추가 「상태이상(스턴) 크기 및 플레이어 스킬이 몬스터·플레이어 크기에 비해 비정상적으로 큼」. 실측(에디트 모드 프로브 · AgentScripts/staging/WL811s/MEASURE.txt): - 액터 실높이 = PC(Ai01 × f_Scale 0.7) 1.191 m · 존 잡몹 0.54~0.73 m · 엘리트(×1.3) 1.22~2.07 m · 보스 Anubis 3.13 m - 이펙트 원 지름(파티클 실좌표 90 백분위 · 고정 시드) = Effect_Stun 5.78 m(PC 높이의 4.9배) · Effect_Slash_10501_3 14.74 m … 값(Lead 규칙을 상한으로 적용 = 하향만): - WLSkillSpectacleSettings 16행 35줄 재산정(차지 1.2 · 본체 슬롯1~3 1.5 · 슬롯4 min(2.5, 화면 세로 50 % 캡 × 0.90) · 착탄 0.8) - 신규 EffectScaleGuard(원본 이펙트 풀 루트 LateUpdate 스윕 · 813m3 NullMaterialGuard 방식) + Runner + WLEffectScaleSettings(SO 57규칙) 프로브 RESULT PASS 24/24 · GC 0 B(Tick ×200,000) · C8 전량 원복 · 원본/Core/UI/프리팹 0줄 Co-Authored-By: Claude Fable 5.1 --- AgentScripts/WL811s_PROBE.txt | 201 ++++++ AgentScripts/WL811s_Probe.cs | 667 ++++++++++++++++++ Assets/WL/Combat/Reaction/EffectScaleGuard.cs | 336 +++++++++ .../Combat/Reaction/EffectScaleGuard.cs.meta | 11 + .../Combat/Reaction/EffectScaleGuardRunner.cs | 42 ++ .../Reaction/EffectScaleGuardRunner.cs.meta | 11 + .../Combat/Reaction/WLEffectScaleSettings.cs | 197 ++++++ .../Reaction/WLEffectScaleSettings.cs.meta | 11 + .../Resources/WL/WLEffectScaleSettings.asset | 428 +++++++++++ .../WL/WLEffectScaleSettings.asset.meta | 8 + .../WL/WLSkillSpectacleSettings.asset | 72 +- 11 files changed, 1948 insertions(+), 36 deletions(-) create mode 100644 AgentScripts/WL811s_PROBE.txt create mode 100644 AgentScripts/WL811s_Probe.cs create mode 100644 Assets/WL/Combat/Reaction/EffectScaleGuard.cs create mode 100644 Assets/WL/Combat/Reaction/EffectScaleGuard.cs.meta create mode 100644 Assets/WL/Combat/Reaction/EffectScaleGuardRunner.cs create mode 100644 Assets/WL/Combat/Reaction/EffectScaleGuardRunner.cs.meta create mode 100644 Assets/WL/Combat/Reaction/WLEffectScaleSettings.cs create mode 100644 Assets/WL/Combat/Reaction/WLEffectScaleSettings.cs.meta create mode 100644 Assets/WL/Combat/Settings/Resources/WL/WLEffectScaleSettings.asset create mode 100644 Assets/WL/Combat/Settings/Resources/WL/WLEffectScaleSettings.asset.meta diff --git a/AgentScripts/WL811s_PROBE.txt b/AgentScripts/WL811s_PROBE.txt new file mode 100644 index 000000000..e20f4fb8f --- /dev/null +++ b/AgentScripts/WL811s_PROBE.txt @@ -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 diff --git a/AgentScripts/WL811s_Probe.cs b/AgentScripts/WL811s_Probe.cs new file mode 100644 index 000000000..b16fdce8a --- /dev/null +++ b/AgentScripts/WL811s_Probe.cs @@ -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 s_err = new System.Collections.Generic.List(); + 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)); + } + + // ─────────────────────────────────────────────────────────────── 공용 측정 + + /// 프리팹을 숨은 인스턴스로 띄운다(원 localScale 유지 · 원점 · 무회전). + static UnityEngine.GameObject Spawn(string path) + { + var pf = UnityEditor.AssetDatabase.LoadAssetAtPath(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; + } + + /// 활성 여부와 무관하게 모든 렌더러의 월드 바운드 합집합. 없으면 false. + static bool RendererBounds(UnityEngine.GameObject go, out UnityEngine.Bounds b, out int count) + { + b = new UnityEngine.Bounds(); + count = 0; + var rs = go.GetComponentsInChildren(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 s_radii = new System.Collections.Generic.List(1 << 14); + + /// 파티클 실좌표(GetParticles)로 원점 기준 반경 분포를 만든다. 렌더러 바운드(=Unity 보수 컬링 바운드)는 쓰지 않는다. + /// diaMax = 전 파티클 최대 지름 · diaP90 = 90 백분위 지름(스트레이 1개에 흔들리지 않는 「덩치」) · hgt = y 폭. + 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(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(true); + for (int i = 0; i < mrs.Length; i++) + { + var mr = mrs[i]; + if (mr == null || mr.GetComponent() != 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() != 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 MeasureActors() + { + var list = new System.Collections.Generic.List(); + // 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 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(kCamPrefab); + if (cam != null) + { + var c = cam.GetComponent(); + if (c != null) ci.fov = c.fieldOfView; + var rc = cam.GetComponent(); + 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; + } + + /// 원점(PC 발밑)에 지름 dia · 높이 hgt 의 구/원기둥이 있을 때 세로 화면 점유율(0~1). + 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(); + 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(); + 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(); // 에디트 모드라 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(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(); + 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("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(); + 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(); + 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" + }; + } +} diff --git a/Assets/WL/Combat/Reaction/EffectScaleGuard.cs b/Assets/WL/Combat/Reaction/EffectScaleGuard.cs new file mode 100644 index 000000000..cad0a2483 --- /dev/null +++ b/Assets/WL/Combat/Reaction/EffectScaleGuard.cs @@ -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` 조회는 전부 비할당. +// 유일한 할당 = **처음 보는 인스턴스 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 +{ + /// 원본 이펙트 풀 인스턴스에 크기 배율을 걸고 반납 시 되돌린다. 정적 · 멱등 · GC 0. + public static class EffectScaleGuard + { + // ── 진단(프로브·QA 가 읽는다) + /// 스윕 횟수 · 추적 중인 인스턴스 수 · 규칙이 붙은 인스턴스 수. + public static int SweepCount, TrackedCount, RuledCount; + /// 배율을 건 횟수 · 되돌린 횟수 · 슬롯 부족으로 건너뛴 수. + public static int AppliedCount, RestoredCount, SkippedCount; + /// 현재 배율이 걸려 있는 인스턴스 수. + public static int ActiveScaled; + /// 대상 액터 높이로 배율을 다시 계산한 횟수 · 대상을 못 찾아 고정 배율로 떨어진 횟수. + public static int HeightResolved, HeightMissed; + /// 마지막으로 배율을 건 이름·배율(표본). + 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 s_map = new Dictionary(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; } } + + // ───────────────────────────────────────── 틱 + + /// 러너가 LateUpdate 마다 부른다. C8·주기 판정은 여기서 한다. + 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); + } + } + + /// 루트의 직속 자식을 훑는다. 처음 보는 것은 등록(예산 소모)하고, 아는 것은 활성/비활성만 반영한다. + 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; + } + + /// 처음 보는 인스턴스를 등록한다(이름 1회 읽기 = 유일한 할당). 슬롯이 없으면 −1. + 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; + } + + /// 활성이면 배율을 대입하고 비활성이면 되돌린다(둘 다 멱등). + 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--; + } + } + + /// 고정 배율 · (옵션) 대상 액터 높이 기반 재계산. + 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)); + } + + /// 이 이펙트가 따라다니는 대상 액터의 실높이(발밑→머리). 원본 private 필드라 리플렉션(캐시 1회). + 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() : 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 · 정리 + + /// WL 스폰 지점용(후속 1줄 연결). 스폰 직후 부르면 그 인스턴스를 즉시 등록·적용한다. + 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); + } + + /// 적용된 배율을 전부 되돌린다(C8 · 씬 전환 · 프로브). + 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; + } + + /// 파괴된 인스턴스 슬롯을 정리한다(슬롯이 꽉 찼을 때 · 씬 전환 뒤). + 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; + } + + /// 프로브용: 카운터·추적을 비운다(적용분은 먼저 원복한다). + 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; + } + } +} diff --git a/Assets/WL/Combat/Reaction/EffectScaleGuard.cs.meta b/Assets/WL/Combat/Reaction/EffectScaleGuard.cs.meta new file mode 100644 index 000000000..c481be0a1 --- /dev/null +++ b/Assets/WL/Combat/Reaction/EffectScaleGuard.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8115b1d4f8e35c709b2d9f6e4c803150 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/WL/Combat/Reaction/EffectScaleGuardRunner.cs b/Assets/WL/Combat/Reaction/EffectScaleGuardRunner.cs new file mode 100644 index 000000000..cab9e1a47 --- /dev/null +++ b/Assets/WL/Combat/Reaction/EffectScaleGuardRunner.cs @@ -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 +{ + /// EffectScaleGuard 전용 러너. DontDestroyOnLoad · LateUpdate 1개. + 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(); + } + + void LateUpdate() { EffectScaleGuard.Tick(Time.unscaledTime); } + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] + static void Boot() + { + if (WLEffectScaleSettings.Enabled) Ensure(); // 꺼져 있으면 오브젝트 0(C8) + } + } +} diff --git a/Assets/WL/Combat/Reaction/EffectScaleGuardRunner.cs.meta b/Assets/WL/Combat/Reaction/EffectScaleGuardRunner.cs.meta new file mode 100644 index 000000000..d875c4271 --- /dev/null +++ b/Assets/WL/Combat/Reaction/EffectScaleGuardRunner.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8115c2e509f46d81ac3ea07f5d914261 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/WL/Combat/Reaction/WLEffectScaleSettings.cs b/Assets/WL/Combat/Reaction/WLEffectScaleSettings.cs new file mode 100644 index 000000000..af5a51736 --- /dev/null +++ b/Assets/WL/Combat/Reaction/WLEffectScaleSettings.cs @@ -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 +{ + /// 인스턴스 이름(끝의 "(Clone)" 제외)과 규칙 문자열을 어떻게 맞추나. + public enum EffectScaleMatch + { + /// 이름 전체가 같다(기본). + Exact = 0, + /// 이름이 이 문자열로 시작한다(계열 일괄 규칙). + Prefix = 1, + /// 이름 안에 이 문자열이 있다. + 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; + + /// 설정 에셋(1회 로드 후 캐시). 없으면 null. + public static WLEffectScaleSettings Instance + { + get + { + if (!s_lookupDone) { s_cached = Resources.Load(ResourcesPath); s_lookupDone = true; } + return s_cached; + } + } + + /// QA·A/B 전용 런타임 스위치. 0 = 에셋의 enabled · 1 = 강제 on · -1 = 강제 off. + public static int RuntimeOverride; + + /// 가드가 살아 있는가. false 면 스윕도 러너도 없다(C8). + public static bool Enabled + { + get + { + var i = Instance; + if (i == null || RuntimeOverride < 0) return false; + return RuntimeOverride > 0 || i.enabled; + } + } + + /// 테스트용: 캐시를 비운다. + 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]; + + /// 인스턴스 이름으로 규칙을 찾는다(선형 · alloc 0). 없으면 −1. + 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; + } + + /// "<이름>(Clone)" 에서 "(Clone)" 을 뺀 유효 길이(부분 문자열을 만들지 않는다 = alloc 0). + 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; + } + + /// 부분 문자열 생성 없이 이름을 맞춘다(alloc 0). + 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; + } + } + } +} diff --git a/Assets/WL/Combat/Reaction/WLEffectScaleSettings.cs.meta b/Assets/WL/Combat/Reaction/WLEffectScaleSettings.cs.meta new file mode 100644 index 000000000..19a06a319 --- /dev/null +++ b/Assets/WL/Combat/Reaction/WLEffectScaleSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8115a0c3e7d24b6f9a1c8e5d3b7f2049 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/WL/Combat/Settings/Resources/WL/WLEffectScaleSettings.asset b/Assets/WL/Combat/Settings/Resources/WL/WLEffectScaleSettings.asset new file mode 100644 index 000000000..42726d62b --- /dev/null +++ b/Assets/WL/Combat/Settings/Resources/WL/WLEffectScaleSettings.asset @@ -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 diff --git a/Assets/WL/Combat/Settings/Resources/WL/WLEffectScaleSettings.asset.meta b/Assets/WL/Combat/Settings/Resources/WL/WLEffectScaleSettings.asset.meta new file mode 100644 index 000000000..798eaa390 --- /dev/null +++ b/Assets/WL/Combat/Settings/Resources/WL/WLEffectScaleSettings.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8115d3f61a057e92bd4fb1806ea25372 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/WL/Combat/Settings/Resources/WL/WLSkillSpectacleSettings.asset b/Assets/WL/Combat/Settings/Resources/WL/WLSkillSpectacleSettings.asset index 91ff28244..a5af62752 100644 --- a/Assets/WL/Combat/Settings/Resources/WL/WLSkillSpectacleSettings.asset +++ b/Assets/WL/Combat/Settings/Resources/WL/WLSkillSpectacleSettings.asset @@ -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