diff --git a/Assets/Res_Addr/Animations/Animation/Assassin/Stander@DualBlades_Attack2.FBX.meta b/Assets/Res_Addr/Animations/Animation/Assassin/Stander@DualBlades_Attack2.FBX.meta index aec956777..61485e15b 100644 --- a/Assets/Res_Addr/Animations/Animation/Assassin/Stander@DualBlades_Attack2.FBX.meta +++ b/Assets/Res_Addr/Animations/Animation/Assassin/Stander@DualBlades_Attack2.FBX.meta @@ -1,7 +1,7 @@ fileFormatVersion: 2 guid: ee7d9d41f5a48514db8ac2144d80154d ModelImporter: - serializedVersion: 22200 + serializedVersion: 24300 internalIDToNameTable: - first: 1: 100000 @@ -339,6 +339,7 @@ ModelImporter: materialName: 0 materialSearch: 1 materialLocation: 1 + searchTexturesGlobally: 1 animations: legacyGenerateAnimations: 4 bakeSimulation: 0 @@ -346,8 +347,6 @@ ModelImporter: optimizeGameObjects: 0 removeConstantScaleCurves: 0 motionNodeName: - rigImportErrors: - rigImportWarnings: animationImportErrors: animationImportWarnings: animationRetargetingWarnings: @@ -419,6 +418,63 @@ ModelImporter: maskType: 3 maskSource: {instanceID: 0} additiveReferencePoseFrame: 0 + - serializedVersion: 16 + name: Attack2_S_10202 + takeName: Take 001 + internalID: 1827226128182048838 + firstFrame: 16 + lastFrame: 36 + wrapMode: 0 + orientationOffsetY: 0 + level: 0 + cycleOffset: 0 + loop: 0 + hasAdditiveReferencePose: 0 + loopTime: 1 + loopBlend: 0 + loopBlendOrientation: 1 + loopBlendPositionY: 1 + loopBlendPositionXZ: 0 + keepOriginalOrientation: 1 + keepOriginalPositionY: 1 + keepOriginalPositionXZ: 1 + heightFromFeet: 0 + mirror: 0 + bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000 + curves: [] + events: + - time: 0 + functionName: LookAtTarget + data: + objectReferenceParameter: {instanceID: 0} + floatParameter: 0 + intParameter: 0 + messageOptions: 0 + - time: 0.25 + functionName: ShowEffect + data: Effect_Slash_10202_2 + objectReferenceParameter: {instanceID: 0} + floatParameter: 0 + intParameter: 1 + messageOptions: 0 + - time: 0.32 + functionName: Move + data: + objectReferenceParameter: {instanceID: 0} + floatParameter: 0.1 + intParameter: 0 + messageOptions: 0 + - time: 0.42 + functionName: Projectile + data: Projectile_10202 + objectReferenceParameter: {instanceID: 0} + floatParameter: 0 + intParameter: 0 + messageOptions: 0 + transformMask: [] + maskType: 3 + maskSource: {instanceID: 0} + additiveReferencePoseFrame: 0 isReadable: 0 meshes: lODScreenPercentages: [] @@ -445,6 +501,9 @@ ModelImporter: maxBonesPerVertex: 4 minBoneWeight: 0.001 optimizeBones: 1 + generateMeshLods: 0 + meshLodGenerationFlags: 0 + maximumMeshLod: -1 meshOptimizationFlags: -1 indexFormat: 0 secondaryUVAngleDistortion: 8 diff --git a/Assets/Res_Addr/Animations/Animation/Assassin/Stander@DualBlades_Attack3.FBX.meta b/Assets/Res_Addr/Animations/Animation/Assassin/Stander@DualBlades_Attack3.FBX.meta index 01f761eee..d315a4601 100644 --- a/Assets/Res_Addr/Animations/Animation/Assassin/Stander@DualBlades_Attack3.FBX.meta +++ b/Assets/Res_Addr/Animations/Animation/Assassin/Stander@DualBlades_Attack3.FBX.meta @@ -475,6 +475,63 @@ ModelImporter: maskType: 3 maskSource: {instanceID: 0} additiveReferencePoseFrame: 0 + - serializedVersion: 16 + name: Attack4_S_10202 + takeName: Take 001 + internalID: -998496362607748373 + firstFrame: 0 + lastFrame: 35 + wrapMode: 0 + orientationOffsetY: 0 + level: 0 + cycleOffset: 0 + loop: 0 + hasAdditiveReferencePose: 0 + loopTime: 1 + loopBlend: 0 + loopBlendOrientation: 1 + loopBlendPositionY: 1 + loopBlendPositionXZ: 0 + keepOriginalOrientation: 1 + keepOriginalPositionY: 1 + keepOriginalPositionXZ: 1 + heightFromFeet: 0 + mirror: 0 + bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000 + curves: [] + events: + - time: 0 + functionName: LookAtTarget + data: + objectReferenceParameter: {instanceID: 0} + floatParameter: 0 + intParameter: 0 + messageOptions: 0 + - time: 0.1 + functionName: ShowEffect + data: Effect_Slash_10202_4 + objectReferenceParameter: {instanceID: 0} + floatParameter: 0 + intParameter: 1 + messageOptions: 0 + - time: 0.14 + functionName: Move + data: + objectReferenceParameter: {instanceID: 0} + floatParameter: 0.2 + intParameter: 0 + messageOptions: 0 + - time: 0.25 + functionName: Projectile + data: Projectile_10202 + objectReferenceParameter: {instanceID: 0} + floatParameter: 0 + intParameter: 0 + messageOptions: 0 + transformMask: [] + maskType: 3 + maskSource: {instanceID: 0} + additiveReferencePoseFrame: 0 isReadable: 0 meshes: lODScreenPercentages: [] diff --git a/Assets/Res_Addr/Animations/pcanim_assassin.controller b/Assets/Res_Addr/Animations/pcanim_assassin.controller index cb246fe8a..4b4930f59 100644 --- a/Assets/Res_Addr/Animations/pcanim_assassin.controller +++ b/Assets/Res_Addr/Animations/pcanim_assassin.controller @@ -503,7 +503,7 @@ AnimatorStateTransition: serializedVersion: 3 m_TransitionDuration: 0.15 m_TransitionOffset: 0 - m_ExitTime: 0.5 + m_ExitTime: 0.8 m_HasExitTime: 1 m_HasFixedDuration: 1 m_InterruptionSource: 0 @@ -517,7 +517,7 @@ AnimatorState: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_Name: DualBlades_DodgeAttack - m_Speed: 1.5 + m_Speed: 1.2 m_CycleOffset: 0 m_Transitions: - {fileID: -524724009125662056} @@ -530,7 +530,7 @@ AnimatorState: m_MirrorParameterActive: 0 m_CycleOffsetParameterActive: 0 m_TimeParameterActive: 0 - m_Motion: {fileID: 1827226128182048838, guid: 248e0d6885e2b4f44b0c03aeaa65dd8a, + m_Motion: {fileID: -998496362607748373, guid: afbbfbe54e2f6d242983469698b3aac3, type: 3} m_Tag: m_SpeedParameter: @@ -567,7 +567,7 @@ AnimatorState: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_Name: DualBlades_Attack2 - m_Speed: 1 + m_Speed: 1.1 m_CycleOffset: 0 m_Transitions: - {fileID: -1743382946548511829} @@ -580,7 +580,7 @@ AnimatorState: m_MirrorParameterActive: 0 m_CycleOffsetParameterActive: 0 m_TimeParameterActive: 0 - m_Motion: {fileID: 1827226128182048838, guid: 08478a94dc135e34cb5ff79c98bb6f75, + m_Motion: {fileID: 1827226128182048838, guid: ee7d9d41f5a48514db8ac2144d80154d, type: 3} m_Tag: m_SpeedParameter: diff --git a/Assets/WL/Combat/CombatAnimMapping.asset b/Assets/WL/Combat/CombatAnimMapping.asset index 12b715b30..f39ddcea8 100644 --- a/Assets/WL/Combat/CombatAnimMapping.asset +++ b/Assets/WL/Combat/CombatAnimMapping.asset @@ -532,48 +532,48 @@ MonoBehaviour: intParameter: 0 floatParameter: 0 - stateName: DualBlades_Attack2 - sourceFbxPath: Assets/ExplosiveLLC/Ninja Warrior Mecanim Animation Pack/Animations/Ninja@Attack2_S.FBX + sourceFbxPath: Assets/Res_Addr/Animations/Animation/Assassin/Stander@DualBlades_Attack2.FBX subClipName: Attack2_S_{CID} - sourceClipName: + sourceClipName: incomingExitTime: 0.587 createIfMissing: 0 - firstFrame: -1 - lastFrame: -1 - speed: 0 + firstFrame: 16 + lastFrame: 36 + speed: 1.1 events: - functionName: LookAtTarget time: 0 useNormalized: 1 normalizedTime: 0 - stringParameter: + stringParameter: intParameter: 0 floatParameter: 0 - functionName: ShowEffect time: 0 useNormalized: 1 - normalizedTime: 0.287 + normalizedTime: 0.25 stringParameter: Effect_Slash_{CID}_2 intParameter: 1 floatParameter: 0 - functionName: Move time: 0 useNormalized: 1 - normalizedTime: 0.347 - stringParameter: + normalizedTime: 0.32 + stringParameter: intParameter: 0 floatParameter: 0.1 - functionName: Projectile time: 0 useNormalized: 1 - normalizedTime: 0.407 + normalizedTime: 0.42 stringParameter: Projectile_{CID} intParameter: 0 floatParameter: 0 - stateName: DualBlades_Attack3 sourceFbxPath: Assets/Res_Addr/Animations/Animation/Paladin/skill_spinslash.FBX subClipName: Attack3_S_{CID} - sourceClipName: - incomingExitTime: 0.5 + sourceClipName: + incomingExitTime: 0.8 createIfMissing: 0 firstFrame: -1 lastFrame: -1 @@ -608,40 +608,40 @@ MonoBehaviour: intParameter: 0 floatParameter: 0 - stateName: DualBlades_DodgeAttack - sourceFbxPath: Assets/Res_Addr/Animations/Animation/Assassin/Stander@DualBlades_DodgeAttack_Attack4.FBX + sourceFbxPath: Assets/Res_Addr/Animations/Animation/Assassin/Stander@DualBlades_Attack3.FBX subClipName: Attack4_S_{CID} - sourceClipName: + sourceClipName: incomingExitTime: 0.55 createIfMissing: 0 firstFrame: -1 - lastFrame: -1 - speed: 1.5 + lastFrame: 26 + speed: 1.2 events: - functionName: LookAtTarget time: 0 useNormalized: 1 normalizedTime: 0 - stringParameter: + stringParameter: intParameter: 0 floatParameter: 0 - functionName: ShowEffect time: 0 useNormalized: 1 - normalizedTime: 0.155 + normalizedTime: 0.1 stringParameter: Effect_Slash_{CID}_4 intParameter: 1 floatParameter: 0 - functionName: Move time: 0 useNormalized: 1 - normalizedTime: 0.126 - stringParameter: + normalizedTime: 0.14 + stringParameter: intParameter: 0 - floatParameter: 0.3 + floatParameter: 0.2 - functionName: Projectile time: 0 useNormalized: 1 - normalizedTime: 0.186 + normalizedTime: 0.25 stringParameter: Projectile_{CID} intParameter: 0 floatParameter: 0 diff --git a/Assets/WL/Combat/Weapon/Resources/WL/WLGhostStrike.json b/Assets/WL/Combat/Weapon/Resources/WL/WLGhostStrike.json index d019d9212..dfbb06f09 100644 --- a/Assets/WL/Combat/Weapon/Resources/WL/WLGhostStrike.json +++ b/Assets/WL/Combat/Weapon/Resources/WL/WLGhostStrike.json @@ -38,22 +38,25 @@ "hitMinInterval": 0.08, "castSeconds": 2.0, "jumpGhosts": 4, - "jumpHeight": 1.0, - "jumpForward": 0.4, + "jumpHeight": 0.15, + "jumpForward": 0.1, + "jumpSide": 1.0, "jumpSeconds": 0.25, "landState": "ghost_land", - "landHeight": 0.2, - "landFallSeconds": 0.06, - "slideDistance": 0.45, - "slideSeconds": 0.3, - "landTotalSeconds": 0.45, + "landHeight": 0.0, + "landFallSeconds": 0.2, + "entrySide": 1.2, + "slideDistance": 0.1, + "slideSeconds": 0.15, + "landTotalSeconds": 0.5, + "_note_959": "#959 PD 「각성 시전 후 공중에 떨어지지 말고 옆에서 튀어나와 정지하는 것처럼」 — 시전 잔상은 위(1.0) 대신 오른쪽 1.0 m 로 빠지고(jumpSide · 위 0.15 · 앞 0.1) · 다시 보이는 순간 최종 자리의 오른쪽 1.2 m(entrySide)에서 0.2 s 동안 감속하며 미끄러져 들어와(그동안 반투명 착지 자세 캐릭터 · 수직 이동 0 = landHeight 0) 정지(앞 미끄러짐 0.45 → 0.1 m) → 모델 + 잔상 3 + 먼지", "landAtSeconds": 3.4, "airClip": "Skill8_Air", "landGhosts": 0, "dustPrefab": "WL/Fx/WL_Fx_LandDust", "dustScale": 0.15, "keepCamera": 1, - "fallForward": 0.08, + "fallForward": 0.0, "landPose": "ghost_land@0.25", "landHideModel": 1, "landGhostColor": [ 0.55, 0.95, 0.85, 0.45 ], diff --git a/Assets/WL/Combat/Weapon/Resources/WL/WLShadowSlash.json b/Assets/WL/Combat/Weapon/Resources/WL/WLShadowSlash.json index 2112cf4dd..cf582b256 100644 --- a/Assets/WL/Combat/Weapon/Resources/WL/WLShadowSlash.json +++ b/Assets/WL/Combat/Weapon/Resources/WL/WLShadowSlash.json @@ -13,12 +13,12 @@ "fxLife": 2.5, "fxAimTarget": 1, "fxRangeFrac": 0.5, - "fxRangeWidth": 1.6, + "fxRangeWidth": 3.2, "fxPrefabWidth": 5.26, "hideOriginalFx": 1, "slashStates": [ "attack", "DualBlades_Attack2" ], - "slashInterval": 0.28, - "slashSpeed": 1.8, + "slashInterval": 0.24, + "slashSpeed": 1.6, "slashSeconds": 2.0, "fxMain": 1, "fxPerSlash": 0, @@ -27,6 +27,15 @@ "fxSlashFraction": 0.6, "fxSlashYOffset": 0.0, "fxSlashFollow": 1, + "fxRepeatInterval": 0.24, + "fxRepeatSeconds": 0, + "slashHardRestart": 1, + "slashGhosts": 1, + "slashGhostInterval": 0.07, + "slashGhostHold": 0.03, + "slashGhostFade": 0.14, + "slashGhostColor": [ 0.55, 0.95, 0.85, 0.4 ], + "_note_959": "#959 PD 「이펙트 크기 2배 더 · 플레이어 동작에 맞게 재생시간을 늘려 모션만 보이지 않게(캐릭터 전진값 고려해 위치도) · 공격 모션에 잔상(빠르게 공격하는 느낌)」 — 폭 = 공격 범위 × 1.6 → 3.2(배율 0.304 → 0.608) · 정면 사본을 0.5 s 부터 0.28 s 마다 난사가 끝나는 2.0 s 까지 되풀이(매번 그때 PC 위치·대상 방향 = 전진 반영 · 초승달 수명 0.3 s 라 1회로는 0.6 s 만 보였음) · 난사 상태는 Animator.Play 로 강제(Play 실측: 배속 1.8 에선 2타→3타 exit 전이가 틱보다 먼저 와 3타 회전·4타 돌진 0.4 m 가 섞였음 = PD 가 본 「전진」) · 난사 2.0 s 동안 0.07 s 마다 지금 자세 잔상(teal · 유지 0.03 · 소멸 0.14) · 틱 0.28 → 0.24 s · 배속 1.8 → 1.6(2타가 #959 원본 DualBlades_Attack2 16~36 f 0.61 s · exit 0.8 이 되어 1.8 배속이면 exit 0.27 s 가 틱보다 먼저 와 3타 회전이 샘 → 1.6 배속 exit 0.31 s > 0.24 s)", "_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 그대로", diff --git a/Assets/WL/Combat/Weapon/WLGhostStrike.cs b/Assets/WL/Combat/Weapon/WLGhostStrike.cs index deafb1810..0fb538bb2 100644 --- a/Assets/WL/Combat/Weapon/WLGhostStrike.cs +++ b/Assets/WL/Combat/Weapon/WLGhostStrike.cs @@ -50,6 +50,9 @@ namespace WL.Combat.Weapon public int landGhosts = 2; // 떨어지는 동안 잔상 수 public string dustPrefab = ""; public float dustScale = 1f; public int keepCamera = 0; // #953 1 = 원본 Skill_DarkAmbush 의 탑뷰 전환(RealCamera.Set_TopView)을 건너뛴다 — 카메라 각도 유지 + // #959 PD 「각성 시전 후 공중에서 떨어지지 말고 옆에서 튀어나와 정지하는 것처럼」 — entrySide ≠ 0: 다시 보이는 순간 최종 자리에서 PC 오른쪽(+ · 왼쪽 −)으로 entrySide m 떨어진 곳에 워프한 뒤 + // landFallSeconds 동안 감속하며 최종 자리로 미끄러져 들어온다(그동안 landPose 반투명 캐릭터 · landHeight 0 이면 수직 이동 없음) · jumpSide: 시전 순간 잔상이 옆으로도 빠진다(m · 같은 부호 = 같은 쪽) + public float entrySide = 0f, jumpSide = 0f; public float fallForward = 0f; // #953 낙하하는 동안 앞으로 가는 거리(m · 대각선으로 미끄러지며 낙하) · 착지 뒤 미끄러짐(slideDistance)은 별도 // #954 PD 「0.5 m 정도 짧은 높이에서 반투명 상태로 갑자기 이펙트와 함께 튀어나오며 랜딩 · 랜딩 시간 짧게 · 랜딩 중 공격 불가」 public string landPose = ""; // 착지 자세("상태@정규화 시각") — 떨어지는 동안 반투명 캐릭터(잔상 재질)로 그리고 실제 모델은 숨긴다(landHideModel) @@ -102,7 +105,7 @@ namespace WL.Combat.Weapon public Vector3 lastPos; public bool hasLast, posesReady; public string sig = ""; // #951 도약 잔상 · 착지 public int jumpDone; public float jumpNext = -1f; public bool wasHidden, sawAir, landing, landed, dustDone, hideDuringFall; public float landT0, modelBaseY, modelY = float.NaN; public int landGhostsDone, hideUntilFrame; - public Vector3 landPos, landFwd; public Transform model; public UnityEngine.AI.NavMeshAgent agent; public object stopWas; + public Vector3 landPos, landFwd, landRight; public float entryMoved; public Transform model; public UnityEngine.AI.NavMeshAgent agent; public object stopWas; // #959 landRight · entryMoved = 옆에서 들어오기 public readonly List smrs = new List(); public readonly List weapons = new List(); public readonly List smrPaths = new List(); @@ -195,15 +198,18 @@ namespace WL.Combat.Weapon s_cast = cast; s_hitQ.Clear(); s_bearing.Clear(); s_lastHitAt.Clear(); Casts++; - if (s_runner == null) - { - var go = new GameObject("[WL943] GhostStrike"); go.hideFlags = HideFlags.HideInHierarchy; Object.DontDestroyOnLoad(go); - s_runner = go.AddComponent(); - } + EnsureRunner(); LastLog = "시전 " + r.skill + " · fx " + (cast.fx != null ? cast.fx.Count : 0) + " · 부품 " + cast.smrs.Count + "+" + cast.weapons.Count; } catch (System.Exception ex) { Errors++; LastLog = "OnSet 실패 — " + ex.Message; } } + /// 풀 정리(Tick · 유지·소멸 뒤 반환)를 돌리는 러너 — #959 실측: 각성(OnSet) 전에 4타 순간이동 잔상·난사 잔상만 쓰면 러너가 없어 잔상 6개가 영영 busy(원장 47 「첫 6개 뒤 0개」의 원인) → 잔상을 띄우는 모든 입구에서 보장. + static void EnsureRunner() + { + if (s_runner != null) return; + var go = new GameObject("[WL943] GhostStrike"); go.hideFlags = HideFlags.HideInHierarchy; Object.DontDestroyOnLoad(go); + s_runner = go.AddComponent(); + } /// 타격 확정(물리 콜백 안) — 이번 시전 스킬이 시전자로 맞힌 것만 요청으로 쌓는다(생성은 러너 Update). static void OnHit(in HitConfirmedEvent e) @@ -237,7 +243,8 @@ namespace WL.Combat.Weapon if (rl.jumpGhosts > 0 && cast.jumpDone < rl.jumpGhosts && now >= cast.jumpNext && cast.jumpNext >= 0f) { float k = (cast.jumpDone + 1f) / rl.jumpGhosts; var sp = cast.shooter.Get_position(); var fw = cast.shooter.transform.forward; fw.y = 0f; if (fw.sqrMagnitude < 1e-4f) fw = Vector3.forward; fw.Normalize(); - if (Spawn(c, cast, sp + Vector3.up * (rl.jumpHeight * k) + fw * (rl.jumpForward * k), fw)) JumpSpawned++; + var rt = Vector3.Cross(Vector3.up, fw); // #959 PC 오른쪽 + if (Spawn(c, cast, sp + Vector3.up * (rl.jumpHeight * k) + fw * (rl.jumpForward * k) + rt * (rl.jumpSide * k), fw)) JumpSpawned++; cast.jumpDone++; cast.jumpNext = now + rl.jumpSeconds / rl.jumpGhosts; } // #951 ② 착지 연출 — 시작 판정은 LateTick(#952-② 다시 보이는 그 프레임에 즉시 · 서 있는 자세가 한 프레임도 안 보이게) · 진행은 여기 @@ -277,6 +284,7 @@ namespace WL.Combat.Weapon try { var c = Cfg; if (c.enabled == 0 || pc == null || n <= 0) return 0; + EnsureRunner(); // #959 풀 정리 보장(원장 47) var cast = new Cast { shooter = pc, rule = new WLGhostStrikeRule(), t0 = Time.time, groundY = pc.Get_position().y, poseCursor = 0 }; Capture(cast, c); var pose = BakePose(pc.transform, cast.smrs, cast.weapons); if (pose == null) return 0; @@ -290,6 +298,27 @@ namespace WL.Combat.Weapon catch (System.Exception ex) { Errors++; LastLog = "잔상 실패 — " + ex.Message; return 0; } } public static int BlinkGhostCount; + /// #959 지금 자세 잔상 1개(살아 있는 메시를 굽는다) — 비수 주력 난사 중 「빠르게 공격하는」 잔상 · 색(RGBA · null = 기본) · 유지 · 소멸(−1 = 기본). + public static int LiveGhosts; + public static bool LiveGhost(Actor pc, float[] color, float hold, float fade) + { + try + { + var c = Cfg; if (c.enabled == 0 || pc == null) return false; + EnsureRunner(); // #959 풀 정리 보장 + var cast = new Cast { shooter = pc, rule = new WLGhostStrikeRule(), t0 = Time.time, groundY = pc.Get_position().y }; + Capture(cast, c); + var pose = BakePose(pc.transform, cast.smrs, cast.weapons); if (pose == null) return false; + var face = pc.transform.forward; face.y = 0f; if (face.sqrMagnitude < 1e-4f) face = Vector3.forward; + bool ok = Spawn(c, cast, pc.Get_position(), face, pose, color, hold, fade); + float life = (hold >= 0f ? hold : c.hold) + (fade >= 0f ? fade : c.fade) + 0.5f; + if (pose.body != null) Object.Destroy(pose.body, life); + foreach (var m in pose.extraMesh) if (m != null) Object.Destroy(m, life); + if (ok) LiveGhosts++; + return ok; + } + catch (System.Exception ex) { Errors++; LastLog = "잔상 실패 — " + ex.Message; return false; } + } /// #954 「랜딩 중 공격할 수 없어야」 — 이 시전자가 착지 연출 중인가(자동 전투는 IsStop 으로 이미 멈춤 · 수동 공격 키는 이걸로 막는다). public static bool IsLanding(Actor a) { var cast = s_cast; return cast != null && cast.landing && a != null && ReferenceEquals(cast.shooter, a); } static System.Reflection.PropertyInfo s_isStop; @@ -371,6 +400,12 @@ namespace WL.Combat.Weapon cast.model = cast.shooter.m_animation != null ? cast.shooter.m_animation.transform : null; cast.modelBaseY = cast.model != null ? cast.model.localPosition.y : 0f; cast.agent = cast.shooter.GetComponent(); if (pc != null) { try { if (s_isStop == null) s_isStop = typeof(Actor).GetProperty("IsStop", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public); cast.stopWas = s_isStop != null ? s_isStop.GetValue(pc, null) : null; } catch (System.Exception) { } SetStop(pc, true); pc.WL_StopNavMove(); } + cast.landRight = Vector3.Cross(Vector3.up, cast.landFwd).normalized; cast.entryMoved = 0f; + if (r.entrySide != 0f) // #959 옆에서 들어오기 — 최종 자리(landPos)에서 옆으로 entrySide 만큼 떨어진 곳으로 워프 → UpdateLanding 이 감속하며 되돌아온다 + { + var from = cast.landPos - cast.landRight * r.entrySide; + try { if (pc != null) pc.Set_Warp(from); else cast.shooter.transform.position = from; } catch (System.Exception) { cast.shooter.transform.position = from; } + } try { if (cast.shooter.m_animation != null) { cast.shooter.m_animation.speed = 1f; cast.shooter.m_animation.CrossFade(r.landState, 0f, 0, 0f); } } catch (System.Exception ex) { LandLog = "착지 클립 실패 — " + ex.Message; } // #952-② 섞임 0 = 서 있는 자세에서 번지지 않게 if (cast.model != null) cast.model.localPosition = new Vector3(cast.model.localPosition.x, cast.modelBaseY + r.landHeight, cast.model.localPosition.z); Landings++; LandLog = "착지 시작 t+" + (now - cast.t0).ToString("F2") + " @" + cast.landPos.ToString("F2") + (cast.agent != null ? " agent" : " agent 없음"); @@ -391,12 +426,25 @@ namespace WL.Combat.Weapon float step = r.fallForward * Time.deltaTime / fall; if (cast.agent != null && cast.agent.enabled && cast.agent.isOnNavMesh) cast.agent.Move(cast.landFwd * step); else cast.shooter.transform.position += cast.landFwd * step; } + if (r.entrySide != 0f) // #959 옆에서 튀어나오기 — 속도 2D/T → 0 감속(총 이동 = |entrySide|) · 방향 = 워프한 쪽의 반대 + { + float total = Mathf.Abs(r.entrySide), kk = Mathf.Clamp01(t / fall); + float step = Mathf.Min(total * (2f / fall) * (1f - kk) * Time.deltaTime, Mathf.Max(0f, total - cast.entryMoved)); + var dir = cast.landRight * Mathf.Sign(r.entrySide); cast.entryMoved += step; + if (cast.agent != null && cast.agent.enabled && cast.agent.isOnNavMesh) cast.agent.Move(dir * step); else cast.shooter.transform.position += dir * step; + } return; } cast.modelY = cast.modelBaseY; if (!cast.dustDone) { cast.dustDone = true; + if (r.entrySide != 0f) // #959 들어오기 끝 = 최종 자리에 정확히 + { + var d = cast.landPos - cast.shooter.Get_position(); d.y = 0f; + if (cast.agent != null && cast.agent.enabled && cast.agent.isOnNavMesh) cast.agent.Move(d); else cast.shooter.transform.position += d; + LandLog += " · 옆 진입 " + cast.entryMoved.ToString("F2") + " m"; + } 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 (!string.IsNullOrEmpty(r.dustPrefab)) diff --git a/Assets/WL/Combat/Weapon/WLShadowSlash.cs b/Assets/WL/Combat/Weapon/WLShadowSlash.cs index 26cc4333f..90b0adf6e 100644 --- a/Assets/WL/Combat/Weapon/WLShadowSlash.cs +++ b/Assets/WL/Combat/Weapon/WLShadowSlash.cs @@ -47,6 +47,13 @@ namespace WL.Combat.Weapon // 띄우고 fxSlashLife 동안 날 방향(수평 성분)·위치를 매 프레임 따라가게(fxSlashFollow 1 · 0 = 처음 자리 고정) public int fxMain = 1, fxPerSlash = 0, fxSlashFollow = 1; public float fxSlashScale = 0.35f, fxSlashLife = 0.6f, fxSlashFraction = 0.6f, fxSlashYOffset = 0f; + // #959 PD 「이펙트 크기 2배 · 플레이어 동작에 맞게 재생시간을 늘려 모션만 보이지 않게(캐릭터 전진값 고려해 위치도) · 공격 모션에 잔상(빠르게 공격하는 느낌)」 — + // fxRepeatInterval > 0 = 정면 사본을 fxDelay 뒤 이 간격으로 fxRepeatSeconds(0 = slashSeconds) 까지 되풀이(매번 그때의 PC 위치·대상 방향으로 다시 계산 = 전진 반영) · + // slashHardRestart 1 = 난사 상태를 CrossFade 대신 Animator.Play 로 강제(Play 실측: 배속 1.8 에선 컨트롤러 exit 전이(2타→3타 0.50)가 0.28 s 틱보다 먼저 와 3타 회전·4타 돌진(0.4 m 전진)이 섞였다) · + // slashGhosts 1 = 난사 동안 slashGhostInterval 마다 지금 자세 잔상(WLGhostStrike.LiveGhost · 색 RGBA · 유지 · 소멸 초) + public float fxRepeatInterval = 0f, fxRepeatSeconds = 0f; + public int slashHardRestart = 0, slashGhosts = 0; + public float slashGhostInterval = 0.07f, slashGhostHold = 0.03f, slashGhostFade = 0.14f; public float[] slashGhostColor = new float[0]; } public static class WLShadowSlash @@ -72,9 +79,9 @@ namespace WL.Combat.Weapon } [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] - static void Boot() { s_cfg = null; Casts = Shown = Scaled = Errors = 0; LastLog = ""; } + static void Boot() { s_cfg = null; Casts = Shown = Scaled = Errors = 0; SlashLoops = SlashLeaks = FxShown = FxRepeats = FxSlashes = SlashGhostCount = 0; LastLog = SlashLog = FxRepeatLog = ""; } - public static string Describe() { return "casts " + Casts + " · shown " + Shown + " · scaled " + Scaled + " · err " + Errors + " · " + LastLog; } + public static string Describe() { return "casts " + Casts + " · shown " + Shown + " · scaled " + Scaled + " · err " + Errors + " · fx " + FxShown + "(반복 " + FxRepeats + ") · 잔상 " + SlashGhostCount + " · " + LastLog; } /// 원본 Skill_ShadowSlash.Set 훅(Set_Invisible 바로 뒤 1줄) — 캐릭터 보이기 · 베기 · 연기 크기 배율. public static void OnSet(Skill_ShadowSlash skill) @@ -101,6 +108,7 @@ namespace WL.Combat.Weapon if (c.fxMain != 0) Runner.StartCoroutine(CoFx(shooter, c)); // #956 fxMain 0 = 정면 사본 없이 난사 사본(fxPerSlash)만 } if (c.slashStates != null && c.slashStates.Length > 0 && !DSUtil.CheckNull(shooter) && shooter.m_animation != null) Runner.StartCoroutine(CoSlash(shooter, c)); // #955 + if (c.slashGhosts != 0 && !DSUtil.CheckNull(shooter)) Runner.StartCoroutine(CoGhosts(shooter, c)); // #959 난사 잔상 LastLog = "cast " + Casts + " · shown " + Shown + " · scaled " + Scaled; } catch (System.Exception ex) { Errors++; LastLog = "실패 — " + ex.Message; } @@ -109,12 +117,25 @@ namespace WL.Combat.Weapon public static int FxShown; static WLShadowSlashRunner 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(); } return s_runner; } } - /// #954 원본 베기가 켜지는 시각(fxDelay)에 WL 사본(녹색 난사)을 PC 정면 fxForward 에 띄운다(PC 방향 · fxLife 뒤 파괴). + public static int FxRepeats; public static string FxRepeatLog = ""; + /// #954 원본 베기가 켜지는 시각(fxDelay)에 WL 사본(녹색 난사)을 PC 정면 fxForward 에 띄운다(PC 방향 · fxLife 뒤 파괴). #959 fxRepeatInterval > 0 이면 fxRepeatSeconds(0 = slashSeconds)까지 되풀이 — 매번 그때 PC 위치·대상 방향. static System.Collections.IEnumerator CoFx(Actor shooter, WLShadowSlashJson c) { if (c.fxDelay > 0f) yield return new WaitForSeconds(c.fxDelay); - if (DSUtil.CheckNull(shooter) || shooter.IsDead()) yield break; - var p = Resources.Load(c.fxPrefab); if (p == null) { LastLog = "fx 프리팹 없음 " + c.fxPrefab; yield break; } + float t0 = Time.time - c.fxDelay, until = c.fxRepeatSeconds > 0f ? c.fxRepeatSeconds : c.slashSeconds; int n = 0; + while (true) + { + if (DSUtil.CheckNull(shooter) || shooter.IsDead()) yield break; + if (!SpawnMainFx(shooter, c)) yield break; + n++; if (n > 1) FxRepeats++; + if (c.fxRepeatInterval <= 0f) yield break; + yield return new WaitForSeconds(c.fxRepeatInterval); + if (Time.time - t0 + 0.01f >= until) { FxRepeatLog = "정면 사본 " + n + "개 · " + (Time.time - t0).ToString("F2") + " s"; yield break; } + } + } + static bool SpawnMainFx(Actor shooter, WLShadowSlashJson c) + { + var p = Resources.Load(c.fxPrefab); if (p == null) { LastLog = "fx 프리팹 없음 " + c.fxPrefab; return false; } var fwd = shooter.transform.forward; fwd.y = 0f; if (fwd.sqrMagnitude < 1e-4f) fwd = Vector3.forward; fwd.Normalize(); var tgt = shooter.Get_Target(); float toTarget = -1f; if (c.fxAimTarget != 0 && !DSUtil.CheckNull(tgt) && !tgt.IsDead()) // #958 대상 쪽 @@ -132,29 +153,52 @@ namespace WL.Combat.Weapon 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; + return true; } - public static int SlashLoops; public static string SlashLog = ""; + public static int SlashGhostCount; + /// #959 난사 동안 지금 자세 잔상 — slashGhostInterval 마다 WLGhostStrike.LiveGhost(색 · 유지 · 소멸). + static System.Collections.IEnumerator CoGhosts(Actor shooter, WLShadowSlashJson c) + { + float t0 = Time.time; var col = c.slashGhostColor != null && c.slashGhostColor.Length >= 4 ? c.slashGhostColor : null; + while (Time.time - t0 < c.slashSeconds) + { + if (DSUtil.CheckNull(shooter) || shooter.IsDead()) yield break; + if (WLGhostStrike.LiveGhost(shooter, col, c.slashGhostHold, c.slashGhostFade)) SlashGhostCount++; + yield return new WaitForSeconds(Mathf.Max(0.02f, c.slashGhostInterval)); + } + } + + public static int SlashLoops, SlashLeaks; public static string SlashLog = ""; /// #955 제자리 난사 — slashSeconds 동안 slashStates 를 번갈아 CrossFade(이벤트 끔 · 배속) · 스킬 상태를 벗어나면 중단 · 끝에 이벤트·배속 원상. static System.Collections.IEnumerator CoSlash(Actor shooter, WLShadowSlashJson c) { var an = shooter.m_animation; if (an == null) yield break; - bool prevEvents = an.fireEvents; an.fireEvents = false; float t0 = Time.time; int i = 0; + 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; try { while (Time.time - t0 < c.slashSeconds) { - if (DSUtil.CheckNull(shooter) || shooter.IsDead()) break; // CurAnim 조건은 뺀다(실측: 3회 만에 끊겨 원본 콤보 전이 Attack3→4(회전)·Jump 사슬이 이어졌다) · 0.28 s 마다 다시 시작하므로 상태 exit 전이에 안 걸린다 - string st = c.slashStates[i % c.slashStates.Length]; int h = Animator.StringToHash(st); - if (an.HasState(0, h)) { an.CrossFadeInFixedTime(h, 0.05f, 0, 0f); an.speed = Mathf.Max(0.1f, c.slashSpeed); SlashLoops++; } - else SlashLog = "상태 없음 " + st; - if (c.fxPerSlash != 0 && !string.IsNullOrEmpty(c.fxPrefab)) SlashFx(shooter, c, i); // #956 휘두르는 손의 날 자리에 작은 사본 - i++; - yield return new WaitForSeconds(Mathf.Max(0.05f, c.slashInterval)); + if (DSUtil.CheckNull(shooter) || shooter.IsDead()) break; // CurAnim 조건은 뺀다(실측: 3회 만에 끊겨 원본 콤보 전이 Attack3→4(회전)·Jump 사슬이 이어졌다) + if (i < 0 || Time.time >= tickAt) + { + i++; tickAt = t0 + (i + 1) * interval; + string st = c.slashStates[i % c.slashStates.Length]; cur = Animator.StringToHash(st); + if (an.HasState(0, cur)) { if (c.slashHardRestart != 0) an.Play(cur, 0, 0f); else an.CrossFadeInFixedTime(cur, 0.05f, 0, 0f); an.speed = Mathf.Max(0.1f, c.slashSpeed); SlashLoops++; } + else SlashLog = "상태 없음 " + st; + if (c.fxPerSlash != 0 && !string.IsNullOrEmpty(c.fxPrefab)) SlashFx(shooter, c, i); // #956 휘두르는 손의 날 자리에 작은 사본 + } + else if (c.slashHardRestart != 0) + { + // #959 Play 실측: 틱 사이에 다른 상태(원본 콤보 AI 의 Attack3→4 · 원본 Jump 사슬 · exit 전이)가 끼어들어 3타 회전·4타 돌진(1.4 m)이 섞였다 → 매 프레임 지키고 어긋나면 그 프레임에 되돌린다 + bool foreign = an.GetCurrentAnimatorStateInfo(0).shortNameHash != cur || (an.IsInTransition(0) && an.GetNextAnimatorStateInfo(0).shortNameHash != cur); + if (foreign) { an.Play(cur, 0, 0f); an.speed = Mathf.Max(0.1f, c.slashSpeed); SlashLeaks++; } + } + yield return null; } } finally { if (!DSUtil.CheckNull(shooter) && shooter.m_animation != null) { shooter.m_animation.fireEvents = prevEvents; shooter.m_animation.speed = 1f; } } - SlashLog = "난사 " + i + "회 · " + (Time.time - t0).ToString("F2") + " s"; + SlashLog = "난사 " + (i + 1) + "회 · " + (Time.time - t0).ToString("F2") + " s · 되돌림 " + SlashLeaks; } public static int FxSlashes; public static string FxSlashLog = "";