ThreeKingdomsSlingshot/Assets/Scripts/Runtime/Skills/SkillSystem.cs

731 lines
28 KiB
C#
Raw 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>유닛 1기의 스킬 상태(액티브 쿨·예약 · 패시브 · 행동 뒤에도 남는 효과).</summary>
public class UnitSkills
{
public Unit Unit;
public string HeroId = "";
/// <summary>무장(영웅·보스·엘리트 장수) — 참장·파로(dmg_vs_general_mult) 대상 판정.</summary>
public bool IsGeneral;
public SkillDef Active;
public int ActiveLv = 1;
public List<SkillEffect> ActiveEffects;
public SkillDef Passive;
public int PassiveLv = 1;
public float PassiveMult = 1f;
public List<SkillEffect> PassiveEffects;
/// <summary>액티브를 쓸 수 있는 첫 아군 턴(전투 시작 = 1 · 사용 턴 + 쿨).</summary>
public int ReadyTurn = 1;
/// <summary>next_action 예약(다음 슬링샷 1회) · 행동 전 다시 탭 = 취소.</summary>
public bool Reserved;
/// <summary>예약한 효과가 지금 걸려 있다(그 유닛의 행동 처리·조준 미리보기 동안만).</summary>
public bool Armed;
/// <summary>추가 이동: 지금 것 다음에 더 남은 횟수 · 추가 이동에서 공격 가능.</summary>
public int BonusMovesLeft;
public bool BonusAttack;
/// <summary>대기(견제) 중 액티브로 얻은 반경 배율·가산·무제한(그 적 턴까지).</summary>
public float OwMult = 1f;
public float OwAdd;
public bool OwUnlimited;
/// <summary>전투 중 관통한 적 누적(장료 팔백 돌격).</summary>
public int PierceHits;
public bool HasActive
{
get { return Active != null && ActiveEffects != null; }
}
public int CooldownLeft(int turn)
{
return Mathf.Max(0, ReadyTurn - turn);
}
}
/// <summary>스킬 효과 1건이 어느 유닛·스킬에서 왔는가(SkillMods 질의 결과).</summary>
public struct SkillHit
{
public UnitSkills Owner;
public SkillDef Def;
public SkillEffect E;
public bool Passive;
}
/// <summary>
/// 전투 1판의 스킬 처리(PD #1093 B · 시스템 기획 §5): 유닛별 스킬 상태 · 즉시형 액티브의 시한 효과(이번 아군 턴 / 이번 적 턴) ·
/// 행동 전후 훅(예약 효과 장착·쿨 소모) · 피해 적용(호위 분담) · HUD 한 줄 기록. 수치 질의는 SkillMods.
/// </summary>
public class SkillSystem
{
public static SkillSystem Current;
public class Timed
{
public UnitSkills Owner;
public Faction Faction;
public SkillDef Def;
public SkillEffect E;
/// <summary>true = 이번 적 턴에만 걸림(다음 아군 턴 시작에 사라짐) · false = 이번 아군 턴(적 턴 시작에 사라짐).</summary>
public bool EnemyWindow;
/// <summary>대상 고정(장판 일갈 = 밀어낸 적) · null = 키 규칙대로.</summary>
public HashSet<Unit> Targets;
}
public struct LogLine
{
public string Text;
public float Time;
}
readonly BattleManager battle;
readonly Dictionary<Unit, UnitSkills> map = new Dictionary<Unit, UnitSkills>();
readonly List<UnitSkills> entries = new List<UnitSkills>();
public readonly List<Timed> TimedEffects = new List<Timed>();
public readonly List<LogLine> Log = new List<LogLine>();
readonly HashSet<string> noted = new HashSet<string>();
readonly int[] startCount = new int[2];
/// <summary>장합 변진: 목표 지점 탭 대기 중인 유닛.</summary>
public Unit PendingTeleport { get; private set; }
public SkillSystem(BattleManager b)
{
battle = b;
}
public BattleManager Battle
{
get { return battle; }
}
public IReadOnlyList<UnitSkills> Entries
{
get { return entries; }
}
// ---------- 부착 ----------
public UnitSkills Of(Unit u)
{
if (u == null) return null;
UnitSkills s;
return map.TryGetValue(u, out s) ? s : null;
}
UnitSkills Ensure(Unit u)
{
var s = Of(u);
if (s != null) return s;
s = new UnitSkills { Unit = u };
map[u] = s;
entries.Add(s);
return s;
}
/// <summary>스킬 부착(액티브·패시브 id · 레벨 · ★5 패시브 배율 · 무장 여부). 최대 HP 패시브(hp_max_pct)는 바로 반영.</summary>
public UnitSkills Attach(Unit u, string activeId, int activeLv, string passiveId, int passiveLv, float passiveMult, bool general)
{
if (u == null) return null;
var s = Ensure(u);
s.IsGeneral = general;
s.Active = SkillCatalog.Get(activeId);
s.ActiveLv = Mathf.Max(1, activeLv);
s.ActiveEffects = s.Active != null ? s.Active.ScaledEffects(s.ActiveLv, 1f) : null;
s.Passive = SkillCatalog.Get(passiveId);
s.PassiveLv = Mathf.Max(1, passiveLv);
s.PassiveMult = passiveMult > 0f ? passiveMult : 1f;
s.PassiveEffects = s.Passive != null ? s.Passive.ScaledEffects(s.PassiveLv, s.PassiveMult) : null;
if (s.Passive != null) s.HeroId = s.Passive.HeroId;
else if (s.Active != null) s.HeroId = s.Active.HeroId;
float hp = SkillMods.Sum(u, SkillKeys.HpMaxPct);
if (Mathf.Abs(hp) > 1e-4f)
{
u.MaxHp = Mathf.Max(1, Mathf.RoundToInt(u.MaxHp * (1f + hp)));
u.Hp = u.MaxHp;
if (u.View != null) u.View.Refresh();
}
return s;
}
/// <summary>heroes.csv 영웅 id 로 부착(테스트·도구용).</summary>
public UnitSkills AttachHero(Unit u, string heroId, int activeLv = 1, int passiveLv = 1, float passiveMult = 1f, bool general = true)
{
var h = SkillCatalog.Hero(heroId);
if (h == null) return null;
return Attach(u, h.skillActive, activeLv, h.skillPassive, passiveLv, passiveMult, general && !h.isSoldier);
}
/// <summary>출전 칸(편성 → 스킬 id·레벨·★ 배율이 채워져 있음).</summary>
public UnitSkills AttachLineup(Unit u, LineupEntry e)
{
if (e == null) return null;
if (string.IsNullOrEmpty(e.skillActive) && string.IsNullOrEmpty(e.skillPassive))
{
if (e.general) Ensure(u).IsGeneral = true;
return Of(u);
}
return Attach(u, e.skillActive, e.activeLv, e.skillPassive, e.passiveLv, e.passiveMult, e.general);
}
/// <summary>적 무장(보스·엘리트) = heroes.csv 에 같은 아트·이름 영웅이 있으면 그 패시브만(Lv1 · 액티브 AI 사용은 2차 · PD 확인 대상).</summary>
public UnitSkills AttachEnemy(Unit u, BalanceConfig.EnemySpawn e)
{
if (u == null || e == null || !(e.isBoss || e.isGeneral)) return null;
var h = SkillCatalog.HeroForEnemy(e.art, e.name);
if (h == null)
{
var s = Ensure(u);
s.IsGeneral = true;
return s;
}
return Attach(u, null, 1, h.skillPassive, 1, 1f, true);
}
public void MarkGeneral(Unit u)
{
if (u != null) Ensure(u).IsGeneral = true;
}
public bool IsGeneral(Unit u)
{
if (u == null) return false;
if (u.IsBoss) return true;
var s = Of(u);
return s != null && s.IsGeneral;
}
public void Clear()
{
map.Clear();
entries.Clear();
TimedEffects.Clear();
noted.Clear();
PendingTeleport = null;
startCount[0] = startCount[1] = 0;
}
/// <summary>전투 시작(배치 뒤): 출전 인원 기록(패자 · team_atk_pct_full) · 쿨 = 바로 사용 가능 · 시한 효과 비움.</summary>
public void BeginBattle()
{
Prune();
TimedEffects.Clear();
noted.Clear();
PendingTeleport = null;
startCount[0] = battle.AliveCount(Faction.Player);
startCount[1] = battle.AliveCount(Faction.Enemy);
foreach (var s in entries)
{
s.ReadyTurn = 1;
s.Reserved = false;
s.Armed = false;
s.BonusMovesLeft = 0;
s.BonusAttack = false;
s.OwMult = 1f;
s.OwAdd = 0f;
s.OwUnlimited = false;
s.PierceHits = 0;
}
}
public int StartCount(Faction f)
{
return startCount[(int)f];
}
void Prune()
{
for (int i = entries.Count - 1; i >= 0; i--)
{
if (entries[i].Unit != null) continue;
entries.RemoveAt(i);
}
var dead = new List<Unit>();
foreach (var kv in map)
if (kv.Key == null) dead.Add(kv.Key);
foreach (var k in dead) map.Remove(k);
}
// ---------- 턴 ----------
/// <summary>적 턴 시작: 이번 아군 턴 효과(동남풍 · 고육계)와 쓰지 않은 예약·추가 이동 정리.</summary>
public void OnEnemyTurnStart()
{
TimedEffects.RemoveAll(t => !t.EnemyWindow);
PendingTeleport = null;
foreach (var s in entries)
{
s.Reserved = false;
s.Armed = false;
s.BonusMovesLeft = 0;
s.BonusAttack = false;
}
}
/// <summary>아군 턴 시작: 이번 적 턴 효과(호위 · 철벽 · 간웅 · 일갈 · 안개)와 아군 견제 강화 정리.</summary>
public void OnPlayerTurnStart()
{
TimedEffects.RemoveAll(t => t.EnemyWindow);
foreach (var s in entries)
{
if (s.Unit == null || s.Unit.Faction != Faction.Player) continue;
s.OwMult = 1f;
s.OwAdd = 0f;
s.OwUnlimited = false;
}
}
public bool WindowActive(Timed t)
{
return !t.EnemyWindow || battle.Phase == BattlePhase.EnemyTurn;
}
// ---------- 액티브: 상태 · 탭 ----------
public enum ActiveState
{
None,
Ready,
Reserved,
Cooldown,
Acted,
BonusMove,
NotTurn,
Targeting
}
public ActiveState StateOf(Unit u)
{
var s = Of(u);
if (u == null || s == null || !s.HasActive || u.Faction != Faction.Player || !u.IsAlive) return ActiveState.None;
if (PendingTeleport == u) return ActiveState.Targeting;
if (s.Reserved) return ActiveState.Reserved;
if (battle.Phase != BattlePhase.PlayerTurn) return ActiveState.NotTurn;
if (s.CooldownLeft(battle.TurnNumber) > 0) return ActiveState.Cooldown;
if (s.Active.Trigger == SkillTrigger.NextAction)
{
if (u.BonusMoveAvailable) return ActiveState.BonusMove;
if (!battle.CanAct(u)) return ActiveState.Acted;
}
return ActiveState.Ready;
}
/// <summary>HUD 스킬 버튼 탭: next_action = 예약/취소 · instant = 즉시 발동(변진 = 목표 지점 탭 대기). 반환 = 안내 문구.</summary>
public string TapActive(Unit u)
{
var s = Of(u);
var st = StateOf(u);
switch (st)
{
case ActiveState.None: return null;
case ActiveState.Targeting:
PendingTeleport = null;
return s.Active.NameKo + " 취소";
case ActiveState.Reserved:
s.Reserved = false;
if (u.View != null) u.View.Refresh();
return s.Active.NameKo + " 예약 취소(쿨 소모 없음)";
case ActiveState.NotTurn: return "아군 턴에만 쓸 수 있습니다";
case ActiveState.Cooldown: return s.Active.NameKo + " — 쿨 " + s.CooldownLeft(battle.TurnNumber) + "턴";
case ActiveState.Acted: return s.Active.NameKo + " — 이미 행동한 유닛";
case ActiveState.BonusMove: return s.Active.NameKo + " — 추가 이동 중에는 예약할 수 없음";
}
if (s.Active.Trigger == SkillTrigger.NextAction)
{
s.Reserved = true;
if (u.View != null) u.View.Refresh();
return s.Active.NameKo + " 예약 — 다음 슬링샷에 발동 · 다시 탭 = 취소";
}
if (s.Active.First(SkillKeys.Teleport) != null)
{
PendingTeleport = u;
return s.Active.NameKo + " — 반경 " + TeleportRadius(s).ToString("0.#") + " 안 빈 곳을 탭(다른 곳 = 취소)";
}
battle.RunSkillRoutine(Instant(s));
return s.Active.NameKo + " 발동!";
}
// ---------- 행동 훅(ActionResolver.Resolve) ----------
/// <summary>행동 시작: 예약한 next_action 효과를 장착(이동만인 추가 이동은 제외) · 발동 연출.</summary>
public void BeginAction(Unit u, bool moveOnly, ActionOutcome o)
{
noted.Clear();
var s = Of(u);
if (s == null || !s.Reserved || !s.HasActive || moveOnly) return;
s.Armed = true;
o.SkillUsed = s.Active;
Announce(s, s.Active);
}
/// <summary>행동 끝: 예약 소모 · 쿨(아군 턴 기준) · 견제 강화 이월.</summary>
public void EndAction(Unit u, ActionOutcome o)
{
var s = Of(u);
if (s == null || !s.Armed) return;
if (o.Overwatch)
{
foreach (var e in s.ActiveEffects)
{
if (e.Key == SkillKeys.OverwatchRadiusMult) s.OwMult *= e.P;
else if (e.Key == SkillKeys.OverwatchRadiusAdd) s.OwAdd += e.P;
else if (e.Key == SkillKeys.OverwatchUnlimited) s.OwUnlimited = true;
}
}
s.Armed = false;
s.Reserved = false;
s.ReadyTurn = battle.TurnNumber + s.Active.Cooldown;
if (u.View != null) u.View.Refresh();
}
/// <summary>조준 미리보기 동안만 예약 효과를 장착(사거리 · 사선 · 광역 표시). 반환 = 복원값.</summary>
public bool PreviewBegin(Unit u)
{
var s = Of(u);
if (s == null) return false;
bool prev = s.Armed;
if (s.Reserved && !u.BonusMoveAvailable) s.Armed = true;
return prev;
}
public void PreviewEnd(Unit u, bool prev)
{
var s = Of(u);
if (s != null) s.Armed = prev;
}
// ---------- 추가 이동 ----------
public bool BonusCanAttack(Unit u)
{
var s = Of(u);
return s != null && s.BonusAttack;
}
/// <summary>행동 뒤 추가 이동을 (또) 주는가 — 처음 명중 = 추가 이동 n 회(인덕) · 추가 이동 뒤 = 남은 횟수.</summary>
public bool NextBonus(Unit u, bool wasBonus, ActionOutcome o)
{
var s = Of(u);
if (!wasBonus)
{
if (!o.BonusMove) return false;
if (s != null)
{
s.BonusMovesLeft = Mathf.Max(0, o.BonusMoves - 1);
s.BonusAttack = o.BonusAttack;
}
return true;
}
if (s != null && s.BonusMovesLeft > 0)
{
s.BonusMovesLeft--;
return true;
}
ClearBonus(u);
return false;
}
public void ClearBonus(Unit u)
{
var s = Of(u);
if (s == null) return;
s.BonusMovesLeft = 0;
s.BonusAttack = false;
}
// ---------- 적 턴: 교전 반응 · 교전 범위 ----------
/// <summary>이번 적 턴에 교전 반응이 없는가(장판 일갈 공포 · 태평요술 안개).</summary>
public bool Suppressed(Unit e)
{
if (e == null) return false;
foreach (var t in TimedEffects)
{
if (!WindowActive(t)) continue;
if (t.E.Key == SkillKeys.AoePushFear && t.Targets != null && t.Targets.Contains(e)) return true;
if (t.E.Key == SkillKeys.EnemyAggroOffTurn && t.Faction != e.Faction) return true;
}
return false;
}
/// <summary>상대 진영 패시브(신산)가 actingFaction 의 교전 범위에 더하는 값.</summary>
public float AggroAdd(Faction actingFaction)
{
float add = 0f;
foreach (var s in entries)
{
if (s.Unit == null || !s.Unit.IsAlive || s.Unit.Faction == actingFaction || s.PassiveEffects == null) continue;
foreach (var e in s.PassiveEffects)
{
if (e.Key != SkillKeys.EnemyAggroAdd) continue;
add += e.P;
NotePassive(s, s.Passive);
}
}
return add;
}
// ---------- 피해 적용(호위) ----------
/// <summary>피해 적용 — 관평 호위(이번 적 턴 반경 안 같은 편 피해 일부를 관평이 대신) 반영. 반환 = d 가 실제 받은 피해.</summary>
public int Hit(Unit d, int dmg)
{
if (d == null || !d.IsAlive || dmg <= 0) return 0;
foreach (var t in TimedEffects)
{
if (t.E.Key != SkillKeys.GuardShare || !WindowActive(t)) continue;
var g = t.Owner.Unit;
if (g == null || !g.IsAlive || g == d || g.Faction != d.Faction) continue;
float r = t.E.HasA2 ? t.E.A2 : 0f;
if (Vector2.Distance(g.Position, d.Position) > r + d.Radius) continue;
int share = Mathf.Clamp(Mathf.RoundToInt(dmg * Mathf.Clamp01(t.E.P)), 0, dmg);
if (share <= 0) continue;
g.TakeDamage(share);
Fx.Popup(g.Position, share + " 호위", SkillFx.GuardColor);
dmg -= share;
break;
}
d.TakeDamage(dmg);
return dmg;
}
/// <summary>스킬 시스템이 없어도 쓰는 피해 적용.</summary>
public static int Deal(Unit d, int dmg)
{
if (Current != null) return Current.Hit(d, dmg);
if (d == null) return 0;
d.TakeDamage(dmg);
return dmg;
}
// ---------- 즉시형 액티브 ----------
/// <summary>즉시 발동(행동 소모 없음): 효과마다 처리 → 쿨.</summary>
public IEnumerator Instant(UnitSkills s)
{
var u = s.Unit;
Announce(s, s.Active);
s.ReadyTurn = battle.TurnNumber + s.Active.Cooldown;
foreach (var e in s.ActiveEffects)
{
switch (e.Key)
{
case SkillKeys.AoePushFear:
yield return PushFear(s, e);
break;
case SkillKeys.HpCostAtk:
PayHp(u, e.HasA2 ? e.A2 : 0f);
AddTimed(s, e, false, null);
break;
case SkillKeys.GuardShare:
case SkillKeys.TeamDmgTakenPctTurn:
case SkillKeys.DmgTakenAura:
case SkillKeys.EnemyAggroOffTurn:
AddTimed(s, e, true, null);
break;
case SkillKeys.Teleport:
break; // 목표 지점 탭(TryTeleport)
default:
AddTimed(s, e, false, null); // 동남풍(sling_dist_pct) 등 = 이번 아군 턴
break;
}
}
yield return Fx.Wait(0.25f);
battle.NotifyChanged();
}
void AddTimed(UnitSkills s, SkillEffect e, bool enemyWindow, HashSet<Unit> targets)
{
TimedEffects.Add(new Timed { Owner = s, Faction = s.Unit.Faction, Def = s.Active, E = e, EnemyWindow = enemyWindow, Targets = targets });
}
static void PayHp(Unit u, float ratio)
{
int cost = Mathf.RoundToInt(u.MaxHp * Mathf.Max(0f, ratio));
if (cost <= 0) return;
u.Hp = Mathf.Max(1, u.Hp - cost); // 스스로 쓰러지지는 않는다
if (u.View != null)
{
u.View.OnHit();
u.View.Refresh();
}
Fx.Popup(u.Position, "HP −" + cost, SkillFx.CostColor);
}
/// <summary>장판 일갈: 반경(p2) 안 상대 전원을 바깥으로 p1 밀고 이번 적 턴 교전 반응 없음.</summary>
IEnumerator PushFear(UnitSkills s, SkillEffect e)
{
var u = s.Unit;
float radius = e.HasA2 ? e.A2 : 0f;
var targets = new HashSet<Unit>();
int running = 0;
Faction other = u.Faction == Faction.Player ? Faction.Enemy : Faction.Player;
foreach (var t in battle.AliveUnits(other))
{
if (Vector2.Distance(u.Position, t.Position) > radius + t.Radius) continue;
targets.Add(t);
Vector2 dir = t.Position - u.Position;
dir = dir.sqrMagnitude > 1e-6f ? dir.normalized : Vector2.up;
float force = e.P * SkillMods.KnockbackTakenMult(t, true);
float slide = Combat.FreeTravel(t, t.Position, dir, force, true, true, u);
if (slide <= 0.01f) continue;
Vector2 end = battle.Board.ClampInside(t.Position + dir * slide, t.Radius);
running++;
var unit = t;
if (unit.View != null) unit.View.SetFacing(dir);
Fx.Run(Track(Fx.MoveAlong(unit.transform, unit.Position, end, battle.Config.moveSpeed, null), () =>
{
unit.Position = end;
running--;
}));
}
Fx.Burst(u.Position, radius, SkillFx.FearColor);
while (running > 0) yield return null;
var t2 = new Timed { Owner = s, Faction = u.Faction, Def = s.Active, E = e, EnemyWindow = true, Targets = targets };
TimedEffects.Add(t2);
battle.NotifyChanged();
}
static IEnumerator Track(IEnumerator inner, System.Action onDone)
{
yield return inner;
onDone();
}
// ---------- 변진(장합): 목표 지점 탭 ----------
public static float TeleportRadius(UnitSkills s)
{
if (s == null || s.ActiveEffects == null) return 0f;
foreach (var e in s.ActiveEffects)
if (e.Key == SkillKeys.Teleport) return e.P;
return 0f;
}
/// <summary>목표 지점이 반경 안 빈 곳인가(벽·유닛과 겹치지 않음 · 전장 안).</summary>
public bool TeleportValid(Unit u, Vector2 w)
{
var s = Of(u);
if (s == null) return false;
if (Vector2.Distance(u.Position, w) > TeleportRadius(s) + 1e-3f) return false;
if ((battle.Board.ClampInside(w, u.Radius) - w).sqrMagnitude > 1e-4f) return false;
Physics2D.SyncTransforms();
foreach (var c in Physics2D.OverlapCircleAll(w, u.Radius))
{
if (c.GetComponent<BoardWall>() != null) return false;
var o = c.GetComponent<Unit>();
if (o != null && o != u && o.IsAlive) return false;
}
return true;
}
/// <summary>변진 대기 중 화면 탭 → 처리했으면 true(유효 = 이동 · 아니면 취소).</summary>
public bool TryTeleportTap(Vector2 w, out string message)
{
message = null;
var u = PendingTeleport;
if (u == null) return false;
PendingTeleport = null;
var s = Of(u);
if (s == null || !u.IsAlive || battle.Phase != BattlePhase.PlayerTurn) return true;
if (!TeleportValid(u, w))
{
message = s.Active.NameKo + " 취소 — 반경 안 빈 곳이 아님";
return true;
}
battle.RunSkillRoutine(TeleportRoutine(s, w));
message = s.Active.NameKo + " 발동!";
return true;
}
public IEnumerator TeleportRoutine(UnitSkills s, Vector2 w)
{
var u = s.Unit;
Announce(s, s.Active);
s.ReadyTurn = battle.TurnNumber + s.Active.Cooldown;
if (u.View != null) u.View.SetFacing(w - u.Position);
yield return Fx.MoveAlong(u.transform, u.Position, w, battle.Config.moveSpeed * 2f, null);
u.Position = w;
battle.NotifyChanged();
}
// ---------- 연출 · HUD 기록 ----------
/// <summary>액티브 발동 = 유닛 위 스킬 이름 + 플래시 + HUD 한 줄.</summary>
public void Announce(UnitSkills s, SkillDef def)
{
if (s == null || def == null) return;
if (s.Unit != null) SkillFx.Announce(s.Unit, def.NameKo);
AddLog("액티브 · " + (s.Unit != null ? s.Unit.DisplayName : "") + " 「" + def.NameKo + "」");
}
/// <summary>패시브 발동 HUD 한 줄(같은 행동 안에서는 스킬마다 1번).</summary>
public void NotePassive(UnitSkills s, SkillDef def)
{
if (s == null || def == null || s.Unit == null) return;
string key = s.Unit.GetInstanceID() + "/" + def.Id;
if (!noted.Add(key)) return;
AddLog("패시브 · " + s.Unit.DisplayName + " 「" + def.NameKo + "」");
}
public void AddLog(string text)
{
Log.Add(new LogLine { Text = text, Time = Time.unscaledTime });
if (Log.Count > 8) Log.RemoveAt(0);
}
/// <summary>최근 seconds 초 안의 기록(최신이 마지막).</summary>
public List<string> RecentLog(float seconds, int max)
{
var res = new List<string>();
float now = Time.unscaledTime;
for (int i = Log.Count - 1; i >= 0 && res.Count < max; i--)
if (now - Log[i].Time <= seconds) res.Insert(0, Log[i].Text);
return res;
}
}
/// <summary>스킬 연출(이름 글자 · 플래시).</summary>
public static class SkillFx
{
public static readonly Color NameColor = new Color(1f, 0.85f, 0.3f);
public static readonly Color FlashColor = new Color(1f, 0.9f, 0.45f, 0.55f);
public static readonly Color GuardColor = new Color(0.55f, 0.85f, 1f);
public static readonly Color FearColor = new Color(1f, 0.4f, 0.25f, 0.4f);
public static readonly Color CostColor = new Color(1f, 0.55f, 0.35f);
public static readonly Color HealColor = new Color(0.45f, 1f, 0.5f);
public static void Announce(Unit u, string name)
{
Fx.Burst(u.Position, u.Radius * 2.2f, FlashColor);
Fx.Run(NameRoutine(u.Position + new Vector2(0f, u.ArtHeight > 0f ? u.ArtHeight * 0.6f : u.Radius + 0.6f), name));
}
static IEnumerator NameRoutine(Vector2 pos, string text)
{
var tm = TextUtil.CreateWorldText(null, text, 0.55f, NameColor, 21);
tm.gameObject.name = "SkillName";
var tr = tm.transform;
float t = 0f;
const float dur = 1.1f;
while (t < dur)
{
t += Time.deltaTime;
float k = Mathf.Clamp01(t / dur);
tr.position = new Vector3(pos.x, pos.y + 0.4f * k, 0f);
var c = NameColor;
c.a = k < 0.7f ? 1f : 1f - (k - 0.7f) / 0.3f;
tm.color = c;
yield return null;
}
Object.Destroy(tm.gameObject);
}
}
}