WL #961 PD 지시 5건(비수) — ① 주력 중 이동 금지(입력 HoldMove + 난사 중 공격 전진 표 차단 = 휘두를 때마다 밀려 10회에 약 1 m 전진하던 원인) · 구르기 캔슬 = 난사·이펙트·판정 즉시 멈춤 + 원본 2.1 s 끝 처리(대기·대상 뒤 순간이동·연기·카메라) 생략(Skill_ShadowSlash 훅 1줄 SkipEnd) ② 이펙트 끝 = 모션 끝(사본 1개가 초승달 6개를 1.05 s 에 나눠 뿜어 2.74 s 까지 남던 것을 1.7 s 에 분출 멈춤 → 2.0 s 함께 소멸 · 그 순간 대기 동작) + 끝난 뒤 0.5 s 공격 금지 ③ 각성 착지 끝 잔상 = 캐릭터 정면 → 등 쪽 0.18 m 간격(popGhostDir) ④ 3타 회전 이펙트 = Slash Effects FREE Basic Slash Blue 사본 WL_Impact_DaggerRound(녹색 · 1바퀴 0.333 s 일정 회전 = 몸 회전과 같은 방향·속도 · 0.34 s 즉시 소멸) ⑤ 각성 피격 대상 주위 = 원본 주력 붉은 회전 검기 사본 WL_Fx_DaggerRedSlash(스크립트·판정 상자 제거 · 맞은 적마다 0.45 s 간격 · 배율 0.5 · 적 위치 추종)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
깃 관리자 2026-10-21 16:56:27 +09:00
parent e04e380307
commit 37b010fee7
16 changed files with 49568 additions and 26 deletions

View File

@ -19,6 +19,7 @@ public class Skill_ShadowSlash : ProjectileBase
_data.shooter.Set_Invisible(2.1f, () => _data.shooter.Set_Invisible(2.1f, () =>
{ {
OptionInfo.Ins.Set_PCMove(_data.shooter, true); OptionInfo.Ins.Set_PCMove(_data.shooter, true);
if (WL.Combat.Weapon.WLShadowSlash.SkipEnd(_data.shooter)) return; // WL #961 구르기로 캔슬된 시전 — 끝 대기 · 대상 뒤 순간이동 · 연기 · 카메라 돌리기 생략
_data.shooter.Play_Idle(); _data.shooter.Play_Idle();
if (!DSUtil.CheckNull(_data.target)) if (!DSUtil.CheckNull(_data.target))

View File

@ -141,6 +141,7 @@ namespace WL.Combat
if (!_pc.WL_CanAttackStep()) return false; if (!_pc.WL_CanAttackStep()) return false;
if (DashDriver.IsDashingNow(_pc)) return false; if (DashDriver.IsDashingNow(_pc)) return false;
if (WL.Combat.Weapon.WLGhostStrike.IsLanding(_pc)) return false; // #960 비수 각성 착지(양손 베기 = 4타 클립)는 옆 진입 뒤 멈춰야 한다 — 4타 전진 표가 착지 뒤 0.65 m 더 밀었다(실측 이동 0.75 m) if (WL.Combat.Weapon.WLGhostStrike.IsLanding(_pc)) return false; // #960 비수 각성 착지(양손 베기 = 4타 클립)는 옆 진입 뒤 멈춰야 한다 — 4타 전진 표가 착지 뒤 0.65 m 더 밀었다(실측 이동 0.75 m)
if (WL.Combat.Weapon.WLShadowSlash.IsSlashing(_pc)) return false; // #961 비수 주력 난사(1·2타 클립 강제 재생) 중 전진 표 금지 — 휘두를 때마다 밀려 10회에 약 1 m 전진 · 이펙트만 그 자리에 남았다(Play 실측)
return true; return true;
} }

View File

@ -0,0 +1,106 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: WL_Mat_Impact_DaggerRound_Hit
m_Shader: {fileID: 211, guid: 0000000000000000f000000000000000, type: 0}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords:
- _ALPHABLEND_ON
m_InvalidKeywords: []
m_LightmapFlags: 0
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: 3000
stringTagMap:
RenderType: Transparent
disabledShaderPasses:
- GRABPASS
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _BumpMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailAlbedoMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailMask:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailNormalMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _EmissionMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _MainTex:
m_Texture: {fileID: 2800000, guid: 0012501c22a23794bb55c9e9143eefea, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _MetallicGlossMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _OcclusionMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _ParallaxMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _BlendOp: 0
- _BumpScale: 1
- _CameraFadingEnabled: 0
- _CameraFarFadeDistance: 2
- _CameraNearFadeDistance: 1
- _ColorMode: 0
- _Cull: 2
- _Cutoff: 0.5
- _DetailNormalMapScale: 1
- _DistortionBlend: 0.5
- _DistortionEnabled: 0
- _DistortionStrength: 1
- _DistortionStrengthScaled: 0
- _DstBlend: 10
- _EmissionEnabled: 0
- _FlipbookMode: 0
- _GlossMapScale: 1
- _Glossiness: 0.5
- _GlossyReflections: 1
- _LightingEnabled: 0
- _Metallic: 0
- _Mode: 2
- _OcclusionStrength: 1
- _Parallax: 0.02
- _SmoothnessTextureChannel: 0
- _SoftParticlesEnabled: 0
- _SoftParticlesFarFadeDistance: 1
- _SoftParticlesNearFadeDistance: 0
- _SpecularHighlights: 1
- _SrcBlend: 5
- _UVSec: 0
- _ZWrite: 0
m_Colors:
- _CameraFadeParams: {r: 0, g: Infinity, b: 0, a: 0}
- _Color: {r: 1.3, g: 1.3, b: 1.3, a: 1}
- _ColorAddSubDiff: {r: 0, g: 0, b: 0, a: 0}
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
- _SoftParticleFadeParams: {r: 0, g: 0, b: 0, a: 0}
m_BuildTextureStacks: []
m_AllowLocking: 1

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 88816594c664ac143bbdc4f09f8d0602
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,106 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: WL_Mat_Impact_DaggerRound_Slash
m_Shader: {fileID: 211, guid: 0000000000000000f000000000000000, type: 0}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords:
- _ALPHABLEND_ON
m_InvalidKeywords: []
m_LightmapFlags: 0
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: 3000
stringTagMap:
RenderType: Transparent
disabledShaderPasses:
- GRABPASS
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _BumpMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailAlbedoMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailMask:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailNormalMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _EmissionMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _MainTex:
m_Texture: {fileID: 2800000, guid: db8918f74a0e0364ab3b43f13a6acda0, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _MetallicGlossMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _OcclusionMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _ParallaxMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _BlendOp: 0
- _BumpScale: 1
- _CameraFadingEnabled: 0
- _CameraFarFadeDistance: 2
- _CameraNearFadeDistance: 1
- _ColorMode: 0
- _Cull: 0
- _Cutoff: 0.5
- _DetailNormalMapScale: 1
- _DistortionBlend: 0.5
- _DistortionEnabled: 0
- _DistortionStrength: 1
- _DistortionStrengthScaled: 0
- _DstBlend: 10
- _EmissionEnabled: 0
- _FlipbookMode: 0
- _GlossMapScale: 1
- _Glossiness: 0.5
- _GlossyReflections: 1
- _LightingEnabled: 0
- _Metallic: 0
- _Mode: 2
- _OcclusionStrength: 1
- _Parallax: 0.02
- _SmoothnessTextureChannel: 0
- _SoftParticlesEnabled: 0
- _SoftParticlesFarFadeDistance: 1
- _SoftParticlesNearFadeDistance: 0
- _SpecularHighlights: 1
- _SrcBlend: 5
- _UVSec: 0
- _ZWrite: 0
m_Colors:
- _CameraFadeParams: {r: 0, g: Infinity, b: 0, a: 0}
- _Color: {r: 1.3, g: 1.3, b: 1.3, a: 1}
- _ColorAddSubDiff: {r: 0, g: 0, b: 0, a: 0}
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
- _SoftParticleFadeParams: {r: 0, g: 0, b: 0, a: 0}
m_BuildTextureStacks: []
m_AllowLocking: 1

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 77e4dc48bfd44c24b9179357e5331669
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 71a5270cb74269844a6bde61fa654675
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -105,15 +105,15 @@
{ {
"classIds": [ 10202 ], "classIds": [ 10202 ],
"stages": [ 3 ], "stages": [ 3 ],
"prefab": "WL_Impact_DaggerWhirl", "prefab": "WL_Impact_DaggerRound",
"scale": 1.0, "scale": 0.75,
"euler": [ 0, 0, 0 ], "euler": [ 0, 0, 0 ],
"yOffset": -0.25, "yOffset": -0.25,
"calibrated": false, "calibrated": false,
"follow": true, "follow": true,
"forward": 0.0, "forward": 0.0,
"side": 0.0, "side": 0.0,
"life": 1.0, "life": 0.34,
"minInterval": 0.3, "minInterval": 0.3,
"alignWeapon": false, "alignWeapon": false,
"alignFraction": 0.3, "alignFraction": 0.3,
@ -124,6 +124,7 @@
"spinSeconds": 0.333, "spinSeconds": 0.333,
"spinFx": false, "spinFx": false,
"spinBlendOut": 0.12, "spinBlendOut": 0.12,
"_note_961": "#961 PD 「비수 3타 회전 이펙트는 Slash Effects FREE 에셋의 한 번만 회전하는 이펙트 — 점차 줄어드는 이펙트가 아닌 회전 후 즉시 소멸하는 이펙트로 교체」(참고 = 청록 초승달 1개 + 흰 점 + 빛 번짐) — 팩 6종 입자 구조 실측(961_fxinfo.cs): 초승달 1개 + 빛 점 30 + 빛 번짐 2 = Basic Slash Blue(감속 회전 700→245°/s · 0.4 s 동안 약 190° · 투명도 1→0.53→0 서서히) → 사본 WL_Impact_DaggerRound(hitbuild936 · 녹색 색조 fxtint954 (0.25,0.95,0.7) ×1.3) 을 961_spinfx.cs 로 초승달 수명 0.333 s · 회전 1081°/s 일정(위에서 보면 반시계 = 몸 −360°/0.333 s 와 같은 방향 같은 속도 · 정확히 1바퀴) · 투명도 0.9 까지 1 → 끝 0 · 크기 변화 끔 · 빛 점·번짐 수명 ×0.83 · 편집 모드 위 카메라 실측 0.02→0.31 s 한 바퀴 · 0.35 s 완전 소멸 · life 0.34 = 풀이 그 순간 끔 · 배율 0.75(사본 고리 반지름 약 2.1 m × 0.75 × 0.55 × 몸 크기 ≈ 1.0 m = 단검 리치) · 이전 WL_Impact_DaggerWhirl(원본 Skill_SpinSlash 사본 · 1초 동안 3바퀴/초로 계속 돌고 입자가 차례로 사라짐)은 파일만 남김",
"_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_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_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_955b": "Play 실측: 팔라딘 Around 클립은 모델 yaw 36° 만 돌아(회전이 포즈에 안 구워짐) → 코드 회전 360°/0.9 s(클립 1.33 s ÷ 1.3)",

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 07b8e6310a9d7304aba19b4374daac35
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -67,6 +67,16 @@
"landGhostHold": 0.03, "landGhostHold": 0.03,
"landGhostFade": 0.18, "landGhostFade": 0.18,
"popGhosts": 3, "popGhosts": 3,
"popGhostDir": "back",
"popGhostStep": 0.18,
"_note_961b": "#961 PD 「비수 각성 스킬 사용 후 그림자가 캐릭터 앞으로 나오는 현상 → 캐릭터 뒤로 이어지게」(PD 캡처 = 착지 뒤 검은 잔상 덩어리가 캐릭터 앞 · 카메라 쪽) — 원인 = 끝 잔상 popGhosts 3개를 착지 정면(landFwd) ±18° 로 0.12 m 씩 앞에 놓았다 → 캐릭터 등 쪽(정면 반대)으로 0.18 m 간격 3개(같은 방향을 봄 · ±8°) · 1차 수정(옆 진입으로 들어온 길 쪽)은 Play 실측에서 PC 방향에 따라 카메라 쪽에 놓여(카메라 깊이 −0.2~−0.5 m) 캐릭터를 다시 덮어 popGhostDir entry 로만 남김",
"hitFxPrefab": "WL/Fx/WL_Fx_DaggerRedSlash",
"hitFxScale": 0.5,
"hitFxYOffset": 0.0,
"hitFxInterval": 0.45,
"hitFxLife": 1.2,
"hitFxFollow": 1,
"_note_961": "#961 PD 「비수 각성 스킬 시전 중일 때 피격 대상이 된 몬스터 주위에 기존 비수 주력 스킬 이펙트(붉은 빛의 회전하는 검기들)를 함께 재생」 — 원본 Skill_ShadowSlash 의 go_effect(자식 Slash · 붉은 칼날 고리 HighPolySwordRing + 검은 파동 고리 3 + 불똥 2 · 0.05~0.45 s 돌고 0.7 s 소멸 · 편집 모드 대조표)를 스크립트(Projectile_StopAndColliderControl) · 판정 상자 없이 떼어 낸 WL 사본 WL_Fx_DaggerRedSlash · 이 스킬 타격 확정(HitConfirmed)마다 맞은 적 몸 중심에 · 배율 0.5 = 주력 원본에 쓰던 slashScale 과 같은 크기 · 같은 적은 0.45 s(붉은 고리 한 번 도는 시간)마다 다시 · 1.2 s 뒤 파괴 · 밀려나는 적 위치를 따라감(hitFxFollow)",
"_note_955": "#955 PD 「너무 높은 데서 떨어짐 → 공중 낙하가 아니라 0.5 m 쯤 살짝 낮은 공간에서 순간이동하듯 등장하며 착지 · 수직 랜딩 뒤 미끄러짐이 아니라 전진하듯 살짝 미끄러지는 연출」 — 높이 0.5 → 0.2 · 낙하 0.15 → 0.06 s(거의 즉시 등장) · 등장 뒤 전진 미끄러짐 0.45 m / 0.3 s 감속 · 잔상 3", "_note_955": "#955 PD 「너무 높은 데서 떨어짐 → 공중 낙하가 아니라 0.5 m 쯤 살짝 낮은 공간에서 순간이동하듯 등장하며 착지 · 수직 랜딩 뒤 미끄러짐이 아니라 전진하듯 살짝 미끄러지는 연출」 — 높이 0.5 → 0.2 · 낙하 0.15 → 0.06 s(거의 즉시 등장) · 등장 뒤 전진 미끄러짐 0.45 m / 0.3 s 감속 · 잔상 3",
"_note_954": "#954 PD 「각성 뒤 너무 높은 위치에서 튀어나옴 → 0.5 m 정도 낮은 높이에서 반투명 상태로 갑자기 이펙트와 함께 튀어나오며 랜딩 · 랜딩 시간 짧게 · 랜딩 중 공격 불가」 — 높이 1.2 → 0.5 · 낙하 0.25 → 0.15 s · 전체 1.0 → 0.45 s · 미끄러짐 0.4/0.35 → 0.3/0.25 · 낙하 중 실제 모델 숨김 + 착지 자세(ghost_land@0.25)의 반투명 teal 캐릭터를 매 프레임(유지 0.02 · 소멸 0.08) · 착지 순간 모델 보임 + 잔상 2 + 먼지 · 공격 금지 = IsLanding(하네스 공격 키) + IsStop(자동 전투)", "_note_954": "#954 PD 「각성 뒤 너무 높은 위치에서 튀어나옴 → 0.5 m 정도 낮은 높이에서 반투명 상태로 갑자기 이펙트와 함께 튀어나오며 랜딩 · 랜딩 시간 짧게 · 랜딩 중 공격 불가」 — 높이 1.2 → 0.5 · 낙하 0.25 → 0.15 s · 전체 1.0 → 0.45 s · 미끄러짐 0.4/0.35 → 0.3/0.25 · 낙하 중 실제 모델 숨김 + 착지 자세(ghost_land@0.25)의 반투명 teal 캐릭터를 매 프레임(유지 0.02 · 소멸 0.08) · 착지 순간 모델 보임 + 잔상 2 + 먼지 · 공격 금지 = IsLanding(하네스 공격 키) + IsStop(자동 전투)",
"_note_953": "#953 PD 「비수 공격 시 카메라 각도가 달라짐 — 바꾸지 말고 현재 상태 유지한 채 착지(탑뷰가 아니라 대각선에서 미끄러지며 낙하하듯)」 — 원인 = 원본 Skill_DarkAmbush 가 시전 때 RealCamera.Set_TopView(PC) · 2.0 s 뒤 해제 → keepCamera 1 로 둘 다 건너뜀(원본 훅 2줄) · fallForward 0.35 m = 낙하 0.25 s 동안 앞으로(대각선) → 착지 뒤 slide 0.4 m 감속 · 값은 PM 추정(원장 40)", "_note_953": "#953 PD 「비수 공격 시 카메라 각도가 달라짐 — 바꾸지 말고 현재 상태 유지한 채 착지(탑뷰가 아니라 대각선에서 미끄러지며 낙하하듯)」 — 원인 = 원본 Skill_DarkAmbush 가 시전 때 RealCamera.Set_TopView(PC) · 2.0 s 뒤 해제 → keepCamera 1 로 둘 다 건너뜀(원본 훅 2줄) · fallForward 0.35 m = 낙하 0.25 s 동안 앞으로(대각선) → 착지 뒤 slide 0.4 m 감속 · 값은 PM 추정(원장 40)",

View File

@ -18,6 +18,13 @@
"hideOriginalFx": 1, "hideOriginalFx": 1,
"slashStates": [ "attack", "DualBlades_Attack2" ], "slashStates": [ "attack", "DualBlades_Attack2" ],
"slashInterval": 0.2, "slashInterval": 0.2,
"moveHold": 1,
"dodgeCancel": 1,
"fxEndAlign": 1,
"fxVisibleLife": 0.3,
"endHoldSeconds": 0.5,
"endIdle": 1,
"_note_961": "#961 PD 「주력 중 캐릭터가 움직이면 이펙트만 그 자리에 남음 → 못 움직이게 · 구르기로 캔슬하면 이펙트도 같이 소멸」 · 「이펙트 소멸 타이밍에 맞게 모션도 종료 · 최소 0.5 s 딜레이 후 공격」 — 이동 = 원본이 PCMove 를 끄지만 하네스 WASD · 가상패드가 Run_byJoystick 으로 움직였음 → moveHold(WLSkillCancel.HoldMove 2.0 s · 구르기는 됨) · 구르기 = 난사 · 반복 · 잔상 · 판정 멈춤 + 살아 있는 정면 사본 지움 · 정면 사본 반복을 0.3 s(초승달 보이는 시간) 일찍 끝내 마지막 이펙트가 난사 끝 2.0 s 에 함께 사라짐 · 끝난 뒤 0.5 s 공격 금지(IsStop 매 프레임 · 하네스 공격 키 IsHolding) · endIdle = 난사 끝(마지막 이펙트 소멸 2.0 s)에 바로 대기 동작(원본은 2.1 s 콜백에서 대기 · 그사이 마지막 휘두름이 이어짐) · 구르기 캔슬이면 원본 2.1 s 끝 처리(대기 · 대상 뒤 순간이동 · 연기 · 카메라 돌리기)를 건너뜀(Skill_ShadowSlash 훅 1줄 SkipEnd) · Play 실측(961_verify): ⓐ 이동 입력은 막혔는데(WASD 막힘 72 · 패드 0 m) 시전 중 0.65~1.06 m 전진 = 강제 재생하는 1·2타 클립의 공격 전진 표(AttackRootMotion) → 난사 중 Gate 차단(IsSlashing) ⓑ 사본 1개 = 초승달 6개를 0~0.75 s 에 나눠 뿜어 약 1.05 s 보임(fxVisibleLife 0.3 가정이 틀려 입자가 2.74 s 까지 남음) → 반복을 멈추는 1.7 s 에 살아 있는 사본의 분출도 멈춤(이미 나온 초승달 수명 0.3 s 만) = 2.0 s 에 함께 소멸",
"slashHits": 10, "slashHits": 10,
"slashHitDelay": 0.1, "slashHitDelay": 0.1,
"slashHitRadius": 3.5, "slashHitRadius": 3.5,

View File

@ -64,6 +64,14 @@ namespace WL.Combat.Weapon
// landStateStart = 착지 상태를 이 정규화 시각부터(양손 베기 클립의 베는 구간이 옆 진입 동안 보이게) · landNoEvents 1 = 착지 동안 애니메이션 이벤트 끔(4타 클립의 순간이동 · 카메라 고정 · 타격이 착지에 끼지 않게) · // landStateStart = 착지 상태를 이 정규화 시각부터(양손 베기 클립의 베는 구간이 옆 진입 동안 보이게) · landNoEvents 1 = 착지 동안 애니메이션 이벤트 끔(4타 클립의 순간이동 · 카메라 고정 · 타격이 착지에 끼지 않게) ·
// entryGhostInterval > 0 = 옆에서 들어오는 동안(실제 모델이 보일 때) 이 간격으로 지금 자세 잔상(landGhostColor · landGhostHold · landGhostFade) = 그림자처럼 지나온 길 // entryGhostInterval > 0 = 옆에서 들어오는 동안(실제 모델이 보일 때) 이 간격으로 지금 자세 잔상(landGhostColor · landGhostHold · landGhostFade) = 그림자처럼 지나온 길
public float landStateStart = 0f, entryGhostInterval = 0f; public float landStateStart = 0f, entryGhostInterval = 0f;
public float popGhostStep = 0.18f; // #961 끝 잔상 간격(m · 캐릭터 뒤로 이어짐)
public string popGhostDir = "back"; // #961 끝 잔상 방향 — back = 캐릭터 등 쪽(정면 반대 · 이전 = 정면) · entry = 옆 진입으로 들어온 길 쪽
// #961 PD 「비수 각성 시전 중 피격 대상이 된 몬스터 주위에 기존 비수 주력 이펙트(붉은 빛의 회전하는 검기들)도 함께 재생」 —
// hitFxPrefab = 이 스킬에 맞은 적마다 몸 중심에 띄우는 이펙트(Resources 경로) · hitFxScale 배율 · hitFxYOffset 높이(m) ·
// hitFxInterval = 같은 적에게 다시 띄우는 간격(초 · 맞는 동안 이어 보이게) · hitFxLife = 띄운 뒤 지우는 초 · hitFxFollow 1 = 밀려나는 적을 따라감(위치만)
public string hitFxPrefab = "";
public float hitFxScale = 0.5f, hitFxYOffset = 0f, hitFxInterval = 0.45f, hitFxLife = 1.2f;
public int hitFxFollow = 1;
public int landNoEvents = 1; public int landNoEvents = 1;
public string poseSource = "clips"; // clips = 숨은 꼭두각시에 poses 를 걸어 굽기 · live = 시전 순간 PC 자세 1 개 public string poseSource = "clips"; // clips = 숨은 꼭두각시에 poses 를 걸어 굽기 · live = 시전 순간 PC 자세 1 개
public string[] poses = new string[0]; // "애니메이터 상태 이름@정규화 시각" — 차례로 돌려 쓴다(시작은 무작위) public string[] poses = new string[0]; // "애니메이터 상태 이름@정규화 시각" — 차례로 돌려 쓴다(시작은 무작위)
@ -133,8 +141,9 @@ namespace WL.Combat.Weapon
static readonly Dictionary<string, WLGhostStrikeRule> s_rules = new Dictionary<string, WLGhostStrikeRule>(); static readonly Dictionary<string, WLGhostStrikeRule> s_rules = new Dictionary<string, WLGhostStrikeRule>();
static readonly Dictionary<int, List<FxPoint>> s_fxCache = new Dictionary<int, List<FxPoint>>(); static readonly Dictionary<int, List<FxPoint>> s_fxCache = new Dictionary<int, List<FxPoint>>();
static Cast s_cast; static Cast s_cast;
static readonly List<HitReq> s_hitQ = new List<HitReq>(16); static readonly List<HitReq> s_hitQ = new List<HitReq>(16), s_fxQ = new List<HitReq>(16); // #961 s_fxQ = 피격 대상 이펙트 요청
static readonly Dictionary<Actor, float> s_bearing = new Dictionary<Actor, float>(), s_lastHitAt = new Dictionary<Actor, float>(); static readonly Dictionary<Actor, float> s_bearing = new Dictionary<Actor, float>(), s_lastHitAt = new Dictionary<Actor, float>(), s_lastFxAt = new Dictionary<Actor, float>();
public static int HitFxSpawned; public static string HitFxLog = "";
static readonly List<Pose> s_poses = new List<Pose>(); static readonly List<Pose> s_poses = new List<Pose>();
static string s_poseSig = ""; static string s_poseSig = "";
static readonly List<Ghost> s_pool = new List<Ghost>(); static readonly List<Ghost> s_pool = new List<Ghost>();
@ -163,12 +172,12 @@ namespace WL.Combat.Weapon
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
static void Boot() static void Boot()
{ {
s_cfg = null; s_cast = null; s_hitQ.Clear(); s_bearing.Clear(); s_lastHitAt.Clear(); s_fxCache.Clear(); s_cfg = null; s_cast = null; s_hitQ.Clear(); s_bearing.Clear(); s_lastHitAt.Clear(); s_fxCache.Clear(); s_fxQ.Clear(); s_lastFxAt.Clear(); HitFxSpawned = 0; HitFxLog = "";
s_poses.Clear(); s_poseSig = ""; s_pool.Clear(); s_mat = null; s_depthMat = null; s_matsPre = s_matsOne = null; s_matTried = false; s_runner = null; s_poses.Clear(); s_poseSig = ""; s_pool.Clear(); s_mat = null; s_depthMat = null; s_matsPre = s_matsOne = null; s_matTried = false; s_runner = null;
Casts = FxSpawned = HitSpawned = SkippedCap = PuppetBuilt = PuppetFailed = PosesBaked = LiveBaked = Errors = 0; LastBakeMs = 0f; LastLog = ""; JumpSpawned = Landings = 0; LandLog = ""; Casts = FxSpawned = HitSpawned = SkippedCap = PuppetBuilt = PuppetFailed = PosesBaked = LiveBaked = Errors = 0; LastBakeMs = 0f; LastLog = ""; JumpSpawned = Landings = 0; LandLog = "";
CombatEvents.HitConfirmed.Remove(OnHit); CombatEvents.HitConfirmed.Remove(OnHit);
if (Cfg.enabled == 0) return; if (Cfg.enabled == 0) return;
foreach (var r in s_rules.Values) if (r.anchor != "fx") { CombatEvents.HitConfirmed.Add(OnHit); break; } foreach (var r in s_rules.Values) if (r.anchor != "fx" || !string.IsNullOrEmpty(r.hitFxPrefab)) { CombatEvents.HitConfirmed.Add(OnHit); break; } // #961 피격 대상 이펙트도 타격 확정을 본다
} }
public static string Describe() public static string Describe()
@ -202,7 +211,7 @@ namespace WL.Combat.Weapon
if (r.anchor != "hit") cast.fx = FxPoints(skill, r); if (r.anchor != "hit") cast.fx = FxPoints(skill, r);
Capture(cast, c); Capture(cast, c);
s_cast = cast; s_cast = cast;
s_hitQ.Clear(); s_bearing.Clear(); s_lastHitAt.Clear(); s_hitQ.Clear(); s_bearing.Clear(); s_lastHitAt.Clear(); s_fxQ.Clear(); s_lastFxAt.Clear();
Casts++; Casts++;
EnsureRunner(); EnsureRunner();
LastLog = "시전 " + r.skill + " · fx " + (cast.fx != null ? cast.fx.Count : 0) + " · 부품 " + cast.smrs.Count + "+" + cast.weapons.Count; LastLog = "시전 " + r.skill + " · fx " + (cast.fx != null ? cast.fx.Count : 0) + " · 부품 " + cast.smrs.Count + "+" + cast.weapons.Count;
@ -223,11 +232,14 @@ namespace WL.Combat.Weapon
try try
{ {
var cast = s_cast; var cast = s_cast;
if (cast == null || cast.rule.anchor == "fx" || DSUtil.CheckNull(e.victim) || !ReferenceEquals(e.attacker, cast.shooter)) return; if (cast == null || DSUtil.CheckNull(e.victim) || !ReferenceEquals(e.attacker, cast.shooter)) return;
bool ghost = cast.rule.anchor != "fx", fx = !string.IsNullOrEmpty(cast.rule.hitFxPrefab); // #961 피격 대상 이펙트
if (!ghost && !fx) return;
var pd = e.projectile; var pd = e.projectile;
var sk = pd != null ? pd.m_SkillListTableData : null; var sk = pd != null ? pd.m_SkillListTableData : null;
if (sk == null || sk.e_Skill.ToString() != cast.rule.skill) return; if (sk == null || sk.e_Skill.ToString() != cast.rule.skill) return;
if (s_hitQ.Count < Mathf.Max(1, Cfg.maxAlive) * 4) s_hitQ.Add(new HitReq { victim = e.victim, time = Time.time }); if (ghost && s_hitQ.Count < Mathf.Max(1, Cfg.maxAlive) * 4) s_hitQ.Add(new HitReq { victim = e.victim, time = Time.time });
if (fx && s_fxQ.Count < 32) s_fxQ.Add(new HitReq { victim = e.victim, time = Time.time });
} }
catch (System.Exception ex) { Errors++; LastLog = "hit 실패 — " + ex.Message; } catch (System.Exception ex) { Errors++; LastLog = "hit 실패 — " + ex.Message; }
} }
@ -265,10 +277,11 @@ namespace WL.Combat.Weapon
cast.lastPos = pos; cast.hasLast = true; cast.lastPos = pos; cast.hasLast = true;
} }
for (int i = 0; i < s_hitQ.Count; i++) HitSpawn(c, cast, s_hitQ[i]); for (int i = 0; i < s_hitQ.Count; i++) HitSpawn(c, cast, s_hitQ[i]);
for (int i = 0; i < s_fxQ.Count; i++) HitFxSpawn(cast, s_fxQ[i]); // #961
bool landPending = !string.IsNullOrEmpty(cast.rule.landState) && !cast.landed && now - cast.t0 < Mathf.Max(0.1f, cast.rule.castSeconds) + 4f; // #951 착지가 끝날 때까지(상한 +4 s) 시전 기록 유지 bool landPending = !string.IsNullOrEmpty(cast.rule.landState) && !cast.landed && now - cast.t0 < Mathf.Max(0.1f, cast.rule.castSeconds) + 4f; // #951 착지가 끝날 때까지(상한 +4 s) 시전 기록 유지
if (now - cast.t0 > Mathf.Max(0.1f, cast.rule.castSeconds) && !cast.landing && !landPending) s_cast = null; if (now - cast.t0 > Mathf.Max(0.1f, cast.rule.castSeconds) && !cast.landing && !landPending) s_cast = null;
} }
s_hitQ.Clear(); s_hitQ.Clear(); s_fxQ.Clear();
for (int i = 0; i < s_pool.Count; i++) for (int i = 0; i < s_pool.Count; i++)
{ {
@ -475,7 +488,12 @@ namespace WL.Combat.Weapon
LandLog += " · 옆 진입 " + cast.entryMoved.ToString("F2") + " m"; LandLog += " · 옆 진입 " + cast.entryMoved.ToString("F2") + " m";
} }
if (cast.hideDuringFall) { cast.hideDuringFall = false; SetVisible(cast, true); } // #954 착지 순간 실제 모델이 튀어나온다 if (cast.hideDuringFall) { cast.hideDuringFall = false; SetVisible(cast, true); } // #954 착지 순간 실제 모델이 튀어나온다
if (r.popGhosts > 0 && s_landPose != null) for (int i = 0; i < r.popGhosts; i++) { var jitter = Quaternion.Euler(0f, (i - (r.popGhosts - 1) * 0.5f) * 18f, 0f) * cast.landFwd; if (Spawn(c, cast, cast.shooter.Get_position() + jitter * 0.12f * (i + 1), jitter, s_landPose)) JumpSpawned++; } if (r.popGhosts > 0 && s_landPose != null)
{
// #961 PD 「각성 뒤 그림자가 캐릭터 앞으로 나온다 → 캐릭터 뒤로 이어지게」 — 이전 = 정면(landFwd) ±18° 로 0.12 m 씩 앞에 놓였다 · 이제 popGhostDir(기본 back = 캐릭터 등 쪽 · entry = 옆 진입으로 들어온 길 쪽)으로 popGhostStep 씩
Vector3 back = r.popGhostDir == "entry" && r.entrySide != 0f ? -cast.landRight * Mathf.Sign(r.entrySide) : -cast.landFwd; // Play 실측: 들어온 길 쪽은 PC 방향에 따라 카메라 쪽(캐릭터를 덮음)이 되어 기본 = 등 쪽
for (int i = 0; i < r.popGhosts; i++) { var face = Quaternion.Euler(0f, (i - (r.popGhosts - 1) * 0.5f) * 8f, 0f) * cast.landFwd; if (Spawn(c, cast, cast.shooter.Get_position() + back * r.popGhostStep * (i + 1), face, s_landPose)) JumpSpawned++; }
}
if (!string.IsNullOrEmpty(r.dustPrefab)) if (!string.IsNullOrEmpty(r.dustPrefab))
{ {
var p = Resources.Load<GameObject>(r.dustPrefab); var p = Resources.Load<GameObject>(r.dustPrefab);
@ -563,6 +581,27 @@ namespace WL.Combat.Weapon
if (Spawn(c, cast, pos, -dir)) HitSpawned++; if (Spawn(c, cast, pos, -dir)) HitSpawned++;
} }
/// <summary>#961 이 스킬에 맞은 적의 몸 중심에 hitFxPrefab(원본 주력 붉은 회전 검기 사본)을 띄운다 — 같은 적은 hitFxInterval 마다 · 월드 축(원본 Projectile_StopAndColliderControl 과 같은 방향) · hitFxLife 뒤 파괴.</summary>
static void HitFxSpawn(Cast cast, HitReq q)
{
var r = cast.rule; var v = q.victim;
if (DSUtil.CheckNull(v) || v.IsDead()) return;
float last;
if (s_lastFxAt.TryGetValue(v, out last) && q.time - last < r.hitFxInterval) return;
var p = Resources.Load<GameObject>(r.hitFxPrefab);
if (p == null) { HitFxLog = "이펙트 없음 " + r.hitFxPrefab; return; }
s_lastFxAt[v] = q.time;
var offset = v.Get_Center_position() - v.Get_position() + Vector3.up * r.hitFxYOffset;
var go = Object.Instantiate(p, v.Get_position() + offset, Quaternion.identity);
go.transform.localScale = p.transform.localScale * Mathf.Max(0.01f, r.hitFxScale);
foreach (var ps in go.GetComponentsInChildren<ParticleSystem>(true)) { var m = ps.main; m.scalingMode = ParticleSystemScalingMode.Hierarchy; }
var root = go.GetComponent<ParticleSystem>(); if (root != null) root.Play(true);
if (r.hitFxFollow != 0) { var f = go.AddComponent<WLGhostHitFxFollow>(); f.target = v.transform; f.offset = offset; } // 적 자식으로 두지 않는다(적 렌더러 일괄 처리 · 풀 반환에 휩쓸리지 않게)
Object.Destroy(go, Mathf.Max(0.3f, r.hitFxLife));
HitFxSpawned++;
HitFxLog = "피격 이펙트 " + HitFxSpawned + " · " + v.name + " @" + go.transform.position.ToString("F2") + " ×" + r.hitFxScale.ToString("F2") + " · 시전 뒤 " + (q.time - cast.t0).ToString("F2") + " s";
}
// ───────────────────────────────────────── 부품 · 자세 // ───────────────────────────────────────── 부품 · 자세
/// <summary>훅 순간(PC 가 보일 때) 켜져 있는 부품의 경로 · 서명(PC · 컨트롤러 · 메시 · poses).</summary> /// <summary>훅 순간(PC 가 보일 때) 켜져 있는 부품의 경로 · 서명(PC · 컨트롤러 · 메시 · poses).</summary>
static void Capture(Cast cast, WLGhostStrikeJson c) static void Capture(Cast cast, WLGhostStrikeJson c)
@ -863,4 +902,11 @@ namespace WL.Combat.Weapon
/// <summary>#943-⑦ 고스트 러너(숨김 GameObject 1 개) — 예약 · 요청 생성 · 페이드 · 꼭두각시 굽기는 여기(Update)에서만.</summary> /// <summary>#943-⑦ 고스트 러너(숨김 GameObject 1 개) — 예약 · 요청 생성 · 페이드 · 꼭두각시 굽기는 여기(Update)에서만.</summary>
public sealed class WLGhostStrikeRunner : MonoBehaviour { void Update() { WLGhostStrike.Tick(); } void LateUpdate() { WLGhostStrike.LateTick(); } } public sealed class WLGhostStrikeRunner : MonoBehaviour { void Update() { WLGhostStrike.Tick(); } void LateUpdate() { WLGhostStrike.LateTick(); } }
/// <summary>#961 피격 대상 이펙트가 밀려나는 적을 위치만 따라간다(적이 꺼지면 그 자리에 남음).</summary>
public sealed class WLGhostHitFxFollow : MonoBehaviour
{
public Transform target; public Vector3 offset;
void LateUpdate() { if (target != null && target.gameObject.activeInHierarchy) transform.position = target.position + offset; }
}
} }

View File

@ -59,6 +59,12 @@ namespace WL.Combat.Weapon
// 범위 = PC 발 기준 slashHitRadius(0 = 공격 범위 × slashHitRangeMul) + 적 몸 반지름 · 대상(없으면 정면) 쪽 ±slashHitHalfAngle° // 범위 = PC 발 기준 slashHitRadius(0 = 공격 범위 × slashHitRangeMul) + 적 몸 반지름 · 대상(없으면 정면) 쪽 ±slashHitHalfAngle°
public int slashHits = 0; public int slashHits = 0;
public float slashHitDelay = 0.1f, slashHitRadius = 0f, slashHitRangeMul = 1.6f, slashHitHalfAngle = 110f; public float slashHitDelay = 0.1f, slashHitRadius = 0f, slashHitRangeMul = 1.6f, slashHitHalfAngle = 110f;
// #961 PD 「주력 중 캐릭터가 움직이면 이펙트만 그 자리에 남음 → 못 움직이게 · 구르기로 캔슬하면 이펙트도 같이 소멸」 · 「이펙트 소멸 타이밍에 맞게 모션도 종료 · 최소 0.5 s 딜레이 후 공격」 —
// moveHold 1 = 난사 동안 이동 입력 무시(WLSkillCancel.HoldMove · 구르기는 그대로) · dodgeCancel 1 = 구르기(eAnim.Dodge)가 시작되면 난사 · 이펙트 반복 · 잔상 · 판정을 멈추고 살아 있는 이펙트를 지운다 ·
// fxEndAlign 1 = 정면 사본 반복을 fxVisibleLife(초승달 보이는 시간) 만큼 일찍 끝내 마지막 이펙트가 난사 끝에 함께 사라지게 · endHoldSeconds = 난사가 끝난 뒤 공격 금지(자동 전투 IsStop · 하네스 공격 키)
public int moveHold = 0, dodgeCancel = 0, fxEndAlign = 0;
public float fxVisibleLife = 0.3f, endHoldSeconds = 0f;
public int endIdle = 0; // #961 1 = 난사가 끝나는 순간(= 마지막 이펙트 소멸) 대기 동작으로(원본은 0.1 s 뒤 2.1 s 콜백에서 대기 · 그사이 마지막 휘두름이 이어졌다)
} }
public static class WLShadowSlash public static class WLShadowSlash
@ -84,7 +90,7 @@ namespace WL.Combat.Weapon
} }
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
static void Boot() { s_cfg = null; Casts = Shown = Scaled = Errors = 0; SlashLoops = SlashLeaks = FxShown = FxRepeats = FxSlashes = SlashGhostCount = SlashHits = SlashHitTargets = 0; LastLog = SlashLog = FxRepeatLog = SlashHitLog = ""; } static void Boot() { s_cfg = null; Casts = Shown = Scaled = Errors = 0; SlashLoops = SlashLeaks = FxShown = FxRepeats = FxSlashes = SlashGhostCount = SlashHits = SlashHitTargets = 0; DodgeCancels = EndHolds = EndSkips = EndIdles = FxStops = 0; s_holdActor = s_castShooter = s_slashActor = null; s_liveFx.Clear(); LastLog = SlashLog = FxRepeatLog = SlashHitLog = ""; }
public static string Describe() { return "casts " + Casts + " · shown " + Shown + " · scaled " + Scaled + " · err " + Errors + " · fx " + FxShown + "(반복 " + FxRepeats + ") · 잔상 " + SlashGhostCount + " · " + LastLog; } public static string Describe() { return "casts " + Casts + " · shown " + Shown + " · scaled " + Scaled + " · err " + Errors + " · fx " + FxShown + "(반복 " + FxRepeats + ") · 잔상 " + SlashGhostCount + " · " + LastLog; }
@ -107,14 +113,16 @@ namespace WL.Combat.Weapon
} }
if (skill.go_effect != null) Scale(skill.go_effect.transform, c.slashScale, c.keepHitBox != 0); if (skill.go_effect != null) Scale(skill.go_effect.transform, c.slashScale, c.keepHitBox != 0);
if (skill.go_invisibleEffect != null) Scale(skill.go_invisibleEffect.transform, c.smokeScale, false); if (skill.go_invisibleEffect != null) Scale(skill.go_invisibleEffect.transform, c.smokeScale, false);
int seq = ++s_castSeq; s_liveFx.Clear(); s_castShooter = shooter; // #961 이번 시전 번호(구르기 캔슬 때 반복 · 잔상을 멈추는 표지)
if (!string.IsNullOrEmpty(c.fxPrefab) && !DSUtil.CheckNull(shooter)) // #954 if (!string.IsNullOrEmpty(c.fxPrefab) && !DSUtil.CheckNull(shooter)) // #954
{ {
if (c.hideOriginalFx != 0 && skill.go_effect != null) foreach (var rd in skill.go_effect.GetComponentsInChildren<Renderer>(true)) rd.enabled = false; // 풀 인스턴스라 한 번 끄면 유지 if (c.hideOriginalFx != 0 && skill.go_effect != null) foreach (var rd in skill.go_effect.GetComponentsInChildren<Renderer>(true)) rd.enabled = false; // 풀 인스턴스라 한 번 끄면 유지
if (c.fxMain != 0) Runner.StartCoroutine(CoFx(shooter, c)); // #956 fxMain 0 = 정면 사본 없이 난사 사본(fxPerSlash)만 if (c.fxMain != 0) Runner.StartCoroutine(CoFx(shooter, c, seq)); // #956 fxMain 0 = 정면 사본 없이 난사 사본(fxPerSlash)만
} }
HitMode(skill, c); // #960 원본 6회 판정 끄기(slashHits > 0) / 되돌리기 HitMode(skill, c); // #960 원본 6회 판정 끄기(slashHits > 0) / 되돌리기
if (c.slashStates != null && c.slashStates.Length > 0 && !DSUtil.CheckNull(shooter) && shooter.m_animation != null) Runner.StartCoroutine(CoSlash(shooter, c, skill)); // #955 · #960 휘두름마다 판정 if (c.moveHold != 0 && shooter is PCActor mh) WLSkillCancel.HoldMove(mh, Mathf.Max(0.1f, c.slashSeconds)); // #961 난사 동안 이동 입력 무시(구르기는 됨)
if (c.slashGhosts != 0 && !DSUtil.CheckNull(shooter)) Runner.StartCoroutine(CoGhosts(shooter, c)); // #959 난사 잔상 if (c.slashStates != null && c.slashStates.Length > 0 && !DSUtil.CheckNull(shooter) && shooter.m_animation != null) Runner.StartCoroutine(CoSlash(shooter, c, skill, seq)); // #955 · #960 휘두름마다 판정 · #961 구르기 캔슬 · 끝 공격 금지
if (c.slashGhosts != 0 && !DSUtil.CheckNull(shooter)) Runner.StartCoroutine(CoGhosts(shooter, c, seq)); // #959 난사 잔상
LastLog = "cast " + Casts + " · shown " + Shown + " · scaled " + Scaled; LastLog = "cast " + Casts + " · shown " + Shown + " · scaled " + Scaled;
} }
catch (System.Exception ex) { Errors++; LastLog = "실패 — " + ex.Message; } catch (System.Exception ex) { Errors++; LastLog = "실패 — " + ex.Message; }
@ -125,18 +133,24 @@ namespace WL.Combat.Weapon
static WLShadowSlashRunner Runner { get { if (s_runner == null) { var go = new GameObject("[WL954] ShadowSlashFx"); go.hideFlags = HideFlags.HideAndDontSave; Object.DontDestroyOnLoad(go); s_runner = go.AddComponent<WLShadowSlashRunner>(); } return s_runner; } } static WLShadowSlashRunner Runner { get { if (s_runner == null) { var go = new GameObject("[WL954] ShadowSlashFx"); go.hideFlags = HideFlags.HideAndDontSave; Object.DontDestroyOnLoad(go); s_runner = go.AddComponent<WLShadowSlashRunner>(); } return s_runner; } }
public static int FxRepeats; public static string FxRepeatLog = ""; public static int FxRepeats; public static string FxRepeatLog = "";
/// <summary>#954 원본 베기가 켜지는 시각(fxDelay)에 WL 사본(녹색 난사)을 PC 정면 fxForward 에 띄운다(PC 방향 · fxLife 뒤 파괴). #959 fxRepeatInterval > 0 이면 fxRepeatSeconds(0 = slashSeconds)까지 되풀이 — 매번 그때 PC 위치·대상 방향.</summary> /// <summary>#954 원본 베기가 켜지는 시각(fxDelay)에 WL 사본(녹색 난사)을 PC 정면 fxForward 에 띄운다(PC 방향 · fxLife 뒤 파괴). #959 fxRepeatInterval > 0 이면 fxRepeatSeconds(0 = slashSeconds)까지 되풀이 — 매번 그때 PC 위치·대상 방향.</summary>
static System.Collections.IEnumerator CoFx(Actor shooter, WLShadowSlashJson c) static System.Collections.IEnumerator CoFx(Actor shooter, WLShadowSlashJson c, int seq)
{ {
if (c.fxDelay > 0f) yield return new WaitForSeconds(c.fxDelay); if (c.fxDelay > 0f) yield return new WaitForSeconds(c.fxDelay);
float t0 = Time.time - c.fxDelay, until = c.fxRepeatSeconds > 0f ? c.fxRepeatSeconds : c.slashSeconds; int n = 0; float t0 = Time.time - c.fxDelay, until = c.fxRepeatSeconds > 0f ? c.fxRepeatSeconds : c.slashSeconds; int n = 0;
if (c.fxEndAlign != 0) until = Mathf.Max(c.fxDelay + 0.01f, until - Mathf.Max(0f, c.fxVisibleLife)); // #961 마지막 초승달이 난사 끝에 함께 사라지게
while (true) while (true)
{ {
if (DSUtil.CheckNull(shooter) || shooter.IsDead()) yield break; if (DSUtil.CheckNull(shooter) || shooter.IsDead() || s_cancelSeq == seq) yield break;
if (!SpawnMainFx(shooter, c)) yield break; if (!SpawnMainFx(shooter, c)) yield break;
n++; if (n > 1) FxRepeats++; n++; if (n > 1) FxRepeats++;
if (c.fxRepeatInterval <= 0f) yield break; if (c.fxRepeatInterval <= 0f) yield break;
yield return new WaitForSeconds(c.fxRepeatInterval); yield return new WaitForSeconds(c.fxRepeatInterval);
if (Time.time - t0 + 0.01f >= until) { FxRepeatLog = "정면 사본 " + n + "개 · " + (Time.time - t0).ToString("F2") + " s"; yield break; } if (Time.time - t0 + 0.01f >= until)
{
// #961 사본 1개 = 초승달 6개를 0~0.75 s 에 나눠 뿜어 약 1.05 s 보인다(편집 모드 실측) → 반복을 멈추는 이 순간 살아 있는 사본의 분출도 멈춰 이미 나온 초승달(수명 fxVisibleLife)만 끝까지 = 난사 끝에 함께 소멸
if (c.fxEndAlign != 0) foreach (var go in s_liveFx) if (go != null) foreach (var ps in go.GetComponentsInChildren<ParticleSystem>(true)) { ps.Stop(false, ParticleSystemStopBehavior.StopEmitting); FxStops++; }
FxRepeatLog = "정면 사본 " + n + "개 · " + (Time.time - t0).ToString("F2") + " s" + (c.fxEndAlign != 0 ? " · 분출 멈춤" : ""); yield break;
}
} }
} }
static bool SpawnMainFx(Actor shooter, WLShadowSlashJson c) static bool SpawnMainFx(Actor shooter, WLShadowSlashJson c)
@ -157,6 +171,7 @@ namespace WL.Combat.Weapon
var go = Object.Instantiate(p, pos, Quaternion.LookRotation(fwd, Vector3.up)); go.transform.localScale = p.transform.localScale * scale; 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); } 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++; Object.Destroy(go, Mathf.Max(0.5f, c.fxLife)); FxShown++;
s_liveFx.RemoveAll(x => x == null); s_liveFx.Add(go); // #961 구르기 캔슬 때 지울 목록
LastLog = "fx " + c.fxPrefab + " @" + pos.ToString("F2") + " ×" + scale.ToString("F3") + " · 거리 " + dist.ToString("F2") + " · 공격 범위 " + range.ToString("F2") + " · 대상 " + (toTarget > 0f ? toTarget.ToString("F2") + " m" : "없음"); LastLog = "fx " + c.fxPrefab + " @" + pos.ToString("F2") + " ×" + scale.ToString("F3") + " · 거리 " + dist.ToString("F2") + " · 공격 범위 " + range.ToString("F2") + " · 대상 " + (toTarget > 0f ? toTarget.ToString("F2") + " m" : "없음");
FxSlashLog = LastLog; FxSlashLog = LastLog;
return true; return true;
@ -164,12 +179,12 @@ namespace WL.Combat.Weapon
public static int SlashGhostCount; public static int SlashGhostCount;
/// <summary>#959 난사 동안 지금 자세 잔상 — slashGhostInterval 마다 WLGhostStrike.LiveGhost(색 · 유지 · 소멸).</summary> /// <summary>#959 난사 동안 지금 자세 잔상 — slashGhostInterval 마다 WLGhostStrike.LiveGhost(색 · 유지 · 소멸).</summary>
static System.Collections.IEnumerator CoGhosts(Actor shooter, WLShadowSlashJson c) static System.Collections.IEnumerator CoGhosts(Actor shooter, WLShadowSlashJson c, int seq)
{ {
float t0 = Time.time; var col = c.slashGhostColor != null && c.slashGhostColor.Length >= 4 ? c.slashGhostColor : null; float t0 = Time.time; var col = c.slashGhostColor != null && c.slashGhostColor.Length >= 4 ? c.slashGhostColor : null;
while (Time.time - t0 < c.slashSeconds) while (Time.time - t0 < c.slashSeconds)
{ {
if (DSUtil.CheckNull(shooter) || shooter.IsDead()) yield break; if (DSUtil.CheckNull(shooter) || shooter.IsDead() || s_cancelSeq == seq) yield break;
if (WLGhostStrike.LiveGhost(shooter, col, c.slashGhostHold, c.slashGhostFade)) SlashGhostCount++; if (WLGhostStrike.LiveGhost(shooter, col, c.slashGhostHold, c.slashGhostFade)) SlashGhostCount++;
yield return new WaitForSeconds(Mathf.Max(0.02f, c.slashGhostInterval)); yield return new WaitForSeconds(Mathf.Max(0.02f, c.slashGhostInterval));
} }
@ -216,9 +231,11 @@ namespace WL.Combat.Weapon
public static int SlashLoops, SlashLeaks; public static string SlashLog = ""; public static int SlashLoops, SlashLeaks; public static string SlashLog = "";
/// <summary>#955 제자리 난사 — slashSeconds 동안 slashStates 를 번갈아 CrossFade(이벤트 끔 · 배속) · 스킬 상태를 벗어나면 중단 · 끝에 이벤트·배속 원상. #960 휘두름마다 판정(slashHits).</summary> /// <summary>#955 제자리 난사 — slashSeconds 동안 slashStates 를 번갈아 CrossFade(이벤트 끔 · 배속) · 스킬 상태를 벗어나면 중단 · 끝에 이벤트·배속 원상. #960 휘두름마다 판정(slashHits).</summary>
static System.Collections.IEnumerator CoSlash(Actor shooter, WLShadowSlashJson c, ProjectileBase pb) static System.Collections.IEnumerator CoSlash(Actor shooter, WLShadowSlashJson c, ProjectileBase pb, int seq)
{ {
var an = shooter.m_animation; if (an == null) yield break; var an = shooter.m_animation; if (an == null) yield break;
bool canceled = false; // #961 구르기 캔슬
if (c.moveHold != 0) s_slashActor = shooter; // #961 난사 중 공격 전진 표 금지(AttackRootMotion.Gate)
bool prevEvents = an.fireEvents; an.fireEvents = false; float t0 = Time.time; int i = -1, cur = 0; float interval = Mathf.Max(0.05f, c.slashInterval), tickAt = t0; bool prevEvents = an.fireEvents; an.fireEvents = false; float t0 = Time.time; int i = -1, cur = 0; float interval = Mathf.Max(0.05f, c.slashInterval), tickAt = t0;
int hits = 0, targets = 0, scheduled = 0; var hitQ = new System.Collections.Generic.Queue<float>(4); var hitTimes = new System.Text.StringBuilder(); // #960 휘두름마다 판정 시각을 쌓는다(프레임 끊김으로 틱 둘이 한 프레임에 와도 빠지지 않게) int hits = 0, targets = 0, scheduled = 0; var hitQ = new System.Collections.Generic.Queue<float>(4); var hitTimes = new System.Text.StringBuilder(); // #960 휘두름마다 판정 시각을 쌓는다(프레임 끊김으로 틱 둘이 한 프레임에 와도 빠지지 않게)
try try
@ -226,6 +243,7 @@ namespace WL.Combat.Weapon
while (Time.time - t0 < c.slashSeconds) while (Time.time - t0 < c.slashSeconds)
{ {
if (DSUtil.CheckNull(shooter) || shooter.IsDead()) break; // CurAnim 조건은 뺀다(실측: 3회 만에 끊겨 원본 콤보 전이 Attack3→4(회전)·Jump 사슬이 이어졌다) if (DSUtil.CheckNull(shooter) || shooter.IsDead()) break; // CurAnim 조건은 뺀다(실측: 3회 만에 끊겨 원본 콤보 전이 Attack3→4(회전)·Jump 사슬이 이어졌다)
if (c.dodgeCancel != 0 && shooter.Get_CurAnim() == eAnim.Dodge) { canceled = true; break; } // #961 구르기 = 캔슬(강제 재생보다 먼저 봐서 구르기 동작을 덮지 않는다)
if (i < 0 || Time.time >= tickAt) if (i < 0 || Time.time >= tickAt)
{ {
i++; tickAt = t0 + (i + 1) * interval; i++; tickAt = t0 + (i + 1) * interval;
@ -248,17 +266,56 @@ namespace WL.Combat.Weapon
} }
yield return null; yield return null;
} }
if (canceled) // #961 구르기 캔슬 — 반복 · 잔상 멈춤 + 살아 있는 이펙트 지움 · 이동 잠금 풀기 · 남은 판정 없음
{
s_cancelSeq = seq; DodgeCancels++;
for (int k = 0; k < s_liveFx.Count; k++) if (s_liveFx[k] != null) Object.Destroy(s_liveFx[k]);
s_liveFx.Clear(); hitQ.Clear();
if (shooter is PCActor cp) WLSkillCancel.EndHoldMove(cp, false);
hitTimes.Append("(구르기 캔슬 " + (Time.time - t0).ToString("F2") + ")");
}
// 끝 — 남은 판정 + (끊김으로 휘두름이 모자랐으면) 모자란 만큼 // 끝 — 남은 판정 + (끊김으로 휘두름이 모자랐으면) 모자란 만큼
if (c.slashHits > 0 && !DSUtil.CheckNull(shooter) && !shooter.IsDead()) if (!canceled && c.slashHits > 0 && !DSUtil.CheckNull(shooter) && !shooter.IsDead())
while (hits < c.slashHits && (hitQ.Count > 0 || scheduled < c.slashHits)) while (hits < c.slashHits && (hitQ.Count > 0 || scheduled < c.slashHits))
{ {
if (hitQ.Count > 0) hitQ.Dequeue(); else scheduled++; if (hitQ.Count > 0) hitQ.Dequeue(); else scheduled++;
hits++; SlashHits++; int n = SlashHit(pb, shooter, c); targets += n; SlashHitTargets += n; hitTimes.Append((Time.time - t0).ToString("F2")).Append("(끝) "); hits++; SlashHits++; int n = SlashHit(pb, shooter, c); targets += n; SlashHitTargets += n; hitTimes.Append((Time.time - t0).ToString("F2")).Append("(끝) ");
} }
} }
finally { if (!DSUtil.CheckNull(shooter) && shooter.m_animation != null) { shooter.m_animation.fireEvents = prevEvents; shooter.m_animation.speed = 1f; } } finally { if (ReferenceEquals(s_slashActor, shooter)) s_slashActor = null; if (!DSUtil.CheckNull(shooter) && shooter.m_animation != null) { shooter.m_animation.fireEvents = prevEvents; shooter.m_animation.speed = 1f; } }
SlashLog = "난사 " + (i + 1) + "회 · " + (Time.time - t0).ToString("F2") + " s · 되돌림 " + SlashLeaks; if (!canceled && c.endIdle != 0 && !DSUtil.CheckNull(shooter) && !shooter.IsDead()) { shooter.Play_Idle(); EndIdles++; } // #961 모션 끝 = 이펙트 끝
SlashLog = "난사 " + (i + 1) + "회 · " + (Time.time - t0).ToString("F2") + " s · 되돌림 " + SlashLeaks + (canceled ? " · 구르기 캔슬" : "");
if (c.slashHits > 0) SlashHitLog = "판정 " + hits + "회 · 맞힌 적 합 " + targets + " · 시각 " + hitTimes; if (c.slashHits > 0) SlashHitLog = "판정 " + hits + "회 · 맞힌 적 합 " + targets + " · 시각 " + hitTimes;
if (!canceled && c.endHoldSeconds > 0f && !DSUtil.CheckNull(shooter) && !shooter.IsDead()) Runner.StartCoroutine(CoEndHold(shooter, c.endHoldSeconds)); // #961
}
// #961 난사가 끝난 뒤 공격 금지 — 그동안 매 프레임 IsStop(자동 전투 공격 · 접근 멈춤 · 원본 2.1 s 콜백이 풀어도 다시 건다) · 하네스 공격 키는 IsHolding 을 본다
static int s_castSeq, s_cancelSeq = -1; static readonly System.Collections.Generic.List<GameObject> s_liveFx = new System.Collections.Generic.List<GameObject>();
public static int DodgeCancels, EndHolds, EndSkips, EndIdles, FxStops; static Actor s_holdActor, s_castShooter, s_slashActor; static float s_holdUntil;
/// <summary>#961 이 PC 가 지금 주력 난사 중(moveHold 1) — AttackRootMotion.Gate 가 공격 전진 표를 막는다.</summary>
public static bool IsSlashing(Actor a) { return a != null && ReferenceEquals(a, s_slashActor); }
/// <summary>#961 원본 Skill_ShadowSlash 의 2.1 s 끝 콜백 훅 — 이 시전이 구르기로 캔슬됐으면 true(끝 Play_Idle · 대상 뒤 순간이동 · 연기 · 카메라 돌리기를 건너뜀 · 이동 허용은 원본이 먼저 부름).</summary>
public static bool SkipEnd(Actor shooter)
{
if (Cfg.dodgeCancel == 0 || s_castSeq <= 0 || s_cancelSeq != s_castSeq || !ReferenceEquals(shooter, s_castShooter)) return false;
EndSkips++; return true;
}
static PropertyInfo s_isStop;
public static bool IsHolding(Actor a) { return a != null && ReferenceEquals(a, s_holdActor) && Time.time < s_holdUntil; }
static void SetStop(Actor a, bool v)
{
try { if (s_isStop == null) s_isStop = typeof(Actor).GetProperty("IsStop", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); if (s_isStop != null && s_isStop.CanWrite) s_isStop.SetValue(a, v, null); } catch (System.Exception) { }
}
static System.Collections.IEnumerator CoEndHold(Actor a, float sec)
{
s_holdActor = a; s_holdUntil = Time.time + sec; EndHolds++;
while (Time.time < s_holdUntil)
{
if (DSUtil.CheckNull(a) || a.IsDead() || !ReferenceEquals(s_holdActor, a)) yield break;
if (a.Get_CurAnim() == eAnim.Dodge) break; // 구르기 = 대기 끝
SetStop(a, true); yield return null;
}
if (ReferenceEquals(s_holdActor, a)) { s_holdActor = null; if (!DSUtil.CheckNull(a) && !a.IsDead()) SetStop(a, false); }
} }
public static int FxSlashes; public static string FxSlashLog = ""; public static int FxSlashes; public static string FxSlashLog = "";

View File

@ -522,7 +522,7 @@ namespace WL.SkillTest
float half = cfg.groundSize * 0.5f - 3f; var pp = pc.Get_position(); float half = cfg.groundSize * 0.5f - 3f; var pp = pc.Get_position();
if (Mathf.Abs(pp.x) > half || Mathf.Abs(pp.z) > half) { pc.Set_Warp(NavPos(V3(cfg.spawnPos))); _msg = "평지 가장자리 — PC·더미 원점 " + ResetDummy(); } if (Mathf.Abs(pp.x) > half || Mathf.Abs(pp.z) > half) { pc.Set_Warp(NavPos(V3(cfg.spawnPos))); _msg = "평지 가장자리 — PC·더미 원점 " + ResetDummy(); }
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(_kAttack)) { if (WL.Combat.Weapon.WLGhostStrike.IsLanding(pc)) _msg = "착지 중 — 공격 불가"; else if (WL.Combat.Weapon.WLShadowSlash.IsHolding(pc)) _msg = "주력 끝 대기 — 공격 불가(#961)"; 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(_kPrev)) _msg = CycleWeapon(-1);
if (Input.GetKeyDown(_kNext)) _msg = CycleWeapon(+1); if (Input.GetKeyDown(_kNext)) _msg = CycleWeapon(+1);
if (cfg.numberKeysWeapon != 0 && Input.anyKeyDown) // #958 숫자 키 = 그 번호 무기(1~9 · 0 = 10번 · 키패드 포함) if (cfg.numberKeysWeapon != 0 && Input.anyKeyDown) // #958 숫자 키 = 그 번호 무기(1~9 · 0 = 10번 · 키패드 포함)