using System.Collections; using System.Collections.Generic; using UnityEngine; namespace TKS { /// /// 스킬 특수 사격(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_*). /// 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); } /// 황충 백발백중: 사거리 무관 · 반지름 p1 거대 화살 → 아군·장애물 통과 · 첫 적 = 공격력 × p2. 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); } } /// 직선 관통(태사자·장임·왕랑·조범·주태 검기): 사거리 안 직선 위 유닛 전원(장애물 무시) · 적 = 공격력 × p1 · 아군 = p2(0 통과 · 1 피해 · 2 회복) · p3 = 밀기. 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(); 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(); 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); } } /// 폭발 사격(서서 화염구 · 가후 독구름 · 주유 운석 · 손권 화살비): 투사체 → 사거리 안 첫 적(아군 너머) 자리 · 반경 p1 안 적 전원 = 공격력 × p2 · 주유 = 불꽃 지대. 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)); } /// 하후연 전광 연사: 부채꼴(p3 도) p1 발 × p2 번 · 화살마다 일반 사격 규칙(첫 유닛 · 아군 오사). 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); } /// 화살 1발(일반 사격 규칙 · 아군 통과 패시브 반영 · 아군 명중 = 회복류 또는 오사) — 부채꼴 · 멀티샷. 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); } /// 제갈근 선풍계: 사거리 안 폭 p1 직선 위 적 전원을 진행 방향으로 p2 밀침(먼 쪽부터) · 피해 = 공격력 × p3(0 = 없음) · 명중 상태 이상(혼란). 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(); 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; } /// 노숙 녹각진: 슬링 목표점(사거리 안)에 진행 방향에 수직으로 목책 p1 칸(칸 지름 p3) · 그 자리 적 = 목책 충돌 피해(기믹 표 자기 피해 %)·p2 밀기 · 아군 = 밀기만. 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 + "칸"); } /// 육손 이릉의 화공: 진행 방향 사거리까지 반폭 p1 불 장벽(p2 턴) · 그 안 적 = 공격력 × p3 + 화상 · 장벽 = 닿는 적마다 같은 피해·화상(지대). 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(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)); // 아군도 화상(명중 상태 이상은 적에게만 붙으므로 직접) } } } /// 지대가 거는 화상 = zone_burn(주유 불꽃 3턴) · 없으면 그 스킬의 hit_burn(육손 장벽). static SkillEffect ZoneStatus(Unit u) { return SkillMods.First(u, SkillKeys.ZoneBurn) ?? SkillMods.First(u, SkillKeys.HitBurn); } /// 일반 사격 덧붙임: 하후연 삼연시(멀티샷 · 좌우 화살) · 감녕 도탄(맞은 유닛과 가까운 n 기 · 아군 포함) · 장로 독지대. 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(); 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; } /// /// 자신의 사격이 아군에 맞음: 감녕 백기습영 = 그 아군 곁으로 이동(피해 없음) + 남은 행동 범위로 재행동 · 순욱·소교·간옹·조범 = 공격력 × p 회복 · /// 미축·순욱 = 이동·공격 버프(지속 턴). 처리했으면 true(피해 없음) · 효과가 없으면 false(오사). /// 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; } /// 이각 약탈: 이번 광역으로 준 피해 × p1 → 자신과 인접(p2)한 가장 HP 비율이 낮은 다른 아군 회복. 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); } } }