680 lines
37 KiB
C#
680 lines
37 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// WLIslandLook.cs — **실제 게임이 로드하는 섬 씬**(FarmingIsland/Scenes/Level01)에
|
||
// 데모 룩과 풀밭을 런타임으로 얹는다 (WL-816e · #816)
|
||
//
|
||
// ■ 왜 필요한가(발주서 §0)
|
||
// 816a 는 복사본 씬 `Assets/WL/Look/Farm/Scenes/WL_FarmLook.unity` 에만 룩을 저장했다.
|
||
// 게임은 `Level01` 을 로드하므로 PD 캡처의 섬은 여전히 **원본 색**(쨍한 파란 하늘·원색 초록)이었다.
|
||
// → 원본 씬을 고치지 않고(`Assets/FarmingIsland/**` 0줄) **로드 직후** 얹는다.
|
||
//
|
||
// ■ 걸리는 지점
|
||
// `[RuntimeInitializeOnLoadMethod]` + `SceneManager.sceneLoaded` — 기존 파일 **0줄 수정**.
|
||
// (816d 의 `WLIslandBridge` 를 고치지 않는다 = 다른 worktree 와 충돌 0)
|
||
//
|
||
// ■ C8 되돌리기 — `WLIslandLookSettings.enabled_ = 0` → 아무것도 얹지 않는다.
|
||
// 씬·머티리얼·프리팹 **파일을 쓰지 않으므로** 복원 작업 자체가 없다.
|
||
//
|
||
// 🔴 Debug.LogError / Debug.LogException 을 쓰지 않는다(ErrorLogHookManager 팝업).
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
using UnityEngine.Rendering;
|
||
using UnityEngine.SceneManagement;
|
||
using CryingSnow.FarmingIsland;
|
||
using FIIsland = CryingSnow.FarmingIsland.Island;
|
||
using FISoil = CryingSnow.FarmingIsland.Soil;
|
||
using FIFarm = CryingSnow.FarmingIsland.Farm;
|
||
|
||
namespace WL.Look.Farm
|
||
{
|
||
/// <summary>섬 씬에 룩·풀밭을 얹는 정적 진입점.</summary>
|
||
public static class WLIslandLook
|
||
{
|
||
public const string RunnerName = "~WLIslandLookRunner";
|
||
public const string ReferenceLookName = "WL_ReferenceLook";
|
||
|
||
// ── 진단 ────────────────────────────────────────────────────────
|
||
public static int SwappedRenderers, SwappedSlots, Rescans, LightingApplied, GrassRebuilds;
|
||
public static bool LookApplied, ReferenceLookApplied, GrassSpawned;
|
||
public static string LastLog = "";
|
||
|
||
static bool s_installed;
|
||
static WLIslandLookRunner s_runner;
|
||
static readonly HashSet<Renderer> s_done = new HashSet<Renderer>();
|
||
static readonly HashSet<FIIsland> s_hooked = new HashSet<FIIsland>();
|
||
|
||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
||
public static void Install()
|
||
{
|
||
if (s_installed) return;
|
||
var cfg = WLIslandLookSettings.Instance;
|
||
if (cfg == null || cfg.enabled_ == 0) return;
|
||
s_installed = true;
|
||
|
||
SceneManager.sceneLoaded += OnSceneLoaded;
|
||
SceneManager.sceneUnloaded += OnSceneUnloaded;
|
||
|
||
for (int i = 0; i < SceneManager.sceneCount; i++)
|
||
{
|
||
var sc = SceneManager.GetSceneAt(i);
|
||
if (sc.isLoaded) OnSceneLoaded(sc, LoadSceneMode.Additive);
|
||
}
|
||
}
|
||
|
||
/// <summary>에디터 프로브가 Play 중에 다시 걸 때 쓴다.</summary>
|
||
public static void ForceApply(Scene scene)
|
||
{
|
||
var cfg = WLIslandLookSettings.Instance;
|
||
if (cfg == null) return;
|
||
s_done.Clear();
|
||
EnsureRunner();
|
||
s_runner.StartCoroutine(Co_Apply(cfg, scene));
|
||
}
|
||
|
||
static void OnSceneLoaded(Scene scene, LoadSceneMode mode)
|
||
{
|
||
var cfg = WLIslandLookSettings.Instance;
|
||
if (cfg == null || cfg.enabled_ == 0) return;
|
||
if (!IsIslandScene(cfg, scene)) return;
|
||
EnsureRunner();
|
||
s_runner.StartCoroutine(Co_Apply(cfg, scene));
|
||
}
|
||
|
||
static void OnSceneUnloaded(Scene scene)
|
||
{
|
||
s_done.Clear();
|
||
s_hooked.Clear();
|
||
s_tonedFarms.Clear();
|
||
LookApplied = false; GrassSpawned = false; ReferenceLookApplied = false;
|
||
}
|
||
|
||
static bool IsIslandScene(WLIslandLookSettings cfg, Scene scene)
|
||
{
|
||
if (!scene.IsValid() || !scene.isLoaded) return false;
|
||
if (cfg.islandSceneNames != null)
|
||
for (int i = 0; i < cfg.islandSceneNames.Length; i++)
|
||
if (scene.name == cfg.islandSceneNames[i]) return true;
|
||
|
||
// 이름을 몰라도 IslandManager 가 있으면 섬이다.
|
||
var roots = scene.GetRootGameObjects();
|
||
for (int i = 0; i < roots.Length; i++)
|
||
if (roots[i].GetComponentInChildren<IslandManager>(true) != null) return true;
|
||
return false;
|
||
}
|
||
|
||
static void EnsureRunner()
|
||
{
|
||
if (s_runner != null) return;
|
||
var go = new GameObject(RunnerName);
|
||
go.hideFlags = HideFlags.DontSave;
|
||
Object.DontDestroyOnLoad(go);
|
||
s_runner = go.AddComponent<WLIslandLookRunner>();
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────
|
||
// 본체
|
||
// ─────────────────────────────────────────────────────────────────
|
||
static IEnumerator Co_Apply(WLIslandLookSettings cfg, Scene scene)
|
||
{
|
||
yield return null; // FI 매니저들의 Awake 를 기다린다
|
||
|
||
if (cfg.disableFiGraphicsManager != 0) DisableGraphicsManager(scene);
|
||
|
||
if (cfg.applyLook != 0)
|
||
{
|
||
SwapMaterials(cfg, scene);
|
||
ApplyIslandOutline(cfg);
|
||
LookApplied = true;
|
||
}
|
||
if (cfg.applyLighting != 0) ApplyLighting(cfg, scene);
|
||
if (cfg.applyLook != 0 && cfg.applyReferenceLook != 0) ApplyReferenceLook(cfg, scene);
|
||
if (cfg.soilToneEnabled != 0) ToneSoil(cfg, scene);
|
||
|
||
// 🔴 816zf — 흙 데칼은 **풀보다 먼저** 만든다. 풀의 밭 제외가 이 마스크를 읽기 때문이다.
|
||
if (cfg.softDirtEnabled != 0) SpawnSoftDirt(cfg, scene);
|
||
if (cfg.seabedEnabled != 0) SpawnSeabed(cfg, scene);
|
||
if (cfg.shoreSkirtEnabled != 0) SpawnShoreSkirt(cfg, scene);
|
||
if (cfg.grassEnabled != 0) SpawnGrass(cfg, scene);
|
||
if (cfg.shoreFoamEnabled != 0) SpawnShoreFoam(cfg, scene);
|
||
|
||
HookIslands(cfg, scene);
|
||
|
||
cfg.Log("섬 룩 적용 — 렌더러 " + SwappedRenderers + " · 슬롯 " + SwappedSlots
|
||
+ " · 조명 " + (cfg.applyLighting != 0 ? "데모" : "원본")
|
||
+ " · ReferenceLook " + (ReferenceLookApplied ? "on" : "off")
|
||
+ " · 풀 " + WLIslandGrass.LastLog
|
||
+ " · 흙데칼 " + WLSoftDirt.LastLog
|
||
+ " · 물가스커트 " + WLShoreSkirt.LastLog
|
||
+ " · 둘레거품 " + WLShoreFoam.LastLog);
|
||
|
||
// 늦게 생기는 오브젝트(상인·작물·아이템)까지 다시 훑는다
|
||
if (cfg.rescanAtSeconds != null)
|
||
{
|
||
float prev = 0f;
|
||
for (int i = 0; i < cfg.rescanAtSeconds.Length; i++)
|
||
{
|
||
float w = cfg.rescanAtSeconds[i] - prev;
|
||
prev = cfg.rescanAtSeconds[i];
|
||
if (w > 0f) yield return new WaitForSeconds(w);
|
||
if (!scene.isLoaded) yield break;
|
||
if (cfg.applyLook != 0) { SwapMaterials(cfg, scene); Rescans++; }
|
||
// 🔴 816f 실측 — PD 실행 경로(Additive)에서 `RenderSettings` 는 **활성 씬(InGame)**
|
||
// 소유이고, 그 씬에는 조명을 덮어쓰는 코드(`MapData`)가 없다(실측).
|
||
// 그래서 조명은 재훑기에서 **다시 걸지 않는다** — 다시 걸면 `WLReferenceLook` 의
|
||
// 무드(Flat)를 Skybox 로 되돌려 오히려 룩이 바뀐다(실측으로 확인).
|
||
if (cfg.soilToneEnabled != 0) ToneSoil(cfg, scene);
|
||
// 🔴 816zf — 밭 구역(`WLIslandGateFlow.ZoneBounds`)은 게이트 플로우가 밭을 훑은 뒤에야 정해진다.
|
||
// 재훑기에서 구역이 처음 잡히거나 바뀌면 데칼을 굽고, **그때만** 풀도 다시 깐다(경계가 같아지게).
|
||
if (cfg.softDirtEnabled != 0)
|
||
{
|
||
int before = WLSoftDirt.Rebuilds;
|
||
SpawnSoftDirt(cfg, scene);
|
||
if (WLSoftDirt.Rebuilds != before && cfg.grassEnabled != 0)
|
||
{
|
||
var g2 = Object.FindFirstObjectByType<WLIslandGrass>(FindObjectsInactive.Include);
|
||
if (g2 != null) { g2.Rebuild(); GrassRebuilds++; }
|
||
}
|
||
}
|
||
// 🔴 816zg — 물가 스커트도 같은 재훑기에서 다시 만든다(발자국이 그대로면 멱등 = 공짜).
|
||
if (cfg.shoreSkirtEnabled != 0) SpawnShoreSkirt(cfg, scene);
|
||
HookIslands(cfg, scene);
|
||
}
|
||
}
|
||
|
||
// 섬 확장 감시 — 새로 열린 타일에도 자동으로 풀이 깔린다
|
||
if (cfg.rebuildPollSeconds > 0f)
|
||
{
|
||
int last = CountUnlocked(scene);
|
||
float nextCloud = Time.time + Mathf.Max(0f, cfg.cloudEdgeRefreshSeconds);
|
||
float nextView = Time.time + Mathf.Max(0f, cfg.viewPollSeconds);
|
||
float nextPoll = Time.time + cfg.rebuildPollSeconds;
|
||
while (scene.isLoaded)
|
||
{
|
||
// 816q — 카메라 감시는 확장 감시(1 s)보다 촘촘해야 해서 따로 돈다
|
||
float wait = cfg.viewCullEnabled != 0 && cfg.viewPollSeconds > 0f
|
||
? Mathf.Min(cfg.rebuildPollSeconds, cfg.viewPollSeconds) : cfg.rebuildPollSeconds;
|
||
yield return new WaitForSeconds(wait);
|
||
if (!scene.isLoaded) yield break;
|
||
|
||
if (cfg.viewCullEnabled != 0 && cfg.viewPollSeconds > 0f && Time.time >= nextView)
|
||
{
|
||
nextView = Time.time + cfg.viewPollSeconds;
|
||
var gv = Object.FindFirstObjectByType<WLIslandGrass>(FindObjectsInactive.Include);
|
||
if (gv != null && gv.ViewMovedEnough()) gv.RefreshView();
|
||
}
|
||
if (Time.time < nextPoll) continue;
|
||
nextPoll = Time.time + cfg.rebuildPollSeconds;
|
||
HookIslands(cfg, scene);
|
||
int now = CountUnlocked(scene);
|
||
if (now != last)
|
||
{
|
||
last = now;
|
||
if (cfg.applyLook != 0) SwapMaterials(cfg, scene);
|
||
RequestGrassRebuild(cfg);
|
||
nextCloud = Time.time + Mathf.Max(0f, cfg.cloudEdgeRefreshSeconds);
|
||
continue;
|
||
}
|
||
|
||
// 816o — 구름이 흐른 만큼 띠를 다시 고른다(타일·제외 사각형은 다시 훑지 않는다)
|
||
if (cfg.cloudEdgeEnabled != 0 && cfg.cloudEdgeRefreshSeconds > 0f && Time.time >= nextCloud)
|
||
{
|
||
nextCloud = Time.time + cfg.cloudEdgeRefreshSeconds;
|
||
var g = Object.FindFirstObjectByType<WLIslandGrass>(FindObjectsInactive.Include);
|
||
if (g != null) g.RefreshCloudEdges();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
static int CountUnlocked(Scene scene)
|
||
{
|
||
var islands = Object.FindObjectsByType<FIIsland>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
|
||
int n = 0;
|
||
for (int i = 0; i < islands.Length; i++)
|
||
if (islands[i] != null && islands[i].IsUnlocked && islands[i].gameObject.activeInHierarchy) n++;
|
||
return n;
|
||
}
|
||
|
||
/// <summary>FI `FIIsland.OnActivated`(public event)에 붙는다 — FI 코드 0줄.</summary>
|
||
static void HookIslands(WLIslandLookSettings cfg, Scene scene)
|
||
{
|
||
var islands = Object.FindObjectsByType<FIIsland>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||
for (int i = 0; i < islands.Length; i++)
|
||
{
|
||
var isl = islands[i];
|
||
if (isl == null || isl.gameObject.scene != scene) continue;
|
||
if (s_hooked.Contains(isl)) continue;
|
||
s_hooked.Add(isl);
|
||
isl.OnActivated += () => OnIslandActivated(cfg, isl);
|
||
}
|
||
}
|
||
|
||
static void OnIslandActivated(WLIslandLookSettings cfg, FIIsland isl)
|
||
{
|
||
if (cfg == null || cfg.enabled_ == 0) return;
|
||
EnsureRunner();
|
||
s_runner.StartCoroutine(Co_AfterActivate(cfg, isl));
|
||
}
|
||
|
||
static IEnumerator Co_AfterActivate(WLIslandLookSettings cfg, FIIsland isl)
|
||
{
|
||
// 확장 애니메이션(DOTween DOScale 1초) + 소품 배치가 끝나기를 기다린다
|
||
yield return new WaitForSeconds(cfg.rebuildDelaySeconds);
|
||
if (isl == null) yield break;
|
||
if (cfg.applyLook != 0) SwapMaterials(cfg, isl.gameObject.scene);
|
||
RequestGrassRebuild(cfg);
|
||
}
|
||
|
||
static void RequestGrassRebuild(WLIslandLookSettings cfg)
|
||
{
|
||
// 🔴 816zf — 흙 데칼을 **먼저** 다시 굽는다. 섬 확장·밭 재구성으로 구역이 바뀌면
|
||
// 풀의 밭 제외도 그 새 경계를 읽어야 흙과 풀이 같은 선에서 만난다.
|
||
if (cfg.softDirtEnabled != 0)
|
||
{
|
||
var sd = Object.FindFirstObjectByType<WLSoftDirt>(FindObjectsInactive.Include);
|
||
if (sd != null) { sd.cfg = cfg; sd.Rebuild(); }
|
||
}
|
||
if (cfg.grassEnabled != 0)
|
||
{
|
||
var g = Object.FindFirstObjectByType<WLIslandGrass>(FindObjectsInactive.Include);
|
||
if (g != null) { g.Rebuild(); GrassRebuilds++; }
|
||
}
|
||
// 🔴 816j2 — 섬이 확장되면 **둘레 거품 띠도 같은 훅으로** 다시 만든다(새 가장자리 자동).
|
||
if (cfg.shoreFoamEnabled != 0)
|
||
{
|
||
var f = Object.FindFirstObjectByType<WLShoreFoam>(FindObjectsInactive.Include);
|
||
if (f != null) { f.cfg = cfg; f.Rebuild(); ShoreFoamRebuilds++; }
|
||
}
|
||
// 🔴 816zg — 섬이 확장되면 물가 스커트도 같은 훅으로 다시 만든다(발자국 변화 → 거리장 재계산).
|
||
if (cfg.shoreSkirtEnabled != 0)
|
||
{
|
||
var sk = Object.FindFirstObjectByType<WLShoreSkirt>(FindObjectsInactive.Include);
|
||
if (sk != null) { sk.cfg = cfg; sk.Rebuild(); ShoreSkirtRebuilds++; }
|
||
}
|
||
}
|
||
|
||
public static int ShoreFoamRebuilds;
|
||
public static bool ShoreFoamSpawned;
|
||
public static bool SoftDirtSpawned;
|
||
|
||
/// <summary>816zf — 밭 자리 부드러운 흙 데칼. 풀·거품과 같은 자리에서 만들고 같은 훅으로 다시 만든다.</summary>
|
||
public static void SpawnSoftDirt(WLIslandLookSettings cfg, Scene scene)
|
||
{
|
||
var d = Object.FindFirstObjectByType<WLSoftDirt>(FindObjectsInactive.Include);
|
||
if (d == null)
|
||
{
|
||
var go = new GameObject(WLSoftDirt.ObjectName);
|
||
SceneManager.MoveGameObjectToScene(go, scene);
|
||
go.transform.SetPositionAndRotation(Vector3.zero, Quaternion.identity);
|
||
d = go.AddComponent<WLSoftDirt>();
|
||
d.cfg = cfg;
|
||
d.Rebuild(); // OnEnable 이 cfg 전에 돌 수 있다
|
||
}
|
||
else { d.cfg = cfg; d.Rebuild(); }
|
||
SoftDirtSpawned = true;
|
||
}
|
||
|
||
public static bool SeabedSpawned;
|
||
public static string SeabedLog = "";
|
||
|
||
/// <summary>
|
||
/// 2026-09-15 Lead 실측 — 데모 물 셰이더(`Farm_Water_Demo3` = 데모 `Water.mat`)는 **깊이**로 색·거품을 정한다.
|
||
/// 데모 연못은 바닥이 1 m 안팎이라 청록 + 흰 물가가 나오지만, 우리 바다는 밑에 아무것도 없어 카메라 배경색이 비쳐
|
||
/// 진파랑으로 찍혔다(PD 「데모와 느낌이 다르다」의 큰 몫). 물 아래 `seabedDepth` 에 모래색 언릿 판 1장(삼각형 2 · 드로우콜 1)을 깐다.
|
||
/// 🔴 모바일 빌드: `Universal Render Pipeline/Unlit` 이 빌드에 포함되지 않으면 분홍 → Always Included 확인.
|
||
/// </summary>
|
||
public static void SpawnSeabed(WLIslandLookSettings cfg, Scene scene)
|
||
{
|
||
const string Name = "~WL_Seabed";
|
||
float waterY = -1f; bool found = false;
|
||
var mrs = Object.FindObjectsByType<MeshRenderer>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||
for (int i = 0; i < mrs.Length; i++)
|
||
{
|
||
var r = mrs[i];
|
||
if (r == null || r.gameObject.name != "Water" || (scene.IsValid() && r.gameObject.scene != scene)) continue;
|
||
waterY = r.transform.position.y; found = true; break;
|
||
}
|
||
GameObject go = null;
|
||
for (int i = 0; i < mrs.Length; i++) if (mrs[i] != null && mrs[i].gameObject.name == Name) { go = mrs[i].gameObject; break; }
|
||
if (go == null)
|
||
{
|
||
go = new GameObject(Name);
|
||
if (scene.IsValid()) SceneManager.MoveGameObjectToScene(go, scene);
|
||
var mf = go.AddComponent<MeshFilter>();
|
||
var mesh = new Mesh { name = "WL_SeabedQuad" };
|
||
mesh.vertices = new[] { new Vector3(-0.5f, 0f, -0.5f), new Vector3(-0.5f, 0f, 0.5f), new Vector3(0.5f, 0f, 0.5f), new Vector3(0.5f, 0f, -0.5f) };
|
||
mesh.triangles = new[] { 0, 1, 2, 0, 2, 3 };
|
||
mesh.normals = new[] { Vector3.up, Vector3.up, Vector3.up, Vector3.up };
|
||
mesh.uv = new[] { Vector2.zero, Vector2.up, Vector2.one, Vector2.right };
|
||
mesh.RecalculateBounds();
|
||
mf.sharedMesh = mesh;
|
||
var mr = go.AddComponent<MeshRenderer>();
|
||
var sh = Shader.Find("Universal Render Pipeline/Unlit");
|
||
var mat = new Material(sh != null ? sh : Shader.Find("Unlit/Color")) { name = "WL_Seabed(runtime)" };
|
||
mr.sharedMaterial = mat;
|
||
mr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
|
||
mr.receiveShadows = false;
|
||
mr.lightProbeUsage = UnityEngine.Rendering.LightProbeUsage.Off;
|
||
}
|
||
var m = go.GetComponent<MeshRenderer>().sharedMaterial;
|
||
if (m.HasProperty("_BaseColor")) m.SetColor("_BaseColor", cfg.seabedColor);
|
||
if (m.HasProperty("_Color")) m.SetColor("_Color", cfg.seabedColor);
|
||
go.transform.position = new Vector3(0f, waterY - Mathf.Max(0.05f, cfg.seabedDepth), 0f);
|
||
go.transform.rotation = Quaternion.identity;
|
||
go.transform.localScale = new Vector3(Mathf.Max(1f, cfg.seabedSize), 1f, Mathf.Max(1f, cfg.seabedSize));
|
||
go.SetActive(cfg.seabedEnabled != 0);
|
||
SeabedSpawned = true;
|
||
SeabedLog = "바다 바닥판 y=" + go.transform.position.y.ToString("F2") + (found ? "(물 " + waterY.ToString("F2") + ")" : "(물 미발견 · 기본 -1)") + " · 깊이 " + cfg.seabedDepth + " m";
|
||
}
|
||
|
||
public static bool ShoreSkirtSpawned;
|
||
public static int ShoreSkirtRebuilds;
|
||
|
||
/// <summary>
|
||
/// 816zg — 섬 둘레 완만한 모래 경사(물가 스커트). 바닥판·거품과 같은 자리에서 만들고 같은 훅으로 다시 만든다.
|
||
/// 열린 타일 발자국의 거리장으로 띠 메시 1장(드로우콜 +1)을 굽는다 — 본체는 <see cref="WLShoreSkirt"/>.
|
||
/// </summary>
|
||
public static void SpawnShoreSkirt(WLIslandLookSettings cfg, Scene scene)
|
||
{
|
||
var s = Object.FindFirstObjectByType<WLShoreSkirt>(FindObjectsInactive.Include);
|
||
if (s == null)
|
||
{
|
||
var go = new GameObject(WLShoreSkirt.ObjectName);
|
||
SceneManager.MoveGameObjectToScene(go, scene);
|
||
go.transform.SetPositionAndRotation(Vector3.zero, Quaternion.identity);
|
||
s = go.AddComponent<WLShoreSkirt>();
|
||
s.cfg = cfg;
|
||
s.Rebuild(); // OnEnable 이 cfg 전에 돌 수 있다
|
||
}
|
||
else { s.cfg = cfg; s.Rebuild(); }
|
||
ShoreSkirtSpawned = true;
|
||
}
|
||
|
||
/// <summary>섬 둘레 거품 띠 — 풀과 같은 자리에서 만들고 같은 훅으로 다시 만든다(816j2).</summary>
|
||
public static void SpawnShoreFoam(WLIslandLookSettings cfg, Scene scene)
|
||
{
|
||
var f = Object.FindFirstObjectByType<WLShoreFoam>(FindObjectsInactive.Include);
|
||
if (f == null)
|
||
{
|
||
var go = new GameObject(WLShoreFoam.ObjectName);
|
||
SceneManager.MoveGameObjectToScene(go, scene);
|
||
go.transform.SetPositionAndRotation(Vector3.zero, Quaternion.identity);
|
||
f = go.AddComponent<WLShoreFoam>();
|
||
f.cfg = cfg;
|
||
f.Rebuild(); // OnEnable 이 cfg 전에 돌 수 있다
|
||
}
|
||
else { f.cfg = cfg; f.Rebuild(); }
|
||
ShoreFoamSpawned = true;
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────
|
||
// ① 머티리얼 — 원본을 **읽기만** 하고 렌더러의 sharedMaterials 만 바꾼다
|
||
// ─────────────────────────────────────────────────────────────────
|
||
public static void SwapMaterials(WLIslandLookSettings cfg, Scene scene)
|
||
{
|
||
if (cfg.materialRemap == null || cfg.materialRemap.Length == 0) return;
|
||
|
||
var rends = Object.FindObjectsByType<Renderer>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||
for (int i = 0; i < rends.Length; i++)
|
||
{
|
||
var r = rends[i];
|
||
if (r == null || r.gameObject.scene != scene) continue;
|
||
if (s_done.Contains(r)) continue;
|
||
if (r is ParticleSystemRenderer) continue;
|
||
|
||
// 🔴 농사 타일은 코드가 `material.color` 로 직접 색을 칠한다(Soil.Initialize).
|
||
// Toon 셰이더에는 그 프로퍼티가 없어 갈아끼우면 밭 색이 죽는다 → 원본 유지.
|
||
if (cfg.skipSoilRenderers != 0 && r.GetComponent<FISoil>() != null) { s_done.Add(r); continue; }
|
||
|
||
bool isIslandBody = r.GetComponent<FIIsland>() != null;
|
||
|
||
var mats = r.sharedMaterials;
|
||
bool changed = false;
|
||
for (int m = 0; m < mats.Length; m++)
|
||
{
|
||
var src = mats[m];
|
||
if (src == null) continue;
|
||
Material dst = null;
|
||
|
||
if (isIslandBody && cfg.islandTopMaterial != null) dst = cfg.islandTopMaterial;
|
||
else dst = Lookup(cfg, src);
|
||
|
||
if (dst == null || dst == src) continue;
|
||
mats[m] = dst; changed = true; SwappedSlots++;
|
||
}
|
||
if (changed) { r.sharedMaterials = mats; SwappedRenderers++; }
|
||
s_done.Add(r);
|
||
}
|
||
}
|
||
|
||
static readonly int OutlineProp = Shader.PropertyToID("_OUTLINESENABLED");
|
||
|
||
/// <summary>
|
||
/// 816ze — 섬 타일 셰이더의 외곽선 패스 스위치.
|
||
/// 데모 `Terrain.mat` 에는 외곽선 프로퍼티가 없으므로 전사본(`Farm_IslandTop_Demo3`)은
|
||
/// `_OUTLINESENABLED 0` 으로 구워져 있다 = 기본값(0)에서는 **아무 것도 하지 않는다**
|
||
/// (공유 에셋을 런타임에 건드리지 않는다 = 에디터에서 에셋이 더러워지지 않는다).
|
||
/// 1 로 두었을 때만 되돌리기용으로 키워드를 다시 켠다.
|
||
/// </summary>
|
||
static void ApplyIslandOutline(WLIslandLookSettings cfg)
|
||
{
|
||
if (cfg.islandOutlineEnabled == 0) return; // 기본 경로 = 무동작
|
||
var m = cfg.islandTopMaterial;
|
||
if (m == null || !m.HasProperty(OutlineProp)) return;
|
||
m.SetFloat(OutlineProp, 1f);
|
||
m.EnableKeyword("_OUTLINESENABLED");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 🔴 816h — 섬 둘레의 바다(`Water` · 1000×1000 평면)를 **WL 워터 셰이더**로.
|
||
/// `Assets/FarmingIsland/**` 는 0줄 — 렌더러의 `sharedMaterials` 만 런타임에 바꾼다.
|
||
/// `waterMode = 0` 이면 이 판정 자체를 건너뛰어 지금(FarmingIsland 물)으로 100 % 돌아간다.
|
||
/// </summary>
|
||
static bool IsWater(WLIslandLookSettings cfg, Material src)
|
||
{
|
||
if (src == null) return false;
|
||
if (cfg.waterFrom != null && src == cfg.waterFrom) return true;
|
||
if (src.shader != null && src.shader.name == "Shader Graphs/Simple Water") return true;
|
||
// 이미 우리 복사본(Farm_SimpleWater_Demo)으로 바뀐 뒤 다시 훑는 경우
|
||
return src.name.StartsWith("Farm_SimpleWater");
|
||
}
|
||
|
||
public static int WaterSwapped;
|
||
|
||
static Material Lookup(WLIslandLookSettings cfg, Material src)
|
||
{
|
||
if (cfg.waterMode != 0 && cfg.waterTo != null && IsWater(cfg, src))
|
||
{
|
||
if (src != cfg.waterTo) WaterSwapped++;
|
||
return cfg.waterTo;
|
||
}
|
||
|
||
for (int i = 0; i < cfg.materialRemap.Length; i++)
|
||
{
|
||
var e = cfg.materialRemap[i];
|
||
if (e == null || e.enabled_ == 0 || e.from == null || e.to == null) continue;
|
||
if (e.from == src) return e.to;
|
||
// 런타임 인스턴스("Palette (Instance)")도 잡는다
|
||
if (src.name.StartsWith(e.from.name) && src.shader == e.from.shader) return e.to;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────
|
||
// ② 조명 · 앰비언트 · 스카이박스 (816a 실측 = 데모/아레나 값)
|
||
// ─────────────────────────────────────────────────────────────────
|
||
public static void ApplyLighting(WLIslandLookSettings cfg, Scene scene)
|
||
{
|
||
// 🔴 816h — 하늘(앰비언트·스카이박스·안개·반사)은 **활성 씬(InGame)** 소유라
|
||
// 여기서 그냥 쓰면 섬에서 나간 뒤 전투 맵·로비까지 그대로 남는다.
|
||
// 그래서 `WLSkyScope` 가 **스냅샷 → 적용 → 나갈 때 원복** 을 맡는다.
|
||
// (값은 그대로 이 SO 것 = 아레나/데모 값. 새 값 0)
|
||
if (cfg.skyScopeEnabled != 0) WLSkyScope.Acquire(cfg, scene.name);
|
||
else
|
||
{
|
||
RenderSettings.ambientMode = AmbientMode.Skybox;
|
||
RenderSettings.ambientSkyColor = cfg.ambientSky;
|
||
RenderSettings.ambientEquatorColor = cfg.ambientEquator;
|
||
RenderSettings.ambientGroundColor = cfg.ambientGround;
|
||
RenderSettings.ambientLight = cfg.ambientSky;
|
||
RenderSettings.ambientIntensity = cfg.ambientIntensity;
|
||
if (cfg.fogOff != 0) RenderSettings.fog = false;
|
||
if (cfg.skybox != null) RenderSettings.skybox = cfg.skybox;
|
||
RenderSettings.defaultReflectionMode = DefaultReflectionMode.Skybox;
|
||
}
|
||
|
||
var lights = Object.FindObjectsByType<Light>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
|
||
for (int i = 0; i < lights.Length; i++)
|
||
{
|
||
var l = lights[i];
|
||
if (l == null || l.gameObject.scene != scene) continue;
|
||
if (l.type != LightType.Directional) continue;
|
||
l.color = cfg.dirLightColor;
|
||
l.intensity = cfg.dirLightIntensity;
|
||
if (cfg.dirLightSoftShadows != 0 && l.shadows != LightShadows.None) l.shadows = LightShadows.Soft;
|
||
RenderSettings.sun = l;
|
||
LightingApplied++;
|
||
}
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────
|
||
// ②-b 밭(Soil) 칸 표시 — **기능은 그대로 두고 색만** 데모 톤으로 (816f · PD 지시)
|
||
// FI `Farm` 의 4색(마름1·마름2·젖음1·젖음2)을 런타임에 낮추고
|
||
// `Soil.Initialize(farm)`(public) 를 다시 불러 칠만 갱신한다.
|
||
// → 물주기 피드백(마름↔젖음)·칸 구분은 전부 살아 있다. `Assets/FarmingIsland/**` 0줄.
|
||
// ─────────────────────────────────────────────────────────────────
|
||
static readonly string[] s_soilFields = { "soilDryColor1", "soilDryColor2", "soilWetColor1", "soilWetColor2" };
|
||
static readonly HashSet<FIFarm> s_tonedFarms = new HashSet<FIFarm>();
|
||
|
||
public static int SoilFarmsToned, SoilTilesRepainted;
|
||
|
||
public static void ToneSoil(WLIslandLookSettings cfg, Scene scene)
|
||
{
|
||
var farms = Object.FindObjectsByType<FIFarm>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||
var t = typeof(FIFarm);
|
||
for (int i = 0; i < farms.Length; i++)
|
||
{
|
||
var farm = farms[i];
|
||
if (farm == null || farm.gameObject.scene != scene) continue;
|
||
if (s_tonedFarms.Contains(farm)) continue;
|
||
|
||
// 1) 마름1/마름2 · 젖음1/젖음2 각각의 평균으로 모으고(대비 축소) 틴트를 곱한다
|
||
var vals = new Color[4];
|
||
var fis = new System.Reflection.FieldInfo[4];
|
||
bool ok = true;
|
||
for (int k = 0; k < 4; k++)
|
||
{
|
||
fis[k] = t.GetField(s_soilFields[k],
|
||
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
|
||
if (fis[k] == null) { ok = false; break; }
|
||
vals[k] = (Color)fis[k].GetValue(farm);
|
||
}
|
||
if (!ok) continue;
|
||
|
||
// 「마름」 중점 밝기 = 기준. 젖음은 이 비를 그대로 유지한다(물주기 피드백 보존).
|
||
float dryLum = SoilLum((vals[0] + vals[1]) * 0.5f);
|
||
Apply(cfg, fis, farm, vals, 0, 1, dryLum); // 마름 한 쌍
|
||
Apply(cfg, fis, farm, vals, 2, 3, dryLum); // 젖음 한 쌍
|
||
s_tonedFarms.Add(farm);
|
||
SoilFarmsToned++;
|
||
|
||
// 2) 이미 칠해진 타일을 다시 칠한다 — FI 의 public 진입점 그대로
|
||
var soils = farm.GetComponentsInChildren<FISoil>(true);
|
||
for (int s = 0; s < soils.Length; s++)
|
||
{
|
||
if (soils[s] == null) continue;
|
||
try { soils[s].Initialize(farm); SoilTilesRepainted++; } catch { }
|
||
}
|
||
}
|
||
}
|
||
|
||
static void Apply(WLIslandLookSettings cfg, System.Reflection.FieldInfo[] fis, FIFarm farm, Color[] v, int a, int b, float dryLum)
|
||
{
|
||
var mid = (v[a] + v[b]) * 0.5f;
|
||
float c = cfg.soilCheckerContrast;
|
||
var ca = Mul(Color.Lerp(mid, v[a], c), cfg.soilTint);
|
||
var cb = Mul(Color.Lerp(mid, v[b], c), cfg.soilTint);
|
||
|
||
// 816r — 목표 흙색으로 통째로 옮긴다. 칸 차이(ca−mid, cb−mid)와 마름↔젖음 밝기비는 보존.
|
||
if (cfg.soilTargetEnabled != 0 && dryLum > 0.0001f)
|
||
{
|
||
var baseMid = Mul(mid, cfg.soilTint);
|
||
float k = SoilLum(mid) / dryLum; // 마름 = 1 · 젖음 = 젖음/마름
|
||
var t = cfg.soilTargetColor * k;
|
||
ca = new Color(t.r + (ca.r - baseMid.r), t.g + (ca.g - baseMid.g), t.b + (ca.b - baseMid.b), ca.a);
|
||
cb = new Color(t.r + (cb.r - baseMid.r), t.g + (cb.g - baseMid.g), t.b + (cb.b - baseMid.b), cb.a);
|
||
}
|
||
fis[a].SetValue(farm, ca);
|
||
fis[b].SetValue(farm, cb);
|
||
}
|
||
|
||
static Color Mul(Color a, Color b) { return new Color(a.r * b.r, a.g * b.g, a.b * b.b, a.a); }
|
||
static float SoilLum(Color c) { return 0.299f * c.r + 0.587f * c.g + 0.114f * c.b; }
|
||
|
||
// ─────────────────────────────────────────────────────────────────
|
||
// ③ 아레나와 같은 외곽선·대비·무드 (씬 비의존 — 816a 실측)
|
||
// ─────────────────────────────────────────────────────────────────
|
||
public static void ApplyReferenceLook(WLIslandLookSettings cfg, Scene scene)
|
||
{
|
||
var existing = Object.FindFirstObjectByType<WL.Look.Arena.WLReferenceLook>(FindObjectsInactive.Include);
|
||
if (existing != null)
|
||
{
|
||
if (!existing.gameObject.activeSelf) existing.gameObject.SetActive(true);
|
||
ReferenceLookApplied = true;
|
||
return;
|
||
}
|
||
var go = new GameObject(ReferenceLookName);
|
||
SceneManager.MoveGameObjectToScene(go, scene);
|
||
go.AddComponent<WL.Look.Arena.WLReferenceLook>(); // OnEnable 이 적용한다
|
||
ReferenceLookApplied = true;
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────
|
||
// ④ 풀밭
|
||
// ─────────────────────────────────────────────────────────────────
|
||
public static void SpawnGrass(WLIslandLookSettings cfg, Scene scene)
|
||
{
|
||
var g = Object.FindFirstObjectByType<WLIslandGrass>(FindObjectsInactive.Include);
|
||
if (g == null)
|
||
{
|
||
var go = new GameObject(WLIslandGrass.ObjectName);
|
||
SceneManager.MoveGameObjectToScene(go, scene);
|
||
go.transform.SetPositionAndRotation(Vector3.zero, Quaternion.identity);
|
||
g = go.AddComponent<WLIslandGrass>(); // OnEnable 이 만든다
|
||
g.cfg = cfg;
|
||
}
|
||
else
|
||
{
|
||
g.cfg = cfg;
|
||
g.Rebuild();
|
||
}
|
||
GrassSpawned = true;
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────
|
||
// ⑤ FI GraphicsManager — Play 중 FI 의 URP 에셋 **파일**을 고친다(816a 실측)
|
||
// 우리 활성 URP 가 아니라 화면 영향 0. `Assets/FarmingIsland/**` 0줄을 지키려면 꺼야 한다.
|
||
// ─────────────────────────────────────────────────────────────────
|
||
static void DisableGraphicsManager(Scene scene)
|
||
{
|
||
var roots = scene.GetRootGameObjects();
|
||
for (int i = 0; i < roots.Length; i++)
|
||
{
|
||
var comps = roots[i].GetComponentsInChildren<MonoBehaviour>(true);
|
||
for (int c = 0; c < comps.Length; c++)
|
||
{
|
||
if (comps[c] == null) continue;
|
||
if (comps[c].GetType().Name != "GraphicsManager") continue;
|
||
comps[c].enabled = false;
|
||
comps[c].gameObject.SetActive(false);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>코루틴 숙주. 씬을 넘어 살아남는다.</summary>
|
||
public sealed class WLIslandLookRunner : MonoBehaviour { }
|
||
}
|