731 lines
28 KiB
C#
731 lines
28 KiB
C#
|
|
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);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|