From 3e6b94db45e8c7c8d3359b19e1d99e7983caa733 Mon Sep 17 00:00:00 2001 From: swrring Date: Wed, 16 Sep 2026 23:55:11 +0900 Subject: [PATCH] =?UTF-8?q?[WL]=20=EC=BA=90=EB=A6=AD=ED=84=B0=201.3?= =?UTF-8?q?=EB=B0=B0=C2=B7=EC=BA=A0=ED=94=84=201.2=EB=B0=B0=20=C2=B7=20?= =?UTF-8?q?=EB=8B=A4=EB=A6=AC=20=EC=B9=B8=20=3D=20=EB=AC=BC=20=EC=9C=84=20?= =?UTF-8?q?=EC=9B=90=EB=B3=B8=20=EB=8B=A4=EB=A6=AC(=EC=A7=80=ED=98=95=20?= =?UTF-8?q?=EC=A0=9C=EC=99=B8=C2=B7=EC=9E=AC=EC=A7=88=C2=B7=EA=B0=80?= =?UTF-8?q?=EB=93=9C=20=EC=98=A4=EC=9D=B8=20=EC=88=98=EC=A0=95)=20=C2=B7?= =?UTF-8?q?=20=ED=99=95=EC=9E=A5=20=EA=B0=80=EA=B2=A9=ED=8C=90=20=EC=9D=B4?= =?UTF-8?q?=EC=9B=83=20=EC=B9=B8=20=EA=B0=80=EC=9E=A5=EC=9E=90=EB=A6=AC=20?= =?UTF-8?q?=EC=9C=84=EB=A1=9C=20=C2=B7=20=ED=99=95=EC=9E=A5=20=3D=20?= =?UTF-8?q?=EC=9B=90=EB=B3=B8=20=EB=84=A4=EB=AA=A8=20=ED=8C=90=20=EC=88=A8?= =?UTF-8?q?=EA=B9=80=20+=20=EC=83=88=20=EB=95=85=20=EC=86=9F=EC=95=84?= =?UTF-8?q?=EC=98=A4=EB=A5=B4=EA=B8=B0=201.2=20s=20(#816)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - WLIslandSceneSetup: islandPlayerScale 1.3(표식 1회) · 캠프 campFireScale/campTentScale 2.88 · installPushMargin 0.75 - WLIslandFlowV2/ExpansionGate: PlacePurchasers — FI 가격판을 확장 방향 이웃(열린) 칸 가장자리 안쪽 2.2 m 지면 위로(트리거 동반 · 3 s 훑기·가드에서 1회) · 다리 위면 다리 상판 - WLDemoTerrain: 다리 칸(IsBridge) 발자국 제외 · 열리는 중(scale<1) 칸 즉시 포함 + 원본 렌더러 숨김 · Co_Rise(옛 격자 → 새 격자 보간 · SetHeightsDelayLOD · 인스턴서·거품 미룸) - WLIslandTileFenceStrip: Bridge 메시는 자르지 않음(목록 0번 유지) · WLIslandGateFlow: 울타리 가드가 「_nofence」를 울타리로 오인해 다리를 매 프레임 끄던 것 수정 · MatchTileMaterials 다리 제외 · WLIslandLook: 다리 본체 재질 원본 유지 - 실측: 플레이어 1.30 · 가격판 (0,16)→(0,0.56,9.8) · (−24,8)→다리 위 (−17.8,1.38,8) · 솟아오르기 59~64 프레임/1.2 s · 다리 mesh=Bridge mat=Palette rendOn · 물 아래 −1.66 · 콘솔 에러 0 Co-Authored-By: Claude Fable 5.1 --- .../Resources/WL/WLIslandSettings.asset | 9 +- Assets/WL/Island/WLIslandFlowV2.cs | 75 ++++++++++- Assets/WL/Island/WLIslandGateFlow.cs | 7 +- Assets/WL/Island/WLIslandSceneSetup.cs | 14 ++ Assets/WL/Island/WLIslandSettings.cs | 17 ++- Assets/WL/Island/WLIslandTileFenceStrip.cs | 3 + .../Resources/WL/WLIslandLookSettings.asset | 2 + Assets/WL/Look/Farm/WLDemoTerrain.cs | 124 +++++++++++++++++- Assets/WL/Look/Farm/WLIslandLook.cs | 5 +- Assets/WL/Look/Farm/WLIslandLookSettings.cs | 8 ++ 10 files changed, 252 insertions(+), 12 deletions(-) diff --git a/Assets/WL/Island/Resources/WL/WLIslandSettings.asset b/Assets/WL/Island/Resources/WL/WLIslandSettings.asset index 35a91f946..ae75f04ca 100644 --- a/Assets/WL/Island/Resources/WL/WLIslandSettings.asset +++ b/Assets/WL/Island/Resources/WL/WLIslandSettings.asset @@ -90,8 +90,13 @@ MonoBehaviour: campTentZoneScale: 0.85 campTentStartYaw: 35 campTentFaceYawOffset: 0 - campFireScale: 2.4 - campTentScale: 2.4 + campFireScale: 2.88 + campTentScale: 2.88 + installPushMargin: 0.75 + islandPlayerScale: 1.3 + expansionPadPlace: 1 + expansionPadInset: 2.2 + expansionPadLift: 0.06 campDecorSnapToGround: 0 applyStartCoin: 1 startCoin: 10 diff --git a/Assets/WL/Island/WLIslandFlowV2.cs b/Assets/WL/Island/WLIslandFlowV2.cs index 694d92e97..c9177e482 100644 --- a/Assets/WL/Island/WLIslandFlowV2.cs +++ b/Assets/WL/Island/WLIslandFlowV2.cs @@ -24,6 +24,8 @@ using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; using FIPurchaser = CryingSnow.FarmingIsland.Purchaser; +using FIIsland = CryingSnow.FarmingIsland.Island; +using FIIslandManager = CryingSnow.FarmingIsland.IslandManager; namespace WL.Island { @@ -448,11 +450,78 @@ namespace WL.Island // 우리는 이미 만들어진 가격판의 **렌더러·콜라이더만** 껐다 켠다 = FI 세이브·격자에 흔적 0. // `SetActive(false)` 를 쓰지 않는 이유는 816g 와 같다(꺼진 오브젝트에서 코루틴이 시작되면 에러). // ───────────────────────────────────────────────────────────────────── + /// 섬 플레이어 크기를 이미 키웠다는 표식(WLIslandSceneSetup.ApplyPlayerScale · 1회). + public sealed class WLPlayerScaled : MonoBehaviour { } + + /// FI 확장 가격판을 이웃 칸 가장자리로 옮겼다는 표식(원래 자리 기억). + public sealed class WLPlacedPurchaser : MonoBehaviour { public Vector3 original; } + public static class WLIslandExpansionGate { // ── 진단(프로브·보고가 읽는다 · 실측만) - public static int PurchasersHidden; - public static string LastLog = ""; + public static int PurchasersHidden, PurchasersPlaced; + public static string LastLog = "", LastPlaceLog = ""; + + // ───────────────────────────────────────────────────────────────── + // 🔴 2026-09-16 PD 「확장 가격판이 맵에 가려져 안 보인다 → 위에 · 확장 방향과 가장 가까운 걸어갈 수 있는 자리」 + // FI 는 가격판을 **잠긴 칸 중앙**(이웃 칸 가장자리에서 4 m 바다 쪽 · y −0.5)에 눕힌다 → 우리 해변 경사(모래)가 판의 안쪽 절반을 덮는다. + // → 확장 방향의 이웃(열린) 칸 가장자리에서 `expansionPadInset` 안쪽 지면 위로 옮긴다. FI 의 `activePurchasers` 는 칸 좌표로 기억하므로 + // 트랜스폼만 옮겨도 격자·세이브·구매 로직은 그대로고, 트리거 상자(십자 ±5 m)도 함께 옮겨져 걸어가 밟을 수 있다. + // ───────────────────────────────────────────────────────────────── + public static void PlacePurchasers(WLIslandSettings cfg, Scene scene) + { + if (cfg == null || cfg.expansionPadPlace == 0) return; + var mgr = FIIslandManager.Instance; + float size = mgr != null && mgr.IslandSize > 0.1f ? mgr.IslandSize : 8f, half = size * 0.5f; + var all = Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None); + FIIsland[] islands = null; + for (int i = 0; i < all.Length; i++) + { + var p = all[i]; + if (p == null || p.GetComponent() != null) continue; + if (scene.IsValid() && p.gameObject.scene != scene && p.gameObject.scene != SceneManager.GetActiveScene()) continue; + if (p.GetComponentInParent() != null) continue; // 우리 발판 + if (islands == null) islands = Object.FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None); + + var pos = p.transform.position; // 잠긴 칸 중앙(FI 원래 자리) + FIIsland best = null; float bd = float.MaxValue; + for (int k = 0; k < islands.Length; k++) + { + var isl = islands[k]; + if (isl == null || !isl.IsUnlocked || !isl.gameObject.activeInHierarchy) continue; + var mf = isl.GetComponent(); if (mf == null || mf.sharedMesh == null) continue; // 더미 제외 + float dx = isl.transform.position.x - pos.x, dz = isl.transform.position.z - pos.z; + float d = Mathf.Sqrt(dx * dx + dz * dz); + if (d <= size * 1.05f && d < bd) { bd = d; best = isl; } // 직교 이웃(다리 칸 포함 = 다리 위에 놓인다) + } + if (best == null) continue; // 이웃을 못 찾으면 FI 자리 그대로 + + var c = best.transform.position; + var dir = new Vector3(pos.x - c.x, 0f, pos.z - c.z); + dir = Mathf.Abs(dir.x) >= Mathf.Abs(dir.z) ? new Vector3(Mathf.Sign(dir.x), 0f, 0f) : new Vector3(0f, 0f, Mathf.Sign(dir.z)); + var target = c + dir * (half - Mathf.Clamp(cfg.expansionPadInset, 0.5f, half)); + float gy = PadGroundY(target, c.y); + float plateDrop = 0f; var frame = p.transform.Find("Frame"); if (frame != null) plateDrop = -frame.localPosition.y; // 판 자식들이 루트보다 아래(−0.49) + p.transform.position = new Vector3(target.x, gy + plateDrop + cfg.expansionPadLift, target.z); + var mk = p.gameObject.AddComponent(); mk.original = pos; + PurchasersPlaced++; + LastPlaceLog = "확장 가격판 " + pos.ToString("F0") + " → " + p.transform.position.ToString("F2") + "(이웃 " + c.ToString("F0") + " 가장자리 안쪽 " + cfg.expansionPadInset.ToString("F1") + " m · 지면 " + gy.ToString("F2") + ")"; + WLIslandBridge.Log(cfg, LastPlaceLog); + } + } + + static float PadGroundY(Vector3 p, float fallbackY) + { + var hits = Physics.RaycastAll(new Vector3(p.x, fallbackY + 5f, p.z), Vector3.down, 20f, ~0, QueryTriggerInteraction.Ignore); + float best = float.NaN; + for (int i = 0; i < hits.Length; i++) + { + float y = hits[i].point.y; + if (y > fallbackY + 1.5f) continue; + if (float.IsNaN(best) || y > best) best = y; + } + return float.IsNaN(best) ? fallbackY : best; + } static readonly List s_rends = new List(64); static readonly List s_cols = new List(64); @@ -485,6 +554,7 @@ namespace WL.Island { if (cfg == null || cfg.gateFlowEnabled == 0 || cfg.expansionAfterFirstWin == 0) return; + PlacePurchasers(cfg, scene); // 자리 옮기기(숨김 여부와 무관 · 1회) if (IsUnlocked(cfg)) { Restore(); @@ -539,6 +609,7 @@ namespace WL.Island yield return new WaitForSeconds(wait); t += wait; if (cfg == null || cfg.expansionAfterFirstWin == 0) yield break; + PlacePurchasers(cfg, scene); if (IsUnlocked(cfg)) { Restore(); yield break; } HideNow(cfg, scene); } diff --git a/Assets/WL/Island/WLIslandGateFlow.cs b/Assets/WL/Island/WLIslandGateFlow.cs index 9443afc59..af15cfc71 100644 --- a/Assets/WL/Island/WLIslandGateFlow.cs +++ b/Assets/WL/Island/WLIslandGateFlow.cs @@ -512,7 +512,9 @@ namespace WL.Island if (!hit) { var mf = r.GetComponent(); - if (mf != null && mf.sharedMesh != null && mf.sharedMesh.name.IndexOf("fence", System.StringComparison.OrdinalIgnoreCase) >= 0) hit = true; + // 🔴 2026-09-16 실측: 난간을 잘라낸 복제본 이름이 「…_nofence」라 「fence」에 걸려 **다리(Bridge_nofence)** 가 매 프레임 꺼졌다 → nofence 는 제외 + if (mf != null && mf.sharedMesh != null && mf.sharedMesh.name.IndexOf("fence", System.StringComparison.OrdinalIgnoreCase) >= 0 + && mf.sharedMesh.name.IndexOf("nofence", System.StringComparison.OrdinalIgnoreCase) < 0) hit = true; } if (!hit) continue; r.enabled = false; FenceGuardHidden++; @@ -551,6 +553,7 @@ namespace WL.Island WL.Combat.Stage.StageBarrierVisual.HideImmediate(); // 늦게 페이드 중이던 전투 장벽 그림까지(슬롯 없으면 비용 0) WLIslandTileFenceStrip.Apply(cfg, scene); // 확장·재구성으로 타일 메시가 바뀌면 다시 잘라낸다(멱등) if (cfg.hideFarms != 0) { Vector3 _c; HideFarms(cfg, scene, out _c); } // 늦게 생긴 밭의 울타리·소품도 + WLIslandExpansionGate.PlacePurchasers(cfg, scene); // 2026-09-16 — 확장 뒤 새로 생긴 FI 가격판도 이웃 칸 가장자리로(표식 있으면 비용 0) } } @@ -598,7 +601,7 @@ namespace WL.Island static void MatchTileMaterials(WLIslandSettings cfg, FIIsland isl) { - if (cfg.matchNewTileMaterials == 0 || isl == null) return; + if (cfg.matchNewTileMaterials == 0 || isl == null || isl.IsBridge) return; // 다리 타일은 원본 재질 유지(2026-09-16) var mine = isl.GetComponentsInChildren(true); if (mine.Length == 0) return; diff --git a/Assets/WL/Island/WLIslandSceneSetup.cs b/Assets/WL/Island/WLIslandSceneSetup.cs index 61018061a..164184f79 100644 --- a/Assets/WL/Island/WLIslandSceneSetup.cs +++ b/Assets/WL/Island/WLIslandSceneSetup.cs @@ -79,6 +79,7 @@ namespace WL.Island 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); @@ -151,6 +152,19 @@ namespace WL.Island } } + // ── 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(); + if (fiPlayer == null || fiPlayer.GetComponent() != null) return; + fiPlayer.transform.localScale = fiPlayer.transform.localScale * cfg.islandPlayerScale; + fiPlayer.gameObject.AddComponent(); + PlayerScaled++; + WLIslandBridge.Log(cfg, "섬 플레이어 크기 ×" + cfg.islandPlayerScale.ToString("F2") + " → " + fiPlayer.transform.localScale.ToString("F2")); + } + static readonly List s_disabledCams = new List(4); static readonly List s_disabledListeners = new List(4); diff --git a/Assets/WL/Island/WLIslandSettings.cs b/Assets/WL/Island/WLIslandSettings.cs index 8f8858a1a..6aa4ab557 100644 --- a/Assets/WL/Island/WLIslandSettings.cs +++ b/Assets/WL/Island/WLIslandSettings.cs @@ -554,8 +554,21 @@ namespace WL.Island [Tooltip("1 = 설치물(좌판·나무 등)이 생길 때 그 범위 안에 플레이어가 있으면 범위 밖으로 밀어낸다(2026-09-16 PD 「가판대에 캐릭터가 끼어버림」). 0 = 안 밀어냄.")] public int installPushPlayer = 1; - [Tooltip("설치물 범위 바깥으로 얼마나 더(m) 밀어낼지(플레이어 반지름 0.4 + 여유).")] - public float installPushMargin = 0.6f; + [Tooltip("설치물 범위 바깥으로 얼마나 더(m) 밀어낼지(플레이어 반지름 0.4×1.3 + 여유).")] + public float installPushMargin = 0.75f; + + [Tooltip("섬에서 플레이어(FI PlayerController) 크기 배수 — 2026-09-16 PD 「캐릭터 30 % 키워」. 1 = 원본. 섬 준비 때 1회(표식 컴포넌트로 중복 방지).")] + public float islandPlayerScale = 1.3f; + + [Header("확장 가격판 자리 (2026-09-16 PD 「맵에 가려짐 → 위에 · 확장 방향과 가장 가까운 걸어갈 수 있는 자리」)")] + [Tooltip("1 = FI 확장 가격판(잠긴 칸 중앙 · 물 위)을 확장 방향 이웃(열린) 칸 가장자리 안쪽 지면 위로 옮긴다(FI 격자·세이브 무변경 · 트리거도 함께). 0 = 원본 자리.")] + public int expansionPadPlace = 1; + + [Tooltip("이웃 칸 가장자리에서 안쪽으로 이만큼(m) 들어온 자리에 놓는다(해변 경사·둥근 모서리를 피해 잔디 위).")] + public float expansionPadInset = 2.2f; + + [Tooltip("판을 지면보다 이만큼(m) 띄운다(z-fighting 방지).")] + public float expansionPadLift = 0.06f; [Tooltip("한 줄 = 한 단계. 위에서부터 차례로 발판이 선다.")] public WLFlowStepDef[] flowSteps = new WLFlowStepDef[] diff --git a/Assets/WL/Island/WLIslandTileFenceStrip.cs b/Assets/WL/Island/WLIslandTileFenceStrip.cs index 3fd449069..3b9b6baee 100644 --- a/Assets/WL/Island/WLIslandTileFenceStrip.cs +++ b/Assets/WL/Island/WLIslandTileFenceStrip.cs @@ -39,6 +39,8 @@ namespace WL.Island if (mf == null || mf.sharedMesh == null) continue; var src = mf.sharedMesh; if (src.name.EndsWith("_nofence")) continue; + // 🔴 2026-09-16 PD 「다리 칸은 물 위 다리로」 — 다리 메시(Bridge · 상판 y≈0.8)는 자르지 않는다(자르면 상판이 사라지고 난간도 다리 것이라 남긴다) + if (isl.IsBridge || src.name.StartsWith("Bridge", System.StringComparison.OrdinalIgnoreCase)) continue; Mesh dst; if (!s_cache.TryGetValue(src, out dst)) { dst = Strip(src, cfg.fenceStripMinY); s_cache[src] = dst; } @@ -72,6 +74,7 @@ namespace WL.Island { var src = list[i]; if (src == null || src.name.EndsWith("_nofence")) continue; + if (src.name.StartsWith("Bridge", System.StringComparison.OrdinalIgnoreCase)) continue; // 다리 메시(목록 0번)는 원본 그대로 Mesh dst; if (!s_cache.TryGetValue(src, out dst)) { dst = Strip(src, cfg.fenceStripMinY); s_cache[src] = dst; } if (dst != null && dst != src) { list[i] = dst; SourceReplaced++; } diff --git a/Assets/WL/Look/Farm/Resources/WL/WLIslandLookSettings.asset b/Assets/WL/Look/Farm/Resources/WL/WLIslandLookSettings.asset index cf3b9332e..f64066e9f 100644 --- a/Assets/WL/Look/Farm/Resources/WL/WLIslandLookSettings.asset +++ b/Assets/WL/Look/Farm/Resources/WL/WLIslandLookSettings.asset @@ -404,6 +404,8 @@ MonoBehaviour: - Level01 - WL_FarmLook demoTerrainEnabled: 1 + expandHideOriginal: 1 + expandRiseSeconds: 1.2 demoCloudStrength: 0.5 demoCloudDensity: 0.025 terrainHeightRes: 129 diff --git a/Assets/WL/Look/Farm/WLDemoTerrain.cs b/Assets/WL/Look/Farm/WLDemoTerrain.cs index f0c514133..2fdee8004 100644 --- a/Assets/WL/Look/Farm/WLDemoTerrain.cs +++ b/Assets/WL/Look/Farm/WLDemoTerrain.cs @@ -33,6 +33,7 @@ // (Settings null · Mesh/Material null · Scale ≤ 0)은 **켜기 전에** 걸러 낸다. // 🔴 asmdef 금지(Assembly-CSharp 단일) · `Environment.Instancing` 은 `global::` 로 고정(WL 네임스페이스와 이름 충돌 방지). // ───────────────────────────────────────────────────────────────────────────── +using System.Collections; using System.Collections.Generic; using UnityEngine; using FIIsland = CryingSnow.FarmingIsland.Island; @@ -173,13 +174,13 @@ namespace WL.Look.Farm } sw.Restart(); - string instLog = SetupInstancer(); + string instLog = _deferInstancer ? "인스턴서 미룸(솟아오르기 뒤)" : SetupInstancer(); _child.SetActive(true); // → 인스턴서 OnEnable(SetAlphamaps 뒤) · Terrain 표시 LastInstancerMs = (float)sw.Elapsed.TotalMilliseconds; _lastTileSig = tileSig; _lastValSig = valSig; _dirty = false; Rebuilds++; - if (Baked != null) Baked(); + if (!_deferInstancer && Baked != null) Baked(); LastLog = "데모 터레인 — 타일 " + _tiles.Count + " · 테두리 변 " + LastEdges + " · 링 " + LastRingPoints + " · 범위 " + LastSizeX.ToString("F0") + "×" + LastSizeZ.ToString("F0") + " m(원점 " @@ -371,8 +372,15 @@ namespace WL.Look.Farm for (int i = 0; i < islands.Length; i++) { var isl = islands[i]; - if (isl == null || !isl.IsUnlocked || !isl.gameObject.activeInHierarchy) continue; + if (isl == null || !isl.gameObject.activeInHierarchy) continue; if (scene.IsValid() && isl.gameObject.scene != scene) continue; + // 🔴 2026-09-16 PD 「다리 칸은 물과 섬이 이어지는 모습이어야(낚시)」 — 다리 칸은 지형 발자국에서 뺀다 → 그 칸은 바다·해변으로 남고 원본 다리 메시가 그대로 보인다. + if (isl.IsBridge) continue; + var mfT = isl.GetComponent(); + if (mfT == null || mfT.sharedMesh == null) continue; // 더미(발판 타깃 Island) 제외 + // 🔴 열리는 중(FI Activate: 켜지고 scale 0 → 1 · 1 s)인 칸도 지금부터 발자국에 넣는다 → 원본 네모 판이 튀어오르기 전에 우리 지형이 그 칸을 덮는다(§0-f). + bool activating = !isl.IsUnlocked && isl.transform.localScale.x < 0.999f; + if (!isl.IsUnlocked && !activating) continue; var p = isl.transform.position; float top = p.y; var r = isl.GetComponent(); // 섬 본체 렌더러(FIIsland 와 같은 오브젝트 · SwapMaterials 와 같은 판별) @@ -719,6 +727,8 @@ namespace WL.Look.Farm } // 6) 내용 비교 — 같은 배열이면 지형에 다시 쓰지 않는다(재대입 = 패치 재생성 = 화면 조각 빔) + if (_riseCo != null) _riseCancel = true; // 솟아오르는 중에 새로 구우면 그 연출은 즉시 끝낸다(최종 높이로) + _lastOx = ox; _lastOz = oz; _lastW = w; _lastL = l; _lastBaseY = baseY; _lastSpan = span; bool heightsSame = _lastHeights != null && SameArray(_lastHeights, heights); bool alphaSame = _lastAlpha != null && SameArray(_lastAlpha, alpha); if (!heightsSame) _data.SetHeights(0, 0, heights); @@ -731,11 +741,119 @@ namespace WL.Look.Farm float[,] _lastHeights; float[,,] _lastAlpha; + float _lastOx, _lastOz, _lastW, _lastL, _lastBaseY, _lastSpan; // 마지막으로 구운 격자(솟아오르기 보간이 옛 격자를 읽는다) Bounds _lastZone; bool _lastZoneValid; public static bool LastBakeUnchanged; public static int SkippedRebuilds; public static string LastInputs = ""; // 진단 — 재굽기 원인 추적(평평 자리 · 밭 구역 · 서명) + // ───────────────────────────────────────────────────────────────── + // 🔴 §0-f 섬 확장 연출 (2026-09-16 PD 「원본 네모 판 없이 자연스럽게 확장」) + // FI `Island.Activate()` = 켜기 + scale 0 → 1(점프·바운스 1 s) → 그 뒤 IsUnlocked·OnActivated. 원본 네모 판은 이 1 s 동안 튀어오른다. + // → 매 프레임 「켜졌는데 아직 잠김·scale<1」인 칸을 잡아 그 프레임에 렌더러를 숨기고, 우리 지형을 그 칸까지 구운 뒤(최종 높이) + // 새 칸 둘레만 이전 높이 → 새 높이로 `expandRiseSeconds` 동안 보간(SetHeightsDelayLOD · 부분 사각형). 풀 인스턴서·거품은 다 솟은 뒤. + // FI 의 OnActivated(1 s 뒤) → 우리 훅의 재굽기는 내용이 같아 생략된다(§6 비교). + // ───────────────────────────────────────────────────────────────── + public static int Rises, RiseFrames; + public static string LastRiseLog = ""; + readonly List _rising = new List(4); + FIIsland[] _watch; float _watchNext; + bool _deferInstancer, _riseCancel; + Coroutine _riseCo; + + void Update() + { + if (cfg == null || cfg.enabled_ == 0 || cfg.demoTerrainEnabled == 0 || cfg.expandHideOriginal == 0) return; + if (_watch == null || Time.unscaledTime >= _watchNext) + { + _watch = Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None); + _watchNext = Time.unscaledTime + 5f; + } + for (int i = 0; i < _watch.Length; i++) + { + var isl = _watch[i]; + if (isl == null || isl.IsUnlocked || isl.IsBridge || !isl.gameObject.activeInHierarchy) continue; + if (isl.transform.localScale.x >= 0.999f) continue; // 잠긴 채 아직 비활성화 전(로드 직후)은 scale 1 → 해당 없음 + if (_rising.Contains(isl)) continue; + var mf = isl.GetComponent(); if (mf == null || mf.sharedMesh == null) continue; // 더미 제외 + if (gameObject.scene.IsValid() && isl.gameObject.scene != gameObject.scene) continue; + StartRise(isl); + } + for (int i = _rising.Count - 1; i >= 0; i--) if (_rising[i] == null || _rising[i].IsUnlocked) _rising.RemoveAt(i); + } + + void StartRise(FIIsland isl) + { + _rising.Add(isl); + var r = isl.GetComponent(); + if (r != null && r.enabled) { r.enabled = false; if (!_hiddenTops.Contains(r)) _hiddenTops.Add(r); } // 네모 판은 첫 프레임부터 안 보인다 + + var oldH = _lastHeights; float oOx = _lastOx, oOz = _lastOz, oW = _lastW, oL = _lastL, oBase = _lastBaseY, oSpan = _lastSpan; + bool animate = cfg.expandRiseSeconds > 0f && oldH != null && _data != null; + _deferInstancer = animate; + _dirty = true; + Rebuild(); // 새 칸 포함 · 최종 높이·스플랫·콜라이더 + if (!animate || LastBakeUnchanged || _lastHeights == null) { FinishRise(); return; } + if (_riseCo != null) { _riseCancel = true; StopCoroutine(_riseCo); } + _riseCancel = false; + _riseCo = StartCoroutine(Co_Rise(isl.transform.position, oldH, oOx, oOz, oW, oL, oBase, oSpan, cfg.expandRiseSeconds)); + Rises++; + } + + void FinishRise() + { + _deferInstancer = false; + _riseCo = null; + if (_child == null || _data == null) return; + SetupInstancer(); + _child.SetActive(true); + if (Baked != null) Baked(); + } + + IEnumerator Co_Rise(Vector3 tilePos, float[,] oldH, float oOx, float oOz, float oW, float oL, float oBase, float oSpan, float seconds) + { + int hRes = _dataHeightRes; + float stepX = _lastW / (hRes - 1), stepZ = _lastL / (hRes - 1); + float ext = TileSize() * 0.5f + Mathf.Max(0.01f, cfg.terrainSlope) * (1f + Mathf.Clamp(cfg.terrainSlopeVar, 0f, 0.8f)) + 3f; + int i0 = Mathf.Clamp(Mathf.FloorToInt((tilePos.x - ext - _lastOx) / stepX), 0, hRes - 1), i1 = Mathf.Clamp(Mathf.CeilToInt((tilePos.x + ext - _lastOx) / stepX), 0, hRes - 1); + int j0 = Mathf.Clamp(Mathf.FloorToInt((tilePos.z - ext - _lastOz) / stepZ), 0, hRes - 1), j1 = Mathf.Clamp(Mathf.CeilToInt((tilePos.z + ext - _lastOz) / stepZ), 0, hRes - 1); + int nx = i1 - i0 + 1, nz = j1 - j0 + 1; + if (nx < 2 || nz < 2) { FinishRise(); yield break; } + var final = new float[nz, nx]; var start = new float[nz, nx]; var cur = new float[nz, nx]; + int oRes = oldH.GetLength(0); float oStepX = oW / (oRes - 1), oStepZ = oL / (oRes - 1); + for (int j = 0; j < nz; j++) + for (int i = 0; i < nx; i++) + { + final[j, i] = _lastHeights[j0 + j, i0 + i]; + float x = _lastOx + (i0 + i) * stepX, z = _lastOz + (j0 + j) * stepZ; + float gx = (x - oOx) / oStepX, gz = (z - oOz) / oStepZ; + float oldNorm = 0f; // 옛 격자 밖 = 옛 바다 바닥 + if (gx >= 0f && gz >= 0f && gx <= oRes - 1 && gz <= oRes - 1) + { + int gi = Mathf.Min((int)gx, oRes - 2), gj = Mathf.Min((int)gz, oRes - 2); float tx = gx - gi, tz = gz - gj; + oldNorm = Mathf.Lerp(Mathf.Lerp(oldH[gj, gi], oldH[gj, gi + 1], tx), Mathf.Lerp(oldH[gj + 1, gi], oldH[gj + 1, gi + 1], tx), tz); + } + float oldY = oBase + oldNorm * oSpan; + start[j, i] = Mathf.Clamp01((oldY - _lastBaseY) / _lastSpan); + } + float t = 0f; int frames = 0; + while (t < seconds && !_riseCancel) + { + float k = SmoothStep01(t / seconds); + for (int j = 0; j < nz; j++) for (int i = 0; i < nx; i++) cur[j, i] = Mathf.Lerp(start[j, i], final[j, i], k); + _data.SetHeightsDelayLOD(i0, j0, cur); + frames++; + yield return null; + t += Time.deltaTime; + } + _data.SetHeightsDelayLOD(i0, j0, final); + _data.SyncHeightmap(); + RiseFrames = frames; + LastRiseLog = "솟아오르기 — 칸 " + tilePos.ToString("F0") + " · 부분 " + nx + "×" + nz + " · " + frames + " 프레임 / " + seconds.ToString("F1") + " s" + (_riseCancel ? "(중단)" : ""); + cfg.Log(LastRiseLog); + FinishRise(); + } + static bool SameArray(float[,] a, float[,] b) { if (a.GetLength(0) != b.GetLength(0) || a.GetLength(1) != b.GetLength(1)) return false; diff --git a/Assets/WL/Look/Farm/WLIslandLook.cs b/Assets/WL/Look/Farm/WLIslandLook.cs index 1a7146bd3..192f0968f 100644 --- a/Assets/WL/Look/Farm/WLIslandLook.cs +++ b/Assets/WL/Look/Farm/WLIslandLook.cs @@ -465,7 +465,10 @@ namespace WL.Look.Farm // Toon 셰이더에는 그 프로퍼티가 없어 갈아끼우면 밭 색이 죽는다 → 원본 유지. if (cfg.skipSoilRenderers != 0 && r.GetComponent() != null) { s_done.Add(r); continue; } - bool isIslandBody = r.GetComponent() != null; + var body = r.GetComponent(); + // 🔴 2026-09-16 PD 「다리 칸은 에셋 기준 물 위 다리로」 — 다리 타일 본체는 원본 아틀라스 재질(나무 무늬) 그대로 둔다(잔디 재질로 갈아끼우면 초록 다리가 된다) + if (body != null && body.IsBridge) { s_done.Add(r); continue; } + bool isIslandBody = body != null; var mats = r.sharedMaterials; bool changed = false; diff --git a/Assets/WL/Look/Farm/WLIslandLookSettings.cs b/Assets/WL/Look/Farm/WLIslandLookSettings.cs index accc7affd..67ebeb222 100644 --- a/Assets/WL/Look/Farm/WLIslandLookSettings.cs +++ b/Assets/WL/Look/Farm/WLIslandLookSettings.cs @@ -667,6 +667,14 @@ namespace WL.Look.Farm [Tooltip("확장 애니메이션(DOTween 1초)이 끝나기를 기다리는 초.")] public float rebuildDelaySeconds = 1.4f; + // ── 2026-09-16 PD 「새 맵을 확장할 때 원본 네모 판이 안 보이고 자연스럽게 확장되게」 + [Header("§0-f — 섬 확장 연출 (데모 터레인 모드)")] + [Tooltip("1 = 잠긴 칸이 열리는 순간(FI 점프·스케일 애니 첫 프레임)부터 그 칸의 원본 타일 렌더러를 숨기고 우리 지형을 그 칸까지 굽는다(다리 칸 제외). 0 = 원본 애니(네모 판이 튀어오름) 그대로.")] + public int expandHideOriginal = 1; + + [Tooltip("새 땅이 바다에서 솟아오르는 시간(초) — 새 칸 둘레 높이맵을 이전 높이 → 새 높이로 보간(SetHeightsDelayLOD · 부분 사각형만). 풀·거품은 다 솟은 뒤 깐다. 0 = 즉시.")] + public float expandRiseSeconds = 1.2f; + // ───────────────────────────────── §2 구름 경계 배치 (816o) [Header("§2 — 구름 그림자 경계에만 풀을 심는다 (816o · PD 「모바일 부하 없게」)")] [Tooltip("🔴 되돌리기 스위치 — 0 이면 816n 상태 100 % 복귀(섬 전체에 균일 격자). " +