From 39fcbe5d440df10c7b892eb74081eb59071403aa Mon Sep 17 00:00:00 2001 From: swrring Date: Tue, 15 Sep 2026 16:04:08 +0900 Subject: [PATCH] =?UTF-8?q?[WL-816zc]=20=EC=84=AC=20=EC=B4=88=EA=B8=B0=20?= =?UTF-8?q?=ED=94=8C=EB=A1=9C=EC=9A=B0=20v2=20(#816)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 캠프파이어 설치(10) → 상인 설치(30) → 사과나무 설치(60) → 던전 생성(100). 한 번에 발판 1개 · 단계는 PlayerPrefs 1키(WL_Island_FlowStep · 시작마다 초기화). 원본 섬 확장 가격판(FI Purchaser)은 첫 던전 승리 전까지 렌더러·콜라이더만 끄고, 승리(OnStageCleared) 시 WL_Island_ExpansionUnlocked=1 → 그대로 다시 켠다 (이후 확장 순서는 FI 원본 Island.Constraint 그대로 · 우리 코드는 켜고 끄기만). 밭은 시작 시 생성 보류 유지(hideFarms 1) · 던전 2~4 행 enabled_ 0. 되돌리기 = flowV2Enabled 0. 원본(Assets/FarmingIsland/** · Assets/Script/**) 0줄. Co-Authored-By: Claude Opus 5 (1M context) --- .../Resources/WL/WLIslandSettings.asset | 8 +- Assets/WL/Island/WLIslandBridge.cs | 7 +- Assets/WL/Island/WLIslandCampDecor.cs | 14 +- Assets/WL/Island/WLIslandFlowV2.cs | 427 ++++++++++++++++++ Assets/WL/Island/WLIslandFlowV2.cs.meta | 2 + Assets/WL/Island/WLIslandGateFlow.cs | 29 +- Assets/WL/Island/WLIslandSave.cs | 3 + Assets/WL/Island/WLIslandSettings.cs | 121 +++++ 8 files changed, 599 insertions(+), 12 deletions(-) create mode 100644 Assets/WL/Island/WLIslandFlowV2.cs create mode 100644 Assets/WL/Island/WLIslandFlowV2.cs.meta diff --git a/Assets/WL/Island/Resources/WL/WLIslandSettings.asset b/Assets/WL/Island/Resources/WL/WLIslandSettings.asset index d888a30bb..2faf667ca 100644 --- a/Assets/WL/Island/Resources/WL/WLIslandSettings.asset +++ b/Assets/WL/Island/Resources/WL/WLIslandSettings.asset @@ -94,7 +94,7 @@ MonoBehaviour: campDecorSnapToGround: 1 applyStartCoin: 1 startCoin: 10 - saveResetToken: 816g-1 + saveResetToken: 816zc-1 resetSaveEveryStart: 1 dungeonCameraMatchIsland: 1 camPitch: 45 @@ -128,7 +128,7 @@ MonoBehaviour: gatePrefabPath: Assets/WL/Island/Prefabs/WL_DungeonGate_Arch.prefab platformUseFarm: 1 platformPosition: {x: 0, y: 0, z: 0} - - enabled_: 1 + - enabled_: 0 id: 2 displayName: "\uB358\uC804 2" sceneName: WL_Dungeon02 @@ -141,7 +141,7 @@ MonoBehaviour: gatePrefabPath: Assets/WL/Island/Prefabs/WL_DungeonGate_Arch.prefab platformUseFarm: 0 platformPosition: {x: -2, y: 0, z: -2} - - enabled_: 1 + - enabled_: 0 id: 3 displayName: "\uB358\uC804 3" sceneName: WL_Dungeon01 @@ -154,7 +154,7 @@ MonoBehaviour: gatePrefabPath: Assets/WL/Island/Prefabs/WL_DungeonGate_Arch.prefab platformUseFarm: 0 platformPosition: {x: -2.5, y: 0, z: 8.5} - - enabled_: 1 + - enabled_: 0 id: 4 displayName: "\uB358\uC804 4" sceneName: WL_Dungeon02 diff --git a/Assets/WL/Island/WLIslandBridge.cs b/Assets/WL/Island/WLIslandBridge.cs index 14631f09d..014f5b203 100644 --- a/Assets/WL/Island/WLIslandBridge.cs +++ b/Assets/WL/Island/WLIslandBridge.cs @@ -255,9 +255,14 @@ namespace WL.Island static void OnStageCleared(in StageClearResult r) { var cfg = WLIslandSettings.Instance; - if (cfg == null || cfg.enabled_ == 0 || cfg.rewardEnabled == 0) return; + if (cfg == null || cfg.enabled_ == 0) return; if (WLDungeonSceneSetup.Current == null) return; // 던전이 아닌 스테이지(#815 데모)는 건드리지 않는다 + // 🔴 WL-816zc ② — 「전투 승리 후 섬 확장 노드 추가」. 보상과 무관하게 승리 그 자체로 연다. + WLIslandExpansionGate.Unlock(cfg); + + if (cfg.rewardEnabled == 0) return; + int gold = ComputeGold(cfg, r); SetPending(cfg, gold); ClearGrants++; diff --git a/Assets/WL/Island/WLIslandCampDecor.cs b/Assets/WL/Island/WLIslandCampDecor.cs index 1834a72e3..23c256afb 100644 --- a/Assets/WL/Island/WLIslandCampDecor.cs +++ b/Assets/WL/Island/WLIslandCampDecor.cs @@ -36,9 +36,19 @@ namespace WL.Island { Clear(); if (cfg == null || cfg.campDecorEnabled == 0) return; - if (WLIslandGateFlow.UnlockedCount(cfg) < 1) { LastLog = "캠프 없음 — 첫 자리 미개방"; return; } - Vector3 center = WLIslandGateFlow.PlatformPosFor(cfg, 0); + // WL-816zc — 초기 플로우 v2 에서는 「캠프파이어 설치」가 **1단계**다(단계 표의 첫 줄 자리). + Vector3 center; + if (WLIslandFlowV2.Active(cfg)) + { + if (WLIslandFlowV2.Step(cfg) < 1) { LastLog = "캠프 없음 — 1단계(캠프파이어 설치) 미지불"; return; } + center = WLIslandFlowV2.PosFor(cfg, 0); + } + else + { + if (WLIslandGateFlow.UnlockedCount(cfg) < 1) { LastLog = "캠프 없음 — 첫 자리 미개방"; return; } + center = WLIslandGateFlow.PlatformPosFor(cfg, 0); + } LastCenter = center; var root = new GameObject(RootName); diff --git a/Assets/WL/Island/WLIslandFlowV2.cs b/Assets/WL/Island/WLIslandFlowV2.cs new file mode 100644 index 000000000..6126ae15d --- /dev/null +++ b/Assets/WL/Island/WLIslandFlowV2.cs @@ -0,0 +1,427 @@ +// ───────────────────────────────────────────────────────────────────────────── +// WLIslandFlowV2.cs — WL-816zc 「섬 초기 플로우 v2」 +// +// PD 원문(2026-09-15) +// 「원본 에셋과 같이 섬이 확장되는 방식과 순서는 유지하되, 내가 수정한 초기 플로우만 바뀌어야 해. +// <최초 시작 시 밭 생성은 보류> +// ① 캠프파이어 설치 → 상인 설치 → 사과 나무 설치 → 던전 생성 +// ② 전투 승리 후 섬 확장 노드 추가 → 이후 원본 에셋의 확장 순서대로 확장시켜야 함.」 +// +// ■ 발판은 **한 번에 하나**다. 값을 치르면 그 자리에 그 단계의 설치물이 남고, 다음 단계의 발판이 선다. +// 순서·문구·가격·자리·설치물은 전부 `WLIslandSettings.flowSteps[]` 값이다(코드 수정 0 · C45). +// ■ 어디까지 왔는지는 PlayerPrefs 키 **1개**(`flowStepPrefsKey`)로만 센다. +// 섬으로 돌아오면 이 숫자를 보고 이미 치른 설치물을 다시 놓는다(세이브 복원). +// ■ 원본 섬 확장 가격판(FI `Purchaser`)은 **첫 던전 승리 전까지 숨긴다**(렌더러·콜라이더만 끈다). +// 승리하면 그대로 다시 켜지고, 이후 확장 순서는 **FI 원본**(`Island.Constraint`)이 정한다 — 우리는 켜고 끄기만. +// ■ 되돌리기 = `flowV2Enabled = 0` → 지금까지의 「던전 발판 4개」 방식 그대로. +// +// 🔴 원본(Assets/FarmingIsland/** · Assets/Script/**)은 한 줄도 고치지 않는다 — 인스턴스만 만든다. +// 🔴 Debug.LogError / Debug.LogException 을 쓰지 않는다(ErrorLogHookManager 가 플레이어 팝업을 띄운다). +// ───────────────────────────────────────────────────────────────────────────── + +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.SceneManagement; +using FIPurchaser = CryingSnow.FarmingIsland.Purchaser; + +namespace WL.Island +{ + /// 초기 플로우(단계 표) 진행 + 설치물 배치. 정적 · 씬에 남는 상태 없음. + public static class WLIslandFlowV2 + { + const string RootName = "~WL_FlowInstalls"; + + // ── 진단(프로브·보고가 읽는다 · 실측만) + public static int InstallsMade; + public static int LastStep; + public static string LastLog = ""; + + static readonly List s_made = new List(8); + + /// 이 플로우가 지금 동작 중인가(꺼져 있으면 기존 발판 방식이 그대로 돈다). + public static bool Active(WLIslandSettings cfg) + { + return cfg != null && cfg.gateFlowEnabled != 0 && cfg.flowV2Enabled != 0 && StepCount(cfg) > 0; + } + + /// 켜져 있는 단계 수. + public static int StepCount(WLIslandSettings cfg) + { + if (cfg == null || cfg.flowSteps == null) return 0; + int n = 0; + for (int i = 0; i < cfg.flowSteps.Length; i++) + if (cfg.flowSteps[i] != null && cfg.flowSteps[i].enabled_ != 0) n++; + return n; + } + + /// 켜져 있는 것 중 n 번째 단계(없으면 null). + public static WLFlowStepDef Nth(WLIslandSettings cfg, int n) + { + if (cfg == null || cfg.flowSteps == null || n < 0) return null; + int k = 0; + for (int i = 0; i < cfg.flowSteps.Length; i++) + { + var s = cfg.flowSteps[i]; + if (s == null || s.enabled_ == 0) continue; + if (k == n) return s; + k++; + } + return null; + } + + /// 지금까지 치른 단계 수(PlayerPrefs 1키 · FI 세이브를 건드리지 않는다). + public static int Step(WLIslandSettings cfg) + { + if (cfg == null || string.IsNullOrEmpty(cfg.flowStepPrefsKey)) return 0; + return Mathf.Max(0, PlayerPrefs.GetInt(cfg.flowStepPrefsKey, 0)); + } + + public static void SetStep(WLIslandSettings cfg, int n) + { + if (cfg == null || string.IsNullOrEmpty(cfg.flowStepPrefsKey)) return; + PlayerPrefs.SetInt(cfg.flowStepPrefsKey, Mathf.Max(0, n)); + PlayerPrefs.Save(); + LastStep = Mathf.Max(0, n); + } + + /// n 번째 단계의 발판 자리(표가 가리킨 던전 줄의 좌표 · -1 이면 밭 구역 중심). + public static Vector3 PosFor(WLIslandSettings cfg, int n) + { + if (cfg == null) return Vector3.zero; + + var s = Nth(cfg, n); + Vector3 p; + if (s == null) p = cfg.platformPosition; + else if (s.platformFromDungeonRow >= 0) + { + var d = cfg.DungeonRow(s.platformFromDungeonRow); + p = d != null ? d.platformPosition : s.platformPosition; + } + else if (cfg.platformUseFarmPosition != 0 && WLIslandGateFlow.HaveFarmCenter) p = WLIslandGateFlow.FarmCenter; + else p = s.platformPosition; + + p += cfg.platformOffset; + return WLIslandGateFlow.SnapToGround(cfg, p); + } + + // ───────────────────────────────────────────────────────────────── + // 발판 + 설치물 다시 그리기 (섬 준비 직후 · 지불 직후 둘 다 여기로) + // ───────────────────────────────────────────────────────────────── + public static void Refresh(WLIslandSettings cfg, Scene scene) + { + if (!Active(cfg)) return; + + int step = Step(cfg); + int total = StepCount(cfg); + LastStep = step; + + // ① 이미 치른 단계의 설치물을 다시 놓는다(세이브 복원 · 매번 지우고 새로 놓는다). + ClearInstalls(); + InstallsMade = 0; + for (int i = 0; i < step && i < total; i++) + if (Install(cfg, scene, Nth(cfg, i), PosFor(cfg, i))) InstallsMade++; + + // 캠프 장식은 전용 코드가 자기 자리(1단계 자리)에 놓는다. + WLIslandCampDecor.Refresh(cfg, scene); + + // ② 다음 발판 1개 + if (WLGoldPlatform.Instance != null) + { + Object.Destroy(WLGoldPlatform.Instance.gameObject); + WLGoldPlatform.Instance = null; + } + + if (step >= total) + { + LastLog = "발판 없음 — 초기 플로우 " + step + "/" + total + " 단계 완료"; + WLIslandBridge.Log(cfg, LastLog); + return; + } + + var next = Nth(cfg, step); + Vector3 pos = PosFor(cfg, step); + string label = string.Format(cfg.flowLabelFormat, next.label, next.price); + + // 🔴 값을 치른 자리에 다음 발판이 곧바로 생기면 **가만히 서 있기만 해도** 연달아 팔린다(816g 실측). + // FI 섬 확장처럼 「한 번 내려왔다가 다시 올라오기」를 요구한다. + var runner = WLIslandBridge.Runner; + if (cfg.platformRequireStepOff != 0 && runner != null && WLIslandGateFlow.PlayerInRange(cfg, pos)) + { + runner.StartCoroutine(Co_SpawnWhenClear(cfg, scene, pos, next.price, step, label, total)); + LastLog = "발판 대기 — 자리에서 내려오면 " + (step + 1) + "단계 「" + next.label + "」 발판을 놓는다"; + WLIslandBridge.Log(cfg, LastLog); + return; + } + + Spawn(cfg, scene, pos, next.price, step, label, total); + } + + static void Spawn(WLIslandSettings cfg, Scene scene, Vector3 pos, int price, int step, string label, int total) + { + var p = WLGoldPlatform.Create(cfg, scene, pos, price, step, label); + if (p != null) { WLIslandGateFlow.PlatformsMade++; WLIslandGateFlow.LastPlatformPos = pos; } + LastLog = "플로우 발판 배치 — " + (step + 1) + "/" + total + " 「" + label.Replace("\n", " ") + + "」 @ " + pos.ToString("F2"); + WLIslandBridge.Log(cfg, LastLog); + } + + static IEnumerator Co_SpawnWhenClear(WLIslandSettings cfg, Scene scene, Vector3 pos, + int price, int step, string label, int total) + { + float t = 0f; + while (t < cfg.platformStepOffTimeoutSeconds && WLIslandGateFlow.PlayerInRange(cfg, pos)) + { + t += Time.unscaledDeltaTime; + yield return null; + } + if (WLGoldPlatform.Instance != null) yield break; // 그 사이 다른 경로로 이미 생겼다 + Spawn(cfg, scene, pos, price, step, label, total); + } + + // ───────────────────────────────────────────────────────────────── + // 지불 완료 → 그 단계의 설치물이 그 자리에 남는다 + // ───────────────────────────────────────────────────────────────── + public static void OnPaid(WLIslandSettings cfg, Vector3 platformPos) + { + if (!Active(cfg)) return; + + int step = Step(cfg); + int total = StepCount(cfg); + if (step >= total) return; + + var s = Nth(cfg, step); + SetStep(cfg, step + 1); + + var scene = SceneManager.GetSceneByName(cfg.islandSceneName); + if (!scene.IsValid() || !scene.isLoaded) scene = SceneManager.GetActiveScene(); + + // 던전 단계 = 원래의 해금 카운터도 같이 올린다 → 섬 복귀 시 `SpawnGates` 가 입구를 다시 놓는다. + if (s != null && s.kind == "dungeon") + { + WLIslandGateFlow.SetUnlockedCount(cfg, DungeonStepsDone(cfg, step + 1)); + WLIslandGateFlow.Unlocks++; + } + + LastLog = "🔴 지불 완료 — " + (step + 1) + "/" + total + " 「" + (s != null ? s.label : "?") + + "」 · 이 자리(" + platformPos.ToString("F1") + ")의 발판은 영영 사라진다"; + WLIslandBridge.Log(cfg, LastLog); + + Refresh(cfg, scene); + } + + /// 치른 단계 중 던전 단계가 몇 개인지(= 열려 있어야 할 던전 수). + static int DungeonStepsDone(WLIslandSettings cfg, int stepsDone) + { + int n = 0; + for (int i = 0; i < stepsDone; i++) + { + var s = Nth(cfg, i); + if (s != null && s.kind == "dungeon") n++; + } + return n; + } + + // ───────────────────────────────────────────────────────────────── + // 설치물 하나 + // ───────────────────────────────────────────────────────────────── + static bool Install(WLIslandSettings cfg, Scene scene, WLFlowStepDef s, Vector3 pos) + { + if (cfg == null || s == null) return false; + + if (s.kind == "camp") return false; // 캠프는 WLIslandCampDecor 가 놓는다 + + if (s.kind == "dungeon") + { + var d = cfg.DungeonRow(s.dungeonRow); + if (d != null) WLIslandBridge.SpawnOneGate(cfg, d, scene); // 이미 있으면 안에서 건너뛴다 + return d != null; + } + + // kind = prefab + if (string.IsNullOrEmpty(s.prefabPath)) return false; + var root = EnsureRoot(scene); + var go = SpawnPrefab(cfg, s.prefabPath, root.transform, pos, s.yaw, s.scale, s.keepCollider != 0); + if (go == null) return false; + + // 덧붙일 것(예: 좌판에 상인이 안 들어 있을 때만 상인을 넣는다) — 원본 프리팹 그대로, 설치물의 자식으로. + if (!string.IsNullOrEmpty(s.extraPrefabPath) && !HasComponentNamed(go, s.extraSkipIfComponent)) + { + var rot = Quaternion.Euler(0f, s.yaw, 0f); + SpawnPrefab(cfg, s.extraPrefabPath, go.transform, pos + rot * s.extraOffset, + s.yaw, s.scale, s.keepCollider != 0); + } + return true; + } + + static GameObject EnsureRoot(Scene scene) + { + for (int i = 0; i < s_made.Count; i++) + if (s_made[i] != null && s_made[i].name == RootName) return s_made[i]; + + var root = new GameObject(RootName); + if (scene.IsValid() && scene.isLoaded) SceneManager.MoveGameObjectToScene(root, scene); + s_made.Add(root); + return root; + } + + static GameObject SpawnPrefab(WLIslandSettings cfg, string path, Transform parent, Vector3 pos, + float yaw, float scale, bool keepCollider) + { + var prefab = WLIslandAssets.LoadPrefab(path); + if (prefab == null) { WLIslandBridge.Log(cfg, "플로우 설치물 프리팹 없음 — " + path); return null; } + + // 🔴 부모를 주고 만든다 — FI 스크립트(예: `Merchant.Awake` 의 `GetComponentInParent()`)가 + // Awake 시점에 부모를 볼 수 있어야 한다. 부모의 씬에 그대로 들어가므로 씬 이동도 필요 없다. + var go = Object.Instantiate(prefab, parent); + go.name = "~WL_Flow_" + prefab.name; + go.transform.position = Snap(cfg, pos); + go.transform.rotation = Quaternion.Euler(0f, yaw, 0f); + float sc = scale > 0.001f ? scale : 1f; + go.transform.localScale = prefab.transform.localScale * sc; + + if (!keepCollider) + { + // 장식 = 이동 방해 0 · NavMesh 재베이크 없음(816y 캠프 장식과 같은 원칙). + var cols = go.GetComponentsInChildren(true); + for (int i = 0; i < cols.Length; i++) if (cols[i] != null) cols[i].enabled = false; + } + return go; + } + + static bool HasComponentNamed(GameObject go, string typeName) + { + if (go == null || string.IsNullOrEmpty(typeName)) return false; + var comps = go.GetComponentsInChildren(true); + for (int i = 0; i < comps.Length; i++) + if (comps[i] != null && comps[i].GetType().Name == typeName) return true; + return false; + } + + static Vector3 Snap(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; + } + + public static void ClearInstalls() + { + for (int i = 0; i < s_made.Count; i++) + if (s_made[i] != null) Object.Destroy(s_made[i]); + s_made.Clear(); + } + } + + // ───────────────────────────────────────────────────────────────────── + // 원본 섬 확장 노드 — 첫 던전 승리 전까지 숨김 + // + // 🔴 FI 의 확장 **순서·조건은 건드리지 않는다**(`IslandManager.RefreshIsland` · `Island.Constraint` 그대로). + // 우리는 이미 만들어진 가격판의 **렌더러·콜라이더만** 껐다 켠다 = FI 세이브·격자에 흔적 0. + // `SetActive(false)` 를 쓰지 않는 이유는 816g 와 같다(꺼진 오브젝트에서 코루틴이 시작되면 에러). + // ───────────────────────────────────────────────────────────────────── + public static class WLIslandExpansionGate + { + // ── 진단(프로브·보고가 읽는다 · 실측만) + public static int PurchasersHidden; + public static string LastLog = ""; + + static readonly List s_rends = new List(64); + static readonly List s_cols = new List(64); + + /// 첫 던전 승리를 이미 했는가(기능이 꺼져 있으면 항상 true = 원본 그대로). + public static bool IsUnlocked(WLIslandSettings cfg) + { + if (cfg == null || cfg.expansionAfterFirstWin == 0) return true; + if (string.IsNullOrEmpty(cfg.expansionUnlockPrefsKey)) return true; + return PlayerPrefs.GetInt(cfg.expansionUnlockPrefsKey, 0) != 0; + } + + /// 전투 승리 시 1회 — 이후 섬 확장은 FI 원본 순서대로 열린다. + public static void Unlock(WLIslandSettings cfg) + { + if (cfg == null || cfg.expansionAfterFirstWin == 0) return; + if (string.IsNullOrEmpty(cfg.expansionUnlockPrefsKey)) return; + if (PlayerPrefs.GetInt(cfg.expansionUnlockPrefsKey, 0) != 0) return; + + PlayerPrefs.SetInt(cfg.expansionUnlockPrefsKey, 1); + PlayerPrefs.Save(); + LastLog = "🔴 첫 전투 승리 — 원본 섬 확장 노드 개방(이후 순서는 FI 원본이 정한다)"; + WLIslandBridge.Log(cfg, LastLog); + } + + /// 섬이 준비될 때마다 1회 — 아직 승리 전이면 원본 가격판을 숨기고, 승리했으면 그대로 둔다. + public static void Apply(WLIslandSettings cfg, Scene scene) + { + if (cfg == null || cfg.gateFlowEnabled == 0 || cfg.expansionAfterFirstWin == 0) return; + + if (IsUnlocked(cfg)) + { + Restore(); + LastLog = "확장 노드 표시 — 첫 승리 완료(FI 원본 순서)"; + WLIslandBridge.Log(cfg, LastLog); + return; + } + + HideNow(cfg, scene); + + // FI 는 세이브 복원·이웃 갱신 시점에 가격판을 **나중에** 만들기도 한다 — 잠시 더 훑는다. + var runner = WLIslandBridge.Runner; + if (runner != null && cfg.expansionHideSeconds > 0f) + runner.StartCoroutine(Co_Guard(cfg, scene)); + } + + public static int HideNow(WLIslandSettings cfg, Scene scene) + { + int n = 0; + var all = Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None); + for (int i = 0; i < all.Length; i++) + { + var p = all[i]; + if (p == null) continue; + if (scene.IsValid() && p.gameObject.scene != scene) continue; + if (p.GetComponentInParent() != null) continue; // 우리 발판은 건드리지 않는다 + + var rends = p.GetComponentsInChildren(true); + for (int r = 0; r < rends.Length; r++) + if (rends[r] != null && rends[r].enabled) { rends[r].enabled = false; s_rends.Add(rends[r]); n++; } + + var cols = p.GetComponentsInChildren(true); + for (int c = 0; c < cols.Length; c++) + if (cols[c] != null && cols[c].enabled) { cols[c].enabled = false; s_cols.Add(cols[c]); } + } + if (n > 0) + { + PurchasersHidden += n; + LastLog = "원본 확장 가격판 숨김 — 렌더러 " + n + "개(첫 던전 승리 전)"; + WLIslandBridge.Log(cfg, LastLog); + } + return n; + } + + static IEnumerator Co_Guard(WLIslandSettings cfg, Scene scene) + { + float wait = Mathf.Max(0.1f, cfg.expansionHideInterval); + float t = 0f; + while (t < cfg.expansionHideSeconds) + { + yield return new WaitForSeconds(wait); + t += wait; + if (cfg == null || cfg.expansionAfterFirstWin == 0) yield break; + if (IsUnlocked(cfg)) { Restore(); yield break; } + HideNow(cfg, scene); + } + } + + /// 우리가 껐던 것만 되돌린다(FI 가 스스로 끈 것은 손대지 않는다). + public static void Restore() + { + for (int i = 0; i < s_rends.Count; i++) if (s_rends[i] != null) s_rends[i].enabled = true; + for (int i = 0; i < s_cols.Count; i++) if (s_cols[i] != null) s_cols[i].enabled = true; + s_rends.Clear(); + s_cols.Clear(); + } + } +} diff --git a/Assets/WL/Island/WLIslandFlowV2.cs.meta b/Assets/WL/Island/WLIslandFlowV2.cs.meta new file mode 100644 index 000000000..820374bb6 --- /dev/null +++ b/Assets/WL/Island/WLIslandFlowV2.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8c3f1a274b6d4e1f9a05d7c2e6b83f41 diff --git a/Assets/WL/Island/WLIslandGateFlow.cs b/Assets/WL/Island/WLIslandGateFlow.cs index 0dcbb41b0..7b4a21314 100644 --- a/Assets/WL/Island/WLIslandGateFlow.cs +++ b/Assets/WL/Island/WLIslandGateFlow.cs @@ -100,13 +100,22 @@ namespace WL.Island s_farmCenter = farmCenter; s_haveFarm = haveFarm; + // WL-816zc ③ — 원본 섬 확장 가격판은 첫 던전 승리 전까지 숨긴다(켜고 끄기만 · FI 순서 그대로). + WLIslandExpansionGate.Apply(cfg, scene); + RefreshPlatform(cfg, scene); - WLIslandCampDecor.Refresh(cfg, scene); // WL-816y — 세이브 복원(해금 ≥ 1 이면 캠프 재생성) + // v2 는 RefreshPlatform 안에서 캠프까지 같이 그린다 — 두 번 그리지 않는다. + if (!WLIslandFlowV2.Active(cfg)) + WLIslandCampDecor.Refresh(cfg, scene); // WL-816y — 세이브 복원(해금 ≥ 1 이면 캠프 재생성) } static Vector3 s_farmCenter; static bool s_haveFarm; + /// 밭 구역 중심(WL-816zc 플로우가 첫 자리로 쓴다). + public static Vector3 FarmCenter { get { return s_farmCenter; } } + public static bool HaveFarmCenter { get { return s_haveFarm; } } + /// /// 🔴 던전 하나 = **발판 한 자리**(WL-816g · PD 정정 2026-09-13 /// 「던전 열기는 한 번만 · 이후 같은 자리에 또 등장해서는 안 된다」). @@ -126,7 +135,7 @@ namespace WL.Island } /// 좌표 위에서 아래로 쏘아 지면 높이를 맞춘다(맞는 것이 없으면 준 값 그대로). - static Vector3 SnapToGround(WLIslandSettings cfg, Vector3 p) + public static Vector3 SnapToGround(WLIslandSettings cfg, Vector3 p) { if (cfg.platformSnapToGround == 0) return p; RaycastHit hit; @@ -139,6 +148,9 @@ namespace WL.Island /// 발판을 지금 해금 상태에 맞게 만들거나 지운다. public static void RefreshPlatform(WLIslandSettings cfg, Scene scene) { + // WL-816zc — 초기 플로우 v2 가 켜져 있으면 발판 순서는 단계 표가 정한다(flowV2Enabled = 0 이면 아래 그대로). + if (WLIslandFlowV2.Active(cfg)) { WLIslandFlowV2.Refresh(cfg, scene); return; } + int unlocked = UnlockedCount(cfg); int usable = UsableDungeonCount(cfg); @@ -188,7 +200,7 @@ namespace WL.Island WLIslandBridge.Log(cfg, LastLog); } - static bool PlayerInRange(WLIslandSettings cfg, Vector3 pos) + public static bool PlayerInRange(WLIslandSettings cfg, Vector3 pos) { var fi = Object.FindFirstObjectByType(); if (fi == null) return false; @@ -205,6 +217,9 @@ namespace WL.Island { if (cfg == null) return; + // WL-816zc — 초기 플로우 v2 가 켜져 있으면 「다음 단계」가 이 자리를 이어받는다. + if (WLIslandFlowV2.Active(cfg)) { WLIslandFlowV2.OnPaid(cfg, platformPos); return; } + int unlocked = UnlockedCount(cfg); int usable = UsableDungeonCount(cfg); if (unlocked >= usable) return; @@ -555,7 +570,9 @@ namespace WL.Island public const string ObjectName = "~WL_GoldPlatform"; - public static WLGoldPlatform Create(WLIslandSettings cfg, Scene scene, Vector3 pos, int price, int nextIndex) + /// 비우면 `platformLabelFormat`(던전 열기) · 채우면 그 글자를 그대로 띄운다(WL-816zc 단계 문구). + public static WLGoldPlatform Create(WLIslandSettings cfg, Scene scene, Vector3 pos, int price, int nextIndex, + string labelOverride = null) { GameObject root = null; @@ -584,7 +601,9 @@ namespace WL.Island // — 모델이 아니라 글자 하나고, `platformShowLabel = 0` 이면 FI 원본 모습 100 % 가 된다. if (cfg.platformShowLabel != 0) WLIslandLabel.Attach(root.transform, new Vector3(0f, cfg.platformLabelHeight, 0f), - string.Format(cfg.platformLabelFormat, price)); + string.IsNullOrEmpty(labelOverride) + ? string.Format(cfg.platformLabelFormat, price) + : labelOverride); return self; } diff --git a/Assets/WL/Island/WLIslandSave.cs b/Assets/WL/Island/WLIslandSave.cs index 074f06599..1064b66b8 100644 --- a/Assets/WL/Island/WLIslandSave.cs +++ b/Assets/WL/Island/WLIslandSave.cs @@ -85,6 +85,9 @@ namespace WL.Island PlayerPrefs.SetString(kTokenKey, cfg.saveResetToken); PlayerPrefs.SetInt(cfg.dungeonUnlockPrefsKey, 0); // 해금한 던전도 0 개로 + // WL-816zc — 초기 플로우 단계·확장 개방도 같이 0 으로(시작마다 초기화 대상) + if (!string.IsNullOrEmpty(cfg.flowStepPrefsKey)) PlayerPrefs.SetInt(cfg.flowStepPrefsKey, 0); + if (!string.IsNullOrEmpty(cfg.expansionUnlockPrefsKey)) PlayerPrefs.SetInt(cfg.expansionUnlockPrefsKey, 0); PlayerPrefs.Save(); WLIslandBridge.ClearPending(); // 보류 보상도 비운다(정적 값까지) } diff --git a/Assets/WL/Island/WLIslandSettings.cs b/Assets/WL/Island/WLIslandSettings.cs index 0f3ee21f5..fd435f0cf 100644 --- a/Assets/WL/Island/WLIslandSettings.cs +++ b/Assets/WL/Island/WLIslandSettings.cs @@ -68,6 +68,58 @@ namespace WL.Island public Vector3 platformPosition = Vector3.zero; } + /// + /// WL-816zc — 「섬 초기 플로우」 한 단계 = 이 표의 한 행. + /// 발판 1개가 이 행의 값(문구·가격·자리)으로 서고, 값을 치르면 이 행의 설치물이 그 자리에 남는다. + /// 코드 수정 0 — 순서·가격·설치물은 전부 여기 값이다(C45). + /// + [Serializable] + public class WLFlowStepDef + { + [Tooltip("0 이면 이 단계는 없는 것으로 친다(발판도 안 선다).")] + public int enabled_ = 1; + + [Tooltip("발판 위에 뜨는 문구(가격은 아래 flowLabelFormat 이 붙인다).")] + public string label = "설치"; + + [Tooltip("이 단계를 여는 값(골드).")] + public int price = 10; + + [Tooltip("발판 자리 = `dungeons[이 번호]` 행의 platformPosition 을 그대로 쓴다(기존 발판 #2·#3·#4 좌표 재사용).\n" + + "-1 이면 밭 구역 중심(= 지금의 첫 발판 자리) · 표 번호는 enabled_ 와 무관한 **줄 번호**다.")] + public int platformFromDungeonRow = -1; + + [Tooltip("위가 -1 이고 밭 자리도 못 찾았을 때 쓸 월드 좌표.")] + public Vector3 platformPosition = Vector3.zero; + + [Tooltip("설치물 종류 — camp(캠프 장식) · prefab(아래 프리팹) · dungeon(던전 입구).")] + public string kind = "prefab"; + + [Tooltip("kind=prefab 일 때 그 자리에 놓을 프리팹 경로.")] + public string prefabPath = ""; + + [Tooltip("같이 놓을 것이 있을 때(예: 좌판 프리팹에 상인이 안 들어 있으면 상인을 덧붙인다). 비우면 안 놓는다.")] + public string extraPrefabPath = ""; + + [Tooltip("덧붙일 것이 이미 설치물 안에 있으면 건너뛸 컴포넌트 이름(비우면 항상 덧붙인다).")] + public string extraSkipIfComponent = ""; + + [Tooltip("덧붙일 것의 설치물 기준 위치 차이.")] + public Vector3 extraOffset = new Vector3(0f, 0f, -1.1f); + + [Tooltip("설치물의 Y 회전(도).")] + public float yaw; + + [Tooltip("설치물 크기 배수(1 = 프리팹 원본).")] + public float scale = 1f; + + [Tooltip("kind=dungeon 일 때 열 던전의 **줄 번호**(`dungeons[이 번호]` · enabled_ 와 무관).")] + public int dungeonRow; + + [Tooltip("1 이면 설치물의 콜라이더를 그대로 둔다. 0 = 전부 끈다(이동 방해 0 · NavMesh 재베이크 없음).")] + public int keepCollider; + } + [CreateAssetMenu(fileName = "WLIslandSettings", menuName = "WL/Island Settings", order = 340)] public sealed class WLIslandSettings : ScriptableObject { @@ -469,6 +521,68 @@ namespace WL.Island [Tooltip("위 대기의 상한(초). 이 시간이 지나면 조건과 무관하게 로딩 칸을 채운다(로딩 화면이 영영 안 닫히는 것 방지).")] public float dungeonLoadingMaxWaitSeconds = 8f; + // ───────────────────────────────── WL-816zc 섬 초기 플로우 v2 + [Header("WL-816zc — 섬 초기 플로우(캠프파이어 → 상인 → 사과나무 → 던전)")] + + [Tooltip("1 = 아래 단계 표 순서대로 발판이 하나씩 선다(PD 2026-09-15).\n" + + "0 = 되돌리기 — 지금까지의 「던전 발판 4개」 방식으로 돌아간다.")] + public int flowV2Enabled = 1; + + [Tooltip("어디까지 치렀는지 세는 PlayerPrefs 키 1개(시작마다 초기화 대상).")] + public string flowStepPrefsKey = "WL_Island_FlowStep"; + + [Tooltip("발판 문구 서식 — {0} = 단계 문구 · {1} = 가격.")] + public string flowLabelFormat = "{0}\n{1} 골드"; + + [Tooltip("한 줄 = 한 단계. 위에서부터 차례로 발판이 선다.")] + public WLFlowStepDef[] flowSteps = new WLFlowStepDef[] + { + new WLFlowStepDef + { + label = "캠프파이어 설치", price = 10, + platformFromDungeonRow = -1, // 밭 구역 중심 + kind = "camp", + }, + new WLFlowStepDef + { + label = "상인 설치", price = 30, + platformFromDungeonRow = 1, // 기존 발판 #2 좌표 + kind = "prefab", + prefabPath = "Assets/FarmingIsland/Prefabs/Buildings/Stall01.prefab", + extraPrefabPath = "Assets/FarmingIsland/Prefabs/Characters/Merchant.prefab", + extraSkipIfComponent = "Merchant", // 좌판 안에 이미 상인이 있으면 안 덧붙인다 + extraOffset = new Vector3(0f, 0f, -1.1f), + yaw = 180f, + }, + new WLFlowStepDef + { + label = "사과나무 설치", price = 60, + platformFromDungeonRow = 2, // 기존 발판 #3 좌표 + kind = "prefab", + prefabPath = "Assets/FarmingIsland/Prefabs/FruitTrees/Tree_Apple.prefab", + }, + new WLFlowStepDef + { + label = "던전 생성", price = 100, + platformFromDungeonRow = 3, // 기존 발판 #4 좌표 + kind = "dungeon", + dungeonRow = 0, // 던전 1 입구(Arch 포탈) + }, + }; + + [Tooltip("1 = 원본 에셋의 섬 확장 가격판(FI Purchaser)을 **첫 던전 승리 전까지** 숨긴다.\n" + + "승리하면 그대로 다시 켜져서 이후는 FI 원본 순서(Island.Constraint)대로 확장된다. 0 = 처음부터 보인다.")] + public int expansionAfterFirstWin = 1; + + [Tooltip("첫 승리 여부를 적는 PlayerPrefs 키(시작마다 초기화 대상).")] + public string expansionUnlockPrefsKey = "WL_Island_ExpansionUnlocked"; + + [Tooltip("확장 가격판 재숨김 주기(초) — FI 가 나중에 만들어 내는 가격판까지 잡는다.")] + public float expansionHideInterval = 0.5f; + + [Tooltip("위 재숨김을 계속하는 시간(초). 0 이면 처음 1회만 숨긴다.")] + public float expansionHideSeconds = 20f; + // ───────────────────────────────── 던전 표 [Header("던전 표 — 한 줄 = 던전 하나")] public WLDungeonDef[] dungeons = new WLDungeonDef[0]; @@ -497,6 +611,13 @@ namespace WL.Island return null; } + /// 던전 표의 **줄 번호**로 그 행을 그대로 준다(enabled_ 와 무관 — 좌표만 빌려 쓸 때 쓴다 · WL-816zc). + public WLDungeonDef DungeonRow(int rawIndex) + { + if (dungeons == null || rawIndex < 0 || rawIndex >= dungeons.Length) return null; + return dungeons[rawIndex]; + } + /// 씬 이름으로 던전 행을 찾는다(같은 씬을 여러 던전이 써도 되므로 맵 ID 우선). public WLDungeonDef FindBySceneName(string sceneName) {