// ───────────────────────────────────────────────────────────────────────────── // 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 { /// Resources 경로 — 런타임 로드 키. public const string ResourcesPath = "WL/WLHudLayoutSettings"; private static WLHudLayoutSettings s_instance; private static bool s_tried; /// 에셋이 없으면 null 을 돌려준다(호출부는 null 이면 배치를 건너뛴다 = C8 롤백). public static WLHudLayoutSettings Instance { get { if (s_instance == null && !s_tried) { s_tried = true; s_instance = Resources.Load(ResourcesPath); } return s_instance; } } /// 에디터에서 에셋을 새로 만들거나 값을 바꾼 뒤 캐시를 버린다. 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); // ── 유틸 ────────────────────────────────────────────────────────────── /// /// px → 캔버스 유닛 환산 계수. /// /// 1순위 = CanvasScaler 산식 재현. 기준 해상도(designWidth/HeightPx)에서 이 캔버스가 갖는 폭(유닛)을 /// 계산한다. 세로에서는 폭이 해상도와 무관하게 고정이라(1080×1920·Expand·참조 1920×1080 → 1920 유닛) /// px 는 진짜 "설계 단위"가 되고, 에디트 모드(레이아웃 미실행)에서도 같은 값이 나온다. /// 2순위 = 실제 캔버스 rect 실측(스케일러가 없거나 고정 픽셀 모드일 때). /// 계수를 코드 상수로 박지 않는다 — 스케일러 설정이 바뀌면 따라간다(802b 노트: 세로에서 1.7778). /// public float UnitsPerPx(Canvas canvas) { if (designWidthPx <= 0f || designHeightPx <= 0f) return 1f; var scaler = canvas != null ? canvas.GetComponent() : 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; } /// 부채꼴 각도 → 모서리 기준 (dx, dy) px. 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; } /// 예약(물약·회피) 슬롯 각도 → 모서리 기준 (dx, dy) px. 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; } } }