using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace TKS
{
///
/// 지형 기믹 충돌 반응(PD #1097 · 지형기믹_기획서 §2~§4 · 병종×기믹 표):
/// 보병 = 내구 피해 + 튕김(알까기 자기 튕김 재사용 · bounce_pct = 자기 HP 피해) · 바위는 굴림 / 창병 = 내구 피해 + spear_push 만큼 밀고 정지(밀린 끝 유닛도 밀림) · 바위 굴림 /
/// 기병 = 관통 불가 → 거기서 정지(복귀 없음) · 내구 피해 + 튕김 + 자기 HP(cav_self_pct) · 바위 = 정지 / 궁·책 이동 = 피해 없이 튕김 ·
/// 투사체 = pass·block·destroy · 책사 광역 = 덤불 제거·수레 파괴·(화공) 불 생성 · 폭약 = 광역 파이프라인(아군 포함 + 밀기 + 불) ·
/// 행동 끝 = 빙판 미끄러짐(행동 유닛 = 병종 루틴 재실행 · 밀린 유닛 = 밀기) → 지대 스윕(독·불·함정) → 다리 → 강.
///
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; }
}
/// 근접 병종 슬링샷 도달점(늪 ×p1 · 고지 오르기 ×p3) — Resolve·조준 미리보기 공용. 원거리 병종은 접근·이동 단계에서 따로 계산.
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;
}
/// 조준 경로가 지형 이동 감소(늪·고지 오르기)에 걸리는가(맹획 남만왕 재행동 · 지형 무시 유닛도 지형은 「지남」으로 봄).
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;
}
/// 자기 행동 이동(궁·책 접근·이동만) = 조인 철벽 감속 → 지형 이동 비용(Fable 검토 B2 · 근접 병종은 ReachPoint).
static float MoveReach(Unit u, Vector2 origin, Vector2 dir, float budget)
{
return Reach(u, origin, dir, SlowZoneReach(u, origin, dir, budget));
}
/// 조인 철벽(zone_slow): 상대 시전자의 행동 반경(기본 슬링 거리) 안을 지나는 구간 = 이동 비용 × (1 + p1).
static float SlowZoneReach(Unit u, Vector2 origin, Vector2 dir, float budget)
{
var sys = SkillSystem.Current;
if (sys == null) return budget;
List> 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>();
zones.Add(new KeyValuePair(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();
}
/// 경로 위 첫 장애물 기믹 — limit(첫 유닛 접촉) · 전장 벽 · 숨은 함정보다 앞일 때만.
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;
}
/// 궁·책 등 이동만 하는 병종: 유닛 앞의 장애물이면 부딪힘(튕김) · 아니면 그대로 이동.
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);
}
/// 보병·창병·궁·책 이동이 장애물 기믹에 부딪힘.
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) · 면 바깥으로
}
/// 기병 관통이 장애물에 막힘(복귀 없음): 내구 피해 → 바위 = 정지 + cav_self_pct · 그 밖 = 튕김 + max(cav_self_pct, bounce_pct).
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);
}
///
/// 폭약 수레 폭발(광역 파이프라인 재사용): 반경 p1 안 유닛 전원(아군 포함 · 성문·성벽 너머 제외) 피해 = 터뜨린 유닛 공 × p2 · 바깥으로 p3 밀기 ·
/// 반경 안 다른 수레 연쇄 · 그 자리 불 지대(반경 p1 · 지속 = 스테이지 화공 배치 burn_turns · 없으면 정의 p2).
///
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(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);
}
/// 바위 굴림: 바위 원을 dir 로 dist 만큼(벽·다른 장애물·유닛에서 멈춤) · 상대 유닛에 닿으면 공 × p1 피해 + p2 밀기.
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() != null || GimmickLayer.BlocksMove(h.collider))
{
travel = h.distance;
break;
}
var v = h.collider.GetComponent();
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();
}
/// 목책 밀기(창병): spear_push 만큼(벽·다른 장애물에서 멈춤) · 새 자리와 겹칠 유닛은 먼저 그만큼 밀고 · 다 못 밀리면 목책도 그만큼만.
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() != null || GimmickLayer.BlocksMove(h.collider)) min = Mathf.Min(min, h.distance);
}
return Mathf.Max(0f, min - Combat.Epsilon);
}
/// 기병 관통 옆 밀기: 늪·강 안 = 0 · 빙판 위 = ×p2 · 장애물·함정에서 멈춤 · 강가·다리에서 강물로 밀리면 빠짐.
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;
}
// ---------- 투사체 · 책사 광역 ----------
/// 책사 광역 계획: 중심 = 첫 피격 유닛 · 유닛이 없고 제거 가능 기믹(덤불·수레)에 막혔으면 그 자리 · 성문·성벽 너머 제외 · 지울 기믹.
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));
}
/// 투사체가 기믹에 막힘: 수레 = 파괴(폭발) · 그 밖 = 막힘(피해 없음).
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));
}
}
/// 책사 광역의 기믹: 덤불 제거 · 수레 파괴(폭발) · 그 반경만큼 안개 걷힘(PD 2026-11-06) · 화공 스테이지 = 그 자리 불.
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("불길");
}
/// 빙판: 사격 전 이동이 빙판에서 끝나면 미끄러짐 → 사거리 밖이면 사격 취소(PD #1097 §9 ③ 제안) · 아니면 새 자리에서 다시 조준.
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);
}
// ---------- 행동 끝 ----------
/// 행동 끝(기믹): 행동 유닛 미끄러짐(행동당 1회 · 최대 p1) → 밀린 유닛 미끄러짐 → 지대 스윕(독·불·함정) → 다리 → 강.
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();
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 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;
}
/// 행동 유닛 미끄러짐 = 같은 방향으로 병종 루틴을 미끄러질 거리만큼 다시(충돌 규칙 그대로) · 명중 보상·복귀·견제는 원래 행동 것만.
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(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);
}
}
}