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

505 lines
25 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>
/// 지형 기믹 충돌 반응(PD #1097 · 지형기믹_기획서 §2~§4 · 병종×기믹 표):
/// 보병 = 내구 피해 + 튕김(알까기 자기 튕김 재사용 · bounce_pct = 자기 HP 피해) · 바위는 굴림 / 창병 = 내구 피해 + spear_push 만큼 밀고 정지(밀린 끝 유닛도 밀림) · 바위 굴림 /
/// 기병 = 관통 불가 → 거기서 정지(복귀 없음) · 내구 피해 + 튕김 + 자기 HP(cav_self_pct) · 바위 = 정지 / 궁·책 이동 = 피해 없이 튕김 ·
/// 투사체 = pass·block·destroy · 책사 광역 = 덤불 제거·수레 파괴·(화공) 불 생성 · 폭약 = 광역 파이프라인(아군 포함 + 밀기 + 불) ·
/// 행동 끝 = 빙판 미끄러짐(행동 유닛 = 병종 루틴 재실행 · 밀린 유닛 = 밀기) → 지대 스윕(독·불·함정) → 다리 → 강.
/// </summary>
public partial class ActionResolver
{
public static readonly Color ExplosionColor = new Color(1f, 0.5f, 0.15f, 0.55f);
static readonly Color IceTextColor = new Color(0.65f, 0.9f, 1f);
static GimmickLayer Layer
{
get { return GimmickLayer.Current; }
}
static bool Gim
{
get { return GimmickLayer.Current != null && GimmickLayer.Current.HasAny; }
}
/// <summary>근접 병종 슬링샷 도달점(늪 ×p1 · 고지 오르기 ×p3) — Resolve·조준 미리보기 공용. 원거리 병종은 접근·이동 단계에서 따로 계산.</summary>
public Vector2 ReachPoint(Unit u, Vector2 target)
{
if (u == null || UnitTypeInfo.IsRanged(u.Type)) return target;
Vector2 d = target - u.Position;
float dist = d.magnitude;
if (dist < 0.001f) return target;
float reach = SlowZoneReach(u, u.Position, d / dist, dist); // 조인 철벽(상대 반경 안 이동 비용 +)
if (Gim && !SkillMods.Has(u, SkillKeys.TerrainImmune)) reach = Layer.CostReach(u, u.Position, d / dist, reach); // 축융 = 지형 이동 감소 무시
return u.Position + d / dist * reach;
}
static float Reach(Unit u, Vector2 origin, Vector2 dir, float budget)
{
if (u != null && SkillMods.Has(u, SkillKeys.TerrainImmune)) return budget;
return Gim ? Layer.CostReach(u, origin, dir, budget) : budget;
}
/// <summary>조준 경로가 지형 이동 감소(늪·고지 오르기)에 걸리는가(맹획 남만왕 재행동 · 지형 무시 유닛도 지형은 「지남」으로 봄).</summary>
static bool CrossesSlowTerrain(Unit u, Vector2 origin, Vector2 aimed)
{
if (!Gim || u == null) return false;
Vector2 d = aimed - origin;
float dist = d.magnitude;
if (dist < 0.001f) return false;
return Layer.CostReach(u, origin, d / dist, dist) < dist - 1e-3f;
}
/// <summary>자기 행동 이동(궁·책 접근·이동만) = 조인 철벽 감속 → 지형 이동 비용(Fable 검토 B2 · 근접 병종은 ReachPoint).</summary>
static float MoveReach(Unit u, Vector2 origin, Vector2 dir, float budget)
{
return Reach(u, origin, dir, SlowZoneReach(u, origin, dir, budget));
}
/// <summary>조인 철벽(zone_slow): 상대 시전자의 행동 반경(기본 슬링 거리) 안을 지나는 구간 = 이동 비용 × (1 + p1).</summary>
static float SlowZoneReach(Unit u, Vector2 origin, Vector2 dir, float budget)
{
var sys = SkillSystem.Current;
if (sys == null) return budget;
List<KeyValuePair<Unit, float>> zones = null;
foreach (var t in sys.TimedEffects)
{
if (t.E.Key != SkillKeys.ZoneSlow || t.Faction == u.Faction || t.Owner == null || t.Owner.Unit == null || !t.Owner.Unit.IsAlive) continue;
if (zones == null) zones = new List<KeyValuePair<Unit, float>>();
zones.Add(new KeyValuePair<Unit, float>(t.Owner.Unit, t.E.P));
}
if (zones == null) return budget;
const float step = 0.05f;
float spent = 0f, moved = 0f;
while (moved < budget + 10f)
{
Vector2 p = origin + dir * (moved + step * 0.5f);
float cost = 1f;
foreach (var z in zones)
if (Vector2.Distance(z.Key.Position, p) <= SkillMods.BaseReach(z.Key)) cost = Mathf.Max(cost, 1f + z.Value);
if (spent + step * cost > budget) return moved + (budget - spent) / cost;
spent += step * cost;
moved += step;
}
return budget;
}
static void NoteForced(Unit u, Vector2 from, Vector2 to)
{
if (Gim) Layer.NoteForced(u, from, to);
}
void GimmickBegin(ActionOutcome o)
{
if (Layer == null) return;
Layer.Outcome = o;
Layer.Forced.Clear();
Layer.Affected.Clear();
}
/// <summary>경로 위 첫 장애물 기믹 — limit(첫 유닛 접촉) · 전장 벽 · 숨은 함정보다 앞일 때만.</summary>
GimmickInstance BlockAhead(Unit u, Vector2 origin, Vector2 dir, float dist, float limit, out float hitDist, out Vector2 normal)
{
hitDist = 0f;
normal = -dir;
if (!Gim) return null;
var g = Combat.FirstGimmickOnPath(u, origin, dir, dist, out hitDist, out normal);
if (g == null) return null;
if (hitDist > limit + Combat.Epsilon || hitDist > Combat.WallTravel(u, origin, dir, dist) + Combat.Epsilon * 2f) return null;
if (Layer.TrapStop(u, origin, dir, dist) < hitDist) return null;
return g;
}
/// <summary>궁·책 등 이동만 하는 병종: 유닛 앞의 장애물이면 부딪힘(튕김) · 아니면 그대로 이동.</summary>
IEnumerator MoveOrHit(Unit u, Vector2 origin, Vector2 dir, float dist, ActionOutcome o)
{
float uh;
var first = Combat.FirstUnitOnPath(u, origin, dir, dist, out uh);
float gh;
Vector2 gn;
var g = BlockAhead(u, origin, dir, dist, first != null ? uh : dist, out gh, out gn);
if (g != null)
{
yield return HitGimmick(u, g, origin, dir, dist, gh, gn, o);
yield break;
}
yield return MoveFree(u, origin, dir, dist, o);
}
/// <summary>보병·창병·궁·책 이동이 장애물 기믹에 부딪힘.</summary>
IEnumerator HitGimmick(Unit u, GimmickInstance g, Vector2 origin, Vector2 dir, float dist, float hitDist, Vector2 normal, ActionOutcome o)
{
Vector2 contact = origin + dir * Mathf.Max(0f, hitDist - Combat.Epsilon);
yield return MoveStraight(u, contact, o);
o.MovedOnly = true;
float remaining = Mathf.Max(0f, dist - hitDist);
var d = g.Def;
var sk = SkillSystem.Current;
if (sk != null && sk.Ctx != null && sk.Ctx.Actor == u) sk.Ctx.HitTerrain = true; // 맹획 남만왕: 지형물에 닿음 = 재행동
float gm = SkillMods.GimmickMult(u); // 전위 고지악래: 지형 오브젝트 밀기 거리 ×
bool pusher = u.Type == UnitType.Infantry || u.Type == UnitType.Spear;
if (pusher && g.Rolls)
{
yield return RollBoulder(u, g, dir, remaining * d.spearPush * gm, o); // 보·창 = 바위를 남은 거리 × spear_push 만큼 굴린다 · 자신은 정지
PushContinue(u, remaining); // 고순: 오브젝트를 밀친 뒤 남은 이동력으로 재행동
yield break;
}
int dmg = d.DamageBy(u.Type);
if (dmg > 0) yield return DamageGimmick(g, Mathf.RoundToInt(dmg * gm), u, o);
if (!u.IsAlive) yield break;
if (u.Type == UnitType.Spear && dmg > 0)
{
if (d.spearPush > 0.01f && g.BlocksMove) yield return ShoveGimmick(g, dir, d.spearPush * gm, u, o); // 창 = spear_push 만큼 밀고 정지(튕기지 않음)
yield break;
}
if (d.bouncePct > 0f) o.Notes.Add(u.DisplayName + " 자기 −" + Layer.PctDamage(u, d.bouncePct, d.nameKo));
if (!u.IsAlive) yield break;
yield return Bounce(u, null, -normal, remaining, o, 0); // 알까기 자기 튕김(남은 거리 × collisionSelfBounceRatio) · 면 바깥으로
}
/// <summary>기병 관통이 장애물에 막힘(복귀 없음): 내구 피해 → 바위 = 정지 + cav_self_pct · 그 밖 = 튕김 + max(cav_self_pct, bounce_pct).</summary>
IEnumerator CavalryHitsGimmick(Unit u, GimmickInstance g, float remaining, Vector2 normal, ActionOutcome o)
{
var d = g.Def;
int dmg = d.DamageBy(UnitType.Cavalry);
if (dmg > 0) yield return DamageGimmick(g, dmg, u, o);
float pct = g.Rolls ? d.cavSelfPct : Mathf.Max(d.cavSelfPct, d.bouncePct);
if (pct > 0f) o.Notes.Add(u.DisplayName + " 자기 −" + Layer.PctDamage(u, pct, d.nameKo));
if (!u.IsAlive || g.Rolls) yield break;
o.Notes.Add(d.nameKo + "에 막힘");
yield return Bounce(u, null, -normal, remaining, o, 0);
}
IEnumerator DamageGimmick(GimmickInstance g, int dmg, Unit by, ActionOutcome o)
{
bool broke = Layer.Hit(g, dmg);
o.Notes.Add(g.Def.nameKo + (broke ? " 파괴" : " −" + dmg));
if (broke && g.Def.projectile == GimmickProjectile.Destroy) yield return Explode(g, by, o, 0);
}
/// <summary>
/// 폭약 수레 폭발(광역 파이프라인 재사용): 반경 p1 안 유닛 전원(아군 포함 · 성문·성벽 너머 제외) 피해 = 터뜨린 유닛 공 × p2 · 바깥으로 p3 밀기 ·
/// 반경 안 다른 수레 연쇄 · 그 자리 불 지대(반경 p1 · 지속 = 스테이지 화공 배치 burn_turns · 없으면 정의 p2).
/// </summary>
IEnumerator Explode(GimmickInstance g, Unit by, ActionOutcome o, int depth)
{
if (depth > 4) yield break;
Vector2 c = g.Shape.Center;
float r = g.P(1, 0f), mult = g.P(2, 1f), push = g.P(3, 0f);
if (!g.Broken)
{
g.Broken = true;
g.Hp = 0;
g.SetCollidersEnabled(false);
if (!Sim.Active && g.View != null) g.View.Shatter();
}
if (!Sim.Active)
{
Fx.Burst(c, r, ExplosionColor);
Fx.Popup(c, g.Def.nameKo + " 폭발!", new Color(1f, 0.6f, 0.2f));
}
o.Notes.Add(g.Def.nameKo + " 폭발");
var victims = Combat.UnitsInCircle(c, r, null, null);
victims.RemoveAll(v => Layer.SplashBlocked(c, v.Position));
if (by != null)
{
foreach (var v in victims)
{
if (!v.IsAlive) continue;
int dmg = SkillSystem.Deal(v, Mathf.RoundToInt(Combat.GimmickDamage(cfg, by, v, mult, rng) * SkillMods.GimmickMult(by)), by, HitKind.Gimmick); // 전위 고지악래 ×
if (v.Faction == by.Faction)
{
o.FriendlyHit.Add(v);
o.FriendlyDamage += dmg;
if (!Sim.Active) Fx.Popup(v.Position, dmg + " 폭발", Fx.FriendlyFireColor);
}
else
{
o.Attacked.Add(v);
o.DamageDealt += dmg;
if (!Sim.Active) Fx.Popup(v.Position, dmg.ToString(), new Color(1f, 0.7f, 0.3f));
}
}
}
int running = 0;
foreach (var v in victims)
{
if (!v.IsAlive || push < 0.01f) continue;
Vector2 n = v.Position - c;
n = n.sqrMagnitude > 1e-6f ? n.normalized : Vector2.up;
running++;
Fx.Run(Track(Push(v, n, push, null, o), () => running--));
}
while (running > 0) yield return null;
foreach (var other in new List<GimmickInstance>(Layer.Items))
{
if (other == g || !other.BlocksMove || other.Def.projectile != GimmickProjectile.Destroy) continue;
if (other.Shape.SignedDistance(c) <= r) yield return Explode(other, by, o, depth + 1);
}
Layer.SpawnFire(c, r);
battle.NotifyChanged();
yield return Fx.Wait(0.2f);
}
/// <summary>바위 굴림: 바위 원을 dir 로 dist 만큼(벽·다른 장애물·유닛에서 멈춤) · 상대 유닛에 닿으면 공 × p1 피해 + p2 밀기.</summary>
IEnumerator RollBoulder(Unit pusher, GimmickInstance g, Vector2 dir, float dist, ActionOutcome o)
{
if (dist < 0.01f || !g.BlocksMove) yield break;
Physics2D.SyncTransforms();
float r = g.Shape.Type == GimmickShapeType.Circle ? g.Shape.R : g.Shape.BoundingRadius;
var hits = Physics2D.CircleCastAll(g.Shape.Center, r * 0.95f, dir, dist);
System.Array.Sort(hits, (a, b) => a.distance.CompareTo(b.distance));
float travel = dist;
Unit victim = null;
foreach (var h in hits)
{
if (g.Colliders.Contains(h.collider)) continue;
if (h.collider.GetComponent<BoardWall>() != null || GimmickLayer.BlocksMove(h.collider))
{
travel = h.distance;
break;
}
var v = h.collider.GetComponent<Unit>();
if (v == null || v == pusher || !v.IsAlive) continue;
travel = h.distance;
victim = v;
break;
}
travel = Mathf.Max(0f, travel - Combat.Epsilon);
if (travel > 0.01f)
{
Vector2 from = g.Shape.Center;
if (!Sim.Active && g.Go != null) yield return Fx.MoveAlong(g.Go.transform, from, from + dir * travel, cfg.moveSpeed, null);
g.MoveBy(dir * travel);
o.Notes.Add(g.Def.nameKo + " 굴림");
}
if (victim != null && victim.Faction != pusher.Faction)
{
int dmg = SkillSystem.Deal(victim, Mathf.RoundToInt(Combat.GimmickDamage(cfg, pusher, victim, g.P(1, 1f), rng) * SkillMods.GimmickMult(pusher)), pusher, HitKind.Gimmick);
o.Attacked.Add(victim);
o.DamageDealt += dmg;
if (!Sim.Active) Fx.Popup(victim.Position, dmg + " 바위", new Color(1f, 0.75f, 0.4f));
if (victim.IsAlive) yield return Push(victim, dir, g.P(2, 0f), pusher, o);
}
battle.NotifyChanged();
}
/// <summary>목책 밀기(창병): spear_push 만큼(벽·다른 장애물에서 멈춤) · 새 자리와 겹칠 유닛은 먼저 그만큼 밀고 · 다 못 밀리면 목책도 그만큼만.</summary>
IEnumerator ShoveGimmick(GimmickInstance g, Vector2 dir, float dist, Unit pusher, ActionOutcome o)
{
float travel = ShapeTravel(g, dir, dist);
if (travel < 0.01f) yield break;
var moved = g.Shape.Translated(dir * travel);
int running = 0;
foreach (var u in battle.Units)
{
if (u == null || !u.IsAlive || u == pusher) continue;
if (moved.SignedDistance(u.Position) >= u.Radius) continue;
float need = 0f;
while (need < travel + u.Radius * 2f && moved.SignedDistance(u.Position + dir * need) < u.Radius + 0.01f) need += 0.02f;
running++;
Fx.Run(Track(Push(u, dir, need + 0.02f, pusher, o), () => running--));
}
while (running > 0) yield return null;
float allowed = travel;
foreach (var u in battle.Units)
{
if (u == null || !u.IsAlive || u == pusher) continue;
while (allowed > 0f && g.Shape.Translated(dir * allowed).SignedDistance(u.Position) < u.Radius - 0.01f) allowed -= 0.02f;
}
allowed = Mathf.Max(0f, allowed);
if (allowed < 0.01f) yield break;
Vector2 from = g.Shape.Center;
if (!Sim.Active && g.Go != null) yield return Fx.MoveAlong(g.Go.transform, from, from + dir * allowed, cfg.moveSpeed, null);
g.MoveBy(dir * allowed);
o.Notes.Add(g.Def.nameKo + " 밀림");
battle.NotifyChanged();
}
static float ShapeTravel(GimmickInstance g, Vector2 dir, float dist)
{
Physics2D.SyncTransforms();
var s = g.Shape;
RaycastHit2D[] hits = s.Type == GimmickShapeType.Rect
? Physics2D.BoxCastAll(s.Center, s.Size * 0.98f, s.Rot, dir, dist)
: Physics2D.CircleCastAll(s.Center, (s.Type == GimmickShapeType.Circle ? s.R : s.BoundingRadius) * 0.95f, dir, dist);
float min = dist;
foreach (var h in hits)
{
if (g.Colliders.Contains(h.collider)) continue;
if (h.collider.GetComponent<BoardWall>() != null || GimmickLayer.BlocksMove(h.collider)) min = Mathf.Min(min, h.distance);
}
return Mathf.Max(0f, min - Combat.Epsilon);
}
/// <summary>기병 관통 옆 밀기: 늪·강 안 = 0 · 빙판 위 = ×p2 · 장애물·함정에서 멈춤 · 강가·다리에서 강물로 밀리면 빠짐.</summary>
Vector2 KnockTo(Unit e, Vector2 dir, float kb)
{
if (Gim) kb *= Layer.KnockMult(e);
Vector2 to = battle.Board.ClampInside(e.Position + dir * kb, e.Radius);
if (!Gim || kb < 0.01f) return to;
float want = Vector2.Distance(e.Position, to);
float free = want;
float h;
Vector2 n;
if (Combat.FirstGimmickOnPath(e, e.Position, dir, want, out h, out n) != null) free = Mathf.Max(0f, h - Combat.Epsilon);
free = Mathf.Min(free, Layer.TrapStop(e, e.Position, dir, want));
NoteForced(e, e.Position, e.Position + dir * free);
return e.Position + dir * free;
}
// ---------- 투사체 · 책사 광역 ----------
/// <summary>책사 광역 계획: 중심 = 첫 피격 유닛 · 유닛이 없고 제거 가능 기믹(덤불·수레)에 막혔으면 그 자리 · 성문·성벽 너머 제외 · 지울 기믹.</summary>
void AimSplash(Unit u, ShotPlan p)
{
p.Splash.Clear();
p.ClearGimmicks.Clear();
p.HasSplash = false;
if (p.Hit != null) p.SplashCenter = p.Hit.Position;
else if (p.HitGimmick != null && p.HitGimmick.Def.strategistClear) p.SplashCenter = p.To;
else return;
p.HasSplash = true;
p.SplashRadius = SkillMods.SplashRadius(u, cfg); // 안개 = 반경은 그대로 · 원거리 피해 ×p1(Combat.Damage · PD 2026-11-06 개정)
p.Splash.AddRange(Combat.UnitsInCircle(p.SplashCenter, p.SplashRadius, p.Hit, u));
if (!Gim) return;
p.Splash.RemoveAll(s => Layer.SplashBlocked(p.SplashCenter, s.Position));
p.ClearGimmicks.AddRange(Layer.ClearableIn(p.SplashCenter, p.SplashRadius));
}
/// <summary>투사체가 기믹에 막힘: 수레 = 파괴(폭발) · 그 밖 = 막힘(피해 없음).</summary>
IEnumerator ArrowHitsGimmick(Unit u, ShotPlan p, ActionOutcome o)
{
var g = p.HitGimmick;
if (g == null) yield break;
if (g.Def.projectile == GimmickProjectile.Destroy) yield return DamageGimmick(g, Mathf.Max(1, g.Hp), u, o);
else
{
o.Notes.Add(g.Def.nameKo + "에 막힘");
if (!Sim.Active) Fx.Popup(p.To, "막힘", new Color(0.85f, 0.85f, 0.85f));
}
}
/// <summary>책사 광역의 기믹: 덤불 제거 · 수레 파괴(폭발) · 그 반경만큼 안개 걷힘(PD 2026-11-06) · 화공 스테이지 = 그 자리 불.</summary>
IEnumerator SplashGimmicks(Unit u, ShotPlan p, ActionOutcome o)
{
if (!Gim || !p.HasSplash) yield break;
foreach (var g in p.ClearGimmicks)
{
if (!g.BlocksMove) continue;
if (g.Def.projectile == GimmickProjectile.Destroy) yield return DamageGimmick(g, Mathf.Max(1, g.Hp), u, o);
else
{
Layer.Remove(g);
o.Notes.Add(g.Def.nameKo + " 제거");
}
}
if (Layer.ClearFog(p.SplashCenter, p.SplashRadius)) o.Notes.Add("안개 걷힘");
if (Layer.OnSplash(p.SplashCenter, p.SplashRadius) != null) o.Notes.Add("불길");
}
/// <summary>빙판: 사격 전 이동이 빙판에서 끝나면 미끄러짐 → 사거리 밖이면 사격 취소(PD #1097 §9 ③ 제안) · 아니면 새 자리에서 다시 조준.</summary>
IEnumerator SlideBeforeShot(Unit u, Vector2 dir, ShotPlan p, Vector2 target, ActionOutcome o)
{
if (!Gim) yield break;
float s = Layer.SlideDistance(u.Position, dir);
if (s < 0.05f) yield break;
Layer.Text(u, "미끄러짐", IceTextColor);
o.Notes.Add(u.DisplayName + " 미끄러짐");
yield return MoveOrHit(u, u.Position, dir, s, o);
p.From = u.Position;
float range = SkillMods.Range(u);
float gap = Vector2.Distance(u.Position, p.Target.Position) - u.Radius * Combat.CastRadiusScale - p.Target.Radius;
if (!p.Target.IsAlive || gap > range + 0.05f)
{
p.Fires = false;
o.Notes.Add("사거리 이탈 · 사격 취소");
yield break;
}
Aim(u, p, dir, target, range);
}
// ---------- 행동 끝 ----------
/// <summary>행동 끝(기믹): 행동 유닛 미끄러짐(행동당 1회 · 최대 p1) → 밀린 유닛 미끄러짐 → 지대 스윕(독·불·함정) → 다리 → 강.</summary>
IEnumerator GimmickAfter(Unit u, bool moveOnly, ActionOutcome o, bool slideMover)
{
if (!Gim) yield break;
Vector2 dir;
if (slideMover && u.IsAlive && LastDir(o.Path, out dir))
{
float s = Layer.SlideDistance(u.Position, dir);
if (s >= 0.05f)
{
Layer.Text(u, "미끄러짐", IceTextColor);
o.Notes.Add(u.DisplayName + " 미끄러짐");
yield return SlideMover(u, dir, s, moveOnly, o);
}
}
var seen = new HashSet<Unit>();
for (int i = o.Displaced.Count - 1; i >= 0; i--)
{
var d = o.Displaced[i];
if (d.Unit == null || d.Unit == u || !d.Unit.IsAlive || !seen.Add(d.Unit)) continue;
Vector2 dd = d.To - d.From;
if (dd.sqrMagnitude < 1e-4f) continue;
float s = Layer.SlideDistance(d.Unit.Position, dd);
if (s < 0.05f) continue;
Layer.Text(d.Unit, "미끄러짐", IceTextColor);
yield return Push(d.Unit, dd.normalized, s, null, o);
}
Layer.ApplyZones(u, o);
battle.NotifyChanged();
}
static bool LastDir(List<Vector2> path, out Vector2 dir)
{
dir = Vector2.zero;
for (int i = path.Count - 1; i > 0; i--)
{
Vector2 d = path[i] - path[i - 1];
if (d.sqrMagnitude < 1e-4f) continue;
dir = d.normalized;
return true;
}
return false;
}
/// <summary>행동 유닛 미끄러짐 = 같은 방향으로 병종 루틴을 미끄러질 거리만큼 다시(충돌 규칙 그대로) · 명중 보상·복귀·견제는 원래 행동 것만.</summary>
IEnumerator SlideMover(Unit u, Vector2 dir, float s, bool moveOnly, ActionOutcome o)
{
bool bonus = o.BonusMove, ret = o.Returned, ow = o.Overwatch, moved = o.MovedOnly, battack = o.BonusAttack;
int bonusN = o.BonusMoves;
bool unitOw = u.Overwatch;
Vector2 from = u.Position;
Vector2 target = from + dir * s;
switch (u.Type)
{
case UnitType.Infantry:
yield return InfantryRoutine(u, from, dir, s, target, moveOnly, o);
break;
case UnitType.Spear:
yield return SpearRoutine(u, from, dir, s, target, moveOnly, o);
break;
case UnitType.Cavalry:
yield return CavalryRoutine(u, from, dir, s, target, moveOnly, o, new List<Unit>(o.Attacked));
break;
default:
yield return MoveOrHit(u, from, dir, s, o);
break;
}
o.BonusMove = bonus;
o.Returned = ret;
o.Overwatch = ow;
o.MovedOnly = moved && o.Attacked.Count == 0;
o.BonusAttack = battack;
o.BonusMoves = bonusN;
if (u.Overwatch != unitOw) u.SetOverwatch(unitOw);
}
}
}