313 lines
12 KiB
C#
313 lines
12 KiB
C#
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
namespace TKS
|
|
{
|
|
/// <summary>슬링샷 한 번의 결과 요약.</summary>
|
|
public class ActionOutcome
|
|
{
|
|
public bool BonusMove; // 보병: 최종 목표점 명중 → 추가 이동
|
|
public bool Returned; // 기병: 최종 목표점 명중 → 제자리 복귀
|
|
public bool Overwatch; // 창병: 최종 목표점 명중 → 대기(견제)
|
|
public bool Crit; // 궁병: 최종 목표점 명중 → 크리티컬
|
|
public bool MovedOnly; // 공격 없이 이동만
|
|
public int DamageDealt;
|
|
public int CounterTaken;
|
|
public List<Unit> Attacked = new List<Unit>();
|
|
/// <summary>실제로 지나간 경로(시작 포함). 창병 견제 판정용.</summary>
|
|
public List<Vector2> Path = new List<Vector2>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 슬링샷 벡터(시작점 → 목표점)를 병종 규칙대로 이동·공격으로 해석한다.
|
|
/// ① 보병: 경로 위 첫 적까지 이동 후 공격 · 목표점 명중 시 추가 이동 1회
|
|
/// ② 기병: 경로 위 적을 모두 관통 피해 · 목표점까지 이동 · 목표점 명중 시 제자리 복귀
|
|
/// ③ 창병: 보병과 동일 · 목표점 명중 시 대기 → 반경 안에서 움직이는 적에게 피해
|
|
/// ④ 궁병: 목표 방향에 적이 있으면 사거리까지 이동 후 사격(반격 없음) · 목표점 명중 시 크리티컬
|
|
/// </summary>
|
|
public class ActionResolver
|
|
{
|
|
readonly BalanceConfig cfg;
|
|
readonly BattleManager battle;
|
|
readonly System.Random rng;
|
|
|
|
public ActionResolver(BalanceConfig cfg, BattleManager battle, System.Random rng)
|
|
{
|
|
this.cfg = cfg;
|
|
this.battle = battle;
|
|
this.rng = rng;
|
|
}
|
|
|
|
public IEnumerator Resolve(Unit u, Vector2 target, bool moveOnly, ActionOutcome o)
|
|
{
|
|
Vector2 origin = u.Position;
|
|
Vector2 delta = target - origin;
|
|
float dist = delta.magnitude;
|
|
o.Path.Add(origin);
|
|
if (dist < 0.001f || !u.IsAlive) yield break;
|
|
Vector2 dir = delta / dist;
|
|
|
|
switch (u.Type)
|
|
{
|
|
case UnitType.Infantry:
|
|
case UnitType.Spear:
|
|
yield return MeleeRoutine(u, origin, dir, dist, target, moveOnly, o);
|
|
break;
|
|
case UnitType.Cavalry:
|
|
yield return CavalryRoutine(u, origin, dir, dist, target, moveOnly, o);
|
|
break;
|
|
case UnitType.Archer:
|
|
yield return ArcherRoutine(u, origin, dir, dist, target, moveOnly, o);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// ---------- 미리보기(드라이런) ----------
|
|
|
|
/// <summary>조작 중 표시용: 이 슬링샷이 공격하게 될 적 목록.</summary>
|
|
public List<Unit> PredictTargets(Unit u, Vector2 target, bool moveOnly)
|
|
{
|
|
var result = new List<Unit>();
|
|
if (moveOnly) return result;
|
|
Vector2 origin = u.Position;
|
|
Vector2 delta = target - origin;
|
|
float dist = delta.magnitude;
|
|
if (dist < 0.001f) return result;
|
|
Vector2 dir = delta / dist;
|
|
float hitDist;
|
|
switch (u.Type)
|
|
{
|
|
case UnitType.Infantry:
|
|
case UnitType.Spear:
|
|
{
|
|
var first = Combat.FirstEnemyOnPath(u, origin, dir, dist, out hitDist);
|
|
if (first != null && Combat.FreeTravel(u, origin, dir, hitDist, false) >= hitDist - 0.05f) result.Add(first);
|
|
break;
|
|
}
|
|
case UnitType.Cavalry:
|
|
{
|
|
float travel = Combat.FreeTravel(u, origin, dir, dist, false);
|
|
var dists = new List<float>();
|
|
result.AddRange(Combat.EnemiesOnPath(u, origin, dir, travel, dists));
|
|
break;
|
|
}
|
|
case UnitType.Archer:
|
|
{
|
|
var first = Combat.FirstEnemyOnPath(u, origin, dir, dist + u.AttackRange, out hitDist);
|
|
if (first != null) result.Add(first);
|
|
break;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// ---------- 보병 · 창병 ----------
|
|
|
|
IEnumerator MeleeRoutine(Unit u, Vector2 origin, Vector2 dir, float dist, Vector2 target, bool moveOnly, ActionOutcome o)
|
|
{
|
|
float hitDist = 0f;
|
|
Unit first = moveOnly ? null : Combat.FirstEnemyOnPath(u, origin, dir, dist, out hitDist);
|
|
if (first == null)
|
|
{
|
|
float free = Combat.FreeTravel(u, origin, dir, dist, true);
|
|
yield return Move(u, origin + dir * free, o);
|
|
o.MovedOnly = true;
|
|
yield break;
|
|
}
|
|
|
|
float freeToEnemy = Combat.FreeTravel(u, origin, dir, hitDist, false);
|
|
if (freeToEnemy < hitDist - 0.05f)
|
|
{
|
|
// 아군·벽이 먼저 막는다 → 거기까지만 이동
|
|
yield return Move(u, origin + dir * freeToEnemy, o);
|
|
o.MovedOnly = true;
|
|
yield break;
|
|
}
|
|
|
|
yield return Move(u, origin + dir * hitDist, o);
|
|
Unit finalHit = Combat.EnemyAtPoint(u, target, cfg.finalHitTolerance);
|
|
bool isFinal = finalHit == first;
|
|
yield return Attack(u, first, false, false, o);
|
|
if (!u.IsAlive) yield break;
|
|
|
|
if (u.Type == UnitType.Infantry && isFinal) o.BonusMove = true;
|
|
if (u.Type == UnitType.Spear && isFinal)
|
|
{
|
|
u.SetOverwatch(true);
|
|
o.Overwatch = true;
|
|
}
|
|
}
|
|
|
|
// ---------- 기병 ----------
|
|
|
|
IEnumerator CavalryRoutine(Unit u, Vector2 origin, Vector2 dir, float dist, Vector2 target, bool moveOnly, ActionOutcome o)
|
|
{
|
|
float travel = Combat.FreeTravel(u, origin, dir, dist, false);
|
|
var dists = new List<float>();
|
|
var enemies = moveOnly ? new List<Unit>() : Combat.EnemiesOnPath(u, origin, dir, travel, dists);
|
|
Unit finalHit = moveOnly ? null : Combat.EnemyAtPoint(u, target, cfg.finalHitTolerance);
|
|
bool willReturn = finalHit != null && enemies.Contains(finalHit);
|
|
|
|
Vector2 end = origin + dir * travel;
|
|
if (!willReturn) end = BackOffFromEnemies(u, origin, dir, travel);
|
|
|
|
if (enemies.Count == 0)
|
|
{
|
|
yield return Move(u, end, o);
|
|
o.MovedOnly = true;
|
|
yield break;
|
|
}
|
|
|
|
int idx = 0;
|
|
var hit = new List<Unit>();
|
|
yield return Fx.MoveAlong(u.transform, origin, end, cfg.moveSpeed, travelled =>
|
|
{
|
|
while (idx < enemies.Count && travelled >= dists[idx] - 0.01f)
|
|
{
|
|
PierceHit(u, enemies[idx], dir, o);
|
|
hit.Add(enemies[idx]);
|
|
idx++;
|
|
}
|
|
});
|
|
u.Position = end;
|
|
o.Path.Add(end);
|
|
for (; idx < enemies.Count; idx++)
|
|
{
|
|
if (dists[idx] <= travel + 0.01f)
|
|
{
|
|
PierceHit(u, enemies[idx], dir, o);
|
|
hit.Add(enemies[idx]);
|
|
}
|
|
}
|
|
yield return Fx.Wait(0.15f);
|
|
|
|
// 살아남은 근접 적의 반격
|
|
foreach (var e in hit)
|
|
{
|
|
if (!u.IsAlive) break;
|
|
if (!e.IsAlive || !UnitTypeInfo.IsMelee(e.Type)) continue;
|
|
int c = Combat.CounterDamage(cfg, e, u, rng);
|
|
u.TakeDamage(c);
|
|
o.CounterTaken += c;
|
|
Fx.Popup(u.Position, c.ToString(), new Color(1f, 0.45f, 0.45f));
|
|
battle.NotifyChanged();
|
|
yield return Fx.Wait(0.12f);
|
|
}
|
|
|
|
if (willReturn && u.IsAlive)
|
|
{
|
|
yield return Fx.Wait(0.1f);
|
|
yield return Move(u, origin, o);
|
|
o.Returned = true;
|
|
}
|
|
}
|
|
|
|
void PierceHit(Unit cav, Unit e, Vector2 dir, ActionOutcome o)
|
|
{
|
|
if (!e.IsAlive) return;
|
|
int dmg = Combat.Damage(cfg, cav, e, false, rng);
|
|
e.TakeDamage(dmg);
|
|
o.Attacked.Add(e);
|
|
o.DamageDealt += dmg;
|
|
Fx.Popup(e.Position, dmg.ToString(), Color.white);
|
|
// 옆으로 밀어내기
|
|
Vector2 side = new Vector2(-dir.y, dir.x);
|
|
float sign = Vector2.Dot(e.Position - cav.Position, side) >= 0f ? 1f : -1f;
|
|
if (e.IsAlive)
|
|
{
|
|
Vector2 to = battle.Board.ClampInside(e.Position + side * sign * cfg.cavalryKnockback, e.Radius);
|
|
Fx.Run(Fx.MoveAlong(e.transform, e.Position, to, cfg.moveSpeed * 0.6f, null));
|
|
}
|
|
battle.NotifyChanged();
|
|
}
|
|
|
|
/// <summary>종착점이 적과 겹치면 경로를 되짚어 겹치지 않는 지점까지 물러선다.</summary>
|
|
Vector2 BackOffFromEnemies(Unit u, Vector2 origin, Vector2 dir, float travel)
|
|
{
|
|
float t = travel;
|
|
for (int i = 0; i < 40 && t > 0f; i++)
|
|
{
|
|
Vector2 p = origin + dir * t;
|
|
bool overlap = false;
|
|
foreach (var e in battle.AliveUnits(u.Faction == Faction.Player ? Faction.Enemy : Faction.Player))
|
|
{
|
|
if (Vector2.Distance(p, e.Position) < u.Radius + e.Radius - 0.01f)
|
|
{
|
|
overlap = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!overlap) return p;
|
|
t -= 0.1f;
|
|
}
|
|
return origin;
|
|
}
|
|
|
|
// ---------- 궁병 ----------
|
|
|
|
IEnumerator ArcherRoutine(Unit u, Vector2 origin, Vector2 dir, float dist, Vector2 target, bool moveOnly, ActionOutcome o)
|
|
{
|
|
float hitDist;
|
|
Unit first = moveOnly ? null : Combat.FirstEnemyOnPath(u, origin, dir, dist + u.AttackRange, out hitDist);
|
|
if (first == null)
|
|
{
|
|
float free = Combat.FreeTravel(u, origin, dir, dist, true);
|
|
yield return Move(u, origin + dir * free, o);
|
|
o.MovedOnly = true;
|
|
yield break;
|
|
}
|
|
|
|
float centerDist = Vector2.Distance(origin, first.Position);
|
|
float need = Mathf.Clamp(centerDist - u.AttackRange, 0f, dist);
|
|
float freeMove = Combat.FreeTravel(u, origin, dir, need, true);
|
|
if (freeMove > 0.01f) yield return Move(u, origin + dir * freeMove, o);
|
|
|
|
if (Vector2.Distance(u.Position, first.Position) <= u.AttackRange + 0.05f)
|
|
{
|
|
bool crit = Combat.EnemyAtPoint(u, target, cfg.finalHitTolerance) == first;
|
|
yield return Attack(u, first, true, crit, o);
|
|
}
|
|
else
|
|
{
|
|
o.MovedOnly = true;
|
|
}
|
|
}
|
|
|
|
// ---------- 공통 ----------
|
|
|
|
IEnumerator Move(Unit u, Vector2 to, ActionOutcome o)
|
|
{
|
|
yield return Fx.MoveAlong(u.transform, u.Position, to, cfg.moveSpeed, null);
|
|
u.Position = to;
|
|
o.Path.Add(to);
|
|
}
|
|
|
|
IEnumerator Attack(Unit a, Unit d, bool ranged, bool crit, ActionOutcome o)
|
|
{
|
|
if (ranged) yield return Fx.Arrow(a.Position, d.Position);
|
|
else yield return Fx.Lunge(a, d.Position);
|
|
|
|
int dmg = Combat.Damage(cfg, a, d, crit, rng);
|
|
d.TakeDamage(dmg);
|
|
o.Attacked.Add(d);
|
|
o.DamageDealt += dmg;
|
|
o.Crit |= crit;
|
|
Fx.Popup(d.Position, crit ? dmg + "!" : dmg.ToString(), crit ? new Color(1f, 0.7f, 0.2f) : Color.white);
|
|
battle.NotifyChanged();
|
|
yield return Fx.Wait(0.2f);
|
|
|
|
if (!ranged && d.IsAlive && a.IsAlive)
|
|
{
|
|
int c = Combat.CounterDamage(cfg, d, a, rng);
|
|
yield return Fx.Lunge(d, a.Position);
|
|
a.TakeDamage(c);
|
|
o.CounterTaken += c;
|
|
Fx.Popup(a.Position, c.ToString(), new Color(1f, 0.45f, 0.45f));
|
|
battle.NotifyChanged();
|
|
yield return Fx.Wait(0.2f);
|
|
}
|
|
}
|
|
}
|
|
}
|