using System;
using System.Collections.Generic;
using System.Globalization;
using TKS.OutGame;
using UnityEngine;
using UnityEngine.UI;
namespace TKS
{
///
/// 스킬 테스트 씬(Assets/Scenes/SkillTest.unity · 메뉴 TKS/Skill Test/Build Scene · 2026-11-09 PD
/// 「유닛마다 액티브 스킬을 구현하기 쉽도록 목록에서 유닛을 선택 · 선택된 유닛으로 기본 스킬 사용 방식 및 슬링샷 조작이 가능한 공간」).
/// WL #947 스킬 테스트 씬과 같은 역할 — 전투 코드(BattleManager · SlingshotInput · ActionResolver · SkillSystem · BattleHUD)는 그대로 쓰고
/// 월드맵·아웃게임·세이브 없이 「고른 영웅 1 + 더미」 만 세운다.
/// 더미 = 죽지 않음 · 기본 행동 안 함(토글) · 배치 = 표 Resources/Skills/skill_test.csv(영웅 출발 자리 기준 상대 좌표).
/// 왼쪽 위 「유닛」 = 영웅 목록(heroes.csv · 병사 제외) · 「이펙트」 = 팩 이펙트 미리보기(에디터 전용 · Assets/VFX 프리팹 검색 → 고른 영웅에 띄움 · 경로 복사).
/// 스킬 이펙트 값 = Resources/Skills/skill_vfx.csv(SkillVfx) · 「다시 배치」 = 표 다시 읽기 + 처음 자리.
///
public class SkillTestBootstrap : MonoBehaviour
{
public const string DummyTablePath = "Skills/skill_test";
const string PrefKey = "TKS_SKILLTEST_HERO";
public const string VfxRoot = "Assets/VFX";
/// 「기운 시험」 스위치가 켜는 이펙트 표 키(skill_vfx.csv test_aura 행).
public const string AuraTestKey = "test_aura";
public BalanceConfig Config;
public static SkillTestBootstrap Instance { get; private set; }
public BattleManager Battle { get; private set; }
public BattleHUD Hud { get; private set; }
public SlingshotInput Sling { get; private set; }
public CameraController CameraCtrl { get; private set; }
public HeroDb.Hero Current { get; private set; }
public IReadOnlyList Heroes
{
get { return heroes; }
}
public bool NoCooldown = true;
public bool EnemyActs;
public bool HeroInvincible = true;
readonly List heroes = new List();
readonly List blockers = new List();
Camera cam;
Text infoText;
Text cdLabel, enemyLabel, invLabel, auraLabel;
RectTransform heroPanel, fxPanel;
void Awake()
{
Instance = this;
if (Config == null) Config = Resources.Load("BalanceConfig");
if (Config == null)
{
Debug.LogError("[TKS] BalanceConfig 자산이 없습니다. 메뉴 TKS/Setup Project 를 실행하세요.");
return;
}
cam = Camera.main;
if (cam == null)
{
var cgo = new GameObject("Main Camera");
cgo.tag = "MainCamera";
cam = cgo.AddComponent();
}
cam.orthographic = true;
cam.clearFlags = CameraClearFlags.SolidColor;
cam.backgroundColor = new Color(0.07f, 0.09f, 0.11f);
cam.transform.position = new Vector3(0f, 0f, -10f);
cam.nearClipPlane = 0.1f;
cam.farClipPlane = 100f;
cam.transparencySortMode = TransparencySortMode.CustomAxis;
cam.transparencySortAxis = Vector3.up;
cam.depthTextureMode |= DepthTextureMode.Depth; // 이펙트 셰이더 소프트 파티클(GameBootstrap 과 같음)
var board = new GameObject("Board").AddComponent();
board.Build(Config.boardSize, Config.boardArt);
CameraCtrl = cam.GetComponent();
if (CameraCtrl == null) CameraCtrl = cam.gameObject.AddComponent();
CameraCtrl.Init(board, Config.cameraViewHeight, Config.cameraMinViewHeight, Config.cameraMaxViewHeight);
Battle = new GameObject("BattleManager").AddComponent();
Battle.Configure(Config, board);
Battle.LineupProvider = Lineup;
Battle.EnemyHold = !EnemyActs;
Hud = new GameObject("HUD").AddComponent();
Hud.Build(Battle, cam, CameraCtrl);
Battle.Hud = Hud;
Sling = new GameObject("SlingshotInput").AddComponent();
Sling.Init(Battle, cam, Hud, CameraCtrl);
Sling.BlockPress = OverPanel;
Battle.Sling = Sling;
var cc = CameraCtrl;
Battle.UnitActing += u => cc.Follow(u.transform);
Battle.PhaseChanged += OnPhase;
Battle.StageStarted += () => cc.SnapTo(Focus());
var db = OutGameData.Default;
if (db != null && db.Heroes != null)
foreach (var h in db.Heroes.heroes)
if (h != null && !h.isSoldier && !string.IsNullOrEmpty(h.id)) heroes.Add(h);
if (heroes.Count == 0)
{
Debug.LogError("[TKS] 스킬 테스트: 영웅 표가 비었습니다(메뉴 TKS/Import OutGame Data).");
return;
}
// 진영(촉 · 위 · 오 · 군) 순으로 묶되 진영 안은 표 순서 그대로(안정 정렬)
var order = new List();
foreach (var h in heroes)
if (!order.Contains(h.faction)) order.Add(h.faction);
var sorted = new List();
foreach (var f in order)
foreach (var h in heroes)
if (h.faction == f) sorted.Add(h);
heroes.Clear();
heroes.AddRange(sorted);
BuildUi();
string last = PlayerPrefs.GetString(PrefKey, "");
var first = heroes.Find(h => h.id == last) ?? heroes[0];
SelectHero(first);
}
void OnDestroy()
{
if (Instance == this) Instance = null;
SkillVfx.TestTravelKey = ""; // 시험 키가 다른 씬(실제 전투)으로 새지 않게
}
// ---------- 전투 구성 ----------
List Lineup()
{
var list = new List();
var h = Current;
if (h == null) return list;
list.Add(new LineupEntry
{
slot = heroSlot, id = h.id, name = h.nameKo, type = h.type, art = h.art,
hpMult = h.hpMult, atkMult = h.atkMult, defMult = h.defMult,
skillActive = h.skillActive ?? "", skillPassive = h.skillPassive ?? "", activeLv = 1, passiveLv = 1, passiveMult = 1f, general = true,
});
return list;
}
public void SelectHero(HeroDb.Hero h)
{
if (h == null) return;
Current = h;
PlayerPrefs.SetString(PrefKey, h.id);
if (heroPanel != null) heroPanel.gameObject.SetActive(false);
Restart();
}
/// 처음 자리로 다시 배치(표 skill_vfx.csv · skill_test.csv 다시 읽음).
public void Restart()
{
if (Battle == null || Current == null) return;
Battle.EnemyHold = !EnemyActs;
var stage = MakeStage(); // 영웅 자리(heroSlot)도 이 표에서 → Lineup 보다 먼저
var mode = new BattleMode { kind = "skilltest", stage = stage, maxTurns = 0, onExit = Restart };
Battle.StartMode(mode);
ApplyFlags();
RefreshInfo();
}
/// 영웅 출발 자리 = BalanceConfig.playerSpawnPoints[heroSlot](표 skill_test.csv 의 e_type = hero 행 n_slot).
int heroSlot;
Vector2 HeroSpawn
{
get
{
var sp = Config.playerSpawnPoints;
return sp != null && sp.Length > 0 ? sp[Mathf.Clamp(heroSlot, 0, sp.Length - 1)] : Vector2.zero;
}
}
BalanceConfig.StageDef MakeStage()
{
var st = new BalanceConfig.StageDef { title = "스킬 테스트", subtitle = "", aiAccuracy = 1f, aiAggroRange = 999f };
var ta = Resources.Load(DummyTablePath);
heroSlot = 0;
if (ta == null) return st;
var rows = new List();
string[] head = null;
foreach (var raw in ta.text.Replace("\r", "").Split('\n'))
{
var line = raw.TrimStart('');
if (line.Trim().Length == 0 || line.TrimStart().StartsWith("#")) continue;
var c = SkillVfx.SplitCsv(line);
if (head == null) head = c;
else rows.Add(c);
}
if (head == null) return st;
int iType = Array.IndexOf(head, "e_type"), iSlot = Array.IndexOf(head, "n_slot");
foreach (var c in rows) // 영웅 자리 먼저(더미 좌표 = 영웅 자리 기준)
{
if (iType < 0 || iType >= c.Length || c[iType].Trim().ToLowerInvariant() != "hero") continue;
if (iSlot >= 0 && iSlot < c.Length) int.TryParse(c[iSlot].Trim(), out heroSlot);
}
head = null;
foreach (var raw in ta.text.Replace("\r", "").Split('\n'))
{
var line = raw.TrimStart('');
if (line.Trim().Length == 0 || line.TrimStart().StartsWith("#")) continue;
var c = SkillVfx.SplitCsv(line);
if (head == null)
{
head = c;
continue;
}
UnitType type = UnitType.Infantry;
float dx = 0f, dy = 0f, hp = 1f;
string name = "";
bool skip = false;
for (int i = 0; i < head.Length && i < c.Length; i++)
{
string k = head[i].Trim(), v = c[i].Trim();
if (v.Length == 0) continue;
if (k == "e_type")
{
if (v.ToLowerInvariant() == "hero" || !Enum.TryParse(v, true, out type)) skip = true;
}
else if (k == "f_dx") dx = F(v);
else if (k == "f_dy") dy = F(v);
else if (k == "f_hp") hp = F(v);
else if (k == "s_name") name = v;
}
if (skip) continue;
st.enemies.Add(new BalanceConfig.EnemySpawn
{
name = name.Length > 0 ? name : "더미 " + UnitTypeInfo.Korean(type),
type = type,
position = HeroSpawn + new Vector2(dx, dy),
hpMult = hp > 0f ? hp : 1f,
art = DummyArt(type),
});
}
return st;
}
static float F(string v)
{
float f;
return float.TryParse(v, NumberStyles.Float, CultureInfo.InvariantCulture, out f) ? f : 0f;
}
/// 더미 그림 = 스테이지 표의 첫 번째 같은 병종 병사 그림(장수 제외 · 없으면 도형).
string DummyArt(UnitType t)
{
if (Config.stages == null) return "";
foreach (var s in Config.stages)
{
if (s == null || s.enemies == null) continue;
foreach (var e in s.enemies)
if (e != null && e.type == t && !e.isBoss && !e.isGeneral && !string.IsNullOrEmpty(e.art)) return e.art;
}
return "";
}
void ApplyFlags()
{
if (Battle == null) return;
foreach (var u in Battle.Units)
{
if (u == null) continue;
u.Invincible = u.Faction == Faction.Enemy || HeroInvincible;
}
}
void OnPhase(BattlePhase p)
{
if (p != BattlePhase.PlayerTurn) return;
ApplyFlags();
foreach (var u in Battle.Units)
{
if (u == null || !u.IsAlive) continue;
if (u.Faction == Faction.Enemy && u.Hp < u.MaxHp)
{
u.Hp = u.MaxHp; // 더미 체력 회복(다음 시험을 같은 조건으로)
if (u.View != null) u.View.Refresh();
}
if (NoCooldown && u.Faction == Faction.Player && Battle.Skills != null)
{
var s = Battle.Skills.Of(u);
if (s != null) s.ReadyTurn = Battle.TurnNumber;
if (u.View != null) u.View.Refresh();
}
}
if (CameraCtrl != null) CameraCtrl.FocusOn(Focus());
Battle.NotifyChanged();
}
Vector2 Focus()
{
var ps = Battle.AliveUnits(Faction.Player);
Vector2 p = ps.Count > 0 ? ps[0].Position : HeroSpawn;
return p + new Vector2(0f, Config.cameraViewHeight * 0.2f);
}
// ---------- UI ----------
static readonly Color PanelColor = new Color(0.08f, 0.09f, 0.12f, 0.92f);
static readonly Color BtnColor = new Color(0.22f, 0.26f, 0.34f, 0.95f);
static readonly Color BtnOnColor = new Color(0.25f, 0.5f, 0.32f, 0.95f);
static readonly Color TextColor = new Color(0.95f, 0.93f, 0.86f);
bool OverPanel(Vector2 screen)
{
foreach (var rt in blockers)
if (rt != null && rt.gameObject.activeInHierarchy && RectTransformUtility.RectangleContainsScreenPoint(rt, screen, null)) return true;
return false;
}
void BuildUi()
{
var cgo = new GameObject("SkillTestUI");
var canvas = cgo.AddComponent