From e1da2ad360709aff25e8f79a325b64caabc6b12b Mon Sep 17 00:00:00 2001 From: swrring Date: Tue, 20 Oct 2026 23:21:09 +0900 Subject: [PATCH] =?UTF-8?q?WL=20#953=20PD=20=EC=A7=80=EC=8B=9C=202?= =?UTF-8?q?=EA=B1=B4(LuckyDefense=20=EC=84=B8=EC=85=98=20=EA=B2=BD?= =?UTF-8?q?=EC=9C=A0)=20=E2=80=94=20=E2=91=A0=20=ED=99=9C=203=ED=83=80=20?= =?UTF-8?q?=3D=20=EC=97=B0=EC=86=8D=203=EB=B0=9C:=20=ED=91=9C=20extraClipE?= =?UTF-8?q?vents(WLWeaponClasses.json)=EB=A1=9C=20Bow=5FSkill4=5FPush=20?= =?UTF-8?q?=ED=81=B4=EB=A6=BD=EC=97=90=20Projectile@0.30=20=EC=9D=84=20Pla?= =?UTF-8?q?y=20=EB=A7=88=EB=8B=A4=20=EB=9F=B0=ED=83=80=EC=9E=84=20?= =?UTF-8?q?=EC=A3=BC=EC=9E=85=20+=20Animator.Rebind(=EC=9D=B4=EC=A0=84=20?= =?UTF-8?q?=3D=20FR=5FBow3=201=EB=B0=9C=20+=20push=201=EB=B0=9C)=20?= =?UTF-8?q?=E2=91=A1=20=EB=B9=84=EC=88=98=20=EA=B0=81=EC=84=B1=20=3D=20?= =?UTF-8?q?=EC=B9=B4=EB=A9=94=EB=9D=BC=20=EA=B0=81=EB=8F=84=20=EC=9C=A0?= =?UTF-8?q?=EC=A7=80(WLGhostStrike.json=20keepCamera=20=E2=86=92=20?= =?UTF-8?q?=EC=9B=90=EB=B3=B8=20Skill=5FDarkAmbush=20=EC=9D=98=20Set=5FTop?= =?UTF-8?q?View=202=EA=B3=B3=20=EA=B1=B4=EB=84=88=EB=9C=80)=20+=20?= =?UTF-8?q?=EB=82=99=ED=95=98=20=EC=A4=91=20=EC=95=9E=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?0.35=20m(fallForward=20=C2=B7=20=EB=8C=80=EA=B0=81=EC=84=A0=20?= =?UTF-8?q?=EB=82=99=ED=95=98)=20=EB=92=A4=20=EB=AF=B8=EB=81=84=EB=9F=AC?= =?UTF-8?q?=EC=A7=90=20=EA=B0=90=EC=86=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- .../Projectile/Skill/Skill_DarkAmbush.cs | 5 +-- .../Weapon/Resources/WL/WLGhostStrike.json | 3 ++ .../Weapon/Resources/WL/WLWeaponClasses.json | 13 +++++++ Assets/WL/Combat/Weapon/WLGhostStrike.cs | 14 ++++++++ Assets/WL/Combat/Weapon/WLWeaponClasses.cs | 36 +++++++++++++++++-- 5 files changed, 67 insertions(+), 4 deletions(-) diff --git a/Assets/Script/Character/Projectile/Skill/Skill_DarkAmbush.cs b/Assets/Script/Character/Projectile/Skill/Skill_DarkAmbush.cs index 180eb8a4f..70555c234 100644 --- a/Assets/Script/Character/Projectile/Skill/Skill_DarkAmbush.cs +++ b/Assets/Script/Character/Projectile/Skill/Skill_DarkAmbush.cs @@ -20,11 +20,12 @@ public class Skill_DarkAmbush : ProjectileBase colliderControl.gameObject.SetActive(false); colliderControl.Set(m_ProjecTileData); m_ProjecTileData.shooter.Set_Invisible(1.5f, null); - if (MyValue.m_RealCamera != null) MyValue.m_RealCamera.Set_TopView(m_ProjecTileData.shooter); // WL #881 섬 RealCamera 없음 가드 + bool keepCam = WL.Combat.Weapon.WLGhostStrike.KeepCamera(this); // WL #953 PD 「카메라 각도를 바꾸지 말고 현재 상태로 착지」 — 규칙(WLGhostStrike.json keepCamera)이면 탑뷰 전환 생략 + if (MyValue.m_RealCamera != null && !keepCam) MyValue.m_RealCamera.Set_TopView(m_ProjecTileData.shooter); // WL #881 섬 RealCamera 없음 가드 yield return new WaitForSeconds(0.25f); colliderControl.gameObject.SetActive(true); yield return new WaitForSeconds(1.75f); - if (MyValue.m_RealCamera != null) MyValue.m_RealCamera.Set_TopView(null); // WL #881 섬 RealCamera 없음 가드 + if (MyValue.m_RealCamera != null && !keepCam) MyValue.m_RealCamera.Set_TopView(null); // WL #881 섬 RealCamera 없음 가드 Off(true); } diff --git a/Assets/WL/Combat/Weapon/Resources/WL/WLGhostStrike.json b/Assets/WL/Combat/Weapon/Resources/WL/WLGhostStrike.json index 07ed15f28..a3a8d8d7a 100644 --- a/Assets/WL/Combat/Weapon/Resources/WL/WLGhostStrike.json +++ b/Assets/WL/Combat/Weapon/Resources/WL/WLGhostStrike.json @@ -52,6 +52,9 @@ "landGhosts": 2, "dustPrefab": "WL/Fx/WL_Fx_LandDust", "dustScale": 0.15, + "keepCamera": 1, + "fallForward": 0.35, + "_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_952": "#952 PD 「이펙트가 끝나는 시점에 즉시 착지(서 있는 모습 없이) · 떨어진 후 미끄러지듯 살짝 전진하다 멈춤 · 흙먼지는 캐릭터 등신에 맞게」 — 착지 시작 = 원본이 모델을 다시 켜는 그 프레임(WLGhostStrike.LateTick · 그 프레임은 숨김 · 섞임 0) · 미끄러짐 = 감속(2D/T → 0 · 0.4 m / 0.35 s) · 먼지 = HCFX Dust_Dash 사본의 긴 줄기(3D 크기 1.7 × 5.0 × lengthScale 1 = 프리팹 단위 5 m) × 0.15 ≈ 0.75 m(PC 키 1.13 m 실측 · Bounds952.Model)", "_note_951": "#951 PD 「각성 시전 시 뛰어오르는 잔상 → 사라진 채 공격 → 잔상과 함께 등장 · 낮은 공중에서 떨어져 착지 포즈 · 0.5 m 미끄러지다 정지 · 흙먼지」 — 시전 순간 위·앞으로 잔상 jumpGhosts 개(jumpSeconds 동안) · PC 가 다시 보이는 순간(또는 landAtSeconds) 모델을 landHeight 에서 landFallSeconds 에 내리며 잔상 landGhosts · 착지 클립 landState(어새신 컨트롤러 ghost_land = Platform Sub_HeroLanding 0~36) · slideSeconds 동안 NavMeshAgent.Move 로 slideDistance 앞으로 · 착지 때 dustPrefab(Hyper Casual Dust_Dash 사본) · landTotalSeconds 뒤 idle", "poseSource": "clips", diff --git a/Assets/WL/Combat/Weapon/Resources/WL/WLWeaponClasses.json b/Assets/WL/Combat/Weapon/Resources/WL/WLWeaponClasses.json index da471edb5..cff53060f 100644 --- a/Assets/WL/Combat/Weapon/Resources/WL/WLWeaponClasses.json +++ b/Assets/WL/Combat/Weapon/Resources/WL/WLWeaponClasses.json @@ -96,6 +96,19 @@ "_note": "#838 (2026-10-11) PD 「대검 아이템 + 2Handed Warrior 모션 공격 타입」 — 모션·이벤트(타격 Projectile_10801 · 베기 Effect_Slash_10101_N 재사용)는 매핑 도구 WL826/WL_CombatAnimSetup 이 2Handed 팩 서브클립에 굽는다(런타임 projectileTimes/effectTimes 불필요) · 모델 = PurePoly 양손검 · 소켓 2 = 한손검 자리(포즈 띠 실측) · 사거리 1.8 = 제안값" } ], + "extraClipEvents": [ + { + "classId": 10402, + "controller": "pcanim_archer", + "clipToken": "Bow_Skill4_Push", + "functionName": "Projectile", + "time": 0.30, + "stringParameter": "Projectile_10402", + "floatParameter": 1.0, + "intParameter": 0, + "_note": "#953 PD 「활 3타 때 투사체가 연속으로 3발 나가도록(지금 3타 째 2발 · 중간에 화살 한 발을 더)」 — 3타 = FR_Bow3(화살 @0.74 s) → Bow_Skill4_Push(화살 @0.77 s · 길이 1.60 s) 연결이라 2발이었음(컨트롤러 실측) · 중간 = push 길이 비율 0.30(= 0.48 s · 실시간 간격 약 0.27 s 씩 3발) · 10401(들꽃 단궁)은 3타가 Bow_Attack_Loop 라 해당 없음" + } + ], "weaponOrient": [ { "prefab": "PP_Theme_05_Bow_003", diff --git a/Assets/WL/Combat/Weapon/WLGhostStrike.cs b/Assets/WL/Combat/Weapon/WLGhostStrike.cs index 7e9154e24..3a1f70a5d 100644 --- a/Assets/WL/Combat/Weapon/WLGhostStrike.cs +++ b/Assets/WL/Combat/Weapon/WLGhostStrike.cs @@ -49,6 +49,8 @@ namespace WL.Combat.Weapon public string airClip = ""; // 원본 스킬의 공중 클립 이름 조각(예 Skill8_Air) — 이 클립이 보였다가 다른 클립으로 바뀌는 순간 = 공격 끝 → 착지 시작 public int landGhosts = 2; // 떨어지는 동안 잔상 수 public string dustPrefab = ""; public float dustScale = 1f; + public int keepCamera = 0; // #953 1 = 원본 Skill_DarkAmbush 의 탑뷰 전환(RealCamera.Set_TopView)을 건너뛴다 — 카메라 각도 유지 + public float fallForward = 0f; // #953 낙하하는 동안 앞으로 가는 거리(m · 대각선으로 미끄러지며 낙하) · 착지 뒤 미끄러짐(slideDistance)은 별도 public string poseSource = "clips"; // clips = 숨은 꼭두각시에 poses 를 걸어 굽기 · live = 시전 순간 PC 자세 1 개 public string[] poses = new string[0]; // "애니메이터 상태 이름@정규화 시각" — 차례로 돌려 쓴다(시작은 무작위) } @@ -162,6 +164,13 @@ namespace WL.Combat.Weapon // ───────────────────────────────────────── 원본 훅 /// 원본 스킬 Set 훅(1줄) — 표에 있는 스킬이면 시전을 기록하고 부품 경로를 잡는다(이 순간 PC 는 아직 보인다 · 원본 숨김은 훅 뒤 Co_Update). + /// #953 PD 「비수 공격 시 왜 카메라 각도가 달라지는가 — 각도를 바꾸지 말고 현재 상태로 착지」 — 원본 Skill_DarkAmbush 가 Set_TopView 전에 묻는다(규칙 keepCamera 1 = 탑뷰 전환 건너뜀 · 원본 훅 2줄). + public static bool KeepCamera(ProjectileBase skill) + { + try { var c = Cfg; if (skill == null || c.enabled == 0) return false; WLGhostStrikeRule r; return s_rules.TryGetValue(skill.GetType().Name, out r) && r != null && r.keepCamera != 0; } + catch (System.Exception) { return false; } + } + public static void OnSet(ProjectileBase skill) { try @@ -348,6 +357,11 @@ namespace WL.Combat.Weapon float k = 1f - t / fall; float y = r.landHeight * k * k; // 가속 낙하(실제 적용은 LateTick) cast.modelY = cast.modelBaseY + y; if (cast.landGhostsDone < r.landGhosts && t >= fall * (cast.landGhostsDone + 1f) / (r.landGhosts + 1f)) { cast.landGhostsDone++; if (Spawn(c, cast, cast.landPos + Vector3.up * y, cast.landFwd)) JumpSpawned++; } + if (r.fallForward > 0f) // #953 대각선 낙하 — 떨어지는 동안 앞으로 fallForward(등속) · 착지 뒤엔 slide 감속 + { + 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; + } return; } cast.modelY = cast.modelBaseY; diff --git a/Assets/WL/Combat/Weapon/WLWeaponClasses.cs b/Assets/WL/Combat/Weapon/WLWeaponClasses.cs index f4fba142a..451e18531 100644 --- a/Assets/WL/Combat/Weapon/WLWeaponClasses.cs +++ b/Assets/WL/Combat/Weapon/WLWeaponClasses.cs @@ -32,7 +32,9 @@ namespace WL.Combat.Weapon } /// #834 무기 인스턴스 로컬 회전 보정(소켓 방향 규약이 팩 손 자세와 다를 때 · 원본 프리팹 0줄). public class WLWeaponOrient { public string prefab = ""; public float[] euler; } - class WLWeaponClassesJson { public List classes; public List weaponOrient; } + /// #953 기존 직업의 클립에 이벤트를 하나 더 붙인다(예 활 3타 push 클립에 화살 1발 추가) — 클립 이름 조각 · 컨트롤러 이름 조각 · 시점(길이 비율) · 함수 · 프리팹 · 배율. 같은 함수가 같은 시점(±0.02 s)에 이미 있으면 안 붙인다(멱등 · 세션 동안 유지). + public class WLExtraClipEvent { public int classId; public string controller = ""; public string clipToken = ""; public string functionName = "Projectile"; public float time = 0.5f; public string stringParameter = ""; public float floatParameter = 1f; public int intParameter; public string _note = ""; } + class WLWeaponClassesJson { public List classes; public List weaponOrient; public List extraClipEvents; } /// 보정을 이미 적용한 무기 인스턴스 표식(중복 회전 방지). public sealed class WLWeaponOrientMark : MonoBehaviour { public string rule = ""; } @@ -57,7 +59,7 @@ namespace WL.Combat.Weapon /// #947 Enter Play Mode 「도메인 리로드 끔」 대비 — Play 마다 표(table_classconfig)·PC 는 새 인스턴스인데 정적 「끝남」 표시가 남으면 수정 행·모델 재부착을 건너뛴다. 표 데이터(s_defs)는 에셋이라 그대로 둔다. [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] - static void ResetStatics() { s_overridden.Clear(); s_modelRefreshed.Clear(); Injected = EventsAdded = Overridden = 0; } + static void ResetStatics() { s_overridden.Clear(); s_modelRefreshed.Clear(); Injected = EventsAdded = Overridden = 0; s_extraDone.Clear(); ExtraEventsAdded = 0; ExtraLog = ""; } // #953 런타임 AddEvent 는 Play 를 나가면 클립에서 사라진다(실측) → Play 마다 다시 붙인다 static readonly List s_orients = new List(); // #834 무기 방향 보정 규칙 public static int OrientApplied; @@ -135,6 +137,7 @@ namespace WL.Combat.Weapon if (j != null && j.classes != null) foreach (var d in j.classes) if (d != null && d.classId > 0) s_defs[d.classId] = d; s_orients.Clear(); if (j != null && j.weaponOrient != null) s_orients.AddRange(j.weaponOrient); + s_extra.Clear(); if (j != null && j.extraClipEvents != null) s_extra.AddRange(j.extraClipEvents); // #953 Loaded = true; LastLog = "추가 직업 표 로드 — " + s_defs.Count + " · 방향 보정 " + s_orients.Count; } @@ -192,6 +195,7 @@ namespace WL.Combat.Weapon /// 그 직업의 공격 클립에 타격 이벤트(Projectile · 프리팹 이름 · 피해 배율)를 런타임으로 붙인다. 컨트롤러가 아직 안 올라왔으면 false. public static bool EnsureClipEvents(PCActor pc, int classId) { + ApplyExtraEvents(pc, classId); // #953 기존 직업 클립에 추가 이벤트(활 3타 3발) var d = Def(classId); if (d == null || pc == null || d.attackClipTokens == null || d.attackClipTokens.Count == 0) return true; var an = pc.GetComponentInChildren(); @@ -241,5 +245,33 @@ namespace WL.Combat.Weapon } return matched > 0; } + + static readonly List s_extra = new List(); + static readonly HashSet s_extraDone = new HashSet(); // (클립 인스턴스 ID · 규칙 index) + public static int ExtraEventsAdded; public static string ExtraLog = ""; + /// #953 PD 「활 3타 때 투사체가 연속으로 3발 나가도록(지금 2발 · 중간에 한 발 더)」 — 표 extraClipEvents 의 그 직업 규칙을 지금 컨트롤러의 클립(이름 조각)에 붙인다. 컨트롤러가 아직 안 바뀌었으면(비동기) 이름 조각이 안 맞아 건너뛴다. 클립은 에셋이라 한 번 붙이면 세션 동안 남는다(같은 시점 중복 금지). + static void ApplyExtraEvents(PCActor pc, int classId) + { + EnsureLoaded(); + if (pc == null || s_extra.Count == 0) return; + var an = pc.GetComponentInChildren(); var rac = an != null ? an.runtimeAnimatorController : null; if (rac == null) return; + int added = 0; + for (int i = 0; i < s_extra.Count; i++) + { + var x = s_extra[i]; if (x == null || x.classId != classId || string.IsNullOrEmpty(x.clipToken) || string.IsNullOrEmpty(x.functionName)) continue; + if (!string.IsNullOrEmpty(x.controller) && !rac.name.Contains(x.controller)) continue; + foreach (var c in rac.animationClips) + { + if (c == null || !c.name.Contains(x.clipToken)) continue; + long key = ((long)c.GetInstanceID() << 8) | (long)(i & 255); if (s_extraDone.Contains(key)) continue; + if (pc.Get_CurAnim() == eAnim.Skill) return; // 붙인 뒤 Rebind 가 동작을 끊으므로 스킬 중엔 미룸(다음 폴링 · 기본 공격은 한 번 끊겨도 됨 — 자동 전투 중엔 대기 프레임이 거의 없음) + float at = Mathf.Clamp(c.length * x.time, 0f, Mathf.Max(c.length - 0.0001f, 0f)); bool has = false; + var evs = c.events; if (evs != null) foreach (var e in evs) if (e.functionName == x.functionName && Mathf.Abs(e.time - at) < 0.02f) { has = true; break; } + if (!has) { c.AddEvent(new AnimationEvent { functionName = x.functionName, time = at, stringParameter = x.stringParameter, floatParameter = x.floatParameter, intParameter = x.intParameter, messageOptions = SendMessageOptions.DontRequireReceiver }); ExtraEventsAdded++; added++; } + s_extraDone.Add(key); ExtraLog = c.name + " +" + x.functionName + "@" + at.ToString("F2") + (has ? "(이미 있음)" : "") + " " + x.stringParameter; + } + } + if (added > 0) { an.Rebind(); ExtraLog += " · Rebind"; } // 런타임 AddEvent 는 Rebind 전엔 애니메이터가 안 읽는다(Hit1LookAtTarget 선례 · #953 실측: Rebind 없이는 3발째가 안 나갔다) + } } }