198 lines
11 KiB
C#
198 lines
11 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// 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)
|
||
{
|
||
// #976 PD 「활 공격 속도만 늦추라고 했는데 다른 무기의 공격 속도도 같이 느려진 것 같아」 — 무기를 바꿀 때 기준을 버리고 지금 값(활 × 0.7 이 덮인 값)을 새 기준으로 잡아
|
||
// 다른 무기에 0.7 배가 남았다 → 같은 PC 면 기준(_baseSpd)을 유지(재계산으로 값이 바뀌었으면 ApplySpeed 가 그 값을 새 기준으로)
|
||
bool keepBase = !pcChanged && _baseSpd > 0f && _lastSetSpd > 0f;
|
||
_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;
|
||
if (!keepBase) { _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 && _baseSpd < 0f) return; // #976 표에 없는 무기 = 배율 1 · 앞 무기 배율을 덮었으면 기준으로 되돌린다
|
||
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;
|
||
}
|
||
}
|
||
}
|