Project_WL/Assets/WL/UI/Settings/WLHudLayoutSettings.cs

279 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ─────────────────────────────────────────────────────────────────────────────
// 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);
[Tooltip("바가 켜질 때 이미 살아 있는 보스가 있으면 즉시 물린다(813d 게이트 이전 스폰 = Q3 결함 D-3). 끄면 Spawned 이벤트만 본다.")]
public bool bossBarBindExisting = true;
[Header("전투 중 하단 메뉴 숨김 · 패드 원점 복귀 (WL-813y · Q3 결함 D-5)")]
[Tooltip("끄면 하단 메뉴를 건드리지 않고 패드도 항상 idle 원점(아래 값)을 쓴다 — 813c+Lead 핫픽스 상태로 복귀(C8 롤백).")]
public bool combatHideBottomMenu = true;
[Tooltip("숨길 하단 메뉴 노드의 루트 기준 경로(813y 실측 = IngameUIs/WL_HUD/HUD_BottomMenu).")]
public string bottomMenuPath = "IngameUIs/WL_HUD/HUD_BottomMenu";
[Tooltip("SetActive 대신 CanvasGroup 으로 숨긴다(하위 스크립트의 Update 가 계속 돌아야 하는 경우 · #796 선례).")]
public bool bottomMenuUseCanvasGroup = true;
[Tooltip("숨김/복귀 페이드 시간(초 · unscaled). 0 이면 즉시.")]
public float bottomMenuFadeSeconds = 0.18f;
[Tooltip("마지막 전투 신호(적 인지 · 피격 · 공격 · 스킬 · 처치) 뒤 이 시간이 지나면 메뉴가 돌아온다.")]
public float combatExitSeconds = 6f;
[Tooltip("전투 신호가 들어오고 이 시간이 지나야 숨긴다(0 = 즉시).")]
public float combatEnterDelaySeconds = 0f;
[Tooltip("현재 타깃이 살아 있으면 전투로 본다(적 인지). 끄면 공격/피격/스킬/처치 이벤트만 본다.")]
public bool combatUseTargetAsSignal = true;
[Tooltip("메뉴가 숨은 동안 되돌릴 버튼 1개를 만든다(런타임 생성 · 프리팹 diff 0).")]
public bool bottomMenuRestoreButton = true;
[Tooltip("복귀 버튼 중심 — x = 화면 가로 중앙 기준(양수 = 오른쪽) · y = 화면 아래 기준(px).")]
public Vector2 restoreButtonCenterPx = new Vector2(-380f, 62f);
[Tooltip("복귀 버튼 지름(px).")]
public float restoreButtonDiameterPx = 88f;
[Tooltip("복귀 버튼 글자(TMP · 아트 에셋 의존 0).")]
public string restoreButtonLabel = "≡";
[Tooltip("복귀 버튼 글자 크기(px).")]
public float restoreButtonFontPx = 46f;
[Tooltip("복귀 버튼 배경 색.")]
public Color restoreButtonBgColor = new Color(0.06f, 0.07f, 0.10f, 0.72f);
[Tooltip("복귀 버튼 글자 색.")]
public Color restoreButtonLabelColor = new Color(0.96f, 0.94f, 0.82f, 1f);
[Tooltip("복귀 버튼으로 메뉴를 되돌린 뒤, 이 시간이 지나면 다시 전투 판정을 따른다(초).")]
public float restoreHoldSeconds = 8f;
[Tooltip("하단 메뉴가 **보이는 동안** 쓰는 부채꼴 중심 — 메뉴(상단 y ≈ 163 px)를 피해 위로 올린 813c+Lead 핫픽스 값.")]
public Vector2 idleFanOriginPx = new Vector2(0f, 260f);
[Tooltip("하단 메뉴가 **보이는 동안** 쓰는 공격 버튼 중심.")]
public Vector2 idleAttackCenterPx = new Vector2(100f, 360f);
// ── 유틸 ──────────────────────────────────────────────────────────────
/// <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>
/// 지금 써야 할 부채꼴 중심. 하단 메뉴가 **보이는 동안**은 메뉴를 피해 올린 자리(idleFanOriginPx),
/// 전투 중(메뉴 숨김)은 모서리 원점(fanOriginPx) — WL-813y §1-6.
/// </summary>
public Vector2 FanOriginPxFor(bool bottomMenuVisible)
{
// C8 롤백: 기능을 끄면 하단 메뉴가 **늘 보이므로** 메뉴를 피한 자리(idle) 를 써야 한다
// = 813c + Lead 핫픽스 상태 그대로. (모서리 원점을 쓰면 메뉴와 다시 겹친다 · Q3 결함 D-5)
if (!combatHideBottomMenu) return idleFanOriginPx;
return bottomMenuVisible ? idleFanOriginPx : fanOriginPx;
}
/// <summary>지금 써야 할 공격 버튼 중심(위와 같은 규칙).</summary>
public Vector2 AttackCenterPxFor(bool bottomMenuVisible)
{
if (!combatHideBottomMenu) return idleAttackCenterPx;
return bottomMenuVisible ? idleAttackCenterPx : attackCenterPx;
}
/// <summary>부채꼴 각도 → 모서리 기준 (dx, dy) px.</summary>
public Vector2 FanSlotPx(int index) { return FanSlotPx(index, HudCombatVisibility.BottomMenuVisible); }
public Vector2 FanSlotPx(int index, bool bottomMenuVisible)
{
float deg = fanStartAngleDeg + fanStepAngleDeg * index;
float rad = deg * Mathf.Deg2Rad;
return FanOriginPxFor(bottomMenuVisible) + new Vector2(Mathf.Cos(rad), Mathf.Sin(rad)) * fanRadiusPx;
}
/// <summary>예약(물약·회피) 슬롯 각도 → 모서리 기준 (dx, dy) px.</summary>
public Vector2 ReserveSlotPx(int index) { return ReserveSlotPx(index, HudCombatVisibility.BottomMenuVisible); }
public Vector2 ReserveSlotPx(int index, bool bottomMenuVisible)
{
float deg = reserveStartAngleDeg + reserveStepAngleDeg * index;
float rad = deg * Mathf.Deg2Rad;
return FanOriginPxFor(bottomMenuVisible) + new Vector2(Mathf.Cos(rad), Mathf.Sin(rad)) * reserveRadiusPx;
}
}
}