2026-11-05 16:20:07 +00:00
|
|
|
using System.Collections;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.IO;
|
|
|
|
|
using TKS.OutGame;
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
|
|
|
|
namespace TKS
|
|
|
|
|
{
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 지형 기믹·적 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장. 카메라는 기믹 쪽으로 · 끝나면 스스로 종료.
|
2026-11-05 17:05:30 +00:00
|
|
|
/// 스테이지 목록(StagesKey · -gimmickStages 1-10,2-10,…)이 오면 ①~③ 대신 그 스테이지들의 시작 화면만 전장 전체로 찍는다(145 배치 확인).
|
2026-11-05 16:20:07 +00:00
|
|
|
/// </summary>
|
|
|
|
|
public class GimmickCaptureDriver : MonoBehaviour
|
|
|
|
|
{
|
|
|
|
|
public const string DirKey = "TKS_GIMMICK_CAPTURE_DIR";
|
2026-11-05 17:05:30 +00:00
|
|
|
public const string StagesKey = "TKS_GIMMICK_CAPTURE_STAGES";
|
2026-11-05 16:20:07 +00:00
|
|
|
public string OutputDir;
|
2026-11-05 17:05:30 +00:00
|
|
|
public string[] StageList;
|
2026-11-05 16:20:07 +00:00
|
|
|
public int Width = 720;
|
|
|
|
|
public int Height = 1280;
|
|
|
|
|
|
|
|
|
|
readonly List<string> log = new List<string>();
|
|
|
|
|
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<GimmickCaptureDriver>();
|
|
|
|
|
d.OutputDir = dir;
|
2026-11-05 17:05:30 +00:00
|
|
|
string stages = UnityEditor.SessionState.GetString(StagesKey, "");
|
|
|
|
|
UnityEditor.SessionState.EraseString(StagesKey);
|
|
|
|
|
if (stages.Length > 0) d.StageList = stages.Split(',');
|
2026-11-05 16:20:07 +00:00
|
|
|
}
|
|
|
|
|
#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<CameraController>();
|
|
|
|
|
Log("gimmickDb=" + (GimmickTables.Db != null ? GimmickTables.Db.defs.Count : -1) + " table=" + (GimmickTables.Table != null ? GimmickTables.Table.rows.Count : -1) + " aiWeights=" + (AiWeights.Load() != null));
|
|
|
|
|
|
2026-11-05 17:05:30 +00:00
|
|
|
if (StageList != null && StageList.Length > 0)
|
|
|
|
|
{
|
|
|
|
|
yield return LayoutShots(root, bm, cam, camCtrl);
|
|
|
|
|
Finish(0);
|
|
|
|
|
yield break;
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-05 16:20:07 +00:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-05 17:05:30 +00:00
|
|
|
/// <summary>StageList 스테이지마다 시작 화면 1장(카메라 = 전장 가운데 · 최대 높이).</summary>
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-05 16:20:07 +00:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>아군 전원이 가까운 적 쪽으로 슬링(최대 거리) → 적 턴(예측 AI)까지 끝나기를 기다린다.</summary>
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>조준 장면용 전장: 유닛을 비우고 그 스테이지 기믹만 다시 깐다.</summary>
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>1-3: 보병이 가시덤불 쪽으로 당긴 조준(덤불 지점 붉게 끊김 · 튕김 · 자기 피해 글자 · 튕겨날 궤적).</summary>
|
|
|
|
|
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");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>9-11: 다리 위 적을 아군 보병이 비스듬히 밀어 강에 빠뜨리는 조준(밀림 궤적 · 강에 빠짐 글자).</summary>
|
|
|
|
|
IEnumerator BridgePushShot(BattleManager bm, Camera cam, CameraController cc)
|
|
|
|
|
{
|
|
|
|
|
yield return Stage(bm);
|
2026-11-05 16:42:15 +00:00
|
|
|
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");
|
2026-11-05 16:20:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>AI 전술: 적 보병이 아군을 함정 쪽으로 튕겨낸다(함정 발동 순간 캡처).</summary>
|
|
|
|
|
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<AiPlan> 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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|