[WL-813c] 전투 패드 4슬롯 + 보스 HP 바 (#813)
This commit is contained in:
parent
21c0fae954
commit
7a42c50cc4
|
|
@ -0,0 +1,226 @@
|
|||
// WL813c_Apply.cs — #813 바퀴 2: 전투 패드 4슬롯(우하단 엄지 부채꼴) + 상단 보스 HP 바 (에디트 모드 · Play 불필요)
|
||||
// unity command run_script --file AgentScripts/WL813c_Apply.cs --entry WL813c_Apply.CreateSettings
|
||||
// unity command run_script --file AgentScripts/WL813c_Apply.cs --entry WL813c_Apply.Resave (무변경 재저장 — 재직렬화 diff 측정 · 802b 교훈)
|
||||
// unity command run_script --file AgentScripts/WL813c_Apply.cs --entry WL813c_Apply.Apply
|
||||
// unity command run_script --file AgentScripts/WL813c_Apply.cs --entry WL813c_Apply.Revert (추가한 컴포넌트·노드 제거 = 원상)
|
||||
//
|
||||
// 방식(발주서 ⓓ "NewGameUI.prefab 변경 최소 · 노드 추가만"):
|
||||
// ① IngameUIs/BattleUI (BattleUI.prefab 인스턴스) 에 **인스턴스 전용 추가 컴포넌트**로
|
||||
// WL.UI.SafeAreaFitter(802b 방식 · 전용 루트 · 스트레치 · offset 0 · 재부모화 0) + WL.UI.WLBattlePadLayout 을 붙인다.
|
||||
// → BattleUI.prefab 원본은 손대지 않으므로 SkillUI/SkillEquipUI 의 다른 인스턴스는 회귀 0.
|
||||
// ② IngameUIs/WL_HUD (이미 SafeAreaFitter 부착 = Safe Area 레이어) 아래에 WL_BossHpBar 노드 1개를 추가한다.
|
||||
// ③ 슬롯 좌표는 프리팹에 굽지 않는다 — 런타임 WLBattlePadLayout 이 에셋 값으로 배치한다(코드 상수 0 · diff 최소).
|
||||
// 롤백: Revert() 또는 git checkout -- Assets/Res_Addr/MainUI/NewGameUI.prefab
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
public static class WL813c_Apply
|
||||
{
|
||||
public const string NewGameUIPath = "Assets/Res_Addr/MainUI/NewGameUI.prefab";
|
||||
public const string SettingsDir = "Assets/WL/UI/Settings/Resources/WL";
|
||||
public const string SettingsPath = SettingsDir + "/WLHudLayoutSettings.asset";
|
||||
public const string BossBarName = "WL_BossHpBar";
|
||||
public const string PadPath = "IngameUIs/BattleUI";
|
||||
public const string HudPath = "IngameUIs/WL_HUD";
|
||||
const string TmpFontGuid = "a387e325271126742ad15569dba1ac57"; // SkillCard/t_cooltime 이 쓰는 폰트(실측)
|
||||
const int UILayer = 5;
|
||||
|
||||
static Transform FindByPath(Transform root, string path)
|
||||
{
|
||||
var cur = root;
|
||||
foreach (var part in path.Split('/'))
|
||||
{
|
||||
Transform next = null;
|
||||
for (int i = 0; i < cur.childCount; i++)
|
||||
if (cur.GetChild(i).name == part) { next = cur.GetChild(i); break; }
|
||||
if (next == null) return null;
|
||||
cur = next;
|
||||
}
|
||||
return cur;
|
||||
}
|
||||
|
||||
static void EnsureFolder(string dir)
|
||||
{
|
||||
var parts = dir.Split('/');
|
||||
var cur = parts[0];
|
||||
for (int i = 1; i < parts.Length; i++)
|
||||
{
|
||||
var next = cur + "/" + parts[i];
|
||||
if (!AssetDatabase.IsValidFolder(next)) AssetDatabase.CreateFolder(cur, parts[i]);
|
||||
cur = next;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>WLHudLayoutSettings.asset 생성(이미 있으면 값 유지 · 덮어쓰지 않는다).</summary>
|
||||
public static object CreateSettings()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var existing = AssetDatabase.LoadAssetAtPath<WL.UI.WLHudLayoutSettings>(SettingsPath);
|
||||
if (existing != null) { sb.AppendLine(SettingsPath + " : 이미 있음 — 값 유지"); }
|
||||
else
|
||||
{
|
||||
EnsureFolder(SettingsDir);
|
||||
var so = ScriptableObject.CreateInstance<WL.UI.WLHudLayoutSettings>(); // 기본값 = 기준서 §B/§F-1 제안값
|
||||
AssetDatabase.CreateAsset(so, SettingsPath);
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
sb.AppendLine(SettingsPath + " : 생성");
|
||||
}
|
||||
WL.UI.WLHudLayoutSettings.ClearCache();
|
||||
var s = WL.UI.WLHudLayoutSettings.Instance;
|
||||
if (s == null) { sb.AppendLine("🔴 Resources 로드 실패 — Resources/WL 경로 확인"); return sb.ToString(); }
|
||||
|
||||
sb.AppendLine("Resources 로드 OK · design=" + s.designWidthPx + "x" + s.designHeightPx +
|
||||
" activeSlots=" + s.activeSkillSlots + " slotDia=" + s.slotDiameterPx +
|
||||
" fanR=" + s.fanRadiusPx + " start=" + s.fanStartAngleDeg + " step=" + s.fanStepAngleDeg);
|
||||
// 기하 검증 — 모서리 기준 좌표 · 이웃 여백 · 최대 도달 반경 (기준서 §B 요소3/7)
|
||||
var pts = new List<Vector2>();
|
||||
var dia = new List<float>();
|
||||
for (int i = 0; i < s.activeSkillSlots; i++) { pts.Add(s.FanSlotPx(i)); dia.Add(s.slotDiameterPx); }
|
||||
for (int i = 0; i < 6 - s.activeSkillSlots; i++) { pts.Add(s.ReserveSlotPx(i)); dia.Add(s.reserveDiameterPx); }
|
||||
pts.Add(s.attackCenterPx); dia.Add(s.attackDiameterPx);
|
||||
pts.Add(s.autoButtonCenterPx); dia.Add(s.autoButtonDiameterPx);
|
||||
float minGap = float.MaxValue, maxReach = 0f, minEdgeX = float.MaxValue, minEdgeY = float.MaxValue, maxTopPx = 0f;
|
||||
for (int i = 0; i < pts.Count; i++)
|
||||
{
|
||||
maxReach = Mathf.Max(maxReach, pts[i].magnitude + dia[i] * 0.5f);
|
||||
minEdgeX = Mathf.Min(minEdgeX, pts[i].x - dia[i] * 0.5f);
|
||||
minEdgeY = Mathf.Min(minEdgeY, pts[i].y - dia[i] * 0.5f);
|
||||
maxTopPx = Mathf.Max(maxTopPx, pts[i].y + dia[i] * 0.5f);
|
||||
for (int j = i + 1; j < pts.Count; j++)
|
||||
minGap = Mathf.Min(minGap, Vector2.Distance(pts[i], pts[j]) - (dia[i] + dia[j]) * 0.5f);
|
||||
sb.AppendLine(" pt" + i + " px(dx=" + pts[i].x.ToString("F1") + ", dy=" + pts[i].y.ToString("F1") +
|
||||
", d=" + dia[i].ToString("F1") + ") reach=" + (pts[i].magnitude + dia[i] * 0.5f).ToString("F1"));
|
||||
}
|
||||
sb.AppendLine(" 검증 minGapPx=" + minGap.ToString("F1") + "(기준 ≥" + s.slotMinGapPx + ")" +
|
||||
" maxReachPx=" + maxReach.ToString("F1") + "(목표 ≤" + s.thumbReachRadiusPx + ")" +
|
||||
" 화면밖여유 dx=" + minEdgeX.ToString("F1") + " dy=" + minEdgeY.ToString("F1"));
|
||||
float bottom40 = s.designHeightPx * 0.4f; // 기준서 §F-1 "하단 40 %" — 설정 값에서 파생(상수 0)
|
||||
sb.AppendLine(" 하단40% 경계=" + bottom40.ToString("F0") + "px · 최상단 버튼 dy+반지름=" +
|
||||
maxTopPx.ToString("F1") + " → 안=" + (maxTopPx <= bottom40));
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>무변경 재저장 — LoadPrefabContents→SaveAsPrefabAsset 이 diff 를 만드는지 먼저 잰다(802b 교훈).</summary>
|
||||
public static object Resave()
|
||||
{
|
||||
var root = PrefabUtility.LoadPrefabContents(NewGameUIPath);
|
||||
try { PrefabUtility.SaveAsPrefabAsset(root, NewGameUIPath); }
|
||||
finally { PrefabUtility.UnloadPrefabContents(root); }
|
||||
AssetDatabase.Refresh();
|
||||
return NewGameUIPath + " : 무변경 재저장 (git diff 로 재직렬화 여부 확인)";
|
||||
}
|
||||
|
||||
public static object Apply()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var settings = AssetDatabase.LoadAssetAtPath<WL.UI.WLHudLayoutSettings>(SettingsPath);
|
||||
if (settings == null) return "🔴 " + SettingsPath + " 없음 — CreateSettings 먼저";
|
||||
|
||||
TMPro.TMP_FontAsset font = null;
|
||||
var fontPath = AssetDatabase.GUIDToAssetPath(TmpFontGuid);
|
||||
if (!string.IsNullOrEmpty(fontPath)) font = AssetDatabase.LoadAssetAtPath<TMPro.TMP_FontAsset>(fontPath);
|
||||
sb.AppendLine("폰트: " + (font != null ? fontPath : "「미확인」 — 이름 라벨 폰트 미주입"));
|
||||
|
||||
var root = PrefabUtility.LoadPrefabContents(NewGameUIPath);
|
||||
try
|
||||
{
|
||||
// ── ① 전투 패드 루트 = IngameUIs/BattleUI (인스턴스 전용 추가 컴포넌트) ──
|
||||
var pad = FindByPath(root.transform, PadPath);
|
||||
if (pad == null) { sb.AppendLine("🔴 " + PadPath + " 없음"); }
|
||||
else
|
||||
{
|
||||
var prt = (RectTransform)pad;
|
||||
if (pad.GetComponent<WL.UI.SafeAreaFitter>() == null)
|
||||
{ pad.gameObject.AddComponent<WL.UI.SafeAreaFitter>(); sb.AppendLine(PadPath + " : SafeAreaFitter 추가"); }
|
||||
else sb.AppendLine(PadPath + " : SafeAreaFitter 이미 있음");
|
||||
|
||||
var layout = pad.GetComponent<WL.UI.WLBattlePadLayout>();
|
||||
if (layout == null) { layout = pad.gameObject.AddComponent<WL.UI.WLBattlePadLayout>(); sb.AppendLine(PadPath + " : WLBattlePadLayout 추가"); }
|
||||
else sb.AppendLine(PadPath + " : WLBattlePadLayout 이미 있음");
|
||||
|
||||
var so = new SerializedObject(layout);
|
||||
so.FindProperty("target").objectReferenceValue = pad.GetComponent<BattleUI>();
|
||||
so.ApplyModifiedPropertiesWithoutUndo();
|
||||
|
||||
// Safe Area 패널 규격(802b 방식): 스트레치 · offset 0 · pivot 중앙 · 재부모화 0
|
||||
sb.AppendLine(" before " + Describe(prt));
|
||||
prt.anchorMin = Vector2.zero; prt.anchorMax = Vector2.one;
|
||||
prt.pivot = new Vector2(0.5f, 0.5f);
|
||||
prt.offsetMin = Vector2.zero; prt.offsetMax = Vector2.zero;
|
||||
prt.localScale = Vector3.one; prt.localRotation = Quaternion.identity;
|
||||
sb.AppendLine(" after " + Describe(prt));
|
||||
}
|
||||
|
||||
// ── ② 상단 보스 HP 바 = IngameUIs/WL_HUD/WL_BossHpBar (기존 Safe Area 레이어 아래) ──
|
||||
var hud = FindByPath(root.transform, HudPath);
|
||||
if (hud == null) { sb.AppendLine("🔴 " + HudPath + " 없음"); }
|
||||
else
|
||||
{
|
||||
var barT = hud.Find(BossBarName);
|
||||
if (barT == null)
|
||||
{
|
||||
var go = new GameObject(BossBarName, typeof(RectTransform));
|
||||
go.layer = UILayer;
|
||||
barT = go.transform;
|
||||
((RectTransform)barT).SetParent((RectTransform)hud, false);
|
||||
sb.AppendLine(HudPath + "/" + BossBarName + " : 노드 추가");
|
||||
}
|
||||
else sb.AppendLine(HudPath + "/" + BossBarName + " : 이미 있음");
|
||||
|
||||
var bar = barT.GetComponent<WL.UI.BossHpBar>();
|
||||
if (bar == null) { bar = barT.gameObject.AddComponent<WL.UI.BossHpBar>(); sb.AppendLine(" BossHpBar 추가"); }
|
||||
if (font != null) bar.SetNameFont(font);
|
||||
bool made = bar.BuildIfNeeded();
|
||||
sb.AppendLine(" BuildIfNeeded made=" + made + " · " + bar.ApplyLayout());
|
||||
// 저장 상태의 기본은 **미표시** — 보스 Spawned 이벤트가 와야 보인다(발주서 §1-2 "이벤트 미도착 = 미표시").
|
||||
var cg = barT.GetComponent<CanvasGroup>();
|
||||
if (cg != null) { cg.alpha = 0f; cg.interactable = false; cg.blocksRaycasts = false; sb.AppendLine(" CanvasGroup alpha=0 (기본 미표시)"); }
|
||||
foreach (Transform c in barT) sb.AppendLine(" child " + c.name + " " + Describe(c as RectTransform));
|
||||
}
|
||||
|
||||
PrefabUtility.SaveAsPrefabAsset(root, NewGameUIPath);
|
||||
}
|
||||
finally { PrefabUtility.UnloadPrefabContents(root); }
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public static object Revert()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var root = PrefabUtility.LoadPrefabContents(NewGameUIPath);
|
||||
try
|
||||
{
|
||||
var pad = FindByPath(root.transform, PadPath);
|
||||
if (pad != null)
|
||||
{
|
||||
var l = pad.GetComponent<WL.UI.WLBattlePadLayout>();
|
||||
if (l != null) { Object.DestroyImmediate(l, true); sb.AppendLine(PadPath + " : WLBattlePadLayout 제거"); }
|
||||
var f = pad.GetComponent<WL.UI.SafeAreaFitter>();
|
||||
if (f != null) { Object.DestroyImmediate(f, true); sb.AppendLine(PadPath + " : SafeAreaFitter 제거"); }
|
||||
sb.AppendLine(" ⚠ 루트 RectTransform 값은 되돌리지 않는다 — git checkout -- " + NewGameUIPath + " 를 쓸 것");
|
||||
}
|
||||
var hud = FindByPath(root.transform, HudPath);
|
||||
var bar = hud != null ? hud.Find(BossBarName) : null;
|
||||
if (bar != null) { Object.DestroyImmediate(bar.gameObject); sb.AppendLine(HudPath + "/" + BossBarName + " : 제거"); }
|
||||
PrefabUtility.SaveAsPrefabAsset(root, NewGameUIPath);
|
||||
}
|
||||
finally { PrefabUtility.UnloadPrefabContents(root); }
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
static string Describe(RectTransform rt)
|
||||
{
|
||||
if (rt == null) return "(rt 없음)";
|
||||
return "aMin=" + rt.anchorMin + " aMax=" + rt.anchorMax + " offMin=" + rt.offsetMin + " offMax=" + rt.offsetMax +
|
||||
" size=" + rt.sizeDelta + " pos=" + rt.anchoredPosition + " pivot=" + rt.pivot + " scale=" + rt.localScale;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,248 @@
|
|||
// WL813c_Probe.cs — #813 전투 패드 · 보스 HP 바 실측 덤프 (에디트 모드 · Play 불필요 · 저장하지 않는다)
|
||||
// unity command run_script --file AgentScripts/WL813c_Probe.cs --entry WL813c_Probe.DumpPad (발주서 ⓑ · 부채꼴 좌표 + 미러 2회)
|
||||
// unity command run_script --file AgentScripts/WL813c_Probe.cs --entry WL813c_Probe.DumpBossEvents (발주서 ⓒ · 가짜 이벤트 → 바 갱신)
|
||||
// unity command run_script --file AgentScripts/WL813c_Probe.cs --entry WL813c_Probe.DumpPrefab (발주서 ⓓ · 저장된 프리팹 노드 상태)
|
||||
// unity command run_script --file AgentScripts/WL813c_Probe.cs --entry WL813c_Probe.DumpOverride (발주서 ⓓ · WLIngameUiOverride 대상 경로 회귀)
|
||||
// 덤프는 Logs/WL813c_probe_*.txt 에도 남긴다(콘솔 잘림 대비).
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public static class WL813c_Probe
|
||||
{
|
||||
// run_script 는 파일마다 독립 어셈블리로 컴파일된다 — WL813c_Apply 의 상수를 참조할 수 없어 여기 다시 둔다(값 동일).
|
||||
public const string NewGameUIPath = "Assets/Res_Addr/MainUI/NewGameUI.prefab";
|
||||
public const string PadPath = "IngameUIs/BattleUI";
|
||||
public const string HudPath = "IngameUIs/WL_HUD";
|
||||
public const string BossBarName = "WL_BossHpBar";
|
||||
const string LogDir = "Logs";
|
||||
|
||||
static string Save(string name, string body)
|
||||
{
|
||||
Directory.CreateDirectory(LogDir);
|
||||
var path = Path.Combine(LogDir, "WL813c_probe_" + name + ".txt");
|
||||
File.WriteAllText(path, body, new System.Text.UTF8Encoding(false));
|
||||
return body + "\n(저장: " + path + ")";
|
||||
}
|
||||
|
||||
static Transform FindByPath(Transform root, string path)
|
||||
{
|
||||
var cur = root;
|
||||
foreach (var part in path.Split('/'))
|
||||
{
|
||||
Transform next = null;
|
||||
for (int i = 0; i < cur.childCount; i++)
|
||||
if (cur.GetChild(i).name == part) { next = cur.GetChild(i); break; }
|
||||
if (next == null) return null;
|
||||
cur = next;
|
||||
}
|
||||
return cur;
|
||||
}
|
||||
|
||||
/// <summary>발주서 ⓑ — 실제 NewGameUI 프리팹 계층에서 배치를 적용하고 좌표를 덤프한다(저장 안 함 · 미러 2회).</summary>
|
||||
public static object DumpPad()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var s = WL.UI.WLHudLayoutSettings.Instance;
|
||||
if (s == null) return "🔴 WLHudLayoutSettings 없음";
|
||||
bool origMirror = s.mirrorLeftHanded;
|
||||
|
||||
var root = PrefabUtility.LoadPrefabContents(NewGameUIPath);
|
||||
try
|
||||
{
|
||||
var pad = FindByPath(root.transform, PadPath);
|
||||
if (pad == null) return "🔴 " + PadPath + " 없음";
|
||||
var layout = pad.GetComponent<WL.UI.WLBattlePadLayout>();
|
||||
if (layout == null) return "🔴 WLBattlePadLayout 없음 — Apply 먼저";
|
||||
|
||||
var canvas = pad.GetComponentInParent<Canvas>();
|
||||
var scaler = canvas != null ? canvas.GetComponent<CanvasScaler>() : null;
|
||||
sb.AppendLine("Canvas=" + (canvas != null ? canvas.name : "없음") +
|
||||
" scaleMode=" + (scaler != null ? scaler.uiScaleMode.ToString() : "-") +
|
||||
" refRes=" + (scaler != null ? scaler.referenceResolution.ToString() : "-") +
|
||||
" matchMode=" + (scaler != null ? scaler.screenMatchMode.ToString() : "-") +
|
||||
" match=" + (scaler != null ? scaler.matchWidthOrHeight.ToString("F2") : "-"));
|
||||
sb.AppendLine("SafeAreaFitter on pad = " + (pad.GetComponent<WL.UI.SafeAreaFitter>() != null) +
|
||||
" · WL_HUD 형제 fitter = " + (FindByPath(root.transform, HudPath) != null &&
|
||||
FindByPath(root.transform, HudPath).GetComponent<WL.UI.SafeAreaFitter>() != null));
|
||||
sb.AppendLine();
|
||||
|
||||
s.mirrorLeftHanded = false;
|
||||
sb.AppendLine("── 1회차 mirror=false ──────────────────────────────");
|
||||
sb.AppendLine(layout.Apply());
|
||||
sb.AppendLine(layout.Dump());
|
||||
|
||||
s.mirrorLeftHanded = true;
|
||||
sb.AppendLine("── 2회차 mirror=true ───────────────────────────────");
|
||||
sb.AppendLine(layout.Apply());
|
||||
sb.AppendLine(layout.Dump());
|
||||
}
|
||||
finally
|
||||
{
|
||||
PrefabUtility.UnloadPrefabContents(root); // 저장하지 않는다 = 프리팹 무변경
|
||||
s.mirrorLeftHanded = origMirror; // 에셋 값 원복(파일 저장 안 함)
|
||||
}
|
||||
return Save("pad", sb.ToString());
|
||||
}
|
||||
|
||||
/// <summary>발주서 ⓒ — 임시 씬에 살아 있는 BossHpBar 를 만들고 가짜 전투 이벤트를 실제 CombatEvents 경로로 발행한다.</summary>
|
||||
public static object DumpBossEvents()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var s = WL.UI.WLHudLayoutSettings.Instance;
|
||||
if (s == null) return "🔴 WLHudLayoutSettings 없음";
|
||||
|
||||
// 배치모드 에디터는 "제목 없는 미저장 씬"이 열려 있어 Additive 새 씬을 못 만든다.
|
||||
// → 현재 씬에 임시 오브젝트를 만들고 끝나면 지운다(저장하지 않는다).
|
||||
GameObject cgo = null;
|
||||
try
|
||||
{
|
||||
// NewGameUI 와 같은 캔버스 규격(실측: Expand · 참조 1920x1080 · match 0)
|
||||
cgo = new GameObject("WL813cProbeCanvas", typeof(Canvas), typeof(CanvasScaler));
|
||||
var canvas = cgo.GetComponent<Canvas>();
|
||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
var sc = cgo.GetComponent<CanvasScaler>();
|
||||
sc.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
sc.referenceResolution = new Vector2(1920f, 1080f);
|
||||
sc.screenMatchMode = CanvasScaler.ScreenMatchMode.Expand;
|
||||
sc.matchWidthOrHeight = 0f;
|
||||
|
||||
var hud = new GameObject("WL_HUD", typeof(RectTransform), typeof(WL.UI.SafeAreaFitter));
|
||||
var hrt = (RectTransform)hud.transform;
|
||||
hrt.SetParent(cgo.transform, false);
|
||||
hrt.anchorMin = Vector2.zero; hrt.anchorMax = Vector2.one;
|
||||
hrt.offsetMin = Vector2.zero; hrt.offsetMax = Vector2.zero;
|
||||
|
||||
int beforeSpawned = WL.Combat.Core.CombatEvents.Spawned.Count;
|
||||
var bgo = new GameObject(BossBarName, typeof(RectTransform));
|
||||
((RectTransform)bgo.transform).SetParent(hrt, false);
|
||||
var bar = bgo.AddComponent<WL.UI.BossHpBar>();
|
||||
// 에디트 모드에서는 OnEnable 이 불리지 않는다 → OnEnable 본체(Initialize)를 직접 부른다(런타임과 같은 경로).
|
||||
sb.AppendLine("Initialize: " + bar.Initialize());
|
||||
|
||||
sb.AppendLine("구독 전/후 Spawned 구독자 = " + beforeSpawned + " → " + WL.Combat.Core.CombatEvents.Spawned.Count);
|
||||
sb.AppendLine("초기: " + bar.Dump());
|
||||
|
||||
// ⓪ 비보스 스폰은 바를 켜지 않아야 한다(발주서 §1-2 "이벤트 미도착/무관 = 미표시")
|
||||
sb.AppendLine("⓪ " + WL.UI.BossHpBar.RaiseFakeNonBossSpawned() + " → visible=" + bar.Visible + " (기대 False)");
|
||||
|
||||
sb.AppendLine("① " + WL.UI.BossHpBar.RaiseFakeBossSpawned(1000f));
|
||||
sb.AppendLine(bar.Dump());
|
||||
sb.AppendLine("② " + WL.UI.BossHpBar.RaiseFakeHit(300d));
|
||||
sb.AppendLine(bar.Dump());
|
||||
sb.AppendLine("③ " + WL.UI.BossHpBar.RaiseFakeHit(350d));
|
||||
sb.AppendLine(bar.Dump());
|
||||
sb.AppendLine("④ " + WL.UI.BossHpBar.RaiseFakeKilled());
|
||||
sb.AppendLine(bar.Dump());
|
||||
|
||||
bar.Subscribe(false); // 에디트 모드에서는 OnDisable 이 불리지 않는다 — 구독을 명시적으로 뗀다
|
||||
Object.DestroyImmediate(bgo);
|
||||
sb.AppendLine("정리 후 Spawned 구독자 = " + WL.Combat.Core.CombatEvents.Spawned.Count + " (기대 " + beforeSpawned + ")");
|
||||
}
|
||||
finally { if (cgo != null) Object.DestroyImmediate(cgo); }
|
||||
return Save("boss", sb.ToString());
|
||||
}
|
||||
|
||||
/// <summary>발주서 ⓓ — 저장된 프리팹의 추가 노드/컴포넌트 상태만 덤프한다(계층 전체 덤프 아님).</summary>
|
||||
public static object DumpPrefab()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var root = PrefabUtility.LoadPrefabContents(NewGameUIPath);
|
||||
try
|
||||
{
|
||||
var pad = FindByPath(root.transform, PadPath);
|
||||
sb.AppendLine(PadPath + " : " + (pad == null ? "없음" : Comp(pad)));
|
||||
if (pad != null)
|
||||
{
|
||||
var prt = (RectTransform)pad;
|
||||
sb.AppendLine(" rect aMin=" + prt.anchorMin + " aMax=" + prt.anchorMax + " offMin=" + prt.offsetMin +
|
||||
" offMax=" + prt.offsetMax + " pivot=" + prt.pivot + " scale=" + prt.localScale +
|
||||
" activeSelf=" + pad.gameObject.activeSelf + " children=" + pad.childCount);
|
||||
for (int i = 0; i < pad.childCount; i++)
|
||||
{
|
||||
var c = pad.GetChild(i);
|
||||
sb.AppendLine(" - " + c.name + " active=" + c.gameObject.activeSelf + " " +
|
||||
Rect2(c as RectTransform));
|
||||
}
|
||||
}
|
||||
var hud = FindByPath(root.transform, HudPath);
|
||||
sb.AppendLine(HudPath + " : " + (hud == null ? "없음" : Comp(hud) + " children=" + hud.childCount));
|
||||
if (hud != null)
|
||||
for (int i = 0; i < hud.childCount; i++)
|
||||
sb.AppendLine(" - " + hud.GetChild(i).name);
|
||||
var bar = hud != null ? hud.Find(BossBarName) : null;
|
||||
if (bar != null)
|
||||
{
|
||||
sb.AppendLine(BossBarName + " : " + Comp(bar) + " " + Rect2(bar as RectTransform));
|
||||
DumpTree(bar, 1, sb);
|
||||
var b = bar.GetComponent<WL.UI.BossHpBar>();
|
||||
if (b != null) sb.AppendLine(b.Dump());
|
||||
}
|
||||
}
|
||||
finally { PrefabUtility.UnloadPrefabContents(root); }
|
||||
return Save("prefab", sb.ToString());
|
||||
}
|
||||
|
||||
/// <summary>발주서 ⓓ — WLIngameUiOverride 가 쓰는 경로가 여전히 유효한지(패드 숨김 회귀 0).</summary>
|
||||
public static object DumpOverride()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var gs = WL.Settings.WLGameplaySettings.Instance;
|
||||
sb.AppendLine("WLGameplaySettings: hideBattlePad=" + (gs != null ? gs.hideBattlePad.ToString() : "(에셋없음)") +
|
||||
" (813c 는 읽기만 · 값 전환은 Lead)");
|
||||
var root = PrefabUtility.LoadPrefabContents(NewGameUIPath);
|
||||
try
|
||||
{
|
||||
foreach (var p in (gs != null && gs.battlePadPaths != null ? gs.battlePadPaths : new string[0]))
|
||||
{
|
||||
var t = FindByPath(root.transform, p);
|
||||
sb.AppendLine(" battlePadPath \"" + p + "\" → " + (t == null ? "🔴 못 찾음(회귀!)" :
|
||||
"OK activeSelf=" + t.gameObject.activeSelf + " children=" + t.childCount));
|
||||
}
|
||||
foreach (var p in (gs != null && gs.ingameHideCanvasGroupPaths != null ? gs.ingameHideCanvasGroupPaths : new string[0]))
|
||||
{
|
||||
var t = FindByPath(root.transform, p);
|
||||
sb.AppendLine(" hideCanvasGroupPath \"" + p + "\" → " + (t == null ? "🔴 못 찾음(회귀!)" : "OK"));
|
||||
}
|
||||
var common = FindByPath(root.transform, "Common");
|
||||
foreach (var n in (gs != null && gs.ingameHideCommonObjectNames != null ? gs.ingameHideCommonObjectNames : new string[0]))
|
||||
{
|
||||
var t = common != null ? common.Find(n) : null;
|
||||
sb.AppendLine(" hideCommonName \"" + n + "\" → " + (t == null ? "「미확인」(NewGameUI.gos[0] 인덱스 기준이라 프리팹 경로와 다를 수 있음)" : "OK"));
|
||||
}
|
||||
}
|
||||
finally { PrefabUtility.UnloadPrefabContents(root); }
|
||||
return Save("override", sb.ToString());
|
||||
}
|
||||
|
||||
static void DumpTree(Transform t, int depth, StringBuilder sb)
|
||||
{
|
||||
for (int i = 0; i < t.childCount; i++)
|
||||
{
|
||||
var c = t.GetChild(i);
|
||||
sb.AppendLine(new string(' ', depth * 2) + "- " + c.name + " " + Comp(c) + " " + Rect2(c as RectTransform));
|
||||
DumpTree(c, depth + 1, sb);
|
||||
}
|
||||
}
|
||||
|
||||
static string Comp(Transform t)
|
||||
{
|
||||
var sb = new StringBuilder("[");
|
||||
var cs = t.GetComponents<Component>();
|
||||
for (int i = 0; i < cs.Length; i++)
|
||||
{
|
||||
if (i > 0) sb.Append(',');
|
||||
sb.Append(cs[i] == null ? "MISSING" : cs[i].GetType().Name);
|
||||
}
|
||||
return sb.Append(']').ToString();
|
||||
}
|
||||
|
||||
static string Rect2(RectTransform rt)
|
||||
{
|
||||
if (rt == null) return "";
|
||||
return "aMin" + rt.anchorMin + " aMax" + rt.anchorMax + " pos" + rt.anchoredPosition + " size" + rt.sizeDelta;
|
||||
}
|
||||
}
|
||||
|
|
@ -6345,6 +6345,43 @@ RectTransform:
|
|||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!1 &410977187151152065
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 7627031014790560531}
|
||||
m_Layer: 5
|
||||
m_Name: Ticks
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &7627031014790560531
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 410977187151152065}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 2628488089120278146}
|
||||
- {fileID: 4234803563267772812}
|
||||
m_Father: {fileID: 4263647952213832125}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!1 &424934420149951930
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -7024,6 +7061,44 @@ MonoBehaviour:
|
|||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 1
|
||||
--- !u!1 &515739860467809776
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 4263647952213832125}
|
||||
m_Layer: 5
|
||||
m_Name: Bar
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &4263647952213832125
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 515739860467809776}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 379739010395703442}
|
||||
- {fileID: 5853486629667327400}
|
||||
- {fileID: 7627031014790560531}
|
||||
m_Father: {fileID: 4008587971485836497}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 49.77778}
|
||||
m_Pivot: {x: 0.5, y: 0}
|
||||
--- !u!1 &527205017749727505
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -21595,6 +21670,77 @@ MonoBehaviour:
|
|||
texts:
|
||||
- {fileID: 3829523605195096373}
|
||||
- {fileID: 1207394060063698035}
|
||||
--- !u!1 &1383167073221206107
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 4008587971485836497}
|
||||
- component: {fileID: 1658727240416140832}
|
||||
- component: {fileID: 3869902529055779347}
|
||||
m_Layer: 5
|
||||
m_Name: WL_BossHpBar
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &4008587971485836497
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1383167073221206107}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 4263647952213832125}
|
||||
- {fileID: 7756575112940689194}
|
||||
m_Father: {fileID: 6746702601929578265}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 1}
|
||||
m_AnchorMax: {x: 0.5, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: -337.77777}
|
||||
m_SizeDelta: {x: 1564.4445, y: 117.333336}
|
||||
m_Pivot: {x: 0.5, y: 1}
|
||||
--- !u!114 &1658727240416140832
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1383167073221206107}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 5759d44fea68d204697e4b572af27bde, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::WL.UI.BossHpBar
|
||||
group: {fileID: 3869902529055779347}
|
||||
bar: {fileID: 4263647952213832125}
|
||||
background: {fileID: 5562866952227585321}
|
||||
fill: {fileID: 7034991675718591158}
|
||||
tickRoot: {fileID: 7627031014790560531}
|
||||
nameLabel: {fileID: 7683836662204136578}
|
||||
nameFont: {fileID: 11400000, guid: a387e325271126742ad15569dba1ac57, type: 2}
|
||||
showName: 1
|
||||
--- !u!225 &3869902529055779347
|
||||
CanvasGroup:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1383167073221206107}
|
||||
m_Enabled: 1
|
||||
m_Alpha: 0
|
||||
m_Interactable: 0
|
||||
m_BlocksRaycasts: 0
|
||||
m_IgnoreParentGroups: 0
|
||||
--- !u!1 &1390787552525075213
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -33812,6 +33958,144 @@ MonoBehaviour:
|
|||
m_Spacing: {x: 5, y: 5}
|
||||
m_Constraint: 1
|
||||
m_ConstraintCount: 3
|
||||
--- !u!1 &2042806660445920432
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 7756575112940689194}
|
||||
- component: {fileID: 5255818241730012330}
|
||||
- component: {fileID: 7683836662204136578}
|
||||
m_Layer: 5
|
||||
m_Name: Name
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &7756575112940689194
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2042806660445920432}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 4008587971485836497}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 53.333332}
|
||||
m_Pivot: {x: 0.5, y: 1}
|
||||
--- !u!222 &5255818241730012330
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2042806660445920432}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &7683836662204136578
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2042806660445920432}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TextMeshProUGUI
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 0
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_text:
|
||||
m_isRightToLeft: 0
|
||||
m_fontAsset: {fileID: 11400000, guid: a387e325271126742ad15569dba1ac57, type: 2}
|
||||
m_sharedMaterial: {fileID: -6459762427321423278, guid: a387e325271126742ad15569dba1ac57,
|
||||
type: 2}
|
||||
m_fontSharedMaterials: []
|
||||
m_fontMaterial: {fileID: 0}
|
||||
m_fontMaterials: []
|
||||
m_fontColor32:
|
||||
serializedVersion: 2
|
||||
rgba: 4294967295
|
||||
m_fontColor: {r: 1, g: 0.92, b: 0.6, a: 1}
|
||||
m_enableVertexGradient: 0
|
||||
m_colorMode: 3
|
||||
m_fontColorGradient:
|
||||
topLeft: {r: 1, g: 1, b: 1, a: 1}
|
||||
topRight: {r: 1, g: 1, b: 1, a: 1}
|
||||
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
|
||||
bottomRight: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_fontColorGradientPreset: {fileID: 0}
|
||||
m_spriteAsset: {fileID: 0}
|
||||
m_tintAllSprites: 0
|
||||
m_StyleSheet: {fileID: 0}
|
||||
m_TextStyleHashCode: 0
|
||||
m_overrideHtmlColors: 0
|
||||
m_faceColor:
|
||||
serializedVersion: 2
|
||||
rgba: 4294967295
|
||||
m_fontSize: 53.333332
|
||||
m_fontSizeBase: 53.333332
|
||||
m_fontWeight: 400
|
||||
m_enableAutoSizing: 0
|
||||
m_fontSizeMin: 0
|
||||
m_fontSizeMax: 0
|
||||
m_fontStyle: 0
|
||||
m_HorizontalAlignment: 2
|
||||
m_VerticalAlignment: 512
|
||||
m_textAlignment: 65535
|
||||
m_characterSpacing: 0
|
||||
m_characterHorizontalScale: 1
|
||||
m_wordSpacing: 0
|
||||
m_lineSpacing: 0
|
||||
m_lineSpacingMax: 0
|
||||
m_paragraphSpacing: 0
|
||||
m_charWidthMaxAdj: 0
|
||||
m_TextWrappingMode: 0
|
||||
m_wordWrappingRatios: 0.4
|
||||
m_overflowMode: 0
|
||||
m_linkedTextComponent: {fileID: 0}
|
||||
parentLinkedComponent: {fileID: 0}
|
||||
m_enableKerning: 0
|
||||
m_ActiveFontFeatures: 00000000
|
||||
m_enableExtraPadding: 0
|
||||
checkPaddingRequired: 0
|
||||
m_isRichText: 1
|
||||
m_EmojiFallbackSupport: 1
|
||||
m_parseCtrlCharacters: 1
|
||||
m_isOrthographic: 0
|
||||
m_isCullingEnabled: 0
|
||||
m_horizontalMapping: 0
|
||||
m_verticalMapping: 0
|
||||
m_uvLineOffset: 0
|
||||
m_geometrySortingOrder: 0
|
||||
m_IsTextObjectScaleStatic: 0
|
||||
m_VertexBufferAutoSizeReduction: 0
|
||||
m_useMaxVisibleDescender: 1
|
||||
m_pageToDisplay: 1
|
||||
m_margin: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_isUsingLegacyAnimationComponent: 0
|
||||
m_isVolumetricText: 0
|
||||
m_hasFontAssetChanged: 0
|
||||
m_baseMaterial: {fileID: 0}
|
||||
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
|
||||
--- !u!1 &2048073296754467902
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -36975,6 +37259,81 @@ MonoBehaviour:
|
|||
m_ChildScaleWidth: 0
|
||||
m_ChildScaleHeight: 0
|
||||
m_ReverseArrangement: 0
|
||||
--- !u!1 &2265900064724467381
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 5853486629667327400}
|
||||
- component: {fileID: 1579818030648339006}
|
||||
- component: {fileID: 7034991675718591158}
|
||||
m_Layer: 5
|
||||
m_Name: Fill
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &5853486629667327400
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2265900064724467381}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 4263647952213832125}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &1579818030648339006
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2265900064724467381}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &7034991675718591158
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2265900064724467381}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 0.99, g: 0.26, b: 0.28, a: 1}
|
||||
m_RaycastTarget: 0
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 0}
|
||||
m_Type: 3
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 0
|
||||
m_FillAmount: 1
|
||||
m_FillClockwise: 1
|
||||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 1
|
||||
--- !u!1 &2273064814390488970
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -74309,6 +74668,81 @@ MonoBehaviour:
|
|||
m_OnValueChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
--- !u!1 &4818555733381499326
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 379739010395703442}
|
||||
- component: {fileID: 36720904701419872}
|
||||
- component: {fileID: 5562866952227585321}
|
||||
m_Layer: 5
|
||||
m_Name: Bg
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &379739010395703442
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 4818555733381499326}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 4263647952213832125}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &36720904701419872
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 4818555733381499326}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &5562866952227585321
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 4818555733381499326}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 0.05, g: 0.05, b: 0.06, a: 0.78}
|
||||
m_RaycastTarget: 0
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 0}
|
||||
m_Type: 0
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
m_FillAmount: 1
|
||||
m_FillClockwise: 1
|
||||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 1
|
||||
--- !u!1 &4820055094452431178
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -90955,6 +91389,81 @@ RectTransform:
|
|||
m_AnchoredPosition: {x: 0, y: -69.6}
|
||||
m_SizeDelta: {x: 654, y: 40}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!1 &5965395721045599513
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 4234803563267772812}
|
||||
- component: {fileID: 8812723386514772295}
|
||||
- component: {fileID: 9111117998758797823}
|
||||
m_Layer: 5
|
||||
m_Name: Tick_1
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &4234803563267772812
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5965395721045599513}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 7627031014790560531}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.4, y: 0}
|
||||
m_AnchorMax: {x: 0.4, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 7.111111, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &8812723386514772295
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5965395721045599513}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &9111117998758797823
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5965395721045599513}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 0.85}
|
||||
m_RaycastTarget: 0
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 0}
|
||||
m_Type: 0
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
m_FillAmount: 1
|
||||
m_FillClockwise: 1
|
||||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 1
|
||||
--- !u!1 &5989676975516823687
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -107736,6 +108245,7 @@ RectTransform:
|
|||
- {fileID: 8773291827388259920}
|
||||
- {fileID: 2856595641425040628}
|
||||
- {fileID: 2614694673119463930}
|
||||
- {fileID: 4008587971485836497}
|
||||
m_Father: {fileID: 2647977513349308266}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
|
|
@ -123348,6 +123858,81 @@ RectTransform:
|
|||
m_AnchoredPosition: {x: 5, y: -320}
|
||||
m_SizeDelta: {x: 490, y: 80}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!1 &8234802103131574368
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 2628488089120278146}
|
||||
- component: {fileID: 7886232442197427265}
|
||||
- component: {fileID: 8342103284696050257}
|
||||
m_Layer: 5
|
||||
m_Name: Tick_0
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &2628488089120278146
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 8234802103131574368}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 7627031014790560531}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.7, y: 0}
|
||||
m_AnchorMax: {x: 0.7, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 7.111111, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &7886232442197427265
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 8234802103131574368}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &8342103284696050257
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 8234802103131574368}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 0.85}
|
||||
m_RaycastTarget: 0
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 0}
|
||||
m_Type: 0
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
m_FillAmount: 1
|
||||
m_FillClockwise: 1
|
||||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 1
|
||||
--- !u!1 &8237958486829975046
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -168173,12 +168758,12 @@ PrefabInstance:
|
|||
- target: {fileID: 4460242486586314136, guid: c327e063c90438e419668019b3490455,
|
||||
type: 3}
|
||||
propertyPath: m_AnchorMax.y
|
||||
value: 0
|
||||
value: 1
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4460242486586314136, guid: c327e063c90438e419668019b3490455,
|
||||
type: 3}
|
||||
propertyPath: m_AnchorMin.x
|
||||
value: 1
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4460242486586314136, guid: c327e063c90438e419668019b3490455,
|
||||
type: 3}
|
||||
|
|
@ -168233,12 +168818,12 @@ PrefabInstance:
|
|||
- target: {fileID: 4460242486586314136, guid: c327e063c90438e419668019b3490455,
|
||||
type: 3}
|
||||
propertyPath: m_AnchoredPosition.x
|
||||
value: -398
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4460242486586314136, guid: c327e063c90438e419668019b3490455,
|
||||
type: 3}
|
||||
propertyPath: m_AnchoredPosition.y
|
||||
value: 218
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4460242486586314136, guid: c327e063c90438e419668019b3490455,
|
||||
type: 3}
|
||||
|
|
@ -168258,7 +168843,15 @@ PrefabInstance:
|
|||
m_RemovedComponents: []
|
||||
m_RemovedGameObjects: []
|
||||
m_AddedGameObjects: []
|
||||
m_AddedComponents: []
|
||||
m_AddedComponents:
|
||||
- targetCorrespondingSourceObject: {fileID: 4055677999475420392, guid: c327e063c90438e419668019b3490455,
|
||||
type: 3}
|
||||
insertIndex: -1
|
||||
addedObject: {fileID: 4965720817739109040}
|
||||
- targetCorrespondingSourceObject: {fileID: 4055677999475420392, guid: c327e063c90438e419668019b3490455,
|
||||
type: 3}
|
||||
insertIndex: -1
|
||||
addedObject: {fileID: 3187828139587058435}
|
||||
m_SourcePrefab: {fileID: 100100000, guid: c327e063c90438e419668019b3490455, type: 3}
|
||||
--- !u!114 &2351113276399051383 stripped
|
||||
MonoBehaviour:
|
||||
|
|
@ -168266,7 +168859,7 @@ MonoBehaviour:
|
|||
type: 3}
|
||||
m_PrefabInstance: {fileID: 9156483218753586562}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_GameObject: {fileID: 5141691710815562090}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 5334ce64f7c47af4ba3028b1af4e69e5, type: 3}
|
||||
|
|
@ -168278,3 +168871,38 @@ RectTransform:
|
|||
type: 3}
|
||||
m_PrefabInstance: {fileID: 9156483218753586562}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
--- !u!1 &5141691710815562090 stripped
|
||||
GameObject:
|
||||
m_CorrespondingSourceObject: {fileID: 4055677999475420392, guid: c327e063c90438e419668019b3490455,
|
||||
type: 3}
|
||||
m_PrefabInstance: {fileID: 9156483218753586562}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
--- !u!114 &4965720817739109040
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5141691710815562090}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 4f4cdcc41055ee140ae3a9916e8563ce, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::WL.UI.SafeAreaFitter
|
||||
applyHorizontal: 1
|
||||
applyVertical: 1
|
||||
--- !u!114 &3187828139587058435
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5141691710815562090}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: d9504994cdfd4f44f9aa7b4a6eaeb36f, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::WL.UI.WLBattlePadLayout
|
||||
target: {fileID: 2351113276399051383}
|
||||
autoButtonName: btn_auto
|
||||
attackButtonName: Attack
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ public class BattleUI : MonoBehaviour
|
|||
Set_Auto();
|
||||
|
||||
DSUtil.InActivateGameObjects(gos);
|
||||
|
||||
WL.UI.WLBattlePadLayout.NotifySet(this); // WL-813c: 서버 프리셋 재적용 뒤 전투 패드 배치 복원(컴포넌트 없으면 무동작)
|
||||
|
||||
//switch (m_BattleUIType)
|
||||
//{
|
||||
// case eUICardType.UI: gos[1].SetActive(true); break;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,432 @@
|
|||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// BossHpBar.cs — 상단 보스 HP 바 + 페이즈 눈금 (WL-813c · #813)
|
||||
//
|
||||
// 기준서 v1 §B 요소 6 "등장 · 처치 연출": 등장 배너 + 상단 HP 바 + 페이즈 눈금(임계 100/70/40).
|
||||
// §C 요소 6 현행: "상단 보스 HP 바 없음(타겟 패널 슬라이더만)".
|
||||
//
|
||||
// ■ 데이터 출처
|
||||
// 811b 의 WL.Combat.Core.CombatEvents 를 **구독만** 한다 — Spawned(isBoss) / HitConfirmed / Killed.
|
||||
// 발주서 §1-2: "이벤트 미도착 시 폴링 폴백 금지 대신 「미표시」" → 이벤트가 오지 않으면 바는 숨은 채로 둔다.
|
||||
// (Update 에서 Actor 를 찾아다니는 폴백을 두지 않는다. 이벤트 배선 문제를 UI 가 가리면 안 된다.)
|
||||
//
|
||||
// ■ 값
|
||||
// 크기 · 여백 · 눈금 위치 · 색 · 사라짐 지연은 전부 WLHudLayoutSettings 에셋(C45).
|
||||
// 페이즈 **전투 임계값** 자체는 813h(Gameplay)가 테이블로 갖는다 — 여기 값은 **표시용 눈금**이다.
|
||||
//
|
||||
// ■ 숨김 방식
|
||||
// GameObject 를 끄지 않는다(꺼지면 OnEnable 이 안 돌아 구독이 끊긴다).
|
||||
// CanvasGroup alpha/blocksRaycasts 로만 숨긴다.
|
||||
//
|
||||
// 🔴 어셈블리 주의: Assets/WL/UI/ 에 .asmdef 를 만들지 말 것(Assembly-CSharp 단일).
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
using System.Text;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using WL.Combat.Core;
|
||||
|
||||
namespace WL.UI
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
[RequireComponent(typeof(RectTransform))]
|
||||
public class BossHpBar : MonoBehaviour
|
||||
{
|
||||
[Header("구성 요소 (BuildIfNeeded 가 만든다)")]
|
||||
[SerializeField] private CanvasGroup group;
|
||||
[SerializeField] private RectTransform bar;
|
||||
[SerializeField] private Image background;
|
||||
[SerializeField] private Image fill;
|
||||
[SerializeField] private RectTransform tickRoot;
|
||||
[SerializeField] private TextMeshProUGUI nameLabel;
|
||||
|
||||
[Header("이름 라벨 폰트 (에디터가 주입)")]
|
||||
[SerializeField] private TMP_FontAsset nameFont;
|
||||
|
||||
[Tooltip("이름 라벨을 표시한다")]
|
||||
[SerializeField] private bool showName = true;
|
||||
|
||||
private RectTransform _rt;
|
||||
private Canvas _canvas;
|
||||
private Actor _boss;
|
||||
private bool _bound;
|
||||
private float _maxHp, _curHp;
|
||||
private float _hideAt = -1f;
|
||||
private bool _subscribed;
|
||||
|
||||
/// <summary>마지막 갱신 결과 — 프로브(에디터)가 읽는 진단 값.</summary>
|
||||
public float LastRatio { get; private set; }
|
||||
public int SpawnedSeen { get; private set; }
|
||||
public int HitSeen { get; private set; }
|
||||
public int KilledSeen { get; private set; }
|
||||
public bool Visible { get { return group != null && group.alpha > 0f; } }
|
||||
|
||||
private void Awake() { _rt = GetComponent<RectTransform>(); }
|
||||
|
||||
private void OnEnable() { Initialize(); }
|
||||
|
||||
private void OnDisable() { Subscribe(false); }
|
||||
|
||||
/// <summary>
|
||||
/// OnEnable 본체. 에디트 모드에서는 OnEnable 이 불리지 않으므로 검증 스크립트가 이걸 직접 부른다
|
||||
/// (런타임과 **완전히 같은 경로**를 타야 검증이 의미가 있다).
|
||||
/// </summary>
|
||||
public string Initialize()
|
||||
{
|
||||
if (_rt == null) _rt = GetComponent<RectTransform>();
|
||||
BuildIfNeeded();
|
||||
string layout = ApplyLayout();
|
||||
HideNow();
|
||||
Subscribe(true);
|
||||
return "initialized subscribed=" + _subscribed + " · " + layout;
|
||||
}
|
||||
|
||||
/// <summary>구독 등록/해제. OnEnable/OnDisable 이 부르고, 에디터 검증도 같은 것을 쓴다.</summary>
|
||||
public void Subscribe(bool on)
|
||||
{
|
||||
if (on == _subscribed) return;
|
||||
_subscribed = on;
|
||||
if (on)
|
||||
{
|
||||
CombatEvents.Spawned.Add(OnSpawned);
|
||||
CombatEvents.HitConfirmed.Add(OnHitConfirmed);
|
||||
CombatEvents.Killed.Add(OnKilled);
|
||||
}
|
||||
else
|
||||
{
|
||||
CombatEvents.Spawned.Remove(OnSpawned);
|
||||
CombatEvents.HitConfirmed.Remove(OnHitConfirmed);
|
||||
CombatEvents.Killed.Remove(OnKilled);
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (_hideAt >= 0f && Time.unscaledTime >= _hideAt) HideNow();
|
||||
}
|
||||
|
||||
// ── 이벤트 구독 (811b CombatEvents) ───────────────────────────────────
|
||||
private void OnSpawned(in SpawnedEvent e)
|
||||
{
|
||||
SpawnedSeen++;
|
||||
if (!e.isBoss) return;
|
||||
Bind(e.actor, null, DebugFallbackMaxHp);
|
||||
}
|
||||
|
||||
private void OnHitConfirmed(in HitConfirmedEvent e)
|
||||
{
|
||||
if (!IsTracked(e.victim)) return;
|
||||
HitSeen++;
|
||||
float hp = ReadHp(e.victim);
|
||||
_curHp = hp >= 0f ? hp : Mathf.Max(0f, _curHp - (float)e.damage);
|
||||
SetRatio(_maxHp > 0f ? _curHp / _maxHp : 0f);
|
||||
}
|
||||
|
||||
private void OnKilled(in KilledEvent e)
|
||||
{
|
||||
if (!IsTracked(e.victim)) return;
|
||||
KilledSeen++;
|
||||
_curHp = 0f;
|
||||
SetRatio(0f);
|
||||
var s = WLHudLayoutSettings.Instance;
|
||||
_hideAt = Time.unscaledTime + (s != null ? s.bossBarHideDelaySec : 0f);
|
||||
}
|
||||
|
||||
// 합성(에디터 검증) 이벤트는 actor 가 null 이다 — null == null 로 같은 경로를 탄다.
|
||||
private bool IsTracked(Actor a) { return _bound && ReferenceEquals(a, _boss); }
|
||||
|
||||
private static float ReadHp(Actor a)
|
||||
{
|
||||
if (a == null) return -1f;
|
||||
var st = a.Get_StatInfo();
|
||||
if (st == null) return -1f;
|
||||
return (float)st.Get_Stat(eStat.HP);
|
||||
}
|
||||
|
||||
private static float ReadMaxHp(Actor a)
|
||||
{
|
||||
if (a == null) return -1f;
|
||||
var st = a.Get_StatInfo();
|
||||
if (st == null) return -1f;
|
||||
return (float)st.Get_Stat(eStat.MaxHP);
|
||||
}
|
||||
|
||||
/// <summary>보스를 바에 물린다. actor 가 null 이면 합성 검증 모드(최대 HP 는 fallbackMaxHp).</summary>
|
||||
public string Bind(Actor actor, string displayName = null, float fallbackMaxHp = 100f)
|
||||
{
|
||||
var s = WLHudLayoutSettings.Instance;
|
||||
if (s != null && !s.bossBarEnabled) return "bossBarEnabled=false — 미표시";
|
||||
|
||||
_boss = actor;
|
||||
_bound = true;
|
||||
_hideAt = -1f;
|
||||
|
||||
float max = ReadMaxHp(actor);
|
||||
_maxHp = max > 0f ? max : Mathf.Max(1f, fallbackMaxHp);
|
||||
float cur = ReadHp(actor);
|
||||
_curHp = cur >= 0f ? cur : _maxHp;
|
||||
|
||||
if (nameLabel != null)
|
||||
{
|
||||
string n = displayName;
|
||||
if (string.IsNullOrEmpty(n)) n = actor != null ? actor.name : "";
|
||||
nameLabel.text = n;
|
||||
nameLabel.gameObject.SetActive(showName && !string.IsNullOrEmpty(n));
|
||||
}
|
||||
|
||||
ApplyLayout();
|
||||
ShowNow();
|
||||
SetRatio(_maxHp > 0f ? _curHp / _maxHp : 0f);
|
||||
return "bound name=" + (nameLabel != null ? nameLabel.text : "-") +
|
||||
" maxHp=" + _maxHp.ToString("F0") + " ratio=" + LastRatio.ToString("F3");
|
||||
}
|
||||
|
||||
/// <summary>바 채움 비율(0~1). 눈금은 고정이고 fill 만 움직인다.</summary>
|
||||
public void SetRatio(float ratio)
|
||||
{
|
||||
LastRatio = Mathf.Clamp01(ratio);
|
||||
if (fill != null) fill.fillAmount = LastRatio;
|
||||
}
|
||||
|
||||
private void ShowNow()
|
||||
{
|
||||
if (group == null) return;
|
||||
group.alpha = 1f; group.blocksRaycasts = false; group.interactable = false;
|
||||
}
|
||||
|
||||
private void HideNow()
|
||||
{
|
||||
_bound = false; _boss = null; _hideAt = -1f;
|
||||
if (group == null) return;
|
||||
group.alpha = 0f; group.blocksRaycasts = false; group.interactable = false;
|
||||
}
|
||||
|
||||
// ── 구성 · 배치 ───────────────────────────────────────────────────────
|
||||
/// <summary>없는 자식만 만든다(에디터 오소링과 런타임이 같은 코드를 쓴다). 이미 있으면 아무 것도 하지 않는다.</summary>
|
||||
public bool BuildIfNeeded()
|
||||
{
|
||||
if (_rt == null) _rt = GetComponent<RectTransform>();
|
||||
bool made = false;
|
||||
|
||||
if (group == null) { group = GetComponent<CanvasGroup>(); if (group == null) { group = gameObject.AddComponent<CanvasGroup>(); made = true; } }
|
||||
|
||||
if (bar == null)
|
||||
{
|
||||
bar = NewChild(_rt, "Bar");
|
||||
made = true;
|
||||
}
|
||||
if (background == null)
|
||||
{
|
||||
var bg = NewChild(bar, "Bg");
|
||||
Stretch(bg);
|
||||
background = bg.gameObject.AddComponent<Image>();
|
||||
background.raycastTarget = false;
|
||||
made = true;
|
||||
}
|
||||
if (fill == null)
|
||||
{
|
||||
var f = NewChild(bar, "Fill");
|
||||
Stretch(f);
|
||||
fill = f.gameObject.AddComponent<Image>();
|
||||
fill.raycastTarget = false;
|
||||
fill.type = Image.Type.Filled;
|
||||
fill.fillMethod = Image.FillMethod.Horizontal;
|
||||
fill.fillOrigin = (int)Image.OriginHorizontal.Left;
|
||||
fill.fillAmount = 1f;
|
||||
made = true;
|
||||
}
|
||||
if (tickRoot == null)
|
||||
{
|
||||
tickRoot = NewChild(bar, "Ticks");
|
||||
Stretch(tickRoot);
|
||||
made = true;
|
||||
}
|
||||
if (nameLabel == null)
|
||||
{
|
||||
var n = NewChild(_rt, "Name");
|
||||
nameLabel = n.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
nameLabel.raycastTarget = false;
|
||||
nameLabel.alignment = TextAlignmentOptions.Center;
|
||||
nameLabel.text = "";
|
||||
if (nameFont != null) nameLabel.font = nameFont;
|
||||
made = true;
|
||||
}
|
||||
return made;
|
||||
}
|
||||
|
||||
/// <summary>설정 값(px)을 캔버스 유닛으로 환산해 바·눈금·색을 맞춘다.</summary>
|
||||
public string ApplyLayout()
|
||||
{
|
||||
if (_rt == null) _rt = GetComponent<RectTransform>();
|
||||
var s = WLHudLayoutSettings.Instance;
|
||||
if (s == null) return "WLHudLayoutSettings 에셋 없음 — 배치 건너뜀";
|
||||
if (bar == null) return "구성 요소 없음 — BuildIfNeeded 먼저";
|
||||
|
||||
float u = ComputeUnitsPerPx(s);
|
||||
|
||||
// 루트 = Safe Area 상단 가로 스트레치 (부모는 SafeAreaFitter 가 붙은 WL_HUD)
|
||||
_rt.anchorMin = new Vector2(0.5f, 1f);
|
||||
_rt.anchorMax = new Vector2(0.5f, 1f);
|
||||
_rt.pivot = new Vector2(0.5f, 1f);
|
||||
_rt.anchoredPosition = new Vector2(0f, -s.bossBarTopMarginPx * u);
|
||||
float nameH = s.bossBarNameFontPx + s.bossBarNameGapPx;
|
||||
_rt.sizeDelta = new Vector2(s.bossBarSizePx.x * u, (s.bossBarSizePx.y + nameH) * u);
|
||||
_rt.localScale = Vector3.one;
|
||||
|
||||
if (nameLabel != null)
|
||||
{
|
||||
var nrt = nameLabel.rectTransform;
|
||||
nrt.anchorMin = new Vector2(0f, 1f); nrt.anchorMax = new Vector2(1f, 1f);
|
||||
nrt.pivot = new Vector2(0.5f, 1f);
|
||||
nrt.offsetMin = new Vector2(0f, 0f); nrt.offsetMax = new Vector2(0f, 0f);
|
||||
nrt.anchoredPosition = Vector2.zero;
|
||||
nrt.sizeDelta = new Vector2(0f, s.bossBarNameFontPx * u);
|
||||
nameLabel.fontSize = s.bossBarNameFontPx * u;
|
||||
nameLabel.color = s.bossBarNameColor;
|
||||
if (nameFont != null && nameLabel.font != nameFont) nameLabel.font = nameFont;
|
||||
}
|
||||
|
||||
bar.anchorMin = new Vector2(0f, 0f); bar.anchorMax = new Vector2(1f, 0f);
|
||||
bar.pivot = new Vector2(0.5f, 0f);
|
||||
bar.anchoredPosition = Vector2.zero;
|
||||
bar.sizeDelta = new Vector2(0f, s.bossBarSizePx.y * u);
|
||||
|
||||
if (background != null) background.color = s.bossBarBgColor;
|
||||
if (fill != null) fill.color = s.bossBarFillColor;
|
||||
|
||||
RebuildTicks(s, u);
|
||||
return "layout size=" + _rt.sizeDelta + " unitsPerPx=" + u.ToString("F4") +
|
||||
" ticks=" + (s.bossPhaseTicks != null ? s.bossPhaseTicks.Length : 0);
|
||||
}
|
||||
|
||||
private void RebuildTicks(WLHudLayoutSettings s, float u)
|
||||
{
|
||||
if (tickRoot == null) return;
|
||||
int want = s.bossPhaseTicks != null ? s.bossPhaseTicks.Length : 0;
|
||||
|
||||
// 남는 눈금은 지운다(설정에서 개수를 줄였을 때).
|
||||
for (int i = tickRoot.childCount - 1; i >= want; i--)
|
||||
{
|
||||
var extra = tickRoot.GetChild(i).gameObject;
|
||||
if (Application.isPlaying) Destroy(extra); else DestroyImmediate(extra);
|
||||
}
|
||||
for (int i = tickRoot.childCount; i < want; i++)
|
||||
{
|
||||
var t = NewChild(tickRoot, "Tick_" + i);
|
||||
var img = t.gameObject.AddComponent<Image>();
|
||||
img.raycastTarget = false;
|
||||
}
|
||||
for (int i = 0; i < want; i++)
|
||||
{
|
||||
var t = tickRoot.GetChild(i) as RectTransform;
|
||||
if (t == null) continue;
|
||||
float at = Mathf.Clamp01(s.bossPhaseTicks[i]);
|
||||
t.anchorMin = new Vector2(at, 0f);
|
||||
t.anchorMax = new Vector2(at, 1f);
|
||||
t.pivot = new Vector2(0.5f, 0.5f);
|
||||
t.anchoredPosition = Vector2.zero;
|
||||
t.sizeDelta = new Vector2(s.bossBarTickWidthPx * u, 0f);
|
||||
t.localScale = Vector3.one;
|
||||
var img = t.GetComponent<Image>();
|
||||
if (img != null) img.color = s.bossBarTickColor;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>px → 캔버스 유닛 계수. 산식은 WLHudLayoutSettings 가 갖는다(전투 패드와 같은 것을 쓴다).</summary>
|
||||
public float ComputeUnitsPerPx(WLHudLayoutSettings s)
|
||||
{
|
||||
if (_canvas == null) _canvas = GetComponentInParent<Canvas>(true);
|
||||
return s.UnitsPerPx(_canvas);
|
||||
}
|
||||
|
||||
private static RectTransform NewChild(RectTransform parent, string childName)
|
||||
{
|
||||
var go = new GameObject(childName, typeof(RectTransform));
|
||||
go.layer = parent != null ? parent.gameObject.layer : 5;
|
||||
var rt = (RectTransform)go.transform;
|
||||
rt.SetParent(parent, false);
|
||||
rt.localScale = Vector3.one;
|
||||
return rt;
|
||||
}
|
||||
|
||||
private static void Stretch(RectTransform rt)
|
||||
{
|
||||
rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one;
|
||||
rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero;
|
||||
rt.pivot = new Vector2(0.5f, 0.5f);
|
||||
}
|
||||
|
||||
/// <summary>에디터가 폰트를 주입한다(런타임 로드 없음).</summary>
|
||||
public void SetNameFont(TMP_FontAsset font)
|
||||
{
|
||||
nameFont = font;
|
||||
if (nameLabel != null && font != null) nameLabel.font = font;
|
||||
}
|
||||
|
||||
// ── 검증 전용 (발주서 ⓒ) ─────────────────────────────────────────────
|
||||
/// <summary>
|
||||
/// 가짜 전투 이벤트를 **실제 CombatEvents 경로로** 발행한다(Assembly-CSharp 내부라 Dispatch 접근 가능).
|
||||
/// Play 없이 구독 → 갱신을 확인하는 용도. actor 가 없으므로 페이로드의 actor/victim 은 null 이다.
|
||||
/// </summary>
|
||||
public static float DebugFallbackMaxHp = 100f; // 합성 이벤트 전용(actor 가 없을 때의 최대 HP)
|
||||
|
||||
public static string RaiseFakeBossSpawned(float maxHp)
|
||||
{
|
||||
DebugFallbackMaxHp = Mathf.Max(1f, maxHp);
|
||||
var e = new SpawnedEvent { actor = null, isBoss = true, isElite = false, position = Vector3.zero, time = Time.unscaledTime, frame = Time.frameCount };
|
||||
CombatEvents.Spawned.Dispatch(in e);
|
||||
return "dispatched Spawned(isBoss=true) subscribers=" + CombatEvents.Spawned.Count;
|
||||
}
|
||||
|
||||
/// <summary>비보스 스폰 — 바가 반응하지 않아야 한다(회귀 확인).</summary>
|
||||
public static string RaiseFakeNonBossSpawned()
|
||||
{
|
||||
var e = new SpawnedEvent { actor = null, isBoss = false, isElite = false, position = Vector3.zero, time = Time.unscaledTime, frame = Time.frameCount };
|
||||
CombatEvents.Spawned.Dispatch(in e);
|
||||
return "dispatched Spawned(isBoss=false) subscribers=" + CombatEvents.Spawned.Count;
|
||||
}
|
||||
|
||||
public static string RaiseFakeHit(double damage)
|
||||
{
|
||||
var e = new HitConfirmedEvent { victim = null, attacker = null, dinfo = null, damage = damage, critical = false, isKill = false, byMainPC = true, time = Time.unscaledTime, frame = Time.frameCount };
|
||||
CombatEvents.HitConfirmed.Dispatch(in e);
|
||||
return "dispatched HitConfirmed(dmg=" + damage + ") subscribers=" + CombatEvents.HitConfirmed.Count;
|
||||
}
|
||||
|
||||
public static string RaiseFakeKilled()
|
||||
{
|
||||
var e = new KilledEvent { victim = null, killer = null, id = 0, position = Vector3.zero, byDirectHit = true, time = Time.unscaledTime, frame = Time.frameCount };
|
||||
CombatEvents.Killed.Dispatch(in e);
|
||||
return "dispatched Killed subscribers=" + CombatEvents.Killed.Count;
|
||||
}
|
||||
|
||||
/// <summary>현재 상태 덤프. 발주서 ⓒ 증거용.</summary>
|
||||
public string Dump()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
if (_rt == null) _rt = GetComponent<RectTransform>();
|
||||
var s = WLHudLayoutSettings.Instance;
|
||||
sb.AppendLine("[BossHpBar] subscribed=" + _subscribed +
|
||||
" visible=" + Visible + " ratio=" + LastRatio.ToString("F3") +
|
||||
" hp=" + _curHp.ToString("F0") + "/" + _maxHp.ToString("F0") +
|
||||
" seen(Spawned/Hit/Killed)=" + SpawnedSeen + "/" + HitSeen + "/" + KilledSeen);
|
||||
sb.AppendLine(" CombatEvents subscribers Spawned=" + CombatEvents.Spawned.Count +
|
||||
" HitConfirmed=" + CombatEvents.HitConfirmed.Count +
|
||||
" Killed=" + CombatEvents.Killed.Count + " Enabled=" + CombatEvents.Enabled);
|
||||
sb.AppendLine(" root " + (_rt != null ? "pos" + _rt.anchoredPosition + " size" + _rt.sizeDelta + " aMin" + _rt.anchorMin + " aMax" + _rt.anchorMax : "(없음)"));
|
||||
sb.AppendLine(" bar " + (bar != null ? "size" + bar.sizeDelta + " aMin" + bar.anchorMin + " aMax" + bar.anchorMax : "(없음)") +
|
||||
" fill=" + (fill != null ? fill.fillAmount.ToString("F3") : "-") +
|
||||
" ticks=" + (tickRoot != null ? tickRoot.childCount : 0));
|
||||
if (tickRoot != null)
|
||||
for (int i = 0; i < tickRoot.childCount; i++)
|
||||
{
|
||||
var t = tickRoot.GetChild(i) as RectTransform;
|
||||
if (t != null) sb.AppendLine(" " + t.name + " at=" + t.anchorMin.x.ToString("F3") + " w=" + t.sizeDelta.x.ToString("F1"));
|
||||
}
|
||||
sb.AppendLine(" 설정 " + (s == null ? "(에셋 없음)" :
|
||||
"sizePx=" + s.bossBarSizePx + " topMarginPx=" + s.bossBarTopMarginPx +
|
||||
" ticks=" + (s.bossPhaseTicks != null ? string.Join(",", System.Array.ConvertAll(s.bossPhaseTicks, v => v.ToString("F2"))) : "-")));
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5759d44fea68d204697e4b572af27bde
|
||||
|
|
@ -0,0 +1,275 @@
|
|||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// WLBattlePadLayout.cs — 전투 패드(우하단 엄지 부채꼴) 배치 드라이버 (WL-813c · #813)
|
||||
//
|
||||
// 기준서 v1 §F-1 권고: 오른손 엄지 부채꼴 **4슬롯** + 물약 1 + 회피 1 · 좌우 미러 옵션.
|
||||
// §B 요소 3: 버튼 지름 132~150 px · 중심 간격 ≥16 px · 요소 7: Safe Area 안 · 한 손 도달.
|
||||
//
|
||||
// ■ 설계
|
||||
// · 원본 BattleUI.prefab 의 **배열을 줄이지 않는다**(skillCards 6 그대로 · 원복 가능).
|
||||
// 활성 슬롯 수만 설정으로 정하고, 남는 슬롯은 물약/회피(813j/813r) 자리로 **비활성**으로 둔다.
|
||||
// · 값은 전부 WLHudLayoutSettings(에셋). 이 파일에 좌표·지름·각도 상수는 없다(C45).
|
||||
// · 배치는 런타임에만 한다([ExecuteAlways] 아님) — 프리팹 YAML 에 좌표를 굽지 않아
|
||||
// NewGameUI.prefab diff 를 "노드/컴포넌트 추가"로만 유지한다(발주서 ⓓ).
|
||||
// · 이 컴포넌트는 **NewGameUI 안 IngameUIs/BattleUI 인스턴스에만** 붙인다.
|
||||
// 같은 BattleUI.prefab 을 쓰는 SkillUI/SkillEquipUI 인스턴스는 손대지 않는다(회귀 0).
|
||||
//
|
||||
// ■ Safe Area
|
||||
// 같은 오브젝트의 SafeAreaFitter(802b)가 루트를 안전 영역에 맞춘다. 이 스크립트는 루트 rect 를
|
||||
// 건드리지 않고(피터가 주인) 자식만 **우하단(미러 시 좌하단) 모서리 앵커**로 배치한다.
|
||||
// 피터가 없으면(단독 테스트) 루트를 스트레치로 정규화한다.
|
||||
//
|
||||
// ■ 롤백(C8)
|
||||
// 설정 에셋이 없거나 battlePadEnabled=false 면 아무 것도 만지지 않는다 = 원본 배치 그대로.
|
||||
//
|
||||
// 🔴 어셈블리 주의: Assets/WL/UI/ 에 .asmdef 를 만들지 말 것(Assembly-CSharp 단일).
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace WL.UI
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
[RequireComponent(typeof(RectTransform))]
|
||||
public class WLBattlePadLayout : MonoBehaviour
|
||||
{
|
||||
[Tooltip("배치 대상 — 비우면 같은 오브젝트의 BattleUI 를 찾는다")]
|
||||
[SerializeField] private BattleUI target;
|
||||
|
||||
[Tooltip("자동전투 토글 오브젝트 이름(BattleUI 자식) — 못 찾으면 건너뛴다")]
|
||||
[SerializeField] private string autoButtonName = "btn_auto";
|
||||
|
||||
[Tooltip("공격 버튼 오브젝트 이름(BattleUI 자식) — 못 찾으면 건너뛴다")]
|
||||
[SerializeField] private string attackButtonName = "Attack";
|
||||
|
||||
private RectTransform _rt;
|
||||
private Canvas _canvas;
|
||||
private bool _lastMirror;
|
||||
private Vector2Int _lastScreen;
|
||||
|
||||
// 마지막 적용 결과 — 프로브(에디터)가 읽는 진단 값. 실측 외에는 채우지 않는다.
|
||||
public float LastUnitsPerPx { get; private set; }
|
||||
public float LastMaxReachPx { get; private set; }
|
||||
public float LastMinGapPx { get; private set; }
|
||||
public int LastActiveSlots { get; private set; }
|
||||
public int LastReserveSlots { get; private set; }
|
||||
|
||||
private void Awake() { _rt = GetComponent<RectTransform>(); }
|
||||
private void OnEnable() { Apply(); }
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// 해상도 전환·설정 토글에만 다시 배치한다(매 프레임 계산 없음).
|
||||
var s = WLHudLayoutSettings.Instance;
|
||||
if (s == null) return;
|
||||
if (Screen.width != _lastScreen.x || Screen.height != _lastScreen.y || s.mirrorLeftHanded != _lastMirror)
|
||||
Apply();
|
||||
}
|
||||
|
||||
/// <summary>BattleUI.Set() 직후 훅 — 서버 프리셋이 슬롯을 다시 채운 뒤 배치를 되살린다.</summary>
|
||||
public static void NotifySet(BattleUI ui)
|
||||
{
|
||||
if (ui == null) return;
|
||||
var layout = ui.GetComponent<WLBattlePadLayout>();
|
||||
if (layout != null && layout.isActiveAndEnabled) layout.Apply();
|
||||
}
|
||||
|
||||
/// <summary>설정 값대로 슬롯·공격·자동 버튼을 재배치한다. 설정이 없으면 아무 것도 하지 않는다.</summary>
|
||||
public string Apply()
|
||||
{
|
||||
if (_rt == null) _rt = GetComponent<RectTransform>();
|
||||
if (target == null) target = GetComponent<BattleUI>();
|
||||
|
||||
var s = WLHudLayoutSettings.Instance;
|
||||
if (s == null) return "WLHudLayoutSettings 에셋 없음 — 배치 건너뜀(원본 유지)";
|
||||
if (!s.battlePadEnabled) return "battlePadEnabled=false — 배치 건너뜀(원본 유지)";
|
||||
if (target == null) return "BattleUI 없음 — 배치 건너뜀";
|
||||
|
||||
_lastScreen = new Vector2Int(Screen.width, Screen.height);
|
||||
_lastMirror = s.mirrorLeftHanded;
|
||||
|
||||
// Safe Area 는 SafeAreaFitter 가 주인 — 없을 때만 루트를 스트레치로 정규화한다.
|
||||
if (GetComponent<SafeAreaFitter>() == null)
|
||||
{
|
||||
_rt.anchorMin = Vector2.zero; _rt.anchorMax = Vector2.one;
|
||||
_rt.offsetMin = Vector2.zero; _rt.offsetMax = Vector2.zero;
|
||||
}
|
||||
|
||||
float u = ComputeUnitsPerPx(s);
|
||||
LastUnitsPerPx = u;
|
||||
|
||||
var cards = target.skillCards;
|
||||
int total = cards != null ? cards.Length : 0;
|
||||
int active = Mathf.Clamp(s.activeSkillSlots, 0, total);
|
||||
LastActiveSlots = active;
|
||||
LastReserveSlots = Mathf.Max(0, total - active);
|
||||
|
||||
float maxReach = 0f;
|
||||
var centers = new Vector2[total];
|
||||
|
||||
for (int i = 0; i < total; i++)
|
||||
{
|
||||
var card = cards[i];
|
||||
if (card == null) continue;
|
||||
var crt = card.transform as RectTransform;
|
||||
if (crt == null) continue;
|
||||
|
||||
bool isSkill = i < active;
|
||||
Vector2 px = isSkill ? s.FanSlotPx(i) : s.ReserveSlotPx(i - active);
|
||||
float dia = isSkill ? s.slotDiameterPx : s.reserveDiameterPx;
|
||||
|
||||
PlaceFromCorner(crt, px, dia, u, s.mirrorLeftHanded);
|
||||
centers[i] = px;
|
||||
maxReach = Mathf.Max(maxReach, px.magnitude + dia * 0.5f);
|
||||
|
||||
// 4 활성 · 나머지는 물약/회피(813j/813r) 예약 자리 = 비활성
|
||||
if (card.gameObject.activeSelf != isSkill) card.gameObject.SetActive(isSkill);
|
||||
}
|
||||
|
||||
// 공격 버튼 · 자동전투 토글
|
||||
var attack = FindChild(attackButtonName);
|
||||
if (attack != null)
|
||||
{
|
||||
PlaceFromCorner(attack, s.attackCenterPx, s.attackDiameterPx, u, s.mirrorLeftHanded);
|
||||
maxReach = Mathf.Max(maxReach, s.attackCenterPx.magnitude + s.attackDiameterPx * 0.5f);
|
||||
}
|
||||
var auto = FindChild(autoButtonName);
|
||||
if (auto != null)
|
||||
{
|
||||
PlaceFromCorner(auto, s.autoButtonCenterPx, s.autoButtonDiameterPx, u, s.mirrorLeftHanded);
|
||||
maxReach = Mathf.Max(maxReach, s.autoButtonCenterPx.magnitude + s.autoButtonDiameterPx * 0.5f);
|
||||
}
|
||||
|
||||
LastMaxReachPx = maxReach;
|
||||
LastMinGapPx = MinGapPx(s, centers, active, total);
|
||||
|
||||
return "applied slots=" + active + "/" + total + " unitsPerPx=" + u.ToString("F4") +
|
||||
" maxReachPx=" + maxReach.ToString("F1") + " minGapPx=" + LastMinGapPx.ToString("F1") +
|
||||
" mirror=" + s.mirrorLeftHanded;
|
||||
}
|
||||
|
||||
/// <summary>모서리(우하단 · 미러면 좌하단) 기준 (dx, dy) px 로 배치한다.</summary>
|
||||
private static void PlaceFromCorner(RectTransform rt, Vector2 px, float diameterPx, float unitsPerPx, bool mirror)
|
||||
{
|
||||
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 * unitsPerPx, px.y * unitsPerPx);
|
||||
rt.sizeDelta = new Vector2(diameterPx * unitsPerPx, diameterPx * unitsPerPx);
|
||||
rt.localScale = Vector3.one;
|
||||
}
|
||||
|
||||
/// <summary>이웃 슬롯 중심 거리 − 지름 = 실제 여백(px). 기준서 §B 요소3 ≥16 검증용.</summary>
|
||||
private static float MinGapPx(WLHudLayoutSettings s, Vector2[] centers, int active, int total)
|
||||
{
|
||||
float min = float.MaxValue;
|
||||
for (int i = 0; i < total; i++)
|
||||
{
|
||||
for (int j = i + 1; j < total; j++)
|
||||
{
|
||||
float ri = (i < active ? s.slotDiameterPx : s.reserveDiameterPx) * 0.5f;
|
||||
float rj = (j < active ? s.slotDiameterPx : s.reserveDiameterPx) * 0.5f;
|
||||
float gap = Vector2.Distance(centers[i], centers[j]) - ri - rj;
|
||||
if (gap < min) min = gap;
|
||||
}
|
||||
}
|
||||
return min == float.MaxValue ? 0f : min;
|
||||
}
|
||||
|
||||
/// <summary>px → 캔버스 유닛 계수. 산식은 WLHudLayoutSettings 가 갖는다(보스 바와 같은 것을 쓴다).</summary>
|
||||
public float ComputeUnitsPerPx(WLHudLayoutSettings s)
|
||||
{
|
||||
if (_canvas == null) _canvas = GetComponentInParent<Canvas>(true);
|
||||
return s.UnitsPerPx(_canvas);
|
||||
}
|
||||
|
||||
private RectTransform FindChild(string childName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(childName)) return null;
|
||||
for (int i = 0; i < transform.childCount; i++)
|
||||
{
|
||||
var c = transform.GetChild(i);
|
||||
if (c.name == childName) return c as RectTransform;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── 검증·디버그 전용 ──────────────────────────────────────────────────
|
||||
/// <summary>런타임 미러 토글(로드된 설정 인스턴스만 바꾼다 — .asset 파일은 건드리지 않는다).</summary>
|
||||
public string SetMirror(bool mirror)
|
||||
{
|
||||
var s = WLHudLayoutSettings.Instance;
|
||||
if (s == null) return "WLHudLayoutSettings 에셋 없음";
|
||||
s.mirrorLeftHanded = mirror;
|
||||
return Apply();
|
||||
}
|
||||
|
||||
/// <summary>현재 배치 덤프(슬롯별 앵커·좌표·크기 · px 환산). 발주서 ⓑ 증거용.</summary>
|
||||
public string Dump()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var s = WLHudLayoutSettings.Instance;
|
||||
float u = LastUnitsPerPx > 0f ? LastUnitsPerPx : (s != null ? ComputeUnitsPerPx(s) : 1f);
|
||||
sb.AppendLine("[WLBattlePadLayout] unitsPerPx=" + u.ToString("F4") +
|
||||
" mirror=" + (s != null ? s.mirrorLeftHanded.ToString() : "?") +
|
||||
" activeSlots=" + LastActiveSlots + "/" + (LastActiveSlots + LastReserveSlots) +
|
||||
" maxReachPx=" + LastMaxReachPx.ToString("F1") +
|
||||
" (목표 " + (s != null ? s.thumbReachRadiusPx.ToString("F0") : "?") + ")" +
|
||||
" minGapPx=" + LastMinGapPx.ToString("F1"));
|
||||
sb.AppendLine(" root " + RectLine(_rt != null ? _rt : GetComponent<RectTransform>()) +
|
||||
" safeAreaFitter=" + (GetComponent<SafeAreaFitter>() != null));
|
||||
|
||||
var ui = target != null ? target : GetComponent<BattleUI>();
|
||||
if (ui != null && ui.skillCards != null)
|
||||
{
|
||||
for (int i = 0; i < ui.skillCards.Length; i++)
|
||||
{
|
||||
var c = ui.skillCards[i];
|
||||
if (c == null) { sb.AppendLine(" slot" + i + " : null"); continue; }
|
||||
var crt = c.transform as RectTransform;
|
||||
sb.AppendLine(" slot" + i + " " + c.name + " active=" + c.gameObject.activeSelf +
|
||||
" " + RectLine(crt) + " " + PxLine(crt, u));
|
||||
sb.AppendLine(" 쿨링 " + CoolRingLine(c));
|
||||
}
|
||||
}
|
||||
foreach (var n in new[] { attackButtonName, autoButtonName, "Avoid", "Equip" })
|
||||
{
|
||||
var t = FindChild(n);
|
||||
sb.AppendLine(" " + n + (t == null ? " : 없음"
|
||||
: " active=" + t.gameObject.activeSelf + " " + RectLine(t) + " " + PxLine(t, u)));
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>쿨 링 실측 — SkillCard 원본의 i_cooltime(Filled/Radial360)이 배치 뒤에도 유효한지(발주서 ⓑ "쿨 링").</summary>
|
||||
private static string CoolRingLine(SkillCard card)
|
||||
{
|
||||
var img = card != null ? card.sprite_cooltime : null;
|
||||
if (img == null) return "sprite_cooltime 없음";
|
||||
var irt = img.rectTransform;
|
||||
return img.gameObject.name + " type=" + img.type + " fillMethod=" + img.fillMethod +
|
||||
" origin=" + img.fillOrigin + " cw=" + img.fillClockwise + " amount=" + img.fillAmount.ToString("F3") +
|
||||
" active=" + img.gameObject.activeInHierarchy +
|
||||
" aMin" + irt.anchorMin + " aMax" + irt.anchorMax + " size" + irt.sizeDelta +
|
||||
" · 구동 = PCActor.cs:91 Set_CoolTime → CardCoolTimeBase.Set_CoolTimeText";
|
||||
}
|
||||
|
||||
private static string RectLine(RectTransform rt)
|
||||
{
|
||||
if (rt == null) return "(rt 없음)";
|
||||
return "aMin" + rt.anchorMin + " aMax" + rt.anchorMax + " pos" + rt.anchoredPosition +
|
||||
" size" + rt.sizeDelta + " piv" + rt.pivot + " scl" + (Vector2)(Vector3)rt.localScale;
|
||||
}
|
||||
|
||||
private static string PxLine(RectTransform rt, float u)
|
||||
{
|
||||
if (rt == null || u <= 0f) return "";
|
||||
return "→ px(dx=" + Mathf.Abs(rt.anchoredPosition.x / u).ToString("F1") +
|
||||
", dy=" + (rt.anchoredPosition.y / u).ToString("F1") +
|
||||
", d=" + (rt.sizeDelta.x / u).ToString("F1") + ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: d9504994cdfd4f44f9aa7b4a6eaeb36f
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: f4aeda4b4f3437e46a49beefb0fd6755
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 71749f48b532a4a48b420ea15652dc92
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 0c51828552a79704da68e37718b523ab
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
%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: 55115957ebcbb1e498a857d9f9bb17f8, type: 3}
|
||||
m_Name: WLHudLayoutSettings
|
||||
m_EditorClassIdentifier: Assembly-CSharp::WL.UI.WLHudLayoutSettings
|
||||
designWidthPx: 1080
|
||||
designHeightPx: 1920
|
||||
battlePadEnabled: 1
|
||||
mirrorLeftHanded: 0
|
||||
activeSkillSlots: 4
|
||||
slotDiameterPx: 140
|
||||
slotMinGapPx: 16
|
||||
thumbReachRadiusPx: 320
|
||||
fanOriginPx: {x: 0, y: 0}
|
||||
fanRadiusPx: 420
|
||||
fanStartAngleDeg: 12.5
|
||||
fanStepAngleDeg: 22
|
||||
reserveDiameterPx: 110
|
||||
reserveRadiusPx: 270
|
||||
reserveStartAngleDeg: 25
|
||||
reserveStepAngleDeg: 40
|
||||
attackCenterPx: {x: 100, y: 100}
|
||||
attackDiameterPx: 132
|
||||
autoButtonCenterPx: {x: 56, y: 545}
|
||||
autoButtonDiameterPx: 34.3
|
||||
joystickCenterPx: {x: 180, y: 260}
|
||||
joystickRadiusPx: 90
|
||||
bossBarEnabled: 1
|
||||
bossBarSizePx: {x: 880, y: 28}
|
||||
bossBarTopMarginPx: 190
|
||||
bossBarNameFontPx: 30
|
||||
bossBarNameGapPx: 8
|
||||
bossBarTickWidthPx: 4
|
||||
bossPhaseTicks:
|
||||
- 0.7
|
||||
- 0.4
|
||||
bossBarHideDelaySec: 1.2
|
||||
bossBarFillColor: {r: 0.99, g: 0.26, b: 0.28, a: 1}
|
||||
bossBarBgColor: {r: 0.05, g: 0.05, b: 0.06, a: 0.78}
|
||||
bossBarTickColor: {r: 1, g: 1, b: 1, a: 0.85}
|
||||
bossBarNameColor: {r: 1, g: 0.92, b: 0.6, a: 1}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e66cc0e7d91be4f48b570e1691776ece
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// WLHudLayoutSettings.cs — 전투 HUD 배치 값의 단일 출처 (WL-813c · #813)
|
||||
//
|
||||
// 기준서 v1 §B 요소 3(슬롯 수·배치·지름·간격) · 요소 6(상단 보스 HP 바·페이즈) · 요소 7(한 손 도달성)
|
||||
// + §F-1 권고(엄지 부채꼴 4슬롯 + 물약 1 + 회피 1 · 좌우 미러 옵션).
|
||||
//
|
||||
// ■ 왜 이 에셋인가 (C45)
|
||||
// 거리·지름·각도·임계 같은 값은 코드 상수로 두지 않는다. 이 파일에는 "필드와 기본값"만 있고,
|
||||
// 실제로 게임이 읽는 값은 Resources/WL/WLHudLayoutSettings.asset 이다(PD·기획이 인스펙터에서 바꾼다).
|
||||
//
|
||||
// ■ 좌표계 (px)
|
||||
// 모든 px 값은 **세로 1080×1920 기준 화면 픽셀**이다(designWidth/HeightPx).
|
||||
// 전투 패드는 **Safe Area 의 우하단 모서리**를 원점으로 삼고 dx = 왼쪽 방향, dy = 위쪽 방향으로 잰다.
|
||||
// 좌우 미러(mirrorLeftHanded)를 켜면 원점이 좌하단으로 바뀌고 dx 는 오른쪽 방향이 된다.
|
||||
// 각도 deg 는 0° = dx 방향(화면 안쪽 가로), 90° = dy 방향(위쪽) 인 반시계 회전이다.
|
||||
//
|
||||
// px → 캔버스 유닛 환산은 코드가 런타임에 캔버스 폭에서 구한다(802b 노트 §2 · 세로에서 1 px = 1.7778 유닛).
|
||||
// 환산 계수를 필드로 박지 않는 이유 = 해상도가 바뀌어도 같은 px 규격이 유지되게 하기 위해서다.
|
||||
//
|
||||
// ■ 「미확인」 · 알려진 규격 충돌 (완료보고 WL-813c §3 에 산술 근거)
|
||||
// §B 요소 7 "버튼 전부 우하단 모서리 반경 ≤ 320 px" 은 §B 요소 3 "버튼 지름 132~150 px · 6개" 와
|
||||
// 기하학적으로 양립하지 않는다(반경 320 px 사분원 넓이 80,425 px² < 지름 140 px 원 6개 92,363 px²).
|
||||
// → thumbReachRadiusPx 는 **목표값**으로 두고 실제 도달 반경은 813m 오버레이가 잰다.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace WL.UI
|
||||
{
|
||||
[CreateAssetMenu(fileName = "WLHudLayoutSettings", menuName = "WL/HUD Layout Settings", order = 30)]
|
||||
public sealed class WLHudLayoutSettings : ScriptableObject
|
||||
{
|
||||
/// <summary>Resources 경로 — 런타임 로드 키.</summary>
|
||||
public const string ResourcesPath = "WL/WLHudLayoutSettings";
|
||||
|
||||
private static WLHudLayoutSettings s_instance;
|
||||
private static bool s_tried;
|
||||
|
||||
/// <summary>에셋이 없으면 null 을 돌려준다(호출부는 null 이면 배치를 건너뛴다 = C8 롤백).</summary>
|
||||
public static WLHudLayoutSettings Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (s_instance == null && !s_tried)
|
||||
{
|
||||
s_tried = true;
|
||||
s_instance = Resources.Load<WLHudLayoutSettings>(ResourcesPath);
|
||||
}
|
||||
return s_instance;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>에디터에서 에셋을 새로 만들거나 값을 바꾼 뒤 캐시를 버린다.</summary>
|
||||
public static void ClearCache() { s_instance = null; s_tried = false; }
|
||||
|
||||
// ── 기준 해상도 (CLAUDE.md §9 · 세로 1080×1920) ────────────────────────
|
||||
[Header("기준 해상도 (px)")]
|
||||
[Tooltip("모든 px 값의 기준이 되는 화면 폭")]
|
||||
public float designWidthPx = 1080f;
|
||||
[Tooltip("모든 px 값의 기준이 되는 화면 높이")]
|
||||
public float designHeightPx = 1920f;
|
||||
|
||||
// ── 전투 패드 공통 ────────────────────────────────────────────────────
|
||||
[Header("전투 패드 — 공통")]
|
||||
[Tooltip("끄면 WLBattlePadLayout 이 아무 것도 만지지 않는다(원본 배치 유지 · C8 롤백)")]
|
||||
public bool battlePadEnabled = true;
|
||||
[Tooltip("좌우 미러(왼손잡이) — 패드 원점이 좌하단으로 간다")]
|
||||
public bool mirrorLeftHanded = false;
|
||||
[Tooltip("활성 스킬 슬롯 수 — 기준서 §F-1 권고 4 (프리팹 배열은 6 그대로 · 나머지는 물약/회피 예약)")]
|
||||
public int activeSkillSlots = 4;
|
||||
[Tooltip("스킬 버튼 지름 — 기준서 §B 요소3 권고 132~150")]
|
||||
public float slotDiameterPx = 140f;
|
||||
[Tooltip("버튼 사이 최소 여백 — 기준서 §B 요소3 ≥16 (검증용 · 배치는 아래 각도/반경이 만든다)")]
|
||||
public float slotMinGapPx = 16f;
|
||||
[Tooltip("엄지 도달 반경 목표 — 기준서 §B 요소7 ≤320. 실제 도달 반경은 813m 오버레이가 잰다")]
|
||||
public float thumbReachRadiusPx = 320f;
|
||||
|
||||
// ── 스킬 부채꼴 (우하단 모서리 원점) ──────────────────────────────────
|
||||
[Header("전투 패드 — 스킬 부채꼴")]
|
||||
[Tooltip("부채꼴 중심 (모서리 기준 dx, dy)")]
|
||||
public Vector2 fanOriginPx = new Vector2(0f, 0f);
|
||||
[Tooltip("부채꼴 중심 → 슬롯 중심 거리")]
|
||||
public float fanRadiusPx = 420f;
|
||||
[Tooltip("첫 슬롯 각도 (0° = 가로 안쪽 · 90° = 위)")]
|
||||
public float fanStartAngleDeg = 12.5f;
|
||||
[Tooltip("슬롯 사이 각도")]
|
||||
public float fanStepAngleDeg = 22f;
|
||||
|
||||
// ── 예약 슬롯 (물약 · 회피 · 813j/813r) ───────────────────────────────
|
||||
[Header("전투 패드 — 예약 슬롯(물약·회피 · 비활성)")]
|
||||
[Tooltip("예약 슬롯 지름 — 스킬보다 작게 두어 부채꼴 안쪽에 자리를 만든다")]
|
||||
public float reserveDiameterPx = 110f;
|
||||
[Tooltip("예약 슬롯 반경 (부채꼴 중심 기준)")]
|
||||
public float reserveRadiusPx = 270f;
|
||||
[Tooltip("첫 예약 슬롯 각도")]
|
||||
public float reserveStartAngleDeg = 25f;
|
||||
[Tooltip("예약 슬롯 사이 각도")]
|
||||
public float reserveStepAngleDeg = 40f;
|
||||
|
||||
// ── 공격 · 자동 버튼 ──────────────────────────────────────────────────
|
||||
[Header("전투 패드 — 공격 · 자동")]
|
||||
[Tooltip("공격 버튼 중심 (모서리 기준)")]
|
||||
public Vector2 attackCenterPx = new Vector2(100f, 100f);
|
||||
[Tooltip("공격 버튼 지름 — 원본 235 유닛 = 132 px")]
|
||||
public float attackDiameterPx = 132f;
|
||||
[Tooltip("자동전투 토글 중심 (모서리 기준)")]
|
||||
public Vector2 autoButtonCenterPx = new Vector2(56f, 545f);
|
||||
[Tooltip("자동전투 토글 지름 — 원본 61 유닛 = 34.3 px")]
|
||||
public float autoButtonDiameterPx = 34.3f;
|
||||
|
||||
// ── 조이스틱 규격 (§B 요소7 · 값만 확정 · 배치는 별도 태스크) ─────────
|
||||
[Header("조이스틱 규격 (값만 확정 · 813c 는 이동시키지 않는다)")]
|
||||
[Tooltip("좌하단 모서리 기준 조이스틱 중심")]
|
||||
public Vector2 joystickCenterPx = new Vector2(180f, 260f);
|
||||
[Tooltip("조이스틱 반경")]
|
||||
public float joystickRadiusPx = 90f;
|
||||
|
||||
// ── 상단 보스 HP 바 ───────────────────────────────────────────────────
|
||||
[Header("보스 HP 바 (상단)")]
|
||||
[Tooltip("끄면 바를 만들지도 켜지도 않는다")]
|
||||
public bool bossBarEnabled = true;
|
||||
[Tooltip("바 크기 (px)")]
|
||||
public Vector2 bossBarSizePx = new Vector2(880f, 28f);
|
||||
[Tooltip("Safe Area 상단에서 바 위쪽까지 여백 — 기존 WL_HUD/HUD_Currency(상단 −130.5~−176.1 px 실측) 아래에 둔다")]
|
||||
public float bossBarTopMarginPx = 190f;
|
||||
[Tooltip("이름 라벨 글자 크기")]
|
||||
public float bossBarNameFontPx = 30f;
|
||||
[Tooltip("이름 라벨과 바 사이 간격")]
|
||||
public float bossBarNameGapPx = 8f;
|
||||
[Tooltip("페이즈 눈금 굵기")]
|
||||
public float bossBarTickWidthPx = 4f;
|
||||
[Tooltip("페이즈 눈금 위치(HP 비율) — 기준서 §B 요소6 임계 100/70/40 중 전환점 2개. 전투 임계값 자체는 813h(Gameplay) 소유")]
|
||||
public float[] bossPhaseTicks = new float[] { 0.7f, 0.4f };
|
||||
[Tooltip("처치 후 바가 사라지기까지 시간(초)")]
|
||||
public float bossBarHideDelaySec = 1.2f;
|
||||
[Tooltip("바 채움 색")]
|
||||
public Color bossBarFillColor = new Color(0.99f, 0.26f, 0.28f, 1f);
|
||||
[Tooltip("바 배경 색")]
|
||||
public Color bossBarBgColor = new Color(0.05f, 0.05f, 0.06f, 0.78f);
|
||||
[Tooltip("페이즈 눈금 색")]
|
||||
public Color bossBarTickColor = new Color(1f, 1f, 1f, 0.85f);
|
||||
[Tooltip("이름 라벨 색")]
|
||||
public Color bossBarNameColor = new Color(1f, 0.92f, 0.6f, 1f);
|
||||
|
||||
// ── 유틸 ──────────────────────────────────────────────────────────────
|
||||
/// <summary>
|
||||
/// px → 캔버스 유닛 환산 계수.
|
||||
///
|
||||
/// 1순위 = CanvasScaler 산식 재현. 기준 해상도(designWidth/HeightPx)에서 이 캔버스가 갖는 폭(유닛)을
|
||||
/// 계산한다. 세로에서는 폭이 해상도와 무관하게 고정이라(1080×1920·Expand·참조 1920×1080 → 1920 유닛)
|
||||
/// px 는 진짜 "설계 단위"가 되고, 에디트 모드(레이아웃 미실행)에서도 같은 값이 나온다.
|
||||
/// 2순위 = 실제 캔버스 rect 실측(스케일러가 없거나 고정 픽셀 모드일 때).
|
||||
/// 계수를 코드 상수로 박지 않는다 — 스케일러 설정이 바뀌면 따라간다(802b 노트: 세로에서 1.7778).
|
||||
/// </summary>
|
||||
public float UnitsPerPx(Canvas canvas)
|
||||
{
|
||||
if (designWidthPx <= 0f || designHeightPx <= 0f) return 1f;
|
||||
|
||||
var scaler = canvas != null ? canvas.GetComponent<CanvasScaler>() : null;
|
||||
if (scaler != null && scaler.uiScaleMode == CanvasScaler.ScaleMode.ScaleWithScreenSize)
|
||||
{
|
||||
Vector2 r = scaler.referenceResolution;
|
||||
if (r.x > 0f && r.y > 0f)
|
||||
{
|
||||
float w = designWidthPx, h = designHeightPx, scale;
|
||||
switch (scaler.screenMatchMode)
|
||||
{
|
||||
case CanvasScaler.ScreenMatchMode.Expand: scale = Mathf.Min(w / r.x, h / r.y); break;
|
||||
case CanvasScaler.ScreenMatchMode.Shrink: scale = Mathf.Max(w / r.x, h / r.y); break;
|
||||
default: scale = Mathf.Pow(w / r.x, 1f - scaler.matchWidthOrHeight) *
|
||||
Mathf.Pow(h / r.y, scaler.matchWidthOrHeight); break;
|
||||
}
|
||||
if (scale > 0f) return (w / scale) / designWidthPx;
|
||||
}
|
||||
}
|
||||
|
||||
var canvasRect = canvas != null ? canvas.transform as RectTransform : null;
|
||||
if (canvasRect != null && canvasRect.rect.width > 0f) return canvasRect.rect.width / designWidthPx;
|
||||
return 1f;
|
||||
}
|
||||
|
||||
/// <summary>부채꼴 각도 → 모서리 기준 (dx, dy) px.</summary>
|
||||
public Vector2 FanSlotPx(int index)
|
||||
{
|
||||
float deg = fanStartAngleDeg + fanStepAngleDeg * index;
|
||||
float rad = deg * Mathf.Deg2Rad;
|
||||
return fanOriginPx + new Vector2(Mathf.Cos(rad), Mathf.Sin(rad)) * fanRadiusPx;
|
||||
}
|
||||
|
||||
/// <summary>예약(물약·회피) 슬롯 각도 → 모서리 기준 (dx, dy) px.</summary>
|
||||
public Vector2 ReserveSlotPx(int index)
|
||||
{
|
||||
float deg = reserveStartAngleDeg + reserveStepAngleDeg * index;
|
||||
float rad = deg * Mathf.Deg2Rad;
|
||||
return fanOriginPx + new Vector2(Mathf.Cos(rad), Mathf.Sin(rad)) * reserveRadiusPx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 55115957ebcbb1e498a857d9f9bb17f8
|
||||
Loading…
Reference in New Issue