// ───────────────────────────────────────────────────────────────────────────── // WLIslandCampDecor.cs — WL-816y 「캠프파이어 설치」 // // PD 원문(2026-09-15) 「게임 시작 후 처음 골드로 지역을 개방하는 위치에 "캠프파이어 설치"로 명명하고, // "Fireplace_01" 에셋의 캠프파이어를 중앙에 배치하고, 가장자리에 "Tent_01" 텐트를 배치해 // 자연스럽게 캠프파이어를 차린 것처럼 꾸며줘」 // // ■ 자리 = 첫 발판이 서 있던 자리(= 「열림 구역」 정중앙 · WLIslandGateFlow.PlatformPosFor(cfg, 0)). // 값을 치르면 발판은 영영 사라지고(816g) 그 자리에 이 캠프가 남는다. // ■ 세이브 복원 = 섬이 준비될 때 해금 수(WL_Island_DungeonsUnlocked) ≥ 1 이면 다시 놓는다. // ■ 장식은 **콜라이더를 전부 끈다** — 이동 방해 0 · NavMesh 재베이크 없음(816g 더미와 같은 원칙). // ■ 값(프리팹 경로·개수·거리·회전·스케일·바닥 스냅)은 전부 `WLIslandSettings` 에 있다(C45). // 🔴 원본 에셋(Assets/FantasyForest/**)은 한 줄도 고치지 않는다 — 인스턴스만 만든다. // 🔴 Debug.LogError / Debug.LogException 을 쓰지 않는다. // ───────────────────────────────────────────────────────────────────────────── using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; namespace WL.Island { public static class WLIslandCampDecor { const string RootName = "~WL_CampDecor"; static readonly List s_made = new List(4); /// 지금 놓여 있는 캠프 장식 루트(들) — 확장 가격판이 텐트·모닥불을 피해 앉을 때 읽는다(2026-09-17). public static IReadOnlyList Made { get { return s_made; } } /// 진단 — 마지막으로 놓은 장식 수·중심. public static int DecorSpawned; public static Vector3 LastCenter; public static string LastLog = ""; /// 지금 해금 상태에 맞게 캠프 장식을 놓거나 지운다(섬 준비 직후 · 지불 직후 둘 다 여기로). public static void Refresh(WLIslandSettings cfg, Scene scene) { Clear(); if (cfg == null || cfg.campDecorEnabled == 0) return; // 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); root.transform.position = center; if (scene.IsValid() && scene.isLoaded) SceneManager.MoveGameObjectToScene(root, scene); s_made.Add(root); int n = 0; // ① 모닥불 = 구역 중앙 var fire = Spawn(cfg, cfg.campFirePrefabPath, root.transform, center, 0f, cfg.campFireScale, "Fire"); if (fire != null) { n++; WLIslandFlowV2.PushPlayerOut(cfg, fire); } // 2026-09-16 PD 「생성물에 끼임」 — 발판 자리(=모닥불 자리)의 플레이어를 밀어낸다 // ② 텐트 = 구역 가장자리 · 불 쪽을 향하게 float r = TentRadius(cfg); int tents = Mathf.Max(0, cfg.campTentCount); for (int i = 0; i < tents; i++) { float yaw = cfg.campTentStartYaw + (tents > 0 ? 360f / tents * i : 0f); float rad = yaw * Mathf.Deg2Rad; Vector3 p = center + new Vector3(Mathf.Sin(rad) * r, 0f, Mathf.Cos(rad) * r); // 텐트가 중앙(불)을 바라보도록 = 바깥 방향의 반대 float faceYaw = yaw + 180f + cfg.campTentFaceYawOffset; var tent = Spawn(cfg, cfg.campTentPrefabPath, root.transform, p, faceYaw, cfg.campTentScale, "Tent_" + i); if (tent != null) { n++; WLIslandFlowV2.PushPlayerOut(cfg, tent); } } DecorSpawned = n; LastLog = "캠프 장식 " + n + "개 — 중심 " + center.ToString("F2") + " · 텐트 거리 " + r.ToString("F2"); WLIslandBridge.Log(cfg, LastLog); } /// 텐트 거리 — 「열림 구역」이 있으면 그 짧은 쪽 반지름(= 가장자리), 없으면 설정값. static float TentRadius(WLIslandSettings cfg) { float r = cfg.campTentRadius; if (cfg.campTentUseZone != 0 && WLIslandGateFlow.ZoneBoundsValid) { var e = WLIslandGateFlow.ZoneBounds.extents; float zr = Mathf.Min(e.x, e.z) * cfg.campTentZoneScale; if (zr > 0.01f) r = zr; } return Mathf.Max(0.01f, r); } static GameObject Spawn(WLIslandSettings cfg, string path, Transform parent, Vector3 pos, float yaw, float scale, string name) { if (string.IsNullOrEmpty(path)) return null; var prefab = WLIslandAssets.LoadPrefab(path); if (prefab == null) { WLIslandBridge.Log(cfg, "캠프 장식 프리팹 없음 — " + path); return null; } var go = Object.Instantiate(prefab); go.name = "~WL_Camp_" + name; go.transform.SetParent(parent, false); go.transform.position = Snap(cfg, pos); go.transform.rotation = Quaternion.Euler(0f, yaw, 0f); float s = scale > 0.001f ? scale : 1f; go.transform.localScale = prefab.transform.localScale * s; var cols = go.GetComponentsInChildren(true); for (int i = 0; i < cols.Length; i++) if (cols[i] != null) cols[i].enabled = false; return go; } static Vector3 Snap(WLIslandSettings cfg, Vector3 p) { if (cfg.campDecorSnapToGround == 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 Clear() { for (int i = 0; i < s_made.Count; i++) if (s_made[i] != null) Object.Destroy(s_made[i]); s_made.Clear(); DecorSpawned = 0; } } }