[WL-813m] 채점 프로브 + 캡처 세트 (#813)
기준서 v1 §E 채점표 24항목과 1:1 인 표를 실측으로 채우는 프로브와, 세로 1080x1920 캡처 5장 세트 + 엄지 반경 오버레이를 추가한다. - Assets/WL/Tools/WL813_ScoreProbe.cs (NEW · 에디터/개발빌드 전용) 절두체 내 몹 수(1 s 샘플 중앙값) · Killed 간격 · Spawned 버스트 · 히트 반응 3종 동프레임 · 시체 잔존 실측 · timeScale 감속/복원 실패 · 스킬 명중비 · fps/히치 · 드랍→획득 시간 · 레벨업 · 보스 페이즈/예고 · 킬캠 · PC 피격/사망 · GC 증가 → score_<HHmmss>.txt 표 + 원시 지표. 코어 이벤트 구독만 하는 읽기 전용 · isKill 정합(QA D-1) 대조 포함. - Assets/WL/Tools/WL813_ShotOverlay.cs (NEW · 에디터/개발빌드 전용) title/hud/combat/boss/popup 5장 자동 저장 + 인게임 슬롯은 반경 오버레이 1장 추가. 오버레이는 캡처 직후 제거(HideFlags.DontSave). - AgentScripts/WL813_Score.cs · WL813_Shots.cs (NEW · run_script 진입점) 게임 코드·에셋 수정 0 · CLI 명령 추가 0(wl_info 등은 802c 소관). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
6d9d8755c1
commit
ef7de60109
|
|
@ -0,0 +1,50 @@
|
|||
// WL813_Score.cs — #813m 채점 프로브 진입점 (기준서 v1 §E 채점표 24항목 1:1 로그)
|
||||
//
|
||||
// unity command run_script --file AgentScripts/WL813_Score.cs --entry WL813_Score.Run --args '[60]' // 60 s 샘플 시작(비차단)
|
||||
// unity command run_script --file AgentScripts/WL813_Score.cs --entry WL813_Score.Status // 진행 상황
|
||||
// unity command run_script --file AgentScripts/WL813_Score.cs --entry WL813_Score.Result // 끝난 뒤 표 전문
|
||||
// unity command run_script --file AgentScripts/WL813_Score.cs --entry WL813_Score.Summary // 한 줄 요약 + 파일 경로
|
||||
// unity command run_script --file AgentScripts/WL813_Score.cs --entry WL813_Score.RunAt --args '[60,0.5]' // 샘플 주기 지정
|
||||
// unity command run_script --file AgentScripts/WL813_Score.cs --entry WL813_Score.Stop // 조기 종료 후 표 기록
|
||||
//
|
||||
// 결과 파일: Screenshots_WL/Score/score_<HHmmss>.txt (Assets 밖 · 미추적)
|
||||
//
|
||||
// 실제 계측·채점 로직은 `Assets/WL/Tools/WL813_ScoreProbe.cs`(에디터 전용 · 컴파일 게이트 대상)에 있고
|
||||
// 이 파일은 run_script 진입점만 노출한다. Play 중에만 동작하며 게임 상태·에셋을 수정하지 않는다.
|
||||
//
|
||||
// 🔴 `Run` 은 즉시 돌아온다(비차단). 샘플이 끝나기 전에 `Result` 를 부르면 "(running)" 이 나온다 —
|
||||
// `Status` 로 남은 시간을 보고 기다린 뒤 `Result` 를 부른다.
|
||||
// 🔴 CLI 명령(`wl_info` 등)은 802c 소관이라 여기서 다시 만들지 않는다. 적 수·timeScale·HP 확인은 `unity command wl_info`.
|
||||
|
||||
using WL.Tools;
|
||||
|
||||
public static class WL813_Score
|
||||
{
|
||||
/// <summary>seconds 초 동안 샘플 → §E 24항목 표 기록. 0 이하면 기본 60 s.</summary>
|
||||
public static object Run(float seconds) { return WL813_ScoreProbe.Start(seconds, 0f); }
|
||||
|
||||
/// <summary>샘플 주기(초)까지 지정. 기본 1 s(기준서 §E 의 1 s 샘플).</summary>
|
||||
public static object RunAt(float seconds, float sampleInterval) { return WL813_ScoreProbe.Start(seconds, sampleInterval); }
|
||||
|
||||
/// <summary>진행 상황(경과 · 프레임 · 샘플 수 · Killed/Spawned).</summary>
|
||||
public static object Status() { return WL813_ScoreProbe.Status(); }
|
||||
|
||||
/// <summary>완료된 표 전문(§E 24항목). 콘솔에도 같은 내용이 남는다.</summary>
|
||||
public static object Result()
|
||||
{
|
||||
var t = WL813_ScoreProbe.LastTable;
|
||||
return string.IsNullOrEmpty(t) ? WL813_ScoreProbe.LastResult : t;
|
||||
}
|
||||
|
||||
/// <summary>한 줄 요약(제안 합계 · 「미확인」 수 · 파일 경로).</summary>
|
||||
public static object Summary()
|
||||
{
|
||||
return WL813_ScoreProbe.LastResult
|
||||
+ " | proposed=" + WL813_ScoreProbe.LastProposedTotal
|
||||
+ " unknown=" + WL813_ScoreProbe.LastUnknownCount
|
||||
+ " path=" + WL813_ScoreProbe.LastPath;
|
||||
}
|
||||
|
||||
/// <summary>진행 중인 샘플을 조기 종료하고 지금까지의 표를 쓴다.</summary>
|
||||
public static object Stop() { return WL813_ScoreProbe.Stop(); }
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
// WL813_Shots.cs — #813m 세로 캡처 5장 세트 + 엄지 반경 오버레이 진입점
|
||||
//
|
||||
// # 0) 게임뷰를 세로 규격으로 (기존 도구 재사용 · 중복 구현 없음)
|
||||
// unity command run_script --file AgentScripts/WL_GameViewSize.cs --entry WL_GameViewSize.Set --args '[1080,1920]'
|
||||
//
|
||||
// # 1) 5장 세트 — 6 s 간격으로 title → hud → combat → boss → popup 을 자동 저장
|
||||
// # 간격 사이에 QA 가 해당 화면을 만든다(콘솔에 "다음 캡처 = <슬롯>" 이 뜬다).
|
||||
// unity command run_script --file AgentScripts/WL813_Shots.cs --entry WL813_Shots.Run --args '[6]'
|
||||
//
|
||||
// # 2) 한 장만 · 오버레이 포함 한 장 더
|
||||
// unity command run_script --file AgentScripts/WL813_Shots.cs --entry WL813_Shots.Shot --args '["hud"]'
|
||||
// unity command run_script --file AgentScripts/WL813_Shots.cs --entry WL813_Shots.ShotReach --args '["hud"]'
|
||||
//
|
||||
// # 3) 오버레이만 켜고/끄기(육안 확인용) · 수치 요약
|
||||
// unity command run_script --file AgentScripts/WL813_Shots.cs --entry WL813_Shots.Overlay --args '[true]'
|
||||
// unity command run_script --file AgentScripts/WL813_Shots.cs --entry WL813_Shots.Layout
|
||||
// unity command run_script --file AgentScripts/WL813_Shots.cs --entry WL813_Shots.Result
|
||||
//
|
||||
// 저장 위치: Screenshots_WL/Shots/<번호>_<슬롯>_<W>x<H>_<HHmmss>.png + shots_<HHmmss>.txt (Assets 밖 · 미추적)
|
||||
//
|
||||
// 엄지 반경 오버레이 = 우하단(미러면 좌하단) 모서리 기준 `WLHudLayoutSettings.thumbReachRadiusPx` 원 +
|
||||
// 버튼별 실제 지름 원(반경 안 초록 · 밖 빨강) + 조이스틱 원(파랑). **캡처 직후 제거**한다.
|
||||
//
|
||||
// 🔴 `capture_game_view --source screen` 은 에디터 창 비율로 나와 세로 검증에 못 쓴다 → 여기서는 ScreenCapture 로 직접 뜬다.
|
||||
// 🔴 Play 중에만 동작하고 게임 상태·에셋을 수정하지 않는다(오버레이는 HideFlags.DontSave 임시 오브젝트).
|
||||
|
||||
using WL.Tools;
|
||||
|
||||
public static class WL813_Shots
|
||||
{
|
||||
/// <summary>5장 세트(title · hud · combat · boss · popup). intervalSeconds 마다 한 장, 인게임 슬롯은 오버레이 1장 추가.</summary>
|
||||
public static object Run(float intervalSeconds) { return WL813_ShotOverlay.RunSet(intervalSeconds, true); }
|
||||
|
||||
/// <summary>오버레이 없이 5장만.</summary>
|
||||
public static object RunPlain(float intervalSeconds) { return WL813_ShotOverlay.RunSet(intervalSeconds, false); }
|
||||
|
||||
/// <summary>지금 화면 1장(오버레이 없음).</summary>
|
||||
public static object Shot(string slot) { return WL813_ShotOverlay.Shot(slot, false); }
|
||||
|
||||
/// <summary>지금 화면 2장(원본 + 엄지 반경 오버레이).</summary>
|
||||
public static object ShotReach(string slot) { return WL813_ShotOverlay.Shot(slot, true); }
|
||||
|
||||
/// <summary>오버레이만 켜기/끄기(캡처 없이 육안 확인).</summary>
|
||||
public static object Overlay(bool on)
|
||||
{
|
||||
return on ? WL813_ShotOverlay.ShowOverlay() : WL813_ShotOverlay.HideOverlay();
|
||||
}
|
||||
|
||||
/// <summary>엄지 반경 수치 요약(설정 기하 vs 실제 배치 · §E 3-1 · 7-2 근거).</summary>
|
||||
public static object Layout() { return WL813_ShotOverlay.LayoutSummary(); }
|
||||
|
||||
/// <summary>진행 상황 · 저장 목록 · 매니페스트.</summary>
|
||||
public static object Result()
|
||||
{
|
||||
var m = WL813_ShotOverlay.LastManifest;
|
||||
return WL813_ShotOverlay.Status() + (string.IsNullOrEmpty(m) ? "" : "\n" + m);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: fbc940e0fc8132a46863c8ff5d335079
|
||||
|
|
@ -0,0 +1,378 @@
|
|||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// WL813_ShotOverlay.cs — #813m 캡처 세트 + 엄지 반경 오버레이 (에디터/개발빌드 전용)
|
||||
//
|
||||
// 무엇인가:
|
||||
// ① 세로 캡처 5장 세트(타이틀 · HUD · 전투 · 보스 · 팝업)를 이름 규약으로 자동 저장한다.
|
||||
// 파일명 `<번호>_<슬롯>_<W>x<H>_<HHmmss>.png` · 폴더 `Screenshots_WL/Shots/`(Assets 밖 · 미추적).
|
||||
// ② **엄지 반경 오버레이** — 우하단(미러면 좌하단) 모서리 기준 반경 원 + 버튼 실제 지름 원을
|
||||
// `WLHudLayoutSettings` 값 그대로 그린다. 반경 안 = 초록 · 밖 = 빨강 · 조이스틱 = 파랑.
|
||||
// 캡처 직후 반드시 제거한다(런타임 GameObject · `HideFlags.DontSave` · 씬 저장 대상 아님).
|
||||
//
|
||||
// 왜 `capture_game_view` 를 그대로 쓰지 않는가: `--source screen` 은 에디터 창 비율로 나와
|
||||
// 세로 1080×1920 검증에 못 쓴다(WL796_Shot 주석과 같은 이유). 여기서는 `ScreenCapture` 로 직접 뜬다.
|
||||
// 게임뷰 해상도 전환은 기존 도구 `AgentScripts/WL_GameViewSize.cs` 를 쓴다(중복 구현 금지).
|
||||
//
|
||||
// 🔴 읽기 전용: 게임 상태·에셋·설정을 바꾸지 않는다. 오버레이는 Play 중 임시 오브젝트이고 캡처 후 파괴된다.
|
||||
// 🔴 CLI 명령을 만들지 않는다(802c 소관). 진입점은 `AgentScripts/WL813_Shots.cs`(얇은 래퍼).
|
||||
// 🔴 릴리즈 빌드 제외.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using WL.UI;
|
||||
|
||||
namespace WL.Tools
|
||||
{
|
||||
/// <summary>#813m 캡처 세트 · 엄지 반경 오버레이.</summary>
|
||||
public static class WL813_ShotOverlay
|
||||
{
|
||||
public const string OutDir = "Screenshots_WL/Shots";
|
||||
|
||||
/// <summary>기준서 §D-1 813m 의 5장 세트 순서.</summary>
|
||||
public static readonly string[] SlotOrder = { "title", "hud", "combat", "boss", "popup" };
|
||||
|
||||
/// <summary>오버레이를 같이 뜨는 슬롯(패드가 보이는 인게임 화면).</summary>
|
||||
static readonly string[] ReachSlots = { "hud", "combat", "boss" };
|
||||
|
||||
public const int DesignWidth = 1080, DesignHeight = 1920;
|
||||
|
||||
public static string LastResult = "(not run)";
|
||||
public static string LastPath = "";
|
||||
public static string LastManifest = "";
|
||||
public static readonly List<string> Saved = new List<string>(16);
|
||||
|
||||
static Runner s_runner;
|
||||
static GameObject s_overlay;
|
||||
|
||||
public static bool OverlayActive { get { return s_overlay != null; } }
|
||||
public static bool Running { get { return s_runner != null; } }
|
||||
|
||||
// ── 진입점 ────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>5장 세트. `interval` 초마다 한 장씩(그 사이에 QA 가 화면을 만든다).</summary>
|
||||
public static string RunSet(float intervalSeconds, bool withOverlay)
|
||||
{
|
||||
if (!Application.isPlaying) return "not playing — Title 에서 Play 후 실행한다";
|
||||
if (s_runner != null) return "already running — " + Status();
|
||||
|
||||
float itv = intervalSeconds > 0f ? intervalSeconds : 6f;
|
||||
EnsureRunner();
|
||||
Saved.Clear();
|
||||
LastResult = "(running)";
|
||||
s_runner.StartCoroutine(s_runner.Co_Set(itv, withOverlay));
|
||||
return "started — 슬롯 " + string.Join(" → ", SlotOrder) + " · " + itv.ToString("F1")
|
||||
+ " s 간격 · 오버레이 " + (withOverlay ? "on" : "off")
|
||||
+ (SizeWarning() ?? "") + " · 진행은 WL813_Shots.Result";
|
||||
}
|
||||
|
||||
/// <summary>한 장만. slot = title|hud|combat|boss|popup(그 밖의 이름도 그대로 파일명에 쓴다).</summary>
|
||||
public static string Shot(string slot, bool withOverlay)
|
||||
{
|
||||
if (!Application.isPlaying) return "not playing";
|
||||
if (s_runner != null) return "busy — 세트 진행 중 (" + Status() + ")";
|
||||
EnsureRunner();
|
||||
string s = string.IsNullOrEmpty(slot) ? "shot" : slot.Trim();
|
||||
s_runner.StartCoroutine(s_runner.Co_One(s, withOverlay));
|
||||
return "capturing " + s + (withOverlay ? " (+엄지 반경 오버레이)" : "") + (SizeWarning() ?? "")
|
||||
+ " — WL813_Shots.Result 로 확인";
|
||||
}
|
||||
|
||||
public static string Status()
|
||||
{
|
||||
if (s_runner != null) return "running · saved " + Saved.Count;
|
||||
return "idle · saved " + Saved.Count + " · last=" + (string.IsNullOrEmpty(LastPath) ? "(none)" : LastPath);
|
||||
}
|
||||
|
||||
/// <summary>세로 규격이 아니면 경고 문구를 돌려준다(전환은 WL_GameViewSize 몫).</summary>
|
||||
public static string SizeWarning()
|
||||
{
|
||||
if (Screen.width == DesignWidth && Screen.height == DesignHeight) return null;
|
||||
return " · 🔴 게임뷰 " + Screen.width + "x" + Screen.height + " (규격 " + DesignWidth + "x" + DesignHeight
|
||||
+ " — WL_GameViewSize.Set [1080,1920] 로 맞춘 뒤 다시)";
|
||||
}
|
||||
|
||||
static void EnsureRunner()
|
||||
{
|
||||
if (s_runner != null) return;
|
||||
var go = GameObject.Find("__wl813_shots");
|
||||
if (go == null)
|
||||
{
|
||||
go = new GameObject("__wl813_shots");
|
||||
go.hideFlags = HideFlags.DontSave;
|
||||
}
|
||||
s_runner = go.GetComponent<Runner>();
|
||||
if (s_runner == null) s_runner = go.AddComponent<Runner>();
|
||||
}
|
||||
|
||||
// ── 오버레이 ──────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>엄지 반경 오버레이를 띄운다. 이미 있으면 다시 그린다.</summary>
|
||||
public static string ShowOverlay()
|
||||
{
|
||||
HideOverlay();
|
||||
var hud = WLHudLayoutSettings.Instance;
|
||||
if (hud == null) return "WLHudLayoutSettings 없음 — 오버레이 생략";
|
||||
|
||||
float k = hud.designWidthPx > 0f ? Screen.width / hud.designWidthPx : 1f;
|
||||
bool mirror = hud.mirrorLeftHanded;
|
||||
|
||||
s_overlay = new GameObject("__wl813_reach_overlay", typeof(RectTransform), typeof(Canvas));
|
||||
s_overlay.hideFlags = HideFlags.DontSave;
|
||||
var canvas = s_overlay.GetComponent<Canvas>();
|
||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
canvas.sortingOrder = 32760; // 게임 UI 위
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.Append("반경 ").Append(hud.thumbReachRadiusPx.ToString("F0")).Append(" px · k=").Append(k.ToString("F3"));
|
||||
|
||||
// 반경 원(모서리 기준)
|
||||
AddRing(canvas.transform, hud.fanOriginPx, hud.thumbReachRadiusPx * 2f, k, mirror,
|
||||
new Color(1f, 0.85f, 0.15f, 0.9f), 4f, "reach");
|
||||
|
||||
float r = hud.thumbReachRadiusPx;
|
||||
int outside = 0, inside = 0;
|
||||
float maxReach = 0f;
|
||||
|
||||
for (int i = 0; i < Mathf.Max(0, hud.activeSkillSlots); i++)
|
||||
Mark(canvas.transform, hud.FanSlotPx(i), hud.slotDiameterPx, k, mirror, r, ref inside, ref outside, ref maxReach, "skill" + i, sb);
|
||||
for (int i = 0; i < 2; i++)
|
||||
Mark(canvas.transform, hud.ReserveSlotPx(i), hud.reserveDiameterPx, k, mirror, r, ref inside, ref outside, ref maxReach, "reserve" + i, sb);
|
||||
Mark(canvas.transform, hud.attackCenterPx, hud.attackDiameterPx, k, mirror, r, ref inside, ref outside, ref maxReach, "attack", sb);
|
||||
Mark(canvas.transform, hud.autoButtonCenterPx, hud.autoButtonDiameterPx, k, mirror, r, ref inside, ref outside, ref maxReach, "auto", sb);
|
||||
|
||||
// 조이스틱(반대쪽 모서리 기준 · 반경 밖 판정 대상 아님)
|
||||
AddRing(canvas.transform, hud.joystickCenterPx, hud.joystickRadiusPx * 2f, k, !mirror,
|
||||
new Color(0.3f, 0.7f, 1f, 0.85f), 3f, "joystick");
|
||||
|
||||
LastResult = "overlay on · " + sb + " · 안 " + inside + " · 밖 " + outside
|
||||
+ " · 최대 도달 " + maxReach.ToString("F1") + " px";
|
||||
return LastResult;
|
||||
}
|
||||
|
||||
public static string HideOverlay()
|
||||
{
|
||||
if (s_overlay == null) return "overlay off";
|
||||
UnityEngine.Object.DestroyImmediate(s_overlay);
|
||||
s_overlay = null;
|
||||
return "overlay removed";
|
||||
}
|
||||
|
||||
/// <summary>버튼 하나를 실제 지름 원으로 표시하고 반경 안/밖을 색으로 구분한다.</summary>
|
||||
static void Mark(Transform parent, Vector2 px, float diameterPx, float k, bool mirror, float radiusPx,
|
||||
ref int inside, ref int outside, ref float maxReach, string name, StringBuilder sb)
|
||||
{
|
||||
float reach = px.magnitude + diameterPx * 0.5f;
|
||||
if (reach > maxReach) maxReach = reach;
|
||||
bool ok = radiusPx <= 0f || reach <= radiusPx;
|
||||
if (ok) inside++; else outside++;
|
||||
var c = ok ? new Color(0.2f, 0.95f, 0.45f, 0.95f) : new Color(1f, 0.25f, 0.28f, 0.95f);
|
||||
AddRing(parent, px, diameterPx, k, mirror, c, 3f, name);
|
||||
sb.Append(" · ").Append(name).Append(' ').Append(reach.ToString("F0"));
|
||||
}
|
||||
|
||||
/// <summary>모서리 기준 (dx, dy) px 자리에 지름 diameterPx 인 링을 놓는다.</summary>
|
||||
static void AddRing(Transform parent, Vector2 px, float diameterPx, float k, bool mirror,
|
||||
Color color, float thicknessPx, string name)
|
||||
{
|
||||
var go = new GameObject("ring_" + name, typeof(RectTransform), typeof(RawImage));
|
||||
go.hideFlags = HideFlags.DontSave;
|
||||
var rt = (RectTransform)go.transform;
|
||||
rt.SetParent(parent, false);
|
||||
|
||||
var img = go.GetComponent<RawImage>();
|
||||
img.raycastTarget = false;
|
||||
img.color = color;
|
||||
img.texture = RingTexture(Mathf.Max(2f, thicknessPx * 128f / Mathf.Max(8f, diameterPx * k)));
|
||||
|
||||
Vector2 anchor = new Vector2(mirror ? 0f : 1f, 0f);
|
||||
rt.anchorMin = anchor;
|
||||
rt.anchorMax = anchor;
|
||||
rt.pivot = new Vector2(0.5f, 0.5f);
|
||||
float sx = mirror ? px.x : -px.x;
|
||||
rt.anchoredPosition = new Vector2(sx * k, px.y * k);
|
||||
rt.sizeDelta = new Vector2(diameterPx * k, diameterPx * k);
|
||||
rt.localScale = Vector3.one;
|
||||
}
|
||||
|
||||
// 링 텍스처는 두께 비율별로 캐시한다(캡처마다 새로 만들지 않는다).
|
||||
static readonly Dictionary<int, Texture2D> s_ringCache = new Dictionary<int, Texture2D>();
|
||||
|
||||
static Texture2D RingTexture(float thicknessTexels)
|
||||
{
|
||||
const int N = 128;
|
||||
float th = Mathf.Clamp(thicknessTexels, 1.5f, 24f);
|
||||
int key = Mathf.RoundToInt(th * 4f);
|
||||
Texture2D tex;
|
||||
if (s_ringCache.TryGetValue(key, out tex) && tex != null) return tex;
|
||||
|
||||
tex = new Texture2D(N, N, TextureFormat.RGBA32, false);
|
||||
tex.hideFlags = HideFlags.DontSave;
|
||||
tex.wrapMode = TextureWrapMode.Clamp;
|
||||
var px = new Color32[N * N];
|
||||
float c = (N - 1) * 0.5f;
|
||||
float outer = c; // 바깥 반지름
|
||||
float mid = outer - th * 0.5f; // 링 중심선
|
||||
for (int y = 0; y < N; y++)
|
||||
{
|
||||
for (int x = 0; x < N; x++)
|
||||
{
|
||||
float dx = x - c, dy = y - c;
|
||||
float d = Mathf.Sqrt(dx * dx + dy * dy);
|
||||
float a = 1f - Mathf.Clamp01((Mathf.Abs(d - mid) - th * 0.5f + 1f) / 1.5f);
|
||||
byte b = (byte)Mathf.RoundToInt(Mathf.Clamp01(a) * 255f);
|
||||
px[y * N + x] = new Color32(255, 255, 255, b);
|
||||
}
|
||||
}
|
||||
tex.SetPixels32(px);
|
||||
tex.Apply(false, false);
|
||||
s_ringCache[key] = tex;
|
||||
return tex;
|
||||
}
|
||||
|
||||
// ── 캡처 ──────────────────────────────────────────────────────────────
|
||||
|
||||
static int SlotIndex(string slot)
|
||||
{
|
||||
for (int i = 0; i < SlotOrder.Length; i++)
|
||||
if (string.Equals(SlotOrder[i], slot, StringComparison.OrdinalIgnoreCase)) return i + 1;
|
||||
return 9;
|
||||
}
|
||||
|
||||
static bool WantsReach(string slot)
|
||||
{
|
||||
for (int i = 0; i < ReachSlots.Length; i++)
|
||||
if (string.Equals(ReachSlots[i], slot, StringComparison.OrdinalIgnoreCase)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
internal sealed class Runner : MonoBehaviour
|
||||
{
|
||||
public IEnumerator Co_One(string slot, bool withOverlay)
|
||||
{
|
||||
yield return Capture(slot, false);
|
||||
if (withOverlay)
|
||||
{
|
||||
ShowOverlay();
|
||||
yield return Capture(slot + "_reach", true);
|
||||
HideOverlay();
|
||||
}
|
||||
Finish();
|
||||
}
|
||||
|
||||
public IEnumerator Co_Set(float interval, bool withOverlay)
|
||||
{
|
||||
var log = new StringBuilder();
|
||||
log.AppendLine("# WL813 캡처 세트 (#813m) " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
|
||||
log.AppendLine("# 게임뷰 " + Screen.width + "x" + Screen.height + " · 간격 " + interval.ToString("F1")
|
||||
+ " s · 오버레이 " + (withOverlay ? "on" : "off"));
|
||||
var w = SizeWarning();
|
||||
if (w != null) log.AppendLine("#" + w);
|
||||
log.AppendLine("# 슬롯 사이 " + interval.ToString("F0") + " s 안에 해당 화면을 만든다(타이틀 → HUD → 전투 → 보스 → 팝업).");
|
||||
log.AppendLine();
|
||||
|
||||
for (int i = 0; i < SlotOrder.Length; i++)
|
||||
{
|
||||
string slot = SlotOrder[i];
|
||||
Debug.Log("[WL813m] 다음 캡처 = " + slot + " (" + interval.ToString("F0") + " s 뒤)");
|
||||
float t0 = Time.unscaledTime;
|
||||
while (Time.unscaledTime - t0 < interval) yield return null;
|
||||
|
||||
yield return Capture(slot, false);
|
||||
log.AppendLine("- " + slot + " → " + (Saved.Count > 0 ? Saved[Saved.Count - 1] : "(실패)"));
|
||||
|
||||
if (withOverlay && WantsReach(slot))
|
||||
{
|
||||
string ov = ShowOverlay();
|
||||
yield return Capture(slot + "_reach", true);
|
||||
log.AppendLine(" · 오버레이 → " + (Saved.Count > 0 ? Saved[Saved.Count - 1] : "(실패)") + " [" + ov + "]");
|
||||
HideOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
log.AppendLine();
|
||||
log.AppendLine(LayoutSummary());
|
||||
LastManifest = log.ToString();
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(OutDir);
|
||||
var p = Path.Combine(OutDir, "shots_" + DateTime.Now.ToString("HHmmss") + ".txt");
|
||||
File.WriteAllText(p, LastManifest, new UTF8Encoding(false));
|
||||
LastManifest += "\n(매니페스트 " + p + ")";
|
||||
}
|
||||
catch (Exception e) { LastManifest += "\n(매니페스트 저장 실패 " + e.Message + ")"; }
|
||||
|
||||
LastResult = "세트 완료 · " + Saved.Count + "장 · " + OutDir;
|
||||
Debug.Log("[WL813m] " + LastResult + "\n" + LastManifest);
|
||||
Finish();
|
||||
}
|
||||
|
||||
IEnumerator Capture(string slot, bool overlayOn)
|
||||
{
|
||||
yield return new WaitForEndOfFrame();
|
||||
Texture2D tex = null;
|
||||
try
|
||||
{
|
||||
tex = ScreenCapture.CaptureScreenshotAsTexture();
|
||||
Directory.CreateDirectory(OutDir);
|
||||
string name = SlotIndex(slot.Replace("_reach", "")).ToString("00") + "_" + slot + "_"
|
||||
+ tex.width + "x" + tex.height + "_" + DateTime.Now.ToString("HHmmss") + ".png";
|
||||
string path = Path.Combine(OutDir, name);
|
||||
File.WriteAllBytes(path, tex.EncodeToPNG());
|
||||
Saved.Add(path);
|
||||
LastPath = path;
|
||||
Debug.Log("[WL813m] 캡처 " + path + (overlayOn ? " (엄지 반경 오버레이)" : ""));
|
||||
}
|
||||
catch (Exception e) { Debug.LogWarning("[WL813m] 캡처 실패 " + slot + " · " + e.Message); }
|
||||
finally { if (tex != null) UnityEngine.Object.Destroy(tex); }
|
||||
}
|
||||
|
||||
void Finish()
|
||||
{
|
||||
HideOverlay();
|
||||
s_runner = null;
|
||||
if (gameObject != null) Destroy(gameObject);
|
||||
}
|
||||
|
||||
void OnDestroy() { if (s_runner == this) s_runner = null; }
|
||||
}
|
||||
|
||||
/// <summary>캡처 판정에 같이 붙일 레이아웃 수치(설정 + 실제 배치).</summary>
|
||||
public static string LayoutSummary()
|
||||
{
|
||||
var hud = WLHudLayoutSettings.Instance;
|
||||
if (hud == null) return "WLHudLayoutSettings 없음";
|
||||
var pad = UnityEngine.Object.FindFirstObjectByType<WLBattlePadLayout>(FindObjectsInactive.Include);
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("## 엄지 반경 수치 (기준서 §E 3-1 · 7-2)");
|
||||
sb.AppendLine("- 반경 " + hud.thumbReachRadiusPx.ToString("F0") + " px · 슬롯 " + hud.activeSkillSlots
|
||||
+ "개 지름 " + hud.slotDiameterPx.ToString("F0") + " px · 최소 간격 기준 " + hud.slotMinGapPx.ToString("F0") + " px");
|
||||
sb.AppendLine("- 부채꼴 원점 (" + hud.fanOriginPx.x.ToString("F0") + "," + hud.fanOriginPx.y.ToString("F0")
|
||||
+ ") · 반지름 " + hud.fanRadiusPx.ToString("F0") + " px · 시작 " + hud.fanStartAngleDeg.ToString("F1")
|
||||
+ "° 간격 " + hud.fanStepAngleDeg.ToString("F1") + "°");
|
||||
float maxGeo = 0f;
|
||||
for (int i = 0; i < Mathf.Max(0, hud.activeSkillSlots); i++)
|
||||
maxGeo = Mathf.Max(maxGeo, hud.FanSlotPx(i).magnitude + hud.slotDiameterPx * 0.5f);
|
||||
for (int i = 0; i < 2; i++)
|
||||
maxGeo = Mathf.Max(maxGeo, hud.ReserveSlotPx(i).magnitude + hud.reserveDiameterPx * 0.5f);
|
||||
maxGeo = Mathf.Max(maxGeo, hud.attackCenterPx.magnitude + hud.attackDiameterPx * 0.5f);
|
||||
maxGeo = Mathf.Max(maxGeo, hud.autoButtonCenterPx.magnitude + hud.autoButtonDiameterPx * 0.5f);
|
||||
sb.AppendLine("- 설정 기하 최대 도달 " + maxGeo.ToString("F1") + " px → 반경 "
|
||||
+ (maxGeo <= hud.thumbReachRadiusPx ? "안 ✅" : "밖 🔴"));
|
||||
sb.AppendLine("- 실제 배치 " + (pad != null
|
||||
? "maxReach " + pad.LastMaxReachPx.ToString("F1") + " px · minGap " + pad.LastMinGapPx.ToString("F1")
|
||||
+ " px · 슬롯 " + pad.LastActiveSlots + "+예약 " + pad.LastReserveSlots
|
||||
: "WLBattlePadLayout 없음(패드 미표시 · 「미확인」)"));
|
||||
sb.AppendLine("- 조이스틱 (" + hud.joystickCenterPx.x.ToString("F0") + "," + hud.joystickCenterPx.y.ToString("F0")
|
||||
+ ") 반경 " + hud.joystickRadiusPx.ToString("F0") + " px · 미러=" + (hud.mirrorLeftHanded ? "좌손" : "우손"));
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 048bebe914fadfa46b9eaa045f6f5551
|
||||
Loading…
Reference in New Issue