2026-11-03 19:10:10 +00:00
|
|
|
using System.Collections;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.IO;
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
|
|
|
|
namespace TKS
|
|
|
|
|
{
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 자동 플레이 실측: 아군도 AI 로 조작해 몇 턴을 돌리며 스크린샷·로그를 남긴다.
|
|
|
|
|
/// 에디터에서 ProjectSetup.AutoPlayCapture 가 SessionState 로 출력 폴더를 넘기면 생성된다.
|
2026-11-03 19:29:16 +00:00
|
|
|
/// 비활성 에디터 창은 프레임이 느리므로 timeScale 을 올려 실시간 기준으로 빨리 끝낸다.
|
2026-11-03 19:10:10 +00:00
|
|
|
/// </summary>
|
|
|
|
|
public class AutoPlayDriver : MonoBehaviour
|
|
|
|
|
{
|
|
|
|
|
public string OutputDir;
|
|
|
|
|
public int MaxTurns = 3;
|
|
|
|
|
public int Width = 720;
|
|
|
|
|
public int Height = 1280;
|
|
|
|
|
|
|
|
|
|
readonly List<string> log = new List<string>();
|
|
|
|
|
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<AutoPlayDriver>();
|
|
|
|
|
d.OutputDir = dir;
|
|
|
|
|
d.MaxTurns = UnityEditor.SessionState.GetInt("TKS_AUTOPLAY_TURNS", 3);
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
IEnumerator Start()
|
|
|
|
|
{
|
|
|
|
|
Directory.CreateDirectory(OutputDir);
|
2026-11-03 19:29:16 +00:00
|
|
|
Time.timeScale = 4f;
|
|
|
|
|
Time.maximumDeltaTime = 1f;
|
2026-11-03 19:10:10 +00:00
|
|
|
float t0 = Time.realtimeSinceStartup;
|
2026-11-05 12:11:10 +00:00
|
|
|
// 시작 화면 = 본영 로비(2026-11-05 #1093) → 출정 = 챕터 월드맵 → 캡처 · 1-1 팝업 캡처 → 출전(PD 2026-11-05)
|
2026-11-04 18:38:47 +00:00
|
|
|
yield return null;
|
2026-11-05 12:11:10 +00:00
|
|
|
var og = TKS.OutGame.OutGameRoot.Instance;
|
|
|
|
|
if (og != null && og.LobbyVisible)
|
|
|
|
|
{
|
|
|
|
|
og.OpenWorldMap();
|
|
|
|
|
yield return null;
|
|
|
|
|
}
|
2026-11-04 18:38:47 +00:00
|
|
|
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();
|
|
|
|
|
}
|
2026-11-03 19:10:10 +00:00
|
|
|
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;
|
|
|
|
|
}
|
2026-11-03 19:29:16 +00:00
|
|
|
var camCtrl = cam.GetComponent<CameraController>();
|
2026-11-03 19:10:10 +00:00
|
|
|
|
2026-11-03 19:29:16 +00:00
|
|
|
Log("stage=" + bm.Stage.title + " players=" + bm.AliveCount(Faction.Player) + " enemies=" + bm.AliveCount(Faction.Enemy) + " board=" + bm.Config.boardSize);
|
2026-11-03 19:10:10 +00:00
|
|
|
yield return Fx.Wait(0.6f);
|
|
|
|
|
yield return Shot(cam, "start");
|
|
|
|
|
|
2026-11-03 19:29:16 +00:00
|
|
|
// 전장 전체 조감(최대 줌아웃) → 원래 줌
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-03 19:10:10 +00:00
|
|
|
var first = FirstActable(bm);
|
|
|
|
|
if (first != null)
|
|
|
|
|
{
|
2026-11-05 04:06:32 +00:00
|
|
|
var tgt = EnemyAI.Decide(first, bm.AliveUnits(Faction.Enemy), 1f, float.PositiveInfinity, bm.Rng, bm.Board, bm.Config);
|
2026-11-03 19:29:16 +00:00
|
|
|
if (tgt.HasValue)
|
|
|
|
|
{
|
|
|
|
|
Vector2 pointer = first.Position - (tgt.Value - first.Position) * 0.5f;
|
2026-11-04 04:05:46 +00:00
|
|
|
bm.Sling.Select(first); // 선택 링 + 사거리 원(궁병이면 도넛) + HUD 정보
|
|
|
|
|
if (first.View != null) first.View.SetAiming(true);
|
2026-11-03 19:29:16 +00:00
|
|
|
bm.Sling.ShowPreview(first, tgt.Value, pointer);
|
|
|
|
|
yield return null;
|
|
|
|
|
yield return Shot(cam, "aim_" + first.DisplayName);
|
|
|
|
|
bm.Sling.HidePreview();
|
2026-11-04 04:05:46 +00:00
|
|
|
if (first.View != null) first.View.SetAiming(false);
|
|
|
|
|
bm.Sling.Select(null);
|
2026-11-03 19:29:16 +00:00
|
|
|
}
|
2026-11-03 19:10:10 +00:00
|
|
|
}
|
|
|
|
|
|
2026-11-04 04:05:46 +00:00
|
|
|
// 사거리 표시 캡처: 궁병 선택(얇은 원 + 공격 가능 도넛) · 적 선택(적의 사거리)
|
|
|
|
|
foreach (var u in bm.AliveUnits(Faction.Player))
|
|
|
|
|
{
|
2026-11-05 04:06:32 +00:00
|
|
|
if (!UnitTypeInfo.IsRanged(u.Type)) continue;
|
2026-11-04 04:05:46 +00:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-03 19:10:10 +00:00
|
|
|
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;
|
|
|
|
|
}
|
2026-11-05 04:06:32 +00:00
|
|
|
var target = EnemyAI.Decide(u, bm.AliveUnits(Faction.Enemy), 1f, float.PositiveInfinity, bm.Rng, bm.Board, bm.Config);
|
2026-11-03 19:29:16 +00:00
|
|
|
if (!target.HasValue) break;
|
|
|
|
|
if (!bm.RequestPlayerAction(u, target.Value)) break;
|
2026-11-03 19:10:10 +00:00
|
|
|
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);
|
2026-11-03 19:29:16 +00:00
|
|
|
yield return Fx.Wait(0.4f);
|
2026-11-03 19:10:10 +00:00
|
|
|
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
|
|
|
|
|
{
|
|
|
|
|
}
|
2026-11-03 19:29:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Finish(int code)
|
|
|
|
|
{
|
|
|
|
|
Log("finish code=" + code);
|
|
|
|
|
Time.timeScale = 1f;
|
2026-11-03 19:10:10 +00:00
|
|
|
#if UNITY_EDITOR
|
|
|
|
|
UnityEditor.EditorApplication.Exit(code);
|
|
|
|
|
#else
|
|
|
|
|
Application.Quit(code);
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|