// ───────────────────────────────────────────────────────────────────────────── // 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; } } /// HUD 쿨 원(0 = 준비 · 1 = 막 씀). 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) { } /// 디버그 · 프로브 한 줄. 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 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 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 hitAt = new Dictionary(); 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 _walls = new List(); readonly Dictionary _hitAt = new Dictionary(); 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 _hearts = new List(); readonly List _until = new List(); 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 _souls = new List(); 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 _blades = new List(); 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 _tw = new List(); 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 _corpse = new List(); readonly List _at = new List(); 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 _small = new List(); 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 _shots = new List(); 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 = ""; /// #883 힘민지 스킬(def.hmSkill) 투사체 1회 — 다음 프레임에(타격 콜백 밖) InGameInfo.Shoot_Effect. 피해 = 힘민지 스킬 공식 그대로. 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; } /// 장착 무기의 내려찍기 모션 — 공격 표에서 Area 단계(없으면 마지막 단계)의 anim_state. 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; } } }