541 lines
28 KiB
C#
541 lines
28 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// WLIslandSceneSetup.cs — 섬 씬(FarmingIsland Level01)에 **런타임으로만** 얹는 것들
|
||
//
|
||
// 발주서 WL-816d §1-① 「원본 씬은 수정 0 — 얹어야 하면 WL 새 파일이 런타임에 얹어라」
|
||
// 816c 설계서 §B-2 표(중복 정리) · §1-B-3(섬 캐릭터 = 우리 전투 캐릭터)
|
||
//
|
||
// ■ 하는 일 3가지 (FI 씬·FI 코드 0줄)
|
||
// ① 맵 노드 : `PCInfo.Make_Actors` 가 부르는 `LoadMapMgr.Ins` 를 섬 씬에도 만들어 준다.
|
||
// 🔴 MapData 는 **일부러 붙이지 않는다** — MapData.Start 가 RenderSettings(앰비언트·안개)를
|
||
// 덮어쓰는데 섬 배경/조명은 816a(gameplay) 소유라 건드리면 안 된다.
|
||
// ② 중복 정리 : WL 전투 PC(Actor) 숨김 · FI 카메라/AudioListener 중복 해제.
|
||
// ③ 캐릭터 교체 : FI 농부 메시를 끄고 그 자리에 치비(LH_M05)를 **자식으로** 붙인다.
|
||
// 조작·상호작용·NavMesh 는 FI `PlayerController` 그대로 — 모델만 바뀐다.
|
||
//
|
||
// 🔴 Debug.LogError / Debug.LogException 을 쓰지 않는다.
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
using UnityEngine.SceneManagement;
|
||
using CryingSnow.FarmingIsland;
|
||
|
||
namespace WL.Island
|
||
{
|
||
/// <summary>섬 씬 런타임 준비. 정적 · 상태 없음.</summary>
|
||
public static class WLIslandSceneSetup
|
||
{
|
||
public const string MapNodesName = "~WL_IslandMapNodes";
|
||
public const string PcVisualName = "~WL_IslandPcVisual";
|
||
|
||
public static int MapNodesMade, PcHidden, CamerasFixed, ListenersFixed, VisualSwaps;
|
||
public static string LastSwapLog = "";
|
||
|
||
// ─────────────────────────────────────────────────────────────────
|
||
// ① 맵 노드 — LoadMapMgr 자리 채우기
|
||
// ─────────────────────────────────────────────────────────────────
|
||
/// <summary>
|
||
/// 섬 씬에도 `LoadMapMgr` 를 만들어 둔다. 없으면 `PCInfo.Make_Actors:24` 가 죽은 참조를 건드린다.
|
||
/// 🔴 `PCInfo.Make_Actors` 보다 **먼저** 불려야 한다(= sceneLoaded 즉시 · 같은 프레임).
|
||
/// </summary>
|
||
public static void EnsureMapNodes(WLIslandSettings cfg, Scene scene)
|
||
{
|
||
if (InGameInfo.Ins == null) return; // 섬 씬만 단독 Play = 원본 흐름이 없다 → 필요 없다
|
||
var mgr = WLIslandMapNodes.Ensure(cfg);
|
||
if (mgr == null) return;
|
||
mgr.LoadSceneName = cfg.islandSceneName;
|
||
mgr.SetStartPos(IslandStartPos(), WLDungeonCameraRig.SolvedYaw(cfg));
|
||
WLIslandBridge.Log(cfg, "섬 맵 노드 준비(LoadMapMgr) — " + scene.name + " · 시작 " + mgr.StartPos.ToString("F1"));
|
||
}
|
||
|
||
/// <summary>섬의 시작 지점 = FI 플레이어가 서 있는 자리(없으면 원점).</summary>
|
||
static Vector3 IslandStartPos()
|
||
{
|
||
var fiPlayer = Object.FindFirstObjectByType<PlayerController>();
|
||
return fiPlayer != null ? fiPlayer.transform.position : Vector3.zero;
|
||
}
|
||
|
||
/// <summary>던전 씬에도 같은 이유로 `LoadMapMgr` 자리를 채운다(맵 프리팹 없음 = 주소 로드 0).</summary>
|
||
public static void EnsureDungeonMapNodes(WLIslandSettings cfg, Scene scene, WLDungeonDef d)
|
||
{
|
||
if (InGameInfo.Ins == null) return;
|
||
var mgr = WLIslandMapNodes.Ensure(cfg);
|
||
if (mgr == null) return;
|
||
mgr.LoadSceneName = d.sceneName;
|
||
mgr.SetStartPos(DungeonStartPos(d), WLDungeonCameraRig.SolvedYaw(cfg));
|
||
WLIslandBridge.Log(cfg, "던전 맵 노드 준비(LoadMapMgr) — " + scene.name + " · 시작 " + mgr.StartPos.ToString("F1"));
|
||
}
|
||
|
||
/// <summary>던전(아레나)에는 포털이 없다 — 스테이지 표의 앵커가 시작 지점이다.</summary>
|
||
public static Vector3 DungeonStartPos(WLDungeonDef d)
|
||
{
|
||
var def = WLDungeonSceneSetup.StageDefOf(d);
|
||
return def != null ? def.anchor : Vector3.zero;
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────
|
||
// ② 중복 정리
|
||
// ─────────────────────────────────────────────────────────────────
|
||
public static void FixDuplicates(WLIslandSettings cfg, Scene scene)
|
||
{
|
||
ApplyIslandCameraDistance(cfg, scene); // 2026-09-15 PD 「섬과 던전 모두 좀 더 가까이」 — 섬 카메라 거리(SO)
|
||
ApplyPlayerScale(cfg); // 2026-09-16 PD 「캐릭터 30 % 키워」
|
||
if (cfg.fixIslandDuplicates == 0) return;
|
||
|
||
HideWlPc(cfg);
|
||
|
||
// FI 카메라·AudioListener 중복 — Additive 로 붙었을 때만 정리한다.
|
||
// · 섬 씬만 단독으로 열려 있으면(검증 Play) FI 카메라가 유일한 카메라라 끄면 안 된다.
|
||
if (SceneManager.sceneCount <= 1) return;
|
||
|
||
// 카메라 소유권 — 섬은 FarmingIsland 처럼 보여야 한다(PD) → 기본은 FI 카메라가 이긴다.
|
||
if (cfg.islandCameraOwner != 0)
|
||
{
|
||
bool fiWins = cfg.islandCameraOwner == 1;
|
||
var cams = Object.FindObjectsByType<Camera>(FindObjectsSortMode.None);
|
||
for (int i = 0; i < cams.Length; i++)
|
||
{
|
||
var c = cams[i];
|
||
if (c == null) continue;
|
||
bool inIsland = c.gameObject.scene == scene;
|
||
bool want = fiWins ? inIsland : !inIsland;
|
||
if (c.enabled == want) continue;
|
||
if (!want) s_disabledCams.Add(c); // 섬을 떠날 때 되살린다
|
||
c.enabled = want;
|
||
CamerasFixed++;
|
||
}
|
||
}
|
||
|
||
var listeners = Object.FindObjectsByType<AudioListener>(FindObjectsSortMode.None);
|
||
int alive = 0;
|
||
for (int i = 0; i < listeners.Length; i++)
|
||
{
|
||
if (listeners[i] == null || !listeners[i].enabled) continue;
|
||
alive++;
|
||
if (alive > 1) { listeners[i].enabled = false; s_disabledListeners.Add(listeners[i]); ListenersFixed++; }
|
||
}
|
||
WLIslandBridge.Log(cfg, "중복 정리 — PC 숨김 " + PcHidden + " · 카메라 " + CamerasFixed +
|
||
" · AudioListener 해제 " + ListenersFixed);
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────
|
||
// ②-b 섬 카메라 거리 — FI `PlayerVirtualCamera` 의 CinemachineFramingTransposer.m_CameraDistance(씬 값 22)를
|
||
// 섬 로드 때 SO 값으로 덮는다(2026-09-15 PD 「섬과 던전 모두 좀 더 가까이」 · FI 씬·코드 0줄).
|
||
// `islandCamDistance` 0 이하면 무동작 = 씬 값 그대로(되돌리기).
|
||
// ─────────────────────────────────────────────────────────────────
|
||
public static int IslandCamApplied;
|
||
|
||
public static void ApplyIslandCameraDistance(WLIslandSettings cfg, Scene scene)
|
||
{
|
||
if (cfg == null || cfg.islandCamDistance <= 0f) return;
|
||
var vcams = Object.FindObjectsByType<Cinemachine.CinemachineVirtualCamera>(FindObjectsSortMode.None);
|
||
for (int i = 0; i < vcams.Length; i++)
|
||
{
|
||
var v = vcams[i];
|
||
if (v == null || v.gameObject.scene != scene) continue;
|
||
if (!string.IsNullOrEmpty(cfg.islandVcamName) && v.name != cfg.islandVcamName) continue;
|
||
var ft = v.GetCinemachineComponent<Cinemachine.CinemachineFramingTransposer>();
|
||
if (ft == null) continue;
|
||
float was = ft.m_CameraDistance;
|
||
ft.m_CameraDistance = cfg.islandCamDistance;
|
||
IslandCamApplied++;
|
||
string pitchLog = "";
|
||
if (cfg.islandCamPitch > 0f)
|
||
{
|
||
// 2026-09-15 PD 「카메라 각도 60도로 비교」 — vcam 은 Aim 없이 고정 회전이라 트랜스폼 회전의 X 만 바꾼다(씬 값 45).
|
||
var e = v.transform.eulerAngles;
|
||
v.transform.rotation = Quaternion.Euler(cfg.islandCamPitch, e.y, 0f);
|
||
pitchLog = " · 각도 " + e.x.ToString("F0") + " → " + cfg.islandCamPitch.ToString("F0") + "°";
|
||
}
|
||
WLIslandBridge.Log(cfg, "섬 카메라 거리 — " + v.name + " " + was.ToString("F1") + " → " +
|
||
cfg.islandCamDistance.ToString("F1") + " m" + pitchLog);
|
||
}
|
||
}
|
||
|
||
// ── 2026-09-16 PD 「캐릭터의 크기를 30 % 키워」 — FI 플레이어 트랜스폼 스케일(CharacterController·애니 함께 커진다) · 프리팹 0줄 · 표식으로 1회만
|
||
public static int PlayerScaled;
|
||
public static void ApplyPlayerScale(WLIslandSettings cfg)
|
||
{
|
||
if (cfg == null || cfg.islandPlayerScale <= 0f || Mathf.Approximately(cfg.islandPlayerScale, 1f)) return;
|
||
var fiPlayer = Object.FindFirstObjectByType<PlayerController>();
|
||
if (fiPlayer == null || fiPlayer.GetComponent<WLPlayerScaled>() != null) return;
|
||
fiPlayer.transform.localScale = fiPlayer.transform.localScale * cfg.islandPlayerScale;
|
||
fiPlayer.gameObject.AddComponent<WLPlayerScaled>();
|
||
PlayerScaled++;
|
||
WLIslandBridge.Log(cfg, "섬 플레이어 크기 ×" + cfg.islandPlayerScale.ToString("F2") + " → " + fiPlayer.transform.localScale.ToString("F2"));
|
||
}
|
||
|
||
static readonly List<Camera> s_disabledCams = new List<Camera>(4);
|
||
static readonly List<AudioListener> s_disabledListeners = new List<AudioListener>(4);
|
||
|
||
/// <summary>섬을 떠날 때 우리가 끈 것만 되살린다(C8 — 원래대로).</summary>
|
||
public static void RestoreDuplicates()
|
||
{
|
||
for (int i = 0; i < s_disabledCams.Count; i++) if (s_disabledCams[i] != null) s_disabledCams[i].enabled = true;
|
||
for (int i = 0; i < s_disabledListeners.Count; i++) if (s_disabledListeners[i] != null) s_disabledListeners[i].enabled = true;
|
||
s_disabledCams.Clear();
|
||
s_disabledListeners.Clear();
|
||
}
|
||
|
||
/// <summary>WL 전투 PC 를 섬에서 숨긴다(플레이어 2명 방지). 던전에 가면 다시 만들어진다.</summary>
|
||
public static bool HideWlPc(WLIslandSettings cfg)
|
||
{
|
||
if (cfg.hideWlPcOnIsland == 0) return true;
|
||
if (!MyValue.bMyPC || MyValue.MyPC == null) return false;
|
||
if (!MyValue.MyPC.gameObject.activeSelf) return true;
|
||
MyValue.MyPC.gameObject.SetActive(false);
|
||
PcHidden++;
|
||
return true;
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────
|
||
// ③ 캐릭터 교체 — FI 조작 그대로, 모델만 우리 치비로
|
||
// ─────────────────────────────────────────────────────────────────
|
||
public static void SwapPlayerVisual(WLIslandSettings cfg, Scene scene)
|
||
{
|
||
if (cfg.swapIslandPlayer == 0) return;
|
||
|
||
var fiPlayer = Object.FindFirstObjectByType<PlayerController>();
|
||
if (fiPlayer == null) { LastSwapLog = "FI PlayerController 없음 — 교체 건너뜀"; return; }
|
||
if (fiPlayer.transform.Find(PcVisualName) != null) return; // 이미 했다
|
||
|
||
var prefab = WLIslandAssets.LoadPrefab(cfg.pcPrefabPath);
|
||
if (prefab == null) { LastSwapLog = "치비 프리팹을 못 찾았다 — " + cfg.pcPrefabPath; return; }
|
||
|
||
// 🔴 비활성 부모 아래에서 인스턴스 → PCActor 등 전투 컴포넌트의 Awake 가 돌지 않는다.
|
||
var holder = new GameObject("~WLIslandSpawnHolder");
|
||
holder.SetActive(false);
|
||
var visual = Object.Instantiate(prefab, holder.transform);
|
||
visual.name = PcVisualName;
|
||
|
||
StripCombatComponents(visual);
|
||
|
||
visual.transform.SetParent(fiPlayer.transform, false);
|
||
visual.transform.localPosition = Vector3.zero;
|
||
visual.transform.localRotation = Quaternion.identity;
|
||
visual.transform.localScale = new Vector3(cfg.islandPcScale, cfg.islandPcScale, cfg.islandPcScale);
|
||
|
||
// FI 농부 메시 숨김(FI 오브젝트는 파괴하지 않는다 — 끄기만 한다 = 되돌리기 가능)
|
||
int hidden = HideFiModel(cfg, fiPlayer.gameObject, visual);
|
||
|
||
// 애니메이션은 FI 컨트롤러를 그대로 흘린다(둘 다 Humanoid → 리타깃)
|
||
var rig = visual.AddComponent<WLIslandPcRig>();
|
||
rig.Bind(fiPlayer.GetComponent<Animator>(), cfg);
|
||
|
||
Object.Destroy(holder);
|
||
visual.SetActive(true);
|
||
VisualSwaps++;
|
||
|
||
float h = MeasureHeight(visual);
|
||
LastSwapLog = "치비 교체 완료 — 스케일 " + visual.transform.localScale.x.ToString("F4") +
|
||
" · 월드 높이 " + h.ToString("F4") + " m · FI 메시 숨김 " + hidden + "개";
|
||
WLIslandBridge.Log(cfg, LastSwapLog);
|
||
}
|
||
|
||
/// <summary>전투/이동 컴포넌트를 떼어 낸다 — 남기는 것은 Animator · 렌더러 · WL 룩/크기 보정뿐.</summary>
|
||
static void StripCombatComponents(GameObject root)
|
||
{
|
||
// 🔴 [RequireComponent] 의존 때문에 순서에 따라 삭제가 거부될 수 있다 → 2회 훑고 개별 try
|
||
for (int pass = 0; pass < 2; pass++)
|
||
{
|
||
var mbs = root.GetComponentsInChildren<MonoBehaviour>(true);
|
||
for (int i = 0; i < mbs.Length; i++)
|
||
{
|
||
var mb = mbs[i];
|
||
if (mb == null) continue;
|
||
if (mb is WL.Character.WLPcScaleCompensator) continue; // 높이 1.1912 m 유지
|
||
if (mb is WL.Look.Character.WLCharacterLook) continue; // 팔레트 UV 도트화(814x) 유지
|
||
Kill(mb);
|
||
}
|
||
}
|
||
|
||
KillAll(root.GetComponentsInChildren<UnityEngine.AI.NavMeshAgent>(true));
|
||
KillAll(root.GetComponentsInChildren<UnityEngine.AI.NavMeshObstacle>(true));
|
||
KillAll(root.GetComponentsInChildren<Rigidbody>(true));
|
||
KillAll(root.GetComponentsInChildren<Collider>(true)); // CharacterController 포함
|
||
KillAll(root.GetComponentsInChildren<AudioSource>(true));
|
||
KillAll(root.GetComponentsInChildren<AudioListener>(true));
|
||
}
|
||
|
||
static void KillAll<T>(T[] arr) where T : Component
|
||
{
|
||
if (arr == null) return;
|
||
for (int i = 0; i < arr.Length; i++) Kill(arr[i]);
|
||
}
|
||
|
||
static void Kill(Component c)
|
||
{
|
||
if (c == null) return;
|
||
try { Object.DestroyImmediate(c); }
|
||
catch (System.Exception) { var b = c as Behaviour; if (b != null) b.enabled = false; }
|
||
}
|
||
|
||
static int HideFiModel(WLIslandSettings cfg, GameObject fiPlayer, GameObject exclude)
|
||
{
|
||
int n = 0;
|
||
if (!string.IsNullOrEmpty(cfg.fiPlayerModelName))
|
||
{
|
||
var t = FindChild(fiPlayer.transform, cfg.fiPlayerModelName, exclude.transform);
|
||
if (t != null) { t.gameObject.SetActive(false); return 1; }
|
||
}
|
||
var smrs = fiPlayer.GetComponentsInChildren<SkinnedMeshRenderer>(true);
|
||
for (int i = 0; i < smrs.Length; i++)
|
||
{
|
||
if (smrs[i] == null) continue;
|
||
if (smrs[i].transform.IsChildOf(exclude.transform)) continue;
|
||
smrs[i].enabled = false; n++;
|
||
}
|
||
return n;
|
||
}
|
||
|
||
static Transform FindChild(Transform root, string name, Transform skip)
|
||
{
|
||
var all = root.GetComponentsInChildren<Transform>(true);
|
||
for (int i = 0; i < all.Length; i++)
|
||
{
|
||
if (all[i] == root || all[i] == skip) continue;
|
||
if (skip != null && all[i].IsChildOf(skip)) continue;
|
||
if (string.Equals(all[i].name, name, System.StringComparison.OrdinalIgnoreCase)) return all[i];
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/// <summary>월드 AABB 높이(실측 보고용).</summary>
|
||
public static float MeasureHeight(GameObject root)
|
||
{
|
||
var rs = root.GetComponentsInChildren<Renderer>(true);
|
||
bool any = false; Bounds b = new Bounds();
|
||
for (int i = 0; i < rs.Length; i++)
|
||
{
|
||
if (rs[i] == null) continue;
|
||
if (!any) { b = rs[i].bounds; any = true; }
|
||
else b.Encapsulate(rs[i].bounds);
|
||
}
|
||
return any ? b.size.y : 0f;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 섬·던전 공용 `LoadMapMgr`. 원본은 Awake 에서 맵 프리팹을 Addressables 로 받아 오는데,
|
||
/// 섬·던전은 씬 자체가 맵이라 받을 것이 없다 → Awake 만 갈아끼워 **주소 로드 0**·싱글턴만 채운다.
|
||
/// 🔴 원본 `LoadMapMgr.cs` 는 한 줄도 고치지 않는다(파생 + private 필드 주입으로 해결).
|
||
///
|
||
/// ■ WL-816i 가 고친 것 2가지 (PD 실플레이 에러 ①)
|
||
/// ① `MissingReferenceException … WLIslandMapNodes has been destroyed` —
|
||
/// 씬마다 새로 만들던 노드가 `LoadSceneMode` 전환에서 파괴되는데 정적 `Ins` 는 그대로 남아
|
||
/// `Get_PortalPos` 의 `StartCoroutine` 이 죽은 객체를 때렸다.
|
||
/// → **DontDestroyOnLoad 단 하나**만 두고 씬이 바뀌면 시작 지점만 갈아끼운다.
|
||
/// ② `Co_Get_PortalPos` 의 `while (CheckNull(m_MapData)) yield return null;` 이 끝나지 않던 것 —
|
||
/// 우리는 맵 프리팹이 없어 `m_MapData` 가 영원히 null 이라 `Make_Actors` 의 콜백(펫·일꾼)과
|
||
/// `PCActor.Set_Warp`·`MobControlMgr` 의 도달성 검사가 **영영 안 불렸다**.
|
||
/// → 런타임 `MapData` 를 하나 만들어 넣는다. **꺼진 오브젝트**에 붙여서
|
||
/// `MapData.Awake/Start` 가 돌지 않게 한다(= `MapData.Ins` 도, RenderSettings 앰비언트·안개도 무변경).
|
||
/// </summary>
|
||
public sealed class WLIslandMapNodes : LoadMapMgr
|
||
{
|
||
public const int ZoneSlots = 16; // n_PortalIndex 가 무엇이 오든 IndexOutOfRange 가 안 나게
|
||
public const int ObjSlots = 8; // Get_tfObj(index) 여유분
|
||
|
||
static WLIslandMapNodes s_ins;
|
||
static readonly System.Reflection.FieldInfo F_MapData =
|
||
typeof(LoadMapMgr).GetField("m_MapData", System.Reflection.BindingFlags.NonPublic |
|
||
System.Reflection.BindingFlags.Instance);
|
||
static readonly System.Reflection.FieldInfo F_GoMap =
|
||
typeof(LoadMapMgr).GetField("go_map", System.Reflection.BindingFlags.NonPublic |
|
||
System.Reflection.BindingFlags.Instance);
|
||
|
||
Transform _start;
|
||
MapData _data;
|
||
GameObject _dummyMap;
|
||
|
||
/// <summary>현재 씬의 시작 지점(보고·진단용).</summary>
|
||
public Vector3 StartPos { get { return _start != null ? _start.position : Vector3.zero; } }
|
||
|
||
protected override void Awake() { Ins = this; isIns = true; }
|
||
|
||
/// <summary>
|
||
/// 씬을 넘어 사는 노드를 **하나만** 만들고 싱글턴 자리를 되찾는다.
|
||
/// (다른 맵의 원본 `LoadMapMgr` 가 `Ins` 를 가져갔다가 그 씬이 언로드되면 죽은 참조가 남는다 — 그것도 여기서 덮는다.)
|
||
/// </summary>
|
||
public static WLIslandMapNodes Ensure(WLIslandSettings cfg)
|
||
{
|
||
if (s_ins == null)
|
||
{
|
||
var go = new GameObject(WLIslandSceneSetup.MapNodesName);
|
||
Object.DontDestroyOnLoad(go);
|
||
s_ins = go.AddComponent<WLIslandMapNodes>();
|
||
s_ins.LoadMapPrefab = "";
|
||
s_ins.Build();
|
||
WLIslandSceneSetup.MapNodesMade++;
|
||
}
|
||
Ins = s_ins; isIns = true; // 죽은 참조로 남지 않게 매번 되찾는다
|
||
return s_ins;
|
||
}
|
||
|
||
void Build()
|
||
{
|
||
// ① 시작 지점 노드 — 포털/부활/농부/오브젝트 자리를 전부 이 하나로 답한다(던전은 아레나라 포털이 없다).
|
||
var holder = new GameObject("MapNodes");
|
||
holder.transform.SetParent(transform, false);
|
||
holder.SetActive(false); // 🔴 꺼진 채로 AddComponent → MapData.Awake/Start 가 아예 안 돈다
|
||
|
||
var startGo = new GameObject("start");
|
||
startGo.transform.SetParent(holder.transform, false);
|
||
_start = startGo.transform;
|
||
|
||
_data = holder.AddComponent<MapData>();
|
||
var objs = new Transform[ObjSlots];
|
||
for (int i = 0; i < ObjSlots; i++) objs[i] = _start;
|
||
|
||
_data.m_MapZoneData = new List<MapZoneData>(ZoneSlots);
|
||
for (int i = 0; i < ZoneSlots; i++)
|
||
{
|
||
_data.m_MapZoneData.Add(new MapZoneData
|
||
{
|
||
go_portal = startGo,
|
||
go_reincarnation = startGo,
|
||
go_mazeNextPortal = null, // Set_MazeNextPortal 은 null 이면 그냥 넘어간다(원본 확인)
|
||
tf_Farmer = _start,
|
||
tf_Obj = objs,
|
||
CamRot_Portal = 0f,
|
||
CamRot_Reincarnation = 0f,
|
||
});
|
||
}
|
||
|
||
// ② SetActive_Map(bool) 이 만지는 자리(비어 있으면 NullReference) — 아무것도 안 든 더미.
|
||
_dummyMap = new GameObject("map");
|
||
_dummyMap.transform.SetParent(transform, false);
|
||
|
||
// ③ 원본의 private 필드에 넣는다 — 원본 코드 0줄로 `Co_Get_*` 의 대기를 즉시 끝낸다.
|
||
if (F_MapData != null) F_MapData.SetValue(this, _data);
|
||
if (F_GoMap != null) F_GoMap.SetValue(this, _dummyMap);
|
||
}
|
||
|
||
/// <summary>씬이 바뀔 때마다 시작 지점과 카메라 초기 방향만 갈아끼운다(객체는 그대로 산다).</summary>
|
||
public void SetStartPos(Vector3 pos, float camYaw)
|
||
{
|
||
if (_start == null) Build();
|
||
_start.position = pos - Vector3.forward; // 원본이 +Vector3.forward 를 더해 돌려준다 → 결과가 pos 가 되게
|
||
if (_data == null || _data.m_MapZoneData == null) return;
|
||
for (int i = 0; i < _data.m_MapZoneData.Count; i++)
|
||
{
|
||
var z = _data.m_MapZoneData[i];
|
||
if (z == null) continue;
|
||
z.CamRot_Portal = camYaw;
|
||
z.CamRot_Reincarnation = camYaw;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>FI 애니메이터의 파라미터를 치비 애니메이터로 그대로 흘린다(Humanoid 리타깃).</summary>
|
||
public sealed class WLIslandPcRig : MonoBehaviour
|
||
{
|
||
Animator _src, _dst;
|
||
AnimatorControllerParameter[] _params;
|
||
|
||
public void Bind(Animator src, WLIslandSettings cfg)
|
||
{
|
||
_src = src;
|
||
_dst = GetComponent<Animator>();
|
||
if (_dst == null) _dst = GetComponentInChildren<Animator>(true);
|
||
if (_src == null || _dst == null) return;
|
||
|
||
if (cfg.mirrorFiAnimator != 0)
|
||
{
|
||
_dst.runtimeAnimatorController = _src.runtimeAnimatorController;
|
||
_dst.applyRootMotion = false;
|
||
_dst.updateMode = _src.updateMode;
|
||
_dst.cullingMode = AnimatorCullingMode.AlwaysAnimate;
|
||
_params = _src.parameters;
|
||
}
|
||
}
|
||
|
||
void LateUpdate()
|
||
{
|
||
if (_src == null || _dst == null || _params == null) return;
|
||
for (int i = 0; i < _params.Length; i++)
|
||
{
|
||
var p = _params[i];
|
||
switch (p.type)
|
||
{
|
||
case AnimatorControllerParameterType.Float: _dst.SetFloat(p.nameHash, _src.GetFloat(p.nameHash)); break;
|
||
case AnimatorControllerParameterType.Int: _dst.SetInteger(p.nameHash, _src.GetInteger(p.nameHash)); break;
|
||
case AnimatorControllerParameterType.Bool: _dst.SetBool(p.nameHash, _src.GetBool(p.nameHash)); break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>섬 코인(= `IslandManager.Coin` · FI 의 유일한 재화)에 닿는 유일한 창구.</summary>
|
||
public static class WLIslandCoin
|
||
{
|
||
public static bool Ready { get { return IslandManager.Instance != null; } }
|
||
|
||
public static bool TryGet(out int coin)
|
||
{
|
||
coin = 0;
|
||
var m = IslandManager.Instance;
|
||
if (m == null) return false;
|
||
coin = m.Coin;
|
||
return true;
|
||
}
|
||
|
||
public static bool Add(int amount)
|
||
{
|
||
var m = IslandManager.Instance;
|
||
if (m == null) return false;
|
||
m.Coin += amount; // setter 가 OnCoinChanged 를 쏜다 → CoinDisplay 가 갱신된다
|
||
return true;
|
||
}
|
||
}
|
||
|
||
/// <summary>프리팹 로드 — 실기(Addressables)와 에디터 단독 Play 양쪽을 덮는다.</summary>
|
||
public static class WLIslandAssets
|
||
{
|
||
static readonly Dictionary<string, GameObject> s_cache = new Dictionary<string, GameObject>(4);
|
||
|
||
static TMPro.TMP_FontAsset s_font;
|
||
static bool s_fontTried;
|
||
|
||
/// <summary>이름표·버튼용 한글 폰트(없으면 null = TMP 기본 폰트).</summary>
|
||
public static TMPro.TMP_FontAsset Font(string path)
|
||
{
|
||
if (s_fontTried) return s_font;
|
||
s_fontTried = true;
|
||
if (string.IsNullOrEmpty(path)) return null;
|
||
#if UNITY_EDITOR
|
||
s_font = UnityEditor.AssetDatabase.LoadAssetAtPath<TMPro.TMP_FontAsset>(path);
|
||
if (s_font != null) return s_font;
|
||
#endif
|
||
try
|
||
{
|
||
var h = UnityEngine.AddressableAssets.Addressables.LoadAssetAsync<TMPro.TMP_FontAsset>(path);
|
||
s_font = h.WaitForCompletion();
|
||
}
|
||
catch (System.Exception) { s_font = null; }
|
||
return s_font;
|
||
}
|
||
|
||
public static GameObject LoadPrefab(string path)
|
||
{
|
||
if (string.IsNullOrEmpty(path)) return null;
|
||
GameObject go;
|
||
if (s_cache.TryGetValue(path, out go) && go != null) return go;
|
||
|
||
#if UNITY_EDITOR
|
||
go = UnityEditor.AssetDatabase.LoadAssetAtPath<GameObject>(path);
|
||
if (go != null) { s_cache[path] = go; return go; }
|
||
#endif
|
||
// 빌드에서는 Addressables 동기 로드가 필요하다(섬 진입은 이미 로딩 화면 안이다).
|
||
try
|
||
{
|
||
var h = UnityEngine.AddressableAssets.Addressables.LoadAssetAsync<GameObject>(path);
|
||
go = h.WaitForCompletion();
|
||
if (go != null) s_cache[path] = go;
|
||
}
|
||
catch (System.Exception ex) { Debug.LogWarning("[WL-816d] 프리팹 로드 실패 " + path + " — " + ex.Message); }
|
||
return go;
|
||
}
|
||
}
|
||
}
|