WL #958 PD 지시 6건(테스트 씬 편의 · 대검 · 비수) + 궤적 이펙트 Play 간 막힘 수정

- ① 숫자 키 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 <noreply@anthropic.com>
This commit is contained in:
깃 관리자 2026-10-21 13:05:18 +09:00
parent 02f16661d0
commit 682c3f67d9
18 changed files with 319 additions and 47 deletions

View File

@ -72,6 +72,9 @@ public class PCActor : MyActor
/// <summary>WL #900 — i 칸 스킬을 원본 자동 시전(Update :98-105)과 같은 규칙(쿨 · 자동 간격 · 사거리 · CC)으로 지금 쓴다. 성공하면 true(대쉬 도착 즉시 시전).</summary>
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)
{

View File

@ -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)",

View File

@ -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; }
/// <summary>지면 충격파. tipContact = 칼끝이 지면(contactHeight 이내)에 닿는 프레임·자리에서 재생(단계 안에서 minDrop 이상 올라갔다 내려올 때) · 못 잡으면 timeout 뒤 최저점(fallbackHeight 이내)에.</summary>
@ -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;
}
/// <summary>#958 엉덩이 뼈 축 중 가장 수평인 것(0 right · 1 up · 2 forward) — 이 PC 의 Root_M 은 축이 기울어 오일러 y 로는 위 축 회전을 못 잰다(실측).</summary>
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; }
/// <summary>#958 그 축의 수평 투영이 루트 정면에서 위 축 둘레로 몇 도(시계 +).</summary>
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; } }
}
}

View File

@ -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,

View File

@ -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,

View File

@ -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)는 그대로"

View File

@ -84,12 +84,24 @@ namespace WL.Combat.Weapon
// #927 위에서 비스듬히 보는 카메라에선 몸 가운데 깊이의 반투명 이펙트가 몸 앞면에 가려 몸이 이펙트 앞에 보인다 →
// 카메라 쪽 시선 방향으로 이만큼(m) 당겨 화면 위치는 그대로 두고 깊이만 몸보다 앞에(0 = 당기지 않음)
public float towardCamera = 0f;
// #958 PD 「대검 각성 회오리가 하얗게 뭉개져 보임 → 회오리 느낌이 나도록 회전 방향의 선이나 그라데이션이 보이게(참고 이미지)」 — 층별 재질 값 덮기
public WLFxLayerOverride[] layers = new WLFxLayerOverride[0];
}
/// <summary>#958 이펙트 렌더러별 재질 값 덮기(프로퍼티 블록 · 재질 인스턴스 0) — names = 렌더러 이름(| 로 여럿 · * = 나머지 전부) ·
/// emission ≥ 0 = _Emission(HS 셰이더 발광 배율 · 원본 5 는 블룸 없는 화면에서 흰색 포화 = 「하얗게 뭉개짐」 편집 모드 실측) · tint = _Color(r,g,b)</summary>
[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();
/// <summary>#958 층별 _Emission · _Color 덮기 — 덮은 렌더러 수.</summary>
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<ParticleSystemRenderer>(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++;
}
}

View File

@ -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; }
/// <summary>#958 이름이 names 에 든 입자계의 렌더러를 끈다(이름 끝 (Clone) 무시) — 끈 수.</summary>
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<ParticleSystem>(true))
{
string nm = ps.name.Replace("(Clone)", "").Trim();
if (System.Array.IndexOf(names, nm) < 0) continue;
var r = ps.GetComponent<ParticleSystemRenderer>(); if (r != null && r.enabled) { r.enabled = false; n++; }
}
return n;
}
/// <summary>#958 입자 색을 수명 그라데이션(g = r,g,b 셋씩 · 2색 이상)으로 — 시작색은 흰색(알파 유지) · 수명 알파는 원래 것(없으면 1).</summary>
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<ParticleSystem>(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);
}
}
}
/// <summary>PC 에 붙는 시전 러너 — 동작(기본 4타) · 모으기 · 반짝임 · 내려치기 · 발사 · 내려친 자세 멈춤 · 끝 · 몸을 땅에 붙임(#926).</summary>
@ -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);
}
/// <summary>#929 내려친 자리(칼끝을 땅으로 · 없으면 시전자 앞 waveStartForward) 지면 이펙트 + 검기가 지나갈 길(시전자 정면 · 앞머리 시작 → waveDistance) 지면 이펙트.</summary>

View File

@ -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<GameObject>(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<ParticleSystem>(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 = "";

View File

@ -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;

View File

@ -14,10 +14,13 @@ namespace WL.Combat.Weapon
{
static readonly Dictionary<string, Material> s_mats = new Dictionary<string, Material>();
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<GameObject> _gos = new List<GameObject>();
readonly List<Renderer> _rs = new List<Renderer>();
readonly List<Vector4> _axis = new List<Vector4>(); // #958 렌더러별 그라데이션 축(오브젝트 공간 · w = 손잡이 끝 투영)
readonly List<float> _len = new List<float>(); // 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;
}
/// <summary>#958 손잡이 color → 칼끝 color2 그라데이션(WL/WeaponGlow 의 _GlowColor2 · _GradAxis · _GradLen).</summary>
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();
}
/// <summary>socket 아래 가장 큰 메시의 경계 중심(socket 로컬) — 무기 가운데. 없으면 원점.</summary>

View File

@ -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; }

View File

@ -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[];

View File

@ -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
}

View File

@ -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);

View File

@ -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 };

View File

@ -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,

View File

@ -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"; } }
/// <summary>테스트 씬에서 원본 InGameInfo.Start 의 이펙트·스킬 예열(인스턴스 514 · 한 프레임 18 s · #947 실측)을 돌릴지 — JSON preloadEffects(기본 0 = 생략 · 첫 사용 때 로드).</summary>
public static bool PreloadEffects { get { return WLSkillTestHarness.Cfg.preloadEffects != 0; } }
// #958 PD 「모든 무기는 각성 상태로 동작 · 테스트 씬에선 치트키 Q(주력) · E(각성)로 내가 쓰는 경우가 아니면 기본 공격만 · 숫자 키 = 그 번호 무기로 즉시 교체(등급 변경 대체)」
// — 하네스 Awake 가 JSON 값으로 켜고 Play 시작마다 끈다(도메인 리로드 꺼짐 · 다른 씬에 남지 않게).
/// <summary>무기 기술 해금 등급 무시(주력 · 각성 둘 다 열림) — WLWeaponSkillSlots · WLSkillChain · WLPartSkillState 가 본다.</summary>
public static bool AllAwakened;
/// <summary>자동 스킬 시전 끔 — 원본 PCActor 자동 시전 · WL_TryAutoSkillNow(우선 시전 · 대쉬 도착 시전) · 파츠 스킬 자동 시전. Q/E 치트키(WLSkillHotkey)만 스킬을 쓴다.</summary>
public static bool ManualSkills;
/// <summary>숫자 키 = 무기 번호(등급 치트 WLWeaponGradeCheat 대신).</summary>
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
/// <summary>낀 무기를 무기 종류(GroupNo) 순으로 앞뒤로(dir ±1 · O = -1 · P = +1). 종류마다 1개 — SortedItems 는 장착 중인 것을 맨 앞에 두므로 그대로 쓰면 같은 종류 2개 사이를 왕복했다(#947 2차 Play 실측 · 묵광 2개).</summary>
public static string CycleWeapon(int dir)
{
var grp = new List<int>(); var ids = new List<int>(); var names = new List<string>();
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);
}
/// <summary>#958 PD 「숫자 키를 눌러 해당하는 넘버의 무기로 즉시 바뀌게(기존 등급 변경 대체)」 — number(1부터) = WeaponList 순서(O/P 순환과 같음 · 화면 목록 번호).</summary>
public static string SelectWeapon(int number)
{
var grp = new List<int>(); var ids = new List<int>(); var names = new List<string>();
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<int> ids, List<string> names)
{
bool ok = WLInventory.Equip(ids[pos]);
return (ok ? "장착 " : "실패 ") + (pos + 1) + " " + names[pos] + " · " + (pos + 1) + "/" + ids.Count + (ok ? "" : " · " + WLInventory.LastMessage);
}
/// <summary>낀 수 있는 무기 — 종류(GroupNo) 오름차순 · 종류마다 1개(SortedItems 는 장착 중인 것을 맨 앞에 두므로 같은 종류는 장착 중인 것) · curIdx = 장착 중인 종류의 번호(0부터 · 없으면 -1). 실패면 이유 문장.</summary>
static string WeaponList(List<int> grp, List<int> ids, List<string> 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<int>(); var ids = new List<int>(); var names = new List<string>();
var tmp = new List<KeyValuePair<int, Item>>();
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<int, Item>(g, it)); // 같은 종류는 첫 것(장착 중이면 그것)만
}
if (grp.Count == 0) return "무기 없음";
var order = new List<int>(); 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;
}
/// <summary>#958 화면 목록 한 줄 — 「1 묵광 · 2 파괴자 · …」(장착 중 = [ ]).</summary>
static string WeaponLine()
{
var grp = new List<int>(); var ids = new List<int>(); var names = new List<string>();
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);