using System.Collections; using System.Collections.Generic; using System.IO; using TKS.OutGame; using UnityEngine; namespace TKS { /// /// 지형 기믹·적 AI 검증 캡처(에디터 GimmickImporter.Capture 가 SessionState 로 출력 폴더를 넘기면 생성 · 세이브 = 메모리 전용 · PD #1097 · #1098). /// ① 샘플 5 스테이지(1-3 · 2-5 · 4-13 · 8-15 · 9-11) 시작 + 1턴(아군 = 가까운 적 쪽 슬링 · 적 = 예측 AI) 뒤 ② 조준선 반응 2장(1-3 바위·덤불 · 9-11 다리 밀기) /// ③ AI 가 아군을 함정 쪽으로 튕겨내는 장면 1장. 카메라는 기믹 쪽으로 · 끝나면 스스로 종료. /// 스테이지 목록(StagesKey · -gimmickStages 1-10,2-10,…)이 오면 ①~③ 대신 그 스테이지들의 시작 화면만 전장 전체로 찍는다(145 배치 확인). /// public class GimmickCaptureDriver : MonoBehaviour { public const string DirKey = "TKS_GIMMICK_CAPTURE_DIR"; public const string StagesKey = "TKS_GIMMICK_CAPTURE_STAGES"; public string OutputDir; public string[] StageList; public int Width = 720; public int Height = 1280; readonly List log = new List(); int shot = 1; static readonly string[] Samples = { "1-3", "2-5", "4-13", "8-15", "9-11" }; #if UNITY_EDITOR [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] static void AutoSpawn() { string dir = UnityEditor.SessionState.GetString(DirKey, ""); if (string.IsNullOrEmpty(dir)) return; UnityEditor.SessionState.EraseString(DirKey); var d = new GameObject("GimmickCaptureDriver").AddComponent(); d.OutputDir = dir; string stages = UnityEditor.SessionState.GetString(StagesKey, ""); UnityEditor.SessionState.EraseString(StagesKey); if (stages.Length > 0) d.StageList = stages.Split(','); } #endif IEnumerator Start() { Directory.CreateDirectory(OutputDir); yield return null; yield return null; var root = OutGameRoot.Instance; var cam = Camera.main; if (root == null || cam == null) { Log("FAIL: OutGameRoot 또는 카메라 없음"); Finish(1); yield break; } var bm = root.Battle; var camCtrl = cam.GetComponent(); Log("gimmickDb=" + (GimmickTables.Db != null ? GimmickTables.Db.defs.Count : -1) + " table=" + (GimmickTables.Table != null ? GimmickTables.Table.rows.Count : -1) + " aiWeights=" + (AiWeights.Load() != null)); if (StageList != null && StageList.Length > 0) { yield return LayoutShots(root, bm, cam, camCtrl); Finish(0); yield break; } foreach (var st in Samples) { var parts = st.Split('-'); int ch = int.Parse(parts[0]), no = int.Parse(parts[1]); root.OpenWorldMap(); yield return null; root.Map.EnterStage(ch - 1, no - 1); float t0 = Time.realtimeSinceStartup; while (bm.Phase != BattlePhase.PlayerTurn && Time.realtimeSinceStartup - t0 < 10f) yield return null; var layer = bm.Gimmicks; Log(st + " 「" + bm.Stage.title + "」 기믹 " + (layer != null ? layer.Items.Count : 0) + " · 아군 " + bm.AliveCount(Faction.Player) + " · 적 " + bm.AliveCount(Faction.Enemy) + " · msg=" + bm.LastMessage); if (layer != null) foreach (var g in layer.Items) Log(" " + g.Id + " " + g.Shape.Type + " @" + g.Shape.Center + (g.Template ? " (생성 규칙)" : "") + (g.MaxHp > 0 ? " hp" + g.Hp : "")); Vector2 focus = Focus(bm, layer); if (camCtrl != null) { camCtrl.SetViewHeight(26f); // 기믹 전체가 보이게 camCtrl.SnapTo(focus); } yield return Wait(); yield return Shot(cam, "stage_" + st + "_start"); if (st == "1-3" || st == "9-11") { if (st == "1-3") yield return ThornAimShot(bm, cam, camCtrl); else yield return BridgePushShot(bm, cam, camCtrl); root.OpenWorldMap(); // 조준 장면용으로 비운 전장을 다시 그 스테이지로 yield return null; root.Map.EnterStage(ch - 1, no - 1); t0 = Time.realtimeSinceStartup; while (bm.Phase != BattlePhase.PlayerTurn && Time.realtimeSinceStartup - t0 < 10f) yield return null; layer = bm.Gimmicks; } if (camCtrl != null) camCtrl.SetViewHeight(bm.Config.cameraViewHeight); yield return PlayOneTurn(bm); if (camCtrl != null) { camCtrl.SetViewHeight(26f); camCtrl.SnapTo(Focus(bm, layer)); } yield return Wait(); Log(st + " 1턴 뒤 · 턴 " + bm.TurnNumber + " · 아군 " + bm.AliveCount(Faction.Player) + " · 적 " + bm.AliveCount(Faction.Enemy) + " · AI=" + (bm.LastAiPlan != null ? bm.LastAiPlan.Unit.DisplayName + " 「" + bm.LastAiPlan.Why + "」 " + bm.LastAiPlan.Score.ToString("0.#") : "-") + " · msg=" + bm.LastMessage); yield return Shot(cam, "stage_" + st + "_turn1"); } yield return TrapShot(bm, cam, camCtrl); Finish(0); } /// StageList 스테이지마다 시작 화면 1장(카메라 = 전장 가운데 · 최대 높이). IEnumerator LayoutShots(OutGameRoot root, BattleManager bm, Camera cam, CameraController camCtrl) { foreach (var raw in StageList) { string st = raw.Trim(); var parts = st.Split('-'); int ch, no; if (parts.Length != 2 || !int.TryParse(parts[0], out ch) || !int.TryParse(parts[1], out no)) { Log("skip 「" + st + "」(장-번호 아님)"); continue; } root.OpenWorldMap(); yield return null; root.Map.EnterStage(ch - 1, no - 1); float t0 = Time.realtimeSinceStartup; while (bm.Phase != BattlePhase.PlayerTurn && Time.realtimeSinceStartup - t0 < 10f) yield return null; var layer = bm.Gimmicks; Log(st + " 「" + bm.Stage.title + "」 기믹 " + (layer != null ? layer.Items.Count : 0) + " · 아군 " + bm.AliveCount(Faction.Player) + " · 적 " + bm.AliveCount(Faction.Enemy)); if (layer != null) foreach (var g in layer.Items) Log(" " + g.Id + " " + g.Shape.Type + " @" + g.Shape.Center + (g.Template ? " (생성 규칙)" : "") + (g.MaxHp > 0 ? " hp" + g.Hp : "")); if (camCtrl != null) { camCtrl.SetViewHeight(camCtrl.MaxViewHeight); camCtrl.SnapTo(Vector2.zero); } yield return Wait(); yield return Shot(cam, "layout_" + st); } } static Vector2 Focus(BattleManager bm, GimmickLayer layer) { Vector2 sum = Vector2.zero; int n = 0; if (layer != null) { foreach (var g in layer.Items) { if (g.Template || g.Shape.BoundingRadius > 12f) continue; sum += g.Shape.Center; n++; } } if (n == 0) return new Vector2(0f, -4f); Vector2 c = sum / n; return new Vector2(c.x, Mathf.Min(c.y, 2f) - 3f); } /// 아군 전원이 가까운 적 쪽으로 슬링(최대 거리) → 적 턴(예측 AI)까지 끝나기를 기다린다. IEnumerator PlayOneTurn(BattleManager bm) { Time.timeScale = 3f; int turn = bm.TurnNumber; float t0 = Time.realtimeSinceStartup; while (bm.TurnNumber == turn && Time.realtimeSinceStartup - t0 < 90f) { if (bm.Phase == BattlePhase.StageClear || bm.Phase == BattlePhase.Defeat) break; if (bm.Phase == BattlePhase.PlayerTurn) { Unit next = null; foreach (var u in bm.AliveUnits(Faction.Player)) { if (bm.CanAct(u)) { next = u; break; } } if (next == null) bm.OnEmptyTap(); else { var foe = Nearest(bm, next); Vector2 d = foe != null ? foe.Position - next.Position : Vector2.up; float len = Mathf.Min(d.magnitude, SkillMods.SlingMax(next)); var target = bm.Board.ClampInside(next.Position + d.normalized * len, next.Radius); bm.RequestPlayerAction(next, target); } } yield return null; } Time.timeScale = 1f; } /// 조준 장면용 전장: 유닛을 비우고 그 스테이지 기믹만 다시 깐다. static IEnumerator Stage(BattleManager bm) { bm.StopAllCoroutines(); var stage = bm.Stage; bm.ClearUnits(); bm.ForcePhase(BattlePhase.PlayerTurn); yield return null; GimmickLayer.Ensure(bm).Build(stage); } IEnumerator AimCapture(BattleManager bm, Camera cam, CameraController cc, Unit u, Vector2 target, Vector2 focus, string name) { bm.Sling.Select(u); bm.Sling.ShowPreview(u, target, u.Position - (target - u.Position).normalized * 1.2f); if (cc != null) { cc.SetViewHeight(13f); cc.SnapTo(focus); } yield return Wait(); var pv = bm.Gimmicks.Preview; Log(name + " · 글자=" + string.Join(" / ", pv.LastIcons.ToArray()) + " · 끊김=" + pv.LastCut + " · 궤적=" + pv.LastTracks + " · 띠=" + pv.LastBandColors.Count); yield return Shot(cam, name); bm.Sling.HidePreview(); bm.Sling.Select(null); } /// 1-3: 보병이 가시덤불 쪽으로 당긴 조준(덤불 지점 붉게 끊김 · 튕김 · 자기 피해 글자 · 튕겨날 궤적). IEnumerator ThornAimShot(BattleManager bm, Camera cam, CameraController cc) { yield return Stage(bm); var u = bm.SpawnUnit(UnitType.Infantry, Faction.Player, new Vector2(6f, -3.2f), "보병"); bm.SpawnUnit(UnitType.Infantry, Faction.Enemy, new Vector2(6f, 2.4f), "덤불 뒤 적"); yield return AimCapture(bm, cam, cc, u, new Vector2(6f, 1.3f), new Vector2(6f, -0.8f), "aim_1-3_thorn"); } /// 9-11: 다리 위 적을 아군 보병이 비스듬히 밀어 강에 빠뜨리는 조준(밀림 궤적 · 강에 빠짐 글자). IEnumerator BridgePushShot(BattleManager bm, Camera cam, CameraController cc) { yield return Stage(bm); var u = bm.SpawnUnit(UnitType.Infantry, Faction.Player, new Vector2(-0.6f, 0.8f), "보병"); var foe = bm.SpawnUnit(UnitType.Infantry, Faction.Enemy, new Vector2(0.35f, 3f), "다리 위 적"); Vector2 n = (foe.Position - u.Position).normalized; yield return AimCapture(bm, cam, cc, u, u.Position + n * 4f, new Vector2(0f, 2.2f), "aim_9-11_bridge_push"); } /// AI 전술: 적 보병이 아군을 함정 쪽으로 튕겨낸다(함정 발동 순간 캡처). IEnumerator TrapShot(BattleManager bm, Camera cam, CameraController cc) { bm.StopAllCoroutines(); bm.ClearUnits(); bm.ForcePhase(BattlePhase.PlayerTurn); yield return null; var layer = GimmickLayer.Ensure(bm); var trap = layer.Spawn("pit_trap", GimmickShape.Circle(new Vector2(0f, 3.4f), 0.6f)); var enemy = bm.SpawnUnit(UnitType.Infantry, Faction.Enemy, new Vector2(0f, -1.5f), "적보병"); var victim = bm.SpawnUnit(UnitType.Spear, Faction.Player, new Vector2(0f, 1.2f), "아군창병"); var other = bm.SpawnUnit(UnitType.Infantry, Faction.Player, new Vector2(-4f, -1.5f), "아군보병"); bm.LastMessage = "AI 전술 — 적 보병이 아군을 함정 쪽으로 튕겨낸다"; if (cc != null) { cc.SetViewHeight(13f); cc.SnapTo(new Vector2(0f, 1f)); } yield return Wait(); List list; var best = TacticalAI.Evaluate(bm, enemy, 1, out list); Log("trap AI: " + (best != null ? best.Why + " · 점수 " + best.Score.ToString("0.#") + " · 후보 " + best.Candidates + " · " + best.Ms.ToString("0.0") + "ms" : "없음")); Time.timeScale = 1f; var o = new ActionOutcome(); Fx.Run(bm.ResolveForTest(enemy, best != null ? best.Target : victim.Position, false, o)); float t0 = Time.realtimeSinceStartup; while (!trap.Revealed && Time.realtimeSinceStartup - t0 < 6f) yield return null; yield return new WaitForSecondsRealtime(0.12f); Log("trap revealed=" + trap.Revealed + " victim=" + victim.Position + " stun=" + (layer.StateOf(victim) != null ? layer.StateOf(victim).Stun : 0) + " other=" + other.IsAlive); yield return Shot(cam, "ai_trap_bounce"); yield return new WaitForSecondsRealtime(1f); } static Unit Nearest(BattleManager bm, Unit from) { Unit best = null; float bd = float.MaxValue; foreach (var e in bm.AliveUnits(from.Faction == Faction.Player ? Faction.Enemy : Faction.Player)) { float d = Vector2.Distance(e.Position, from.Position); if (d < bd) { bd = d; best = e; } } return best; } static IEnumerator Wait() { yield return null; yield return new WaitForSecondsRealtime(0.35f); } IEnumerator Shot(Camera cam, string name) { string file = Path.Combine(OutputDir, shot.ToString("00") + "_" + name + ".png"); shot++; yield return CaptureUtil.Capture(cam, Width, Height, file); Log("shot " + file); } void Log(string s) { log.Add(s); Debug.Log("[GimmickCapture] " + s); try { File.WriteAllLines(Path.Combine(OutputDir, "capture_log.txt"), log.ToArray()); } catch { } } void Finish(int code) { Log("finish code=" + code); #if UNITY_EDITOR UnityEditor.EditorApplication.Exit(code); #else Application.Quit(code); #endif } } }