224 lines
9.3 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|