Project_WL/Assets/WL/Combat/Weapon/WLSkillChain.cs

162 lines
8.9 KiB
C#
Raw Normal View History

// ─────────────────────────────────────────────────────────────────────────────
// WLSkillChain.cs — 조건 발동 스킬: 무기 기술 표 trigger 칸(예: hit:Skill_ShieldAttack)의 스킬을 그 조건 때 쿨 없이 시전
//
// PD 원문(#890 · 2026-10-17): 「한손검 전설 스킬은 별도의 쿨타임 없이 "방패 치기 명중 시" 발동 되도록 해줘.」
//
// ■ 원리 — 타격 확정 훅(CombatEvents.HitConfirmed)에서 내 PC 가 쏜 스킬(피해 정보 Beater_pd)이 명중하면, 낀 무기의 표 행 중
// trigger = "hit:<그 스킬 eEffect>" 이고 등급이 해금 단계 이상인 행을 찾아 delay 초 뒤 시전한다(같은 프레임 재진입 회피).
// 원본 Use_Skill 은 스킬 동작 중이면 실패한다 → cancelTrigger 1 이면 앞 스킬의 남은 동작을 Play_Idle 로 끊고 바로 잇는다.
// 시전 = 시험 모드(쿨 무시 · MP 0 복사본 · 사거리 = 표 range_m) — 그 행은 자동 시전 슬롯에 안 들어간다(WLWeaponSkillSlots).
// ■ 값 = Resources/WL/WLSkillChain.json (C45) · 끄기 = enabled 0(구독 0) · 원본 수정 0줄
// 🔴 Debug.LogError 금지(원본 ErrorLogHook 이 팝업을 띄운다).
// ─────────────────────────────────────────────────────────────────────────────
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
using WL.Combat.Core;
using WL.Equip;
namespace WL.Combat.Weapon
{
[System.Serializable]
public class WLSkillChainJson
{
public int enabled = 0;
public float delay = 0.12f; // 명중 → 시전까지(s)
public int cancelTrigger = 1; // 1 = 앞 스킬 남은 동작을 끊고 잇는다 · 0 = 끝날 때까지 기다린다
public float maxWait = 1f; // cancelTrigger 0 일 때 최대 대기(s)
public float minRange = 0f; // 이어 쓰는 스킬의 시전 거리 하한(m) — 명중 직후라 대상이 밀려나도 끊기지 않게
public string note = "";
}
public static class WLSkillChain
{
public const string ResourcesPath = "WL/WLSkillChain";
public static int Triggered, Cast, Failed;
public static string LastLog = "", LastFail = "";
static WLSkillChainJson s_cfg;
static WLSkillChainRunner s_runner;
static PCActor s_pc;
static WLWeaponSkillRow s_row;
static float s_at, s_deadline;
static bool s_pending;
static readonly Dictionary<int, SkillListTableData> s_data = new Dictionary<int, SkillListTableData>();
static WLSkillChainJson Cfg
{
get
{
if (s_cfg != null) return s_cfg;
s_cfg = new WLSkillChainJson();
var ta = Resources.Load<TextAsset>(ResourcesPath);
if (ta == null) { LastLog = "설정 없음 " + ResourcesPath; return s_cfg; }
try { JsonUtility.FromJsonOverwrite(ta.text, s_cfg); } catch (System.Exception ex) { LastLog = "설정 파싱 실패 — " + ex.Message; }
return s_cfg;
}
}
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
static void Boot()
{
s_cfg = null; s_pending = false; s_data.Clear();
if (Cfg.enabled == 0) { CombatEvents.HitConfirmed.Remove(OnHit); return; }
CombatEvents.HitConfirmed.Add(OnHit);
}
static void OnHit(in HitConfirmedEvent e)
{
try
{
if (s_pending) return; // 이미 예약됨(같은 시전의 다단 히트)
var sk = e.projectile != null ? e.projectile.m_SkillListTableData : null;
var pc = e.attacker as PCActor;
if (sk == null || pc == null || !pc.IsMainPC()) return;
var w = WLInventory.EquippedInSlot(eFREquipmentType.Weapon);
if (w == null || w.itemJson == null) return;
var rows = WLWeaponSkillTable.Rows(w.itemJson.GroupNo);
if (rows == null) return;
string trig = "hit:" + sk.e_Skill;
WLGradeTier gt; int plus;
int tier = WLEquipGrades.TryMap(w.itemJson, out gt, out plus) ? gt.tier : 1;
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>
2026-10-21 04:05:18 +00:00
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;
var c = Cfg;
s_pc = pc; s_row = r; s_at = Time.time + c.delay; s_deadline = s_at + c.maxWait; s_pending = true;
EnsureRunner();
Triggered++;
LastLog = "예약 " + r.skillName + " ← " + sk.e_Skill + " 명중 #" + Triggered;
break;
}
}
catch (System.Exception ex) { Failed++; LastLog = "명중 처리 실패 — " + ex.Message; }
}
static void EnsureRunner()
{
if (!DSUtil.CheckNull(s_runner)) return;
var go = new GameObject("[WL890] SkillChainRunner");
go.hideFlags = HideFlags.HideAndDontSave;
s_runner = go.AddComponent<WLSkillChainRunner>();
}
internal static void Tick()
{
if (!s_pending || Time.time < s_at) return;
var pc = s_pc; var r = s_row;
if (DSUtil.CheckNull(pc) || pc.IsDead() || r == null) { s_pending = false; return; }
var c = Cfg;
// #908 이동 중(조이스틱 1 · 구르기)이면 잇지 않는다 — 이어 붙인 스킬이 이동 입력에 바로 스킵(WLSkillCancel)되어 첫 프레임만 번쩍였다(실측 2 프레임)
if (pc.JoystickStatus == 1 || pc.Get_CurAnim() == eAnim.Dodge || pc.Get_CurAnim() == eAnim.Run) { s_pending = false; Failed++; LastLog = "이동 중 — 연계 안 함 " + r.skillName; return; }
if (pc.Get_CurAnim() >= eAnim.Skill)
{
if (c.cancelTrigger != 0) pc.Play_Idle(); // 앞 스킬(방패 치기)의 남은 동작을 끊고 잇는다
else if (Time.time < s_deadline) return;
}
s_pending = false;
try
{
var data = Prep(pc, r);
if (data == null) { Failed++; LastLog = "스킬 데이터 없음 " + r.skillId; return; }
bool ok = pc.Use_Skill(data, true, Mathf.Clamp(r.slot - 1, 0, 5), true);
if (ok) { Cast++; LastLog = "발동 " + r.skillName + " #" + Cast; }
else
{
Failed++;
var tg = pc.Get_Target();
LastFail = "발동 안 됨 " + r.skillName + " · 동작 " + pc.Get_CurAnim() + " · 대상 " + (DSUtil.CheckNull(tg) ? "없음" : Vector3.Distance(tg.Get_position(), pc.Get_position()).ToString("F2") + " m / 사거리 " + data.f_SkillRange) + " · CC " + pc.IsCC();
LastLog = LastFail;
}
}
catch (System.Exception ex) { Failed++; LastLog = "발동 실패 — " + ex.Message; }
}
static MethodInfo s_clone; static FieldInfo s_fId;
/// <summary>시험 시전용 복사본(원본 표 데이터 무수정) — MP 0 · 사거리 = 표 range_m · 직업 칸 = PC 직업.</summary>
static SkillListTableData Prep(PCActor pc, WLWeaponSkillRow r)
{
SkillListTableData d;
if (s_data.TryGetValue(r.skillId, out d)) return d;
var src = table_skilllist.Ins != null ? table_skilllist.Ins.Get_Data_orNull(r.skillId) : null;
if (src == null) return null;
if (s_clone == null) s_clone = typeof(object).GetMethod("MemberwiseClone", BindingFlags.Instance | BindingFlags.NonPublic);
if (s_fId == null) s_fId = typeof(Actor).GetField("m_ID", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
d = (SkillListTableData)s_clone.Invoke(src, null);
d.n_MP = 0;
if (r.rangeM > 0f) d.f_SkillRange = r.rangeM;
d.f_SkillRange = Mathf.Max(d.f_SkillRange, Cfg.minRange);
if (s_fId != null) d.n_SkillClassID = (int)s_fId.GetValue(pc);
s_data[r.skillId] = d;
return d;
}
}
/// <summary>WLSkillChain 의 시간 축. 숨김 GameObject 1개 · 예약이 있을 때만 일한다.</summary>
internal sealed class WLSkillChainRunner : MonoBehaviour
{
void Update() { WLSkillChain.Tick(); }
}
}