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
}
}
}