2026-10-29 13:21:25 +00:00
using System ;
using System.Collections.Generic ;
using UnityEngine ;
2026-10-29 13:58:23 +00:00
using UnityEngine.UI ;
2026-10-29 13:21:25 +00:00
using VS.Sim ;
using VS.View ;
namespace VS.EditorTools
{
public static partial class EditorRender
{
static void SystemShots ( GameApp app )
{
var g = app . G ;
// 3일차 · 선물 해금 → 슬롯 채움 + 채팅 흐름
g . DebugSetPlayDay ( 3 ) ; g . S . giftUnlockedLifetime = true ;
g . ActivateX2Boost ( ) ; g . ActivateAutoClick ( ) ;
app . DebugTick ( 0.25f , 80 ) ;
app . CloseAllPopups ( ) ;
Shot ( app , "main_gifts_chat" ) ;
int t = - 1 ; foreach ( var s in g . GiftSlot ) if ( s > = 0 ) { t = s ; break ; }
if ( t < 0 ) { g . GiftSlot [ 0 ] = GiftTypes . BigMoneyPack ; t = GiftTypes . BigMoneyPack ; }
app . OpenGiftPopup ( t ) ; Shot ( app , "pop_gift" ) ; app . ClosePopup ( ) ;
app . OpenBoosterX2 ( ) ; Shot ( app , "pop_booster_x2" ) ; app . ClosePopup ( ) ;
g . S . afkPending = "987654" ; g . AfkSeconds = 5400 ;
app . OpenAfkPopup ( false ) ; Shot ( app , "pop_afk" ) ; app . ClosePopup ( ) ;
app . OpenTasks ( ) ; Shot ( app , "pop_task" ) ; app . ClosePopup ( ) ;
app . OpenDaily ( ) ; Shot ( app , "pop_daily" ) ; app . ClosePopup ( ) ;
app . OpenTicketShop ( ) ; Shot ( app , "pop_ticketshop" ) ; app . ClosePopup ( ) ;
g . S . rankIndex = Math . Max ( g . S . rankIndex , g . NextHouseRankReq ( ) ) ;
app . OpenBuyNewHouse ( ) ; Shot ( app , "pop_newhouse" ) ; app . ClosePopup ( ) ;
g . S . afkPending = "0" ;
BattleShots ( app ) ;
2026-10-29 14:42:47 +00:00
UnitShots ( app ) ;
2026-10-31 10:29:10 +00:00
Stage50Shots ( app ) ; // 스테이지 50 — 선택 · 정보 · 안내 카드 · 지형 · 필드 · 새 공격 · 성 능력(EditorRender.Stage50.cs)
2026-10-29 13:21:25 +00:00
app . SwitchTab ( 0 ) ;
}
// 전투 탭 — 기본 흐름 · 맵 4종 · 성 기믹 3종 · 성 투사체 5종 (카드 = 아래 가운데에서 즉시 소환)
static void BattleShots ( GameApp app )
{
var g = app . G ;
app . BattleTest = null ;
2026-10-29 13:58:23 +00:00
// 스테이지 선택 — 전투 탭 = 고르는 화면부터(첫 화면 = 다음에 깰 스테이지 1) · 잠긴 스테이지 · 지도 = 정보 창 · 시작 = 바로 전투
app . SwitchTab ( 1 ) ; app . RefreshAll ( ) ;
StageCheck ( app , "stage_select" , 1 , true ) ; Shot ( app , "stage_select" ) ;
app . ShowStage ( 3 ) ; StageCheck ( app , "stage_locked" , 3 , false ) ; Shot ( app , "stage_locked" ) ;
app . OpenStageInfo ( ) ; Shot ( app , "stage_info" ) ; app . ClosePopup ( ) ;
2026-10-31 10:29:10 +00:00
// 처음 나오는 요소 = 안내 카드(스테이지 1 = 일반 화살) → 「전투 시작」
2026-10-29 13:58:23 +00:00
app . StartStage ( 1 , 5 ) ;
2026-10-31 10:29:10 +00:00
if ( app . OpenPopupId ! = "StageIntro" ) { fails + + ; notes . Add ( "stage_intro: 스테이지 1 시작 때 안내 카드가 안 뜸" ) ; }
else { Shot ( app , "stage_intro" ) ; app . ConfirmIntro ( ) ; }
2026-10-29 13:58:23 +00:00
if ( app . StageRoot . gameObject . activeSelf | | ! app . BattleRoot . gameObject . activeSelf | | app . B . State ! = BattleState . Fighting | | app . B . Stage ! = 1 ) { fails + + ; notes . Add ( "stage_start: 시작을 눌렀는데 전투 화면이 아님" ) ; }
2026-10-29 14:42:47 +00:00
if ( app . Root . Find ( "TabBar" ) . gameObject . activeSelf ) { fails + + ; notes . Add ( "stage_start: 전투 중에 아래 메뉴가 보임" ) ; }
2026-10-29 13:58:23 +00:00
app . B . Mana = 10 ;
2026-10-29 13:21:25 +00:00
app . DeployCard ( 0 ) ; app . DebugTick ( 0.05f , 40 ) ; app . DeployCard ( 0 ) ; app . DebugTick ( 0.05f , 40 ) ; app . DeployCard ( 0 ) ;
g . S . battleSlots = 6 ; app . B . MaxMana = 15 ; app . B . Mana = 15 ;
app . DeployCard ( 1 ) ; app . DeployCard ( 4 ) ; app . DeployCard ( 5 ) ;
app . DebugTick ( 0.05f , 240 ) ;
Shot ( app , "battle_fight" ) ;
RivalCheck ( app , "battle_fight" ) ;
LayoutCheck ( app , "battle_fight" ) ;
app . OpenBattleUpgrade ( ) ; Shot ( app , "battle_upgrade" ) ; app . ClosePopup ( ) ;
app . OpenBattleTest ( ) ; Shot ( app , "battle_test" ) ; app . ClosePopup ( ) ;
app . B . CastleHpDebugKill ( ) ; app . DebugTick ( 0.05f , 2 ) ; Shot ( app , "battle_won" ) ;
2026-10-29 13:58:23 +00:00
// 이긴 뒤 스테이지 선택 = 1 클리어 표시 · 2 열림(첫 화면)
app . OpenStageSelect ( 1 ) ; StageCheck ( app , "stage_cleared" , 1 , true ) ; Shot ( app , "stage_cleared" ) ;
app . OpenStageSelect ( app . G . StageFocus ) ; StageCheck ( app , "stage_next" , 2 , true ) ; Shot ( app , "stage_next" ) ;
2026-10-29 13:21:25 +00:00
// 맵 4종 (바위·언덕 켬 · 성 공격 끔 — 길 찾기 장면)
string [ ] mapKey = { "plain" , "b1" , "b2" , "b3" } ;
for ( int k = 0 ; k < 4 ; k + + )
{
Scene ( app , new BattleSetup { mapType = k , obstacles = 1 , shots = new List < string > ( ) , gimmicks = new List < string > ( ) } , 30 + k ) ;
app . DebugTick ( 0.05f , 300 ) ;
Shot ( app , "battle_map_" + mapKey [ k ] ) ;
}
// 성 기믹 · 투사체 — 다리 2개 · 장애물 없음 · 그것만 켜고 발동 순간
CastleScene ( app , "battle_gim_fireball" , L ( ) , L ( "fireball" ) , ( ) = > app . B . Fx . Exists ( f = > f . type = = "blast" ) ) ;
CastleScene ( app , "battle_gim_lightning" , L ( ) , L ( "lightning" ) , ( ) = > app . B . Fx . Exists ( f = > f . type = = "lightning" ) ) ;
CastleScene ( app , "battle_gim_summon" , L ( ) , L ( "summonMelee" , "summonRanged" ) , ( ) = > app . B . Minions . Count > = MaxMinions ( app . B ) & & app . B . Minions . Exists ( m = > m . attacking ) ) ;
CastleScene ( app , "battle_shot_normal" , L ( "normal" ) , L ( ) , ( ) = > app . B . Shots . Exists ( s = > s . type = = "normal" & & UnityEngine . Vector2 . Distance ( s . pos , app . B . CastlePos ) > app . B . C . castle . radius + 0.8f ) ) ;
CastleScene ( app , "battle_shot_splash" , L ( "splash" ) , L ( ) , ( ) = > app . B . Fx . Exists ( f = > f . type = = "blast" ) ) ;
CastleScene ( app , "battle_shot_multi" , L ( "multi" ) , L ( ) , ( ) = > app . B . Shots . FindAll ( s = > s . type = = "multi" & & s . left < app . B . Range ( s . def ) - 1.2f ) . Count > = 3 ) ;
CastleScene ( app , "battle_shot_slow" , L ( "slow" ) , L ( ) , ( ) = > app . B . Units . Exists ( u = > u . slow > 0f ) & & app . B . Shots . Exists ( s = > s . type = = "slow" ) ) ;
CastleScene ( app , "battle_shot_laser" , L ( "laser" ) , L ( ) , ( ) = > app . B . Fx . Exists ( f = > f . type = = "laser" & & f . t < 0.1f ) ) ;
// 깃발 — 왼쪽 다리 건너편에 꽂으면 전원이 그리로 먼저
Scene ( app , new BattleSetup { mapType = 2 , obstacles = 0 , castleX = 9f , shots = L ( ) , gimmicks = L ( ) } , 41 ) ;
app . DebugTick ( 0.05f , 20 ) ;
app . PlaceFlag ( new Vector2 ( app . B . Map . Bridges [ 0 ] , app . B . C . riverY + 3f ) ) ;
app . B . Mana = 15 ; app . DeployCard ( 0 ) ; app . DeployCard ( 1 ) ;
app . DebugTick ( 0.05f , 110 ) ;
var flag = app . BattleRoot . Find ( "Arena/Flag" ) ;
if ( flag = = null | | ! flag . gameObject . activeSelf | | ! app . B . Units . Exists ( u = > u . flag ! = app . B . FlagVer ) ) { fails + + ; notes . Add ( "battle_flag: 깃발 · 깃발로 가는 유닛 못 잡음" ) ; }
Shot ( app , "battle_flag" ) ;
// 물량 — 1단계 성 구성(stageLoadouts)에 검사를 마나 차는 대로 계속 보내 성벽에 많이 붙은 순간 (카드 1장 = 1마리)
var lo1 = g . BC . Loadout ( 1 ) ;
app . BattleTest = new BattleSetup { shots = new List < string > ( lo1 . shots ) , gimmicks = new List < string > ( lo1 . gimmicks ) } ; app . NewBattleRound ( 7 ) ; app . B . Start ( ) ;
bool swarm = false ;
for ( int i = 0 ; i < 2400 & & ! swarm ; i + + ) { if ( app . B . Mana > = g . BattleUnit ( 0 ) . cost ) app . DeployCard ( 0 ) ; app . DebugTick ( 0.05f , 1 ) ; swarm = app . B . Units . FindAll ( u = > u . attacking ) . Count > = 15 ; }
if ( ! swarm ) { fails + + ; notes . Add ( "battle_swarm: 물량 장면 못 잡음" ) ; }
LayoutCheck ( app , "battle_swarm" ) ;
Shot ( app , "battle_swarm" ) ;
// 성벽 회복 — 맞은 뒤 regenDelay 초 지나 초록 빛
app . BattleTest = new BattleSetup { mapType = 0 , obstacles = 0 , castleX = 9f , shots = L ( ) , gimmicks = L ( ) } ; app . NewBattleRound ( 42 ) ; app . B . Start ( ) ;
app . B . HitCastle ( app . B . CastleMaxHp * 0.4f ) ;
bool heal = false ;
for ( int i = 0 ; i < 400 & & ! heal ; i + + ) { app . DebugTick ( 0.05f , 1 ) ; heal = app . B . Fx . Exists ( f = > f . type = = "heal" & & f . t < 0.15f ) & & app . B . CastleHeals > = 2 ; }
if ( ! heal ) { fails + + ; notes . Add ( "battle_regen: 회복 장면 못 잡음" ) ; }
Shot ( app , "battle_regen" ) ;
// 상대방 칸 — 투사체 5 + 기믹 4 전부 켜도 카드 칸과 안 겹침
var all = L ( ) ; foreach ( var d in g . BC . skills ) all . Add ( d . id ) ; foreach ( var d in g . BC . summons ) all . Add ( d . id ) ;
Scene ( app , new BattleSetup { mapType = 1 , obstacles = 0 , shots = g . BC . shots . ConvertAll ( d = > d . id ) , gimmicks = all } , 43 ) ;
app . DebugTick ( 0.05f , 200 ) ;
RivalCheck ( app , "battle_rival_all" ) ;
Shot ( app , "battle_rival_all" ) ;
app . BattleTest = null ;
}
/// 오른쪽 위 = 상대방 얼굴 + 스킬 줄(성 구성 수만큼) · 남은 시간 칸 없음 · 카드 칸(아래 view.trayH)과 안 겹침
static void RivalCheck ( GameApp app , string name )
{
var root = app . BattleRoot ; var rv = root . Find ( "Rival" ) as RectTransform ;
2026-10-31 10:29:10 +00:00
int want = app . B . CastleShots . Count + app . B . CastleSkills . Count + app . B . Summoners . Count + app . B . Powers . Count , rows = 0 ;
2026-10-29 13:21:25 +00:00
if ( rv ! = null ) foreach ( Transform ch in rv ) if ( ch . name . StartsWith ( "Skill_" ) ) rows + + ;
bool timer = root . Find ( "Timer" ) ! = null ;
if ( timer | | rv = = null | | rows ! = want ) { fails + + ; notes . Add ( $"{name}: 상대방 칸 이상 (남은 시간 칸 {timer} · 스킬 줄 {rows}/{want})" ) ; return ; }
float h = root . rect . height , bottom = h + rv . anchoredPosition . y - rv . sizeDelta . y ;
if ( h > 0f & & bottom < app . G . BC . view . trayH ) { fails + + ; notes . Add ( $"{name}: 상대방 칸이 카드 칸과 겹침 (아래 끝 {bottom:0})" ) ; }
}
/// 클래시 로얄 비율(battle.json view) — 맵 18칸이 화면 폭의 85% 이상 · 카드 칸 높이 = trayH · 맵이 카드 칸 위에서 화면 안 · 유닛 그림 한 변 ≥ 1.5칸
static void LayoutCheck ( GameApp app , string name )
{
var c = app . G . BC ; var v = c . view ; var root = app . BattleRoot ;
var ar = root . Find ( "Arena" ) as RectTransform ; var tray = root . Find ( "CardPanel" ) as RectTransform ;
float w = root . rect . width , h = root . rect . height , playW = c . arenaW * v . tileX , bottom = ar . anchoredPosition . y - ar . sizeDelta . y * 0.5f , top = bottom + ar . sizeDelta . y ;
float minUnit = float . MaxValue ;
foreach ( Transform t in ar . Find ( "Units" ) ) if ( t . name . StartsWith ( "U" ) ) minUnit = Mathf . Min ( minUnit , ( ( RectTransform ) t ) . sizeDelta . y ) ;
bool ok = w < = 0f | | ( playW > = w * 0.85f & & Mathf . Abs ( tray . sizeDelta . y - v . trayH ) < 0.5f & & bottom > = v . trayH - 0.5f & & top < = h + 0.5f & & ( minUnit = = float . MaxValue | | minUnit > = 1.5f * v . tileX ) ) ;
if ( ! ok ) { fails + + ; notes . Add ( $"{name}: 화면 비율 이상 (맵 {playW:0}/{w:0} · 카드 칸 {tray.sizeDelta.y:0} · 맵 아래 {bottom:0} 위 {top:0}/{h:0} · 유닛 그림 {minUnit:0})" ) ; }
else Debug . Log ( $"[VS] LAYOUT {name} 맵 {playW:0}/{w:0} px({playW / Mathf.Max(1f, w) * 100f:0}%) · 칸 {v.tileX:0}× {v.tileY:0} · 카드 칸 {tray.sizeDelta.y:0} · 맵 {bottom:0}~{top:0}/{h:0} · 가장 작은 유닛 그림 {minUnit:0} px" ) ;
}
2026-10-29 13:58:23 +00:00
/// 스테이지 선택 화면 — 보임(전투 화면은 숨김) · 제목 「번호. 이름」 · 시작 버튼 열림/잠김 · 화살표(처음 = 왼쪽 없음 · 끝 = 오른쪽 없음) · 클리어 표시
static void StageCheck ( GameApp app , string name , int stage , bool open )
{
var root = app . StageRoot ; int n = app . G . StageCount ;
var title = root . Find ( "Title" ) ? . GetComponent < Text > ( ) ; var start = root . Find ( "Start" ) ? . GetComponent < Button > ( ) ;
bool prev = root . Find ( "Prev" ) . gameObject . activeSelf , next = root . Find ( "Next" ) . gameObject . activeSelf , clear = root . Find ( "Clear" ) . gameObject . activeSelf ;
2026-10-29 14:42:47 +00:00
bool ok = root . gameObject . activeSelf & & ! app . BattleRoot . gameObject . activeSelf & & app . Root . Find ( "TabBar" ) . gameObject . activeSelf & & title ! = null & & title . text . StartsWith ( stage + ". " ) & & start ! = null & & start . interactable = = open
2026-10-29 13:58:23 +00:00
& & prev = = ( stage > 1 ) & & next = = ( stage < n ) & & clear = = app . G . StageCleared ( stage ) ;
if ( ! ok ) { fails + + ; notes . Add ( $"{name}: 스테이지 선택 화면 이상 (제목 {title?.text} · 시작 {start?.interactable} · 화살표 {prev}/{next} · 클리어 {clear})" ) ; }
2026-10-29 14:42:47 +00:00
}
// 유닛 메뉴 — 아래 메뉴 3개 · 덱(프리셋 · 6칸 · 잠긴 칸) · 유닛 목록 · 상세 · 프리셋 2 + 빈 칸 · 바꿀 칸 고르기
static void UnitShots ( GameApp app )
{
var g = app . G ; var us = g . BC . units ; int slots0 = g . S . battleSlots ;
g . S . battleSlots = 3 ;
app . SwitchTab ( 2 ) ; UnitCheck ( app , "units_deck" ) ; Shot ( app , "units_deck" ) ;
2026-10-29 15:31:24 +00:00
app . ScrollUnits ( 0.5f ) ; Shot ( app , "units_locked" ) ; app . ScrollUnits ( 1f ) ;
2026-10-29 14:42:47 +00:00
app . OpenUnitDetail ( us [ 0 ] . id ) ; Shot ( app , "unit_detail" ) ;
app . OpenUnitDetail ( us [ us . Count - 1 ] . id ) ; Shot ( app , "unit_detail_last" ) ;
app . CloseUnitDetail ( ) ;
2026-10-29 15:31:24 +00:00
// 편성 — 「편성」 → 유닛 누름 = 덱 칸마다 화살표 → 덱 칸 누름 = 그 칸으로 교체(프리셋 2 · 빈 칸 하나)
app . SelectPreset ( 1 ) ; g . DeckRemove ( us [ 0 ] . id ) ;
app . ToggleUnitEdit ( ) ; EditCheck ( app , "units_edit" , false ) ; Shot ( app , "units_edit" ) ;
app . TapUnitCard ( us [ 0 ] . id ) ; EditCheck ( app , "units_edit_pick" , true ) ; Shot ( app , "units_edit_pick" ) ;
app . PickDeckSlot ( 3 ) ; EditCheck ( app , "units_edit_put" , false ) ;
if ( g . ActiveDeck [ 3 ] ! = us [ 0 ] . id | | g . DeckSel ! = 1 ) { fails + + ; notes . Add ( "units_edit: 고른 칸에 안 들어감" ) ; }
app . ToggleUnitEdit ( ) ; app . SelectPreset ( 0 ) ; g . S . battleSlots = slots0 ;
}
/// 편성 중 — 유닛을 고르면 덱 칸마다 화살표 · 넣고 나면 화살표 없음 · 편성 버튼 = 「완료」
static void EditCheck ( GameApp app , string name , bool arrows )
{
var content = app . UnitRoot . Find ( "List/Viewport/Content" ) ; int n = 0 ;
foreach ( Transform ch in content . Find ( "DeckGrid" ) ) if ( ch . gameObject . activeSelf & & ch . Find ( "Arrow" ) ! = null ) n + + ;
var label = content . Find ( "CollHead/Edit/Label" ) ? . GetComponent < Text > ( ) ;
if ( n ! = ( arrows ? app . G . BC . slotMax : 0 ) | | label = = null | | label . text ! = "완료" ) { fails + + ; notes . Add ( $"{name}: 편성 화면 이상 (화살표 {n} · 버튼 {label?.text})" ) ; }
2026-10-29 14:42:47 +00:00
}
/// 아래 메뉴 = 연구 · 전투 · 유닛 · 유닛 화면 = 덱 칸 slotMax 개 · 목록 = 유닛 수 · 프리셋 버튼 = deckPresets 개
static void UnitCheck ( GameApp app , string name )
{
var g = app . G ; var root = app . UnitRoot ; var bar = app . Root . Find ( "TabBar" ) ;
var content = root . Find ( "List/Viewport/Content" ) ;
int deck = content . Find ( "DeckGrid" ) . childCount , coll = content . Find ( "CollGrid" ) . childCount , presets = 0 ;
foreach ( Transform ch in content . Find ( "DeckHead" ) ) if ( ch . name . StartsWith ( "P" ) ) presets + + ;
string tabs = "" ;
if ( bar ! = null ) foreach ( Transform ch in bar ) { var t = ch . Find ( "Label" ) ? . GetComponent < Text > ( ) ; if ( t ! = null ) tabs + = t . text + "|" ; }
2026-10-29 15:31:24 +00:00
// 미보유 = 목록 그림 중 보유 유닛이 쓰지 않는 것 · 「편성」 버튼 = 목록 머리줄 오른쪽
var ownedArts = new HashSet < int > ( ) ; foreach ( var u in g . BC . units ) { int a = UnitArt . ArtOf ( u . id ) ; if ( a > 0 ) ownedArts . Add ( a ) ; }
int want = 0 ; foreach ( var r in g . BC . roster ) if ( ! ownedArts . Contains ( r . art ) ) want + + ;
int locked = content . Find ( "LockGrid" ) . childCount ; bool edit = content . Find ( "CollHead/Edit" ) ! = null ;
bool ok = root . gameObject . activeSelf & & bar ! = null & & bar . gameObject . activeSelf & & tabs = = "연구|전투|유닛|" & & deck = = g . BC . slotMax & & coll = = g . BC . units . Count & & presets = = g . DeckPresets & & want > 0 & & locked = = want & & edit ;
if ( ! ok ) { fails + + ; notes . Add ( $"{name}: 유닛 화면 이상 (메뉴 {tabs} · 덱 {deck} · 보유 {coll} · 미보유 {locked}/{want} · 프리셋 {presets} · 편성 {edit})" ) ; }
2026-10-29 13:58:23 +00:00
}
2026-10-29 13:21:25 +00:00
static List < string > L ( params string [ ] s ) = > new List < string > ( s ) ;
static int MaxMinions ( Battle b ) { int n = 0 ; foreach ( var s in b . Summoners ) n + = s . def . max ; return n ; }
static void Scene ( GameApp app , BattleSetup s , int seed )
{
app . BattleTest = s ; app . NewBattleRound ( seed ) ;
app . B . Start ( ) ; app . B . MaxMana = 15 ; app . B . Mana = 15 ;
app . DeployCard ( 2 ) ; app . DeployCard ( 1 ) ; app . DeployCard ( 0 ) ; app . DeployCard ( 0 ) ; app . DeployCard ( 4 ) ;
}
static void CastleScene ( GameApp app , string name , List < string > shots , List < string > gimmicks , Func < bool > moment )
{
Scene ( app , new BattleSetup { mapType = 2 , obstacles = 0 , castleX = 9f , shots = shots , gimmicks = gimmicks } , 40 ) ;
bool ok = false ;
for ( int i = 0 ; i < 1200 & & ! ok ; i + + ) { app . DebugTick ( 0.05f , 1 ) ; ok = moment ( ) ; }
if ( ! ok ) { fails + + ; notes . Add ( name + ": 발동 장면 못 잡음" ) ; }
Shot ( app , name ) ;
}
}
}