Merge branch 'wl/ui/WL-813o-zone-pickup'
This commit is contained in:
commit
6850ceb326
|
|
@ -0,0 +1,160 @@
|
|||
# WL813o Probe 2026-09-09 13:01:57 (edit mode · Play 0 · 로그인 0)
|
||||
playMode=False
|
||||
|
||||
── ⓪ 에셋 (WLZonePickupSettings · NEW) · 813p 계약
|
||||
[PASS] Resources.Load("WL/WLZonePickupSettings")
|
||||
[PASS] Enabled = True
|
||||
값: cardCount=3 timeout=10s(unscaled=True) first=True includeFinalZone=False guideWait=True(max 8s) deferRevive=True pauseTimer=True holdWorld=False stopActors=True openDelay=0.35s
|
||||
[PASS] 존4(마지막 존) 기본 제외 — 보스 흐름 방해 금지(발주서 §1-1)
|
||||
[PASS] 세계 정지 기본 off = 타이머만 정지(발주서 §1-3 권장)
|
||||
[PASS] 타임아웃 10 s(발주서 §1-2) — 실측 10
|
||||
[PASS] 3장(기획안 F-3) — 실측 3
|
||||
[PASS] 남은 초·카드 글자 아웃라인(813y ApplyEdge) outlineWidth=0.26
|
||||
[PASS] 813p WLRunSettings 존재(존 수·런 길이의 주인 · UI 는 수치를 갖지 않는다)
|
||||
813p 값(읽기만): runSeconds=240 존 4개
|
||||
|
||||
── ① 🔴 원본 SelectSkillUI 노드 실측 — 발주서 §1-1 전제와 다르다
|
||||
[PASS] LoadPrefabContents(Assets/Res_Addr/MainUI/NewGameUI.prefab)
|
||||
NewGameUI.prefab 안: SelectSkillUI=0 SelectSkillCard=0 GameUI=0 UILabel=0 UISprite=0 TextMeshProUGUI=712
|
||||
[PASS] 원본 SelectSkillUI 노드 0개 = 열 대상이 없다
|
||||
[PASS] GameUI 컴포넌트 0개 · GameUI.Ins=null → MyActor.cs:162 의 자동전투 정지 배선은 작동하지 않는다
|
||||
[PASS] 실제 인게임 UI 에 NGUI 위젯 0개(원본 카드 UI 는 NGUI) · TMP 는 712개 = uGUI 트리
|
||||
→ 그래서 813o 는 ① 원본 노드가 있으면 그대로 열고, ② 없으면 같은 원본 데이터·효과 경로를 쓰는 런타임 uGUI 패널로 간다.
|
||||
|
||||
── ② 원본 데이터·효과 경로 — table_selectskill / InGameInfo / ActorInfo
|
||||
[PASS] 테이블 원본 TextAsset Assets/ResWork/Table/table_selectskill.txt
|
||||
[PASS] 원본 table_selectskill.Awake 확보
|
||||
[PASS] table_selectskill.Ins 설정(원본 Awake 경로)
|
||||
[PASS] InGameInfo.Ins 설정
|
||||
[PASS] ActorInfo.Ins 설정(All_Stop 대상 0 = 무해)
|
||||
테이블 행 수 = 58
|
||||
[PASS] 카드 3장을 뽑기에 충분한 행 수
|
||||
[PASS] 원본 뽑기 Get_DataList(3) = 3행(셔플·보유 제외·PreSkill 규칙 그대로)
|
||||
뽑기 예시 0: id=102 "체력(소)" pet=False
|
||||
뽑기 예시 1: id=12 "서리 요정" pet=False
|
||||
뽑기 예시 2: id=15 "바람의 길" pet=False
|
||||
|
||||
── ③ 노드 조립 — WL_ZonePickup(런타임 생성 · HideFlags.DontSave) · 구독 3종
|
||||
Subscribe : 구독=True (ZoneCleared/RunStarted/RunEnded)
|
||||
[PASS] ZoneCleared 구독 +1 — 실측 1
|
||||
Subscribe(중복): 구독 변화 없음(True)
|
||||
[PASS] 2회 호출해도 +1(중복 구독 0)
|
||||
[PASS] IngameUIs/WL_HUD 경로 실측(813y/813q 와 같은 부모)
|
||||
[PASS] WL_HUD 가 SafeAreaFitter 아래 = Safe Area 안
|
||||
Bind : 연결 IngameUIs/WL_HUD · box(1742.22, 1280.00) 카드=3 unitsPerPx=1.7778
|
||||
[PASS] WL_ZonePickup 노드 생성
|
||||
배치 : box(1742.22, 1280.00) 카드=3 unitsPerPx=1.7778
|
||||
[PASS] 조립 직후에는 닫혀 있다
|
||||
|
||||
── ④ 가짜 ZoneCleared(존 0) → 카드 3장 열림
|
||||
런 시작: phase=Zones zoneCount=4
|
||||
[PASS] ZoneCleared 수신 1 — 실측 1
|
||||
[PASS] 예약 상태 = Pending
|
||||
[PASS] 예약 존 = 0
|
||||
Tick(지연 전): 지연 대기
|
||||
[PASS] openDelaySeconds 전에는 열리지 않는다
|
||||
Tick(지연 후): 열림 — 런타임 패널
|
||||
[PASS] 카드 열림 — phase=Open open=True zone=0 picks=0 cards=3 sec=10 timerPaused=True worldHeld=False actorsStopped=True orig=False bound=True
|
||||
[PASS] 원본 노드가 없어 런타임 패널 경로(폴백)로 열렸다
|
||||
[PASS] 카드 3장 — 실측 3
|
||||
카드 0: id=12 "서리 요정"
|
||||
카드 1: id=17 "히키코모리 요정"
|
||||
카드 2: id=106 "치명타 확률(소)"
|
||||
[PASS] 3장 전부 실제 스킬 ID
|
||||
[PASS] 중복 없음
|
||||
[PASS] 카드 이름이 테이블 값으로 채워졌다
|
||||
[PASS] 제목 = "스킬 선택"
|
||||
[PASS] 남은 초 첫 표시 = "10초 후 자동 선택"
|
||||
[PASS] 원본 ActorInfo.All_Stop(true) 도달 = 자동전투 정지
|
||||
[PASS] 런 타이머 정지 요청 on(RunDirector.PauseTimer) — 세계 정지 없음
|
||||
[PASS] timeScale 무접촉 = 1.00(holdWorld off)
|
||||
|
||||
── ⑤ 타임아웃 → 첫 카드 자동 선택 · 닫힘 · 정지 해제
|
||||
6.8s 남음 표시 = "7초 후 자동 선택" (sec=7)
|
||||
[PASS] 올림 표기 6.8 → 7 — 실측 7
|
||||
1.5s 남음 표시 = "2초 후 자동 선택" (경고색 구간)
|
||||
[PASS] 올림 표기 1.5 → 2 — 실측 2
|
||||
[PASS] 표시 쓰기 발생 2회
|
||||
Tick(타임아웃): 선택 0 id=12 (자동)
|
||||
[PASS] 타임아웃 1회 — 실측 1
|
||||
[PASS] 선택 1회 — 실측 1
|
||||
[PASS] 닫힘 — phase=Idle open=False zone=-1 picks=1 cards=3 sec=-2147483648 timerPaused=False worldHeld=False actorsStopped=False orig=False bound=True
|
||||
[PASS] 원본 InGameInfo.Add_SelectSkill(12) 도달 = 첫 카드가 적용됐다(효과는 원본 로직 그대로)
|
||||
[PASS] 원본 All_Stop(false) 도달 = 자동전투 복귀
|
||||
[PASS] 런 타이머 정지 해제
|
||||
[PASS] 이번 런의 픽업 수 = 1
|
||||
|
||||
── ⑥ 카드 버튼 클릭 경로(자동이 아닌 사용자 선택)
|
||||
[PASS] 존 1 클리어로 다시 열림
|
||||
클릭 카드 1: id=13 "바람 요정"
|
||||
ClickCard(1): 클릭 1
|
||||
[PASS] 클릭 즉시 닫힘
|
||||
[PASS] 타임아웃 아님(클릭 선택) — 실측 0
|
||||
[PASS] 클릭한 카드의 효과가 원본 경로로 적용됐다
|
||||
|
||||
── ⑦ 마지막 존(존4) 제외 · SO 로 포함 전환
|
||||
존 수 = 4 (마지막 인덱스 3)
|
||||
[PASS] 마지막 존은 카드를 띄우지 않는다 — 마지막 존 제외
|
||||
[PASS] 건너뜀 1회 — 실측 1
|
||||
[PASS] includeFinalZone=1 이면 마지막 존도 예약된다(코드 변경 0)
|
||||
|
||||
── ⑧ 813i/813z 부활 팝업과 동시 — 부활 우선(카드는 미룬다)
|
||||
[PASS] 프리팹에서 ReviveDialog 발견 — 1개
|
||||
ReviveDialog.Initialize : initialized · 폴백 팝업 box size(1351.11, 675.56) unitsPerPx=1.7778
|
||||
ReviveDialog.ShowNowOnce: 폴백 팝업 표시 msg="존 시작점에서 부활합니다.
|
||||
무료"
|
||||
[PASS] 부활 팝업 Busy = True
|
||||
[PASS] 부활 팝업이 떠 있는 동안 카드는 열리지 않는다
|
||||
[PASS] 미룸 카운트 1
|
||||
[PASS] 픽업이 timeScale 을 건드리지 않았다
|
||||
[PASS] 부활 팝업 닫힘 — Busy=False
|
||||
[PASS] 부활이 닫힌 뒤 카드가 뜬다
|
||||
[PASS] 정리 후 닫힘 · 타이머 정지 해제
|
||||
|
||||
── ⑨ 813w RunGuide 이동 중이면 도착 뒤에 연다
|
||||
RunGuide.Active=True State=Off
|
||||
BeginZone(1) 후 RunGuide.State=Travel (존 1 유도 시작(probe-813o) → (147.0, 2.6, 30.0) [SO 폴백])
|
||||
[PASS] 이동 중에는 열리지 않는다
|
||||
[PASS] 가이드 대기 1회
|
||||
[PASS] 도착(가이드 종료) 뒤 열린다
|
||||
|
||||
── ⑩ 런 타이머 정지(813p RunDirector.PauseTimer · 세계 정지 0)
|
||||
[PASS] 시작 상태 = 정지 아님
|
||||
[PASS] PauseTimer(true) → ExternalTimerPause=True
|
||||
[PASS] timeScale 무접촉 = 1.00
|
||||
[PASS] 틱을 돌려도 정지가 유지된다(PollPause 가 외부 정지도 같은 층으로 본다)
|
||||
[PASS] PauseTimer(false) → 해제
|
||||
[PASS] 열림=정지 · 닫힘=해제 (짝 맞음 · PauseCalls=2)
|
||||
정지 전 경과 = 10.00s
|
||||
[PASS] 정지 시작 시각이 잡혔다(s_pauseStart=129.45)
|
||||
[PASS] 프로브가 정지 창을 5초로 벌렸다
|
||||
정지 중 5초 경과 후 = 10.00s (정지가 들으면 그대로 10.00)
|
||||
[PASS] 🔴 정지 중 5초가 흘러도 런 경과 불변 = 10.00s
|
||||
[PASS] 정지 누적 = 5.00s 로 집계됐다
|
||||
[PASS] 해제 뒤 5초는 정상적으로 흐른다 = 15.00s (대조군)
|
||||
|
||||
── ⑪ GC — 같은 초 100틱 = 표시 쓰기 0회
|
||||
[PASS] GC 측정용으로 열림
|
||||
같은 초 100틱: 표시 쓰기 0회 · ΔGetTotalMemory=0 B · gc0 287→287
|
||||
[PASS] 표시 쓰기 0회(값이 안 바뀌면 SetText 를 아예 안 부른다)
|
||||
[PASS] ΔGetTotalMemory = 0 B
|
||||
대조군 16 MB: Δ=16781312 B (계기 감도 확인)
|
||||
[PASS] 대조군은 잡힌다 = 「0」이 진짜 0
|
||||
|
||||
── ⑫ C8 — enabled_ = 0 이면 아무것도 하지 않는다
|
||||
[PASS] Enabled=False
|
||||
Tick: off
|
||||
[PASS] ZoneCleared 를 받아도 예약 0 — off
|
||||
[PASS] 표시 0
|
||||
[PASS] 런 타이머 정지 0
|
||||
Bind(off): off — 조립 생략
|
||||
[PASS] 813p WLRunSettings.Enabled 불변 = True
|
||||
[PASS] 복구 후 Enabled=True
|
||||
|
||||
── ⑬ 정리 — 구독 해제 · 노드 파괴 · 프리팹 저장 0
|
||||
Teardown : 정리 완료
|
||||
[PASS] ZoneCleared 구독자 0 — 실측 0
|
||||
[PASS] 타이머 정지 해제 확인
|
||||
[PASS] 프리팹 UnloadPrefabContents(저장 0) · 대역 오브젝트 파괴
|
||||
|
||||
RESULT PASS (pass=92 fail=0)
|
||||
|
|
@ -0,0 +1,524 @@
|
|||
// WL-813o 프로브 — 존 클리어 픽업 카드 실측 (에디트 모드 · Play 0 · 로그인 0).
|
||||
// unity command run_script --file AgentScripts/WL813o_Probe.cs --entry WL813o_Probe.RunAll
|
||||
// 산출물: <worktree>/AgentScripts/WL813o_PROBE.txt
|
||||
// 🔴 batchmode 는 Time.unscaledTime 이 전혀 전진하지 않는다(실측: run_script 두 번 사이 12 s 를 두어도 11.35 고정).
|
||||
// 그래서 「멈춘 초」는 813p ShiftClockForProbe + 프로브 전용 리플렉션(s_pauseStart 되감기)으로 재현해 잰다(⑩).
|
||||
// 🔴 NewGameUI.prefab 은 LoadPrefabContents 로 열고 **저장하지 않는다**(UnloadPrefabContents(false)).
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using TMPro;
|
||||
using WL.UI;
|
||||
using WL.Combat.Run;
|
||||
|
||||
public static class WL813o_Probe
|
||||
{
|
||||
const string kPrefab = "Assets/Res_Addr/MainUI/NewGameUI.prefab";
|
||||
const string kTableTxt = "Assets/ResWork/Table/table_selectskill.txt";
|
||||
|
||||
static StringBuilder _o;
|
||||
static int _pass, _fail;
|
||||
static GameObject _root;
|
||||
static GameObject _fakeInfos; // table_selectskill · InGameInfo · ActorInfo 대역
|
||||
|
||||
static void H(string s) { _o.AppendLine(); _o.AppendLine("── " + s); }
|
||||
static void N(string s) { _o.AppendLine(" " + s); }
|
||||
static bool Chk(bool ok, string what)
|
||||
{
|
||||
if (ok) { _pass++; _o.AppendLine(" [PASS] " + what); }
|
||||
else { _fail++; _o.AppendLine(" [FAIL] " + what); }
|
||||
return ok;
|
||||
}
|
||||
|
||||
static string Root { get { return Directory.GetParent(Application.dataPath).FullName; } }
|
||||
static string OutPath(string name) { return Path.Combine(Path.Combine(Root, "AgentScripts"), name); }
|
||||
|
||||
public static string RunAll()
|
||||
{
|
||||
_o = new StringBuilder();
|
||||
_pass = _fail = 0;
|
||||
_o.AppendLine("# WL813o Probe " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " (edit mode · Play 0 · 로그인 0)");
|
||||
_o.AppendLine("playMode=" + Application.isPlaying);
|
||||
|
||||
bool savedDisabled = WLZonePickupSettings.RuntimeDisabled;
|
||||
try
|
||||
{
|
||||
Step0_Asset();
|
||||
Step1_OriginalMissing();
|
||||
Step2_Fakes();
|
||||
Step3_Bind();
|
||||
Step4_OpenOnZoneCleared();
|
||||
Step5_TimeoutAutoPick();
|
||||
Step6_ClickPick();
|
||||
Step7_FinalZoneExcluded();
|
||||
Step8_RevivePriority();
|
||||
Step9_GuideWait();
|
||||
Step10_TimerPauseWiring();
|
||||
Step11_Gc();
|
||||
Step12_C8();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_fail++;
|
||||
_o.AppendLine(" [FAIL] 예외 " + e.GetType().Name + " : " + e.Message);
|
||||
_o.AppendLine(e.StackTrace);
|
||||
}
|
||||
finally
|
||||
{
|
||||
WLZonePickupSettings.RuntimeDisabled = savedDisabled;
|
||||
Step13_Cleanup();
|
||||
}
|
||||
|
||||
_o.AppendLine();
|
||||
_o.AppendLine("RESULT " + (_fail == 0 ? "PASS" : "FAIL") + " (pass=" + _pass + " fail=" + _fail + ")");
|
||||
string path = OutPath("WL813o_PROBE.txt");
|
||||
File.WriteAllText(path, _o.ToString(), new UTF8Encoding(false));
|
||||
return _o.ToString() + "\n(파일: " + path + ")";
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────── ⓪ 에셋 · 계약
|
||||
static void Step0_Asset()
|
||||
{
|
||||
H("⓪ 에셋 (WLZonePickupSettings · NEW) · 813p 계약");
|
||||
WLZonePickupSettings.ClearCache();
|
||||
var s = WLZonePickupSettings.Instance;
|
||||
Chk(s != null, "Resources.Load(\"" + WLZonePickupSettings.ResourcesPath + "\")");
|
||||
if (s == null) return;
|
||||
Chk(WLZonePickupSettings.Enabled, "Enabled = " + WLZonePickupSettings.Enabled);
|
||||
N("값: cardCount=" + s.cardCount + " timeout=" + s.pickTimeoutSeconds + "s(unscaled=" + s.timeoutUnscaled +
|
||||
") first=" + s.autoPickFirstCard + " includeFinalZone=" + s.includeFinalZone +
|
||||
" guideWait=" + s.waitForGuideArrival + "(max " + s.maxGuideDelaySeconds + "s)" +
|
||||
" deferRevive=" + s.deferWhileRevive + " pauseTimer=" + s.pauseRunTimer + " holdWorld=" + s.holdWorld +
|
||||
" stopActors=" + s.stopActorsWhileOpen + " openDelay=" + s.openDelaySeconds + "s");
|
||||
Chk(!s.includeFinalZone, "존4(마지막 존) 기본 제외 — 보스 흐름 방해 금지(발주서 §1-1)");
|
||||
Chk(!s.holdWorld, "세계 정지 기본 off = 타이머만 정지(발주서 §1-3 권장)");
|
||||
Chk(Math.Abs(s.pickTimeoutSeconds - 10f) < 0.001f, "타임아웃 10 s(발주서 §1-2) — 실측 " + s.pickTimeoutSeconds);
|
||||
Chk(s.cardCount == 3, "3장(기획안 F-3) — 실측 " + s.cardCount);
|
||||
Chk(s.edge.enabled && s.edge.outlineWidth > 0f,
|
||||
"남은 초·카드 글자 아웃라인(813y ApplyEdge) outlineWidth=" + s.edge.outlineWidth);
|
||||
|
||||
var rs = WLRunSettings.Instance;
|
||||
Chk(rs != null, "813p WLRunSettings 존재(존 수·런 길이의 주인 · UI 는 수치를 갖지 않는다)");
|
||||
if (rs != null)
|
||||
N("813p 값(읽기만): runSeconds=" + rs.runSeconds + " 존 " + rs.zoneSpawnerIds.Length + "개");
|
||||
}
|
||||
|
||||
// ─────────────── ① 🔴 원본 SelectSkillUI 노드 부재 실측(발주서 §1-1 과 다른 곳)
|
||||
static void Step1_OriginalMissing()
|
||||
{
|
||||
H("① 🔴 원본 SelectSkillUI 노드 실측 — 발주서 §1-1 전제와 다르다");
|
||||
var root = PrefabUtility.LoadPrefabContents(kPrefab);
|
||||
_root = root;
|
||||
Chk(root != null, "LoadPrefabContents(" + kPrefab + ")");
|
||||
if (root == null) return;
|
||||
|
||||
var ui = root.GetComponentsInChildren<SelectSkillUI>(true);
|
||||
var cards = root.GetComponentsInChildren<SelectSkillCard>(true);
|
||||
var gameUi = root.GetComponentsInChildren<GameUI>(true);
|
||||
var nguiLabels = root.GetComponentsInChildren<UILabel>(true);
|
||||
var nguiSprites = root.GetComponentsInChildren<UISprite>(true);
|
||||
var tmp = root.GetComponentsInChildren<TextMeshProUGUI>(true);
|
||||
N("NewGameUI.prefab 안: SelectSkillUI=" + ui.Length + " SelectSkillCard=" + cards.Length +
|
||||
" GameUI=" + gameUi.Length + " UILabel=" + nguiLabels.Length + " UISprite=" + nguiSprites.Length +
|
||||
" TextMeshProUGUI=" + tmp.Length);
|
||||
Chk(ui.Length == 0, "원본 SelectSkillUI 노드 0개 = 열 대상이 없다");
|
||||
Chk(gameUi.Length == 0 && GameUI.Ins == null,
|
||||
"GameUI 컴포넌트 0개 · GameUI.Ins=null → MyActor.cs:162 의 자동전투 정지 배선은 작동하지 않는다");
|
||||
Chk(nguiLabels.Length == 0 && nguiSprites.Length == 0,
|
||||
"실제 인게임 UI 에 NGUI 위젯 0개(원본 카드 UI 는 NGUI) · TMP 는 " + tmp.Length + "개 = uGUI 트리");
|
||||
N("→ 그래서 813o 는 ① 원본 노드가 있으면 그대로 열고, ② 없으면 같은 원본 데이터·효과 경로를 쓰는 런타임 uGUI 패널로 간다.");
|
||||
}
|
||||
|
||||
// ─────────────── ② 원본 데이터·효과 경로 대역 세우기 (table/InGameInfo/ActorInfo)
|
||||
static void Step2_Fakes()
|
||||
{
|
||||
H("② 원본 데이터·효과 경로 — table_selectskill / InGameInfo / ActorInfo");
|
||||
_fakeInfos = new GameObject("WL813o_Probe_Infos");
|
||||
_fakeInfos.hideFlags = HideFlags.HideAndDontSave;
|
||||
|
||||
var tbl = _fakeInfos.AddComponent<table_selectskill>();
|
||||
tbl.m_json = AssetDatabase.LoadAssetAtPath<TextAsset>(kTableTxt);
|
||||
Chk(tbl.m_json != null, "테이블 원본 TextAsset " + kTableTxt);
|
||||
// 에디트 모드는 Awake 를 부르지 않는다 → 원본 Awake 를 그대로 태운다(파싱 로직 무수정).
|
||||
// SendMessage 는 HideAndDontSave 오브젝트에서 'ShouldRunBehaviour()' Assert 를 남기므로 직접 호출한다.
|
||||
var awake = typeof(table_selectskill).GetMethod("Awake", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
Chk(awake != null, "원본 table_selectskill.Awake 확보");
|
||||
if (awake != null) awake.Invoke(tbl, null);
|
||||
Chk(table_selectskill.Ins != null, "table_selectskill.Ins 설정(원본 Awake 경로)");
|
||||
|
||||
var info = _fakeInfos.AddComponent<InGameInfo>();
|
||||
InGameInfo.Ins = info; // Awake 는 무거워서 부르지 않는다(Ins 만 세운다)
|
||||
Chk(InGameInfo.Ins != null, "InGameInfo.Ins 설정");
|
||||
|
||||
var actors = _fakeInfos.AddComponent<ActorInfo>();
|
||||
ActorInfo.Ins = actors;
|
||||
Chk(ActorInfo.Ins != null, "ActorInfo.Ins 설정(All_Stop 대상 0 = 무해)");
|
||||
|
||||
int all = table_selectskill.Ins != null ? table_selectskill.Ins.Get_DataList().Count : 0;
|
||||
N("테이블 행 수 = " + all);
|
||||
Chk(all > 3, "카드 3장을 뽑기에 충분한 행 수");
|
||||
|
||||
var draw = table_selectskill.Ins.Get_DataList(3);
|
||||
Chk(draw != null && draw.Count == 3, "원본 뽑기 Get_DataList(3) = 3행(셔플·보유 제외·PreSkill 규칙 그대로)");
|
||||
if (draw != null)
|
||||
for (int i = 0; i < draw.Count; i++)
|
||||
{
|
||||
var r = draw[i] as SelectSkillTableData;
|
||||
if (r != null) N(" 뽑기 예시 " + i + ": id=" + r.ID + " \"" + r.Name + "\" pet=" + r.PetSkill);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────── ③ 노드 조립 · 구독
|
||||
static void Step3_Bind()
|
||||
{
|
||||
H("③ 노드 조립 — WL_ZonePickup(런타임 생성 · HideFlags.DontSave) · 구독 3종");
|
||||
if (_root == null) { Chk(false, "프리팹 루트 없음"); return; }
|
||||
|
||||
int before = RunEvents.ZoneCleared.Count;
|
||||
N("Subscribe : " + ZonePickup.Subscribe(true));
|
||||
Chk(RunEvents.ZoneCleared.Count == before + 1, "ZoneCleared 구독 +1 — 실측 " + RunEvents.ZoneCleared.Count);
|
||||
N("Subscribe(중복): " + ZonePickup.Subscribe(true));
|
||||
Chk(RunEvents.ZoneCleared.Count == before + 1, "2회 호출해도 +1(중복 구독 0)");
|
||||
|
||||
var hud = WLVignetteUtil.FindUiPath(_root.transform, "IngameUIs/WL_HUD");
|
||||
Chk(hud != null, "IngameUIs/WL_HUD 경로 실측(813y/813q 와 같은 부모)");
|
||||
if (hud != null)
|
||||
{
|
||||
bool safe = hud.GetComponentInParent<SafeAreaFitter>(true) != null || hud.GetComponent<SafeAreaFitter>() != null;
|
||||
Chk(safe, "WL_HUD 가 SafeAreaFitter 아래 = Safe Area 안");
|
||||
}
|
||||
N("Bind : " + ZonePickup.Bind(_root.transform));
|
||||
Chk(ZonePickup.Bound, "WL_ZonePickup 노드 생성");
|
||||
N("배치 : " + ZonePickup.ApplyLayout());
|
||||
Chk(!ZonePickup.IsOpen, "조립 직후에는 닫혀 있다");
|
||||
}
|
||||
|
||||
// ─────────────── ④ 존 클리어 → 열림 · 3장
|
||||
static void Step4_OpenOnZoneCleared()
|
||||
{
|
||||
H("④ 가짜 ZoneCleared(존 0) → 카드 3장 열림");
|
||||
var s = WLZonePickupSettings.Instance;
|
||||
ZonePickup.ResetState();
|
||||
RunDirector.EnsureSubscribed();
|
||||
RunDirector.StartRun("probe-813o");
|
||||
N("런 시작: phase=" + RunDirector.Phase + " zoneCount=" + RunDirector.ZoneCount);
|
||||
|
||||
WLZonePickupProbeHooks.RaiseZoneCleared(0, 813001, 31.5f, 12, 12, false);
|
||||
Chk(ZonePickup.ZoneClearedSeen == 1, "ZoneCleared 수신 1 — 실측 " + ZonePickup.ZoneClearedSeen);
|
||||
Chk(ZonePickup.Phase == ZonePickupPhase.Pending, "예약 상태 = " + ZonePickup.Phase);
|
||||
Chk(ZonePickup.PendingZone == 0, "예약 존 = " + ZonePickup.PendingZone);
|
||||
|
||||
float t0 = ZonePickup.NowClock();
|
||||
N("Tick(지연 전): " + ZonePickup.Tick(t0));
|
||||
Chk(!ZonePickup.IsOpen, "openDelaySeconds 전에는 열리지 않는다");
|
||||
|
||||
N("Tick(지연 후): " + ZonePickup.Tick(t0 + s.openDelaySeconds + 0.01f));
|
||||
Chk(ZonePickup.IsOpen, "카드 열림 — " + ZonePickup.Dump());
|
||||
Chk(!ZonePickup.UsedOriginalUi, "원본 노드가 없어 런타임 패널 경로(폴백)로 열렸다");
|
||||
Chk(ZonePickup.CardCount == 3, "카드 3장 — 실측 " + ZonePickup.CardCount);
|
||||
for (int i = 0; i < ZonePickup.CardCount; i++)
|
||||
N(" 카드 " + i + ": id=" + ZonePickup.CardId(i) + " \"" + ZonePickup.CardNameText(i) + "\"");
|
||||
Chk(ZonePickup.CardId(0) != 0 && ZonePickup.CardId(1) != 0 && ZonePickup.CardId(2) != 0, "3장 전부 실제 스킬 ID");
|
||||
Chk(ZonePickup.CardId(0) != ZonePickup.CardId(1) && ZonePickup.CardId(1) != ZonePickup.CardId(2), "중복 없음");
|
||||
Chk(!string.IsNullOrEmpty(ZonePickup.CardNameText(0)), "카드 이름이 테이블 값으로 채워졌다");
|
||||
Chk(ZonePickup.TitleText == s.titleText, "제목 = \"" + ZonePickup.TitleText + "\"");
|
||||
Chk(ZonePickup.TimerText.Contains("10"), "남은 초 첫 표시 = \"" + ZonePickup.TimerText + "\"");
|
||||
Chk(ZonePickup.ActorsStopped, "원본 ActorInfo.All_Stop(true) 도달 = 자동전투 정지");
|
||||
Chk(RunDirector.ExternalTimerPause, "런 타이머 정지 요청 on(RunDirector.PauseTimer) — 세계 정지 없음");
|
||||
Chk(Math.Abs(Time.timeScale - 1f) < 0.0001f, "timeScale 무접촉 = " + Time.timeScale.ToString("F2") + "(holdWorld off)");
|
||||
}
|
||||
|
||||
// ─────────────── ⑤ 타임아웃 → 첫 카드 자동 선택 · 닫힘
|
||||
static void Step5_TimeoutAutoPick()
|
||||
{
|
||||
H("⑤ 타임아웃 → 첫 카드 자동 선택 · 닫힘 · 정지 해제");
|
||||
var s = WLZonePickupSettings.Instance;
|
||||
if (!ZonePickup.IsOpen) { Chk(false, "④에서 열려 있어야 한다"); return; }
|
||||
|
||||
int firstId = ZonePickup.CardId(0);
|
||||
float t0 = ZonePickup.NowClock();
|
||||
|
||||
// 카운트다운 표시 — 남은 초가 바뀔 때마다 1회씩만 쓴다
|
||||
int w0 = ZonePickup.TimerWrites;
|
||||
ZonePickup.Tick(t0 + s.openDelaySeconds + 3.2f);
|
||||
N("6.8s 남음 표시 = \"" + ZonePickup.TimerText + "\" (sec=" + ZonePickup.ShownSecond + ")");
|
||||
Chk(ZonePickup.ShownSecond == 7, "올림 표기 6.8 → 7 — 실측 " + ZonePickup.ShownSecond);
|
||||
ZonePickup.Tick(t0 + s.openDelaySeconds + 8.5f);
|
||||
N("1.5s 남음 표시 = \"" + ZonePickup.TimerText + "\" (경고색 구간)");
|
||||
Chk(ZonePickup.ShownSecond == 2, "올림 표기 1.5 → 2 — 실측 " + ZonePickup.ShownSecond);
|
||||
Chk(ZonePickup.TimerWrites > w0, "표시 쓰기 발생 " + (ZonePickup.TimerWrites - w0) + "회");
|
||||
|
||||
N("Tick(타임아웃): " + ZonePickup.Tick(t0 + s.openDelaySeconds + s.pickTimeoutSeconds + 0.01f));
|
||||
Chk(ZonePickup.TimeoutCount == 1, "타임아웃 1회 — 실측 " + ZonePickup.TimeoutCount);
|
||||
Chk(ZonePickup.PickCount == 1, "선택 1회 — 실측 " + ZonePickup.PickCount);
|
||||
Chk(!ZonePickup.IsOpen, "닫힘 — " + ZonePickup.Dump());
|
||||
var dic = InGameInfo.Ins != null ? InGameInfo.Ins.Get_SelectSkills() : null;
|
||||
Chk(dic != null && dic.ContainsKey(firstId),
|
||||
"원본 InGameInfo.Add_SelectSkill(" + firstId + ") 도달 = 첫 카드가 적용됐다(효과는 원본 로직 그대로)");
|
||||
Chk(!ZonePickup.ActorsStopped, "원본 All_Stop(false) 도달 = 자동전투 복귀");
|
||||
Chk(!RunDirector.ExternalTimerPause, "런 타이머 정지 해제");
|
||||
Chk(ZonePickup.PicksThisRun == 1, "이번 런의 픽업 수 = " + ZonePickup.PicksThisRun);
|
||||
}
|
||||
|
||||
// ─────────────── ⑥ 카드 클릭 경로
|
||||
static void Step6_ClickPick()
|
||||
{
|
||||
H("⑥ 카드 버튼 클릭 경로(자동이 아닌 사용자 선택)");
|
||||
var s = WLZonePickupSettings.Instance;
|
||||
ZonePickup.ResetState();
|
||||
WLZonePickupProbeHooks.RaiseZoneCleared(1, 813002, 62f, 14, 14, false);
|
||||
float t0 = ZonePickup.NowClock();
|
||||
ZonePickup.Tick(t0 + s.openDelaySeconds + 0.01f);
|
||||
Chk(ZonePickup.IsOpen, "존 1 클리어로 다시 열림");
|
||||
int id1 = ZonePickup.CardId(1);
|
||||
N("클릭 카드 1: id=" + id1 + " \"" + ZonePickup.CardNameText(1) + "\"");
|
||||
N("ClickCard(1): " + ZonePickup.ClickCard(1));
|
||||
Chk(!ZonePickup.IsOpen, "클릭 즉시 닫힘");
|
||||
Chk(ZonePickup.TimeoutCount == 0, "타임아웃 아님(클릭 선택) — 실측 " + ZonePickup.TimeoutCount);
|
||||
var dic = InGameInfo.Ins != null ? InGameInfo.Ins.Get_SelectSkills() : null;
|
||||
Chk(dic != null && dic.ContainsKey(id1), "클릭한 카드의 효과가 원본 경로로 적용됐다");
|
||||
}
|
||||
|
||||
// ─────────────── ⑦ 마지막 존 제외
|
||||
static void Step7_FinalZoneExcluded()
|
||||
{
|
||||
H("⑦ 마지막 존(존4) 제외 · SO 로 포함 전환");
|
||||
var s = WLZonePickupSettings.Instance;
|
||||
ZonePickup.ResetState();
|
||||
int last = RunDirector.ZoneCount - 1;
|
||||
N("존 수 = " + RunDirector.ZoneCount + " (마지막 인덱스 " + last + ")");
|
||||
|
||||
WLZonePickupProbeHooks.RaiseZoneCleared(last, 813004, 150f, 8, 8, true);
|
||||
Chk(ZonePickup.Phase == ZonePickupPhase.Idle, "마지막 존은 카드를 띄우지 않는다 — " + ZonePickup.LastSkipReason);
|
||||
Chk(ZonePickup.SkipCount == 1, "건너뜀 1회 — 실측 " + ZonePickup.SkipCount);
|
||||
|
||||
bool saved = s.includeFinalZone;
|
||||
s.includeFinalZone = true;
|
||||
try
|
||||
{
|
||||
WLZonePickupProbeHooks.RaiseZoneCleared(last, 813004, 150f, 8, 8, true);
|
||||
Chk(ZonePickup.Phase == ZonePickupPhase.Pending, "includeFinalZone=1 이면 마지막 존도 예약된다(코드 변경 0)");
|
||||
}
|
||||
finally { s.includeFinalZone = saved; }
|
||||
ZonePickup.ResetState();
|
||||
}
|
||||
|
||||
// ─────────────── ⑧ 부활 팝업 우선
|
||||
static void Step8_RevivePriority()
|
||||
{
|
||||
H("⑧ 813i/813z 부활 팝업과 동시 — 부활 우선(카드는 미룬다)");
|
||||
var s = WLZonePickupSettings.Instance;
|
||||
var dlg = _root != null ? _root.GetComponentsInChildren<ReviveDialog>(true) : new ReviveDialog[0];
|
||||
Chk(dlg != null && dlg.Length > 0, "프리팹에서 ReviveDialog 발견 — " + (dlg != null ? dlg.Length : 0) + "개");
|
||||
if (dlg == null || dlg.Length == 0) return;
|
||||
var d = dlg[0];
|
||||
N("ReviveDialog.Initialize : " + d.Initialize());
|
||||
N("ReviveDialog.ShowNowOnce: " + d.ShowNowOnce());
|
||||
Chk(ReviveDialog.Busy, "부활 팝업 Busy = " + ReviveDialog.Busy);
|
||||
|
||||
ZonePickup.ResetState();
|
||||
WLZonePickupProbeHooks.RaiseZoneCleared(0, 813001, 20f, 12, 12, false);
|
||||
float t0 = ZonePickup.NowClock();
|
||||
ZonePickup.Tick(t0 + s.openDelaySeconds + 0.01f);
|
||||
Chk(!ZonePickup.IsOpen, "부활 팝업이 떠 있는 동안 카드는 열리지 않는다");
|
||||
Chk(ZonePickup.ReviveDeferCount > 0, "미룸 카운트 " + ZonePickup.ReviveDeferCount);
|
||||
Chk(Math.Abs(Time.timeScale - 1f) < 0.0001f || ReviveDialog.Busy, "픽업이 timeScale 을 건드리지 않았다");
|
||||
|
||||
d.HideNow();
|
||||
Chk(!ReviveDialog.Busy, "부활 팝업 닫힘 — Busy=" + ReviveDialog.Busy);
|
||||
ZonePickup.Tick(t0 + s.openDelaySeconds + 0.02f);
|
||||
Chk(ZonePickup.IsOpen, "부활이 닫힌 뒤 카드가 뜬다");
|
||||
ZonePickup.Close("프로브 정리");
|
||||
Chk(!ZonePickup.IsOpen && !RunDirector.ExternalTimerPause, "정리 후 닫힘 · 타이머 정지 해제");
|
||||
}
|
||||
|
||||
// ─────────────── ⑨ 813w 가이드 이동 중 대기
|
||||
static void Step9_GuideWait()
|
||||
{
|
||||
H("⑨ 813w RunGuide 이동 중이면 도착 뒤에 연다");
|
||||
var s = WLZonePickupSettings.Instance;
|
||||
N("RunGuide.Active=" + RunGuide.Active + " State=" + RunGuide.State);
|
||||
ZonePickup.ResetState();
|
||||
|
||||
if (!RunGuide.Active)
|
||||
{
|
||||
N("가이드 축이 꺼져 있어 Travel 상태를 만들 수 없다 — 대기 분기는 「미확인」(인게임 QA).");
|
||||
Chk(true, "가이드 off 면 대기 없이 바로 연다(발주서 §1-1 의 폴백 경로)");
|
||||
return;
|
||||
}
|
||||
RunGuide.BeginZone(1, "probe-813o");
|
||||
N("BeginZone(1) 후 RunGuide.State=" + RunGuide.State + " (" + RunGuide.LastLog + ")");
|
||||
WLZonePickupProbeHooks.RaiseZoneCleared(0, 813001, 20f, 12, 12, false);
|
||||
float t0 = ZonePickup.NowClock();
|
||||
ZonePickup.Tick(t0 + s.openDelaySeconds + 0.01f);
|
||||
if (RunGuide.State == RunGuideState.Travel)
|
||||
{
|
||||
Chk(!ZonePickup.IsOpen, "이동 중에는 열리지 않는다");
|
||||
Chk(ZonePickup.GuideWaitCount > 0, "가이드 대기 " + ZonePickup.GuideWaitCount + "회");
|
||||
RunGuide.StopGuide("probe-813o");
|
||||
ZonePickup.Tick(t0 + s.openDelaySeconds + 0.02f);
|
||||
Chk(ZonePickup.IsOpen, "도착(가이드 종료) 뒤 열린다");
|
||||
}
|
||||
else
|
||||
{
|
||||
N("Travel 상태가 만들어지지 않았다(에디트 모드엔 PC·NavMesh 가 없다) — 대기 분기는 「미확인」.");
|
||||
Chk(true, "대기 상한 " + s.maxGuideDelaySeconds + "s 를 넘으면 이동 중이어도 연다(코드 경로 존재)");
|
||||
}
|
||||
ZonePickup.Close("프로브 정리");
|
||||
}
|
||||
|
||||
// ─────────────── ⑩ 런 타이머 정지 배선
|
||||
static void Step10_TimerPauseWiring()
|
||||
{
|
||||
H("⑩ 런 타이머 정지(813p RunDirector.PauseTimer · 세계 정지 0)");
|
||||
ZonePickup.ResetState();
|
||||
Chk(!RunDirector.ExternalTimerPause, "시작 상태 = 정지 아님");
|
||||
|
||||
RunDirector.PauseTimer(true);
|
||||
Chk(RunDirector.ExternalTimerPause, "PauseTimer(true) → ExternalTimerPause=True");
|
||||
Chk(Math.Abs(Time.timeScale - 1f) < 0.0001f, "timeScale 무접촉 = " + Time.timeScale.ToString("F2"));
|
||||
RunDirector.ForceTick();
|
||||
Chk(RunDirector.ExternalTimerPause, "틱을 돌려도 정지가 유지된다(PollPause 가 외부 정지도 같은 층으로 본다)");
|
||||
RunDirector.PauseTimer(false);
|
||||
Chk(!RunDirector.ExternalTimerPause, "PauseTimer(false) → 해제");
|
||||
|
||||
// 픽업이 열렸다 닫히면 반드시 짝이 맞는다(정지가 새지 않는다)
|
||||
var s = WLZonePickupSettings.Instance;
|
||||
WLZonePickupProbeHooks.RaiseZoneCleared(0, 813001, 10f, 12, 12, false);
|
||||
float t0 = ZonePickup.NowClock();
|
||||
ZonePickup.Tick(t0 + s.openDelaySeconds + 0.01f);
|
||||
bool onWhileOpen = RunDirector.ExternalTimerPause;
|
||||
ZonePickup.Tick(t0 + s.openDelaySeconds + s.pickTimeoutSeconds + 0.01f);
|
||||
Chk(onWhileOpen && !RunDirector.ExternalTimerPause, "열림=정지 · 닫힘=해제 (짝 맞음 · PauseCalls=" + ZonePickup.PauseCalls + ")");
|
||||
|
||||
// 🔴 「멈춘 초」 실측 — batchmode 는 Time.unscaledTime 이 전혀 전진하지 않는다(호출 사이에도 11.35 고정).
|
||||
// 그래서 813p 의 ShiftClockForProbe(시작 시각 되감기)에 더해, 프로브가 **정지 시작 시각도 같이 되감아**
|
||||
// 「정지 중에 N 초가 흘렀다」를 그대로 재현한다(게임 코드 수정 0 · 프로브 전용 리플렉션).
|
||||
RunDirector.ResetAll();
|
||||
RunDirector.StartRun("probe-813o-pause");
|
||||
RunDirector.ShiftClockForProbe(10f);
|
||||
float e0 = RunDirector.Elapsed;
|
||||
N("정지 전 경과 = " + e0.ToString("F2") + "s");
|
||||
RunDirector.PauseTimer(true);
|
||||
Chk(PauseStartRaw() >= 0f, "정지 시작 시각이 잡혔다(s_pauseStart=" + PauseStartRaw().ToString("F2") + ")");
|
||||
|
||||
RunDirector.ShiftClockForProbe(5f); // 5초가 흘렀다
|
||||
bool shifted = ShiftPauseStart(5f); // 그 5초는 「정지 중」이었다
|
||||
Chk(shifted, "프로브가 정지 창을 5초로 벌렸다");
|
||||
float e1 = RunDirector.Elapsed;
|
||||
N("정지 중 5초 경과 후 = " + e1.ToString("F2") + "s (정지가 들으면 그대로 " + e0.ToString("F2") + ")");
|
||||
Chk(Math.Abs(e1 - e0) < 0.01f, "🔴 정지 중 5초가 흘러도 런 경과 불변 = " + e1.ToString("F2") + "s");
|
||||
|
||||
RunDirector.PauseTimer(false);
|
||||
float total = PausedTotalRaw();
|
||||
Chk(Math.Abs(total - 5f) < 0.01f, "정지 누적 = " + total.ToString("F2") + "s 로 집계됐다");
|
||||
RunDirector.ShiftClockForProbe(5f);
|
||||
float e2 = RunDirector.Elapsed;
|
||||
Chk(Math.Abs(e2 - (e0 + 5f)) < 0.01f, "해제 뒤 5초는 정상적으로 흐른다 = " + e2.ToString("F2") + "s (대조군)");
|
||||
RunDirector.EndRun(RunOutcome.Abandon);
|
||||
RunDirector.ResetAll();
|
||||
RunDirector.StartRun("probe-813o"); // 뒤 절차용으로 런을 되살린다
|
||||
}
|
||||
|
||||
static FieldInfo Fld(string name)
|
||||
{
|
||||
return typeof(RunDirector).GetField(name, BindingFlags.NonPublic | BindingFlags.Static);
|
||||
}
|
||||
static float PauseStartRaw() { var f = Fld("s_pauseStart"); return f == null ? -2f : (float)f.GetValue(null); }
|
||||
static float PausedTotalRaw() { var f = Fld("s_pausedTotal"); return f == null ? -2f : (float)f.GetValue(null); }
|
||||
static bool ShiftPauseStart(float seconds)
|
||||
{
|
||||
var f = Fld("s_pauseStart");
|
||||
if (f == null) return false;
|
||||
float v = (float)f.GetValue(null);
|
||||
if (v < 0f) return false;
|
||||
f.SetValue(null, v - seconds);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ─────────────── ⑪ GC
|
||||
static void Step11_Gc()
|
||||
{
|
||||
H("⑪ GC — 같은 초 100틱 = 표시 쓰기 0회");
|
||||
var s = WLZonePickupSettings.Instance;
|
||||
ZonePickup.ResetState();
|
||||
WLZonePickupProbeHooks.RaiseZoneCleared(0, 813001, 10f, 12, 12, false);
|
||||
float t0 = ZonePickup.NowClock();
|
||||
ZonePickup.Tick(t0 + s.openDelaySeconds + 0.01f);
|
||||
Chk(ZonePickup.IsOpen, "GC 측정용으로 열림");
|
||||
|
||||
float t = t0 + s.openDelaySeconds + 2.51f; // 남은 7.5s → 올림 8 · 0.1s 동안 같은 초
|
||||
ZonePickup.Tick(t); // 첫 쓰기
|
||||
int w0 = ZonePickup.TimerWrites;
|
||||
long m0 = GC.GetTotalMemory(false);
|
||||
int gc0 = GC.CollectionCount(0);
|
||||
for (int i = 0; i < 100; i++) ZonePickup.Tick(t + i * 0.001f); // 같은 초
|
||||
long m1 = GC.GetTotalMemory(false);
|
||||
int gc1 = GC.CollectionCount(0);
|
||||
N("같은 초 100틱: 표시 쓰기 " + (ZonePickup.TimerWrites - w0) + "회 · ΔGetTotalMemory=" + (m1 - m0) +
|
||||
" B · gc0 " + gc0 + "→" + gc1);
|
||||
Chk(ZonePickup.TimerWrites == w0, "표시 쓰기 0회(값이 안 바뀌면 SetText 를 아예 안 부른다)");
|
||||
Chk(m1 - m0 == 0L, "ΔGetTotalMemory = 0 B");
|
||||
|
||||
// 대조군 — 계기 감도 확인(813q 실측대로 이 런타임은 16 MB 규모라야 잡힌다)
|
||||
long c0 = GC.GetTotalMemory(false);
|
||||
var junk = new byte[16 * 1024 * 1024];
|
||||
junk[0] = 1;
|
||||
long c1 = GC.GetTotalMemory(false);
|
||||
N("대조군 16 MB: Δ=" + (c1 - c0) + " B (계기 감도 확인)");
|
||||
Chk(c1 - c0 > 0L, "대조군은 잡힌다 = 「0」이 진짜 0");
|
||||
|
||||
ZonePickup.Close("GC 측정 종료");
|
||||
}
|
||||
|
||||
// ─────────────── ⑫ C8 롤백
|
||||
static void Step12_C8()
|
||||
{
|
||||
H("⑫ C8 — enabled_ = 0 이면 아무것도 하지 않는다");
|
||||
ZonePickup.ResetState();
|
||||
WLZonePickupSettings.RuntimeDisabled = true;
|
||||
Chk(!WLZonePickupSettings.Enabled, "Enabled=False");
|
||||
N("Tick: " + ZonePickup.Tick(ZonePickup.NowClock()));
|
||||
WLZonePickupProbeHooks.RaiseZoneCleared(0, 813001, 10f, 12, 12, false);
|
||||
Chk(ZonePickup.Phase == ZonePickupPhase.Idle, "ZoneCleared 를 받아도 예약 0 — " + ZonePickup.LastSkipReason);
|
||||
Chk(!ZonePickup.IsOpen, "표시 0");
|
||||
Chk(!RunDirector.ExternalTimerPause, "런 타이머 정지 0");
|
||||
N("Bind(off): " + ZonePickup.Bind(_root != null ? _root.transform : null));
|
||||
|
||||
// 813p 축은 그대로 살아 있어야 한다
|
||||
Chk(WLRunSettings.Enabled, "813p WLRunSettings.Enabled 불변 = " + WLRunSettings.Enabled);
|
||||
WLZonePickupSettings.RuntimeDisabled = false;
|
||||
Chk(WLZonePickupSettings.Enabled, "복구 후 Enabled=True");
|
||||
}
|
||||
|
||||
// ─────────────── ⑬ 정리
|
||||
static void Step13_Cleanup()
|
||||
{
|
||||
H("⑬ 정리 — 구독 해제 · 노드 파괴 · 프리팹 저장 0");
|
||||
try
|
||||
{
|
||||
N("Teardown : " + ZonePickup.Teardown());
|
||||
Chk(RunEvents.ZoneCleared.Count == 0, "ZoneCleared 구독자 0 — 실측 " + RunEvents.ZoneCleared.Count);
|
||||
RunDirector.EndRun(RunOutcome.Abandon);
|
||||
RunDirector.ResetAll();
|
||||
RunDirector.Unsubscribe();
|
||||
RunDirector.PauseTimer(false);
|
||||
Chk(!RunDirector.ExternalTimerPause, "타이머 정지 해제 확인");
|
||||
|
||||
table_selectskill.Ins = null;
|
||||
InGameInfo.Ins = null;
|
||||
ActorInfo.Ins = null;
|
||||
if (_fakeInfos != null) UnityEngine.Object.DestroyImmediate(_fakeInfos);
|
||||
if (_root != null) PrefabUtility.UnloadPrefabContents(_root); // 저장 0
|
||||
_root = null; _fakeInfos = null;
|
||||
Chk(true, "프리팹 UnloadPrefabContents(저장 0) · 대역 오브젝트 파괴");
|
||||
}
|
||||
catch (Exception e) { _fail++; _o.AppendLine(" [FAIL] 정리 예외 " + e.Message); }
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -140,6 +140,15 @@ namespace WL.Combat.Run
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>외부 타이머 정지(813o 픽업 카드) — 세계 정지 없이 런 타이머만 멈춘다.</summary>
|
||||
public static bool ExternalTimerPause { get { return s_extPause; } }
|
||||
static bool s_extPause;
|
||||
public static void PauseTimer(bool pause)
|
||||
{
|
||||
if (s_extPause == pause) return;
|
||||
s_extPause = pause; var c = Cfg; if (c != null) PollPause(c, Time.unscaledTime);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// 구독
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
|
@ -548,8 +557,7 @@ namespace WL.Combat.Run
|
|||
|
||||
static void PollPause(WLRunSettings c, float now)
|
||||
{
|
||||
if (!c.pauseTimerWhileWorldHeld) { if (s_pauseStart >= 0f) { s_pausedTotal += now - s_pauseStart; s_pauseStart = -1f; } return; }
|
||||
bool held = DeathFlow.WorldHeld;
|
||||
bool held = (c.pauseTimerWhileWorldHeld && DeathFlow.WorldHeld) || s_extPause; // 813o: 외부 정지도 같은 층
|
||||
if (held && s_pauseStart < 0f) s_pauseStart = now;
|
||||
else if (!held && s_pauseStart >= 0f) { s_pausedTotal += now - s_pauseStart; s_pauseStart = -1f; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,809 @@
|
|||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// ZonePickup.cs — 존 클리어 픽업 카드(로그라이크 3장 중 1장) (WL-813o · #813)
|
||||
//
|
||||
// 기준서 §D-2 813o 행 + 기획안 F-3 + 발주서 WL-813o §1(Lead 설계 결정).
|
||||
// 트리거 계약 = 813p 완료보고 §4 `RunEvents.ZoneCleared`(`Assets/WL/Combat/Run/RunEvents.cs` · 읽기만)
|
||||
// → 원본 `MobControlMgr` 전멸 훅 **0**.
|
||||
//
|
||||
// ■ 🔴 실측(2026-09-09 · 발주서 §1-1 과 다른 곳) — **원본 `SelectSkillUI` 노드는 존재하지 않는다.**
|
||||
// · `SelectSkillUI.cs` 스크립트 GUID `92b16098987b86b4d850255ef6a56f64` 는 `Assets` 전수 검색에서
|
||||
// **자기 `.meta` 한 곳에서만** 나온다(어떤 `.prefab`/`.unity` 에도 붙어 있지 않다).
|
||||
// · 상위 `GameUI`(GUID `abec0a6a…`)도 씬 27개·`Res_Addr`·`ResWork`·`Resources` 어디에도 없다
|
||||
// → `GameUI.Ins` 는 항상 null → `MyActor.cs:162` 의 자동전투 정지 배선은 **작동한 적이 없다**.
|
||||
// · 원본 카드 UI 는 **NGUI**(`UILabel`/`UISprite`)인데 실제 인게임 UI(`Res_Addr/MainUI/NewGameUI.prefab`)에는
|
||||
// NGUI 컴포넌트가 **0개**다(UIPanel/UIRoot/UICamera/UILabel/UISprite 전부 0) → 붙여도 렌더링되지 않는다.
|
||||
// 그래서 **가장 가까운 방식**으로 간다 —
|
||||
// ① 원본 노드가 있으면 그대로 연다(`SelectSkillUI.Set` / `SelectSkillCard.OnClick_SelectSkill`
|
||||
// 전부 이미 public · `Assets/Script/**` 수정 **0줄**). 없으면 ②.
|
||||
// ② 카드 데이터·효과·자동전투 정지를 **전부 원본 경로 그대로** 쓰고 표시만 런타임 uGUI 로 만든다:
|
||||
// 뽑기 = `table_selectskill.Ins.Get_DataList(n)`(원본 셔플·중복 제외·PreSkill 규칙 그대로)
|
||||
// 효과 = `InGameInfo.Ins.Add_SelectSkill(id)` (원본 `SelectSkillUI.SkillSelect` 59행과 같은 줄)
|
||||
// 정지 = `ActorInfo.Ins.All_Stop(true/false)` (원본 `SelectSkillUI` 29·61행과 같은 줄)
|
||||
//
|
||||
// ■ 순서 — ① 813w `RunGuide` 가 다음 존으로 이동 중(Travel)이면 **도착 뒤에** 연다(최대 `maxGuideDelaySeconds`).
|
||||
// ② 813i/813z 부활 팝업이 떠 있으면 **부활이 우선**이고 카드는 미룬다(`deferWhileRevive`).
|
||||
//
|
||||
// ■ 런 타이머 — 카드가 열린 동안 813p `RunDirector.PauseTimer(true)`(세계 정지 없음 · 전투는 계속).
|
||||
// `holdWorld` 를 켜면 813z `DeathFlow.HoldWorld` 와 같은 `timeScale=0` 방식으로도 갈 수 있다(기본 off).
|
||||
//
|
||||
// ■ GC 0 — 남은 초는 **표시값이 바뀐 초에만** `SetText(format, float)`(TMP 무할당 오버로드)로 쓴다.
|
||||
// 카드 문자열은 열 때 1회만 쓴다. 로그 문자열은 **만들기 전에** `verboseLog` 로 막는다(811b FIX-3 규약).
|
||||
//
|
||||
// ■ 프리팹 diff 0 — 노드는 전부 런타임 생성(`HideFlags.DontSave`) · 1회 만들고 재사용.
|
||||
// ■ C8 롤백 — `WLZonePickupSettings.enabled_ = 0` → 구독 0 · 노드 0 · 표시 0 · 타이머 정지 0.
|
||||
// 🔴 어셈블리 주의: Assets/WL/UI/ 에 .asmdef 를 만들지 말 것(Assembly-CSharp 단일).
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using WL.Combat.Run;
|
||||
using WL.Combat.Survival;
|
||||
|
||||
namespace WL.UI
|
||||
{
|
||||
/// <summary>픽업 카드의 진행 단계.</summary>
|
||||
public enum ZonePickupPhase
|
||||
{
|
||||
/// <summary>대기 — 예약 없음.</summary>
|
||||
Idle = 0,
|
||||
/// <summary>존 클리어를 받아 열기를 예약했다(지연·가이드·부활 대기).</summary>
|
||||
Pending = 1,
|
||||
/// <summary>카드가 열려 있다.</summary>
|
||||
Open = 2,
|
||||
}
|
||||
|
||||
/// <summary>존 클리어 픽업 카드(정적 · 노드는 런타임 생성 · 프리팹 diff 0).</summary>
|
||||
public static class ZonePickup
|
||||
{
|
||||
// ── 노드 ──────────────────────────────────────────────────────────────
|
||||
private static Transform s_uiRoot;
|
||||
private static RectTransform s_root;
|
||||
private static CanvasGroup s_group;
|
||||
private static Image s_dim;
|
||||
private static RectTransform s_box;
|
||||
private static Image s_boxBg;
|
||||
private static TextMeshProUGUI s_title;
|
||||
private static TextMeshProUGUI s_timer;
|
||||
private static RectTransform s_cardRoot;
|
||||
private static RectTransform[] s_card = new RectTransform[0];
|
||||
private static Image[] s_cardBg = new Image[0];
|
||||
private static TextMeshProUGUI[] s_cardName = new TextMeshProUGUI[0];
|
||||
private static TextMeshProUGUI[] s_cardDesc = new TextMeshProUGUI[0];
|
||||
private static Button[] s_cardButton = new Button[0];
|
||||
private static TMP_FontAsset s_font;
|
||||
|
||||
// ── 상태 ──────────────────────────────────────────────────────────────
|
||||
private static bool s_subscribed;
|
||||
private static ZonePickupPhase s_phase;
|
||||
private static float s_pendingSince = -1f; // 예약 시각(unscaled)
|
||||
private static float s_openAt = -1f; // 열린 시각
|
||||
private static int s_pendingZone = -1;
|
||||
private static int s_picksThisRun;
|
||||
private static bool s_timerPaused, s_worldHeld, s_actorsStopped;
|
||||
private static float s_prevTimeScale = 1f;
|
||||
private static int s_shownSec = int.MinValue;
|
||||
private static bool s_shownWarn;
|
||||
private static bool s_usedOriginalUi;
|
||||
|
||||
/// <summary>이번에 뽑힌 카드의 스킬 ID(0 = 빈 칸). 폴백 패널 경로에서만 채운다.</summary>
|
||||
private static readonly List<int> s_ids = new List<int>(8);
|
||||
private static readonly List<object> s_rows = new List<object>(8);
|
||||
|
||||
// ── 진단 (프로브가 읽는다) ────────────────────────────────────────────
|
||||
public static int ZoneClearedSeen, OpenCount, PickCount, TimeoutCount, SkipCount,
|
||||
GuideWaitCount, ReviveDeferCount, GiveUpCount, TimerWrites, PauseCalls;
|
||||
public static string LastLog = "";
|
||||
public static string LastSkipReason = "";
|
||||
|
||||
public static bool Subscribed { get { return s_subscribed; } }
|
||||
public static bool Bound { get { return s_root != null; } }
|
||||
public static bool IsOpen { get { return s_phase == ZonePickupPhase.Open; } }
|
||||
public static ZonePickupPhase Phase { get { return s_phase; } }
|
||||
public static int PendingZone { get { return s_pendingZone; } }
|
||||
public static int PicksThisRun { get { return s_picksThisRun; } }
|
||||
public static bool TimerPaused { get { return s_timerPaused; } }
|
||||
public static bool WorldHeldByPickup { get { return s_worldHeld; } }
|
||||
public static bool ActorsStopped { get { return s_actorsStopped; } }
|
||||
public static bool UsedOriginalUi { get { return s_usedOriginalUi; } }
|
||||
public static int CardCount { get { return s_ids.Count; } }
|
||||
public static int CardId(int i) { return i >= 0 && i < s_ids.Count ? s_ids[i] : 0; }
|
||||
public static string TitleText { get { return s_title != null ? s_title.text : ""; } }
|
||||
public static string TimerText { get { return s_timer != null ? s_timer.text : ""; } }
|
||||
public static int ShownSecond { get { return s_shownSec; } }
|
||||
public static string CardNameText(int i)
|
||||
{
|
||||
return i >= 0 && i < s_cardName.Length && s_cardName[i] != null ? s_cardName[i].text : "";
|
||||
}
|
||||
|
||||
private static WLZonePickupSettings St { get { return WLZonePickupSettings.Instance; } }
|
||||
|
||||
// ── 부팅 (노드 0 · 프리팹 diff 0) ─────────────────────────────────────
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
||||
private static void Boot()
|
||||
{
|
||||
if (!WLZonePickupSettings.Enabled) return;
|
||||
Subscribe(true);
|
||||
WLZonePickupRunner.Ensure();
|
||||
}
|
||||
|
||||
// ── 구독 (813p RunEvents 3종) ─────────────────────────────────────────
|
||||
public static string Subscribe(bool on)
|
||||
{
|
||||
if (on == s_subscribed) return "구독 변화 없음(" + s_subscribed + ")";
|
||||
if (on)
|
||||
{
|
||||
RunEvents.ZoneCleared.Add(OnZoneCleared);
|
||||
RunEvents.RunStarted.Add(OnRunStarted);
|
||||
RunEvents.RunEnded.Add(OnRunEnded);
|
||||
}
|
||||
else
|
||||
{
|
||||
RunEvents.ZoneCleared.Remove(OnZoneCleared);
|
||||
RunEvents.RunStarted.Remove(OnRunStarted);
|
||||
RunEvents.RunEnded.Remove(OnRunEnded);
|
||||
}
|
||||
s_subscribed = on;
|
||||
return "구독=" + on + " (ZoneCleared/RunStarted/RunEnded)";
|
||||
}
|
||||
|
||||
// ── 이벤트 핸들러 (in 파라미터 = 복사 0 · 813p 계약) ──────────────────
|
||||
private static void OnRunStarted(in RunStartedEvent e)
|
||||
{
|
||||
s_picksThisRun = 0;
|
||||
if (s_phase != ZonePickupPhase.Idle) Close("런 시작");
|
||||
}
|
||||
|
||||
private static void OnRunEnded(in RunResult r)
|
||||
{
|
||||
if (s_phase != ZonePickupPhase.Idle) Close("런 종료");
|
||||
s_pendingZone = -1;
|
||||
}
|
||||
|
||||
private static void OnZoneCleared(in ZoneClearedEvent e)
|
||||
{
|
||||
ZoneClearedSeen++;
|
||||
var s = St;
|
||||
if (s == null || !WLZonePickupSettings.Enabled) { Skip("off"); return; }
|
||||
if (e.zoneIndex < s.minZoneIndex) { Skip("존 인덱스 하한 미만"); return; }
|
||||
|
||||
// 마지막 존 제외(기본) — allZonesCleared 이거나 인덱스가 마지막이면 보스 흐름을 방해하지 않는다.
|
||||
if (!s.includeFinalZone)
|
||||
{
|
||||
int last = RunDirector.ZoneCount - 1;
|
||||
if (e.allZonesCleared || (last >= 0 && e.zoneIndex >= last)) { Skip("마지막 존 제외"); return; }
|
||||
}
|
||||
if (s.maxPicksPerRun > 0 && s_picksThisRun >= s.maxPicksPerRun) { Skip("런당 상한 도달"); return; }
|
||||
if (s_phase != ZonePickupPhase.Idle) { Skip("이미 진행 중"); return; }
|
||||
|
||||
s_phase = ZonePickupPhase.Pending;
|
||||
s_pendingZone = e.zoneIndex;
|
||||
s_pendingSince = NowClock();
|
||||
if (s.verboseLog) Debug.Log("[ZonePickup] 존 클리어 #" + e.zoneIndex + " — 카드 예약");
|
||||
}
|
||||
|
||||
private static void Skip(string reason)
|
||||
{
|
||||
SkipCount++;
|
||||
LastSkipReason = reason;
|
||||
}
|
||||
|
||||
// ── 매 프레임 ─────────────────────────────────────────────────────────
|
||||
public static string Tick(float now)
|
||||
{
|
||||
var s = St;
|
||||
if (s == null || !WLZonePickupSettings.Enabled)
|
||||
{
|
||||
if (s_phase != ZonePickupPhase.Idle) Close("비활성");
|
||||
return "off";
|
||||
}
|
||||
|
||||
if (s_phase == ZonePickupPhase.Pending) return TickPending(s, now);
|
||||
if (s_phase == ZonePickupPhase.Open) return TickOpen(s, now);
|
||||
return "대기";
|
||||
}
|
||||
|
||||
private static string TickPending(WLZonePickupSettings s, float now)
|
||||
{
|
||||
float waited = now - s_pendingSince;
|
||||
if (waited < s.openDelaySeconds) return "지연 대기";
|
||||
|
||||
if (waited > s.maxDeferSeconds)
|
||||
{
|
||||
GiveUpCount++;
|
||||
s_phase = ZonePickupPhase.Idle;
|
||||
s_pendingZone = -1;
|
||||
if (s.verboseLog) Debug.Log("[ZonePickup] 대기 상한 초과 — 이번 픽업 취소");
|
||||
return "포기(대기 상한)";
|
||||
}
|
||||
|
||||
if (s.deferWhileRevive && ReviveBusy()) { ReviveDeferCount++; return "부활 우선 — 미룸"; }
|
||||
|
||||
if (s.waitForGuideArrival && waited < s.maxGuideDelaySeconds && GuideTravelling())
|
||||
{
|
||||
GuideWaitCount++;
|
||||
return "가이드 이동 중 — 도착 대기";
|
||||
}
|
||||
|
||||
return Open(s, now);
|
||||
}
|
||||
|
||||
private static string TickOpen(WLZonePickupSettings s, float now)
|
||||
{
|
||||
// 원본 노드 경로에서 원본이 스스로 닫았으면(클릭·원본 타임아웃) 정리한다.
|
||||
if (s_usedOriginalUi && !OriginalUiActive()) { Close("원본이 닫음"); return "원본이 닫음"; }
|
||||
|
||||
float left = s.pickTimeoutSeconds - (now - s_openAt);
|
||||
WriteTimer(s, left);
|
||||
if (left > 0f) return "열림";
|
||||
|
||||
TimeoutCount++;
|
||||
if (s.verboseLog) Debug.Log("[ZonePickup] 타임아웃 — 자동 선택");
|
||||
return Pick(PickIndexOnTimeout(s), true);
|
||||
}
|
||||
|
||||
private static int PickIndexOnTimeout(WLZonePickupSettings s)
|
||||
{
|
||||
if (s.autoPickFirstCard) return 0;
|
||||
int n = s_usedOriginalUi ? OriginalCardCount() : s_ids.Count;
|
||||
return n > 1 ? Random.Range(0, n) : 0;
|
||||
}
|
||||
|
||||
// ── 열기 ──────────────────────────────────────────────────────────────
|
||||
private static string Open(WLZonePickupSettings s, float now)
|
||||
{
|
||||
s_usedOriginalUi = false;
|
||||
string how;
|
||||
|
||||
if (s.preferOriginalUi && TryOpenOriginal(s)) { s_usedOriginalUi = true; how = "원본 SelectSkillUI"; }
|
||||
else if (OpenFallback(s)) how = "런타임 패널";
|
||||
else
|
||||
{
|
||||
GiveUpCount++;
|
||||
s_phase = ZonePickupPhase.Idle;
|
||||
s_pendingZone = -1;
|
||||
if (s.verboseLog) Debug.Log("[ZonePickup] 카드를 만들 수 없어 이번 픽업을 건너뛴다");
|
||||
return "열기 실패";
|
||||
}
|
||||
|
||||
s_phase = ZonePickupPhase.Open;
|
||||
s_openAt = now;
|
||||
s_shownSec = int.MinValue;
|
||||
OpenCount++;
|
||||
PauseTimer(s, true);
|
||||
HoldWorld(s, true);
|
||||
StopActors(s, true);
|
||||
WriteTimer(s, s.pickTimeoutSeconds);
|
||||
if (s.verboseLog) Debug.Log("[ZonePickup] 카드 열림(" + how + ") — 존 " + s_pendingZone);
|
||||
LastLog = how;
|
||||
return "열림 — " + how;
|
||||
}
|
||||
|
||||
/// <summary>원본 `SelectSkillUI` 노드가 살아 있으면 그대로 연다(원본 API 만 · 수정 0줄).</summary>
|
||||
private static bool TryOpenOriginal(WLZonePickupSettings s)
|
||||
{
|
||||
var ui = OriginalUi();
|
||||
if (ui == null) return false;
|
||||
try
|
||||
{
|
||||
ui.Set(null); // 원본 열기 API — 카드 뽑기·All_Stop(true)·자기 타이머까지 원본 그대로
|
||||
return ui.gameObject.activeInHierarchy;
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
if (s.verboseLog) Debug.Log("[ZonePickup] 원본 SelectSkillUI.Set 예외 — " + ex.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static SelectSkillUI OriginalUi()
|
||||
{
|
||||
var g = GameUI.Ins;
|
||||
if (g == null) return null;
|
||||
var bundle = g.IngameUI;
|
||||
if (bundle == null) return null;
|
||||
var ui = bundle.m_SelectSkillUI;
|
||||
return ui != null ? ui : null;
|
||||
}
|
||||
|
||||
private static bool OriginalUiActive()
|
||||
{
|
||||
var ui = OriginalUi();
|
||||
return ui != null && ui.gameObject.activeInHierarchy;
|
||||
}
|
||||
|
||||
private static int OriginalCardCount()
|
||||
{
|
||||
var ui = OriginalUi();
|
||||
return ui != null && ui.arr_SelectSkillCard != null ? ui.arr_SelectSkillCard.Length : 0;
|
||||
}
|
||||
|
||||
/// <summary>폴백 = 원본 데이터·효과 경로 + 런타임 uGUI 표시.</summary>
|
||||
private static bool OpenFallback(WLZonePickupSettings s)
|
||||
{
|
||||
if (s_root == null) return false;
|
||||
if (!DrawCards(s)) return false;
|
||||
EnsureCards(s);
|
||||
ApplyLayout();
|
||||
FillCards(s);
|
||||
if (s_group != null) { s_group.alpha = 1f; s_group.blocksRaycasts = true; s_group.interactable = true; }
|
||||
s_root.gameObject.SetActive(true);
|
||||
s_root.SetAsLastSibling();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>원본 뽑기 그대로 — `table_selectskill.Get_DataList(n)`(셔플·보유 제외·PreSkill 규칙).</summary>
|
||||
private static bool DrawCards(WLZonePickupSettings s)
|
||||
{
|
||||
s_ids.Clear();
|
||||
s_rows.Clear();
|
||||
var t = table_selectskill.Ins;
|
||||
if (t == null || InGameInfo.Ins == null) return false;
|
||||
int want = Mathf.Max(1, s.cardCount);
|
||||
try
|
||||
{
|
||||
var list = t.Get_DataList(want);
|
||||
if (list == null || list.Count < want) return false;
|
||||
for (int i = 0; i < list.Count; i++)
|
||||
{
|
||||
var row = list[i] as SelectSkillTableData;
|
||||
if (row == null) return false;
|
||||
s_ids.Add(row.ID);
|
||||
s_rows.Add(row);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
if (s.verboseLog) Debug.Log("[ZonePickup] 카드 뽑기 실패 — " + ex.Message);
|
||||
s_ids.Clear();
|
||||
s_rows.Clear();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 선택 · 닫기 ───────────────────────────────────────────────────────
|
||||
/// <summary>카드 i 를 고른다. 효과 적용은 원본 경로 그대로.</summary>
|
||||
public static string Pick(int index, bool auto)
|
||||
{
|
||||
var s = St;
|
||||
if (s_phase != ZonePickupPhase.Open) return "열려 있지 않음";
|
||||
|
||||
PickCount++;
|
||||
s_picksThisRun++;
|
||||
|
||||
if (s_usedOriginalUi)
|
||||
{
|
||||
var ui = OriginalUi();
|
||||
if (ui != null && ui.arr_SelectSkillCard != null && ui.arr_SelectSkillCard.Length > 0)
|
||||
{
|
||||
int i = Mathf.Clamp(index, 0, ui.arr_SelectSkillCard.Length - 1);
|
||||
var card = ui.arr_SelectSkillCard[i];
|
||||
// 원본 클릭 경로와 완전히 같은 줄(SelectSkillCard.OnClick_SelectSkill → SkillSelect).
|
||||
if (card != null) { try { card.OnClick_SelectSkill(); } catch (System.Exception) { } }
|
||||
}
|
||||
Close(auto ? "자동 선택(원본)" : "선택(원본)");
|
||||
return "선택 " + index + (auto ? " (자동)" : "");
|
||||
}
|
||||
|
||||
int id = index >= 0 && index < s_ids.Count ? s_ids[index] : 0;
|
||||
if (id != 0)
|
||||
{
|
||||
var info = InGameInfo.Ins;
|
||||
// 원본 SelectSkillUI.SkillSelect 59행과 같은 줄 = 효과 적용은 원본 로직 그대로.
|
||||
if (info != null) { try { info.Add_SelectSkill(id); } catch (System.Exception ex) { if (s != null && s.verboseLog) Debug.Log("[ZonePickup] Add_SelectSkill 예외 — " + ex.Message); } }
|
||||
}
|
||||
if (s != null && s.verboseLog) Debug.Log("[ZonePickup] 카드 선택 " + index + " id=" + id + (auto ? " (자동)" : ""));
|
||||
Close(auto ? "자동 선택" : "선택");
|
||||
return "선택 " + index + " id=" + id + (auto ? " (자동)" : "");
|
||||
}
|
||||
|
||||
private static void OnCard0() { Pick(0, false); }
|
||||
private static void OnCard1() { Pick(1, false); }
|
||||
private static void OnCard2() { Pick(2, false); }
|
||||
private static void OnCard3() { Pick(3, false); }
|
||||
private static void OnCard4() { Pick(4, false); }
|
||||
private static readonly UnityEngine.Events.UnityAction[] s_cardListeners =
|
||||
{
|
||||
OnCard0, OnCard1, OnCard2, OnCard3, OnCard4,
|
||||
};
|
||||
|
||||
/// <summary>닫기 — 정지 3종을 전부 되돌린다(원본 All_Stop(false) 포함).</summary>
|
||||
public static string Close(string reason)
|
||||
{
|
||||
var s = St;
|
||||
bool wasOpen = s_phase == ZonePickupPhase.Open;
|
||||
s_phase = ZonePickupPhase.Idle;
|
||||
s_pendingZone = -1;
|
||||
s_shownSec = int.MinValue;
|
||||
|
||||
Hide();
|
||||
StopActors(s, false);
|
||||
HoldWorld(s, false);
|
||||
PauseTimer(s, false);
|
||||
s_usedOriginalUi = false;
|
||||
LastLog = reason;
|
||||
return (wasOpen ? "닫음 — " : "정리 — ") + reason;
|
||||
}
|
||||
|
||||
private static void Hide()
|
||||
{
|
||||
if (s_group != null) { s_group.alpha = 0f; s_group.blocksRaycasts = false; s_group.interactable = false; }
|
||||
if (s_root != null) s_root.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
// ── 정지 3종 ──────────────────────────────────────────────────────────
|
||||
private static void PauseTimer(WLZonePickupSettings s, bool on)
|
||||
{
|
||||
bool want = on && s != null && s.pauseRunTimer;
|
||||
if (want == s_timerPaused) return;
|
||||
s_timerPaused = want;
|
||||
PauseCalls++;
|
||||
RunDirector.PauseTimer(want); // 813p ≤10줄 추가분 — 세계 정지 없이 런 타이머만
|
||||
}
|
||||
|
||||
private static void HoldWorld(WLZonePickupSettings s, bool on)
|
||||
{
|
||||
bool want = on && s != null && s.holdWorld;
|
||||
if (want == s_worldHeld) return;
|
||||
if (want)
|
||||
{
|
||||
// 813z 실측: 세계 정지 층은 TimeScaleArbiter 가 아니라 timeScale 직접 대입이다.
|
||||
// 부활(813i/813z)이 이미 세계를 잡고 있으면 우리는 손대지 않는다(부활 우선).
|
||||
if (DeathFlow.WorldHeld) return;
|
||||
s_prevTimeScale = Time.timeScale;
|
||||
Time.timeScale = s.holdWorldTimeScale;
|
||||
s_worldHeld = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
s_worldHeld = false;
|
||||
if (!DeathFlow.WorldHeld) Time.timeScale = s_prevTimeScale > 0f ? s_prevTimeScale : 1f;
|
||||
}
|
||||
}
|
||||
|
||||
private static void StopActors(WLZonePickupSettings s, bool on)
|
||||
{
|
||||
bool want = on && s != null && s.stopActorsWhileOpen;
|
||||
if (want == s_actorsStopped) return;
|
||||
var info = ActorInfo.Ins;
|
||||
if (info == null) { s_actorsStopped = false; return; }
|
||||
// 원본 SelectSkillUI 29·61행과 같은 줄.
|
||||
try { info.All_Stop(want); s_actorsStopped = want; }
|
||||
catch (System.Exception) { s_actorsStopped = false; }
|
||||
}
|
||||
|
||||
// ── 대기 조건 ─────────────────────────────────────────────────────────
|
||||
private static bool ReviveBusy()
|
||||
{
|
||||
return ReviveDialog.Busy || DeathFlow.WorldHeld;
|
||||
}
|
||||
|
||||
private static bool GuideTravelling()
|
||||
{
|
||||
return RunGuide.Active && RunGuide.State == RunGuideState.Travel;
|
||||
}
|
||||
|
||||
/// <summary>이 축이 쓰는 시계 — `timeoutUnscaled` 가 꺼져 있을 때만 scaled. 러너·핸들러가 같은 값을 쓴다.</summary>
|
||||
public static float NowClock()
|
||||
{
|
||||
var s = St;
|
||||
return s != null && !s.timeoutUnscaled ? Time.time : Time.unscaledTime;
|
||||
}
|
||||
|
||||
// ── 표시 ──────────────────────────────────────────────────────────────
|
||||
/// <summary>남은 초 — 표시값이 바뀐 초에만 쓴다(같은 초를 100번 틱해도 SetText 0회 = GC 0).</summary>
|
||||
private static void WriteTimer(WLZonePickupSettings s, float remaining)
|
||||
{
|
||||
if (s_timer == null || s == null) return;
|
||||
int t = remaining <= 0f ? 0 : Mathf.CeilToInt(remaining);
|
||||
bool warn = s.timerWarnSeconds > 0f && remaining <= s.timerWarnSeconds;
|
||||
if (t == s_shownSec && warn == s_shownWarn) return;
|
||||
s_shownSec = t;
|
||||
if (warn != s_shownWarn) { s_shownWarn = warn; s_timer.color = warn ? s.timerWarnColor : s.timerColor; }
|
||||
s_timer.SetText(s.timerFormat, t); // TMP 무할당 오버로드({0} = float)
|
||||
TimerWrites++;
|
||||
}
|
||||
|
||||
private static void FillCards(WLZonePickupSettings s)
|
||||
{
|
||||
for (int i = 0; i < s_card.Length; i++)
|
||||
{
|
||||
bool used = i < s_rows.Count;
|
||||
if (s_card[i] != null) s_card[i].gameObject.SetActive(used);
|
||||
if (!used) continue;
|
||||
var row = s_rows[i] as SelectSkillTableData;
|
||||
if (row == null) continue;
|
||||
if (s_cardName[i] != null) s_cardName[i].SetText(row.Name);
|
||||
if (s_cardDesc[i] != null) s_cardDesc[i].SetText(row.Desc);
|
||||
if (s_cardBg[i] != null) s_cardBg[i].color = row.PetSkill ? s.cardPetBgColor : s.cardBgColor;
|
||||
}
|
||||
if (s_title != null) s_title.SetText(s.titleText);
|
||||
}
|
||||
|
||||
// ── 조립 ──────────────────────────────────────────────────────────────
|
||||
public static string Bind(Transform uiRoot)
|
||||
{
|
||||
var s = St;
|
||||
if (s == null) return "WLZonePickupSettings 에셋 없음";
|
||||
if (!WLZonePickupSettings.Enabled) return "off — 조립 생략";
|
||||
if (uiRoot == null) return "uiRoot=null";
|
||||
if (s_root != null && s_uiRoot == uiRoot) return "이미 연결됨 — " + s_root.name;
|
||||
|
||||
s_uiRoot = uiRoot;
|
||||
var prt = WLVignetteUtil.FindUiPath(uiRoot, s.parentPath) as RectTransform;
|
||||
if (prt == null) return "부모 없음 — 경로 \"" + s.parentPath + "\"";
|
||||
if (s_font == null) s_font = RunHud.BorrowFont(uiRoot);
|
||||
|
||||
if (s_root == null || s_root.parent != prt)
|
||||
{
|
||||
var go = new GameObject("WL_ZonePickup", typeof(RectTransform));
|
||||
go.layer = WLVignetteUtil.UILayer;
|
||||
go.hideFlags = HideFlags.DontSave;
|
||||
s_root = (RectTransform)go.transform;
|
||||
s_root.SetParent(prt, false);
|
||||
s_group = go.AddComponent<CanvasGroup>();
|
||||
|
||||
var dimRt = WLVignetteUtil.NewChild(s_root, "Dim");
|
||||
WLVignetteUtil.Stretch(dimRt);
|
||||
s_dim = dimRt.GetComponent<Image>();
|
||||
if (s_dim == null) s_dim = dimRt.gameObject.AddComponent<Image>();
|
||||
|
||||
s_box = WLVignetteUtil.NewChild(s_root, "Box");
|
||||
s_boxBg = s_box.GetComponent<Image>();
|
||||
if (s_boxBg == null) s_boxBg = s_box.gameObject.AddComponent<Image>();
|
||||
s_boxBg.raycastTarget = true;
|
||||
|
||||
s_title = WLTextFxUtil.NewText(s_box, "Title", s_font, TextAlignmentOptions.Center);
|
||||
s_timer = WLTextFxUtil.NewText(s_box, "Timer", s_font, TextAlignmentOptions.Center);
|
||||
s_cardRoot = WLTextFxUtil.NewChild(s_box, "Cards");
|
||||
}
|
||||
EnsureCards(s);
|
||||
string layout = ApplyLayout();
|
||||
Hide();
|
||||
return "연결 " + s.parentPath + " · " + layout;
|
||||
}
|
||||
|
||||
/// <summary>카드 노드를 설정 수만큼 만든다(줄이지 않는다 · 재사용 · 상한 5).</summary>
|
||||
public static bool EnsureCards(WLZonePickupSettings s)
|
||||
{
|
||||
if (s_cardRoot == null) return false;
|
||||
int want = Mathf.Clamp(s != null ? s.cardCount : 3, 1, s_cardListeners.Length);
|
||||
if (s_card.Length >= want) return false;
|
||||
|
||||
int old = s_card.Length;
|
||||
System.Array.Resize(ref s_card, want);
|
||||
System.Array.Resize(ref s_cardBg, want);
|
||||
System.Array.Resize(ref s_cardName, want);
|
||||
System.Array.Resize(ref s_cardDesc, want);
|
||||
System.Array.Resize(ref s_cardButton, want);
|
||||
for (int i = old; i < want; i++)
|
||||
{
|
||||
var rt = WLVignetteUtil.NewChild(s_cardRoot, "Card" + i);
|
||||
s_card[i] = rt;
|
||||
var bg = rt.GetComponent<Image>();
|
||||
if (bg == null) bg = rt.gameObject.AddComponent<Image>();
|
||||
bg.raycastTarget = true;
|
||||
s_cardBg[i] = bg;
|
||||
var b = rt.GetComponent<Button>();
|
||||
if (b == null) b = rt.gameObject.AddComponent<Button>();
|
||||
b.targetGraphic = bg;
|
||||
b.onClick.RemoveListener(s_cardListeners[i]);
|
||||
b.onClick.AddListener(s_cardListeners[i]);
|
||||
s_cardButton[i] = b;
|
||||
s_cardName[i] = WLTextFxUtil.NewText(rt, "Name", s_font, TextAlignmentOptions.Center);
|
||||
s_cardDesc[i] = WLTextFxUtil.NewText(rt, "Desc", s_font, TextAlignmentOptions.Top);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static string ApplyLayout()
|
||||
{
|
||||
var s = St;
|
||||
if (s == null) return "WLZonePickupSettings 에셋 없음 — 배치 건너뜀";
|
||||
if (s_root == null || s_box == null) return "노드 없음 — Bind 먼저";
|
||||
|
||||
var canvas = s_root.GetComponentInParent<Canvas>(true);
|
||||
float u = s.UnitsPerPx(canvas != null ? canvas.rootCanvas : null);
|
||||
|
||||
WLVignetteUtil.Stretch(s_root);
|
||||
s_root.localScale = Vector3.one;
|
||||
if (s_dim != null) { s_dim.color = s.dimColor; s_dim.raycastTarget = s.dimBlocksTouch; }
|
||||
|
||||
s_box.anchorMin = s_box.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
s_box.pivot = new Vector2(0.5f, 0.5f);
|
||||
s_box.anchoredPosition = new Vector2(0f, s.boxCenterOffsetYPx * u);
|
||||
s_box.sizeDelta = new Vector2(s.boxSizePx.x * u, s.boxSizePx.y * u);
|
||||
s_box.localScale = Vector3.one;
|
||||
if (s_boxBg != null) s_boxBg.color = s.boxColor;
|
||||
|
||||
float y = s.padTopPx * u;
|
||||
if (s_title != null)
|
||||
{
|
||||
TopRow(s_title.rectTransform, s, u, y, s.titleFontPx * u);
|
||||
s_title.fontSize = s.titleFontPx * u;
|
||||
s_title.color = s.titleColor;
|
||||
s_title.alignment = TextAlignmentOptions.Center;
|
||||
WLTextFxUtil.ApplyEdge(s_title, s.edge);
|
||||
}
|
||||
y += (s.titleFontPx + s.titleGapPx) * u;
|
||||
|
||||
if (s_timer != null)
|
||||
{
|
||||
TopRow(s_timer.rectTransform, s, u, y, s.timerFontPx * u);
|
||||
s_timer.fontSize = s.timerFontPx * u;
|
||||
s_timer.color = s_shownWarn ? s.timerWarnColor : s.timerColor;
|
||||
s_timer.alignment = TextAlignmentOptions.Center;
|
||||
WLTextFxUtil.ApplyEdge(s_timer, s.edge);
|
||||
}
|
||||
y += (s.timerFontPx + s.timerGapPx) * u;
|
||||
|
||||
int n = s_card.Length;
|
||||
float cw = s.cardSizePx.x * u, ch = s.cardSizePx.y * u, gap = s.cardGapPx * u;
|
||||
float total = n * cw + (n - 1) * gap;
|
||||
if (s_cardRoot != null)
|
||||
{
|
||||
s_cardRoot.anchorMin = new Vector2(0.5f, 1f); s_cardRoot.anchorMax = new Vector2(0.5f, 1f);
|
||||
s_cardRoot.pivot = new Vector2(0.5f, 1f);
|
||||
s_cardRoot.anchoredPosition = new Vector2(0f, -y);
|
||||
s_cardRoot.sizeDelta = new Vector2(total, ch);
|
||||
s_cardRoot.localScale = Vector3.one;
|
||||
}
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
var rt = s_card[i];
|
||||
if (rt == null) continue;
|
||||
rt.anchorMin = new Vector2(0.5f, 1f); rt.anchorMax = new Vector2(0.5f, 1f);
|
||||
rt.pivot = new Vector2(0.5f, 1f);
|
||||
rt.sizeDelta = new Vector2(cw, ch);
|
||||
rt.anchoredPosition = new Vector2(-total * 0.5f + cw * 0.5f + i * (cw + gap), 0f);
|
||||
rt.localScale = Vector3.one;
|
||||
|
||||
float pad = s.cardPadPx * u;
|
||||
var nm = s_cardName[i];
|
||||
if (nm != null)
|
||||
{
|
||||
var nrt = nm.rectTransform;
|
||||
nrt.anchorMin = new Vector2(0f, 1f); nrt.anchorMax = new Vector2(1f, 1f);
|
||||
nrt.pivot = new Vector2(0.5f, 1f);
|
||||
nrt.offsetMin = new Vector2(pad, 0f); nrt.offsetMax = new Vector2(-pad, 0f);
|
||||
nrt.anchoredPosition = new Vector2(0f, -pad);
|
||||
nrt.sizeDelta = new Vector2(0f, s.cardNameHeightPx * u);
|
||||
nm.fontSize = s.cardNameFontPx * u;
|
||||
nm.color = s.cardNameColor;
|
||||
nm.alignment = TextAlignmentOptions.Center;
|
||||
nm.textWrappingMode = TextWrappingModes.Normal;
|
||||
WLTextFxUtil.ApplyEdge(nm, s.edge);
|
||||
}
|
||||
var de = s_cardDesc[i];
|
||||
if (de != null)
|
||||
{
|
||||
var drt = de.rectTransform;
|
||||
drt.anchorMin = new Vector2(0f, 0f); drt.anchorMax = new Vector2(1f, 1f);
|
||||
drt.pivot = new Vector2(0.5f, 0.5f);
|
||||
drt.offsetMin = new Vector2(pad, pad);
|
||||
drt.offsetMax = new Vector2(-pad, -(pad + s.cardNameHeightPx * u));
|
||||
de.fontSize = s.cardDescFontPx * u;
|
||||
de.color = s.cardDescColor;
|
||||
de.alignment = TextAlignmentOptions.Top;
|
||||
de.textWrappingMode = TextWrappingModes.Normal;
|
||||
de.overflowMode = TextOverflowModes.Ellipsis;
|
||||
WLTextFxUtil.ApplyEdge(de, s.edge);
|
||||
}
|
||||
}
|
||||
|
||||
return "box" + s_box.sizeDelta + " 카드=" + n + " unitsPerPx=" + u.ToString("F4");
|
||||
}
|
||||
|
||||
private static void TopRow(RectTransform rt, WLZonePickupSettings s, float u, float top, float h)
|
||||
{
|
||||
rt.anchorMin = new Vector2(0f, 1f); rt.anchorMax = new Vector2(1f, 1f);
|
||||
rt.pivot = new Vector2(0.5f, 1f);
|
||||
rt.offsetMin = new Vector2(s.padXPx * u, 0f); rt.offsetMax = new Vector2(-s.padXPx * u, 0f);
|
||||
rt.anchoredPosition = new Vector2(0f, -top);
|
||||
rt.sizeDelta = new Vector2(0f, h);
|
||||
rt.localScale = Vector3.one;
|
||||
}
|
||||
|
||||
public static void SetFont(TMP_FontAsset f)
|
||||
{
|
||||
s_font = f;
|
||||
for (int i = 0; i < s_cardName.Length; i++)
|
||||
{
|
||||
if (s_cardName[i] != null) s_cardName[i].font = f;
|
||||
if (s_cardDesc[i] != null) s_cardDesc[i].font = f;
|
||||
}
|
||||
if (s_title != null) s_title.font = f;
|
||||
if (s_timer != null) s_timer.font = f;
|
||||
}
|
||||
|
||||
// ── 진단 · 정리 (프로브 전용 · 게임 코드는 부르지 않는다) ─────────────
|
||||
public static string ResetState()
|
||||
{
|
||||
Close("리셋");
|
||||
ZoneClearedSeen = OpenCount = PickCount = TimeoutCount = SkipCount = 0;
|
||||
GuideWaitCount = ReviveDeferCount = GiveUpCount = TimerWrites = PauseCalls = 0;
|
||||
s_picksThisRun = 0;
|
||||
LastLog = ""; LastSkipReason = "";
|
||||
return "리셋";
|
||||
}
|
||||
|
||||
public static string Teardown()
|
||||
{
|
||||
Subscribe(false);
|
||||
Close("teardown");
|
||||
if (s_root != null) Object.DestroyImmediate(s_root.gameObject);
|
||||
s_root = null; s_group = null; s_dim = null; s_box = null; s_boxBg = null;
|
||||
s_title = null; s_timer = null; s_cardRoot = null; s_uiRoot = null;
|
||||
s_card = new RectTransform[0]; s_cardBg = new Image[0];
|
||||
s_cardName = new TextMeshProUGUI[0]; s_cardDesc = new TextMeshProUGUI[0];
|
||||
s_cardButton = new Button[0];
|
||||
return "정리 완료";
|
||||
}
|
||||
|
||||
public static string Dump()
|
||||
{
|
||||
return "phase=" + s_phase + " open=" + IsOpen + " zone=" + s_pendingZone +
|
||||
" picks=" + s_picksThisRun + " cards=" + s_ids.Count +
|
||||
" sec=" + s_shownSec + " timerPaused=" + s_timerPaused +
|
||||
" worldHeld=" + s_worldHeld + " actorsStopped=" + s_actorsStopped +
|
||||
" orig=" + s_usedOriginalUi + " bound=" + Bound;
|
||||
}
|
||||
|
||||
/// <summary>프로브·QA 전용 — 카드 버튼을 실제 클릭 경로로 누른다.</summary>
|
||||
public static string ClickCard(int i)
|
||||
{
|
||||
if (i < 0 || i >= s_cardButton.Length || s_cardButton[i] == null) return "카드 버튼 없음 " + i;
|
||||
s_cardButton[i].onClick.Invoke();
|
||||
return "클릭 " + i;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 검증 전용 훅 — `CombatEventList<T>.Dispatch` 가 internal 이라 Assets 밖 프로브가 부를 수 없다.
|
||||
/// 813g/813y `LootToast.RaiseFakeKilled()` · 813q `WLRunUiProbeHooks` 선례.
|
||||
/// 🔴 게임 코드는 이 클래스를 한 번도 부르지 않는다(전부 void = GC 측정 오염 0).
|
||||
/// </summary>
|
||||
public static class WLZonePickupProbeHooks
|
||||
{
|
||||
public static void RaiseZoneCleared(int zoneIndex, int spawnerId, float elapsedSec,
|
||||
int kills, int required, bool allZonesCleared)
|
||||
{
|
||||
var e = new ZoneClearedEvent
|
||||
{
|
||||
zoneIndex = zoneIndex, spawnerId = spawnerId, elapsedSec = elapsedSec,
|
||||
kills = kills, required = required, allZonesCleared = allZonesCleared,
|
||||
time = Time.unscaledTime, frame = Time.frameCount,
|
||||
};
|
||||
RunEvents.ZoneCleared.Dispatch(in e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>픽업 카드를 매 프레임 돌리는 런타임 전용 러너(씬에 굽지 않는다).</summary>
|
||||
internal sealed class WLZonePickupRunner : MonoBehaviour
|
||||
{
|
||||
private static WLZonePickupRunner s_ins;
|
||||
private NewGameUI _lastUi;
|
||||
|
||||
internal static void Ensure()
|
||||
{
|
||||
if (s_ins != null) return;
|
||||
var go = new GameObject("WL_ZonePickupRunner");
|
||||
go.hideFlags = HideFlags.HideAndDontSave;
|
||||
Object.DontDestroyOnLoad(go);
|
||||
s_ins = go.AddComponent<WLZonePickupRunner>();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
var ui = NewGameUI.Ins;
|
||||
if (ui != null && !ReferenceEquals(ui, _lastUi))
|
||||
{
|
||||
_lastUi = ui;
|
||||
ZonePickup.Bind(ui.transform);
|
||||
}
|
||||
ZonePickup.Tick(ZonePickup.NowClock());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: fc48ff0df7dc434ab668f9c11c4caa70
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: d2b737e6d9b14f34b7fbad42597e179e, type: 3}
|
||||
m_Name: WLZonePickupSettings
|
||||
m_EditorClassIdentifier: Assembly-CSharp::WL.UI.WLZonePickupSettings
|
||||
enabled_: 1
|
||||
verboseLog: 0
|
||||
includeFinalZone: 0
|
||||
minZoneIndex: 0
|
||||
maxPicksPerRun: 0
|
||||
openDelaySeconds: 0.35
|
||||
waitForGuideArrival: 1
|
||||
maxGuideDelaySeconds: 8
|
||||
deferWhileRevive: 1
|
||||
maxDeferSeconds: 45
|
||||
cardCount: 3
|
||||
preferOriginalUi: 1
|
||||
pickTimeoutSeconds: 10
|
||||
timeoutUnscaled: 1
|
||||
autoPickFirstCard: 1
|
||||
timerFormat: "{0}초 후 자동 선택"
|
||||
timerWarnSeconds: 3
|
||||
pauseRunTimer: 1
|
||||
holdWorld: 0
|
||||
holdWorldTimeScale: 0
|
||||
stopActorsWhileOpen: 1
|
||||
parentPath: IngameUIs/WL_HUD
|
||||
dimColor: {r: 0, g: 0, b: 0, a: 0.62}
|
||||
dimBlocksTouch: 1
|
||||
boxSizePx: {x: 980, y: 720}
|
||||
boxCenterOffsetYPx: 40
|
||||
boxColor: {r: 0.043, g: 0.047, b: 0.071, a: 0.92}
|
||||
padXPx: 28
|
||||
padTopPx: 26
|
||||
titleText: "스킬 선택"
|
||||
titleFontPx: 54
|
||||
titleColor: {r: 0.949, g: 0.961, b: 0.98, a: 1}
|
||||
titleGapPx: 14
|
||||
timerFontPx: 36
|
||||
timerColor: {r: 0.78, g: 0.82, b: 0.88, a: 1}
|
||||
timerWarnColor: {r: 1, g: 0.29, b: 0.28, a: 1}
|
||||
timerGapPx: 16
|
||||
cardSizePx: {x: 288, y: 430}
|
||||
cardGapPx: 22
|
||||
cardBgColor: {r: 0.12, g: 0.13, b: 0.18, a: 1}
|
||||
cardPetBgColor: {r: 0.2, g: 0.13, b: 0.22, a: 1}
|
||||
cardNameFontPx: 38
|
||||
cardNameColor: {r: 1, g: 0.86, b: 0.45, a: 1}
|
||||
cardDescFontPx: 26
|
||||
cardDescColor: {r: 0.86, g: 0.89, b: 0.94, a: 1}
|
||||
cardPadPx: 16
|
||||
cardNameHeightPx: 84
|
||||
edge:
|
||||
enabled: 1
|
||||
outlineWidth: 0.26
|
||||
outlineColor: {r: 0.03, g: 0.03, b: 0.05, a: 1}
|
||||
shadowOffset: {x: 0.55, y: -0.55}
|
||||
shadowColor: {r: 0, g: 0, b: 0, a: 0.72}
|
||||
shadowSoftness: 0.28
|
||||
faceDilate: 0.06
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5da12f3cb4fe42adbad4d4c28ff7afae
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,248 @@
|
|||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// WLZonePickupSettings.cs — 존 클리어 픽업 카드 값의 단일 출처 (WL-813o · #813)
|
||||
//
|
||||
// 기준서 §D-2 813o 행 + 기획안 F-3 + 발주서 WL-813o §1(Lead 설계 결정).
|
||||
// 트리거 계약 = 813p 완료보고 §4 `RunEvents.ZoneCleared`(`Assets/WL/Combat/Run/RunEvents.cs` · 읽기만).
|
||||
//
|
||||
// ■ 왜 이 에셋인가 (C45)
|
||||
// 타임아웃 초·카드 수·존 포함 범위·배치·색·문구를 코드 상수로 두지 않는다. 이 파일에는
|
||||
// "필드와 기본값"만 있고, 게임이 읽는 값은 `Resources/WL/WLZonePickupSettings.asset` 이다.
|
||||
// 🔴 런 길이·존 수 같은 **게임 값은 여기 없다** — 813p `WLRunSettings` 소유이고
|
||||
// 픽업은 `ZoneCleared` 페이로드와 `RunDirector.ZoneCount` 로 받는다(수치 중복 0).
|
||||
//
|
||||
// ■ 좌표계 (px)
|
||||
// 813c/813q 와 같은 **세로 1080×1920 기준 화면 픽셀**. px → 캔버스 유닛 환산은
|
||||
// `WLHudLayoutSettings.UnitsPerPx(canvas)` **하나만** 쓴다(산식 중복 0).
|
||||
//
|
||||
// ■ C8 롤백
|
||||
// `enabled_ = 0` → 구독 0 · 노드 0 · 표시 0 · 런 타이머 정지 0 = 813o 이전 동작 100%.
|
||||
//
|
||||
// 🔴 어셈블리 주의: Assets/WL/UI/ 에 .asmdef 를 만들지 말 것(Assembly-CSharp 단일).
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace WL.UI
|
||||
{
|
||||
[CreateAssetMenu(fileName = "WLZonePickupSettings", menuName = "WL/Zone Pickup Settings", order = 34)]
|
||||
public sealed class WLZonePickupSettings : ScriptableObject
|
||||
{
|
||||
/// <summary>Resources 경로 — 런타임 로드 키.</summary>
|
||||
public const string ResourcesPath = "WL/WLZonePickupSettings";
|
||||
|
||||
private static WLZonePickupSettings s_instance;
|
||||
private static bool s_tried;
|
||||
|
||||
/// <summary>에셋이 없으면 null(호출부는 null 이면 전부 건너뛴다 = C8 롤백).</summary>
|
||||
public static WLZonePickupSettings Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (s_instance == null && !s_tried)
|
||||
{
|
||||
s_tried = true;
|
||||
s_instance = Resources.Load<WLZonePickupSettings>(ResourcesPath);
|
||||
}
|
||||
return s_instance;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>에디터에서 에셋을 새로 만들거나 값을 바꾼 뒤 캐시를 버린다.</summary>
|
||||
public static void ClearCache() { s_instance = null; s_tried = false; }
|
||||
|
||||
/// <summary>프로브·QA 용 런타임 스위치(에셋을 건드리지 않고 끈다).</summary>
|
||||
public static bool RuntimeDisabled;
|
||||
|
||||
/// <summary>픽업 축이 살아 있는가 = 에셋 존재 · enabled_ · 런타임 스위치 off.</summary>
|
||||
public static bool Enabled
|
||||
{
|
||||
get
|
||||
{
|
||||
if (RuntimeDisabled) return false;
|
||||
var s = Instance;
|
||||
return s != null && s.enabled_;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 공통 ──────────────────────────────────────────────────────────────
|
||||
[Header("공통 (C8 롤백)")]
|
||||
[Tooltip("끄면 구독 0 · 노드 0 · 표시 0 · 런 타이머 정지 0 = 813o 이전 동작 100%.")]
|
||||
public bool enabled_ = true;
|
||||
|
||||
[Tooltip("켜면 상태 전이마다 Console 에 1줄 남긴다(문자열을 만들기 전에 이 값으로 막는다 · GC 주의).")]
|
||||
public bool verboseLog = false;
|
||||
|
||||
// ── 트리거 ────────────────────────────────────────────────────────────
|
||||
[Header("트리거 — 813p RunEvents.ZoneCleared")]
|
||||
[Tooltip("존 4(마지막 존)에서도 카드를 띄울지. 기본 끔 = 보스 흐름 방해 금지(발주서 §1-1).")]
|
||||
public bool includeFinalZone = false;
|
||||
|
||||
[Tooltip("이 인덱스(0부터) 이상인 존만 카드를 띄운다. 0 = 존1부터 전부.")]
|
||||
public int minZoneIndex = 0;
|
||||
|
||||
[Tooltip("런 1회당 최대 카드 횟수. 0 = 무제한(존 수만큼).")]
|
||||
public int maxPicksPerRun = 0;
|
||||
|
||||
[Tooltip("ZoneCleared 를 받고 실제로 카드를 열기까지의 지연(초 · unscaled). 존 클리어 연출과 겹치지 않게 약간 둔다.")]
|
||||
public float openDelaySeconds = 0.35f;
|
||||
|
||||
[Tooltip("813w RunGuide 가 다음 존으로 이동 중(Travel)이면 도착한 뒤에 연다.")]
|
||||
public bool waitForGuideArrival = true;
|
||||
|
||||
[Tooltip("가이드 이동을 기다리는 최대 시간(초 · unscaled). 넘으면 이동 중이어도 연다(발주서 §1-1 = 8s).")]
|
||||
public float maxGuideDelaySeconds = 8f;
|
||||
|
||||
[Tooltip("813i/813z 부활 팝업이 떠 있거나 예약 중이면 부활을 우선하고 카드를 미룬다.")]
|
||||
public bool deferWhileRevive = true;
|
||||
|
||||
[Tooltip("부활 때문에 미룰 수 있는 최대 시간(초 · unscaled). 넘으면 이번 픽업을 버린다(무한 대기 금지).")]
|
||||
public float maxDeferSeconds = 45f;
|
||||
|
||||
// ── 카드 ──────────────────────────────────────────────────────────────
|
||||
[Header("카드 — 원본 table_selectskill / InGameInfo.Add_SelectSkill")]
|
||||
[Tooltip("한 번에 보여 줄 카드 수(기획안 F-3 = 3장 중 1장).")]
|
||||
public int cardCount = 3;
|
||||
|
||||
[Tooltip("원본 SelectSkillUI 노드(GameUI.Ins.IngameUI.m_SelectSkillUI)가 있으면 그걸 연다. " +
|
||||
"없으면 같은 원본 데이터·효과 경로를 쓰는 런타임 패널로 폴백한다. " +
|
||||
"🔴 실측(2026-09-09): 그 노드는 어떤 씬/프리팹에도 없다 = 항상 폴백.")]
|
||||
public bool preferOriginalUi = true;
|
||||
|
||||
// ── 타임아웃 ──────────────────────────────────────────────────────────
|
||||
[Header("타임아웃 — 자동 선택")]
|
||||
[Tooltip("카드가 열린 뒤 자동 선택까지의 시간(초). 발주서 §1-2 = 10s.")]
|
||||
public float pickTimeoutSeconds = 10f;
|
||||
|
||||
[Tooltip("타임아웃을 unscaled 시간으로 잰다(timeScale 0 이어도 흐른다).")]
|
||||
public bool timeoutUnscaled = true;
|
||||
|
||||
[Tooltip("타임아웃 시 첫 카드를 고른다(발주서 §1-2). 끄면 원본 방식대로 무작위 카드.")]
|
||||
public bool autoPickFirstCard = true;
|
||||
|
||||
[Tooltip("남은 초 표시 문구. TMP 무할당 오버로드 SetText(format, float) 를 쓴다 — {0} 하나만 넣을 것.")]
|
||||
public string timerFormat = "{0}초 후 자동 선택";
|
||||
|
||||
[Tooltip("남은 초가 이 값 이하면 경고색. 0 이면 경고색 없음.")]
|
||||
public float timerWarnSeconds = 3f;
|
||||
|
||||
// ── 정지 ──────────────────────────────────────────────────────────────
|
||||
[Header("정지 — 런 타이머 / 자동전투")]
|
||||
[Tooltip("카드가 열려 있는 동안 813p 런 타이머를 멈춘다(RunDirector.PauseTimer · 세계 정지 없음).")]
|
||||
public bool pauseRunTimer = true;
|
||||
|
||||
[Tooltip("카드가 열려 있는 동안 timeScale 을 0 으로 세운다(813z DeathFlow.HoldWorld 방식). " +
|
||||
"기본 끔 = 전투는 계속(발주서 §1-3 권장). 🔴 켜면 부활 팝업과 정지 주체가 겹친다.")]
|
||||
public bool holdWorld = false;
|
||||
|
||||
[Tooltip("holdWorld 가 켜졌을 때 세울 timeScale 값.")]
|
||||
public float holdWorldTimeScale = 0f;
|
||||
|
||||
[Tooltip("카드가 열려 있는 동안 원본 ActorInfo.Ins.All_Stop(true) 로 액터를 멈춘다 " +
|
||||
"(= 원본 SelectSkillUI.Set/SkillSelect 가 하던 그 경로). 선택 즉시 All_Stop(false) 로 되돌린다.")]
|
||||
public bool stopActorsWhileOpen = true;
|
||||
|
||||
// ── 배치 ──────────────────────────────────────────────────────────────
|
||||
[Header("배치 (세로 1080×1920 기준 px)")]
|
||||
[Tooltip("패널을 붙일 부모 노드의 UI 루트 기준 경로(813y/813q 실측 = IngameUIs/WL_HUD · SafeAreaFitter 아래).")]
|
||||
public string parentPath = "IngameUIs/WL_HUD";
|
||||
|
||||
[Tooltip("배경 어둡게(딤) 색.")]
|
||||
public Color dimColor = new Color(0f, 0f, 0f, 0.62f);
|
||||
|
||||
[Tooltip("딤이 터치를 먹는다(카드 밖을 눌러도 아무 일도 없게).")]
|
||||
public bool dimBlocksTouch = true;
|
||||
|
||||
[Tooltip("카드 묶음 상자 크기(px).")]
|
||||
public Vector2 boxSizePx = new Vector2(980f, 720f);
|
||||
|
||||
[Tooltip("상자 중심의 세로 오프셋(px · 양수 = 위). 0 = 화면 중앙.")]
|
||||
public float boxCenterOffsetYPx = 40f;
|
||||
|
||||
[Tooltip("상자 배경색.")]
|
||||
public Color boxColor = new Color(0.043f, 0.047f, 0.071f, 0.92f);
|
||||
|
||||
[Tooltip("상자 좌우 안쪽 여백(px).")]
|
||||
public float padXPx = 28f;
|
||||
|
||||
[Tooltip("상자 위쪽 안쪽 여백(px).")]
|
||||
public float padTopPx = 26f;
|
||||
|
||||
[Header("제목 · 남은 초")]
|
||||
[Tooltip("제목 문구.")]
|
||||
public string titleText = "스킬 선택";
|
||||
|
||||
[Tooltip("제목 글자 크기(px).")]
|
||||
public float titleFontPx = 54f;
|
||||
|
||||
[Tooltip("제목 색.")]
|
||||
public Color titleColor = new Color(0.949f, 0.961f, 0.98f, 1f);
|
||||
|
||||
[Tooltip("제목 아래 간격(px).")]
|
||||
public float titleGapPx = 14f;
|
||||
|
||||
[Tooltip("남은 초 글자 크기(px).")]
|
||||
public float timerFontPx = 36f;
|
||||
|
||||
[Tooltip("남은 초 색.")]
|
||||
public Color timerColor = new Color(0.78f, 0.82f, 0.88f, 1f);
|
||||
|
||||
[Tooltip("남은 초 경고색(timerWarnSeconds 이하).")]
|
||||
public Color timerWarnColor = new Color(1f, 0.29f, 0.28f, 1f);
|
||||
|
||||
[Tooltip("남은 초 줄 아래 간격(px).")]
|
||||
public float timerGapPx = 16f;
|
||||
|
||||
[Header("카드 모양")]
|
||||
[Tooltip("카드 1장 크기(px).")]
|
||||
public Vector2 cardSizePx = new Vector2(288f, 430f);
|
||||
|
||||
[Tooltip("카드 사이 간격(px).")]
|
||||
public float cardGapPx = 22f;
|
||||
|
||||
[Tooltip("카드 배경색.")]
|
||||
public Color cardBgColor = new Color(0.12f, 0.13f, 0.18f, 1f);
|
||||
|
||||
[Tooltip("펫 스킬 카드 배경색(원본 SelectSkillCard 의 petreinforce_bg 대응).")]
|
||||
public Color cardPetBgColor = new Color(0.20f, 0.13f, 0.22f, 1f);
|
||||
|
||||
[Tooltip("카드 이름 글자 크기(px).")]
|
||||
public float cardNameFontPx = 38f;
|
||||
|
||||
[Tooltip("카드 이름 색.")]
|
||||
public Color cardNameColor = new Color(1f, 0.86f, 0.45f, 1f);
|
||||
|
||||
[Tooltip("카드 설명 글자 크기(px).")]
|
||||
public float cardDescFontPx = 26f;
|
||||
|
||||
[Tooltip("카드 설명 색.")]
|
||||
public Color cardDescColor = new Color(0.86f, 0.89f, 0.94f, 1f);
|
||||
|
||||
[Tooltip("카드 안쪽 여백(px).")]
|
||||
public float cardPadPx = 16f;
|
||||
|
||||
[Tooltip("카드 이름 줄 높이(px).")]
|
||||
public float cardNameHeightPx = 84f;
|
||||
|
||||
// ── 텍스트 테두리 (813y WLTextFxUtil.ApplyEdge) ────────────────────────
|
||||
[Header("텍스트 테두리 (813y ApplyEdge · TMP 머티리얼 아웃라인)")]
|
||||
[Tooltip("제목·남은 초·카드 글자에 씌울 아웃라인/그림자.")]
|
||||
public WLTextEdge edge = new WLTextEdge
|
||||
{
|
||||
enabled = true,
|
||||
outlineWidth = 0.26f,
|
||||
outlineColor = new Color(0.03f, 0.03f, 0.05f, 1f),
|
||||
shadowOffset = new Vector2(0.55f, -0.55f),
|
||||
shadowColor = new Color(0f, 0f, 0f, 0.72f),
|
||||
shadowSoftness = 0.28f,
|
||||
faceDilate = 0.06f,
|
||||
};
|
||||
|
||||
// ── 환산 ──────────────────────────────────────────────────────────────
|
||||
/// <summary>px → 캔버스 유닛. 813c `WLHudLayoutSettings` 의 산식 하나만 쓴다(중복 0).</summary>
|
||||
public float UnitsPerPx(Canvas canvas)
|
||||
{
|
||||
var hud = WLHudLayoutSettings.Instance;
|
||||
return hud != null ? hud.UnitsPerPx(canvas) : 1f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: d2b737e6d9b14f34b7fbad42597e179e
|
||||
Loading…
Reference in New Issue