// ─────────────────────────────────────────────────────────────────────────────
// 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
{
/// 더미 전용 머티리얼 공장(기존 머티리얼은 건드리지 않는다).
public static class WLIslandLook
{
static readonly Dictionary s_cache = new Dictionary(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;
/// 아레나·섬이 쓰는 Toon 셰이더(`Shader Graphs/Toon`). 없으면 null.
public static Shader ToonShader
{
get
{
if (s_toonTried) return s_toon;
s_toonTried = true;
s_toon = Shader.Find("Shader Graphs/Toon");
return s_toon;
}
}
///
/// 아레나와 같은 Toon 재질(WL-816g §2). 셰이더가 없으면 (URP/Lit)로 떨어진다.
/// 🔴 `_BaseMap` 은 비워 둔다(기본 흰 텍스처) — 색은 `_DiffuseColor` 로만 준다.
/// 실측 근거: `Farm_IslandTop_Demo.mat` 가 `_DiffuseColor`·`_ShadowDiffuseColor` 로 색을 준다.
///
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;
}
}
/// 입구 위에 뜨는 3D 이름표.
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();
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();
if (rt != null) rt.sizeDelta = new Vector2(6f, 1.4f);
var bb = go.AddComponent();
bb.upright = cfg == null || cfg.platformLabelUpright != 0;
}
}
///
/// 이름표가 항상 카메라를 본다.
/// 🔴 2026-09-16 PD 「글자를 바닥 판과 90° 로 세워 플레이어 기준 정면으로」 → `upright` 면 **세운 채** 카메라 쪽 방향(수평)만 따라 돈다.
///
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);
}
}
/// 화면 아래 「들어가기」 — 런타임 생성 Canvas 1개(기존 UI 무변경).
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 GameObject s_ownEventSystem;
public static int EventSystemsRemoved;
///
/// 🔴 2026-09-17 실측: 우리가 「EventSystem 없음」일 때 만든 `~WL_EventSystem` 이 나중에 올라온 원본 씬의 EventSystem 과 겹쳐
/// 매 프레임 「There are 2 event systems」 경고가 콘솔을 가득 채웠다(에러 원문이 밀려남). 원본 것이 있으면 우리 것을 지운다.
///
public static void ReconcileEventSystems()
{
if (s_ownEventSystem == null) return;
var all = Object.FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
if (all.Length <= 1) return;
Object.Destroy(s_ownEventSystem);
s_ownEventSystem = null;
EventSystemsRemoved++;
}
static void Ensure(WLIslandSettings cfg)
{
ReconcileEventSystems();
if (s_root != null) return;
s_root = new GameObject("~WL_IslandPrompt");
Object.DontDestroyOnLoad(s_root);
var canvas = s_root.AddComponent