Project_WL/Assets/WL/Island/WLIslandUi.cs

224 lines
9.3 KiB
C#

// ─────────────────────────────────────────────────────────────────────────────
// WLIslandUi.cs — 던전 입구의 이름표·「들어가기」 버튼 · 더미 머티리얼
//
// 발주서 WL-816d §1-② 「플레이어가 가까이 가면 『들어가기』가 뜨게 하라」
// 🔴 기존 UI 프리팹·Canvas 를 고치지 않는다 — 필요한 것만 런타임에 만들고, 떠날 때 지운다.
// 🔴 배경/캐릭터 머티리얼은 읽지도 고치지도 않는다(816a 소유). 더미용 머티리얼만 새로 만든다.
// ─────────────────────────────────────────────────────────────────────────────
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace WL.Island
{
/// <summary>더미 전용 머티리얼 공장(기존 머티리얼은 건드리지 않는다).</summary>
public static class WLIslandLook
{
static readonly Dictionary<int, Material> s_cache = new Dictionary<int, Material>(8);
static Shader s_shader;
public static Shader LitShader
{
get
{
if (s_shader != null) return s_shader;
s_shader = Shader.Find("Universal Render Pipeline/Lit");
if (s_shader == null) s_shader = Shader.Find("Universal Render Pipeline/Simple Lit");
if (s_shader == null) s_shader = Shader.Find("Sprites/Default");
return s_shader;
}
}
static Shader s_toon;
static bool s_toonTried;
/// <summary>아레나·섬이 쓰는 Toon 셰이더(`Shader Graphs/Toon`). 없으면 null.</summary>
public static Shader ToonShader
{
get
{
if (s_toonTried) return s_toon;
s_toonTried = true;
s_toon = Shader.Find("Shader Graphs/Toon");
return s_toon;
}
}
/// <summary>
/// 아레나와 같은 Toon 재질(WL-816g §2). 셰이더가 없으면 <see cref="GateMaterial"/>(URP/Lit)로 떨어진다.
/// 🔴 `_BaseMap` 은 비워 둔다(기본 흰 텍스처) — 색은 `_DiffuseColor` 로만 준다.
/// 실측 근거: `Farm_IslandTop_Demo.mat` 가 `_DiffuseColor`·`_ShadowDiffuseColor` 로 색을 준다.
/// </summary>
public static Material ToonMaterial(Color c, bool useToon)
{
if (!useToon) return GateMaterial(c);
var sh = ToonShader;
if (sh == null) return GateMaterial(c);
int key = ((Color32)c).GetHashCode() ^ 0x7001;
Material m;
if (s_cache.TryGetValue(key, out m) && m != null) return m;
m = new Material(sh) { name = "WL_PlatformToon", hideFlags = HideFlags.DontSave };
if (m.HasProperty("_DiffuseColor")) m.SetColor("_DiffuseColor", c);
if (m.HasProperty("_ShadowDiffuseColor")) m.SetColor("_ShadowDiffuseColor", c * 0.55f);
if (m.HasProperty("_BaseColor")) m.SetColor("_BaseColor", c);
if (m.HasProperty("_MinimumDarkness")) m.SetFloat("_MinimumDarkness", 0.2f);
s_cache[key] = m;
return m;
}
public static Material GateMaterial(Color c)
{
int key = ((Color32)c).GetHashCode();
Material m;
if (s_cache.TryGetValue(key, out m) && m != null) return m;
if (LitShader == null) return null;
m = new Material(LitShader) { name = "WL_GateDummy", hideFlags = HideFlags.DontSave };
if (m.HasProperty("_BaseColor")) m.SetColor("_BaseColor", c);
if (m.HasProperty("_Color")) m.SetColor("_Color", c);
if (m.HasProperty("_Smoothness")) m.SetFloat("_Smoothness", 0.15f);
s_cache[key] = m;
return m;
}
}
/// <summary>입구 위에 뜨는 3D 이름표.</summary>
public static class WLIslandLabel
{
public static void Attach(Transform parent, Vector3 localPos, string text)
{
var cfg = WLIslandSettings.Instance;
var go = new GameObject("label");
go.transform.SetParent(parent, false);
go.transform.localPosition = localPos;
var tmp = go.AddComponent<TextMeshPro>();
var f = WLIslandAssets.Font(cfg != null ? cfg.fontAssetPath : null);
if (f != null) tmp.font = f;
tmp.text = text;
tmp.fontSize = 3.2f;
tmp.alignment = TextAlignmentOptions.Center;
tmp.color = Color.white;
var rt = tmp.GetComponent<RectTransform>();
if (rt != null) rt.sizeDelta = new Vector2(6f, 1.4f);
var bb = go.AddComponent<WLIslandBillboard>();
bb.upright = cfg == null || cfg.platformLabelUpright != 0;
}
}
/// <summary>
/// 이름표가 항상 카메라를 본다.
/// 🔴 2026-09-16 PD 「글자를 바닥 판과 90° 로 세워 플레이어 기준 정면으로」 → `upright` 면 **세운 채** 카메라 쪽 방향(수평)만 따라 돈다.
/// </summary>
public sealed class WLIslandBillboard : MonoBehaviour
{
[Tooltip("1 = 세운 채 수평 방향만 카메라를 따른다 · 0 = 카메라를 정면으로 보게 기울인다.")]
public bool upright = true;
void LateUpdate()
{
var cam = Camera.main;
if (cam == null) return;
var dir = transform.position - cam.transform.position;
if (upright) dir.y = 0f;
if (dir.sqrMagnitude < 1e-6f) return;
transform.rotation = Quaternion.LookRotation(dir, Vector3.up);
}
}
/// <summary>화면 아래 「들어가기」 — 런타임 생성 Canvas 1개(기존 UI 무변경).</summary>
public static class WLIslandPrompt
{
static GameObject s_root;
static TextMeshProUGUI s_text;
static WLDungeonGate s_target;
public static bool Visible { get { return s_root != null && s_root.activeSelf; } }
public static string LastText = "";
public static void Show(WLIslandSettings cfg, string text, WLDungeonGate gate)
{
Ensure(cfg);
s_target = gate;
LastText = text;
if (s_text != null) s_text.text = text;
if (s_root != null) s_root.SetActive(true);
}
public static void Hide()
{
s_target = null;
if (s_root != null) s_root.SetActive(false);
}
static void Ensure(WLIslandSettings cfg)
{
if (s_root != null) return;
s_root = new GameObject("~WL_IslandPrompt");
Object.DontDestroyOnLoad(s_root);
var canvas = s_root.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.sortingOrder = 9000;
var scaler = s_root.AddComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1080f, 1920f);
scaler.matchWidthOrHeight = 0.5f;
s_root.AddComponent<GraphicRaycaster>();
if (Object.FindFirstObjectByType<EventSystem>() == null)
{
var es = new GameObject("~WL_EventSystem");
Object.DontDestroyOnLoad(es);
es.AddComponent<EventSystem>();
es.AddComponent<StandaloneInputModule>();
}
// 버튼
var btnGo = new GameObject("btn_enter", typeof(RectTransform));
btnGo.transform.SetParent(s_root.transform, false);
var brt = btnGo.GetComponent<RectTransform>();
brt.anchorMin = new Vector2(0.5f, 0f);
brt.anchorMax = new Vector2(0.5f, 0f);
brt.pivot = new Vector2(0.5f, 0f);
brt.anchoredPosition = new Vector2(0f, 260f);
brt.sizeDelta = new Vector2(520f, 150f);
var img = btnGo.AddComponent<Image>();
img.color = new Color(0.13f, 0.09f, 0.20f, 0.92f);
var btn = btnGo.AddComponent<Button>();
btn.targetGraphic = img;
btn.onClick.AddListener(OnClick);
var txtGo = new GameObject("text", typeof(RectTransform));
txtGo.transform.SetParent(btnGo.transform, false);
var trt = txtGo.GetComponent<RectTransform>();
trt.anchorMin = Vector2.zero; trt.anchorMax = Vector2.one;
trt.offsetMin = Vector2.zero; trt.offsetMax = Vector2.zero;
s_text = txtGo.AddComponent<TextMeshProUGUI>();
var pf = WLIslandAssets.Font(cfg != null ? cfg.fontAssetPath : null);
if (pf != null) s_text.font = pf;
s_text.fontSize = 44f;
s_text.alignment = TextAlignmentOptions.Center;
s_text.color = Color.white;
s_text.text = cfg != null ? cfg.enterButtonText : "들어가기";
s_root.SetActive(false);
}
static void OnClick()
{
var g = s_target;
if (g != null) g.Enter();
}
}
}