ThreeKingdomsSlingshot/Assets/Scripts/Runtime/ActionResolver.Skills.cs

384 lines
20 KiB
C#
Raw Permalink 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.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace TKS
{
/// <summary>
/// 스킬 특수 사격(2026-11-10 #1112 · 효과 표 skill_effects.csv · 수치 = 표 p 칸):
/// shot_giant(황충 거대 화살 · 사거리 무관 · 첫 적) · shot_pierce(직선 관통 · 아군 통과/피해/회복 · 밀기) · shot_blast(투사체 → 첫 적 자리 폭발 · 불꽃 지대) ·
/// shot_fan(부채꼴 n 발 × 연속) · shot_whirl(회오리 = 직선 안 적 밀기) · shot_barricade(목책 세우기) · shot_firewall(불 장벽 지대) ·
/// 일반 사격 덧붙임 = multishot(멀티샷) · shot_ricochet(도탄) · poison_zone(독지대) · 아군 명중 = 회복·버프·이동(ally_hit_*).
/// </summary>
public partial class ActionResolver
{
static readonly string[] ShotKeys =
{
SkillKeys.ShotGiant, SkillKeys.ShotPierce, SkillKeys.ShotBlast, SkillKeys.ShotFan, SkillKeys.ShotWhirl, SkillKeys.ShotBarricade, SkillKeys.ShotFirewall
};
static bool HasSpecialShot(Unit u)
{
foreach (var k in ShotKeys)
if (SkillMods.Has(u, k)) return true;
return false;
}
IEnumerator SpecialShot(Unit u, Vector2 origin, Vector2 dir, Vector2 target, ActionOutcome o)
{
o.Fired = true;
Face(u, dir);
if (u.View != null) u.View.PlayAttack();
if (u.Type == UnitType.Strategist) BattleEvents.Raise(BattleEvents.StrategistShots, u);
SkillEffect e;
if ((e = SkillMods.First(u, SkillKeys.ShotGiant)) != null) yield return GiantArrow(u, dir, e, o);
else if ((e = SkillMods.First(u, SkillKeys.ShotPierce)) != null) yield return PierceShot(u, dir, e, o);
else if ((e = SkillMods.First(u, SkillKeys.ShotBlast)) != null) yield return BlastShot(u, dir, e, o);
else if ((e = SkillMods.First(u, SkillKeys.ShotFan)) != null) yield return FanShot(u, dir, e, o);
else if ((e = SkillMods.First(u, SkillKeys.ShotWhirl)) != null) yield return WhirlShot(u, dir, e, o);
else if ((e = SkillMods.First(u, SkillKeys.ShotBarricade)) != null) yield return Barricade(u, dir, target, e, o);
else if ((e = SkillMods.First(u, SkillKeys.ShotFirewall)) != null) yield return Firewall(u, dir, e, o);
battle.NotifyChanged();
yield return Fx.Wait(0.2f);
}
/// <summary>황충 백발백중: 사거리 무관 · 반지름 p1 거대 화살 → 아군·장애물 통과 · 첫 적 = 공격력 × p2.</summary>
IEnumerator GiantArrow(Unit u, Vector2 dir, SkillEffect e, ActionOutcome o)
{
Vector2 from = u.Position;
float far = battle.Board != null ? 200f : 60f;
float travel;
GimmickInstance g;
Combat.ArrowIgnoreGimmicks = true;
Unit hit;
try
{
hit = Combat.FirstUnitOnArrow(u, from, dir, far, Mathf.Max(cfg.arrowRadius, e.P), u.Faction, out travel, out g);
}
finally
{
Combat.ArrowIgnoreGimmicks = false;
}
Vector2 to = from + dir * travel;
yield return Fx.Arrow(from, to, new Color(1f, 0.85f, 0.4f));
if (hit != null)
{
o.ShotHit = hit;
ShotDamage(u, hit, Combat.SkillDamage(cfg, u, hit, e.Get(1, 1f), rng), false, o);
}
}
/// <summary>직선 관통(태사자·장임·왕랑·조범·주태 검기): 사거리 안 직선 위 유닛 전원(장애물 무시) · 적 = 공격력 × p1 · 아군 = p2(0 통과 · 1 피해 · 2 회복) · p3 = 밀기.</summary>
IEnumerator PierceShot(Unit u, Vector2 dir, SkillEffect e, ActionOutcome o)
{
Vector2 from = u.Position;
float range = SkillMods.Range(u) + u.Radius;
float travel = Combat.WallTravel(u, from, dir, range);
var dists = new List<float>();
var units = Combat.UnitsOnPath(u, from, dir, travel, dists);
yield return Fx.Arrow(from, from + dir * travel, u.Type == UnitType.Strategist ? Fx.StrategistShotColor : new Color(0.75f, 0.95f, 1f));
int allyMode = Mathf.RoundToInt(e.Get(1));
var pushed = new List<Unit>();
foreach (var t in units)
{
if (!t.IsAlive) continue;
if (t.Faction == u.Faction)
{
if (allyMode == 0) continue;
if (allyMode == 2)
{
AllyHitSpecial(u, t, from, o, true);
continue;
}
}
if (o.ShotHit == null) o.ShotHit = t;
ShotDamage(u, t, Combat.SkillDamage(cfg, u, t, e.P, rng), false, o);
if (t.IsAlive) pushed.Add(t);
}
float push = e.Get(2);
if (push > 0.01f)
{
pushed.Reverse(); // 먼 쪽부터 밀어 서로 막히지 않게
foreach (var t in pushed)
if (t.IsAlive) yield return Push(t, dir, push * SkillMods.KnockbackTakenMult(t, true), u, o);
}
}
/// <summary>폭발 사격(서서 화염구 · 가후 독구름 · 주유 운석 · 손권 화살비): 투사체 → 사거리 안 첫 적(아군 너머) 자리 · 반경 p1 안 적 전원 = 공격력 × p2 · 주유 = 불꽃 지대.</summary>
IEnumerator BlastShot(Unit u, Vector2 dir, SkillEffect e, ActionOutcome o)
{
Vector2 from = u.Position;
float range = SkillMods.Range(u) + u.Radius;
float travel;
GimmickInstance g;
var hit = Combat.FirstUnitOnArrow(u, from, dir, range, cfg.arrowRadius, u.Faction, out travel, out g);
Vector2 center = hit != null ? hit.Position : from + dir * travel;
yield return Fx.Arrow(from, center, u.Type == UnitType.Strategist ? Fx.StrategistShotColor : Fx.ArrowColor);
float r = e.P * SkillMods.Product(u, SkillKeys.SplashRadiusMult, true); // 사마의 응시낭고·보물 광역 배율도(Fable 검토 B3)
Fx.Burst(center, r, ExplosionColor);
o.ShotHit = hit;
foreach (var t in Combat.UnitsInCircle(center, r, u, null))
{
if (!t.IsAlive || t.Faction == u.Faction) continue;
ShotDamage(u, t, Combat.SkillDamage(cfg, u, t, e.Get(1, 1f), rng), false, o);
}
var fz = SkillMods.First(u, SkillKeys.FireZone);
var sys = SkillSystem.Current;
if (fz != null && sys != null && sys.Of(u) != null)
sys.AddZone(sys.Of(u), center, center, fz.P, Mathf.RoundToInt(fz.Get(1, 1f)), fz.Get(2), ZoneStatus(u), new Color(1f, 0.45f, 0.1f, 0.35f), SkillMods.Has(u, SkillKeys.ZoneHitsAllies));
}
/// <summary>하후연 전광 연사: 부채꼴(p3 도) p1 발 × p2 번 · 화살마다 일반 사격 규칙(첫 유닛 · 아군 오사).</summary>
IEnumerator FanShot(Unit u, Vector2 dir, SkillEffect e, ActionOutcome o)
{
int n = Mathf.Max(1, Mathf.RoundToInt(e.P));
int volleys = Mathf.Max(1, Mathf.RoundToInt(e.Get(1, 1f)));
float spread = e.Get(2, 30f);
for (int v = 0; v < volleys && u.IsAlive; v++)
{
for (int i = 0; i < n; i++)
{
float ang = n > 1 ? -spread * 0.5f + spread * i / (n - 1) : 0f;
FireOne(u, Rotate(dir, ang), o, 1f);
}
battle.NotifyChanged();
yield return Fx.Wait(0.18f);
}
}
static Vector2 Rotate(Vector2 v, float deg)
{
float r = deg * Mathf.Deg2Rad;
float c = Mathf.Cos(r), s = Mathf.Sin(r);
return new Vector2(v.x * c - v.y * s, v.x * s + v.y * c);
}
/// <summary>화살 1발(일반 사격 규칙 · 아군 통과 패시브 반영 · 아군 명중 = 회복류 또는 오사) — 부채꼴 · 멀티샷.</summary>
void FireOne(Unit u, Vector2 d, ActionOutcome o, float ratio)
{
float range = SkillMods.Range(u) + u.Radius;
bool obstacles;
var pass = ArrowPass(u, out obstacles);
float travel;
GimmickInstance g;
Combat.ArrowIgnoreGimmicks = obstacles;
Unit hit;
try
{
hit = Combat.FirstUnitOnArrow(u, u.Position, d, range, cfg.arrowRadius, pass, out travel, out g);
}
finally
{
Combat.ArrowIgnoreGimmicks = false;
}
Fx.Run(Fx.Arrow(u.Position, u.Position + d * (hit != null ? travel + cfg.arrowRadius : travel)));
if (hit == null) return;
if (o.ShotHit == null) o.ShotHit = hit;
if (hit.Faction == u.Faction && AllyHitSpecial(u, hit, u.Position, o, true)) return;
ShotDamage(u, hit, ratio >= 0.999f && ratio <= 1.001f ? Combat.Damage(cfg, u, hit, false, rng, false) : Combat.SkillDamage(cfg, u, hit, ratio, rng), false, o);
}
/// <summary>제갈근 선풍계: 사거리 안 폭 p1 직선 위 적 전원을 진행 방향으로 p2 밀침(먼 쪽부터) · 피해 = 공격력 × p3(0 = 없음) · 명중 상태 이상(혼란).</summary>
IEnumerator WhirlShot(Unit u, Vector2 dir, SkillEffect e, ActionOutcome o)
{
Vector2 from = u.Position;
float range = SkillMods.Range(u) + u.Radius;
float travel = Combat.WallTravel(u, from, dir, range);
Vector2 to = from + dir * travel;
yield return Fx.Arrow(from, to, new Color(0.7f, 1f, 0.85f));
var targets = new List<Unit>();
foreach (var t in battle.AliveUnits(u.Faction == Faction.Player ? Faction.Enemy : Faction.Player))
if (Combat.SegmentPointDistance(from, to, t.Position) <= e.P + t.Radius) targets.Add(t);
targets.Sort((a, b) => Vector2.Distance(from, b.Position).CompareTo(Vector2.Distance(from, a.Position)));
var sys = SkillSystem.Current;
foreach (var t in targets)
{
if (!t.IsAlive) continue;
if (e.Get(2) > 0f) ShotDamage(u, t, Combat.SkillDamage(cfg, u, t, e.Get(2), rng), false, o);
else
{
o.Attacked.Add(t);
if (sys != null) sys.ApplyHitStatuses(u, t, false, HitKind.Skill);
}
if (t.IsAlive) yield return Push(t, dir, e.Get(1, 1f) * SkillMods.KnockbackTakenMult(t, true), u, o);
}
if (o.Attacked.Count == 0) o.MovedOnly = true;
}
/// <summary>노숙 녹각진: 슬링 목표점(사거리 안)에 진행 방향에 수직으로 목책 p1 칸(칸 지름 p3) · 그 자리 적 = 목책 충돌 피해(기믹 표 자기 피해 %)·p2 밀기 · 아군 = 밀기만.</summary>
IEnumerator Barricade(Unit u, Vector2 dir, Vector2 target, SkillEffect e, ActionOutcome o)
{
int n = Mathf.Max(1, Mathf.RoundToInt(e.P));
float size = Mathf.Max(0.3f, e.Get(2, 1f));
float range = SkillMods.Range(u) + u.Radius;
Vector2 c = target;
if (Vector2.Distance(u.Position, c) > range) c = u.Position + dir * range;
c = battle.Board.ClampInside(c, size * 0.5f);
Vector2 perp = new Vector2(-dir.y, dir.x);
var layer = GimmickLayer.Current;
var def = GimmickTables.Db != null ? GimmickTables.Db.Get(GimmickIds.Palisade) : null;
float pct = def != null ? Mathf.Max(def.cavSelfPct, def.bouncePct) : 0f;
yield return Fx.Arrow(u.Position, c, Fx.StrategistShotColor);
var sys = SkillSystem.Current;
for (int i = 0; i < n; i++)
{
Vector2 p = c + perp * ((i - (n - 1) * 0.5f) * size);
p = battle.Board.ClampInside(p, size * 0.5f);
foreach (var t in Combat.UnitsInCircle(p, size * 0.5f, null, null))
{
if (!t.IsAlive) continue;
if (t.Faction != u.Faction && pct > 0f && sys != null)
{
int d = sys.Hit(t, Mathf.Max(1, Mathf.RoundToInt(t.MaxHp * pct)), u, HitKind.Skill);
o.Attacked.Add(t);
o.DamageDealt += d;
Fx.Popup(t.Position, d + " 목책", new Color(1f, 0.75f, 0.4f));
}
if (t.IsAlive) yield return Push(t, dir, Mathf.Max(e.Get(1, 1f), size), u, o);
}
if (layer != null) layer.Spawn(GimmickIds.Palisade, GimmickShape.Circle(p, size * 0.5f));
}
o.Notes.Add("목책 " + n + "칸");
}
/// <summary>육손 이릉의 화공: 진행 방향 사거리까지 반폭 p1 불 장벽(p2 턴) · 그 안 적 = 공격력 × p3 + 화상 · 장벽 = 닿는 적마다 같은 피해·화상(지대).</summary>
IEnumerator Firewall(Unit u, Vector2 dir, SkillEffect e, ActionOutcome o)
{
Vector2 a = u.Position + dir * (u.Radius + e.P);
float range = SkillMods.Range(u) + u.Radius;
Vector2 b = u.Position + dir * Mathf.Max(u.Radius + e.P, Combat.WallTravel(u, u.Position, dir, range));
yield return Fx.Arrow(u.Position, b, new Color(1f, 0.45f, 0.1f));
var sys = SkillSystem.Current;
bool allies = SkillMods.Has(u, SkillKeys.ZoneHitsAllies); // 육손 = 아군도(PD #1112 결정 ⑬)
SkillZone z = null;
if (sys != null && sys.Of(u) != null)
z = sys.AddZone(sys.Of(u), a, b, e.P, Mathf.RoundToInt(e.Get(1, 1f)), e.Get(2, 1f), ZoneStatus(u), new Color(1f, 0.4f, 0.05f, 0.35f), allies);
foreach (var t in new List<Unit>(battle.Units))
{
if (t == null || !t.IsAlive || t == u || (t.Faction == u.Faction && !allies)) continue;
if (Combat.SegmentPointDistance(a, b, t.Position) > e.P + t.Radius) continue;
if (z != null) z.Touched.Add(t);
int dmg = Combat.SkillDamage(cfg, u, t, e.Get(2, 1f), rng); // 아군도 같은 피해(PD 결정 ⑬ · Fable 검토 C4)
ShotDamage(u, t, dmg, false, o);
if (t.Faction == u.Faction && t.IsAlive && sys != null)
{
var bs = ZoneStatus(u);
if (bs != null) sys.ApplyBurn(t, Mathf.RoundToInt(bs.P), bs.Get(1, 0.05f)); // 아군도 화상(명중 상태 이상은 적에게만 붙으므로 직접)
}
}
}
/// <summary>지대가 거는 화상 = zone_burn(주유 불꽃 3턴) · 없으면 그 스킬의 hit_burn(육손 장벽).</summary>
static SkillEffect ZoneStatus(Unit u)
{
return SkillMods.First(u, SkillKeys.ZoneBurn) ?? SkillMods.First(u, SkillKeys.HitBurn);
}
/// <summary>일반 사격 덧붙임: 하후연 삼연시(멀티샷 · 좌우 화살) · 감녕 도탄(맞은 유닛과 가까운 n 기 · 아군 포함) · 장로 독지대.</summary>
IEnumerator ExtraArrows(Unit u, ShotPlan p, ActionOutcome o)
{
var ms = SkillMods.First(u, SkillKeys.Multishot, true);
if (ms != null && u.Type == UnitType.Archer)
{
int n = Mathf.Max(1, Mathf.RoundToInt(ms.P));
float spread = ms.Get(1, 30f);
Vector2 d = (p.To - p.From).sqrMagnitude > 1e-6f ? (p.To - p.From).normalized : Vector2.up;
for (int i = 0; i < n; i++)
{
float ang = n > 1 ? -spread * 0.5f + spread * i / (n - 1) : 0f;
if (Mathf.Abs(ang) < 0.01f) continue; // 가운데 = 본 화살
FireOne(u, Rotate(d, ang), o, 1f);
}
}
var rc = SkillMods.First(u, SkillKeys.ShotRicochet);
if (rc != null && p.Hit != null)
{
var near = new List<Unit>();
foreach (var t in battle.Units)
if (t != null && t.IsAlive && t != u && t != p.Hit) near.Add(t);
Vector2 c = p.Hit.Position;
near.Sort((a, b) => Vector2.Distance(c, a.Position).CompareTo(Vector2.Distance(c, b.Position)));
int cnt = Mathf.Max(0, Mathf.RoundToInt(rc.P));
for (int i = 0; i < near.Count && i < cnt; i++)
{
var t = near[i];
Fx.Run(Fx.Arrow(c, t.Position));
ShotDamage(u, t, Combat.SkillDamage(cfg, u, t, rc.Get(1, 0.5f), rng), false, o);
}
if (cnt > 0) o.Notes.Add("도탄");
}
var pz = SkillMods.First(u, SkillKeys.PoisonZone);
var sys = SkillSystem.Current;
if (pz != null && sys != null && sys.Of(u) != null && (p.Hit != null || p.HitGimmick != null))
sys.AddZone(sys.Of(u), p.To, p.To, pz.P, Mathf.RoundToInt(pz.Get(1, 1f)), 0f, SkillMods.First(u, SkillKeys.HitPoison), new Color(0.6f, 0.3f, 0.9f, 0.3f));
yield return null;
}
/// <summary>
/// 자신의 사격이 아군에 맞음: 감녕 백기습영 = 그 아군 곁으로 이동(피해 없음) + 남은 행동 범위로 재행동 · 순욱·소교·간옹·조범 = 공격력 × p 회복 ·
/// 미축·순욱 = 이동·공격 버프(지속 턴). 처리했으면 true(피해 없음) · 효과가 없으면 false(오사).
/// </summary>
bool AllyHitSpecial(Unit u, Unit ally, Vector2 from, ActionOutcome o, bool splash = false)
{
var sys = SkillSystem.Current;
if (sys == null || ally == null || ally == u) return false;
bool any = false;
float heal = SkillMods.Sum(u, SkillKeys.AllyHitHeal, true);
if (heal > 0f)
{
sys.Heal(ally, Mathf.RoundToInt(SkillSystem.AtkOf(u) * heal), u);
any = true;
}
foreach (var h in SkillMods.Get(u, SkillKeys.AllyHitBuff))
{
if (h.Owner == null) continue;
int rounds = Mathf.Max(1, h.E.Dur);
if (h.E.P > 0f) sys.AddBuff(h.Owner, ally, SkillKeys.SlingDistPct, h.E.P, rounds);
if (h.E.Get(1) > 0f) sys.AddBuff(h.Owner, ally, SkillKeys.AtkPct, h.E.Get(1), rounds);
if (h.Passive) sys.NotePassive(h.Owner, h.Def);
any = true;
}
if (!splash && SkillMods.Has(u, SkillKeys.AllyHitWarp, true))
{
var s = sys.Of(u);
Vector2 d = ally.Position - u.Position;
d = d.sqrMagnitude > 1e-6f ? d.normalized : Vector2.up;
Vector2 spot = battle.Board.ClampInside(ally.Position - d * (u.Radius + ally.Radius + 0.05f), u.Radius);
float used = Vector2.Distance(o.Path.Count > 0 ? o.Path[0] : u.Position, spot);
Vector2 start = u.Position;
if (!Sim.Active)
{
u.transform.position = new Vector3(start.x, start.y, 0f);
Fx.Run(Fx.MoveAlong(u.transform, start, spot, cfg.moveSpeed * 2f, null));
}
u.Position = spot;
o.Path.Add(spot);
float left = SkillMods.SlingMax(u) - used;
if (s != null && left >= cfg.minDragDistance)
{
s.ReactCredits++;
s.ReactLimit = left;
}
o.Notes.Add(ally.DisplayName + " 곁으로");
any = true;
}
return any;
}
/// <summary>이각 약탈: 이번 광역으로 준 피해 × p1 → 자신과 인접(p2)한 가장 HP 비율이 낮은 다른 아군 회복.</summary>
void SplashHealLowest(Unit u, int total)
{
var e = SkillMods.First(u, SkillKeys.SplashHealLowest, true);
var sys = SkillSystem.Current;
if (e == null || total <= 0 || sys == null) return;
Unit pick = null;
foreach (var a in battle.AliveUnits(u.Faction))
{
if (a == u || !SkillMods.Within(u, a, e.Get(1, 2f))) continue;
if (pick == null || (float)a.Hp / a.MaxHp < (float)pick.Hp / pick.MaxHp) pick = a;
}
if (pick != null) sys.Heal(pick, Mathf.RoundToInt(total * e.P), u);
}
}
}