diff --git a/Assets/WL/Island/WLIslandFlowV2.cs b/Assets/WL/Island/WLIslandFlowV2.cs index 1a1068ecd..d24867920 100644 --- a/Assets/WL/Island/WLIslandFlowV2.cs +++ b/Assets/WL/Island/WLIslandFlowV2.cs @@ -235,6 +235,8 @@ namespace WL.Island { var d = cfg.DungeonRow(s.dungeonRow); if (d != null) WLIslandBridge.SpawnOneGate(cfg, d, scene); // 이미 있으면 안에서 건너뛴다 + // 2026-09-16 PD 「던전 개방 이후에는 기존 에셋과 동일한 순서로 확장 노드」 — 던전 노드가 열리면 원본 확장 가격판을 개방한다. + if (d != null && cfg.expansionUnlockOnDungeonOpen != 0) WLIslandExpansionGate.Unlock(cfg); return d != null; } @@ -301,10 +303,22 @@ namespace WL.Island 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; + // 2026-09-16 PD 「상인이 공중에 떠 있다」 실측: 재훑기마다 다시 스냅하는데, 광선이 **방금 놓은 설치물 자신의 콜라이더**(좌판 3 m 상자) 위를 + // 맞혀 한 번 훑을 때마다 3 m 씩 떠올랐다(y 3.00 · 콜라이더 위 6.00 실측). 설치물(`~WL_FlowInstalls` 밑)은 건너뛰고, + // 요청 높이보다 `platformSnapMaxRise` 이상 위로 솟는 맞힘(다른 소품 위)도 무시한다 — 설치물은 타일 윗면에 놓는 것이 목적이다. + var hits = Physics.RaycastAll(new Vector3(p.x, p.y + 30f, p.z), Vector3.down, 60f, ~0, QueryTriggerInteraction.Ignore); + float best = float.NaN; + for (int i = 0; i < hits.Length; i++) + { + var c = hits[i].collider; if (c == null) continue; + var t = c.transform; bool ours = false; + while (t != null) { if (t.name == RootName) { ours = true; break; } t = t.parent; } + if (ours) continue; + float y = hits[i].point.y; + if (y > p.y + Mathf.Max(0f, cfg.platformSnapMaxRise)) continue; + if (float.IsNaN(best) || y > best) best = y; + } + return float.IsNaN(best) ? p : new Vector3(p.x, best, p.z); } public static void ClearInstalls() diff --git a/Assets/WL/Island/WLIslandSettings.cs b/Assets/WL/Island/WLIslandSettings.cs index bddfa6a2b..8e2bef870 100644 --- a/Assets/WL/Island/WLIslandSettings.cs +++ b/Assets/WL/Island/WLIslandSettings.cs @@ -561,6 +561,7 @@ namespace WL.Island label = "상인 설치", price = 30, platformFromDungeonRow = 1, // 기존 발판 #2 좌표 kind = "prefab", + scale = 0.5f, // 2026-09-16 PD 「상인 프랍 크기 50 %」 prefabPath = "Assets/FarmingIsland/Prefabs/Buildings/Stall01.prefab", extraPrefabPath = "Assets/FarmingIsland/Prefabs/Characters/Merchant.prefab", extraSkipIfComponent = "Merchant", // 좌판 안에 이미 상인이 있으면 안 덧붙인다 @@ -589,6 +590,14 @@ namespace WL.Island "승리하면 그대로 다시 켜져서 이후는 FI 원본 순서(Island.Constraint)대로 확장된다. 0 = 처음부터 보인다.")] public int expansionAfterFirstWin = 1; + [Tooltip("1 = 초기 플로우의 **던전 노드가 열리는 순간**(4단계 설치) 원본 확장 가격판을 개방한다(2026-09-16 PD 「던전 개방 이후에는 기존 에셋 순서대로 확장 노드」). " + + "0 = 첫 전투 승리 때만(이전 방식). 둘 다 켜져 있으면 먼저 오는 쪽.")] + public int expansionUnlockOnDungeonOpen = 1; + + [Tooltip("설치물·발판 지면 스냅에서 허용하는 최대 상승(m). 요청 높이보다 이만큼 이상 위에서 맞은 콜라이더(다른 소품·설치물)는 무시한다 — " + + "2026-09-16 실측: 재훑기 스냅이 좌판 자신의 콜라이더 위(3 m)를 맞혀 상인이 공중에 떴다.")] + public float platformSnapMaxRise = 0.6f; + [Tooltip("첫 승리 여부를 적는 PlayerPrefs 키(시작마다 초기화 대상).")] public string expansionUnlockPrefsKey = "WL_Island_ExpansionUnlocked"; diff --git a/Assets/WL/Look/Farm/Resources/WL/WLIslandLookSettings.asset b/Assets/WL/Look/Farm/Resources/WL/WLIslandLookSettings.asset index ffdbc69fd..45052219b 100644 --- a/Assets/WL/Look/Farm/Resources/WL/WLIslandLookSettings.asset +++ b/Assets/WL/Look/Farm/Resources/WL/WLIslandLookSettings.asset @@ -412,12 +412,12 @@ MonoBehaviour: demoGrass: scatterIndex: 0 probability: 100 - scale: 0.35 + scale: 0.22 normalOffset: 0.04 demoFlower: scatterIndex: 1 probability: 1 - scale: 0.35 + scale: 0.26 normalOffset: 0.04 demoGravel: scatterIndex: 2 diff --git a/Assets/WL/Look/Farm/WLDemoTerrain.cs b/Assets/WL/Look/Farm/WLDemoTerrain.cs index 53844d72d..14ea46e87 100644 --- a/Assets/WL/Look/Farm/WLDemoTerrain.cs +++ b/Assets/WL/Look/Farm/WLDemoTerrain.cs @@ -368,9 +368,32 @@ namespace WL.Look.Farm float round = Mathf.Max(0f, cfg.skirtGrassRound); float cellA = Mathf.Max(w / aRes, l / aRes); float far = slope + blend + Mathf.Max(0f, cfg.skirtWaveAmp) + cellA * 2f; // 이보다 먼 바깥은 높이·스플랫 모두 상수 - GatherRing(size, half, minX, minZ, maxX, maxZ, Mathf.Min(w / aRes, l / aRes), round); + // 🔴 2026-09-16 Lead 실측: 링 0.125 m 간격(632점) × 스플랫 65k 텍셀 = 굽기 851 ms → 상인 설치 때 화면이 멎어 「지형이 사라졌다 보임」. + // 링은 0.5 m 간격(≈1/4)으로, 경계 거리 s 는 높이맵 격자(0.25 m)에서 한 번만 계산해 스플랫은 겹선형으로 읽는다(blend 1 m 에 충분). + GatherRing(size, half, minX, minZ, maxX, maxZ, Mathf.Max(0.5f, Mathf.Min(w / aRes, l / aRes)), round); - // 4) 스플랫 — 층0 잔디 = 둥글린 발자국 안(경계 blend 에 걸쳐 1→0) · 층1 모래 = 나머지 + 밭 구역(WLSoftDirt 와 같은 둥근+노이즈 경계) + // 4) 높이맵 + 경계 거리 격자 — 발자국 안 윗면 → 경계에서 바깥 slope 동안 smoothstep 으로 바닥 → 그 밖 평평 + // terrainRoundHeight 1 = 둥글린·굽이친 경계에서 내려간다(발주서) · 0 = 타일 네모 가장자리에서 내려간다(콜라이더와 일치 · 둥글림은 스플랫만) + bool roundHeight = cfg.terrainRoundHeight != 0; + var heights = new float[hRes, hRes]; + var sGrid = new float[hRes * hRes]; // 둥글림+굽이 경계 거리(스플랫이 읽는다) + float stepX = w / (hRes - 1), stepZ = l / (hRes - 1); + for (int j = 0; j < hRes; j++) + { + float z = oz + j * stepZ; + for (int i = 0; i < hRes; i++) + { + float x = ox + i * stepX; + float d = RawDist(x, z, half); + float sb = Boundary(x, z, d, round, far); + sGrid[j * hRes + i] = sb; + float s = roundHeight ? sb : d; + float y = s <= 0f ? topY : s >= slope ? bottom : Mathf.Lerp(topY, bottom, SmoothStep01(s / slope)); + heights[j, i] = Mathf.Clamp01((y - bottom) / span); + } + } + + // 5) 스플랫 — 층0 잔디 = 둥글린 발자국 안(경계 blend 에 걸쳐 1→0) · 층1 모래 = 나머지 + 밭 구역(WLSoftDirt 와 같은 둥근+노이즈 경계) float zcx = 0f, zcz = 0f, zhx = 0f, zhz = 0f, zrad = 0f, znoise = 0f, zcell = 1f, feather = 1f; if (zoneValid) { @@ -390,7 +413,11 @@ namespace WL.Look.Farm for (int i = 0; i < aRes; i++) { float x = ox + (i + 0.5f) * w / aRes; - float s = Boundary(x, z, RawDist(x, z, half), round, far); + // 경계 거리 = 높이맵 격자의 s 를 겹선형으로(0.25 m 격자 · blend 1 m 에 충분 · 링 재탐색 0) + float gx = Mathf.Clamp((x - ox) / stepX, 0f, hRes - 1.0001f), gz = Mathf.Clamp((z - oz) / stepZ, 0f, hRes - 1.0001f); + int gi = (int)gx, gj = (int)gz; float tx = gx - gi, tz = gz - gj; + float s00 = sGrid[gj * hRes + gi], s10 = sGrid[gj * hRes + gi + 1], s01 = sGrid[(gj + 1) * hRes + gi], s11 = sGrid[(gj + 1) * hRes + gi + 1]; + float s = Mathf.Lerp(Mathf.Lerp(s00, s10, tx), Mathf.Lerp(s01, s11, tx), tz); float grass = 1f - SmoothStep01(s / blend); // s ≤ 0 잔디 1 → s ≥ blend 잔디 0(모래) float dirt = 0f; if (zoneValid && Mathf.Abs(x - zcx) <= zhx + znoise + feather && Mathf.Abs(z - zcz) <= zhz + znoise + feather) @@ -404,24 +431,6 @@ namespace WL.Look.Farm } } - // 5) 높이맵 — 발자국 안 윗면 → 경계에서 바깥 slope 동안 smoothstep 으로 바닥 → 그 밖 평평 - // terrainRoundHeight 1 = 둥글린·굽이친 경계에서 내려간다(발주서) · 0 = 타일 네모 가장자리에서 내려간다(콜라이더와 일치 · 둥글림은 스플랫만) - bool roundHeight = cfg.terrainRoundHeight != 0; - var heights = new float[hRes, hRes]; - float stepX = w / (hRes - 1), stepZ = l / (hRes - 1); - for (int j = 0; j < hRes; j++) - { - float z = oz + j * stepZ; - for (int i = 0; i < hRes; i++) - { - float x = ox + i * stepX; - float d = RawDist(x, z, half); - float s = roundHeight ? Boundary(x, z, d, round, far) : d; - float y = s <= 0f ? topY : s >= slope ? bottom : Mathf.Lerp(topY, bottom, SmoothStep01(s / slope)); - heights[j, i] = Mathf.Clamp01((y - bottom) / span); - } - } - _data.SetHeights(0, 0, heights); _data.SetAlphamaps(0, 0, alpha); Bakes++;