505 lines
25 KiB
C#
505 lines
25 KiB
C#
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);
|
||
}
|
||
}
|
||
}
|