ThreeKingdomsSlingshot/Assets/Scripts/Runtime/AutoPlayDriver.cs

216 lines
8.7 KiB
C#
Raw Permalink Normal View History

using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
namespace TKS
{
/// <summary>
/// 자동 플레이 실측: 아군도 AI 로 조작해 몇 턴을 돌리며 스크린샷·로그를 남긴다.
/// 에디터에서 ProjectSetup.AutoPlayCapture 가 SessionState 로 출력 폴더를 넘기면 생성된다.
/// 비활성 에디터 창은 프레임이 느리므로 timeScale 을 올려 실시간 기준으로 빨리 끝낸다.
/// </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);
Time.timeScale = 4f;
Time.maximumDeltaTime = 1f;
float t0 = Time.realtimeSinceStartup;
아웃게임 1차(PD #1093 세션 A) — 본영 로비 · 세이브 · 군량 · 영웅(레벨·승급 6칸·진화·스킬 표시) · 편성(전열 3/후열 2 · 프리셋 3) · 거래(뽑기·골드 장비 상점) · 스테이지 보상·별 3·소탕 - 데이터: 기획 tables/ 12장 → Assets/Data/OutGame/*.csv · 메뉴 TKS/Import OutGame Data(Editor/DataImporter) → Resources/OutGame 9 자산(HeroDb·GearDb·GachaConfig·EconomyConfig·RewardTable·LevelTable·PromotionTable·EvolveTable·SkillInfoDb) · 검증(행 수·필수 열·id 유일·hero↔skill↔gear·확률 합) 오류 시 자산 불변 · BalanceConfig.heroes[]/HeroDef 삭제 → HeroDb(기존 5인 id 유지) · economy.csv 키 17 + gacha.csv 키 2 추가(기획서 본문 값 · C45) - 세이브: persistentDataPath/save.json · ver 1 · 즉시 저장 · 오프라인 군량 회복 · 일일 초기화 · 신규 유저 = 유비·관우·장비·조운·황충 Lv1 녹 ★=희귀도 · 골드 1000 · 보옥 300 · 군량 30 · 테스트·캡처 = 메모리 전용 - 화면(uGUI 와이어프레임 · img03·06·08·11): 시작 = 본영 로비(출정 → 월드맵 · 편성 · 연무장/보물/내정 준비 중 · 동맹 추후) · 재화 바(월드맵 포함 · 군량 타이머) · 하단 탭 5 · ◀/Esc 일관 · 영웅 목록·상세·상세정보·도감 · 편성(탭·드래그) · 뽑기(천장 n/80 · 확률 공개 · 첫 10연 무료 · 10연 ★★ 보장 · 카드 순차 공개) · 장비 상점(흰/녹 6종 일일 · sell×5 · 새로고침 보옥 20) - 전투 연결: BattleManager.LineupProvider/Deployed(LineupEntry · §3-2 스탯 · 방어 배율 생성 뒤) · playerSpawnPoints = 전열 (−4/0/4, −12) · 후열 (−2/2, −14) · WorldMapScreen 훅(팝업 군량·보상·별 · 입장 군량 · 노드 별 star_on/off · 소탕 · ◀ 본영 · TopInset) · 결과 패널(별 3 조건 · 골드·경험·보옥·장비) · GameBootstrap/AutoPlayDriver 로비 시작 - B 세션 파일(ActionResolver·Combat·UnitView·BattleHUD·SlingshotInput·EnemyAI) 미변경 - 테스트 56/56(기존 41 유지 · 신규 OutGameTests 15) · console error 0 · 사본 GUI 캡처 11장(DataImporter.CaptureOutGame) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 12:11:10 +00:00
// 시작 화면 = 본영 로비(2026-11-05 #1093) → 출정 = 챕터 월드맵 → 캡처 · 1-1 팝업 캡처 → 출전(PD 2026-11-05)
시작 화면 = 챕터 월드맵(스크롤 2160×3840 · 10장) → 스테이지 팝업(임시) → 전투 → 결과 탭 = 맵 복귀·다음 노드 열림 + 아군 뒷모습·황건적·동탁군 포즈 반영 (PD 2026-11-05) - WorldMapScreen(신규): ScrollRect 맵 · 길 따라 깃발 노드(open/clear/lock · 보스) · 길 점 · 현재 위치 핀 · 챕터 띠 ◀▶ · 스테이지 팝업(적 구성·보스 · 출전/닫기) · 임시 진행도(메모리) - 데이터 = Resources/ChapterMap/Data/world.json(원화 세션 chap_install.py 생성 · 맵 크기·실제 길·부품 크기/피벗/번호 자리·임시 스테이지 연결 = 챕터당 10노드 · 전투 = 기존 스테이지 0/1/2) · 지명판 = 끔(생성 그림과 위치 어긋남) - GameBootstrap: StartOnWorldMap(기본 true) · BattleManager: ResultHandler · GoIdle · BattleHUD 결과 문구 · AutoPlayDriver: 월드맵·팝업 캡처 후 1-1 출전 - UnitArtImporter: ChapterMap/Maps(4096 · 압축) · ChapterMap/UI(import.json 피벗·9-slice·PPU 133.33) - 유닛: 아군 5인 앞·뒤 × idle/ready/action(키아트 세션 · md5 30/30) · 황건적 5 = 인박스 20261105_0312_hgj(QA PASS · md5 15/15) · 동탁군 5(여포 포함 · 원화 세션 poses md5 15/15 · PD 지시로 직접 반영) - 테스트 27/27(+ 월드맵 시작 · 팝업→전투→맵 복귀 · 맵/UI 리소스 · 포즈 세트·동작별 스프라이트 교체 · 앞뒤 클립 고정 자료 hgj_infantry → 아군 아트) · console error 0 - 남은 것(QA r2 B 2 · PD 결정): ch04 보스 초상 틀이 위 HUD 줄 안 · ch08 경로가 다리 없는 큰길을 피해 풀밭·강둑으로 우회 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 18:38:47 +00:00
yield return null;
아웃게임 1차(PD #1093 세션 A) — 본영 로비 · 세이브 · 군량 · 영웅(레벨·승급 6칸·진화·스킬 표시) · 편성(전열 3/후열 2 · 프리셋 3) · 거래(뽑기·골드 장비 상점) · 스테이지 보상·별 3·소탕 - 데이터: 기획 tables/ 12장 → Assets/Data/OutGame/*.csv · 메뉴 TKS/Import OutGame Data(Editor/DataImporter) → Resources/OutGame 9 자산(HeroDb·GearDb·GachaConfig·EconomyConfig·RewardTable·LevelTable·PromotionTable·EvolveTable·SkillInfoDb) · 검증(행 수·필수 열·id 유일·hero↔skill↔gear·확률 합) 오류 시 자산 불변 · BalanceConfig.heroes[]/HeroDef 삭제 → HeroDb(기존 5인 id 유지) · economy.csv 키 17 + gacha.csv 키 2 추가(기획서 본문 값 · C45) - 세이브: persistentDataPath/save.json · ver 1 · 즉시 저장 · 오프라인 군량 회복 · 일일 초기화 · 신규 유저 = 유비·관우·장비·조운·황충 Lv1 녹 ★=희귀도 · 골드 1000 · 보옥 300 · 군량 30 · 테스트·캡처 = 메모리 전용 - 화면(uGUI 와이어프레임 · img03·06·08·11): 시작 = 본영 로비(출정 → 월드맵 · 편성 · 연무장/보물/내정 준비 중 · 동맹 추후) · 재화 바(월드맵 포함 · 군량 타이머) · 하단 탭 5 · ◀/Esc 일관 · 영웅 목록·상세·상세정보·도감 · 편성(탭·드래그) · 뽑기(천장 n/80 · 확률 공개 · 첫 10연 무료 · 10연 ★★ 보장 · 카드 순차 공개) · 장비 상점(흰/녹 6종 일일 · sell×5 · 새로고침 보옥 20) - 전투 연결: BattleManager.LineupProvider/Deployed(LineupEntry · §3-2 스탯 · 방어 배율 생성 뒤) · playerSpawnPoints = 전열 (−4/0/4, −12) · 후열 (−2/2, −14) · WorldMapScreen 훅(팝업 군량·보상·별 · 입장 군량 · 노드 별 star_on/off · 소탕 · ◀ 본영 · TopInset) · 결과 패널(별 3 조건 · 골드·경험·보옥·장비) · GameBootstrap/AutoPlayDriver 로비 시작 - B 세션 파일(ActionResolver·Combat·UnitView·BattleHUD·SlingshotInput·EnemyAI) 미변경 - 테스트 56/56(기존 41 유지 · 신규 OutGameTests 15) · console error 0 · 사본 GUI 캡처 11장(DataImporter.CaptureOutGame) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 12:11:10 +00:00
var og = TKS.OutGame.OutGameRoot.Instance;
if (og != null && og.LobbyVisible)
{
og.OpenWorldMap();
yield return null;
}
시작 화면 = 챕터 월드맵(스크롤 2160×3840 · 10장) → 스테이지 팝업(임시) → 전투 → 결과 탭 = 맵 복귀·다음 노드 열림 + 아군 뒷모습·황건적·동탁군 포즈 반영 (PD 2026-11-05) - WorldMapScreen(신규): ScrollRect 맵 · 길 따라 깃발 노드(open/clear/lock · 보스) · 길 점 · 현재 위치 핀 · 챕터 띠 ◀▶ · 스테이지 팝업(적 구성·보스 · 출전/닫기) · 임시 진행도(메모리) - 데이터 = Resources/ChapterMap/Data/world.json(원화 세션 chap_install.py 생성 · 맵 크기·실제 길·부품 크기/피벗/번호 자리·임시 스테이지 연결 = 챕터당 10노드 · 전투 = 기존 스테이지 0/1/2) · 지명판 = 끔(생성 그림과 위치 어긋남) - GameBootstrap: StartOnWorldMap(기본 true) · BattleManager: ResultHandler · GoIdle · BattleHUD 결과 문구 · AutoPlayDriver: 월드맵·팝업 캡처 후 1-1 출전 - UnitArtImporter: ChapterMap/Maps(4096 · 압축) · ChapterMap/UI(import.json 피벗·9-slice·PPU 133.33) - 유닛: 아군 5인 앞·뒤 × idle/ready/action(키아트 세션 · md5 30/30) · 황건적 5 = 인박스 20261105_0312_hgj(QA PASS · md5 15/15) · 동탁군 5(여포 포함 · 원화 세션 poses md5 15/15 · PD 지시로 직접 반영) - 테스트 27/27(+ 월드맵 시작 · 팝업→전투→맵 복귀 · 맵/UI 리소스 · 포즈 세트·동작별 스프라이트 교체 · 앞뒤 클립 고정 자료 hgj_infantry → 아군 아트) · console error 0 - 남은 것(QA r2 B 2 · PD 결정): ch04 보스 초상 틀이 위 HUD 줄 안 · ch08 경로가 다리 없는 큰길을 피해 풀밭·강둑으로 우회 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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();
}
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<CameraController>();
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
}
}
}