Merge branch 'wl/systems/WL-816g-gate-platform'
This commit is contained in:
commit
1c41891d2c
|
|
@ -45,6 +45,39 @@ MonoBehaviour:
|
||||||
dungeonSpawnerRange: 6
|
dungeonSpawnerRange: 6
|
||||||
fixDungeonDuplicates: 1
|
fixDungeonDuplicates: 1
|
||||||
dungeonEnterDelay: 1.5
|
dungeonEnterDelay: 1.5
|
||||||
|
gateFlowEnabled: 1
|
||||||
|
hideFarms: 1
|
||||||
|
farmReHideDelaySeconds: 3
|
||||||
|
matchNewTileMaterials: 1
|
||||||
|
dungeonUnlockPrefsKey: WL_Island_DungeonsUnlocked
|
||||||
|
dungeonUnlockPrices:
|
||||||
|
- 10
|
||||||
|
- 30
|
||||||
|
- 60
|
||||||
|
- 100
|
||||||
|
maxDungeons: 4
|
||||||
|
platformUseFarmPosition: 1
|
||||||
|
platformPosition: {x: 0, y: 0, z: 0}
|
||||||
|
platformOffset: {x: 0, y: 0, z: 0}
|
||||||
|
platformPrefabPath:
|
||||||
|
platformShowPad: 0
|
||||||
|
platformCenterOnPlate: 1
|
||||||
|
platformPlateLift: 0.03
|
||||||
|
platformShowLabel: 1
|
||||||
|
platformRadius: 1.7
|
||||||
|
platformColor: {r: 0.92, g: 0.72, b: 0.24, a: 1}
|
||||||
|
platformUseToon: 1
|
||||||
|
platformLabelFormat: "던전 열기"
|
||||||
|
platformLabelHeight: 2.2
|
||||||
|
platformTriggerSize: 3.4
|
||||||
|
platformTriggerHeight: 3
|
||||||
|
platformRespawnDelaySeconds: 1.3
|
||||||
|
platformSnapToGround: 1
|
||||||
|
platformRequireStepOff: 1
|
||||||
|
platformStepOffTimeoutSeconds: 120
|
||||||
|
applyStartCoin: 1
|
||||||
|
startCoin: 10
|
||||||
|
saveResetToken: 816g-1
|
||||||
dungeons:
|
dungeons:
|
||||||
- enabled_: 1
|
- enabled_: 1
|
||||||
id: 1
|
id: 1
|
||||||
|
|
@ -57,6 +90,8 @@ MonoBehaviour:
|
||||||
gateScale: 1
|
gateScale: 1
|
||||||
gateColor: {r: 0.45, g: 0.32, b: 0.62, a: 1}
|
gateColor: {r: 0.45, g: 0.32, b: 0.62, a: 1}
|
||||||
gatePrefabPath:
|
gatePrefabPath:
|
||||||
|
platformUseFarm: 1
|
||||||
|
platformPosition: {x: 0, y: 0, z: 0}
|
||||||
- enabled_: 1
|
- enabled_: 1
|
||||||
id: 2
|
id: 2
|
||||||
displayName: "던전 2"
|
displayName: "던전 2"
|
||||||
|
|
@ -68,6 +103,8 @@ MonoBehaviour:
|
||||||
gateScale: 1
|
gateScale: 1
|
||||||
gateColor: {r: 0.28, g: 0.5, b: 0.66, a: 1}
|
gateColor: {r: 0.28, g: 0.5, b: 0.66, a: 1}
|
||||||
gatePrefabPath:
|
gatePrefabPath:
|
||||||
|
platformUseFarm: 0
|
||||||
|
platformPosition: {x: -2, y: 0, z: -2}
|
||||||
- enabled_: 1
|
- enabled_: 1
|
||||||
id: 3
|
id: 3
|
||||||
displayName: "던전 3"
|
displayName: "던전 3"
|
||||||
|
|
@ -79,6 +116,8 @@ MonoBehaviour:
|
||||||
gateScale: 1
|
gateScale: 1
|
||||||
gateColor: {r: 0.62, g: 0.42, b: 0.24, a: 1}
|
gateColor: {r: 0.62, g: 0.42, b: 0.24, a: 1}
|
||||||
gatePrefabPath:
|
gatePrefabPath:
|
||||||
|
platformUseFarm: 0
|
||||||
|
platformPosition: {x: -2.5, y: 0, z: 8.5}
|
||||||
- enabled_: 1
|
- enabled_: 1
|
||||||
id: 4
|
id: 4
|
||||||
displayName: "던전 4"
|
displayName: "던전 4"
|
||||||
|
|
@ -90,3 +129,5 @@ MonoBehaviour:
|
||||||
gateScale: 1
|
gateScale: 1
|
||||||
gateColor: {r: 0.55, g: 0.24, b: 0.3, a: 1}
|
gateColor: {r: 0.55, g: 0.24, b: 0.3, a: 1}
|
||||||
gatePrefabPath:
|
gatePrefabPath:
|
||||||
|
platformUseFarm: 0
|
||||||
|
platformPosition: {x: 6, y: 0, z: -2}
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,9 @@ namespace WL.Island
|
||||||
/// <summary>현재 씬에 살아 있는 던전 입구들.</summary>
|
/// <summary>현재 씬에 살아 있는 던전 입구들.</summary>
|
||||||
public static List<WLDungeonGate> Gates { get { return s_gates; } }
|
public static List<WLDungeonGate> Gates { get { return s_gates; } }
|
||||||
|
|
||||||
|
/// <summary>씬을 넘어 사는 코루틴 러너(WL-816g 의 발판 흐름도 이것을 쓴다).</summary>
|
||||||
|
public static WLIslandRunner Runner { get { EnsureRunner(); return s_runner; } }
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────
|
||||||
// 설치 — 씬 하나 없이 자동으로 걸린다(원본 훅 0줄)
|
// 설치 — 씬 하나 없이 자동으로 걸린다(원본 훅 0줄)
|
||||||
// ─────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -125,6 +128,8 @@ namespace WL.Island
|
||||||
WLIslandSceneSetup.FixDuplicates(cfg, scene);
|
WLIslandSceneSetup.FixDuplicates(cfg, scene);
|
||||||
WLIslandSceneSetup.SwapPlayerVisual(cfg, scene);
|
WLIslandSceneSetup.SwapPlayerVisual(cfg, scene);
|
||||||
SpawnGates(cfg, scene);
|
SpawnGates(cfg, scene);
|
||||||
|
// WL-816g — 밭을 숨기고 그 자리에 「골드 납품 발판」 1개를 놓는다(꺼져 있으면 무동작).
|
||||||
|
WLIslandGateFlow.Apply(cfg, scene);
|
||||||
|
|
||||||
// 보류 보상 지급(⑥) — IslandManager 가 살아날 때까지 잠깐 기다린다.
|
// 보류 보상 지급(⑥) — IslandManager 가 살아날 때까지 잠깐 기다린다.
|
||||||
float t = 0f;
|
float t = 0f;
|
||||||
|
|
@ -149,14 +154,34 @@ namespace WL.Island
|
||||||
s_gates.Clear();
|
s_gates.Clear();
|
||||||
if (cfg.dungeons == null) return;
|
if (cfg.dungeons == null) return;
|
||||||
|
|
||||||
|
// 🔴 WL-816g: 초기에는 던전이 **하나도** 보이지 않는다. 발판에서 골드를 낸 만큼만 열린다.
|
||||||
|
// `gateFlowEnabled = 0` 이면 UnlockedCount 가 int.MaxValue = 지금까지처럼 전부 배치.
|
||||||
|
int limit = WLIslandGateFlow.UnlockedCount(cfg);
|
||||||
|
int made = 0;
|
||||||
|
|
||||||
for (int i = 0; i < cfg.dungeons.Length; i++)
|
for (int i = 0; i < cfg.dungeons.Length; i++)
|
||||||
{
|
{
|
||||||
var d = cfg.dungeons[i];
|
var d = cfg.dungeons[i];
|
||||||
if (d == null || d.enabled_ == 0) continue;
|
if (d == null || d.enabled_ == 0) continue;
|
||||||
|
if (made >= limit) break;
|
||||||
var gate = WLDungeonGate.Create(cfg, d, scene);
|
var gate = WLDungeonGate.Create(cfg, d, scene);
|
||||||
if (gate != null) { s_gates.Add(gate); GatesSpawned++; }
|
if (gate != null) { s_gates.Add(gate); GatesSpawned++; made++; }
|
||||||
}
|
}
|
||||||
Log(cfg, "던전 입구 " + s_gates.Count + "개 배치");
|
Log(cfg, "던전 입구 " + s_gates.Count + "개 배치(해금 " +
|
||||||
|
(limit == int.MaxValue ? "전체" : limit.ToString()) + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>발판에서 값을 치렀을 때 던전 입구 1개를 추가로 켠다(표 한 행 = 던전 하나).</summary>
|
||||||
|
public static void SpawnOneGate(WLIslandSettings cfg, WLDungeonDef d, Scene scene)
|
||||||
|
{
|
||||||
|
if (cfg == null || d == null) return;
|
||||||
|
for (int i = 0; i < s_gates.Count; i++)
|
||||||
|
if (s_gates[i] != null && s_gates[i].Def == d) return; // 이미 있다
|
||||||
|
|
||||||
|
var gate = WLDungeonGate.Create(cfg, d, scene);
|
||||||
|
if (gate == null) return;
|
||||||
|
s_gates.Add(gate); GatesSpawned++;
|
||||||
|
Log(cfg, "던전 입구 추가 — " + d.displayName + " @ " + d.gatePosition.ToString("F1"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -262,6 +287,14 @@ namespace WL.Island
|
||||||
return v < 0 ? 0 : v;
|
return v < 0 ? 0 : v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>보류 보상을 비운다(WL-816g 세이브 초기화가 부른다 — 초기 골드가 10 이 되도록).</summary>
|
||||||
|
public static void ClearPending()
|
||||||
|
{
|
||||||
|
s_pendingGold = 0;
|
||||||
|
var cfg = WLIslandSettings.Instance;
|
||||||
|
if (cfg != null) PlayerPrefs.SetInt(cfg.pendingPrefsKey, 0);
|
||||||
|
}
|
||||||
|
|
||||||
static void SetPending(WLIslandSettings cfg, int gold)
|
static void SetPending(WLIslandSettings cfg, int gold)
|
||||||
{
|
{
|
||||||
s_pendingGold = gold < 0 ? 0 : gold;
|
s_pendingGold = gold < 0 ? 0 : gold;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,618 @@
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// WLIslandGateFlow.cs — 「초기에는 밭·던전을 숨기고, 밭 자리의 발판에 10 골드를 내면 던전이 열린다」
|
||||||
|
//
|
||||||
|
// PD 지시 원문(2026-09-13 · 발주서 WL-816g §2·§3)
|
||||||
|
// 「최초에는 작물을 심을 수 있는 밭과 던전을 모두 안보이게 하고,
|
||||||
|
// 현재 밭 위치에 골드를 납품할 수 있는 발판 1개만 배치해줘.
|
||||||
|
// 초기 보유 골드는 10 골드이고, 이 발판 위로 올라와 10 골드를 지불하면 던전이 생성되어야 해.」
|
||||||
|
//
|
||||||
|
// ■ 🔴 PD 정정(2026-09-13) 「**던전 열기 골드 발판은 원래 에셋에 있던 것을 활용하면 돼.**」
|
||||||
|
// → 발판 = FI 가 잠긴 섬 옆에 세우는 **그 가격판 프리팹 그대로**다(`Purchaser.prefab`).
|
||||||
|
// 모양·숫자·지불 감각이 전부 섬 확장과 같다. **새 모델을 만들지 않는다**(더미 원판은 기본 꺼짐).
|
||||||
|
// 실측: 그 가격판(`Price`)은 루트보다 1 m 앞에서 **바닥에 눕는 4×2 m 판**이다 — 그 자체가 발판이다.
|
||||||
|
//
|
||||||
|
// ■ 🔴 지불 방식 = FI `Purchaser` **그대로 재사용**한다(발주서 §2 지시).
|
||||||
|
// FI 섬 확장과 **똑같은 조작·똑같은 연출**이다 — 발판(트리거) 위에 서 있으면 코인이 초당
|
||||||
|
// 빨려 들어가고(최대 5초 · 피치가 올라가는 Pop SFX · 가격판이 줄어듦), 다 내면 물보라가
|
||||||
|
// 터지고 대상이 `Activate()` 된다. 학습 비용 0.
|
||||||
|
// 재사용 방법: `Purchaser.Init(island, price, data)` 가 요구하는 `Island` 자리에
|
||||||
|
// **보이지 않는 더미 `Island`**(메시 없음)를 물려 주고, 그 `Island.OnActivated`(public event)를
|
||||||
|
// 받아 「던전 1개 켜기」를 한다. → **FI 코드 0줄 · FI 프리팹 0줄**.
|
||||||
|
//
|
||||||
|
// ■ 되돌리기(C8) — `gateFlowEnabled = 0` 하나면 지금 상태(밭·던전 처음부터 보임) 100 % 복귀.
|
||||||
|
//
|
||||||
|
// 🔴 Debug.LogError / Debug.LogException 을 쓰지 않는다(ErrorLogHookManager 팝업 방지).
|
||||||
|
// 🔴 `Assets/FarmingIsland/**` 와 `Assets/Script/**` 는 이 파일 때문에 한 줄도 바뀌지 않는다.
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.SceneManagement;
|
||||||
|
using FIFarm = CryingSnow.FarmingIsland.Farm;
|
||||||
|
using FIIsland = CryingSnow.FarmingIsland.Island;
|
||||||
|
using FIIslandManager = CryingSnow.FarmingIsland.IslandManager;
|
||||||
|
using FIPurchaser = CryingSnow.FarmingIsland.Purchaser;
|
||||||
|
using FIPurchaserData = CryingSnow.FarmingIsland.PurchaserData;
|
||||||
|
|
||||||
|
namespace WL.Island
|
||||||
|
{
|
||||||
|
/// <summary>초기 상태(밭·던전 숨김) + 골드 납품 발판 + 던전 해금 카운터. 정적 · 씬에 남는 상태 없음.</summary>
|
||||||
|
public static class WLIslandGateFlow
|
||||||
|
{
|
||||||
|
// ── 진단(프로브·보고가 읽는다)
|
||||||
|
public static int FarmsHidden, RenderersHidden, PlatformsMade, Unlocks;
|
||||||
|
public static string LastLog = "";
|
||||||
|
public static Vector3 LastPlatformPos;
|
||||||
|
|
||||||
|
/// <summary>해금된 던전 수(PlayerPrefs · FI 세이브를 건드리지 않는다).</summary>
|
||||||
|
public static int UnlockedCount(WLIslandSettings cfg)
|
||||||
|
{
|
||||||
|
if (cfg == null || cfg.gateFlowEnabled == 0) return int.MaxValue; // 꺼져 있으면 전부 보인다
|
||||||
|
return Mathf.Max(0, PlayerPrefs.GetInt(cfg.dungeonUnlockPrefsKey, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SetUnlockedCount(WLIslandSettings cfg, int n)
|
||||||
|
{
|
||||||
|
if (cfg == null) return;
|
||||||
|
PlayerPrefs.SetInt(cfg.dungeonUnlockPrefsKey, Mathf.Max(0, n));
|
||||||
|
PlayerPrefs.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>다음 던전 1개를 여는 값(표에 없으면 마지막 값을 이어 쓴다).</summary>
|
||||||
|
public static int PriceFor(WLIslandSettings cfg, int nextIndex)
|
||||||
|
{
|
||||||
|
if (cfg == null || cfg.dungeonUnlockPrices == null || cfg.dungeonUnlockPrices.Length == 0) return 10;
|
||||||
|
int i = Mathf.Clamp(nextIndex, 0, cfg.dungeonUnlockPrices.Length - 1);
|
||||||
|
return Mathf.Max(0, cfg.dungeonUnlockPrices[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>표에서 실제로 쓸 수 있는(켜져 있는) 던전 행 수 · `maxDungeons` 로 더 줄일 수 있다.</summary>
|
||||||
|
public static int UsableDungeonCount(WLIslandSettings cfg)
|
||||||
|
{
|
||||||
|
if (cfg == null || cfg.dungeons == null) return 0;
|
||||||
|
int n = 0;
|
||||||
|
for (int i = 0; i < cfg.dungeons.Length; i++)
|
||||||
|
if (cfg.dungeons[i] != null && cfg.dungeons[i].enabled_ != 0) n++;
|
||||||
|
if (cfg.maxDungeons > 0) n = Mathf.Min(n, cfg.maxDungeons);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
// 진입점 — 섬 준비가 끝난 뒤 1회
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
public static void Apply(WLIslandSettings cfg, Scene scene)
|
||||||
|
{
|
||||||
|
if (cfg == null || cfg.gateFlowEnabled == 0) return;
|
||||||
|
|
||||||
|
Vector3 farmCenter;
|
||||||
|
bool haveFarm = HideFarms(cfg, scene, out farmCenter);
|
||||||
|
|
||||||
|
HookIslandActivation(cfg, scene);
|
||||||
|
|
||||||
|
s_farmCenter = farmCenter;
|
||||||
|
s_haveFarm = haveFarm;
|
||||||
|
|
||||||
|
RefreshPlatform(cfg, scene);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Vector3 s_farmCenter;
|
||||||
|
static bool s_haveFarm;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 🔴 던전 하나 = **발판 한 자리**(WL-816g · PD 정정 2026-09-13
|
||||||
|
/// 「던전 열기는 한 번만 · 이후 같은 자리에 또 등장해서는 안 된다」).
|
||||||
|
/// 값을 치르면 그 자리는 영영 비고, 다음 던전의 발판은 **표가 지정한 다른 자리**에 선다.
|
||||||
|
/// </summary>
|
||||||
|
public static Vector3 PlatformPosFor(WLIslandSettings cfg, int index)
|
||||||
|
{
|
||||||
|
var d = NthEnabled(cfg, index);
|
||||||
|
Vector3 p;
|
||||||
|
if (d != null && d.platformUseFarm == 0) p = d.platformPosition;
|
||||||
|
else if (cfg.platformUseFarmPosition != 0 && s_haveFarm) p = s_farmCenter;
|
||||||
|
else if (d != null) p = d.platformPosition;
|
||||||
|
else p = cfg.platformPosition;
|
||||||
|
|
||||||
|
p += cfg.platformOffset;
|
||||||
|
return SnapToGround(cfg, p);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>좌표 위에서 아래로 쏘아 지면 높이를 맞춘다(맞는 것이 없으면 준 값 그대로).</summary>
|
||||||
|
static Vector3 SnapToGround(WLIslandSettings cfg, Vector3 p)
|
||||||
|
{
|
||||||
|
if (cfg.platformSnapToGround == 0) return p;
|
||||||
|
RaycastHit hit;
|
||||||
|
if (Physics.Raycast(new Vector3(p.x, p.y + 30f, p.z), Vector3.down, out hit, 60f,
|
||||||
|
~0, QueryTriggerInteraction.Ignore))
|
||||||
|
return new Vector3(p.x, hit.point.y, p.z);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>발판을 지금 해금 상태에 맞게 만들거나 지운다.</summary>
|
||||||
|
public static void RefreshPlatform(WLIslandSettings cfg, Scene scene)
|
||||||
|
{
|
||||||
|
int unlocked = UnlockedCount(cfg);
|
||||||
|
int usable = UsableDungeonCount(cfg);
|
||||||
|
|
||||||
|
if (WLGoldPlatform.Instance != null) { Object.Destroy(WLGoldPlatform.Instance.gameObject); WLGoldPlatform.Instance = null; }
|
||||||
|
|
||||||
|
if (unlocked >= usable)
|
||||||
|
{
|
||||||
|
LastLog = "발판 없음 — 던전 " + unlocked + "/" + usable + " 전부 해금됨";
|
||||||
|
WLIslandBridge.Log(cfg, LastLog);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int price = PriceFor(cfg, unlocked);
|
||||||
|
Vector3 pos = PlatformPosFor(cfg, unlocked);
|
||||||
|
|
||||||
|
// 🔴 값을 치른 자리에 다음 발판이 곧바로 생기면, **가만히 서 있기만 해도** 던전이 연달아 팔린다
|
||||||
|
// (골드가 많을 때 실측됨). FI 섬 확장처럼 「한 번 내려왔다가 다시 올라오기」를 요구한다.
|
||||||
|
var runner = WLIslandBridge.Runner;
|
||||||
|
if (cfg.platformRequireStepOff != 0 && runner != null && PlayerInRange(cfg, pos))
|
||||||
|
{
|
||||||
|
runner.StartCoroutine(Co_SpawnWhenClear(cfg, scene, pos, price, unlocked, usable));
|
||||||
|
LastLog = "발판 대기 — 플레이어가 자리에서 내려오면 다음 발판(가격 " + price + ")을 놓는다";
|
||||||
|
WLIslandBridge.Log(cfg, LastLog);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var p = WLGoldPlatform.Create(cfg, scene, pos, price, unlocked);
|
||||||
|
if (p != null) { PlatformsMade++; LastPlatformPos = pos; }
|
||||||
|
LastLog = "골드 납품 발판 배치 — " + pos.ToString("F2") + " · 가격 " + price +
|
||||||
|
" · 다음 던전 #" + (unlocked + 1) + "/" + usable;
|
||||||
|
WLIslandBridge.Log(cfg, LastLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
static IEnumerator Co_SpawnWhenClear(WLIslandSettings cfg, Scene scene, Vector3 pos, int price, int unlocked, int usable)
|
||||||
|
{
|
||||||
|
float t = 0f;
|
||||||
|
while (t < cfg.platformStepOffTimeoutSeconds && PlayerInRange(cfg, pos))
|
||||||
|
{
|
||||||
|
t += Time.unscaledDeltaTime;
|
||||||
|
yield return null;
|
||||||
|
}
|
||||||
|
if (WLGoldPlatform.Instance != null) yield break; // 그 사이 다른 경로로 이미 생겼다
|
||||||
|
var p = WLGoldPlatform.Create(cfg, scene, pos, price, unlocked);
|
||||||
|
if (p != null) { PlatformsMade++; LastPlatformPos = pos; }
|
||||||
|
LastLog = "골드 납품 발판 배치 — " + pos.ToString("F2") + " · 가격 " + price +
|
||||||
|
" · 다음 던전 #" + (unlocked + 1) + "/" + usable;
|
||||||
|
WLIslandBridge.Log(cfg, LastLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool PlayerInRange(WLIslandSettings cfg, Vector3 pos)
|
||||||
|
{
|
||||||
|
var fi = Object.FindFirstObjectByType<CryingSnow.FarmingIsland.PlayerController>();
|
||||||
|
if (fi == null) return false;
|
||||||
|
float r = cfg.platformTriggerSize * 0.5f + 0.6f;
|
||||||
|
var d = fi.transform.position - pos;
|
||||||
|
d.y = 0f;
|
||||||
|
return d.sqrMagnitude <= r * r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
// 지불 완료 → 던전 1개 생성
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
public static void OnPaid(WLIslandSettings cfg, Vector3 platformPos)
|
||||||
|
{
|
||||||
|
if (cfg == null) return;
|
||||||
|
|
||||||
|
int unlocked = UnlockedCount(cfg);
|
||||||
|
int usable = UsableDungeonCount(cfg);
|
||||||
|
if (unlocked >= usable) return;
|
||||||
|
|
||||||
|
SetUnlockedCount(cfg, unlocked + 1);
|
||||||
|
Unlocks++;
|
||||||
|
|
||||||
|
var d = NthEnabled(cfg, unlocked);
|
||||||
|
var scene = SceneManager.GetSceneByName(cfg.islandSceneName);
|
||||||
|
if (!scene.IsValid() || !scene.isLoaded) scene = SceneManager.GetActiveScene();
|
||||||
|
|
||||||
|
if (d != null) WLIslandBridge.SpawnOneGate(cfg, d, scene);
|
||||||
|
|
||||||
|
LastLog = "🔴 지불 완료 — 던전 생성 " + (d != null ? d.displayName : "(표 없음)") +
|
||||||
|
" · 해금 " + (unlocked + 1) + "/" + usable +
|
||||||
|
" · 이 자리(" + platformPos.ToString("F1") + ")의 발판은 영영 사라진다";
|
||||||
|
WLIslandBridge.Log(cfg, LastLog);
|
||||||
|
|
||||||
|
RefreshPlatform(cfg, scene);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WLDungeonDef NthEnabled(WLIslandSettings cfg, int n)
|
||||||
|
{
|
||||||
|
if (cfg == null || cfg.dungeons == null) return null;
|
||||||
|
int k = 0;
|
||||||
|
for (int i = 0; i < cfg.dungeons.Length; i++)
|
||||||
|
{
|
||||||
|
var d = cfg.dungeons[i];
|
||||||
|
if (d == null || d.enabled_ == 0) continue;
|
||||||
|
if (k == n) return d;
|
||||||
|
k++;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
// 밭 숨기기
|
||||||
|
// 🔴 `GameObject.SetActive(false)` 를 쓰지 않는다 — 나중에 섬이 열릴 때
|
||||||
|
// `Farm.Animate(instant:false)` 가 그 오브젝트에서 코루틴을 시작하는데,
|
||||||
|
// 꺼져 있으면 Unity 가 「inactive 라 코루틴을 못 돌린다」 에러를 찍는다(콘솔 error 0 위반).
|
||||||
|
// 그래서 **렌더러·콜라이더만** 끈다 = FI 흐름은 그대로 돌고 화면에서만 사라진다.
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
static readonly List<Renderer> s_hiddenRends = new List<Renderer>(128);
|
||||||
|
static readonly List<Collider> s_hiddenCols = new List<Collider>(128);
|
||||||
|
static readonly HashSet<FIIsland> s_hooked = new HashSet<FIIsland>();
|
||||||
|
|
||||||
|
static bool HideFarms(WLIslandSettings cfg, Scene scene, out Vector3 center)
|
||||||
|
{
|
||||||
|
center = Vector3.zero;
|
||||||
|
if (cfg.hideFarms == 0) return false;
|
||||||
|
|
||||||
|
var farms = Object.FindObjectsByType<FIFarm>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||||||
|
bool haveCenter = false;
|
||||||
|
|
||||||
|
for (int i = 0; i < farms.Length; i++)
|
||||||
|
{
|
||||||
|
var f = farms[i];
|
||||||
|
if (f == null) continue;
|
||||||
|
if (scene.IsValid() && f.gameObject.scene != scene) continue;
|
||||||
|
|
||||||
|
// 발판 자리 = **지금 보이는 밭**(= 해금된 섬 위의 밭) **하나**의 한가운데
|
||||||
|
bool visible = f.gameObject.activeInHierarchy;
|
||||||
|
Bounds b = new Bounds();
|
||||||
|
bool boundsSet = false;
|
||||||
|
|
||||||
|
var rends = f.GetComponentsInChildren<Renderer>(true);
|
||||||
|
for (int r = 0; r < rends.Length; r++)
|
||||||
|
{
|
||||||
|
if (rends[r] == null || !rends[r].enabled) continue;
|
||||||
|
if (visible)
|
||||||
|
{
|
||||||
|
if (!boundsSet) { b = rends[r].bounds; boundsSet = true; }
|
||||||
|
else b.Encapsulate(rends[r].bounds);
|
||||||
|
}
|
||||||
|
rends[r].enabled = false; s_hiddenRends.Add(rends[r]); RenderersHidden++;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cols = f.GetComponentsInChildren<Collider>(true);
|
||||||
|
for (int c = 0; c < cols.Length; c++)
|
||||||
|
{
|
||||||
|
if (cols[c] == null || !cols[c].enabled) continue;
|
||||||
|
cols[c].enabled = false; s_hiddenCols.Add(cols[c]);
|
||||||
|
}
|
||||||
|
|
||||||
|
FarmsHidden++;
|
||||||
|
|
||||||
|
if (visible && !haveCenter)
|
||||||
|
{
|
||||||
|
center = boundsSet ? new Vector3(b.center.x, b.min.y, b.center.z) : f.transform.position;
|
||||||
|
haveCenter = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return haveCenter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>섬이 나중에 열리면 ① 그 섬의 밭을 다시 숨기고 ② 타일 재질을 기존 타일과 맞춘다(§1).</summary>
|
||||||
|
static void HookIslandActivation(WLIslandSettings cfg, Scene scene)
|
||||||
|
{
|
||||||
|
if (cfg.hideFarms == 0 && cfg.matchNewTileMaterials == 0) return;
|
||||||
|
var islands = Object.FindObjectsByType<FIIsland>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||||||
|
for (int i = 0; i < islands.Length; i++)
|
||||||
|
{
|
||||||
|
var isl = islands[i];
|
||||||
|
if (isl == null) continue;
|
||||||
|
if (scene.IsValid() && isl.gameObject.scene != scene) continue;
|
||||||
|
if (s_hooked.Contains(isl)) continue;
|
||||||
|
s_hooked.Add(isl);
|
||||||
|
var captured = isl;
|
||||||
|
isl.OnActivated += () => OnIslandActivated(cfg, captured);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void OnIslandActivated(WLIslandSettings cfg, FIIsland isl)
|
||||||
|
{
|
||||||
|
if (cfg == null || cfg.gateFlowEnabled == 0) return;
|
||||||
|
if (cfg.hideFarms == 0 && cfg.matchNewTileMaterials == 0) return;
|
||||||
|
var runner = WLIslandBridge.Runner;
|
||||||
|
if (runner == null || isl == null) return;
|
||||||
|
runner.StartCoroutine(Co_ReHide(cfg, isl));
|
||||||
|
}
|
||||||
|
|
||||||
|
static IEnumerator Co_ReHide(WLIslandSettings cfg, FIIsland isl)
|
||||||
|
{
|
||||||
|
// 확장 애니메이션(DOScale 1초) + 소품 배치(AnimateSoils 0.05초 × 30칸) + 룩 재적용
|
||||||
|
// (`WLIslandLookSettings.rebuildDelaySeconds` = 1.4 실측)이 끝나기를 기다린다.
|
||||||
|
yield return new WaitForSeconds(cfg.farmReHideDelaySeconds);
|
||||||
|
if (isl == null) yield break;
|
||||||
|
|
||||||
|
MatchTileMaterials(cfg, isl);
|
||||||
|
|
||||||
|
if (cfg.hideFarms == 0) yield break;
|
||||||
|
var farms = isl.GetComponentsInChildren<FIFarm>(true);
|
||||||
|
for (int i = 0; i < farms.Length; i++)
|
||||||
|
{
|
||||||
|
if (farms[i] == null) continue;
|
||||||
|
var rends = farms[i].GetComponentsInChildren<Renderer>(true);
|
||||||
|
for (int r = 0; r < rends.Length; r++)
|
||||||
|
if (rends[r] != null && rends[r].enabled) { rends[r].enabled = false; s_hiddenRends.Add(rends[r]); RenderersHidden++; }
|
||||||
|
var cols = farms[i].GetComponentsInChildren<Collider>(true);
|
||||||
|
for (int c = 0; c < cols.Length; c++)
|
||||||
|
if (cols[c] != null && cols[c].enabled) { cols[c].enabled = false; s_hiddenCols.Add(cols[c]); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
// §1 새 타일 경계 색차 — 남은 몫(외곽선 패스)
|
||||||
|
//
|
||||||
|
// 실측(2026-09-13 · Play):
|
||||||
|
// · **바닥 색 자체는 Lead 의 `b1e58d795`(대비 복제 제외)로 해결됐다** — 새로 연 타일도
|
||||||
|
// 기존 타일과 **같은 머티리얼 인스턴스** `Farm_IslandTop_Demo#83780` 을 쓴다.
|
||||||
|
// · 다만 새로 연 타일은 슬롯이 **1개**뿐이다(기존 타일은 2개 = 바닥 + `WL/HullOutline`).
|
||||||
|
// `WLReferenceLook.Apply` 가 씬 로드 때 **활성 렌더러만** 훑는데 잠긴 타일은 꺼져 있어
|
||||||
|
// 외곽선 패스를 못 받는다. 그래서 **경계에서 테두리 유무가 갈린다.**
|
||||||
|
// 조치: 열린 직후, 이미 열려 있는 타일의 머티리얼 배열을 **그대로** 물려준다(새 머티리얼 0개).
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
public static int TilesMatched, SlotsMatched;
|
||||||
|
|
||||||
|
static void MatchTileMaterials(WLIslandSettings cfg, FIIsland isl)
|
||||||
|
{
|
||||||
|
if (cfg.matchNewTileMaterials == 0 || isl == null) return;
|
||||||
|
|
||||||
|
var mine = isl.GetComponentsInChildren<Renderer>(true);
|
||||||
|
if (mine.Length == 0) return;
|
||||||
|
|
||||||
|
// 기준 = 이미 열려 있고 **슬롯이 더 많은** 타일(= 외곽선을 받은 타일)
|
||||||
|
FIIsland refIsl = null;
|
||||||
|
int bestSlots = 0;
|
||||||
|
var all = Object.FindObjectsByType<FIIsland>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||||||
|
for (int i = 0; i < all.Length; i++)
|
||||||
|
{
|
||||||
|
var o = all[i];
|
||||||
|
if (o == null || o == isl || !o.IsUnlocked || !o.gameObject.activeInHierarchy) continue;
|
||||||
|
if (o.gameObject.scene != isl.gameObject.scene) continue;
|
||||||
|
var r = o.GetComponent<MeshRenderer>();
|
||||||
|
if (r == null) continue;
|
||||||
|
int n = r.sharedMaterials != null ? r.sharedMaterials.Length : 0;
|
||||||
|
if (n > bestSlots) { bestSlots = n; refIsl = o; }
|
||||||
|
}
|
||||||
|
if (refIsl == null) return;
|
||||||
|
|
||||||
|
// 🔴 이름으로 짝을 지으면 **타일 본체**(GameObject 이름이 `Island (3)` / `Island (6)` 처럼 다 다르다)가
|
||||||
|
// 안 맞는다(실측 — 첫 시도에서 본체만 슬롯 1개로 남았다). 루트 기준 **상대 경로**로 짝 짓는다.
|
||||||
|
var refs = refIsl.GetComponentsInChildren<Renderer>(true);
|
||||||
|
var byPath = new Dictionary<string, Renderer>(refs.Length);
|
||||||
|
for (int j = 0; j < refs.Length; j++)
|
||||||
|
if (refs[j] != null) byPath[RelPath(refIsl.transform, refs[j].transform)] = refs[j];
|
||||||
|
|
||||||
|
int changed = 0;
|
||||||
|
for (int i = 0; i < mine.Length; i++)
|
||||||
|
{
|
||||||
|
var m = mine[i];
|
||||||
|
if (m == null || m is ParticleSystemRenderer) continue;
|
||||||
|
var mm = m.sharedMaterials;
|
||||||
|
if (mm == null || mm.Length == 0 || mm[0] == null) continue;
|
||||||
|
|
||||||
|
Renderer r;
|
||||||
|
if (!byPath.TryGetValue(RelPath(isl.transform, m.transform), out r) || r == null) continue;
|
||||||
|
var rm = r.sharedMaterials;
|
||||||
|
if (rm == null || rm.Length <= mm.Length || rm[0] != mm[0]) continue;
|
||||||
|
m.sharedMaterials = rm; // 같은 인스턴스 배열 = 픽셀이 같아진다
|
||||||
|
changed++; SlotsMatched += rm.Length - mm.Length;
|
||||||
|
}
|
||||||
|
if (changed > 0)
|
||||||
|
{
|
||||||
|
TilesMatched++;
|
||||||
|
WLIslandBridge.Log(cfg, "새 타일 재질 맞춤 — " + isl.name + " 렌더러 " + changed +
|
||||||
|
"개를 기존 타일(" + refIsl.name + ")과 같은 배열로(외곽선 포함)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>타일 루트 기준 상대 경로(프리팹 인스턴스끼리 짝 짓는 열쇠 · 루트 자신은 빈 문자열).</summary>
|
||||||
|
static string RelPath(Transform root, Transform t)
|
||||||
|
{
|
||||||
|
if (t == root) return "";
|
||||||
|
string s = t.name;
|
||||||
|
var p = t.parent;
|
||||||
|
while (p != null && p != root) { s = p.name + "/" + s; p = p.parent; }
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>우리가 끈 것만 되살린다(C8).</summary>
|
||||||
|
public static void RestoreFarms()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < s_hiddenRends.Count; i++) if (s_hiddenRends[i] != null) s_hiddenRends[i].enabled = true;
|
||||||
|
for (int i = 0; i < s_hiddenCols.Count; i++) if (s_hiddenCols[i] != null) s_hiddenCols[i].enabled = true;
|
||||||
|
s_hiddenRends.Clear();
|
||||||
|
s_hiddenCols.Clear();
|
||||||
|
s_hooked.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
|
// 골드 납품 발판 — 더미 모양 + FI Purchaser 재사용
|
||||||
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 「골드 납품 발판」 1개. 모양은 **더미**(원판 + 테두리 + 안내)로 충분하다(PD 합의).
|
||||||
|
/// 지불은 **FI `Purchaser` 원본 프리팹**이 그대로 한다 — 우리는 보이지 않는 더미 `Island` 를
|
||||||
|
/// 물려 주고 `OnActivated` 만 받는다. 교체는 `platformPrefabPath` 값 하나.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class WLGoldPlatform : MonoBehaviour
|
||||||
|
{
|
||||||
|
public static WLGoldPlatform Instance;
|
||||||
|
|
||||||
|
public int Price;
|
||||||
|
public int NextDungeonIndex;
|
||||||
|
public FIPurchaser Purchaser;
|
||||||
|
public FIIsland DummyTarget;
|
||||||
|
public bool Paid;
|
||||||
|
|
||||||
|
public const string ObjectName = "~WL_GoldPlatform";
|
||||||
|
|
||||||
|
public static WLGoldPlatform Create(WLIslandSettings cfg, Scene scene, Vector3 pos, int price, int nextIndex)
|
||||||
|
{
|
||||||
|
GameObject root = null;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(cfg.platformPrefabPath))
|
||||||
|
{
|
||||||
|
var prefab = WLIslandAssets.LoadPrefab(cfg.platformPrefabPath);
|
||||||
|
if (prefab != null) root = Object.Instantiate(prefab);
|
||||||
|
}
|
||||||
|
// 🔴 PD 정정(2026-09-13) 「던전 열기 골드 발판은 **원래 에셋에 있던 것**을 활용하면 돼.」
|
||||||
|
// → 기본값은 **빈 루트 + FI `Purchaser` 프리팹 하나**다. 새 모델을 만들지 않는다.
|
||||||
|
// platformShowPad = 1 로 켜면 예전의 더미 원판이 다시 깔린다(기본 0).
|
||||||
|
if (root == null) root = cfg.platformShowPad != 0 ? BuildPad(cfg) : new GameObject("platform");
|
||||||
|
|
||||||
|
root.name = ObjectName;
|
||||||
|
root.transform.position = pos;
|
||||||
|
if (scene.IsValid()) SceneManager.MoveGameObjectToScene(root, scene);
|
||||||
|
|
||||||
|
var self = root.AddComponent<WLGoldPlatform>();
|
||||||
|
self.Price = price;
|
||||||
|
self.NextDungeonIndex = nextIndex;
|
||||||
|
Instance = self;
|
||||||
|
|
||||||
|
self.AttachPurchaser(cfg, price);
|
||||||
|
|
||||||
|
// 🔴 가격 숫자는 **FI 가격판 그대로**다(위 프리팹이 「10」을 띄운다). 여기 글자는 **무엇이 열리는지**만 알린다
|
||||||
|
// — 모델이 아니라 글자 하나고, `platformShowLabel = 0` 이면 FI 원본 모습 100 % 가 된다.
|
||||||
|
if (cfg.platformShowLabel != 0)
|
||||||
|
WLIslandLabel.Attach(root.transform, new Vector3(0f, cfg.platformLabelHeight, 0f),
|
||||||
|
string.Format(cfg.platformLabelFormat, price));
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 선택 사항 — 낮은 원판(발 디딜 자리 표시). 🔴 기본은 **꺼져 있다**(PD: 새 모델 금지).
|
||||||
|
/// FI 가격판이 바닥에 눕는 판(`Price` · 4×2 m · X 90°)이라 원판을 깔면 **그 판을 덮어 버린다**(실측).
|
||||||
|
/// </summary>
|
||||||
|
static GameObject BuildPad(WLIslandSettings cfg)
|
||||||
|
{
|
||||||
|
float r = cfg.platformRadius <= 0f ? 1.6f : cfg.platformRadius;
|
||||||
|
|
||||||
|
var root = new GameObject("platform");
|
||||||
|
var baseMat = WLIslandLook.ToonMaterial(cfg.platformColor * 0.55f, cfg.platformUseToon != 0);
|
||||||
|
|
||||||
|
MakeDisc(root.transform, "rim", new Vector3(0f, 0.005f, 0f), r * 2f, 0.01f, baseMat);
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void MakeDisc(Transform parent, string name, Vector3 pos, float diameter, float height, Material mat)
|
||||||
|
{
|
||||||
|
var go = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
|
||||||
|
go.name = name;
|
||||||
|
go.transform.SetParent(parent, false);
|
||||||
|
go.transform.localPosition = pos;
|
||||||
|
go.transform.localScale = new Vector3(diameter, height * 0.5f, diameter); // Cylinder 는 높이가 2 단위
|
||||||
|
var col = go.GetComponent<Collider>();
|
||||||
|
if (col != null) Object.Destroy(col); // 더미 — 길·NavMesh 를 건드리지 않는다
|
||||||
|
var rd = go.GetComponent<Renderer>();
|
||||||
|
if (rd != null && mat != null) rd.sharedMaterial = mat;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// 🔴 FI Purchaser 재사용
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
void AttachPurchaser(WLIslandSettings cfg, int price)
|
||||||
|
{
|
||||||
|
var mgr = FIIslandManager.Instance;
|
||||||
|
if (mgr == null) { WLIslandBridge.Log(cfg, "발판: IslandManager 없음 — Purchaser 재사용 불가"); return; }
|
||||||
|
|
||||||
|
var prefab = PurchaserPrefab(mgr);
|
||||||
|
if (prefab == null) { WLIslandBridge.Log(cfg, "발판: FI Purchaser 프리팹을 못 찾았다 — 지불 불가"); return; }
|
||||||
|
|
||||||
|
// 보이지 않는 더미 `Island` — Purchaser 가 요구하는 자리만 채운다(메시 없음 = 안 보인다).
|
||||||
|
// 🔴 `IslandManager` 의 자식이 **아니다** → 섬 격자·가격·세이브에 절대 끼어들지 않는다
|
||||||
|
// (`IslandManager.Awake` 는 `GetComponentsInChildren<Island>()` 로만 섬을 모은다).
|
||||||
|
var dummyGo = new GameObject("~WL_GoldPlatformTarget");
|
||||||
|
dummyGo.SetActive(false); // Awake 를 Activate() 시점까지 미룬다
|
||||||
|
dummyGo.transform.SetParent(transform, false);
|
||||||
|
var dummy = dummyGo.AddComponent<FIIsland>();
|
||||||
|
dummy.HasRoad = true; // 🔴 false 면 Awake 가 비어 있는 roadMesh 를 건드려 죽는다
|
||||||
|
dummy.IsBridge = false;
|
||||||
|
dummy.IsUnlocked = false;
|
||||||
|
var mf = dummyGo.GetComponent<MeshFilter>();
|
||||||
|
if (mf != null) mf.sharedMesh = null; // 메시 0 = 화면에 아무것도 안 나온다
|
||||||
|
DummyTarget = dummy;
|
||||||
|
dummy.OnActivated += OnPurchaseComplete;
|
||||||
|
|
||||||
|
var p = Object.Instantiate(prefab, transform.position, Quaternion.identity);
|
||||||
|
p.name = "purchaser";
|
||||||
|
p.transform.SetParent(transform, true);
|
||||||
|
Purchaser = p;
|
||||||
|
|
||||||
|
// 🔴 FI 가격판은 프리팹 안에서 **루트보다 앞·아래**에 놓여 있다(실측 2026-09-13:
|
||||||
|
// Frame/Price/Coin 이 전부 y = -0.49 · z = 루트+(-1/0/+1) — 잠긴 섬 자리의 **물 위**에 눕는 판이다).
|
||||||
|
// 섬 위(y≈0)에 그대로 놓으면 **지면 아래로 들어가 안 보인다** → 프리팹 전체의 렌더 범위를 재서
|
||||||
|
// 「밭 한가운데 · 바닥 바로 위」로 맞춘다. 프리팹은 한 글자도 고치지 않는다.
|
||||||
|
if (cfg.platformCenterOnPlate != 0)
|
||||||
|
{
|
||||||
|
var rends = p.GetComponentsInChildren<Renderer>(true);
|
||||||
|
Bounds b = new Bounds(); bool set = false;
|
||||||
|
for (int i = 0; i < rends.Length; i++)
|
||||||
|
{
|
||||||
|
if (rends[i] == null) continue;
|
||||||
|
if (!set) { b = rends[i].bounds; set = true; } else b.Encapsulate(rends[i].bounds);
|
||||||
|
}
|
||||||
|
if (set)
|
||||||
|
{
|
||||||
|
var t = transform.position;
|
||||||
|
p.transform.position += new Vector3(t.x - b.center.x,
|
||||||
|
t.y + cfg.platformPlateLift - b.min.y,
|
||||||
|
t.z - b.center.z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 트리거를 **발판(가격판) 크기**로 줄인다(원본 프리팹은 섬 1칸 8 m 를 덮는 十자 두 개다).
|
||||||
|
var boxes = p.GetComponentsInChildren<BoxCollider>(true);
|
||||||
|
float s = cfg.platformTriggerSize <= 0f ? 3.2f : cfg.platformTriggerSize;
|
||||||
|
var localCenter = p.transform.InverseTransformPoint(transform.position);
|
||||||
|
for (int i = 0; i < boxes.Length; i++)
|
||||||
|
{
|
||||||
|
if (boxes[i] == null) continue;
|
||||||
|
boxes[i].size = new Vector3(s, cfg.platformTriggerHeight, s);
|
||||||
|
boxes[i].center = new Vector3(localCenter.x, cfg.platformTriggerHeight * 0.5f, localCenter.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔴 `Init` 은 `Purchaser.Start()` 보다 먼저여야 한다(Start 가 가격판을 그린다) — 같은 프레임이라 안전.
|
||||||
|
p.Init(dummy, price, new FIPurchaserData(Vector3Int.RoundToInt(transform.position), price));
|
||||||
|
|
||||||
|
WLIslandBridge.Log(cfg, "발판: FI Purchaser 재사용 — 가격 " + price +
|
||||||
|
" · 트리거 " + s.ToString("F1") + " m · 더미 Island 연결");
|
||||||
|
}
|
||||||
|
|
||||||
|
static System.Reflection.FieldInfo s_purchaserField;
|
||||||
|
|
||||||
|
/// <summary>FI 가 섬 구입에 쓰는 **바로 그 프리팹**(가격판·코인 아이콘·물보라·SFX 가 전부 들어 있다).</summary>
|
||||||
|
static FIPurchaser PurchaserPrefab(FIIslandManager mgr)
|
||||||
|
{
|
||||||
|
if (s_purchaserField == null)
|
||||||
|
s_purchaserField = typeof(FIIslandManager).GetField("purchaserPrefab",
|
||||||
|
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
|
||||||
|
if (s_purchaserField == null) return null;
|
||||||
|
return s_purchaserField.GetValue(mgr) as FIPurchaser;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnPurchaseComplete()
|
||||||
|
{
|
||||||
|
if (Paid) return;
|
||||||
|
Paid = true;
|
||||||
|
var cfg = WLIslandSettings.Instance;
|
||||||
|
var pos = transform.position;
|
||||||
|
var runner = WLIslandBridge.Runner;
|
||||||
|
// 🔴 이 오브젝트는 곧 지워진다 — 코루틴은 씬을 넘어 사는 러너가 돌린다.
|
||||||
|
if (runner != null) runner.StartCoroutine(Co_AfterPaid(cfg, pos));
|
||||||
|
else WLIslandGateFlow.OnPaid(cfg, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
static IEnumerator Co_AfterPaid(WLIslandSettings cfg, Vector3 pos)
|
||||||
|
{
|
||||||
|
// 물보라·DOJump(1초)가 끝난 뒤에 발판을 갈아 끼운다(진행 중 트윈의 대상이 사라지지 않게).
|
||||||
|
float w = cfg != null ? cfg.platformRespawnDelaySeconds : 0.4f;
|
||||||
|
if (w > 0f) yield return new WaitForSeconds(w);
|
||||||
|
WLIslandGateFlow.OnPaid(cfg, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnDestroy() { if (Instance == this) Instance = null; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 97243a3a73c38054492e250bd7e07f52
|
||||||
|
|
@ -27,6 +27,20 @@ namespace WL.Island
|
||||||
/// <summary>이 이름들의 씬이 붙으면 「몬스터 없는 맵」으로 본다.</summary>
|
/// <summary>이 이름들의 씬이 붙으면 「몬스터 없는 맵」으로 본다.</summary>
|
||||||
static readonly string[] NoMobScenes = { "Level01", "Level02", "Level03", "WL_FarmLook" };
|
static readonly string[] NoMobScenes = { "Level01", "Level02", "Level03", "WL_FarmLook" };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 🔴 WL-816g — **던전 씬도 같은 문제다.** 던전(`WL_Dungeon01`·`02`)은 아레나 복사본이라
|
||||||
|
/// `MobControlMgrList` 가 **한 개도 없다**(실측: 세 씬 전부 0개 · `WL_ArenaProto` 포함).
|
||||||
|
/// 몹은 `StageDirector` 가 스테이지 시작 때 스포너로 만든다 — 로딩 칸 0 을 채워 줄 주인이 없다.
|
||||||
|
/// 이름을 더 적는 대신 **던전 표(`WLIslandSettings.dungeons[].sceneName`)를 그대로 본다** —
|
||||||
|
/// 표에 줄을 추가하면 그 씬도 자동으로 포함된다.
|
||||||
|
/// </summary>
|
||||||
|
static bool IsDungeonScene(string sceneName)
|
||||||
|
{
|
||||||
|
var cfg = WLIslandSettings.Instance;
|
||||||
|
if (cfg == null || cfg.enabled_ == 0) return false;
|
||||||
|
return cfg.FindBySceneName(sceneName) != null;
|
||||||
|
}
|
||||||
|
|
||||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
||||||
static void Hook()
|
static void Hook()
|
||||||
{
|
{
|
||||||
|
|
@ -39,6 +53,7 @@ namespace WL.Island
|
||||||
bool match = false;
|
bool match = false;
|
||||||
for (int i = 0; i < NoMobScenes.Length; i++)
|
for (int i = 0; i < NoMobScenes.Length; i++)
|
||||||
if (scene.name == NoMobScenes[i]) { match = true; break; }
|
if (scene.name == NoMobScenes[i]) { match = true; break; }
|
||||||
|
if (!match) match = IsDungeonScene(scene.name);
|
||||||
if (!match) return;
|
if (!match) return;
|
||||||
|
|
||||||
var host = new GameObject("~WL_IslandLoadingFix");
|
var host = new GameObject("~WL_IslandLoadingFix");
|
||||||
|
|
|
||||||
|
|
@ -41,14 +41,79 @@ namespace WL.Island
|
||||||
|
|
||||||
public static class WLIslandSave
|
public static class WLIslandSave
|
||||||
{
|
{
|
||||||
/// <summary>`IslandManager.Awake` 의 1줄 훅이 부른다. 끄면 받은 값을 그대로 돌려준다.</summary>
|
/// <summary>
|
||||||
|
/// `IslandManager.Awake` 의 **기존 1줄 훅**이 부른다. 끄면 받은 값을 그대로 돌려준다.
|
||||||
|
///
|
||||||
|
/// 🔴 WL-816g — 이 호출은 `IslandManager.Awake` 안에서 `LoadGameData()` **바로 앞**에 있다
|
||||||
|
/// (`IslandManager.cs:119` vs `:120`). 그래서 「새 게임 시작 골드」와 「세이브 1회 초기화」를
|
||||||
|
/// 걸 수 있는 **유일한 시점**이다 — 덕분에 FI 원본에 새 줄을 한 줄도 넣지 않았다.
|
||||||
|
/// </summary>
|
||||||
public static string ResolveSaveName(string sceneName)
|
public static string ResolveSaveName(string sceneName)
|
||||||
{
|
{
|
||||||
var cfg = WLIslandSaveSettings.Get();
|
var cfg = WLIslandSaveSettings.Get();
|
||||||
if (cfg == null || cfg.enabled_ == 0 || string.IsNullOrEmpty(cfg.saveName)) return sceneName;
|
string resolved = sceneName;
|
||||||
if (cfg.verboseLog != 0)
|
if (cfg != null && cfg.enabled_ != 0 && !string.IsNullOrEmpty(cfg.saveName))
|
||||||
Debug.Log("[WL-816] 섬 세이브 이름 '" + sceneName + "' → '" + cfg.saveName + "' (경로 무관 고정)");
|
{
|
||||||
return cfg.saveName;
|
resolved = cfg.saveName;
|
||||||
|
if (cfg.verboseLog != 0)
|
||||||
|
Debug.Log("[WL-816] 섬 세이브 이름 '" + sceneName + "' → '" + resolved + "' (경로 무관 고정)");
|
||||||
|
}
|
||||||
|
|
||||||
|
WLIslandStart.Prepare(resolved);
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 새 게임의 시작 상태(WL-816g §3) — **시작 골드 10** + 필요하면 세이브 1회 초기화.
|
||||||
|
/// FI 기본 시작 골드는 500 이다(`IslandManager.cs:30` `startingMoney`).
|
||||||
|
/// </summary>
|
||||||
|
public static class WLIslandStart
|
||||||
|
{
|
||||||
|
public static int LastStartCoin = -1;
|
||||||
|
public static bool DidReset;
|
||||||
|
public static string LastLog = "";
|
||||||
|
|
||||||
|
const string kTokenKey = "WL_Island_SaveToken";
|
||||||
|
static System.Reflection.FieldInfo s_startingMoney;
|
||||||
|
|
||||||
|
/// <summary>`IslandManager.Awake` 안(= `LoadGameData()` 직전)에서 단 한 번 불린다.</summary>
|
||||||
|
public static void Prepare(string saveName)
|
||||||
|
{
|
||||||
|
var cfg = WLIslandSettings.Instance;
|
||||||
|
if (cfg == null || cfg.enabled_ == 0 || cfg.gateFlowEnabled == 0) return;
|
||||||
|
|
||||||
|
// ① 세이브 1회 초기화 — 토큰이 바뀌었을 때만(PD 에게 새 초기 상태를 보여 줄 때)
|
||||||
|
if (!string.IsNullOrEmpty(cfg.saveResetToken) &&
|
||||||
|
PlayerPrefs.GetString(kTokenKey, "") != cfg.saveResetToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string path = Application.persistentDataPath + "/" + saveName + ".dat";
|
||||||
|
if (System.IO.File.Exists(path)) { System.IO.File.Delete(path); DidReset = true; }
|
||||||
|
}
|
||||||
|
catch (System.Exception ex) { Debug.LogWarning("[WL-816g] 세이브 초기화 실패 — " + ex.Message); }
|
||||||
|
|
||||||
|
PlayerPrefs.SetString(kTokenKey, cfg.saveResetToken);
|
||||||
|
PlayerPrefs.SetInt(cfg.dungeonUnlockPrefsKey, 0); // 해금한 던전도 0 개로
|
||||||
|
PlayerPrefs.Save();
|
||||||
|
WLIslandBridge.ClearPending(); // 보류 보상도 비운다(정적 값까지)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ② 시작 골드 — `startingMoney` 는 `data == null`(세이브 없음)일 때만 쓰인다(IslandManager.cs:397)
|
||||||
|
if (cfg.applyStartCoin == 0) return;
|
||||||
|
var mgr = CryingSnow.FarmingIsland.IslandManager.Instance;
|
||||||
|
if (mgr == null) return;
|
||||||
|
|
||||||
|
if (s_startingMoney == null)
|
||||||
|
s_startingMoney = typeof(CryingSnow.FarmingIsland.IslandManager).GetField("startingMoney",
|
||||||
|
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
|
||||||
|
if (s_startingMoney == null) return;
|
||||||
|
|
||||||
|
s_startingMoney.SetValue(mgr, cfg.startCoin);
|
||||||
|
LastStartCoin = cfg.startCoin;
|
||||||
|
LastLog = "시작 골드 500 → " + cfg.startCoin + (DidReset ? " · 세이브 초기화함" : " · 기존 세이브 유지");
|
||||||
|
if (cfg.verboseLog != 0) Debug.Log("[WL-816g] " + LastLog);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,13 @@ namespace WL.Island
|
||||||
|
|
||||||
[Tooltip("비우면 내장 더미(문틀+포털판)를 만든다. 채우면 그 프리팹을 대신 놓는다(추후 교체용).")]
|
[Tooltip("비우면 내장 더미(문틀+포털판)를 만든다. 채우면 그 프리팹을 대신 놓는다(추후 교체용).")]
|
||||||
public string gatePrefabPath = "";
|
public string gatePrefabPath = "";
|
||||||
|
|
||||||
|
// ── WL-816g: 이 던전을 여는 「골드 납품 발판」 (한 던전당 한 자리 · 지불하면 그 자리는 영영 비워진다)
|
||||||
|
[Tooltip("1 이면 이 던전의 발판을 **지금 밭이 있는 자리**에 놓는다(첫 던전용). 0 이면 아래 좌표.")]
|
||||||
|
public int platformUseFarm;
|
||||||
|
|
||||||
|
[Tooltip("이 던전을 여는 발판의 월드 좌표(높이는 지면으로 자동 보정한다).")]
|
||||||
|
public Vector3 platformPosition = Vector3.zero;
|
||||||
}
|
}
|
||||||
|
|
||||||
[CreateAssetMenu(fileName = "WLIslandSettings", menuName = "WL/Island Settings", order = 340)]
|
[CreateAssetMenu(fileName = "WLIslandSettings", menuName = "WL/Island Settings", order = 340)]
|
||||||
|
|
@ -203,6 +210,109 @@ namespace WL.Island
|
||||||
[Tooltip("던전 입장 후 StageDirector.Enter 를 부르기까지 기다리는 초(맵·PC 로딩 여유).")]
|
[Tooltip("던전 입장 후 StageDirector.Enter 를 부르기까지 기다리는 초(맵·PC 로딩 여유).")]
|
||||||
public float dungeonEnterDelay = 1.5f;
|
public float dungeonEnterDelay = 1.5f;
|
||||||
|
|
||||||
|
// ───────────────────────────────── 초기 상태 · 골드 납품 발판 (WL-816g · PD 2026-09-13)
|
||||||
|
[Header("초기 상태 — 밭·던전 숨김 + 발판 1개 (WL-816g)")]
|
||||||
|
[Tooltip("0 이면 **지금 상태로 100 % 복귀**한다 — 밭도 던전도 처음부터 전부 보인다(C8 되돌리기).")]
|
||||||
|
public int gateFlowEnabled = 1;
|
||||||
|
|
||||||
|
[Tooltip("1 이면 작물을 심는 밭(Farm·Soil)을 화면에서 숨긴다. " +
|
||||||
|
"🔴 오브젝트를 끄지 않고 **렌더러·콜라이더만** 끈다 — 끄면 섬 확장 때 FI 가 " +
|
||||||
|
"꺼진 오브젝트에서 코루틴을 시작해 콘솔 에러가 난다.")]
|
||||||
|
public int hideFarms = 1;
|
||||||
|
|
||||||
|
[Tooltip("섬이 나중에 열렸을 때 그 밭을 다시 숨기고 타일 재질을 맞추기까지 기다리는 초 " +
|
||||||
|
"(확장 연출 1초 + 흙칸 연출 + 룩 재적용 1.4초가 끝날 시간).")]
|
||||||
|
public float farmReHideDelaySeconds = 3.0f;
|
||||||
|
|
||||||
|
[Tooltip("🔴 §1 — 1 이면 **새로 열린 타일**의 머티리얼 배열을 이미 열려 있는 타일과 **같게** 만든다. " +
|
||||||
|
"실측: 바닥 색 자체는 이미 같지만(main b1e58d795), 새 타일만 외곽선 패스(WL/HullOutline)를 " +
|
||||||
|
"못 받아 경계에서 테두리 유무가 갈린다. 새 머티리얼을 만들지 않고 기존 배열을 물려준다.")]
|
||||||
|
public int matchNewTileMaterials = 1;
|
||||||
|
|
||||||
|
[Tooltip("해금된 던전 수를 적어 두는 PlayerPrefs 키(FI 세이브 파일은 건드리지 않는다).")]
|
||||||
|
public string dungeonUnlockPrefsKey = "WL_Island_DungeonsUnlocked";
|
||||||
|
|
||||||
|
[Tooltip("던전을 1개·2개·3개…로 늘릴 때 내야 하는 골드(PD 「2개+@」). " +
|
||||||
|
"표보다 더 많이 열면 마지막 값을 이어 쓴다.")]
|
||||||
|
public int[] dungeonUnlockPrices = new int[] { 10, 30, 60, 100 };
|
||||||
|
|
||||||
|
[Tooltip("이 섬에서 열 수 있는 던전 수 상한(0 = dungeons 표 전체).")]
|
||||||
|
public int maxDungeons = 4;
|
||||||
|
|
||||||
|
[Header("골드 납품 발판(더미)")]
|
||||||
|
[Tooltip("1 이면 **지금 밭이 있는 자리**에 발판을 놓는다. 0 이면 아래 좌표를 쓴다.")]
|
||||||
|
public int platformUseFarmPosition = 1;
|
||||||
|
|
||||||
|
[Tooltip("platformUseFarmPosition = 0 일 때 쓸 월드 좌표.")]
|
||||||
|
public Vector3 platformPosition = Vector3.zero;
|
||||||
|
|
||||||
|
[Tooltip("밭 자리에서 얼마나 옮길지(미세 조정용).")]
|
||||||
|
public Vector3 platformOffset = Vector3.zero;
|
||||||
|
|
||||||
|
[Tooltip("비우면 **FI 원본 가격판(Purchaser 프리팹)만** 놓는다(PD 2026-09-13 정정 · 새 모델 금지). " +
|
||||||
|
"채우면 그 프리팹이 대신 놓인다 — 추후 교체는 이 값 하나.")]
|
||||||
|
public string platformPrefabPath = "";
|
||||||
|
|
||||||
|
[Tooltip("🔴 기본 0 — 1 로 켜면 FI 가격판 아래에 얇은 원판(발 디딜 자리)을 하나 더 깐다. " +
|
||||||
|
"FI 가격판이 **바닥에 눕는 판**이라 두꺼운 원판을 깔면 그 판을 덮는다(816g 실측).")]
|
||||||
|
public int platformShowPad = 0;
|
||||||
|
|
||||||
|
[Tooltip("1 이면 FI 가격판 전체의 렌더 범위를 재서 **밭 한가운데 · 바닥 바로 위**로 맞춘다. " +
|
||||||
|
"원본 프리팹은 물 위(y≈-0.5)에 눕도록 만들어져 있어 섬 위에 그대로 놓으면 지면 아래로 들어간다(816g 실측).")]
|
||||||
|
public int platformCenterOnPlate = 1;
|
||||||
|
|
||||||
|
[Tooltip("가격판을 바닥에서 얼마나 띄울지(m). 너무 낮으면 지면과 z-파이팅이 난다.")]
|
||||||
|
public float platformPlateLift = 0.03f;
|
||||||
|
|
||||||
|
[Tooltip("1 이면 「무엇이 열리는지」 알리는 글자를 발판 위에 띄운다(모델이 아니라 글자). 0 = FI 원본 모습 100 %.")]
|
||||||
|
public int platformShowLabel = 1;
|
||||||
|
|
||||||
|
[Tooltip("platformShowPad = 1 일 때 원판 반지름(m).")]
|
||||||
|
public float platformRadius = 1.7f;
|
||||||
|
|
||||||
|
[Tooltip("발판 색(더미).")]
|
||||||
|
public Color platformColor = new Color(0.92f, 0.72f, 0.24f, 1f);
|
||||||
|
|
||||||
|
[Tooltip("1 이면 아레나와 같은 Toon 셰이더를 먼저 찾고, 없으면 URP/Lit 로 만든다. " +
|
||||||
|
"🔴 0 이면 던전 입구 더미와 같은 URP/Lit 만 쓴다(안 보이면 0 으로 되돌릴 것).")]
|
||||||
|
public int platformUseToon = 1;
|
||||||
|
|
||||||
|
[Tooltip("안내 글 — {0} = 지금 내야 하는 골드.")]
|
||||||
|
public string platformLabelFormat = "던전 열기\n{0} 골드";
|
||||||
|
|
||||||
|
[Tooltip("안내 글 높이(m).")]
|
||||||
|
public float platformLabelHeight = 2.2f;
|
||||||
|
|
||||||
|
[Tooltip("발판 트리거 한 변(m) — FI Purchaser 원본은 섬 1칸(8 m)을 덮는 十자라 발판 크기로 줄인다.")]
|
||||||
|
public float platformTriggerSize = 3.4f;
|
||||||
|
|
||||||
|
[Tooltip("발판 트리거 높이(m).")]
|
||||||
|
public float platformTriggerHeight = 3f;
|
||||||
|
|
||||||
|
[Tooltip("지불 완료 뒤 발판을 갈아 끼우기까지 기다리는 초(물보라·해금 연출이 끝날 시간).")]
|
||||||
|
public float platformRespawnDelaySeconds = 1.3f;
|
||||||
|
|
||||||
|
[Tooltip("1 이면 발판 자리를 지면 높이에 맞춘다(위에서 아래로 레이).")]
|
||||||
|
public int platformSnapToGround = 1;
|
||||||
|
|
||||||
|
[Tooltip("1 이면 새 발판은 플레이어가 그 자리에서 **내려온 뒤에** 생긴다 — " +
|
||||||
|
"골드가 많을 때 가만히 서서 던전이 연달아 팔리는 것을 막는다(816g 실측).")]
|
||||||
|
public int platformRequireStepOff = 1;
|
||||||
|
|
||||||
|
[Tooltip("위 대기의 상한(초). 넘으면 그냥 놓는다.")]
|
||||||
|
public float platformStepOffTimeoutSeconds = 120f;
|
||||||
|
|
||||||
|
[Header("시작 골드 (PD: 초기 10 골드)")]
|
||||||
|
[Tooltip("1 이면 **새 게임**의 시작 골드를 아래 값으로 바꾼다(FI 기본값 500 · IslandManager.cs:30).")]
|
||||||
|
public int applyStartCoin = 1;
|
||||||
|
|
||||||
|
[Tooltip("새 게임 시작 골드.")]
|
||||||
|
public int startCoin = 10;
|
||||||
|
|
||||||
|
[Tooltip("이 문자열이 바뀌면 섬 세이브를 **한 번** 지우고 처음부터 시작한다(해금 수도 0 으로). " +
|
||||||
|
"비우면 아무것도 지우지 않는다. PD 에게 새 상태를 보여 줄 때 값을 바꾼다.")]
|
||||||
|
public string saveResetToken = "816g-1";
|
||||||
|
|
||||||
// ───────────────────────────────── 던전 표
|
// ───────────────────────────────── 던전 표
|
||||||
[Header("던전 표 — 한 줄 = 던전 하나")]
|
[Header("던전 표 — 한 줄 = 던전 하나")]
|
||||||
public WLDungeonDef[] dungeons = new WLDungeonDef[0];
|
public WLDungeonDef[] dungeons = new WLDungeonDef[0];
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,45 @@ namespace WL.Island
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
public static Material GateMaterial(Color c)
|
||||||
{
|
{
|
||||||
int key = ((Color32)c).GetHashCode();
|
int key = ((Color32)c).GetHashCode();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue