Project_WL/Assets/WL/PartSkill/WLPartSkillBehaviors.cs

1025 lines
54 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.

// ─────────────────────────────────────────────────────────────────────────────
// WLPartSkillBehaviors.cs — 파츠 스킬 동작(FR 공용 스킬 14 · 변형 4 · 정령왕 · 무기 시너지 6) (PD 지시 #879 · 2026-10-16)
//
// 수치 = 레벨표 행(FR 원본 칸 이름 그대로: count · cool · hit_range · dmg_pct · effects …). 시너지면 FR 각성 단계(syn_level) 행.
// 거리 = FR 단위 × distanceScale(범위 확장 패시브 반영) · 속도 = × speedScale(기민한 바람 반영) · 지속 = × 끈질긴 사냥꾼 · 쿨 = × 명석한 두뇌 · 목걸이.
// 피해 = dmg_pct ÷ 100 × 등급 배율(불멸 1.2 · 신화 1.5) × damageScale × 스킬 피해(목걸이) — WLPartSkillCombat.Damage.
// 모바일: 오브젝트 상한(maxLiveObjects) · 적 목록은 러너 캐시(0.15 s) · 대상별 재타격 간격(perTargetHitSec).
// ─────────────────────────────────────────────────────────────────────────────
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using WL.Combat.Core;
namespace WL.PartSkill
{
public abstract class PSkill
{
public WLPartSkillRunner R;
public WLPartSlot slot;
public WLPartSkillDef def;
public WLPartLevelRow row, sub;
public bool syn;
public float mult = 1f, cd;
public int casts;
protected static WLPartSkillSettings S { get { return WLPartSkillData.S; } }
protected WLPartLoadout L { get { return R.L; } }
public virtual bool Persistent { get { return false; } }
public virtual float CoolSec { get { return row != null && row.cool > 0f ? row.cool * L.coolMul : 0f; } }
/// <summary>HUD 쿨 원(0 = 준비 · 1 = 막 씀).</summary>
public virtual float CoolFrac { get { float c = CoolSec; return c > 0f ? Mathf.Clamp01(cd / c) : 0f; } }
public virtual bool WantCast() { return R.AnyEnemyWithin(R.PcPos, S.combatRange); }
public virtual void Init() { }
public virtual void Cast() { }
public virtual void Tick(float dt) { }
public virtual void Dispose() { }
public virtual void OnKilled(Vector3 pos) { }
/// <summary>디버그 · 프로브 한 줄.</summary>
public virtual string State { get { return ""; } }
// ── 환산 ────────────────────────────────────────────────────────────
protected float Rad(float fr) { return Mathf.Max(S.minRadius, fr * S.distanceScale * L.rangeMul); } // 범위(범위 확장 반영)
protected float Len(float fr) { return fr * S.distanceScale; } // 거리(범위 확장 무관)
protected float Spd(float fr) { return fr * S.speedScale * L.projSpeedMul; }
protected float Dur(float sec) { return sec * L.durMul; }
protected float Dmg(float pct) { return pct / 100f * mult * S.damageScale * L.dmgMul; }
protected float Seq(WLPartLevelRow r) { return r == null || r.intMax <= 0f ? 0.12f : Random.Range(r.intMin, r.intMax) * S.intervalUnit; }
protected string Key { get { return def != null ? def.key : "?"; } }
protected Vector3 Ground(Vector3 p) { p.y = R.PcPos.y + S.fxGroundY; return p; }
protected void HitFx(Vector3 p) { if (!string.IsNullOrEmpty(def.fxHit)) WLPartSkillFx.Play(def.fxHit, p + Vector3.up * 0.6f, R.FxScale(def.fxHit, S.hitFxScale), 1.2f); }
protected void HitArea(Vector3 c, float r, float ratio, bool anim, System.Action<Actor> extra = null)
{
R.Collect(c, r, R.Tmp);
for (int i = 0; i < R.Tmp.Count; i++)
{
var a = R.Tmp[i];
if (WLPartSkillCombat.Damage(a, ratio, anim, Key)) { extra?.Invoke(a); }
}
}
protected void Knock(Actor a, Vector3 from)
{
if (row == null || !row.Has("Knock")) return;
WLPartSkillCombat.Knock(a, from, Len(row.Eff("Knock", "Distance", 0f)), Mathf.Max(1f, row.Eff("Knock", "Speed", 8f) * S.speedScale));
}
protected bool Recent(Dictionary<int, float> hitAt, Actor a, float gap)
{
int id = a.GetInstanceID(); float t;
if (hitAt.TryGetValue(id, out t) && Time.time - t < gap) return true;
hitAt[id] = Time.time; return false;
}
}
// ── 공용 이동체(칼날 · 영혼 · 도끼 · 검기 · 화살) ─────────────────────────
public sealed class PMover
{
public GameObject fx;
public Vector3 pos, dir;
public float speed, radius, life, travel, maxTravel, turnDeg, pendingAt = -1f;
public bool pierce, homing, alive = true;
public Actor target;
public readonly Dictionary<int, float> hitAt = new Dictionary<int, float>();
public int hits;
public void Place()
{
if (fx == null) return;
fx.transform.position = pos;
if (dir.sqrMagnitude > 0.0001f) fx.transform.rotation = Quaternion.LookRotation(dir, Vector3.up);
}
public void Kill(float fade) { if (!alive) return; alive = false; if (fx != null) WLPartSkillFx.Stop(fx, fade); fx = null; }
}
// 1 수호의 진 — 상시 오라 · 매초 피해 · 시너지 감속
public sealed class SkAura : PSkill
{
GameObject _fx; float _tick;
public override bool Persistent { get { return true; } }
public override float CoolFrac { get { return 0f; } }
float Radius { get { return Rad(row.hitRange); } }
public override void Tick(float dt)
{
bool on = R.InCombat;
if (on && _fx == null) _fx = WLPartSkillFx.Get(def.fxMain, Ground(R.PcPos), Quaternion.identity, R.FxScale(def.fxMain, Radius));
if (!on && _fx != null) { WLPartSkillFx.Stop(_fx, 0.6f); _fx = null; }
if (_fx != null) { _fx.transform.position = Ground(R.PcPos); }
if (!on) return;
_tick -= dt;
if (_tick > 0f) return;
_tick = Mathf.Max(0.2f, row.Eff("DotDamage", "Interval", 1f));
float slowR = row.Eff("Daze", "DecreaseMovePercentage", 0f) / 100f, slowS = row.Eff("Daze", "Duration", 0f);
HitArea(R.PcPos, Radius, Dmg(row.dmgPct), false, a => { if (slowR > 0f) WLPartSkillCombat.Slow(a, slowR, slowS); });
casts++;
}
public override void Dispose() { if (_fx != null) WLPartSkillFx.Stop(_fx, 0.3f); _fx = null; }
public override string State { get { return "r=" + Radius.ToString("F1") + " fx=" + (_fx != null); } }
}
// 2 대지 방벽 — 내 주변을 도는 방벽 · 투사체 막기 · 닿은 적 넉백 · 시너지 = 상시
public sealed class SkOrbitWall : PSkill
{
readonly List<GameObject> _walls = new List<GameObject>();
readonly Dictionary<int, float> _hitAt = new Dictionary<int, float>();
float _angle, _until, _blockTick;
bool Permanent { get { return row.duration < 0f; } }
public override float CoolSec { get { return Permanent ? 0f : base.CoolSec + Dur(row.duration); } }
public override bool WantCast() { return _walls.Count == 0 && base.WantCast(); }
float OrbitR { get { return Mathf.Max(0.8f, Len(row.Eff("RelativeLocation", "Z", 2.5f)) * L.rangeMul); } }
public override void Cast()
{
Clear();
int n = Mathf.Max(1, Mathf.RoundToInt(row.count));
if (!R.CanSpawn(n)) return;
float wallScale = def.v1 > 0f ? def.v1 : 0.45f;
for (int i = 0; i < n; i++) { var w = WLPartSkillFx.Get(def.fxMain, R.PcPos, Quaternion.identity, R.FxScale(def.fxMain, wallScale)); if (w != null) _walls.Add(w); else _walls.Add(null); }
_until = Permanent ? float.MaxValue : Time.time + Dur(row.duration);
R.Track(n);
casts++;
}
public override void Tick(float dt)
{
if (_walls.Count == 0) { if (Permanent && R.InCombat) Cast(); return; }
if (Time.time >= _until || (Permanent && !R.InCombat)) { Clear(); return; }
_angle += row.orbitSpeed * dt;
int n = _walls.Count;
float hitR = Rad(row.hitRange) + 0.2f;
bool block = row.Has("BlockProjectile");
_blockTick -= dt;
for (int i = 0; i < n; i++)
{
float a = (_angle + 360f * i / n) * Mathf.Deg2Rad;
var p = R.PcPos + new Vector3(Mathf.Cos(a), 0f, Mathf.Sin(a)) * OrbitR + Vector3.up * def.v2; // 높이 = 정의 v2(바위 = 바닥 0)
if (_walls[i] != null) _walls[i].transform.SetPositionAndRotation(p, Quaternion.LookRotation(new Vector3(-Mathf.Sin(a), 0f, Mathf.Cos(a))));
R.Collect(p, hitR, R.Tmp);
for (int k = 0; k < R.Tmp.Count; k++)
{
var e = R.Tmp[k];
if (Recent(_hitAt, e, S.perTargetHitSec)) continue;
if (WLPartSkillCombat.Damage(e, Dmg(row.dmgPct), true, Key)) { Knock(e, R.PcPos); HitFx(e.Get_position()); }
}
if (block && _blockTick <= 0f) WLPartSkillCombat.BlockProjectiles(p + Vector3.up * 0.9f, hitR + S.projectileBlockRadiusPad, def.fxHit); // 투사체 높이(가슴)
}
if (_blockTick <= 0f) _blockTick = 0.1f;
}
void Clear()
{
for (int i = 0; i < _walls.Count; i++) if (_walls[i] != null) WLPartSkillFx.Stop(_walls[i], 0.4f);
if (_walls.Count > 0) R.Untrack(_walls.Count);
_walls.Clear();
}
public override void Dispose() { Clear(); }
public override string State { get { return "walls=" + _walls.Count + " r=" + OrbitR.ToString("F1"); } }
}
// 3 수호 보호막(변형) — 공격 1회 흡수 · 깨지면 무적 · 쿨마다 다시
public sealed class SkShield : PSkill
{
GameObject _fx; bool _up; float _invUntil;
public override bool Persistent { get { return true; } }
public bool Absorb()
{
if (Time.time < _invUntil) return true;
if (!_up) return false;
_up = false;
if (_fx != null) { WLPartSkillFx.Stop(_fx, 0.2f); _fx = null; }
WLPartSkillFx.Play(def.fxHit, R.PcPos + Vector3.up, R.FxScale(def.fxHit, 1.2f), 1.2f);
_invUntil = Time.time + Dur(row.Eff("Invincible", "Duration", row.duration));
cd = CoolSec;
casts++;
return true;
}
public override void Tick(float dt)
{
if (!_up) { cd -= dt; if (cd <= 0f) { _up = true; cd = 0f; } }
if (_up && _fx == null && R.InCombat) _fx = WLPartSkillFx.Get(def.fxMain, R.PcPos + Vector3.up * 0.9f, Quaternion.identity, R.FxScale(def.fxMain, 1f));
if (_fx != null)
{
if (!R.InCombat) { WLPartSkillFx.Stop(_fx, 0.3f); _fx = null; }
else _fx.transform.position = R.PcPos + Vector3.up * 0.9f;
}
}
public override float CoolFrac { get { return _up ? 0f : base.CoolFrac; } }
public override void Dispose() { if (_fx != null) WLPartSkillFx.Stop(_fx, 0.2f); _fx = null; }
public override string State { get { return "up=" + _up + " inv=" + (Time.time < _invUntil); } }
}
// 4 회복 하트(변형) — 쿨마다 하트 · 밟으면 최대 체력 % 회복
public sealed class SkHealHeart : PSkill
{
readonly List<GameObject> _hearts = new List<GameObject>();
readonly List<float> _until = new List<float>();
public override void Cast()
{
if (!R.CanSpawn(1)) return;
float ang = Random.Range(0f, Mathf.PI * 2f), d = Random.Range(def.v2 > 0f ? def.v2 : 2f, def.v3 > 0f ? def.v3 : 3.5f);
var p = Ground(R.PcPos + new Vector3(Mathf.Cos(ang), 0f, Mathf.Sin(ang)) * d) + Vector3.up * 0.4f;
var go = WLPartSkillFx.Get(def.fxMain, p, Quaternion.identity, R.FxScale(def.fxMain, 0.6f));
_hearts.Add(go); _until.Add(Time.time + (def.v1 > 0f ? def.v1 : row.duration));
R.Track(1);
casts++;
}
public override void Tick(float dt)
{
for (int i = _hearts.Count - 1; i >= 0; i--)
{
var go = _hearts[i];
bool expired = Time.time >= _until[i];
bool picked = false;
if (go != null && !expired)
{
var hp = go.transform.position; hp.y = R.PcPos.y;
picked = (hp - R.PcPos).sqrMagnitude <= S.pickupRadius * S.pickupRadius;
}
if (!picked && !expired && go != null) continue;
if (picked)
{
var pc = WLPartSkillCombat.PC;
float rate = row.Eff("Heal", "Percent", row.dmgPct) / 100f * mult;
if (pc != null) pc.Heal(rate, true);
WLPartSkillFx.Play(def.fxHit, R.PcPos + Vector3.up * 0.2f, R.FxScale(def.fxHit, 1f), 1.5f);
}
if (go != null) WLPartSkillFx.Stop(go, 0.2f);
_hearts.RemoveAt(i); _until.RemoveAt(i); R.Untrack(1);
}
}
public override void Dispose() { foreach (var h in _hearts) if (h != null) WLPartSkillFx.Stop(h, 0.1f); if (_hearts.Count > 0) R.Untrack(_hearts.Count); _hearts.Clear(); _until.Clear(); }
public override string State { get { return "hearts=" + _hearts.Count; } }
}
// 5 충격파(변형) — 내 주변 폭발 + 넉백
public sealed class SkShockwave : PSkill
{
public override bool WantCast() { return R.AnyEnemyWithin(R.PcPos, Rad(row.hitRange) + 0.5f); }
public override void Cast()
{
float r = Rad(row.hitRange);
WLPartSkillFx.Play(def.fxMain, Ground(R.PcPos), R.FxScale(def.fxMain, r), 1.5f);
HitArea(R.PcPos, r, Dmg(row.dmgPct), true, a => Knock(a, R.PcPos));
casts++;
}
}
// 6 불의 정령 — 따라다니며 앞쪽 부채꼴 화염 숨결(하위 10411)
public sealed class SkSpiritFire : PSkill
{
GameObject _body; float _breathCd; Vector3 _pos; bool _placed;
public override bool Persistent { get { return true; } }
public override float CoolSec { get { return sub != null ? sub.cool * L.coolMul : 4f; } }
public override float CoolFrac { get { float c = CoolSec; return c > 0f ? Mathf.Clamp01(_breathCd / c) : 0f; } }
Vector3 Anchor { get { return R.PcPos + R.PcRight * -S.spiritFollowDist * 0.8f - R.PcFwd * S.spiritFollowDist * 0.5f + Vector3.up * S.spiritHeight; } }
public override void Tick(float dt)
{
if (!_placed) { _pos = Anchor; _placed = true; }
_pos = Vector3.Lerp(_pos, Anchor, 1f - Mathf.Exp(-6f * dt));
if (_body == null) _body = WLPartSkillFx.Get(def.fxMain, _pos, Quaternion.identity, R.FxScale(def.fxMain, 0.35f));
if (_body != null) _body.transform.position = _pos;
_breathCd -= dt;
if (_breathCd > 0f || sub == null) return;
float range = Rad(sub.hitRange);
var t = R.Nearest(_pos, range + 0.5f);
if (t == null) return;
_breathCd = CoolSec;
R.StartCoroutine(Breath(t.Get_position(), range));
casts++;
}
IEnumerator Breath(Vector3 at, float range)
{
var from = _pos; from.y = R.PcPos.y + 0.6f;
var dir = at - from; dir.y = 0f; if (dir.sqrMagnitude < 0.01f) dir = R.PcFwd;
dir.Normalize();
WLPartSkillFx.Play(def.fxHit, from, Quaternion.LookRotation(dir), R.FxScale(def.fxHit, range * 0.5f), Mathf.Max(1f, sub.duration + 0.5f));
int ticks = Mathf.Max(1, Mathf.RoundToInt(sub.Eff("DotDamage", "MaxHitCount", 1f)));
float gap = Mathf.Max(0.15f, sub.duration / ticks), half = sub.hitAngle > 0f ? sub.hitAngle * 0.5f : 60f;
for (int k = 0; k < ticks; k++)
{
R.Collect(from, range, R.Tmp2);
for (int i = 0; i < R.Tmp2.Count; i++)
{
var e = R.Tmp2[i]; var to = e.Get_position() - from; to.y = 0f;
if (to.sqrMagnitude > 0.01f && Vector3.Angle(dir, to) > half) continue;
WLPartSkillCombat.Damage(e, Dmg(sub.dmgPct), false, Key);
}
yield return new WaitForSeconds(gap);
}
}
public override void Dispose() { if (_body != null) WLPartSkillFx.Stop(_body, 0.3f); _body = null; }
public override string State { get { return "breathCd=" + _breathCd.ToString("F1"); } }
}
// 7 물의 정령 — 따라다니며 서릿발(하위 10511) · 감속
public sealed class SkSpiritWater : PSkill
{
GameObject _body; float _frostCd; Vector3 _pos; bool _placed;
public override bool Persistent { get { return true; } }
public override float CoolSec { get { return sub != null ? sub.cool * L.coolMul : 3f; } }
public override float CoolFrac { get { float c = CoolSec; return c > 0f ? Mathf.Clamp01(_frostCd / c) : 0f; } }
Vector3 Anchor { get { return R.PcPos + R.PcRight * S.spiritFollowDist * 0.8f - R.PcFwd * S.spiritFollowDist * 0.5f + Vector3.up * S.spiritHeight; } }
public override void Tick(float dt)
{
if (!_placed) { _pos = Anchor; _placed = true; }
_pos = Vector3.Lerp(_pos, Anchor, 1f - Mathf.Exp(-6f * dt));
if (_body == null) _body = WLPartSkillFx.Get(def.fxMain, _pos, Quaternion.identity, R.FxScale(def.fxMain, 0.35f));
if (_body != null) _body.transform.position = _pos;
_frostCd -= dt;
if (_frostCd > 0f || sub == null) return;
var t = R.Nearest(R.PcPos, S.combatRange * 0.7f);
if (t == null) return;
_frostCd = CoolSec;
float r = Rad(sub.hitRange) * 0.5f;
var c = t.Get_position();
WLPartSkillFx.Play(def.fxHit, Ground(c), R.FxScale(def.fxHit, r), 1.5f);
HitArea(c, r, Dmg(sub.dmgPct), true, a => WLPartSkillCombat.Slow(a, def.v1 > 0f ? def.v1 : 0.3f, def.v2 > 0f ? def.v2 : 1.5f));
casts++;
}
public override void Dispose() { if (_body != null) WLPartSkillFx.Stop(_body, 0.3f); _body = null; }
public override string State { get { return "frostCd=" + _frostCd.ToString("F1"); } }
}
// 8 정령왕(결합) — 운석(10582) + 눈보라(10583)
public sealed class SkSpiritKing : PSkill
{
GameObject _body; float _meteorCd = 1f, _blizCd = 3f; Vector3 _pos; bool _placed;
WLPartLevelRow _bliz;
public override bool Persistent { get { return true; } }
public override void Init() { _bliz = WLPartSkillData.Level("sub", def.subId, 1); }
public override float CoolSec { get { return row != null ? row.cool * L.coolMul : 5f; } }
public override float CoolFrac { get { float c = CoolSec; return c > 0f ? Mathf.Clamp01(_meteorCd / c) : 0f; } }
Vector3 Anchor { get { return R.PcPos - R.PcFwd * S.spiritKingFollowDist * 0.6f + Vector3.up * S.spiritKingHeight; } }
public override void Tick(float dt)
{
if (!_placed) { _pos = Anchor; _placed = true; }
_pos = Vector3.Lerp(_pos, Anchor, 1f - Mathf.Exp(-4f * dt));
if (_body == null) _body = WLPartSkillFx.Get(def.fxMain, _pos, Quaternion.identity, R.FxScale(def.fxMain, 0.6f));
if (_body != null) _body.transform.position = _pos;
_meteorCd -= dt; _blizCd -= dt;
float reach = Mathf.Min(S.combatRange, Len(row != null ? row.targetRange : 15f));
if (_meteorCd <= 0f && row != null)
{
var t = R.RandomEnemy(R.PcPos, reach);
if (t != null) { _meteorCd = CoolSec; R.StartCoroutine(Meteor(t.Get_position())); casts++; }
}
if (_blizCd <= 0f && _bliz != null)
{
Vector3 c;
if (R.DensestPoint(R.PcPos, reach, Rad(_bliz.hitRange), out c)) { _blizCd = _bliz.cool * L.coolMul; R.StartCoroutine(Blizzard(c)); casts++; }
}
}
IEnumerator Meteor(Vector3 at)
{
float r = Rad(row.hitRange);
WLPartSkillFx.Play(def.fxHit, Ground(at), R.FxScale(def.fxHit, r), 2.5f);
yield return new WaitForSeconds(0.6f);
HitArea(at, r, Dmg(row.dmgPct), true, a => WLPartSkillCombat.Stun(a, row.Eff("Stun", "Duration", 0f)));
}
IEnumerator Blizzard(Vector3 c)
{
float r = Rad(_bliz.hitRange), dur = Dur(_bliz.duration);
var fx = WLPartSkillFx.Get(def.fxExtra, Ground(c), Quaternion.identity, R.FxScale(def.fxExtra, r));
int ticks = Mathf.Max(1, Mathf.RoundToInt(_bliz.Eff("DotDamage", "MaxHitCount", dur)));
float gap = Mathf.Max(0.2f, _bliz.Eff("DotDamage", "Interval", 1f));
float slowR = _bliz.Eff("Daze", "DecreaseMovePercentage", 0f) / 100f, slowS = _bliz.Eff("Daze", "Duration", 1f);
for (int k = 0; k < ticks; k++)
{
HitArea(c, r, Dmg(_bliz.dmgPct), false, a => { if (slowR > 0f) WLPartSkillCombat.Slow(a, slowR, slowS); });
yield return new WaitForSeconds(gap);
}
if (fx != null) WLPartSkillFx.Stop(fx, 0.8f);
}
public override void Dispose() { if (_body != null) WLPartSkillFx.Stop(_body, 0.3f); _body = null; }
public override string State { get { return "meteor=" + _meteorCd.ToString("F1") + " bliz=" + _blizCd.ToString("F1"); } }
}
// 9 벼락 — 주변 적 머리 위로 차례로 낙하
public sealed class SkLightning : PSkill
{
public override void Cast() { R.StartCoroutine(Run()); casts++; }
IEnumerator Run()
{
int n = Mathf.Max(1, Mathf.RoundToInt(row.count));
float r = Rad(row.hitRange);
for (int i = 0; i < n; i++)
{
var t = R.RandomEnemy(R.PcPos, S.combatRange);
if (t != null)
{
var p = t.Get_position();
WLPartSkillFx.Play(def.fxMain, p, R.FxScale(def.fxMain, 1f), 1.2f);
WLPartSkillFx.Play(def.fxHit, Ground(p), R.FxScale(def.fxHit, r), 1.2f);
HitArea(p, r, Dmg(row.dmgPct), true);
}
yield return new WaitForSeconds(Seq(row));
}
}
}
// 10 뼈 영혼 — 쫓아가 닿으면 잠시 뒤 폭발
public sealed class SkBoneSoul : PSkill
{
readonly List<PMover> _souls = new List<PMover>();
public override void Cast()
{
int n = Mathf.Max(1, Mathf.RoundToInt(row.count));
if (!R.CanSpawn(n)) return;
for (int i = 0; i < n; i++)
{
float a = 360f * i / n * Mathf.Deg2Rad;
var m = new PMover
{
pos = R.PcPos + Vector3.up * 1.1f + new Vector3(Mathf.Cos(a), 0f, Mathf.Sin(a)) * 0.6f,
dir = new Vector3(Mathf.Cos(a), 0f, Mathf.Sin(a)), speed = Mathf.Max(1.2f, Spd(row.projSpeed)), radius = Rad(row.hitRange),
life = Time.time + Dur(row.duration), homing = true, turnDeg = 300f
};
m.target = R.RandomEnemy(R.PcPos, Mathf.Min(S.combatRange * 1.5f, Len(row.projTargetRange)));
m.fx = WLPartSkillFx.Get(def.fxMain, m.pos, Quaternion.identity, R.FxScale(def.fxMain, 0.35f));
_souls.Add(m);
}
R.Track(n);
casts++;
}
public override void Tick(float dt)
{
for (int i = _souls.Count - 1; i >= 0; i--)
{
var m = _souls[i];
if (m.pendingAt > 0f)
{
if (Time.time >= m.pendingAt) { Explode(m.pos); m.Kill(0.1f); }
}
else if (Time.time >= m.life) m.Kill(0.3f);
else
{
if (!WLPartSkillCombat.Valid(m.target)) m.target = R.Nearest(m.pos, S.combatRange * 1.5f);
if (m.target != null)
{
var to = m.target.Get_position() + Vector3.up * 0.8f - m.pos;
m.dir = Vector3.RotateTowards(m.dir, to.normalized, m.turnDeg * Mathf.Deg2Rad * dt, 0f);
if (to.magnitude <= m.radius + 0.3f) m.pendingAt = Time.time + row.Eff("PendingHit", "PendingTime", 0.5f);
}
m.pos += m.dir * m.speed * dt;
m.Place();
}
if (!m.alive) { _souls.RemoveAt(i); R.Untrack(1); }
}
}
void Explode(Vector3 p)
{
float r = Rad(row.splash > 0f ? row.splash : row.hitRange);
WLPartSkillFx.Play(def.fxHit, p, R.FxScale(def.fxHit, r), 1.2f);
HitArea(p, r, Dmg(row.dmgPct), true);
}
public override void Dispose() { foreach (var m in _souls) m.Kill(0.1f); if (_souls.Count > 0) R.Untrack(_souls.Count); _souls.Clear(); }
public override string State { get { return "souls=" + _souls.Count; } }
}
// 11 마력의 칼날 — 관통 칼날 여러 개 · 시너지 = 하나로 합쳐 끝까지 추적(상시)
public sealed class SkBlades : PSkill
{
readonly List<PMover> _blades = new List<PMover>();
bool Chase { get { return row.duration < 0f; } }
public override bool Persistent { get { return Chase; } }
public override float CoolFrac { get { return Chase ? 0f : base.CoolFrac; } }
public override void Cast()
{
int n = Mathf.Max(1, Mathf.RoundToInt(row.count));
if (!R.CanSpawn(n)) return;
var t = R.Nearest(R.PcPos, S.combatRange);
var baseDir = t != null ? (t.Get_position() - R.PcPos) : R.PcFwd; baseDir.y = 0f; baseDir.Normalize();
float spread = row.projRandAngle > 0f ? row.projRandAngle : 60f;
for (int i = 0; i < n; i++)
{
float ang = n == 1 ? 0f : Mathf.Lerp(-spread * 0.5f, spread * 0.5f, i / (float)(n - 1));
var d = Quaternion.Euler(0f, ang, 0f) * baseDir;
var m = new PMover { pos = R.PcPos + Vector3.up * 1f, dir = d, speed = Mathf.Max(3f, Spd(row.projSpeed)), radius = Mathf.Max(0.5f, Rad(row.hitRange)), pierce = true, maxTravel = S.projectileMaxTravel, life = Time.time + Dur(row.duration) };
m.fx = WLPartSkillFx.Get(def.fxMain, m.pos, Quaternion.LookRotation(d), R.FxScale(def.fxMain, 0.6f));
_blades.Add(m);
}
R.Track(n);
casts++;
}
public override void Tick(float dt)
{
if (Chase && _blades.Count == 0 && R.InCombat) SpawnChaser();
if (Chase && !R.InCombat && _blades.Count > 0) { Dispose(); return; }
for (int i = _blades.Count - 1; i >= 0; i--)
{
var m = _blades[i];
if (Chase)
{
if (!WLPartSkillCombat.Valid(m.target) || m.hitAt.ContainsKey(m.target.GetInstanceID()) && Time.time - m.hitAt[m.target.GetInstanceID()] < 0.05f)
m.target = R.NearestNotRecent(m.pos, S.combatRange * 1.4f, m.hitAt, S.perTargetHitSec);
if (m.target != null)
{
var to = m.target.Get_position() + Vector3.up * 0.8f - m.pos;
m.dir = Vector3.RotateTowards(m.dir, to.normalized, (def.v1 > 0f ? def.v1 : 360f) * Mathf.Deg2Rad * dt, 0f);
}
else
{ // 대상이 없으면 내 주변을 맴돈다
var back = R.PcPos + Vector3.up - m.pos;
if (back.magnitude > 2.5f) m.dir = Vector3.RotateTowards(m.dir, back.normalized, 4f * dt, 0f);
}
}
m.pos += m.dir * m.speed * dt; m.travel += m.speed * dt;
m.Place();
R.Collect(m.pos, m.radius, R.Tmp);
for (int k = 0; k < R.Tmp.Count; k++)
{
var e = R.Tmp[k];
if (Recent(m.hitAt, e, Chase ? S.perTargetHitSec : 999f)) continue;
if (WLPartSkillCombat.Damage(e, Dmg(row.dmgPct), true, Key)) HitFx(e.Get_position());
}
if (!Chase && (m.travel >= m.maxTravel || Time.time >= m.life)) m.Kill(0.2f);
if (!m.alive) { _blades.RemoveAt(i); R.Untrack(1); }
}
}
void SpawnChaser()
{
if (!R.CanSpawn(1)) return;
var m = new PMover { pos = R.PcPos + Vector3.up, dir = R.PcFwd, speed = Mathf.Max(4f, Spd(row.projSpeed)), radius = Mathf.Max(0.6f, Rad(row.hitRange)), pierce = true };
m.fx = WLPartSkillFx.Get(def.fxMain, m.pos, Quaternion.LookRotation(m.dir), R.FxScale(def.fxMain, 0.9f));
_blades.Add(m); R.Track(1);
casts++;
}
public override void Dispose() { foreach (var m in _blades) m.Kill(0.1f); if (_blades.Count > 0) R.Untrack(_blades.Count); _blades.Clear(); }
public override string State { get { return "blades=" + _blades.Count + (Chase ? " chase" : ""); } }
}
// 12 화염 장막(변형) — 내 자리에 불 고리 · 매초 피해 · 4단계부터 감속
public sealed class SkFireCurtain : PSkill
{
public override void Cast() { R.StartCoroutine(Run(R.PcPos)); casts++; }
IEnumerator Run(Vector3 c)
{
float r = Rad(row.hitRange), dur = Dur(row.duration);
var fx = WLPartSkillFx.Get(def.fxMain, Ground(c), Quaternion.identity, R.FxScale(def.fxMain, r));
float slowR = row.Eff("Daze", "DecreaseMovePercentage", 0f) / 100f, slowS = row.Eff("Daze", "Duration", 0f);
float end = Time.time + dur;
while (Time.time < end)
{
HitArea(c, r, Dmg(row.dmgPct), false, a => { if (slowR > 0f) WLPartSkillCombat.Slow(a, slowR, slowS); });
yield return new WaitForSeconds(1f);
}
if (fx != null) WLPartSkillFx.Stop(fx, 0.6f);
}
}
// 13 회오리바람 — 떠돌며 닿은 적 띄움(짧은 기절) · 감속 · 시너지 = 투사체도 빨아들인다
public sealed class SkTornado : PSkill
{
readonly List<PMover> _tw = new List<PMover>();
public override void Cast()
{
int n = Mathf.Max(1, Mathf.RoundToInt(row.count));
if (!R.CanSpawn(n)) return;
for (int i = 0; i < n; i++)
{
float a = Random.Range(0f, Mathf.PI * 2f);
var m = new PMover { pos = Ground(R.PcPos), dir = new Vector3(Mathf.Cos(a), 0f, Mathf.Sin(a)), speed = Mathf.Max(1f, Spd(row.projSpeed)), radius = Rad(row.hitRange), life = Time.time + Dur(row.duration), turnDeg = Time.time };
m.fx = WLPartSkillFx.Get(def.fxMain, m.pos, Quaternion.identity, R.FxScale(def.fxMain, m.radius));
_tw.Add(m);
}
R.Track(n);
casts++;
}
public override void Tick(float dt)
{
bool sucks = row.Has("EnableProjectileTornado");
for (int i = _tw.Count - 1; i >= 0; i--)
{
var m = _tw[i];
if (Time.time >= m.life) { m.Kill(0.5f); _tw.RemoveAt(i); R.Untrack(1); continue; }
if (Time.time - m.turnDeg > 1f)
{ // 1 초마다 방향 바꿈 — 적 쪽 70 % · 무작위 30 %
m.turnDeg = Time.time;
var t = R.Nearest(m.pos, S.combatRange);
var want = t != null && Random.value < 0.7f ? (t.Get_position() - m.pos) : new Vector3(Random.Range(-1f, 1f), 0f, Random.Range(-1f, 1f));
want.y = 0f; if (want.sqrMagnitude > 0.01f) m.dir = want.normalized;
}
var off = m.pos - R.PcPos; off.y = 0f;
if (off.magnitude > S.combatRange) m.dir = -off.normalized;
m.pos += m.dir * m.speed * dt; m.pos = Ground(m.pos);
m.Place();
R.Collect(m.pos, m.radius, R.Tmp);
for (int k = 0; k < R.Tmp.Count; k++)
{
var e = R.Tmp[k];
if (Recent(m.hitAt, e, S.perTargetHitSec * 2f)) continue;
if (!WLPartSkillCombat.Damage(e, Dmg(row.dmgPct), false, Key)) continue;
float sr = row.Eff("Daze", "DecreaseMovePercentage", (def.v1 > 0f ? def.v1 : 0.3f) * 100f) / 100f;
WLPartSkillCombat.Slow(e, sr, row.Eff("Daze", "Duration", def.v2 > 0f ? def.v2 : 2.5f));
if (row.Has("Bounce")) WLPartSkillCombat.Stun(e, def.v3 > 0f ? def.v3 : 0.4f);
}
if (sucks) WLPartSkillCombat.BlockProjectiles(m.pos + Vector3.up, m.radius + S.projectileBlockRadiusPad, def.fxHit);
}
}
public override void Dispose() { foreach (var m in _tw) m.Kill(0.2f); if (_tw.Count > 0) R.Untrack(_tw.Count); _tw.Clear(); }
public override string State { get { return "tornado=" + _tw.Count; } }
}
// 14 역병 — 병을 던져 독 장판 · 시너지 = 장판 위 감속
public sealed class SkPlague : PSkill
{
public override void Cast()
{
int n = Mathf.Max(1, Mathf.RoundToInt(row.count));
for (int i = 0; i < n; i++)
{
Vector3 p;
var t = R.RandomEnemy(R.PcPos, Mathf.Min(S.combatRange, Len(row.spawnMax) + 2f));
if (t != null) p = t.Get_position();
else { float a = Random.Range(0f, Mathf.PI * 2f), d = Random.Range(Len(row.spawnMin), Len(row.spawnMax)); p = R.PcPos + new Vector3(Mathf.Cos(a), 0f, Mathf.Sin(a)) * d; }
R.StartCoroutine(Throw(Ground(p), i * Seq(row)));
}
casts++;
}
IEnumerator Throw(Vector3 at, float delay)
{
if (delay > 0f) yield return new WaitForSeconds(delay);
float fl = def.v1 > 0f ? def.v1 : 0.5f, t0 = Time.time;
var from = R.PcPos + Vector3.up * 1.2f;
var b = WLPartSkillFx.Get(def.fxExtra, from, Quaternion.identity, R.FxScale(def.fxExtra, 0.3f));
while (Time.time - t0 < fl)
{
float k = (Time.time - t0) / fl;
if (b != null) b.transform.position = Vector3.Lerp(from, at, k) + Vector3.up * Mathf.Sin(k * Mathf.PI) * 1.5f;
yield return null;
}
if (b != null) WLPartSkillFx.Stop(b, 0.1f);
float r = Rad(row.hitRange), dur = Dur(row.duration);
WLPartSkillFx.Play(def.fxHit, at, R.FxScale(def.fxHit, r * 0.6f), 1.2f);
var pool = WLPartSkillFx.Get(def.fxMain, at, Quaternion.identity, R.FxScale(def.fxMain, r));
int ticks = Mathf.Max(1, Mathf.RoundToInt(row.Eff("DotDamage", "MaxHitCount", dur)));
float gap = Mathf.Max(0.2f, row.Eff("DotDamage", "Interval", 1f));
for (int k = 0; k < ticks; k++)
{
HitArea(at, r, Dmg(row.dmgPct), false, a => { if (syn) WLPartSkillCombat.Slow(a, def.v2 > 0f ? def.v2 : 0.3f, def.v3 > 0f ? def.v3 : 1.5f); });
yield return new WaitForSeconds(gap);
}
if (pool != null) WLPartSkillFx.Stop(pool, 0.6f);
}
}
// 15 가시촉수 — 밀집 지점에 촉수 · 끌어당긴 뒤 내리침 · 시너지 = 출혈
public sealed class SkTentacle : PSkill
{
public override void Cast()
{
Vector3 c;
float r = Rad(row.hitRange);
if (!R.DensestPoint(R.PcPos, Mathf.Max(Len(row.targetRange), S.combatRange * 0.6f), r, out c)) return;
R.StartCoroutine(Run(c, r));
casts++;
}
IEnumerator Run(Vector3 c, float r)
{
var fx = WLPartSkillFx.Get(def.fxMain, Ground(c), Quaternion.identity, R.FxScale(def.fxMain, r));
float grow = Mathf.Max(0.3f, row.duration * 0.5f), end = Time.time + grow, pull = def.v1 > 0f ? def.v1 : 4f;
while (Time.time < end)
{
R.Collect(c, r * 1.3f, R.Tmp2);
for (int i = 0; i < R.Tmp2.Count; i++) WLPartSkillCombat.Pull(R.Tmp2[i], c, pull, Time.deltaTime);
yield return null;
}
WLPartSkillFx.Play(def.fxHit, Ground(c), R.FxScale(def.fxHit, r), 1.2f);
float bleedPct = row.Eff("DotDamageAddiction", "DamagePercentage", 0f) / 100f;
float bleedSec = row.Eff("DotDamageAddiction", "MaxHitCount", 0f) * row.Eff("DotDamageAddiction", "Interval", 1f);
HitArea(c, r, Dmg(row.dmgPct), true, a => { if (bleedPct > 0f) WLPartSkillCombat.Bleed(a, bleedPct * mult, bleedSec); });
if (fx != null) WLPartSkillFx.Stop(fx, 0.5f);
}
}
// 16 휘감는 덩굴 — 덩굴 지대 · 틱마다 확률 속박(하위 11301 · 시너지 = 3단계 = 출혈)
public sealed class SkVine : PSkill
{
public override void Cast()
{
Vector3 c;
float r = Rad(row.hitRange);
if (!R.DensestPoint(R.PcPos, Mathf.Max(Len(row.spawnMax) + 1f, S.combatRange * 0.6f), r, out c)) return;
R.StartCoroutine(Run(c, r));
casts++;
}
IEnumerator Run(Vector3 c, float r)
{
var fx = WLPartSkillFx.Get(def.fxMain, Ground(c), Quaternion.identity, R.FxScale(def.fxMain, r));
int ticks = Mathf.Max(1, Mathf.RoundToInt(row.Eff("DotDamage", "MaxHitCount", 3f)));
float gap = Mathf.Max(0.2f, row.Eff("DotDamage", "Interval", 0.8f));
float chance = row.Eff("InvokeSkillOnHit", "Percentage", 0f) / 100f;
var root = WLPartSkillData.Level("sub", def.subId, Mathf.RoundToInt(row.Eff("InvokeSkillOnHit", "Level", 1f)));
for (int k = 0; k < ticks; k++)
{
HitArea(c, r, Dmg(row.dmgPct), false, a =>
{
if (root == null || Random.value >= chance) return;
WLPartSkillCombat.Stun(a, root.Eff("Stun", "Duration", 2f));
float bp = root.Eff("DotDamageAddiction", "DamagePercentage", 0f) / 100f;
if (bp > 0f) WLPartSkillCombat.Bleed(a, bp * mult, root.Eff("DotDamageAddiction", "MaxHitCount", 0f) * root.Eff("DotDamageAddiction", "Interval", 1f));
HitFx(a.Get_position());
});
yield return new WaitForSeconds(gap);
}
if (fx != null) WLPartSkillFx.Stop(fx, 0.5f);
}
}
// 17 거인의 손 — 적 위로 내리쳐 기절 · 투사체 막기
public sealed class SkGiantHand : PSkill
{
public override void Cast()
{
Vector3 c;
float r = Rad(row.hitRange);
if (!R.DensestPoint(R.PcPos, S.combatRange, r, out c)) return;
R.StartCoroutine(Run(c, r));
casts++;
}
IEnumerator Run(Vector3 c, float r)
{
WLPartSkillFx.Play(def.fxMain, c + Vector3.up * 0.4f, R.FxScale(def.fxMain, r * 1.6f), 1.5f);
yield return new WaitForSeconds(def.v1 > 0f ? def.v1 : 0.35f);
WLPartSkillFx.Play(def.fxHit, Ground(c), R.FxScale(def.fxHit, r), 1.5f);
HitArea(c, r, Dmg(row.dmgPct), true, a => WLPartSkillCombat.Stun(a, row.Eff("Stun", "Duration", 0.5f)));
if (row.Has("BlockProjectile")) WLPartSkillCombat.BlockProjectiles(c + Vector3.up, r + S.projectileBlockRadiusPad, null);
}
}
// 18 시체 폭발 — 최근 처치 자리 폭발 · 시너지 = 넉백
public sealed class SkCorpseBlast : PSkill
{
readonly List<Vector3> _corpse = new List<Vector3>();
readonly List<float> _at = new List<float>();
public override void OnKilled(Vector3 pos) { _corpse.Add(pos); _at.Add(Time.time); if (_corpse.Count > 24) { _corpse.RemoveAt(0); _at.RemoveAt(0); } }
public override bool WantCast()
{
for (int i = _at.Count - 1; i >= 0; i--) if (Time.time - _at[i] > S.corpseMemorySec) { _at.RemoveAt(i); _corpse.RemoveAt(i); }
return _corpse.Count > 0;
}
public override void Cast()
{
int n = Mathf.Min(_corpse.Count, Mathf.Max(1, Mathf.RoundToInt(row.count)));
float r = Rad(row.hitRange);
for (int i = 0; i < n; i++)
{
int idx = _corpse.Count - 1;
var p = _corpse[idx]; _corpse.RemoveAt(idx); _at.RemoveAt(idx);
WLPartSkillFx.Play(def.fxMain, Ground(p), R.FxScale(def.fxMain, r), 1.6f);
var at = p;
HitArea(p, r, Dmg(row.dmgPct), true, a => Knock(a, at));
}
casts++;
}
public override string State { get { return "corpses=" + _corpse.Count; } }
}
// 19 회전 도끼(대검) — 내 주변을 튕기며 도는 도끼 · 시너지 = 작은 도끼 난사(하위 10601)
public sealed class SkSpinAxe : PSkill
{
PMover _axe;
readonly List<PMover> _small = new List<PMover>();
float _smallCd, _spin;
WLPartLevelRow _invoke;
public override bool Persistent { get { return true; } }
public override float CoolFrac { get { return 0f; } }
public override void Init() { _invoke = row.Has("InvokeSkill") ? WLPartSkillData.Level("sub", def.subId, Mathf.RoundToInt(row.Eff("InvokeSkill", "Level", 1f))) : null; }
float Bound { get { return (def.v1 > 0f ? def.v1 : 5f) * L.rangeMul; } }
public override void Tick(float dt)
{
if (!R.InCombat) { Dispose(); return; }
if (_axe == null)
{
if (!R.CanSpawn(1)) return;
_axe = new PMover { pos = R.PcPos + Vector3.up * 0.8f, dir = R.PcFwd, speed = Mathf.Max(2f, Spd(row.projSpeed)), radius = Rad(row.hitRange) };
_axe.fx = WLPartSkillFx.Get(def.fxMain, _axe.pos, Quaternion.identity, R.FxScale(def.fxMain, _axe.radius));
R.Track(1);
casts++;
}
var m = _axe;
var off = m.pos - R.PcPos; off.y = 0f;
if (off.magnitude > Bound)
{ // 경계에 닿으면 내 쪽으로 튕긴다(±40° 흔들림)
m.dir = Quaternion.Euler(0f, Random.Range(-40f, 40f), 0f) * (-off.normalized);
}
m.pos += m.dir * m.speed * dt; m.pos.y = R.PcPos.y + 0.8f;
_spin += S.axeSpinDegPerSec * dt;
if (m.fx != null) m.fx.transform.SetPositionAndRotation(m.pos, Quaternion.Euler(0f, _spin, 0f) * Quaternion.Euler(S.axeTiltDeg, 0f, 0f)); // 눕혀서 원반처럼 회전
R.Collect(m.pos, m.radius, R.Tmp);
for (int k = 0; k < R.Tmp.Count; k++)
{
var e = R.Tmp[k];
if (Recent(m.hitAt, e, S.perTargetHitSec)) continue;
if (WLPartSkillCombat.Damage(e, Dmg(row.dmgPct), true, Key)) { Knock(e, m.pos); HitFx(e.Get_position()); }
}
// 시너지 — 작은 도끼(InvokeSkill CoolTimeMin~Max 마다)
if (_invoke != null)
{
_smallCd -= dt;
if (_smallCd <= 0f && R.CanSpawn(1))
{
_smallCd = Random.Range(row.Eff("InvokeSkill", "CoolTimeMin", 0.3f), row.Eff("InvokeSkill", "CoolTimeMax", 0.8f));
float a = Random.Range(0f, 360f);
var d = Quaternion.Euler(0f, a, 0f) * Vector3.forward;
var s = new PMover { pos = m.pos, dir = d, speed = Mathf.Max(3f, Spd(_invoke.projSpeed)), radius = Mathf.Max(0.5f, Rad(_invoke.hitRange)), pierce = true, maxTravel = Mathf.Min(S.projectileMaxTravel, 8f) };
s.fx = WLPartSkillFx.Get(def.fxExtra, s.pos, Quaternion.LookRotation(d), R.FxScale(def.fxExtra, 0.5f));
_small.Add(s); R.Track(1);
}
for (int i = _small.Count - 1; i >= 0; i--)
{
var s = _small[i];
s.pos += s.dir * s.speed * dt; s.travel += s.speed * dt;
if (s.fx != null) s.fx.transform.SetPositionAndRotation(s.pos, Quaternion.Euler(0f, _spin * 1.3f, 0f) * Quaternion.Euler(S.axeTiltDeg, 0f, 0f));
R.Collect(s.pos, s.radius, R.Tmp);
for (int k = 0; k < R.Tmp.Count; k++)
{
var e = R.Tmp[k];
if (Recent(s.hitAt, e, 999f)) continue;
WLPartSkillCombat.Damage(e, Dmg(_invoke.dmgPct), true, Key);
}
if (s.travel >= s.maxTravel) { s.Kill(0.1f); _small.RemoveAt(i); R.Untrack(1); }
}
}
}
public override void Dispose()
{
if (_axe != null) { _axe.Kill(0.2f); _axe = null; R.Untrack(1); }
foreach (var s in _small) s.Kill(0.1f);
if (_small.Count > 0) R.Untrack(_small.Count);
_small.Clear();
}
public override string State { get { return "axe=" + (_axe != null) + " small=" + _small.Count; } }
}
// ── 무기 시너지(기본 공격 타격에 붙는 효과) ─────────────────────────────
public sealed class SkWeaponSyn : PSkill
{
int _hitCount; float _last = -9f;
readonly List<PMover> _shots = new List<PMover>();
public override bool Persistent { get { return true; } }
public override float CoolFrac { get { return 0f; } }
public void OnBasicHit(Actor victim)
{
if (!syn || victim == null) return;
float gap = def.v3 > 0f ? def.v3 : 0.1f;
if (Time.time - _last < gap) return;
_hitCount++;
int every = Mathf.Max(1, Mathf.RoundToInt(def.v2 > 0f ? def.v2 : 1f));
if (_hitCount % every != 0) return;
if (def.v1 > 0f && def.v1 < 1f && Random.value > def.v1) return;
_last = Time.time;
casts++;
switch (def.behavior)
{
case "venom":
WLPartSkillCombat.Poison(victim, row.Eff("DotDamageAddiction", "DamagePercentage", 10f) / 100f * mult, row.Eff("DotDamageAddiction", "MaxHitCount", 3f) * row.Eff("DotDamageAddiction", "Interval", 1f));
break;
case "bleed":
WLPartSkillCombat.Bleed(victim, row.Eff("DotDamageAddiction", "DamagePercentage", 3f) / 100f * mult, row.Eff("DotDamageAddiction", "MaxHitCount", 7f) * row.Eff("DotDamageAddiction", "Interval", 1f));
break;
case "quake":
{
float r = Rad(row.hitRange);
WLPartSkillFx.Play(def.fxMain, Ground(R.PcPos), R.FxScale(def.fxMain, r), 1.2f);
HitArea(R.PcPos, r, Dmg(row.dmgPct), true);
}
break;
case "quake_clone":
{ // #881 PD 「플레이어가 아니라 분신이 나와서 공격」 — 분신이 대상 옆에서 내려찍고 그 자리에 360° 파동
float r = Rad(row.hitRange);
bool ok = WLPartSkillClone.Spawn(victim, SlamState(), at =>
{
WLPartSkillFx.Play(def.fxMain, Ground(at), R.FxScale(def.fxMain, r), 1.2f);
HitArea(at, r, Dmg(row.dmgPct), true);
});
if (!ok) casts--; // 분신이 다 쓰이는 중이면 이번 타격은 건너뛴다(쿨 = v3)
}
break;
case "bomb":
R.StartCoroutine(Bomb(victim.Get_position()));
break;
case "hm_skill": // #883 PD 「A3 비수의 공격은 힘민지의 단검 투척으로 대체」 — 힘민지 스킬 투사체를 시전 동작 없이 바로 쏜다
R.StartCoroutine(HmSkill(victim));
break;
case "sword_beam":
Shoot(R.PcFwd, false);
break;
case "homing":
Shoot(R.PcFwd, true);
break;
}
}
public static int HmShots;
public static string HmLast = "";
/// <summary>#883 힘민지 스킬(def.hmSkill) 투사체 1회 — 다음 프레임에(타격 콜백 밖) InGameInfo.Shoot_Effect. 피해 = 힘민지 스킬 공식 그대로.</summary>
IEnumerator HmSkill(Actor victim)
{
yield return null;
var pc = WLPartSkillCombat.PC;
var data = def.hmSkill > 0 && table_skilllist.Ins != null ? table_skilllist.Ins.Get_Data_orNull(def.hmSkill) : null;
if (pc == null || data == null || InGameInfo.Ins == null) { HmLast = "없음 " + def.hmSkill; yield break; }
Actor target = victim != null && !victim.IsDead() ? victim : R.Nearest(R.PcPos, S.combatRange);
if (target == null) yield break;
var ptd = new ProjectileData { shooter = pc, target = target, m_SkillListTableData = data };
InGameInfo.Ins.Shoot_Effect(ptd);
HmShots++; HmLast = data.e_Skill + " → " + target.name;
}
/// <summary>장착 무기의 내려찍기 모션 — 공격 표에서 Area 단계(없으면 마지막 단계)의 anim_state.</summary>
string SlamState()
{
var w = slot != null ? slot.item : null;
var steps = w != null && w.itemJson != null ? WL.Combat.Weapon.WLWeaponAttackTable.Steps(w.itemJson.GroupNo) : null;
if (steps == null || steps.Count == 0) return "";
for (int i = steps.Count - 1; i >= 0; i--) if (steps[i].hitType == "Area") return steps[i].animState;
return steps[steps.Count - 1].animState;
}
IEnumerator Bomb(Vector3 at)
{
var from = R.PcPos + Vector3.up * 1.2f; float fl = S.bombFlightSec, t0 = Time.time;
var b = WLPartSkillFx.Get(def.fxMain, from, Quaternion.identity, R.FxScale(def.fxMain, 0.35f));
while (Time.time - t0 < fl)
{
float k = (Time.time - t0) / fl;
if (b != null) b.transform.position = Vector3.Lerp(from, at, k) + Vector3.up * Mathf.Sin(k * Mathf.PI) * 1.2f;
yield return null;
}
if (b != null) WLPartSkillFx.Stop(b, 0.1f);
float r = Rad(row.hitRange);
WLPartSkillFx.Play(def.fxHit, Ground(at), R.FxScale(def.fxHit, r), 1.2f);
HitArea(at, r, Dmg(row.dmgPct), true);
}
void Shoot(Vector3 dir, bool homing)
{
if (!R.CanSpawn(1)) return;
dir.y = 0f; if (dir.sqrMagnitude < 0.01f) dir = Vector3.forward; dir.Normalize();
var m = new PMover { pos = R.PcPos + Vector3.up + dir * 0.5f, dir = dir, speed = Mathf.Max(4f, Spd(row.projSpeed)), radius = Mathf.Max(0.5f, Rad(row.hitRange) * (homing ? 1f : 0.5f)), pierce = true, homing = homing, maxTravel = S.projectileMaxTravel, turnDeg = 540f };
if (homing) m.target = R.Nearest(m.pos, S.combatRange * 1.4f);
m.fx = WLPartSkillFx.Get(def.fxMain, m.pos, Quaternion.LookRotation(dir), R.FxScale(def.fxMain, homing ? 0.5f : 0.9f));
_shots.Add(m); R.Track(1);
}
public override void Tick(float dt)
{
for (int i = _shots.Count - 1; i >= 0; i--)
{
var m = _shots[i];
if (m.homing)
{
if (!WLPartSkillCombat.Valid(m.target)) m.target = R.NearestNotRecent(m.pos, S.combatRange * 1.4f, m.hitAt, 999f);
if (m.target != null) m.dir = Vector3.RotateTowards(m.dir, (m.target.Get_position() + Vector3.up * 0.8f - m.pos).normalized, m.turnDeg * Mathf.Deg2Rad * dt, 0f);
}
m.pos += m.dir * m.speed * dt; m.travel += m.speed * dt;
m.Place();
R.Collect(m.pos, m.radius, R.Tmp);
for (int k = 0; k < R.Tmp.Count; k++)
{
var e = R.Tmp[k];
if (Recent(m.hitAt, e, 999f)) continue;
if (WLPartSkillCombat.Damage(e, Dmg(row.dmgPct), true, Key)) { HitFx(e.Get_position()); if (m.homing) m.target = null; }
}
if (m.travel >= m.maxTravel) m.Kill(0.15f);
if (!m.alive) { _shots.RemoveAt(i); R.Untrack(1); }
}
}
public override void Dispose() { foreach (var m in _shots) m.Kill(0.1f); if (_shots.Count > 0) R.Untrack(_shots.Count); _shots.Clear(); }
public override string State { get { return "syn=" + syn + " hits=" + _hitCount + " shots=" + _shots.Count; } }
}
public static class PSkillFactory
{
public static PSkill Create(string behavior)
{
switch (behavior)
{
case "aura": return new SkAura();
case "orbit_wall": return new SkOrbitWall();
case "shield": return new SkShield();
case "heal_heart": return new SkHealHeart();
case "shockwave": return new SkShockwave();
case "spirit_fire": return new SkSpiritFire();
case "spirit_water": return new SkSpiritWater();
case "spirit_king": return new SkSpiritKing();
case "lightning": return new SkLightning();
case "bone_soul": return new SkBoneSoul();
case "blades": return new SkBlades();
case "fire_curtain": return new SkFireCurtain();
case "tornado": return new SkTornado();
case "plague": return new SkPlague();
case "tentacle": return new SkTentacle();
case "vine": return new SkVine();
case "giant_hand": return new SkGiantHand();
case "corpse_blast": return new SkCorpseBlast();
case "spin_axe": return new SkSpinAxe();
case "hammer_quake": return new SkHammerQuake(); // #911 해머 각성 대지 분쇄(WLHammerQuake.cs)
case "venom": case "bleed": case "quake": case "quake_clone": case "bomb": case "sword_beam": case "homing": case "hm_skill": return new SkWeaponSyn();
}
return null;
}
}
}