using System.Collections; using System.Collections.Generic; using System.IO; using UnityEngine; namespace TKS { /// /// 자동 플레이 실측: 아군도 AI 로 조작해 몇 턴을 돌리며 스크린샷·로그를 남긴다. /// 에디터에서 ProjectSetup.AutoPlayCapture 가 SessionState 로 출력 폴더를 넘기면 생성된다. /// 비활성 에디터 창은 프레임이 느리므로 timeScale 을 올려 실시간 기준으로 빨리 끝낸다. /// public class AutoPlayDriver : MonoBehaviour { public string OutputDir; public int MaxTurns = 3; public int Width = 720; public int Height = 1280; readonly List log = new List(); int shotIndex = 1; #if UNITY_EDITOR [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] static void AutoSpawn() { string dir = UnityEditor.SessionState.GetString("TKS_AUTOPLAY_DIR", ""); if (string.IsNullOrEmpty(dir)) return; UnityEditor.SessionState.EraseString("TKS_AUTOPLAY_DIR"); var go = new GameObject("AutoPlayDriver"); var d = go.AddComponent(); d.OutputDir = dir; d.MaxTurns = UnityEditor.SessionState.GetInt("TKS_AUTOPLAY_TURNS", 3); } #endif IEnumerator Start() { Directory.CreateDirectory(OutputDir); Time.timeScale = 4f; Time.maximumDeltaTime = 1f; float t0 = Time.realtimeSinceStartup; // 시작 화면 = 본영 로비(2026-11-05 #1093) → 출정 = 챕터 월드맵 → 캡처 · 1-1 팝업 캡처 → 출전(PD 2026-11-05) yield return null; var og = TKS.OutGame.OutGameRoot.Instance; if (og != null && og.LobbyVisible) { og.OpenWorldMap(); yield return null; } var wm = WorldMapScreen.Instance; if (wm != null && wm.IsVisible && Camera.main != null) { yield return new WaitForSecondsRealtime(0.4f); yield return Shot(Camera.main, "worldmap"); wm.OpenPopup(0); yield return null; yield return Shot(Camera.main, "worldmap_popup"); wm.ConfirmPopup(); } while ((BattleManager.Instance == null || BattleManager.Instance.Phase != BattlePhase.PlayerTurn) && Time.realtimeSinceStartup - t0 < 20f) { yield return null; } var bm = BattleManager.Instance; var cam = Camera.main; if (bm == null || cam == null) { Log("FAIL: BattleManager 또는 카메라 없음"); Finish(1); yield break; } var camCtrl = cam.GetComponent(); Log("stage=" + bm.Stage.title + " players=" + bm.AliveCount(Faction.Player) + " enemies=" + bm.AliveCount(Faction.Enemy) + " board=" + bm.Config.boardSize); yield return Fx.Wait(0.6f); yield return Shot(cam, "start"); // 전장 전체 조감(최대 줌아웃) → 원래 줌 if (camCtrl != null) { float prev = camCtrl.ViewHeight; camCtrl.SetViewHeight(camCtrl.MaxViewHeight); camCtrl.SnapTo(Vector2.zero); yield return null; yield return Shot(cam, "overview_zoom_out"); camCtrl.SetViewHeight(prev); camCtrl.PanBy(new Vector2(0f, 6f)); // 스크롤 확인 yield return null; yield return Shot(cam, "scrolled_up"); camCtrl.FocusOn(bm.AliveUnits(Faction.Player)[0].Position + new Vector2(0f, 3f)); yield return Fx.Wait(0.5f); } var first = FirstActable(bm); if (first != null) { var tgt = EnemyAI.Decide(first, bm.AliveUnits(Faction.Enemy), 1f, float.PositiveInfinity, bm.Rng, bm.Board, bm.Config); if (tgt.HasValue) { Vector2 pointer = first.Position - (tgt.Value - first.Position) * 0.5f; bm.Sling.Select(first); // 선택 링 + 사거리 원(궁병이면 도넛) + HUD 정보 if (first.View != null) first.View.SetAiming(true); bm.Sling.ShowPreview(first, tgt.Value, pointer); yield return null; yield return Shot(cam, "aim_" + first.DisplayName); bm.Sling.HidePreview(); if (first.View != null) first.View.SetAiming(false); bm.Sling.Select(null); } } // 사거리 표시 캡처: 궁병 선택(얇은 원 + 공격 가능 도넛) · 적 선택(적의 사거리) foreach (var u in bm.AliveUnits(Faction.Player)) { if (!UnitTypeInfo.IsRanged(u.Type)) continue; if (camCtrl != null) camCtrl.FocusOn(u.Position + new Vector2(0f, 2f)); yield return Fx.Wait(0.5f); bm.Sling.Select(u); yield return null; yield return Shot(cam, "range_archer_" + u.DisplayName); bm.Sling.Select(null); break; } var enemies0 = bm.AliveUnits(Faction.Enemy); if (enemies0.Count > 0) { var e = enemies0[0]; if (camCtrl != null) camCtrl.FocusOn(e.Position); yield return Fx.Wait(0.5f); bm.Sling.Select(e); yield return null; yield return Shot(cam, "range_enemy_" + e.DisplayName); bm.Sling.Select(null); if (camCtrl != null) camCtrl.FocusOn(bm.AliveUnits(Faction.Player)[0].Position + new Vector2(0f, 3f)); yield return Fx.Wait(0.5f); } bool firstActionShot = false; for (int turn = 1; turn <= MaxTurns; turn++) { int guard = 0; while (bm.Phase == BattlePhase.PlayerTurn && guard++ < 24) { var u = FirstActable(bm); if (u == null) { bm.OnEmptyTap(); break; } var target = EnemyAI.Decide(u, bm.AliveUnits(Faction.Enemy), 1f, float.PositiveInfinity, bm.Rng, bm.Board, bm.Config); if (!target.HasValue) break; if (!bm.RequestPlayerAction(u, target.Value)) break; while (bm.Phase == BattlePhase.Resolving) yield return null; Log("turn " + turn + " | " + bm.LastMessage); if (!firstActionShot) { firstActionShot = true; yield return Shot(cam, "after_first_action"); } if (bm.Phase == BattlePhase.StageClear || bm.Phase == BattlePhase.Defeat) break; } while (bm.Phase == BattlePhase.EnemyTurn || bm.Phase == BattlePhase.Resolving) yield return null; Log("turn " + turn + " end | phase=" + bm.Phase + " players=" + bm.AliveCount(Faction.Player) + " enemies=" + bm.AliveCount(Faction.Enemy) + " | " + bm.LastMessage); yield return Fx.Wait(0.4f); yield return Shot(cam, "turn" + turn + "_end"); if (bm.Phase == BattlePhase.StageClear || bm.Phase == BattlePhase.Defeat) break; } Finish(0); } static Unit FirstActable(BattleManager bm) { foreach (var u in bm.AliveUnits(Faction.Player)) { if (!u.ActedThisTurn) return u; } return null; } IEnumerator Shot(Camera cam, string name) { string file = Path.Combine(OutputDir, shotIndex.ToString("00") + "_" + name + ".png"); shotIndex++; yield return CaptureUtil.Capture(cam, Width, Height, file); Log("shot " + file); } void Log(string s) { log.Add(s); Debug.Log("[AutoPlay] " + s); try { File.WriteAllLines(Path.Combine(OutputDir, "autoplay_log.txt"), log.ToArray()); } catch { } } void Finish(int code) { Log("finish code=" + code); Time.timeScale = 1f; #if UNITY_EDITOR UnityEditor.EditorApplication.Exit(code); #else Application.Quit(code); #endif } } }