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

195 lines
10 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ─────────────────────────────────────────────────────────────────────────────
// WLWeaponAttackPattern.cs — 무기 계열별 기본 공격 패턴 (#825 · 2026-10-09 · PD 「6종 무기의 기본 공격 패턴 구현」)
//
// 모션·사거리·탄(근접 Projectile_Stop / 원거리 Projectile_Strait)·무기 모델은 WLWeaponClassApplier 의 직업 전환이 바꾼다.
// 이 드라이버는 공격 표(WLWeaponAttackTable.csv)의 나머지 칸을 힘민지 전투 훅(원본 0줄)에 얹는다:
// - dmg_ratio_per_hit : 타격 순간(CombatEvents.HitboxHit) ProjectileData.AddDmgPercent 를 그 콤보 단계의 값으로 덮는다
// (Get_DamageInfo 가 BaseDmg × AddDmgPercent 로 계산 · 원본은 클립 이벤트 float)
// - hits : 2 이상이면 그 단계 동안 MultiShot CC(원본 Shoot_Projectile 의 다발 사격 경로)를 켠다 → 탄 수 = hits · 피해 감산 0
// - attack_speed_mult : FinalAttackSpeed 를 기준값 × 배율로 유지(재계산으로 되돌아오면 0.5초 안에 다시)
// - range_m(원거리) : 활·지팡이는 WLTargetingSettings 의 탐색·추적 거리(#813t3 근접 3 m 제한)를 표 사거리 + 여유로 덮는다
// - 미적용(다음 단계) : arc_deg(클립 판정 상자 고정) · aoe_radius(판정 상자 크기) · proj_speed/pierce(탄 프리팹 값) · move_m(루트모션 표)
// 켜고 끄기 = WLWeaponSettings.attackPattern(값 하나 · C8).
// ─────────────────────────────────────────────────────────────────────────────
using System.Collections.Generic;
using UnityEngine;
using WL.Combat.Core;
using WL.Equip;
namespace WL.Combat.Weapon
{
public class WLWeaponAttackPattern : MonoBehaviour
{
public static WLWeaponAttackPattern Instance { get; private set; }
public static int Applied, HitsScaled, StagesSeen, MultiShotSet, SpeedSet;
public static string LastLog { get; private set; } = "";
public static int CurrentWeapon, CurrentStage;
public static float LastRatio, LastSpeed;
List<WLWeaponAttackStep> _steps;
int _weapon = -2; UnityEngine.Object _pc; float _t;
float _baseSpd = -1f, _lastSetSpd = -1f, _mult = 1f;
int _stage = 1; int _multiShotVal = -1;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
static void Boot()
{
var cfg = WLWeaponSettings.Instance;
if (cfg == null || cfg.enabled_ == 0 || Instance != null) return;
var go = new GameObject("WL_WeaponAttackPattern");
DontDestroyOnLoad(go);
Instance = go.AddComponent<WLWeaponAttackPattern>();
}
void OnEnable()
{
CombatEvents.ComboStage.Add(OnStage);
CombatEvents.HitboxHit.Add(OnHit);
CombatEvents.AttackStarted.Add(OnAttackStarted);
WLInventory.Changed += Dirty;
}
void OnDisable()
{
CombatEvents.ComboStage.Remove(OnStage);
CombatEvents.HitboxHit.Remove(OnHit);
CombatEvents.AttackStarted.Remove(OnAttackStarted);
WLInventory.Changed -= Dirty;
ClearOverrides();
}
void OnDestroy() { if (Instance == this) Instance = null; }
void Dirty() { _weapon = -2; _t = 999f; }
void Update()
{
var cfg = WLWeaponSettings.Instance;
if (cfg == null || cfg.enabled_ == 0) return;
if (cfg.attackPattern == 0) { if (_steps != null) ClearOverrides(); return; }
_t += Time.unscaledDeltaTime;
if (_t < cfg.pollSec) return;
_t = 0f;
try { Tick(cfg); }
catch (System.Exception ex) { LastLog = "예외 — " + ex.Message; }
}
void Tick(WLWeaponSettings cfg)
{
if (!MyValue.bMyPC || MyValue.MyPC == null) { _pc = null; return; }
var pc = MyValue.MyPC;
if (!WLInventory.EnsureLoaded() || !WLWeaponAttackTable.EnsureLoaded()) return;
var w = WLInventory.EquippedInSlot(eFREquipmentType.Weapon);
int wid = w != null && w.itemJson != null ? w.itemJson.GroupNo : 0;
bool pcChanged = !ReferenceEquals(pc, _pc);
if (wid != _weapon || pcChanged)
{
_pc = pc; _weapon = wid; CurrentWeapon = wid;
_steps = wid > 0 ? WLWeaponAttackTable.Steps(wid) : null;
var f = _steps != null && _steps.Count > 0 ? _steps[0] : null;
_mult = f != null && f.attackSpeedMult > 0f ? f.attackSpeedMult : 1f;
_baseSpd = -1f; _lastSetSpd = -1f;
// 원거리 계열 = 표 사거리로 탐색·추적 거리 덮기(근접은 에셋 값 그대로)
bool ranged = f != null && (f.family == "Bow" || f.family == "Staff");
float r = ranged ? f.rangeM + cfg.rangedRangeMargin : cfg.meleeSearchRange;
// #893 PD 「인식 범위는 모든 무기 공통 · 활 사정거리와 동일」 — 계열이 정해져 있으면 그 계열 표 사거리를 모든 무기에 쓴다
float common = WLWeaponAttackTable.MaxRangeOfFamily(cfg.recognitionFamily);
if (common > 0f) r = common;
WL.Settings.WLTargetingSettings.RuntimeSearchRangeOverride = r;
WL.Settings.WLTargetingSettings.RuntimeChaseRangeOverride = r;
SetMultiShot(pc, 0);
Applied++;
LastLog = "무기 " + wid + (f != null ? "(" + f.weaponName + " · " + f.family + ")" : "") + " 단계 " + (_steps != null ? _steps.Count : 0) +
" · 공속 ×" + _mult.ToString("0.##") + (common > 0f ? " · 인식 " + r.ToString("0.#") + " m(" + cfg.recognitionFamily + " 공통)" : ranged ? " · 사거리 " + r.ToString("0.#") + " m" : "");
if (cfg.verboseLog != 0) Debug.Log("[WL-825] 공격 패턴 " + LastLog);
}
ApplySpeed(pc);
}
/// <summary>FinalAttackSpeed = 기준 × 배율. 재계산(Reset)으로 값이 바뀌면 그 값을 새 기준으로 삼아 다시 덮는다.</summary>
void ApplySpeed(PCActor pc)
{
if (_steps == null) return;
ActorStatInfo info; try { info = pc.Get_StatInfo(); } catch { return; }
if (info == null) return;
float cur = (float)info.Get_Stat(eStat.FinalAttackSpeed);
if (_baseSpd < 0f || Mathf.Abs(cur - _lastSetSpd) > 0.0005f) _baseSpd = cur; // 처음 · 또는 밖에서 바뀜 = 새 기준
float desired = _baseSpd * _mult;
if (Mathf.Abs(cur - desired) > 0.0005f)
{
info.Set_Stat(eStat.FinalAttackSpeed, desired);
_lastSetSpd = desired; LastSpeed = desired; SpeedSet++;
}
else _lastSetSpd = cur;
}
WLWeaponAttackStep StepOf(int stage)
{
if (_steps == null || _steps.Count == 0) return null;
for (int i = 0; i < _steps.Count; i++) if (_steps[i].step == stage) return _steps[i];
return _steps[Mathf.Clamp(stage - 1, 0, _steps.Count - 1)];
}
void OnAttackStarted(in AttackStartedEvent e)
{
if (!e.isMainPC || _steps == null) return;
_stage = 1; CurrentStage = 1;
ApplyStage(e.actor as PCActor, 1);
}
void OnStage(in ComboStageEvent e)
{
if (_steps == null || !ReferenceEquals(e.actor, MyValue.MyPC)) return;
StagesSeen++;
_stage = Mathf.Max(1, e.stage); CurrentStage = _stage;
ApplyStage(e.actor, _stage);
}
void ApplyStage(PCActor pc, int stage)
{
var s = StepOf(stage);
if (s == null || pc == null) return;
SetMultiShot(pc, Mathf.Max(0, s.hits - 1));
}
/// <summary>원본 다발 사격 경로(Shoot_Projectile 의 MultiShot CC) — 탄 수 = 1 + CCVal · 피해 감산 CCDmg = 0.</summary>
void SetMultiShot(PCActor pc, int extra)
{
if (pc == null || _multiShotVal == extra) return;
try
{
var cc = pc.Get_CCData(eCC.MultiShot);
if (cc == null) return;
cc.CCVal = extra;
cc.CCDmg = 0;
cc.CCTime = extra > 0 ? 3600f : 0f;
_multiShotVal = extra; MultiShotSet++;
}
catch (System.Exception ex) { LastLog = "MultiShot 실패 — " + ex.Message; }
}
/// <summary>타격 순간 — 이 콤보 단계의 타당 피해 배율로 AddDmgPercent 를 덮는다(원본 Get_DamageInfo 가 바로 뒤에서 읽는다).</summary>
void OnHit(in HitboxHitEvent e)
{
if (_steps == null || e.isSkill || e.data == null || !ReferenceEquals(e.shooter, MyValue.MyPC)) return;
var s = StepOf(_stage);
if (s == null || s.dmgRatioPerHit <= 0f) return;
e.data.AddDmgPercent = s.dmgRatioPerHit;
LastRatio = s.dmgRatioPerHit; HitsScaled++;
}
void ClearOverrides()
{
WL.Settings.WLTargetingSettings.RuntimeSearchRangeOverride = -1f;
WL.Settings.WLTargetingSettings.RuntimeChaseRangeOverride = -1f;
if (MyValue.bMyPC && MyValue.MyPC != null) SetMultiShot(MyValue.MyPC, 0);
_steps = null; _weapon = -2;
}
public static string Dump()
{
return "weapon=" + CurrentWeapon + " stage=" + CurrentStage + " applied=" + Applied + " stages=" + StagesSeen + " hitsScaled=" + HitsScaled + " lastRatio=" + LastRatio.ToString("0.##") +
" multiShotSet=" + MultiShotSet + " speedSet=" + SpeedSet + " lastSpeed=" + LastSpeed.ToString("0.###") +
" search=" + WL.Settings.WLTargetingSettings.RetargetSearchRange.ToString("0.#") + " chase=" + WL.Settings.WLTargetingSettings.ChaseRange.ToString("0.#") + " | " + LastLog;
}
}
}