From 682c3f67d92a2556a5c16a7cafaf9cf7734b95de Mon Sep 17 00:00:00 2001 From: swrring Date: Wed, 21 Oct 2026 13:05:18 +0900 Subject: [PATCH] =?UTF-8?q?WL=20#958=20PD=20=EC=A7=80=EC=8B=9C=206?= =?UTF-8?q?=EA=B1=B4(=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=94=AC=20=ED=8E=B8?= =?UTF-8?q?=EC=9D=98=20=C2=B7=20=EB=8C=80=EA=B2=80=20=C2=B7=20=EB=B9=84?= =?UTF-8?q?=EC=88=98)=20+=20=EA=B6=A4=EC=A0=81=20=EC=9D=B4=ED=8E=99?= =?UTF-8?q?=ED=8A=B8=20Play=20=EA=B0=84=20=EB=A7=89=ED=9E=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ① 숫자 키 1~9 · 0 = 그 번호 무기로 즉시 교체(WLSkillTestHarness.SelectWeapon · 번호 = O/P 순환과 같은 종류 순 · 화면 목록) · 이 씬에서 등급 치트 끔 - ② 모든 무기 각성 상태(WLTestScene.AllAwakened → 무기 기술 · 연계 · 파츠 스킬 칸 해금 등급 무시) · 자동 스킬 끔(ManualSkills → PCActor 자동 시전 · WL_TryAutoSkillNow · 파츠 스킬 자동 시전) — Q/E 치트키로만 · 실측 해머 8 s 공격 427 프레임 · 스킬 진입 0 · E = 대지 분쇄 발동 - ③ 대검 주력 모으기 = 검 빛 손잡이 금색 → 칼끝 노랑 축 그라데이션(WL/WeaponGlow _GlowColor2 · _GradAxis · _GradLen · WLWeaponGlowFx 5인자 Set) + 모으기 입자 수명색 노랑 계열(TintGradient · 어두운 층 제외) · 파란 호 = 모으기 사본 루트 ChargePath 렌더러 끔(chargeFxHide) - ④ 대검 각성 회오리 흰 뭉개짐 = WL_Fx_BladeStormTornado 의 HS 재질 _Emission 5(블룸 없는 화면에서 흰색 포화 · 층별 끄기로 확정) → wraps layers 로 몸통 _Emission 0.7 + 푸른 회색 · 회전 줄무늬 2.5 · 바닥 고리 1.8(프로퍼티 블록) - ⑤ 비수 3타 = 이펙트와 같은 반시계 1080°/s 한 바퀴(−360°/0.333 s) · 팔라딘 클립 자체 시계 300°/0.18 s 회전을 덮어씀(WLSwingExtras yaw 덮기 · 끝 0.12 s 섞기) · 실측 엉덩이 각이 코드 각을 그대로 따라감 - ⑥ 비수 주력 = 양손 작은 사본 끔 · 대상 쪽 공격 범위 × 0.5 자리에 1개(폭 = 범위 × 1.6) · 실측 거리 0.50 · 범위 1.00 · 배율 0.304 - 발견 수정: WLImpactFx.s_swingAt(같은 PC·단계 궤적 0.3 s 묶음)가 도메인 리로드 꺼짐으로 Play 사이에 남아 새 Play 의 궤적 이펙트 · 비수 3타 회전이 막힘(실측 swing +0 · spins +0) → Play 시작마다 비움 · 연속 Play 2회 확인(swing +6 · spins +3) Co-Authored-By: Claude Opus 5.5 --- Assets/Script/Character/PCActor.cs | 6 ++ .../Impact/Resources/WL/WLImpactFx.json | 8 +- Assets/WL/Combat/Impact/WLImpactFx.cs | 41 ++++++-- .../Weapon/Resources/WL/WLBladeStorm.json | 8 +- .../Weapon/Resources/WL/WLGreatWave.json | 21 +++- .../Weapon/Resources/WL/WLShadowSlash.json | 11 ++- Assets/WL/Combat/Weapon/WLBladeStorm.cs | 40 +++++++- Assets/WL/Combat/Weapon/WLGreatWave.cs | 58 ++++++++++- Assets/WL/Combat/Weapon/WLShadowSlash.cs | 23 ++++- Assets/WL/Combat/Weapon/WLSkillChain.cs | 1 + Assets/WL/Combat/Weapon/WLWeaponGlowFx.cs | 29 +++++- Assets/WL/Combat/Weapon/WLWeaponGradeCheat.cs | 1 + Assets/WL/Combat/Weapon/WLWeaponSkillSlots.cs | 1 + .../Character/Shaders/WLWeaponGlow.shader | 13 ++- Assets/WL/PartSkill/WLPartSkillRunner.cs | 1 + Assets/WL/PartSkill/WLPartSkillState.cs | 2 +- .../SkillTest/Resources/WL/WLSkillTest.json | 4 + Assets/WL/SkillTest/WLSkillTestHarness.cs | 98 ++++++++++++++++--- 18 files changed, 319 insertions(+), 47 deletions(-) diff --git a/Assets/Script/Character/PCActor.cs b/Assets/Script/Character/PCActor.cs index 084bfec52..5b8a41581 100644 --- a/Assets/Script/Character/PCActor.cs +++ b/Assets/Script/Character/PCActor.cs @@ -72,6 +72,9 @@ public class PCActor : MyActor /// WL #900 — i 칸 스킬을 원본 자동 시전(Update :98-105)과 같은 규칙(쿨 · 자동 간격 · 사거리 · CC)으로 지금 쓴다. 성공하면 true(대쉬 도착 즉시 시전). public bool WL_TryAutoSkillNow(int i, bool ignoreAutoGap = false) { +#if UNITY_EDITOR + if (WL.SkillTest.WLTestScene.ManualSkills) return false; // WL #958 스킬 테스트 씬 = 자동 스킬 끔(Q · E 치트키로만) +#endif if (arr_magicData == null || i < 0 || i >= arr_magicData.Length) return false; var d = arr_magicData[i]; if (DSUtil.CheckNull(d) || d.e_SkillType == eSkillType.Passive || arr_magicCoolTime[i] > 0f || (!ignoreAutoGap && t_autoskill > 0f)) return false; @@ -109,6 +112,9 @@ public class PCActor : MyActor if (!IsEnemy()) arr_PCSkillCard[i].Set_CoolTime(arr_magicCoolTime[i], arr_magicMaxCoolTime[i], i); +#if UNITY_EDITOR + if (WL.SkillTest.WLTestScene.ManualSkills) continue; // WL #958 스킬 테스트 씬 = 자동 스킬 끔(쿨 감소 · 카드 표시는 위에서 그대로) +#endif if (!IsStop && m_Auto && !IsDead() && !DSUtil.CheckNull(arr_magicData[i]) && arr_magicCoolTime[i] <= 0f && t_autoskill <= 0f && !IsCC() && CurAnim < eAnim.Skill && isTarget && Vector3.Distance(Get_position(), m_Target.Get_position()) <= arr_magicData[i].f_SkillRange) { diff --git a/Assets/WL/Combat/Impact/Resources/WL/WLImpactFx.json b/Assets/WL/Combat/Impact/Resources/WL/WLImpactFx.json index 180c21c4e..627b0e916 100644 --- a/Assets/WL/Combat/Impact/Resources/WL/WLImpactFx.json +++ b/Assets/WL/Combat/Impact/Resources/WL/WLImpactFx.json @@ -120,9 +120,11 @@ "massLocal": [ 0, 0, 0 ], "fixedDir": false, "lockFacing": false, - "spinDegrees": 360, - "spinSeconds": 0.9, - "spinFx": true, + "spinDegrees": -360, + "spinSeconds": 0.333, + "spinFx": false, + "spinBlendOut": 0.12, + "_note_958": "#958 PD 「비수 3타 플레이어 회전 속도를 더 빠르게(이펙트와 일치하게)」 — 편집 모드 실측(958_spin.cs): 이펙트 자체 회전 = 칼날 층 slash_0 −1080°/s · slash (1) −1355 · slash (2) −1150 (루트 위 축 · 음수 = 위에서 보면 반시계) · 몸은 +360°/0.9 s = 400°/s 시계 방향이라 반대 방향 · 2.7배 느렸다 → −360°/0.333 s(= −1080°/s · 같은 방향 같은 속도) · 이펙트는 이미 스스로 돌므로 몸 각도로 한 번 더 돌리지 않음(spinFx false · 켜면 2160°/s 로 보임) · Play 프레임 실측(SpinTrace): 팔라딘 Around 클립 자체가 엉덩이를 시계 방향 약 300°/0.18 s 돌려 더하기 방식이면 상쇄 → 클립 yaw 를 덮어씀(WLSwingExtras · 끝 0.12 s 동안 클립 자세로 섞음)", "_note_956": "#956 PD 「3타 때 몸을 회전하지 않는 것 같다 → 몸을 회전 1회하며 공격 · 이펙트도 회전 모션에 맞게」 — Play 실측: m_animation.transform 은 PC 루트 자체(Animator 가 루트)라 #955 2차 「모델 자식 회전」도 루트 회전이었고 원본 LookAtTarget 이 매 프레임 되돌려 Attack3 클립당 61° 만 남음 → 코드 회전을 엉덩이 뼈(Root_M)에 누적 각으로 적용(루트·판정·카메라 그대로) + PC 에 붙은 이 이펙트를 같은 각으로(spinFx)", "_note_955b": "Play 실측: 팔라딘 Around 클립은 모델 yaw 36° 만 돌아(회전이 포즈에 안 구워짐) → 코드 회전 360°/0.9 s(클립 1.33 s ÷ 1.3)", "_note_955": "#955 PD 「3타 이펙트 범위가 단검 리치보다 과하고 회전 이펙트가 아님 → 실제로 한바퀴 회전하며 공격 + 회전하는 녹색 이펙트」 — 동작 = 매핑 3타를 Paladin skill_spinslash.FBX(Sword&Shield_Skill7_Around 1.33 s · 속도 1.3)로 · 이펙트 = 원본 Skill_SpinSlash(회전 칼날 고리 · 편집 모드 대조표 955_fx_sheet) 사본 WL_Impact_DaggerWhirl(스크립트·충돌체 제거 · 녹색 tint 재질 사본) PC 에 붙어 함께 돎 · 배율 1.0(실제 0.64 · 고리 반지름 ≈ 0.8 m 추정 = 단검 리치) · spinDegrees 0 = 클립 자체 회전(실측 뒤 안 돌면 360)", diff --git a/Assets/WL/Combat/Impact/WLImpactFx.cs b/Assets/WL/Combat/Impact/WLImpactFx.cs index 8c0a55300..8b3c0fbf9 100644 --- a/Assets/WL/Combat/Impact/WLImpactFx.cs +++ b/Assets/WL/Combat/Impact/WLImpactFx.cs @@ -37,6 +37,8 @@ namespace WL.Combat.Impact public float blinkDistance; public int blinkGhosts = 4; public float camHoldSeconds; public float camZoomOut; public float spinDegrees; public float spinSeconds = 0.6f; // #956 PD 「3타 때 몸을 회전 1회하며 공격 · 이펙트도 회전 모션에 맞게」 — spinFx: PC 에 붙은(follow) 이펙트를 몸과 같은 각으로 돌린다(기본 켬) public bool spinFx = true; + // #958 회전은 클립 yaw 를 덮어쓴다(클립 자체 회전과 상쇄 방지) · spinBlendOut = 끝난 뒤 애니메이터 자세로 되돌아가는 시간(초) + public float spinBlendOut = 0.12f; // #943-⑤ PD 「타격하려는 방향을 고정한 채 이펙트가 재생되는 동안 타격 판정 … 이펙트가 캐릭터 방향에 따라 마구 바뀌면 안 됨」 — fixedDir = 띄우는 순간 PC 정면(LookAtTarget 뒤 = 표적 방향)을 잡아 두고 life 동안 이펙트 방향 고정 + PC 회전도 그 방향으로 잠금(공격 상태인 동안) · 자리 = PC 중심 + 정면 alignForward + 오른쪽 alignSide + 위 yOffset − R × massLocal × 배율(PC 위치는 따라감) public bool fixedDir; public float alignForward = 0.5f; public float alignSide; public bool lockFacing = true; } /// 지면 충격파. tipContact = 칼끝이 지면(contactHeight 이내)에 닿는 프레임·자리에서 재생(단계 안에서 minDrop 이상 올라갔다 내려올 때) · 못 잡으면 timeout 뒤 최저점(fallbackHeight 이내)에. @@ -650,6 +652,7 @@ namespace WL.Combat.Impact { foreach (var kv in s_pool) foreach (var g in kv.Value) if (g != null) Object.Destroy(g); s_pool.Clear(); s_prefabs.Clear(); s_body.Clear(); s_profiles.Clear(); s_skillHitAt.Clear(); + s_swingAt.Clear(); // #958 도메인 리로드 꺼짐 — 지난 Play 의 시각(Time.time)이 남으면 새 Play(시각 0부터)의 같은 PC·단계 궤적 이펙트가 「throttled」로 막혀 비수 3타 이펙트·몸 회전이 안 나왔다(실측: 4타 콤보 6 s · swing +0 · spins +0) } } @@ -658,6 +661,7 @@ namespace WL.Combat.Impact public sealed class WLSwingExtras : MonoBehaviour { PCActor _pc; float _spinLeft, _spinRate, _spinAcc; bool _spinning; Transform _spinBone; GameObject _fx; Vector3 _fxPos; Quaternion _fxRot; + int _axis; float _yaw0, _blendDur, _blendT; bool _blending; // #958 yaw 덮어쓰기 — 시작 때 엉덩이 수평 축 · 그때 yaw · 끝난 뒤 애니메이터 자세로 섞는 시간 public static int Blinks, Spins, CamHolds; public static string LastLog = "", SpinLog = ""; public static void Begin(PCActor pc, WLSwingFxRule r, GameObject fx) { @@ -680,6 +684,9 @@ namespace WL.Combat.Impact // 「모델 자식 회전」(#955 2차)도 루트 회전이었고 원본 LookAtTarget 이 매 프레임 되돌려 클립당 61° 만 남았다 → 엉덩이 뼈(Humanoid Hips = Root_M)를 루트 축으로 // 누적 각만큼 LateUpdate(애니메이션 평가 뒤)에서 돌린다 — 애니메이터가 뼈를 매 프레임 다시 쓰므로 델타가 아니라 누적 각 · 루트·판정 방향·카메라는 그대로(보이는 몸만) · PC 에 붙은 이펙트는 같은 각으로 ex._spinBone = SpinBone(pc); ex._spinLeft = r.spinDegrees; ex._spinAcc = 0f; ex._spinRate = r.spinDegrees / Mathf.Max(0.05f, r.spinSeconds); ex._spinning = ex._spinBone != null; + // #958 프레임 실측(958_probe SpinTrace): 팔라딘 Around 클립 자체가 엉덩이를 시계 방향 약 300°/0.18 s 돌려 코드 회전(반시계)과 상쇄됐다 → 더하지 않고 「시작 yaw + 누적 각」으로 덮어쓴다(클립의 yaw 변화 제거) + ex._blending = false; ex._blendDur = Mathf.Max(0f, r.spinBlendOut); + if (ex._spinBone != null) { ex._axis = HorizAxis(ex._spinBone); ex._yaw0 = Yaw(ex._spinBone, pc.transform, ex._axis); } ex._fx = r.spinFx && fx != null && fx.transform.parent == pc.transform ? fx : null; if (ex._fx != null) { ex._fxPos = fx.transform.localPosition; ex._fxRot = fx.transform.localRotation; } Spins++; SpinLog = (ex._spinning ? "spin " + r.spinDegrees + "°/" + r.spinSeconds + " s · 뼈 " + ex._spinBone.name : "spin 뼈 없음") + (ex._fx != null ? " · fx 함께" : " · fx 없음"); @@ -692,18 +699,32 @@ namespace WL.Combat.Impact if (b == null) foreach (Transform c in pc.transform) if (c.name.StartsWith("Root")) { b = c; break; } return b; } + /// #958 엉덩이 뼈 축 중 가장 수평인 것(0 right · 1 up · 2 forward) — 이 PC 의 Root_M 은 축이 기울어 오일러 y 로는 위 축 회전을 못 잰다(실측). + static int HorizAxis(Transform b) { Vector3[] ax = { b.right, b.up, b.forward }; int best = 0; float bd = 2f; for (int i = 0; i < 3; i++) { float d = Mathf.Abs(Vector3.Dot(ax[i], Vector3.up)); if (d < bd) { bd = d; best = i; } } return best; } + /// #958 그 축의 수평 투영이 루트 정면에서 위 축 둘레로 몇 도(시계 +). + static float Yaw(Transform b, Transform root, int axis) + { + Vector3 a = axis == 0 ? b.right : axis == 1 ? b.up : b.forward; a = Vector3.ProjectOnPlane(a, root.up); var f = Vector3.ProjectOnPlane(root.forward, root.up); + return a.sqrMagnitude > 1e-6f && f.sqrMagnitude > 1e-6f ? Vector3.SignedAngle(f, a, root.up) : 0f; + } + public static float LastSpinErr; // 검증용 — 마지막 프레임의 (목표 yaw − 덮은 뒤 yaw) void LateUpdate() { - if (!_spinning || _pc == null) return; - if (_spinBone == null) { _spinning = false; _fx = null; return; } - float step = _spinRate * Time.deltaTime; if (Mathf.Abs(step) > Mathf.Abs(_spinLeft)) step = _spinLeft; - _spinLeft -= step; _spinAcc += step; - bool done = Mathf.Approximately(_spinLeft, 0f); - float a = done ? 0f : _spinAcc; // 끝 = 애니메이터 포즈 그대로(원상) · 이펙트도 처음 자리 - var root = _pc.transform; - if (a != 0f) _spinBone.RotateAround(root.position, root.up, a); // 루트 발 위치 · 위 축으로 몸 전체(뼈 아래 양손 무기 포함)를 돌린다 - if (_fx != null) { if (!_fx.activeInHierarchy || _fx.transform.parent != root) _fx = null; else { var q = Quaternion.Euler(0f, a, 0f); _fx.transform.localRotation = q * _fxRot; _fx.transform.localPosition = q * _fxPos; } } - if (done) { _spinning = false; _fx = null; } + if ((!_spinning && !_blending) || _pc == null) return; + if (_spinBone == null) { _spinning = _blending = false; _fx = null; return; } + var root = _pc.transform; float w = 1f; + if (_spinning) + { + float step = _spinRate * Time.deltaTime; if (Mathf.Abs(step) > Mathf.Abs(_spinLeft)) step = _spinLeft; + _spinLeft -= step; _spinAcc += step; + if (Mathf.Approximately(_spinLeft, 0f)) { _spinning = false; _blending = _blendDur > 0f; _blendT = 0f; if (_fx != null) { _fx.transform.localRotation = _fxRot; _fx.transform.localPosition = _fxPos; _fx = null; } } + } + else { _blendT += Time.deltaTime; w = 1f - Mathf.Clamp01(_blendT / Mathf.Max(0.01f, _blendDur)); if (w <= 0f) { _blending = false; return; } } + // 목표 yaw = 시작 yaw + 누적 각(끝 = 한 바퀴 = 시작 yaw) · 애니메이터가 이번 프레임에 쓴 자세의 yaw 와의 차이만큼 루트 발 위치 · 위 축으로 몸 전체(뼈 아래 양손 무기 포함)를 돌린다 + float cur = Yaw(_spinBone, root, _axis), d = Mathf.DeltaAngle(cur, _yaw0 + _spinAcc) * w; + if (Mathf.Abs(d) > 0.01f) _spinBone.RotateAround(root.position, root.up, d); + LastSpinErr = Mathf.DeltaAngle(Yaw(_spinBone, root, _axis), _yaw0 + _spinAcc); + if (_fx != null && _spinning) { if (!_fx.activeInHierarchy || _fx.transform.parent != root) _fx = null; else { var q = Quaternion.Euler(0f, _spinAcc, 0f); _fx.transform.localRotation = q * _fxRot; _fx.transform.localPosition = q * _fxPos; } } } } diff --git a/Assets/WL/Combat/Weapon/Resources/WL/WLBladeStorm.json b/Assets/WL/Combat/Weapon/Resources/WL/WLBladeStorm.json index b02d04c44..a137233ae 100644 --- a/Assets/WL/Combat/Weapon/Resources/WL/WLBladeStorm.json +++ b/Assets/WL/Combat/Weapon/Resources/WL/WLBladeStorm.json @@ -23,6 +23,7 @@ 0.0 ], "hideOriginal": 1, + "_note_958": "#958 PD 「대검 각성 회오리가 하얗게 뭉개져 보임 → 회오리 느낌이 나도록 회전 방향의 선이나 그라데이션(참고 이미지 = 만화풍 토네이도)」 — Play 층별 끄기(958_storm_layers) = 흰 뭉개짐은 WL_Fx_BladeStormTornado 층 · 원인 = HS_Blend 재질 _Emission 5(블룸 없는 화면에서 흰색 포화 · 편집 모드 렌더 958_tornado_views) → wraps 회오리 layers 로 몸통 _Emission 0.7 + 푸른 회색 _Color(0.55,0.68,0.92) · 회전 줄무늬(TrailsUp · TrailMiddle) 2.5 흰색 · 바닥 고리(WaveBorders) 1.8 흰색(편집 모드 비교 958_tor_zoom v2)", "moveMul": 0.5, "wraps": [ { @@ -39,7 +40,12 @@ "fx": "WL/Fx/WL_Fx_BladeStormTornado", "scale": 0.7, "height": -0.3, - "towardCamera": 1.2 + "towardCamera": 1.2, + "layers": [ + { "names": "TrailsUp|TrailMiddle", "emission": 2.5, "tint": [1.0, 1.0, 1.0] }, + { "names": "WaveBorders", "emission": 1.8, "tint": [1.0, 1.0, 1.0] }, + { "names": "*", "emission": 0.7, "tint": [0.55, 0.68, 0.92] } + ] } ], "hideBody": 0, diff --git a/Assets/WL/Combat/Weapon/Resources/WL/WLGreatWave.json b/Assets/WL/Combat/Weapon/Resources/WL/WLGreatWave.json index 57ae528bc..8690cbec5 100644 --- a/Assets/WL/Combat/Weapon/Resources/WL/WLGreatWave.json +++ b/Assets/WL/Combat/Weapon/Resources/WL/WLGreatWave.json @@ -30,10 +30,25 @@ "groundPad": 0.0, "glowMaterial": "WL/Fx/WL_Mat_WeaponGlow", "glowColor": [ - 2.0, - 2.0, - 2.0 + 1.6, + 0.75, + 0.08 ], + "glowColor2": [ + 1.8, + 1.55, + 0.35 + ], + "chargeFxGradient": [ + 1.0, 0.97, 0.7, + 1.0, 0.8, 0.25, + 1.0, 0.55, 0.08 + ], + "chargeFxHide": [ + "WL_Fx_GreatChargeWhite" + ], + "_note_958b": "#958 PD 「대검 주력 스킬 발동 중 파란색 궤적 이펙트는 보이지 않도록」 — 진단 Play(958_great_charge · P958.Fx): 모으기 0.35 s 에 보이는 호 = 모으기 사본 WL_Fx_GreatChargeWhite 루트 입자계(재질 ChargePath · 11개 · 폭 7.3 m · PD 캡처의 하늘색 호와 크기·자리 같음 · 같은 사본의 Circle · WL_Halo 시작색 청록 0.45,1,0.86) · 검기(WL_Fx_GreatSlashFly)는 빨강 계열 · 기본 공격 먹물 궤적(WL_Impact_Swing_Ink)은 빨강+초록 → 루트 호 렌더러만 끔(chargeFxHide) · 청록 층은 노란 그라데이션으로 바뀜", + "_note_958": "#958 PD 「대검 주력 스킬 모으기에 그라데이션 형태의 색상(노란색 계열)」 — 검 빛 = 손잡이 쪽 glowColor(진한 금색) → 칼끝 쪽 glowColor2(밝은 노랑) 축 그라데이션(WL/WeaponGlow _GlowColor2 · WLWeaponGlowFx 5인자 Set) · 모으기 입자(WL_Fx_GreatChargeWhite) = 수명 색 연노랑 → 금색 → 주황(chargeFxGradient · 시작색 흰색) · 이전 = 흰색 [2,2,2] 단색", "glowMax": 1.0, "glowFill": 0.35, "glowRimPower": 2.5, diff --git a/Assets/WL/Combat/Weapon/Resources/WL/WLShadowSlash.json b/Assets/WL/Combat/Weapon/Resources/WL/WLShadowSlash.json index c0ec06193..2112cf4dd 100644 --- a/Assets/WL/Combat/Weapon/Resources/WL/WLShadowSlash.json +++ b/Assets/WL/Combat/Weapon/Resources/WL/WLShadowSlash.json @@ -9,20 +9,25 @@ "fxScale": 0.85, "fxDelay": 0.5, "fxForward": 0.45, - "fxYOffset": 0.3, + "fxYOffset": 0.0, "fxLife": 2.5, + "fxAimTarget": 1, + "fxRangeFrac": 0.5, + "fxRangeWidth": 1.6, + "fxPrefabWidth": 5.26, "hideOriginalFx": 1, "slashStates": [ "attack", "DualBlades_Attack2" ], "slashInterval": 0.28, "slashSpeed": 1.8, "slashSeconds": 2.0, - "fxMain": 0, - "fxPerSlash": 1, + "fxMain": 1, + "fxPerSlash": 0, "fxSlashScale": 0.35, "fxSlashLife": 0.6, "fxSlashFraction": 0.6, "fxSlashYOffset": 0.0, "fxSlashFollow": 1, + "_note_958": "#958 PD 「주력 이펙트가 양손에 작게 하나씩 → 무기 방향 전방에 다른 무기 궤적처럼 하나만 · 전방의 적이 있는 방향에 1개 · 플레이어가 타겟하려는 대상의 무기 공격 범위가 중점」 — 양손 난사 사본 끔(fxPerSlash 0) · 정면 사본 1개(fxMain 1 · 0.5 s) · 방향 = 대상 쪽(fxAimTarget 1) · 중심 = PC 몸 중심 + 공격 범위(WL_AttackRange) × 0.5(대상이 더 가까우면 대상 자리) · 폭 = 공격 범위 × 1.6(배율 = 폭 ÷ 프리팹 단위 초승달 폭 5.26 m · #956 편집 모드 실측 · fxScale 무시) · 높이 0 = 몸 중심", "_note_956": "#956 PD 「주력 이펙트가 너무 먼 위치(정면 1.2 m)에 재생 → 플레이어가 휘두르는 무기 위치에 맞게(휘두르는 느낌)」 — 정면 1회 사본은 끔(fxMain 0 · 켜면 fxForward 0.45 · 손 높이 0.3) · 대신 난사 루프 0.28 s 마다 같은 프리팹의 작은 사본(0.35 · 편집 모드 실측 프리팹 단위 초승달 AABB 5.26 m → 약 1.8 m · 초승달 수명 0.30 s · 2번째 초승달 시차)을 양손 소켓(w_bladedancer_l/r) 손잡이→날 끝 0.6 자리에 번갈아 띄우고 0.6 s 동안 날 방향(수평)·위치를 따라가게", "_note_955": "#955 PD 「주력 쓸 때 캐릭터가 공중에 뜬 채 행동 → 제자리에서 단검 난사」 — 원본 상태 사슬 DualBlades_Jump_Start → JumpAttack1·2·3(공중 자세)을 대신해 2.0 s 동안 1·2타 상태를 0.28 s 마다 번갈아(배속 1.8 · 애니메이션 이벤트 끔 = 추가 타격·궤적 없음 · 이동은 원본대로 금지) · 2.1 s 뒤 원본 순간이동·Play_Idle 그대로", "_note_954": "#954 PD 「비수 주력 = Slash Effects FREE 의 녹색 난사(스크린샷)로」 — 사본 WL_Fx_DaggerMulti(= Multiple Slash 2 · 초승달 2개가 시차로 · 시작색 teal) · 원본 베기(go_effect · 판정 상자 그대로) 렌더러 끔 · 원본이 베기를 켜는 0.5 s 에 PC 정면 1.2 m · 높이 +0.1 · 배율 0.6 · 2.5 s 뒤 파괴 · 연기(smoke)는 그대로" diff --git a/Assets/WL/Combat/Weapon/WLBladeStorm.cs b/Assets/WL/Combat/Weapon/WLBladeStorm.cs index 4b9fa1dae..7cecbca5e 100644 --- a/Assets/WL/Combat/Weapon/WLBladeStorm.cs +++ b/Assets/WL/Combat/Weapon/WLBladeStorm.cs @@ -84,12 +84,24 @@ namespace WL.Combat.Weapon // #927 위에서 비스듬히 보는 카메라에선 몸 가운데 깊이의 반투명 이펙트가 몸 앞면에 가려 몸이 이펙트 앞에 보인다 → // 카메라 쪽 시선 방향으로 이만큼(m) 당겨 화면 위치는 그대로 두고 깊이만 몸보다 앞에(0 = 당기지 않음) public float towardCamera = 0f; + // #958 PD 「대검 각성 회오리가 하얗게 뭉개져 보임 → 회오리 느낌이 나도록 회전 방향의 선이나 그라데이션이 보이게(참고 이미지)」 — 층별 재질 값 덮기 + public WLFxLayerOverride[] layers = new WLFxLayerOverride[0]; + } + + /// #958 이펙트 렌더러별 재질 값 덮기(프로퍼티 블록 · 재질 인스턴스 0) — names = 렌더러 이름(| 로 여럿 · * = 나머지 전부) · + /// emission ≥ 0 = _Emission(HS 셰이더 발광 배율 · 원본 5 는 블룸 없는 화면에서 흰색 포화 = 「하얗게 뭉개짐」 편집 모드 실측) · tint = _Color(r,g,b) + [System.Serializable] + public class WLFxLayerOverride + { + public string names = "*"; + public float emission = -1f; + public float[] tint = new float[0]; } public static class WLBladeStorm { public const string ResourcesPath = "WL/WLBladeStorm"; - public static int Taken, Ticks, Hits, FxSpawned, Ends, Aborts, Errors; + public static int Taken, Ticks, Hits, FxSpawned, Ends, Aborts, Errors, LayerOverrides; public static float LastSpinTotal, LastSeconds, LastFade; public static int HitFxShown, BodyOnTop; public static string LastLog = ""; @@ -275,6 +287,31 @@ namespace WL.Combat.Weapon _qR.Clear(); _qM.Clear(); _qNew.Clear(); } + static readonly MaterialPropertyBlock s_mpb = new MaterialPropertyBlock(); + /// #958 층별 _Emission · _Color 덮기 — 덮은 렌더러 수. + static int ApplyLayers(GameObject go, WLFxLayerOverride[] layers) + { + if (go == null || layers == null || layers.Length == 0) return 0; + int n = 0; + foreach (var r in go.GetComponentsInChildren(true)) + { + var m = r.sharedMaterial; if (m == null) continue; + WLFxLayerOverride hit = null, def = null; + foreach (var L in layers) + { + if (L == null || string.IsNullOrEmpty(L.names)) continue; + if (L.names == "*") { if (def == null) def = L; continue; } + if (System.Array.IndexOf(L.names.Split('|'), r.name) >= 0) { hit = L; break; } + } + var o = hit ?? def; if (o == null) continue; + r.GetPropertyBlock(s_mpb); + if (o.emission >= 0f && m.HasProperty("_Emission")) s_mpb.SetFloat("_Emission", o.emission); + if (o.tint != null && o.tint.Length >= 3 && m.HasProperty("_Color")) s_mpb.SetColor("_Color", new Color(o.tint[0], o.tint[1], o.tint[2], 1f)); + r.SetPropertyBlock(s_mpb); n++; + } + return n; + } + void SpawnWraps() { if (_c.wraps == null) return; @@ -286,6 +323,7 @@ namespace WL.Combat.Weapon if (prefab == null) { WLBladeStorm.LastLog = "감싸기 이펙트 없음 " + w.fx; continue; } var go = Instantiate(prefab, _pc.Get_position() + Vector3.up * w.height, prefab.transform.rotation); go.transform.localScale = prefab.transform.localScale * Mathf.Max(0.01f, w.scale); + int ov = ApplyLayers(go, w.layers); if (ov > 0) WLBladeStorm.LayerOverrides += ov; // #958 _wraps.Add(go); _wrapH.Add(w.height); _wrapT.Add(w.towardCamera); WLBladeStorm.FxSpawned++; } } diff --git a/Assets/WL/Combat/Weapon/WLGreatWave.cs b/Assets/WL/Combat/Weapon/WLGreatWave.cs index 64cb9b824..fc194384a 100644 --- a/Assets/WL/Combat/Weapon/WLGreatWave.cs +++ b/Assets/WL/Combat/Weapon/WLGreatWave.cs @@ -65,6 +65,11 @@ namespace WL.Combat.Weapon // 모으기 연출 public string glowMaterial = "WL/Fx/WL_Mat_WeaponGlow"; public float[] glowColor = { 1.2f, 2.8f, 2.4f }; + // #958 PD 「대검 주력 스킬 모으기에 그라데이션 형태의 색상(노란색 계열)」 — glowColor2 = 칼끝 쪽 색(glowColor = 손잡이 쪽 · 비면 단색) · chargeFxGradient = 모으기 입자 수명 색(r,g,b 셋씩 2개 이상 · 비면 원래 색) + public float[] glowColor2 = new float[0]; + public float[] chargeFxGradient = new float[0]; + // #958 PD 「대검 주력 스킬 발동 중 파란색 궤적 이펙트는 보이지 않도록」 — 모으기 이펙트에서 끌 입자계 이름(사본 이름의 (Clone) 무시 · 호 모양 ChargePath = 루트) + public string[] chargeFxHide = new string[0]; public float glowMax = 1f, glowFill = 0.35f, glowRimPower = 2.5f, glowFadeSeconds = 0.25f; public string chargeFx = ""; public float chargeFxScale = 0.12f; @@ -194,6 +199,41 @@ namespace WL.Combat.Weapon } internal static float Get(float[] a, int i, float d) { return a != null && a.Length > i ? a[i] : d; } + + /// #958 이름이 names 에 든 입자계의 렌더러를 끈다(이름 끝 (Clone) 무시) — 끈 수. + internal static int HideLayers(GameObject go, string[] names) + { + if (go == null || names == null || names.Length == 0) return 0; + int n = 0; + foreach (var ps in go.GetComponentsInChildren(true)) + { + string nm = ps.name.Replace("(Clone)", "").Trim(); + if (System.Array.IndexOf(names, nm) < 0) continue; + var r = ps.GetComponent(); if (r != null && r.enabled) { r.enabled = false; n++; } + } + return n; + } + + /// #958 입자 색을 수명 그라데이션(g = r,g,b 셋씩 · 2색 이상)으로 — 시작색은 흰색(알파 유지) · 수명 알파는 원래 것(없으면 1). + internal static void TintGradient(GameObject go, float[] g) + { + if (go == null || g == null || g.Length < 6) return; + int n = g.Length / 3; var keys = new GradientColorKey[Mathf.Min(n, 8)]; + for (int i = 0; i < keys.Length; i++) keys[i] = new GradientColorKey(new Color(g[i * 3], g[i * 3 + 1], g[i * 3 + 2]), (float)i / (keys.Length - 1)); + foreach (var ps in go.GetComponentsInChildren(true)) + { + var sc0 = ps.main.startColor; + if (sc0.mode == ParticleSystemGradientMode.Color && Mathf.Max(sc0.color.r, Mathf.Max(sc0.color.g, sc0.color.b)) < 0.25f) continue; // 어둡게 까는 층(DarkBG)은 그대로 + var col = ps.colorOverLifetime; + GradientAlphaKey[] ak = col.enabled && col.color.mode == ParticleSystemGradientMode.Gradient && col.color.gradient != null + ? col.color.gradient.alphaKeys : new[] { new GradientAlphaKey(1f, 0f), new GradientAlphaKey(1f, 1f) }; + var gr = new Gradient(); gr.SetKeys(keys, ak); + col.enabled = true; col.color = new ParticleSystem.MinMaxGradient(gr); + var m = ps.main; var sc = m.startColor; + float a = sc.mode == ParticleSystemGradientMode.Color ? sc.color.a : 1f; + m.startColor = new Color(1f, 1f, 1f, a); + } + } } /// PC 에 붙는 시전 러너 — 동작(기본 4타) · 모으기 · 반짝임 · 내려치기 · 발사 · 내려친 자세 멈춤 · 끝 · 몸을 땅에 붙임(#926). @@ -288,7 +328,11 @@ namespace WL.Combat.Weapon _socket = sk; _tipLocal = tl; if (_socket != null) { _glow.Begin(_socket, c.glowMaterial); _midLocal = WLWeaponGlowFx.CenterLocal(_socket); } var col = new Color(WLGreatWave.Get(c.glowColor, 0, 1f), WLGreatWave.Get(c.glowColor, 1, 1f), WLGreatWave.Get(c.glowColor, 2, 1f), 1f); + _grad = c.glowColor2 != null && c.glowColor2.Length >= 3; // #958 손잡이 → 칼끝 그라데이션 + if (_grad) _col2 = new Color(c.glowColor2[0], c.glowColor2[1], c.glowColor2[2], 1f); _charge = WLGreatWave.Spawn(c.chargeFx, Blade(c.chargeFxAlong), Quaternion.identity, c.chargeFxScale, c.chargeSeconds + 1f); + WLGreatWave.TintGradient(_charge, c.chargeFxGradient); // #958 모으기 입자 노란 계열 그라데이션 + WLGreatWave.HideLayers(_charge, c.chargeFxHide); // #958 파란 호(ChargePath) 숨김 bool other = !string.IsNullOrEmpty(c.chargeState); string cs = other ? c.chargeState : c.state; float cn = other ? c.chargeNormalized : c.holdNormalized; float t = 0f; @@ -297,14 +341,14 @@ namespace WL.Combat.Weapon if (Broken()) { Finish(false); yield break; } Pin(cs, cn); float k = Mathf.Clamp01(t / Mathf.Max(0.01f, c.chargeSeconds)); - _glow.Set(c.glowMax * k, col, c.glowFill, c.glowRimPower); + GlowSet(c.glowMax * k, col); Face(Foe(), false); if (_shine == null && k >= c.shineStart && !string.IsNullOrEmpty(c.shineFx)) _shine = WLGreatWave.Spawn(c.shineFx, Blade(c.shineFrom), Quaternion.identity, c.shineScale, c.chargeSeconds + 3f); _shineAlong = Mathf.Lerp(c.shineFrom, c.shineTo, Mathf.InverseLerp(c.shineStart, 1f, k)); t += Time.deltaTime; yield return null; } - _glow.Set(c.glowMax, col, c.glowFill, c.glowRimPower); + GlowSet(c.glowMax, col); StopFx(ref _charge, 0.6f); _shineAlong = c.shineTo; // ③ 내려치기 @@ -368,7 +412,15 @@ namespace WL.Combat.Weapon void Fade(float since, Color col) { - _glow.Set(_c.glowMax * (1f - Mathf.Clamp01((Time.time - since) / Mathf.Max(0.01f, _c.glowFadeSeconds))), col, _c.glowFill, _c.glowRimPower); + GlowSet(_c.glowMax * (1f - Mathf.Clamp01((Time.time - since) / Mathf.Max(0.01f, _c.glowFadeSeconds))), col); + } + + // #958 단색(glowColor) 또는 손잡이 glowColor → 칼끝 glowColor2 그라데이션 + Color _col2; bool _grad; + void GlowSet(float g, Color col) + { + if (_grad) _glow.Set(g, col, _col2, _c.glowFill, _c.glowRimPower); + else _glow.Set(g, col, _c.glowFill, _c.glowRimPower); } /// #929 내려친 자리(칼끝을 땅으로 · 없으면 시전자 앞 waveStartForward) 지면 이펙트 + 검기가 지나갈 길(시전자 정면 · 앞머리 시작 → waveDistance) 지면 이펙트. diff --git a/Assets/WL/Combat/Weapon/WLShadowSlash.cs b/Assets/WL/Combat/Weapon/WLShadowSlash.cs index 5381a296c..26cc4333f 100644 --- a/Assets/WL/Combat/Weapon/WLShadowSlash.cs +++ b/Assets/WL/Combat/Weapon/WLShadowSlash.cs @@ -33,6 +33,11 @@ namespace WL.Combat.Weapon // #954 PD 「비수 주력 = Slash Effects FREE 의 녹색 난사(Multiple Slash 2)로」 — 원본 베기 렌더러를 끄고(판정 상자 그대로) WL 사본을 같은 자리·시각에 띄운다 public string fxPrefab = ""; // Resources 경로(비우면 원본 베기 그대로) public float fxScale = 1f, fxDelay = 0.5f, fxForward = 1.5f, fxYOffset = 0f, fxLife = 2.5f; // 배율 · 시전 뒤 시각(원본 go_effect 0.5 s) · PC 정면 거리(원본 1.5) · 높이 · 수명 + // #958 PD 「이펙트가 무기 방향 전방에 다른 무기 궤적처럼 하나만 — 전방의 적이 있는 방향에 1개 · 플레이어가 타겟하려는 대상의 무기 공격 범위가 중점」 — + // fxAimTarget 1 = 방향 = 지금 대상 쪽(없으면 PC 정면) · fxRangeFrac > 0 = 중심 거리 = 무기 공격 범위(PCActor.WL_AttackRange) × 이 값(대상이 더 가까우면 대상 자리) · + // fxRangeWidth > 0 = 보이는 폭 = 공격 범위 × 이 값(배율 = 폭 ÷ fxPrefabWidth · 프리팹 단위 폭 편집 모드 실측) — 0 이면 fxForward · fxScale 그대로 + public int fxAimTarget = 0; + public float fxRangeFrac = 0f, fxRangeWidth = 0f, fxPrefabWidth = 5.26f; public int hideOriginalFx = 0; // 1 = 원본 베기(go_effect) 렌더러 끔 // #955 PD 「주력 쓸 때 왜 캐릭터가 공중에 뜬 채 행동하나 — 제자리에서 단검을 난사하도록」 — 원본 상태 사슬(DualBlades_Jump_Start → JumpAttack1·2·3 = 공중 자세)을 쓰지 않고 slashSeconds 동안 slashStates 를 slashInterval 마다 번갈아 재생(배속 slashSpeed · 애니메이션 이벤트 끔 = 추가 타격·궤적 없음) public string[] slashStates = new string[0]; @@ -111,10 +116,22 @@ namespace WL.Combat.Weapon if (DSUtil.CheckNull(shooter) || shooter.IsDead()) yield break; var p = Resources.Load(c.fxPrefab); if (p == null) { LastLog = "fx 프리팹 없음 " + c.fxPrefab; yield break; } var fwd = shooter.transform.forward; fwd.y = 0f; if (fwd.sqrMagnitude < 1e-4f) fwd = Vector3.forward; fwd.Normalize(); - var pos = shooter.Get_CenterPositionFoward(c.fxForward) + Vector3.up * c.fxYOffset; - var go = Object.Instantiate(p, pos, Quaternion.LookRotation(fwd, Vector3.up)); go.transform.localScale = p.transform.localScale * c.fxScale; + var tgt = shooter.Get_Target(); float toTarget = -1f; + if (c.fxAimTarget != 0 && !DSUtil.CheckNull(tgt) && !tgt.IsDead()) // #958 대상 쪽 + { + var d = tgt.Get_position() - shooter.Get_position(); d.y = 0f; + if (d.sqrMagnitude > 1e-4f) { toTarget = d.magnitude; fwd = d / toTarget; } + } + var pcA = shooter as PCActor; float range = pcA != null ? pcA.WL_AttackRange : 0f; + float dist = c.fxForward, scale = c.fxScale; + if (c.fxRangeFrac > 0f && range > 0.05f) { dist = range * c.fxRangeFrac; if (toTarget > 0f && toTarget < dist) dist = toTarget; } // #958 공격 범위 가운데(대상이 더 가까우면 대상 자리) + if (c.fxRangeWidth > 0f && range > 0.05f) scale = range * c.fxRangeWidth / Mathf.Max(0.1f, c.fxPrefabWidth); + var pos = shooter.Get_Center_position() + fwd * dist + Vector3.up * c.fxYOffset; + var go = Object.Instantiate(p, pos, Quaternion.LookRotation(fwd, Vector3.up)); go.transform.localScale = p.transform.localScale * scale; foreach (var ps in go.GetComponentsInChildren(true)) { var m = ps.main; m.scalingMode = ParticleSystemScalingMode.Hierarchy; ps.Play(true); } - Object.Destroy(go, Mathf.Max(0.5f, c.fxLife)); FxShown++; LastLog = "fx " + c.fxPrefab + " @" + pos.ToString("F2") + " ×" + c.fxScale; + Object.Destroy(go, Mathf.Max(0.5f, c.fxLife)); FxShown++; + LastLog = "fx " + c.fxPrefab + " @" + pos.ToString("F2") + " ×" + scale.ToString("F3") + " · 거리 " + dist.ToString("F2") + " · 공격 범위 " + range.ToString("F2") + " · 대상 " + (toTarget > 0f ? toTarget.ToString("F2") + " m" : "없음"); + FxSlashLog = LastLog; } public static int SlashLoops; public static string SlashLog = ""; diff --git a/Assets/WL/Combat/Weapon/WLSkillChain.cs b/Assets/WL/Combat/Weapon/WLSkillChain.cs index 5655e1f84..0cb570e14 100644 --- a/Assets/WL/Combat/Weapon/WLSkillChain.cs +++ b/Assets/WL/Combat/Weapon/WLSkillChain.cs @@ -79,6 +79,7 @@ namespace WL.Combat.Weapon string trig = "hit:" + sk.e_Skill; WLGradeTier gt; int plus; int tier = WLEquipGrades.TryMap(w.itemJson, out gt, out plus) ? gt.tier : 1; + if (WL.SkillTest.WLTestScene.AllAwakened) tier = Mathf.Max(tier, WLEquipGrades.MaxTier); // #958 스킬 테스트 씬 = 모든 무기 각성 상태 foreach (var r in rows) { if (!r.IsTriggered || r.trigger != trig || tier < r.unlockTier || r.skillId == sk.n_SkillID) continue; diff --git a/Assets/WL/Combat/Weapon/WLWeaponGlowFx.cs b/Assets/WL/Combat/Weapon/WLWeaponGlowFx.cs index 476a15c44..577fe1026 100644 --- a/Assets/WL/Combat/Weapon/WLWeaponGlowFx.cs +++ b/Assets/WL/Combat/Weapon/WLWeaponGlowFx.cs @@ -14,10 +14,13 @@ namespace WL.Combat.Weapon { static readonly Dictionary s_mats = new Dictionary(); static readonly int kGlow = Shader.PropertyToID("_Glow"), kColor = Shader.PropertyToID("_GlowColor"), - kFill = Shader.PropertyToID("_Fill"), kRim = Shader.PropertyToID("_RimPower"); + kFill = Shader.PropertyToID("_Fill"), kRim = Shader.PropertyToID("_RimPower"), + kColor2 = Shader.PropertyToID("_GlowColor2"), kAxis = Shader.PropertyToID("_GradAxis"), kLen = Shader.PropertyToID("_GradLen"); // #958 그라데이션 readonly List _gos = new List(); readonly List _rs = new List(); + readonly List _axis = new List(); // #958 렌더러별 그라데이션 축(오브젝트 공간 · w = 손잡이 끝 투영) + readonly List _len = new List(); // 1 / (칼끝 − 손잡이 투영) MaterialPropertyBlock _mpb; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] @@ -47,10 +50,32 @@ namespace WL.Combat.Weapon r.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; r.receiveShadows = false; r.lightProbeUsage = UnityEngine.Rendering.LightProbeUsage.Off; r.reflectionProbeUsage = UnityEngine.Rendering.ReflectionProbeUsage.Off; _gos.Add(go); _rs.Add(r); + // #958 그라데이션 축 = 메시 경계의 가장 긴 축 · 방향 = 소켓(손잡이) 쪽 끝 → 먼 끝(칼끝) + var b = mf.sharedMesh.bounds; var s = b.size; + Vector3 ax = s.x >= s.y && s.x >= s.z ? Vector3.right : (s.y >= s.z ? Vector3.up : Vector3.forward); + float lo = Vector3.Dot(b.min, ax), hi = Vector3.Dot(b.max, ax); + float sock = Vector3.Dot(mf.transform.InverseTransformPoint(socket.position), ax); + if (Mathf.Abs(sock - hi) < Mathf.Abs(sock - lo)) { ax = -ax; float t = lo; lo = -hi; hi = -t; } + _axis.Add(new Vector4(ax.x, ax.y, ax.z, lo)); _len.Add(hi - lo > 1e-4f ? 1f / (hi - lo) : 0f); } return _rs.Count; } + /// #958 손잡이 color → 칼끝 color2 그라데이션(WL/WeaponGlow 의 _GlowColor2 · _GradAxis · _GradLen). + public void Set(float glow, Color color, Color color2, float fill, float rimPower) + { + if (_rs.Count == 0) return; + if (_mpb == null) _mpb = new MaterialPropertyBlock(); + for (int i = 0; i < _rs.Count; i++) + { + var r = _rs[i]; if (r == null) continue; + r.enabled = glow > 0.001f; + _mpb.Clear(); _mpb.SetFloat(kGlow, glow); _mpb.SetColor(kColor, color); _mpb.SetFloat(kFill, fill); _mpb.SetFloat(kRim, rimPower); + _mpb.SetColor(kColor2, color2); _mpb.SetVector(kAxis, i < _axis.Count ? _axis[i] : Vector4.zero); _mpb.SetFloat(kLen, i < _len.Count ? _len[i] : 0f); + r.SetPropertyBlock(_mpb); + } + } + public void Set(float glow, Color color, float fill, float rimPower) { if (_rs.Count == 0) return; @@ -67,7 +92,7 @@ namespace WL.Combat.Weapon public void End() { for (int i = 0; i < _gos.Count; i++) if (_gos[i] != null) Object.Destroy(_gos[i]); - _gos.Clear(); _rs.Clear(); + _gos.Clear(); _rs.Clear(); _axis.Clear(); _len.Clear(); } /// socket 아래 가장 큰 메시의 경계 중심(socket 로컬) — 무기 가운데. 없으면 원점. diff --git a/Assets/WL/Combat/Weapon/WLWeaponGradeCheat.cs b/Assets/WL/Combat/Weapon/WLWeaponGradeCheat.cs index 47e1dc36c..29bc755a6 100644 --- a/Assets/WL/Combat/Weapon/WLWeaponGradeCheat.cs +++ b/Assets/WL/Combat/Weapon/WLWeaponGradeCheat.cs @@ -136,6 +136,7 @@ namespace WL.Combat.Weapon void Update() { if (!Input.anyKeyDown) return; + if (WL.SkillTest.WLTestScene.NumberKeysWeapon) return; // #958 스킬 테스트 씬은 숫자 키 = 무기 번호(하네스) int max = Mathf.Min(s_alpha.Length, WLEquipGrades.MaxTier); for (int i = 0; i < max; i++) if (Input.GetKeyDown(s_alpha[i]) || Input.GetKeyDown(s_pad[i])) { WLWeaponGradeCheat.Apply(i + 1); break; } diff --git a/Assets/WL/Combat/Weapon/WLWeaponSkillSlots.cs b/Assets/WL/Combat/Weapon/WLWeaponSkillSlots.cs index c5119228b..40d5dc605 100644 --- a/Assets/WL/Combat/Weapon/WLWeaponSkillSlots.cs +++ b/Assets/WL/Combat/Weapon/WLWeaponSkillSlots.cs @@ -147,6 +147,7 @@ namespace WL.Combat.Weapon int wid = w != null && w.itemJson != null ? w.itemJson.GroupNo : 0; int tier = 1, plus; WLGradeTier gt; if (w != null && WLEquipGrades.TryMap(w.itemJson, out gt, out plus)) tier = gt.tier; + if (WL.SkillTest.WLTestScene.AllAwakened) tier = Mathf.Max(tier, WLEquipGrades.MaxTier); // #958 스킬 테스트 씬 = 모든 무기 각성 상태(주력 · 각성 해금) int cls = CurrentClassId(pc); bool samePc = ReferenceEquals(pc, _lastPc); var arr = s_fData.GetValue(pc) as SkillListTableData[]; diff --git a/Assets/WL/Look/Character/Shaders/WLWeaponGlow.shader b/Assets/WL/Look/Character/Shaders/WLWeaponGlow.shader index 4e0bc5585..fae977125 100644 --- a/Assets/WL/Look/Character/Shaders/WLWeaponGlow.shader +++ b/Assets/WL/Look/Character/Shaders/WLWeaponGlow.shader @@ -2,11 +2,16 @@ // PD 「모으기 이펙트가 재생될 때 망치가 점차 하얗게 빛날 수 있게」 — 해머 각성(WLHammerQuake)이 망치 메시를 복제한 자식에 이 재질을 씌우고 // 세기 _Glow(0~1) · 색 _GlowColor(HDR · 1 넘으면 블룸)를 렌더러별 프로퍼티 블록으로 쓴다. 몸통 덮기 _Fill · 테두리 _RimPower · 겉껍질 _Inflate(월드 m) = 재질 값. // 모바일: 조명 · 텍스처 없는 단일 패스 · 그림자 없음. +// #958 PD 「대검 주력 모으기에 그라데이션 색(노란색 계열)」 — _GlowColor(손잡이 쪽) → _GlowColor2(칼끝 쪽) 오브젝트 공간 축 그라데이션 +// (_GradAxis xyz = 축 · w = 손잡이 끝 투영 · _GradLen = 1/길이 · 0 = 단색 = 기존과 같음 · 렌더러별 프로퍼티 블록 = WLWeaponGlowFx.Set 5인자) Shader "WL/WeaponGlow" { Properties { [HDR] _GlowColor ("Glow Color (HDR)", Color) = (2, 2, 2, 1) + [HDR] _GlowColor2 ("Glow Color 2 (HDR · 칼끝 쪽)", Color) = (2, 2, 2, 1) + _GradAxis ("Gradient Axis (오브젝트 xyz · w = 시작 투영)", Vector) = (0, 0, 1, 0) + _GradLen ("Gradient 1/Length (0 = 단색)", Float) = 0 _Glow ("Glow (0~1 · 프로퍼티 블록)", Range(0, 1)) = 0 _Fill ("Body Fill (몸통 덮기)", Range(0, 1)) = 0.6 _RimPower ("Rim Power (테두리)", Range(0.5, 8)) = 2 @@ -30,6 +35,9 @@ Shader "WL/WeaponGlow" CBUFFER_START(UnityPerMaterial) half4 _GlowColor; + half4 _GlowColor2; + float4 _GradAxis; + float _GradLen; half _Glow; half _Fill; half _RimPower; @@ -37,7 +45,7 @@ Shader "WL/WeaponGlow" CBUFFER_END struct Attributes { float4 positionOS : POSITION; float3 normalOS : NORMAL; }; - struct Varyings { float4 positionCS : SV_POSITION; float3 normalWS : TEXCOORD0; float3 viewWS : TEXCOORD1; }; + struct Varyings { float4 positionCS : SV_POSITION; float3 normalWS : TEXCOORD0; float3 viewWS : TEXCOORD1; float grad : TEXCOORD2; }; Varyings vert(Attributes v) { @@ -47,6 +55,7 @@ Shader "WL/WeaponGlow" o.positionCS = TransformWorldToHClip(p); o.normalWS = n; o.viewWS = GetWorldSpaceViewDir(p); + o.grad = saturate((dot(v.positionOS.xyz, _GradAxis.xyz) - _GradAxis.w) * _GradLen); // #958 손잡이 0 → 칼끝 1 (_GradLen 0 = 0 = 단색) return o; } @@ -55,7 +64,7 @@ Shader "WL/WeaponGlow" half ndv = saturate(dot(normalize(i.normalWS), normalize(i.viewWS))); half rim = pow(1.0h - ndv, _RimPower); half a = saturate(_Glow) * saturate(_Fill + rim); - return half4(_GlowColor.rgb, a); + return half4(lerp(_GlowColor.rgb, _GlowColor2.rgb, (half)i.grad), a); } ENDHLSL } diff --git a/Assets/WL/PartSkill/WLPartSkillRunner.cs b/Assets/WL/PartSkill/WLPartSkillRunner.cs index 835b09710..db52f6111 100644 --- a/Assets/WL/PartSkill/WLPartSkillRunner.cs +++ b/Assets/WL/PartSkill/WLPartSkillRunner.cs @@ -107,6 +107,7 @@ namespace WL.PartSkill sk.Tick(dt); if (sk.Persistent) continue; sk.cd -= dt; + if (WL.SkillTest.WLTestScene.ManualSkills) continue; // #958 스킬 테스트 씬 = 자동 시전 끔(Q · E 치트키 WLSkillHotkey.FirePartSkill 만 Cast) if (sk.cd > 0f || !sk.WantCast()) continue; sk.Cast(); sk.cd = Mathf.Max(0.2f, sk.CoolSec); diff --git a/Assets/WL/PartSkill/WLPartSkillState.cs b/Assets/WL/PartSkill/WLPartSkillState.cs index 3ed7166bb..93b13cea2 100644 --- a/Assets/WL/PartSkill/WLPartSkillState.cs +++ b/Assets/WL/PartSkill/WLPartSkillState.cs @@ -115,7 +115,7 @@ namespace WL.PartSkill if (wrows != null) foreach (var r in wrows) { - if (!r.IsWlSkill || wslot.tier < r.unlockTier) continue; + if (!r.IsWlSkill || (wslot.tier < r.unlockTier && !WL.SkillTest.WLTestScene.AllAwakened)) continue; // #958 스킬 테스트 씬 = 모든 무기 각성 상태(해머 대지 분쇄 등 파츠 스킬 칸도 해금) var wd = WLPartSkillData.Def(r.wlSkill); if (wd == null || wd.kind != WLPartKind.Active) continue; var ws = new WLPartSlot { part = eFREquipmentType.Weapon, item = wslot.item, def = wd, tier = wslot.tier, level = wslot.level, mult = wslot.mult }; diff --git a/Assets/WL/SkillTest/Resources/WL/WLSkillTest.json b/Assets/WL/SkillTest/Resources/WL/WLSkillTest.json index d9fb2b04e..b3a6f99f1 100644 --- a/Assets/WL/SkillTest/Resources/WL/WLSkillTest.json +++ b/Assets/WL/SkillTest/Resources/WL/WLSkillTest.json @@ -19,6 +19,10 @@ "slowKey": "T", "slowScale": 0.2, "pauseKey": "Backslash", + "numberKeysWeapon": 1, + "allAwakened": 1, + "manualSkills": 1, + "_note_958": "#958 PD — numberKeysWeapon 1 = 숫자 키 1~9 · 0(10번)이 그 번호 무기로 즉시 교체(번호 = O/P 순환과 같은 종류 순 · 화면 왼쪽 위 목록 · 등급 치트는 이 씬에서 끔) · allAwakened 1 = 모든 무기 각성 상태(주력 · 각성 해금 등급 무시) · manualSkills 1 = 자동 스킬 시전 끔(Q · E 치트키로만 · 그 밖엔 기본 공격만)", "label": 1, "labelFontScale": 0.018, "groundSize": 100, diff --git a/Assets/WL/SkillTest/WLSkillTestHarness.cs b/Assets/WL/SkillTest/WLSkillTestHarness.cs index eea9402d9..18c8256f7 100644 --- a/Assets/WL/SkillTest/WLSkillTestHarness.cs +++ b/Assets/WL/SkillTest/WLSkillTestHarness.cs @@ -11,7 +11,8 @@ // 이동·넉백·소환 AI 분기(Actor 1665·1710·2198 · MobActor 75 · SummonActor 60 · WizardActor 53·61)는 StatTest 전용으로 남겨 **실제 게임 동작**을 유지한다. // ■ 이 클래스가 MainToStatTest 를 상속하는 이유 = 원본 분기가 `MainToStatTest.Ins.actors[1]`(대상)·`Set_Dmglabel`(피해 글자)를 부른다 → 같은 자리를 채워 준다. // ■ 조작: **원본 가상패드**(NewGameUI 의 Floating Joystick 중첩 프리팹을 빌더가 같은 값으로 복제 · 화면 왼쪽 드래그 · PD 2026-10-20 #947 「원본과 동일한 가상패드 + 키 입력」) -// + WASD/방향키 이동(같은 호출 · Set_AutoBraking(false) → JoystickStatus 1 → Run_byJoystick) · Space 기본 공격 1타 · Q 주력 · E 각성 · 숫자 등급(기존 훅) +// + WASD/방향키 이동(같은 호출 · Set_AutoBraking(false) → JoystickStatus 1 → Run_byJoystick) · Space 기본 공격 1타 · Q 주력 · E 각성 · 숫자 = 그 번호 무기(#958 · 등급 치트 대체) +// · #958 모든 무기 각성 상태(해금 등급 무시) · 자동 스킬 끔(Q · E 치트키로만 스킬 · 그 밖엔 기본 공격만) — WLTestScene.AllAwakened · ManualSkills · NumberKeysWeapon // · O 무기 이전(왼쪽 순환) · P 무기 다음(오른쪽 순환) — PD 2026-10-20 #947 지시 · 종류(GroupNo)마다 1개 · R 더미 제자리 · T 슬로 모션 · Pause = 설정값(기본 Backslash) // ■ 값 = Resources/WL/WLSkillTest.json (C45) · 상태 파일 = Library/WLSkillTest_status.txt(외부 스크립트는 Play 중 eval 대신 이 파일) · 🔴 Debug.LogError 금지(원본 ErrorLogHook 팝업) // ───────────────────────────────────────────────────────────────────────────── @@ -34,6 +35,17 @@ namespace WL.SkillTest public static bool Active { get { var n = SceneManager.GetActiveScene().name; return n == Name || n == "StatTest"; } } /// 테스트 씬에서 원본 InGameInfo.Start 의 이펙트·스킬 예열(인스턴스 514 · 한 프레임 18 s · #947 실측)을 돌릴지 — JSON preloadEffects(기본 0 = 생략 · 첫 사용 때 로드). public static bool PreloadEffects { get { return WLSkillTestHarness.Cfg.preloadEffects != 0; } } + + // #958 PD 「모든 무기는 각성 상태로 동작 · 테스트 씬에선 치트키 Q(주력) · E(각성)로 내가 쓰는 경우가 아니면 기본 공격만 · 숫자 키 = 그 번호 무기로 즉시 교체(등급 변경 대체)」 + // — 하네스 Awake 가 JSON 값으로 켜고 Play 시작마다 끈다(도메인 리로드 꺼짐 · 다른 씬에 남지 않게). + /// 무기 기술 해금 등급 무시(주력 · 각성 둘 다 열림) — WLWeaponSkillSlots · WLSkillChain · WLPartSkillState 가 본다. + public static bool AllAwakened; + /// 자동 스킬 시전 끔 — 원본 PCActor 자동 시전 · WL_TryAutoSkillNow(우선 시전 · 대쉬 도착 시전) · 파츠 스킬 자동 시전. Q/E 치트키(WLSkillHotkey)만 스킬을 쓴다. + public static bool ManualSkills; + /// 숫자 키 = 무기 번호(등급 치트 WLWeaponGradeCheat 대신). + public static bool NumberKeysWeapon; + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] + static void ResetFlags() { AllAwakened = ManualSkills = NumberKeysWeapon = false; } } [System.Serializable] @@ -59,6 +71,9 @@ namespace WL.SkillTest public string slowKey = "T"; public float slowScale = 0.2f; public string pauseKey = "Backslash"; // P 를 무기에 내주고 일시정지는 \ 로 + public int numberKeysWeapon = 1; // #958 1 = 숫자 키 1~9 · 0(=10) 이 그 번호 무기로 즉시 교체(번호 = O/P 순환과 같은 종류 순 · 화면 왼쪽 위 목록) · 등급 치트는 이 씬에서 끔 + public int allAwakened = 1; // #958 1 = 모든 무기 각성 상태(등급과 무관하게 주력 · 각성 해금) + public int manualSkills = 1; // #958 1 = 자동 스킬 시전 끔(Q · E 치트키로만 스킬 · 그 밖엔 기본 공격만) public int label = 1; public float labelFontScale = 0.018f; public float groundSize = 100f; @@ -87,7 +102,9 @@ namespace WL.SkillTest float _lastRt, _simH, _simV, _formTimer, _outOfRange; bool _moving, _slow; KeyCode _kAttack = KeyCode.Space, _kPrev = KeyCode.O, _kNext = KeyCode.P, _kReset = KeyCode.R, _kSlow = KeyCode.T, _kPause = KeyCode.Backslash; - string _msg = ""; + static readonly KeyCode[] s_numKeys = { KeyCode.Alpha1, KeyCode.Alpha2, KeyCode.Alpha3, KeyCode.Alpha4, KeyCode.Alpha5, KeyCode.Alpha6, KeyCode.Alpha7, KeyCode.Alpha8, KeyCode.Alpha9, KeyCode.Alpha0 }; + static readonly KeyCode[] s_padKeys = { KeyCode.Keypad1, KeyCode.Keypad2, KeyCode.Keypad3, KeyCode.Keypad4, KeyCode.Keypad5, KeyCode.Keypad6, KeyCode.Keypad7, KeyCode.Keypad8, KeyCode.Keypad9, KeyCode.Keypad0 }; + string _msg = "", _weaponLine = ""; GUIStyle _style; public static WLSkillTestJson Cfg @@ -119,6 +136,7 @@ namespace WL.SkillTest _kAttack = Key(cfg.attackKey, KeyCode.Space); _kPrev = Key(cfg.weaponPrevKey, KeyCode.O); _kNext = Key(cfg.weaponNextKey, KeyCode.P); _kReset = Key(cfg.dummyResetKey, KeyCode.R); _kSlow = Key(cfg.slowKey, KeyCode.T); _kPause = Key(cfg.pauseKey, KeyCode.Backslash); MoveLog = ""; _simH = _simV = 0f; + WLTestScene.AllAwakened = cfg.allAwakened != 0; WLTestScene.ManualSkills = cfg.manualSkills != 0; WLTestScene.NumberKeysWeapon = cfg.numberKeysWeapon != 0; // #958 if (actors == null || actors.Length < 2) actors = new Actor[] { pcActor, dummyActor }; Lap("awake"); } @@ -153,6 +171,7 @@ namespace WL.SkillTest WithStageMode(() => pcActor.Set(false, null, sdata, cls, true)); // Set 안의 Set_Obj 가 Lobby 면 로비 컨트롤러를 입힌다(#948 실측: 지팡이가 pcanim_lobby 로 서서 투사체 0) → 섬처럼 그 동안만 Stage FillMagicSlots(pcActor, sdata); ActorInfo.Ins.Add_Actor(pcActor, eRole.PC, 0, pcActor.name); + if (cfg.allAwakened != 0) { try { WL.PartSkill.WLPartSkillState.Rebuild("skilltest958"); } catch (System.Exception ex) { LastLog = "파츠 스킬 다시 짜기 실패 " + ex.Message; } } // #958 각성 해금을 파츠 스킬 칸(해머 대지 분쇄)에도 바로 })) yield break; Lap("pc " + cls); @@ -389,24 +408,68 @@ namespace WL.SkillTest /// 낀 무기를 무기 종류(GroupNo) 순으로 앞뒤로(dir ±1 · O = -1 · P = +1). 종류마다 1개 — SortedItems 는 장착 중인 것을 맨 앞에 두므로 그대로 쓰면 같은 종류 2개 사이를 왕복했다(#947 2차 Play 실측 · 묵광 2개). public static string CycleWeapon(int dir) { + var grp = new List(); var ids = new List(); var names = new List(); + int curIdx; string err = WeaponList(grp, ids, names, out curIdx); + if (err != null) return err; + int pos = curIdx < 0 ? 0 : (curIdx + dir + grp.Count) % grp.Count; + return EquipAt(pos, ids, names); + } + + /// #958 PD 「숫자 키를 눌러 해당하는 넘버의 무기로 즉시 바뀌게(기존 등급 변경 대체)」 — number(1부터) = WeaponList 순서(O/P 순환과 같음 · 화면 목록 번호). + public static string SelectWeapon(int number) + { + var grp = new List(); var ids = new List(); var names = new List(); + int curIdx; string err = WeaponList(grp, ids, names, out curIdx); + if (err != null) return err; + if (number < 1 || number > grp.Count) return "무기 " + number + "번 없음(1~" + grp.Count + ")"; + if (number - 1 == curIdx) return "이미 " + number + " " + names[curIdx]; + return EquipAt(number - 1, ids, names); + } + + static string EquipAt(int pos, List ids, List names) + { + bool ok = WLInventory.Equip(ids[pos]); + return (ok ? "장착 " : "실패 ") + (pos + 1) + " " + names[pos] + " · " + (pos + 1) + "/" + ids.Count + (ok ? "" : " · " + WLInventory.LastMessage); + } + + /// 낀 수 있는 무기 — 종류(GroupNo) 오름차순 · 종류마다 1개(SortedItems 는 장착 중인 것을 맨 앞에 두므로 같은 종류는 장착 중인 것) · curIdx = 장착 중인 종류의 번호(0부터 · 없으면 -1). 실패면 이유 문장. + static string WeaponList(List grp, List ids, List names, out int curIdx) + { + curIdx = -1; if (!WLInventory.EnsureLoaded()) return "인벤토리 미로드"; var cur = WLInventory.EquippedInSlot(eFREquipmentType.Weapon); int curGroup = cur != null && cur.itemJson != null ? cur.itemJson.GroupNo : -1; - var grp = new List(); var ids = new List(); var names = new List(); + var tmp = new List>(); foreach (var it in WLInventory.SortedItems()) { if (it == null || it.itemJson == null || !it.itemJson.IsEquipment || it.itemJson.EquipmentType != eFREquipmentType.Weapon) continue; - int g = it.itemJson.GroupNo; - if (grp.IndexOf(g) >= 0) continue; // 같은 종류는 첫 것(장착 중이면 그것)만 - grp.Add(g); ids.Add(it.rawItem._id); names.Add(it.itemJson.Item_name + "(" + g + ")"); + int g = it.itemJson.GroupNo; bool dup = false; + foreach (var kv in tmp) if (kv.Key == g) { dup = true; break; } + if (!dup) tmp.Add(new KeyValuePair(g, it)); // 같은 종류는 첫 것(장착 중이면 그것)만 } - if (grp.Count == 0) return "무기 없음"; - var order = new List(); for (int i = 0; i < grp.Count; i++) order.Add(i); - order.Sort((a, b) => grp[a].CompareTo(grp[b])); - int curIdx = -1; for (int i = 0; i < order.Count; i++) if (grp[order[i]] == curGroup) { curIdx = i; break; } - int pos = curIdx < 0 ? 0 : (curIdx + dir + order.Count) % order.Count, next = order[pos]; - bool ok = WLInventory.Equip(ids[next]); - return (ok ? "장착 " : "실패 ") + names[next] + " " + (pos + 1) + "/" + order.Count + (ok ? "" : " · " + WLInventory.LastMessage); + if (tmp.Count == 0) return "무기 없음"; + tmp.Sort((a, b) => a.Key.CompareTo(b.Key)); + for (int i = 0; i < tmp.Count; i++) + { + grp.Add(tmp[i].Key); ids.Add(tmp[i].Value.rawItem._id); names.Add(tmp[i].Value.itemJson.Item_name); + if (tmp[i].Key == curGroup) curIdx = i; + } + return null; + } + + /// #958 화면 목록 한 줄 — 「1 묵광 · 2 파괴자 · …」(장착 중 = [ ]). + static string WeaponLine() + { + var grp = new List(); var ids = new List(); var names = new List(); + int curIdx; if (WeaponList(grp, ids, names, out curIdx) != null) return ""; + var sb = new System.Text.StringBuilder(); + for (int i = 0; i < names.Count; i++) + { + if (i > 0) sb.Append(" "); + string key = i < 9 ? (i + 1).ToString() : i == 9 ? "0" : "-"; + sb.Append(i == curIdx ? "[" : "").Append(key).Append(' ').Append(names[i]).Append(i == curIdx ? "]" : ""); + } + return sb.ToString(); } public static string Describe() @@ -433,7 +496,7 @@ namespace WL.SkillTest if (pc == null) return; var cfg = Cfg; _formTimer += Time.unscaledDeltaTime; - if (_formTimer >= Mathf.Max(0.1f, cfg.combatFormCheckSeconds)) { _formTimer = 0f; CheckCombatForm(pc); } + if (_formTimer >= Mathf.Max(0.1f, cfg.combatFormCheckSeconds)) { _formTimer = 0f; CheckCombatForm(pc); if (cfg.numberKeysWeapon != 0) _weaponLine = WeaponLine(); } // 이동 — 섬 조이스틱(WLIslandAvatar.DrivePc · 원본 FloatingJoystick)과 같은 호출 순서. 누름 = Set_AutoBraking(false) → JoystickStatus 1 · 매 프레임 Run_byJoystick(카메라 기준) · 뗌 = JoystickStatus 0 → Set_AutoBraking(true)(Play_Idle · 자동 전투 재개) float h = 0f, v = 0f; @@ -461,6 +524,9 @@ namespace WL.SkillTest if (Input.GetKeyDown(_kAttack)) { if (WL.Combat.Weapon.WLGhostStrike.IsLanding(pc)) _msg = "착지 중 — 공격 불가"; else { if (Dummy != null) pc.Change_Target(false, Dummy); pc.Play_Attack(0, 1f); _msg = "기본 공격"; } } // #954 랜딩 중 공격 금지 if (Input.GetKeyDown(_kPrev)) _msg = CycleWeapon(-1); if (Input.GetKeyDown(_kNext)) _msg = CycleWeapon(+1); + if (cfg.numberKeysWeapon != 0 && Input.anyKeyDown) // #958 숫자 키 = 그 번호 무기(1~9 · 0 = 10번 · 키패드 포함) + for (int i = 0; i < s_numKeys.Length; i++) + if (Input.GetKeyDown(s_numKeys[i]) || Input.GetKeyDown(s_padKeys[i])) { _msg = SelectWeapon(i + 1); break; } if (Input.GetKeyDown(_kReset)) _msg = ResetDummy(); if (Input.GetKeyDown(_kSlow)) { _slow = !_slow; Time.timeScale = _slow ? Mathf.Clamp(cfg.slowScale, 0.05f, 1f) : 1f; _msg = _slow ? "슬로 모션 ×" + Time.timeScale : "정상 속도"; } #if UNITY_EDITOR @@ -478,8 +544,10 @@ namespace WL.SkillTest if (WLInventory.Loaded) { var cur = WLInventory.EquippedInSlot(eFREquipmentType.Weapon); if (cur != null && cur.itemJson != null) w = cur.itemJson.Item_name + "(" + cur.itemJson.GroupNo + ")"; } string s = "WL 스킬 테스트 · " + Stage + (Ready ? "" : " …") + "\n무기 " + w + " 더미 HP " + (Dummy != null ? Dummy.Get_HP().ToString("F0") : "-") + (_slow ? " [슬로 ×" + Time.timeScale + "]" : "") + - "\n가상패드(왼쪽 화면 드래그 · 원본과 동일) 또는 WASD 이동 · " + _kAttack + " 공격 · " + WLSkillHotkey.Key + " 주력 · " + (WLSkillHotkey.Key2 != KeyCode.None ? WLSkillHotkey.Key2 + " 각성 · " : "") + "숫자 등급 · " + + "\n가상패드(왼쪽 화면 드래그 · 원본과 동일) 또는 WASD 이동 · " + _kAttack + " 공격 · " + WLSkillHotkey.Key + " 주력 · " + (WLSkillHotkey.Key2 != KeyCode.None ? WLSkillHotkey.Key2 + " 각성 · " : "") + (Cfg.numberKeysWeapon != 0 ? "숫자 무기 · " : "숫자 등급 · ") + _kPrev + " 무기 이전 · " + _kNext + " 무기 다음 · " + _kReset + " 더미 · " + _kSlow + " 슬로 · " + _kPause + " 일시정지" + + (Cfg.manualSkills != 0 ? "\n스킬 = " + WLSkillHotkey.Key + " · " + WLSkillHotkey.Key2 + " 로만(자동 스킬 끔)" + (Cfg.allAwakened != 0 ? " · 모든 무기 각성" : "") : "") + + (string.IsNullOrEmpty(_weaponLine) ? "" : "\n" + _weaponLine) + (string.IsNullOrEmpty(_msg) ? "" : "\n" + _msg) + (Ready ? "\n" + string.Join(" · ", Laps.ToArray()) : ""); var r = new Rect(Screen.width * 0.01f, Screen.height * 0.01f, Screen.width * 0.6f, Screen.height * 0.3f); GUI.color = new Color(0f, 0f, 0f, 0.6f); GUI.Label(new Rect(r.x + 1, r.y + 1, r.width, r.height), s, _style);