ThreeKingdomsSlingshot/Assets/Scripts/OutGame/UICaptureDriver.cs

331 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
namespace TKS.OutGame
{
/// <summary>
/// UI 리뉴얼 검증 캡처(PD #1100 · 에디터 DataImporter.CaptureUI 가 SessionState 로 출력 폴더를 넘기면 생성 · 세이브 = 메모리 전용 신규 유저).
/// 1080×1920 = 로비 · 영웅 목록 · 영웅 상세 · 편성 · 거래 · 보물 · 내정 · 연무장 · 월드맵 팝업 · 전투 HUD · 전투 결과 · 팝업 + 1536×2048 = 로비 · 영웅 상세(겹침 확인) · 끝나면 스스로 종료.
/// </summary>
public class UICaptureDriver : MonoBehaviour
{
public const string DirKey = "TKS_UI_CAPTURE_DIR";
/// <summary>찍을 화면만(쉼표 · 정확한 이름 또는 「접두*」 · 비면 전부) — PD 2026-11-09 「수정된 화면만 체크」.</summary>
public const string OnlyKey = "TKS_UI_CAPTURE_ONLY";
public string OutputDir;
string[] only;
readonly HashSet<string> matched = new HashSet<string>();
const int W = 1080, H = 1920, W2 = 1536, H2 = 2048;
readonly List<string> log = new List<string>();
int shot = 1;
#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("UICaptureDriver").AddComponent<UICaptureDriver>();
d.OutputDir = dir;
string only = UnityEditor.SessionState.GetString(OnlyKey, "");
UnityEditor.SessionState.EraseString(OnlyKey);
if (!string.IsNullOrEmpty(only)) d.only = only.Split(new[] { ',' }, System.StringSplitOptions.RemoveEmptyEntries);
}
#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 s = root.Session;
var db = s.Db;
var d = s.Save;
Log("theme=" + (UITheme.Current != null) + " titleFont=" + (UISkin.TitleFont != null ? UISkin.TitleFont.name : "-") + " memoryOnly=" + SaveStore.MemoryOnly);
// 캡처용 진행(메모리 세이브만): 재화 · 장비 · 보물 · 상자 · 순찰
d.gold = 124500;
d.gem = 1280;
var id = db.Economy.StartHeroes.Length > 1 ? db.Economy.StartHeroes[1] : db.Economy.StartHeroes[0];
var h = s.Def(id);
var hs = s.Save.Hero(id);
for (int i = 0; i < 4; i++) d.inv.AddGear(HeroRules.RequiredGear(db, h, hs, i), 1);
for (int i = 0; i < 3; i++) s.Equip(id, i);
HeroRules.GrantExp(hs, db.Levels.Get(1).expToNext + 5);
var rng = new System.Random(1100);
Counts.Set(d.inv.chests, "gold", 6);
for (int i = 0; i < 6; i++) TreasureRules.Open(db, d, "gold", rng);
Counts.Set(d.inv.chests, "bronze", 4);
Counts.Set(d.inv.chests, "silver", 18);
Counts.Set(d.inv.chests, "general", 1);
d.inv.chestPoints = 35;
d.patrol.startAt = SaveStore.Iso(s.Now.AddHours(-7));
s.Commit();
root.ShowLobby();
yield return Shot(cam, "lobby", W, H);
yield return LobbyAnim(cam);
// 목록 카드 등급색 확인용(메모리 세이브만): 나머지 시작 영웅 등급 = 각자 장비 세트의 다음 등급들
int gi = 1;
foreach (var sid in db.Economy.StartHeroes)
{
var sh = s.Def(sid);
var shs = s.Save.Hero(sid);
if (sid == id || sh == null || shs == null || sh.gear.Count == 0) continue;
shs.grade = sh.gear[Mathf.Min(gi++, sh.gear.Count - 1)].grade;
}
// 진영 원 배지 확인용(메모리 세이브만 · PD 2026-11-09): 위 · 오 · 군 영웅 1명씩 보유 추가
foreach (var fac in new[] { "wei", "wu", "other" })
foreach (var fh in db.Heroes.heroes)
if (!fh.isSoldier && fh.faction == fac && !d.Owns(fh.id))
{
HeroRules.Grant(db, d, fh, HeroRules.GearSlotCount(db));
break;
}
root.Show(root.HeroList);
yield return Shot(cam, "hero_list", W, H);
Log("hero_list cols=" + root.HeroList.Columns);
// 해상도 대응(PD 2026-11-09): 넓은 화면(3:4) · 좁은 화면(9:20)
yield return Shot(cam, "hero_list_1536x2048", W2, H2);
Log("hero_list_1536x2048 cols=" + root.HeroList.Columns);
yield return Shot(cam, "hero_list_1080x2400", 1080, 2400);
Log("hero_list_1080x2400 cols=" + root.HeroList.Columns);
yield return Shot(cam, "hero_list_1080x2520", 1080, 2520);
Log("hero_list_1080x2520 cols=" + root.HeroList.Columns);
root.ShowHero(id);
yield return Shot(cam, "hero_detail", W, H);
// 영웅 상세 등급 변형(UI 상업용 재설계 ① · 목업 hero_detail_5등급 대조 · 메모리 세이브만) = 그 영웅 장비 세트의 등급 순서대로
string g0 = hs.grade;
foreach (var set in h.gear)
{
if (set.grade == g0) continue;
hs.grade = set.grade;
root.HeroDetail.Refresh();
yield return Shot(cam, "hero_detail_" + set.grade, W, H);
}
hs.grade = g0;
root.HeroDetail.Refresh();
root.Show(root.Formation);
yield return Shot(cam, "formation", W, H);
root.Show(root.Trade);
root.Trade.SetPage(0);
yield return Shot(cam, "trade_gacha", W, H);
root.Show(root.Treasures);
root.Treasures.SetPage(2);
root.Treasures.SelectChest("gold");
yield return Shot(cam, "treasure_chests", W, H);
root.Treasures.SetPage(0);
yield return Shot(cam, "treasure_owned", W, H);
// 내정(PD 2026-11-12 내정 메뉴 개편 · 탭마다 배경 장면) — 훈련(병종별) · 훈련 동작 연속 · 순찰 · 드롭다운 펼침 · 수확 팝업(광고 2배 선택) · 개발
// 파견(메모리 세이브만 · PD 2026-11-12 「영웅 파견 · 지휘관 · 문 앞 · 개발 배치」) = 보유 영웅으로 훈련(보·창·기·궁·책 각 1) · 순찰 · 개발
foreach (var ut in root.Domestic.TrainTypes)
{
var c = DispatchRules.Candidates(db, d, DispatchRules.TrainSlot(ut));
if (c.Count > 0) DispatchRules.Assign(db, d, DispatchRules.TrainSlot(ut), c[0].id);
}
foreach (var slot in new[] { DispatchRules.Patrol, DispatchRules.Dev })
foreach (var c in DispatchRules.Candidates(db, d, slot))
if (DispatchRules.Where(d, c.id) == null)
{
DispatchRules.Assign(db, d, slot, c.id);
break;
}
s.Commit();
root.Show(root.Domestic);
root.Domestic.SetTrainType(UnitType.Infantry);
root.Domestic.SetPage(0);
yield return Shot(cam, "domestic_train", W, H);
Log("domestic dispatch train=" + DispatchRules.Get(d, DispatchRules.TrainSlot(UnitType.Infantry)) + " patrol=" + DispatchRules.Get(d, DispatchRules.Patrol) + " dev=" + DispatchRules.Get(d, DispatchRules.Dev));
Log("domestic_train soldiers=" + root.Domestic.SoldierCount);
yield return DomesticAnim(cam);
foreach (var ut in root.Domestic.TrainTypes)
{
if (ut == UnitType.Infantry) continue;
root.Domestic.SetTrainType(ut);
yield return Shot(cam, "domestic_train_" + ut.ToString().ToLowerInvariant(), W, H);
// 병종별 훈련 동작 연속 캡처(PD 2026-11-12 기병 박자 · 궁병 다시 제작 · 책사 크기) — -uiOnly 에 이름을 줄 때만
yield return AnimFrames(cam, "domestic_train_anim_" + ut.ToString().ToLowerInvariant(), 40, 0.06f);
}
root.Domestic.SetTrainType(UnitType.Infantry);
root.Domestic.SetPage(1);
yield return Shot(cam, "domestic_patrol", W, H);
yield return AnimFrames(cam, "domestic_patrol_anim", 24, 0.12f); // 순찰 영웅·창병·NPC 대기 숨 고르기 확인(PD 2026-11-12)
root.Domestic.OpenHarvest();
yield return Shot(cam, "domestic_patrol_harvest", W, H);
root.CloseModal();
root.Domestic.OpenDispatch(DispatchRules.Patrol);
yield return Shot(cam, "domestic_dispatch", W, H);
Log("domestic_dispatch choices=" + root.Domestic.DispatchChoices);
root.CloseModal();
root.Domestic.SetPage(2);
yield return Shot(cam, "domestic_dev", W, H);
Log("domestic_dev hero=" + root.Domestic.DevHeroShown + " nodes=" + root.Domestic.NodeCount);
root.Arena.SetPage(0);
root.Show(root.Arena);
yield return Shot(cam, "arena", W, H);
root.StaminaModal();
yield return Shot(cam, "popup_stamina", W, H);
root.CloseModal();
root.ShowLobby();
root.ShowHero(id);
yield return Shot(cam, "hero_detail_1536x2048", W2, H2);
root.ShowLobby();
yield return Shot(cam, "lobby_1536x2048", W2, H2);
root.OpenWorldMap();
yield return Wait();
var wm = root.Map;
wm.OpenPopup(0);
yield return Shot(cam, "worldmap_popup", W, H);
wm.ConfirmPopup();
var bm = root.Battle;
float t0 = Time.realtimeSinceStartup;
while (bm.Phase != BattlePhase.PlayerTurn && Time.realtimeSinceStartup - t0 < 10f) yield return null;
yield return new WaitForSecondsRealtime(0.6f);
Unit pick = null;
foreach (var u in bm.AliveUnits(Faction.Player))
if (bm.Skills != null && bm.Skills.Of(u) != null && bm.Skills.Of(u).Active != null)
{
pick = u;
break;
}
if (pick != null) bm.Sling.Select(pick);
Log("hud select=" + (pick != null ? pick.DisplayName : "-") + " skill=" + bm.Hud.SkillButtonText);
yield return Shot(cam, "battle_hud", W, H);
foreach (var e in bm.AliveUnits(Faction.Enemy)) e.TakeDamage(e.Hp);
bm.CheckBattleEnd();
yield return new WaitForSecondsRealtime(0.6f);
yield return Shot(cam, "battle_result", W, H);
Finish(0);
}
/// <summary>
/// 로비 배경 연출 연속 캡처(-uiOnly lobby_anim 일 때만 · PD 2026-11-11 로비 연출 개선 확인) — 원본 크기(1080×1920 · 해상도 판단용 · 반 크기는 축소로 깨져 보임) 40장 ·
/// 실제 시각(초)을 로그에 남김(움직임 영상 만들 때 간격).
/// </summary>
IEnumerator LobbyAnim(Camera cam)
{
if (only == null || Match("lobby_anim") == null) yield break;
matched.Add("lobby_anim");
float t0 = Time.realtimeSinceStartup;
var times = new List<string>();
for (int f = 0; f < 40; f++)
{
yield return new WaitForSecondsRealtime(0.05f);
times.Add((Time.realtimeSinceStartup - t0).ToString("0.000", System.Globalization.CultureInfo.InvariantCulture));
yield return CaptureUtil.Capture(cam, W, H, Path.Combine(OutputDir, "lobby_anim_" + f.ToString("00") + ".png"));
}
Log("lobby_anim times " + string.Join(",", times));
}
/// <summary>
/// 훈련 장면 병사 동작 연속 캡처(-uiOnly domestic_train_anim 일 때만 · PD 2026-11-12 내정 개편) — 원본 크기 45장 · 0.08 초 간격(동작 1회 3.0 초를 덮음) · 실제 시각을 로그에(영상 간격).
/// </summary>
IEnumerator DomesticAnim(Camera cam)
{
if (only == null || Match("domestic_train_anim") == null) yield break;
matched.Add("domestic_train_anim");
float t0 = Time.realtimeSinceStartup;
var times = new List<string>();
for (int f = 0; f < 45; f++)
{
yield return new WaitForSecondsRealtime(0.08f);
times.Add((Time.realtimeSinceStartup - t0).ToString("0.000", System.Globalization.CultureInfo.InvariantCulture));
yield return CaptureUtil.Capture(cam, W, H, Path.Combine(OutputDir, "domestic_train_anim_" + f.ToString("00") + ".png"));
}
Log("domestic_train_anim times " + string.Join(",", times));
}
/// <summary>지정했을 때만(-uiOnly 이름) 원본 크기 연속 캡처 n 장 · 실제 시각을 로그에.</summary>
IEnumerator AnimFrames(Camera cam, string name, int n, float gap)
{
if (only == null || Match(name) == null) yield break;
matched.Add(name);
float t0 = Time.realtimeSinceStartup;
var times = new List<string>();
for (int f = 0; f < n; f++)
{
yield return new WaitForSecondsRealtime(gap);
times.Add((Time.realtimeSinceStartup - t0).ToString("0.000", System.Globalization.CultureInfo.InvariantCulture));
yield return CaptureUtil.Capture(cam, W, H, Path.Combine(OutputDir, name + "_" + f.ToString("00") + ".png"));
}
Log(name + " times " + string.Join(",", times));
}
static IEnumerator Wait()
{
yield return null;
yield return new WaitForSecondsRealtime(0.3f);
}
/// <summary>지정 목록에 맞는 토큰(없으면 null).</summary>
string Match(string name)
{
foreach (var t in only)
{
string k = t.Trim();
if (k.EndsWith("*") ? name.StartsWith(k.Substring(0, k.Length - 1)) : name == k) return k;
}
return null;
}
IEnumerator Shot(Camera cam, string name, int w, int h)
{
if (only != null)
{
string k = Match(name);
if (k == null)
{
// 지정 화면을 모두 찍었으면 나머지 흐름(월드맵 · 전투 등)은 건너뛰고 끝낸다
if (matched.Count >= only.Length) Finish(0);
yield break;
}
matched.Add(k);
}
yield return Wait();
// 캔버스 층 · 드로우콜 실측(PD 2026-11-10 UI 구조 개선 전·후 비교 · ui_layers.csv)
yield return UiLayerProbe.Measure(name, Log, OutputDir);
string file = Path.Combine(OutputDir, shot.ToString("00") + "_" + name + ".png");
shot++;
yield return CaptureUtil.Capture(cam, w, h, file);
Log("shot " + file);
}
void Log(string s)
{
log.Add(s);
Debug.Log("[UICapture] " + 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
}
}
}