1025 lines
54 KiB
C#
1025 lines
54 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// 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;
|
||
}
|
||
}
|
||
}
|