[WL] 가격판 = 최소제곱 평면 눕힘(경사·다리 아치 밀착) · 잠긴 칸 자리 기억(다리 위 판 고정 · OnActivated 즉시 배치) · 십자 간격(inset 1.3 · 0.45배 · 트리거 2.6 · 장식/판/소품 회피) · 글자 깊이 검사(캐릭터가 가림) · 자동 진입 단일 인스턴스 + 8 s 대기
- WLIslandFlowV2: PadSurfaceY 최소제곱 평면 + 테두리 묻힘 0 · IsGroundCollider(소품·벽·난간 제외) · 자리 기억 PlayerPrefs WL_Island_PadSpots · Watch(OnActivated 즉시 PlacePurchasers · 다음 프레임/Baked 뒤 RefitPlaced) · IsSpotBlocked(캠프 장식·던전 문·발판·다른 판·IProp 콜라이더) 가장자리 따라 ±1/±2/끝 비킴 · ApplyOverlay(textAlwaysOnTop) - WLIslandSettings: expansionPadInset 1.3 · Scale 0.45 · TriggerSize 2.6 · Lift 0.05 · TiltMax 20 · padClearRadius 1.4 · expansionPadOverlay 1 = 스프라이트만 깊이 검사 - WLSpriteOverlay.shader: Offset -1,-1 → 0,-2 - WLIslandSave: 세이브 초기화 때 자리 기억 삭제 · WLIslandCampDecor.Made 공개 - WLLeadAutoEnter: 단일 인스턴스(Create/Exists) · 로그인 완료 뒤 EnterDelay 8 s · 재시도 90 s Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
edaadc18fa
commit
c313dd88fd
|
|
@ -95,11 +95,12 @@ MonoBehaviour:
|
||||||
installPushMargin: 0.75
|
installPushMargin: 0.75
|
||||||
islandPlayerScale: 1.3
|
islandPlayerScale: 1.3
|
||||||
expansionPadPlace: 1
|
expansionPadPlace: 1
|
||||||
expansionPadInset: 1
|
expansionPadInset: 1.3
|
||||||
padClearRadius: 1.6
|
padClearRadius: 1.4
|
||||||
expansionPadLift: 0.06
|
expansionPadLift: 0.05
|
||||||
expansionPadTriggerSize: 3.4
|
expansionPadTriggerSize: 2.6
|
||||||
expansionPadScale: 0.5
|
expansionPadScale: 0.45
|
||||||
|
expansionPadTiltMax: 20
|
||||||
expansionPadOverlay: 1
|
expansionPadOverlay: 1
|
||||||
campDecorSnapToGround: 0
|
campDecorSnapToGround: 0
|
||||||
applyStartCoin: 1
|
applyStartCoin: 1
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
// WLSpriteOverlay.shader — 스프라이트를 **항상 위에**(깊이 무시) 그리는 언릿 · 알파 블렌드 셰이더
|
// WLSpriteOverlay.shader — 가격판 스프라이트용 언릿 · 알파 블렌드 셰이더(깊이 검사 · 바닥 밀착)
|
||||||
// 2026-09-17 PD 「가격판이 풀·오브젝트에 가려지지 않게 상단에 보이게」 — 확장 가격판(FI Purchaser 의 Frame/Coin 스프라이트)에 런타임 복사본 재질로 물린다.
|
// 2026-09-17 PD 「가격판이 풀·오브젝트에 가려지지 않게 상단에 보이게」 → 같은 날 「가격 판이 캐릭터를 가리지 않게 · 바닥에 붙은 느낌」.
|
||||||
|
// 확장 가격판(FI Purchaser 의 Frame/Coin 스프라이트)·우리 발판에 런타임 복사본 재질로 물린다.
|
||||||
// SpriteRenderer 가 _MainTex 에 스프라이트 텍스처를 넣어 주고 정점색을 곱한다. 모바일: 텍스처 1장 · 조명 없음.
|
// SpriteRenderer 가 _MainTex 에 스프라이트 텍스처를 넣어 주고 정점색을 곱한다. 모바일: 텍스처 1장 · 조명 없음.
|
||||||
Shader "WL/SpriteOverlay"
|
Shader "WL/SpriteOverlay"
|
||||||
{
|
{
|
||||||
|
|
@ -14,10 +15,11 @@ Shader "WL/SpriteOverlay"
|
||||||
Cull Off
|
Cull Off
|
||||||
Lighting Off
|
Lighting Off
|
||||||
ZWrite Off
|
ZWrite Off
|
||||||
// 2026-09-17 PD 「가격판은 캐릭터보다 아래」 — 깊이 검사는 하되(캐릭터·소품이 가린다) 바닥과 겹치지 않게 살짝 당긴다.
|
// 2026-09-17 PD 「가격판은 캐릭터보다 아래」 — 깊이 검사(캐릭터·소품이 앞이면 가린다). 판은 지형 표본 위 5 cm 에 놓이므로 깊이 당김은
|
||||||
|
// 버퍼 단위 2 만(기울기 계수 −1 은 비스듬한 바닥 면에서 크게 당겨져 코인이 캐릭터 발 위로 올라왔다 · 실측).
|
||||||
// 풀에 가려지는 문제는 지형 쪽에서 판 자리에 모래를 깔아(풀 없음) 해결한다(WLDemoTerrain 판 자리 모래).
|
// 풀에 가려지는 문제는 지형 쪽에서 판 자리에 모래를 깔아(풀 없음) 해결한다(WLDemoTerrain 판 자리 모래).
|
||||||
ZTest LEqual
|
ZTest LEqual
|
||||||
Offset -1, -1
|
Offset 0, -2
|
||||||
Blend SrcAlpha OneMinusSrcAlpha
|
Blend SrcAlpha OneMinusSrcAlpha
|
||||||
|
|
||||||
Pass
|
Pass
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,9 @@ namespace WL.Island
|
||||||
|
|
||||||
static readonly List<GameObject> s_made = new List<GameObject>(4);
|
static readonly List<GameObject> s_made = new List<GameObject>(4);
|
||||||
|
|
||||||
|
/// <summary>지금 놓여 있는 캠프 장식 루트(들) — 확장 가격판이 텐트·모닥불을 피해 앉을 때 읽는다(2026-09-17).</summary>
|
||||||
|
public static IReadOnlyList<GameObject> Made { get { return s_made; } }
|
||||||
|
|
||||||
/// <summary>진단 — 마지막으로 놓은 장식 수·중심.</summary>
|
/// <summary>진단 — 마지막으로 놓은 장식 수·중심.</summary>
|
||||||
public static int DecorSpawned;
|
public static int DecorSpawned;
|
||||||
public static Vector3 LastCenter;
|
public static Vector3 LastCenter;
|
||||||
|
|
|
||||||
|
|
@ -486,11 +486,15 @@ namespace WL.Island
|
||||||
var pos = p.transform.position; // 잠긴 칸 중앙(FI 원래 자리)
|
var pos = p.transform.position; // 잠긴 칸 중앙(FI 원래 자리)
|
||||||
Vector3 target, c;
|
Vector3 target, c;
|
||||||
if (!ComputePadSpot(cfg, pos, islands, size, out target, out c)) continue; // 이웃을 못 찾으면 FI 자리 그대로
|
if (!ComputePadSpot(cfg, pos, islands, size, out target, out c)) continue; // 이웃을 못 찾으면 FI 자리 그대로
|
||||||
float gy = PadGroundY(target, c.y);
|
|
||||||
float plateDropLocal = 0f; var frame = p.transform.Find("Frame"); if (frame != null) plateDropLocal = -frame.localPosition.y; // 판 자식들이 루트보다 아래(로컬 −0.49)
|
float plateDropLocal = 0f; var frame = p.transform.Find("Frame"); if (frame != null) plateDropLocal = -frame.localPosition.y; // 판 자식들이 루트보다 아래(로컬 −0.49)
|
||||||
// 🔴 2026-09-17 PD 「확장 가격판이 너무 크다 → 캠프파이어 발판과 같은 크기」 = 우리 발판과 같은 배율(platformScale 0.5 = expansionPadScale)
|
// 🔴 2026-09-17 PD 「확장 가격판이 너무 크다 → 캠프파이어 발판과 같은 크기」 = 우리 발판과 같은 배율(platformScale 0.5 = expansionPadScale)
|
||||||
float ps = cfg.expansionPadScale > 0.05f ? cfg.expansionPadScale : 1f;
|
float ps = cfg.expansionPadScale > 0.05f ? cfg.expansionPadScale : 1f;
|
||||||
p.transform.localScale = Vector3.one * ps;
|
p.transform.localScale = Vector3.one * ps;
|
||||||
|
// 🔴 2026-09-17 PD 「가격판이 경사에 가려지지 않게」 — 판 넓이(반지름 ≈ 2.56×배율) 안 5점의 지형·콜라이더 높이 중 **최고점** 위에 놓고,
|
||||||
|
// 지형 법선에 맞춰 살짝 눕힌다(최대 expansionPadTiltMax°) → 해변 경사·둔덕 어느 쪽에도 묻히지 않는다.
|
||||||
|
Quaternion rot;
|
||||||
|
float gy = PadSurfaceY(new Vector3(target.x, c.y, target.z), c.y, 2.56f * ps, cfg.expansionPadTiltMax, out rot);
|
||||||
|
p.transform.rotation = rot;
|
||||||
p.transform.position = new Vector3(target.x, gy + plateDropLocal * ps + cfg.expansionPadLift, target.z);
|
p.transform.position = new Vector3(target.x, gy + plateDropLocal * ps + cfg.expansionPadLift, target.z);
|
||||||
// 🔴 2026-09-17 — 원본 트리거는 십자(10×5×5 두 개 · ±5 m)라 가장자리로 옮긴 판끼리 겹쳐 두 구매가 동시에 시작됐다(원본 활성화 경쟁 → 「Bridge (0) inactive」).
|
// 🔴 2026-09-17 — 원본 트리거는 십자(10×5×5 두 개 · ±5 m)라 가장자리로 옮긴 판끼리 겹쳐 두 구매가 동시에 시작됐다(원본 활성화 경쟁 → 「Bridge (0) inactive」).
|
||||||
// 판 크기(expansionPadTriggerSize · 월드 m)만큼의 상자 하나로 줄인다 = 판을 밟아야 산다(우리 발판과 같은 규칙). 로컬 치수는 배율로 역보정.
|
// 판 크기(expansionPadTriggerSize · 월드 m)만큼의 상자 하나로 줄인다 = 판을 밟아야 산다(우리 발판과 같은 규칙). 로컬 치수는 배율로 역보정.
|
||||||
|
|
@ -510,23 +514,25 @@ namespace WL.Island
|
||||||
boxes[b].center = new Vector3(0f, -plateDropLocal + 2f / ps, 0f); // 판 높이에서 위로 4 m(월드)
|
boxes[b].center = new Vector3(0f, -plateDropLocal + 2f / ps, 0f); // 판 높이에서 위로 4 m(월드)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (cfg.expansionPadOverlay != 0) ApplyOverlay(p.gameObject); // 풀·소품에 가려지지 않게 항상 위에
|
if (cfg.expansionPadOverlay != 0) ApplyOverlay(p.gameObject, cfg.expansionPadOverlay >= 2); // 1 = 깊이 검사(캐릭터가 가림) · 2 = 글자까지 항상 위
|
||||||
var mk = p.gameObject.AddComponent<WLPlacedPurchaser>(); mk.original = pos;
|
var mk = p.gameObject.AddComponent<WLPlacedPurchaser>(); mk.original = pos;
|
||||||
PurchasersPlaced++;
|
PurchasersPlaced++;
|
||||||
LastPlaceLog = "확장 가격판 " + pos.ToString("F0") + " → " + p.transform.position.ToString("F2") + "(이웃 " + c.ToString("F0") + " 가장자리 안쪽 " + cfg.expansionPadInset.ToString("F1") + " m · 지면 " + gy.ToString("F2") + ")";
|
LastPlaceLog = "확장 가격판 " + pos.ToString("F0") + " → " + p.transform.position.ToString("F2") + "(이웃 " + c.ToString("F0") + " 가장자리 · 옆으로 " + LastSlide.ToString("F1") + " m · 표면 " + gy.ToString("F2") + " · " + LastFitLog + ")";
|
||||||
WLIslandBridge.Log(cfg, LastPlaceLog);
|
WLIslandBridge.Log(cfg, LastPlaceLog);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────
|
||||||
// 🔴 2026-09-17 PD 「가격판이 풀·오브젝트에 가려지지 않게 상단에 보이게」 — 스프라이트(Frame·Coin)는 깊이 무시 셰이더 `WL/SpriteOverlay`,
|
// 🔴 2026-09-17 PD 「가격판이 풀·오브젝트에 가려지지 않게」 → 「가격 판이 캐릭터를 가리지 않게 · 바닥에 붙은 느낌」(같은 날 두 번째 지시)
|
||||||
// 가격 글자(TMP)는 TMP 의 Overlay 변형 셰이더로 바꾼 **런타임 복사본** 재질을 물린다(에셋 0줄 · 큐 3100 = 물·거품 뒤).
|
// 스프라이트(Frame·Coin)는 `WL/SpriteOverlay`(깊이 검사 LEqual + 깊이 오프셋 = 바닥과 z-파이팅 없이 붙고, 캐릭터·소품이 앞에 있으면 가려진다).
|
||||||
|
// 가격 글자(TMP)는 **원본 재질 그대로**(깊이 검사) — `expansionPadOverlay 2` 일 때만 Overlay 변형(항상 위) 복사본을 물린다(에셋 0줄).
|
||||||
|
// 풀은 판 자리 모래 원(padClearRadius)이 비우므로 항상-위 그리기가 더는 필요 없다.
|
||||||
// ─────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────
|
||||||
static Material s_spriteOverlay;
|
static Material s_spriteOverlay;
|
||||||
static readonly Dictionary<Material, Material> s_tmpOverlay = new Dictionary<Material, Material>();
|
static readonly Dictionary<Material, Material> s_tmpOverlay = new Dictionary<Material, Material>();
|
||||||
public static int OverlayApplied;
|
public static int OverlayApplied;
|
||||||
|
|
||||||
public static void ApplyOverlay(GameObject root)
|
public static void ApplyOverlay(GameObject root, bool textAlwaysOnTop)
|
||||||
{
|
{
|
||||||
if (root == null) return;
|
if (root == null) return;
|
||||||
var srs = root.GetComponentsInChildren<SpriteRenderer>(true);
|
var srs = root.GetComponentsInChildren<SpriteRenderer>(true);
|
||||||
|
|
@ -540,6 +546,7 @@ namespace WL.Island
|
||||||
if (s_spriteOverlay != null)
|
if (s_spriteOverlay != null)
|
||||||
for (int i = 0; i < srs.Length; i++) if (srs[i] != null && srs[i].sharedMaterial != s_spriteOverlay) { srs[i].sharedMaterial = s_spriteOverlay; OverlayApplied++; }
|
for (int i = 0; i < srs.Length; i++) if (srs[i] != null && srs[i].sharedMaterial != s_spriteOverlay) { srs[i].sharedMaterial = s_spriteOverlay; OverlayApplied++; }
|
||||||
}
|
}
|
||||||
|
if (!textAlwaysOnTop) return;
|
||||||
var texts = root.GetComponentsInChildren<TMPro.TMP_Text>(true);
|
var texts = root.GetComponentsInChildren<TMPro.TMP_Text>(true);
|
||||||
for (int i = 0; i < texts.Length; i++)
|
for (int i = 0; i < texts.Length; i++)
|
||||||
{
|
{
|
||||||
|
|
@ -563,9 +570,60 @@ namespace WL.Island
|
||||||
/// 그 칸 가장자리에서 `expansionPadInset` 안쪽 지점을 돌려준다. 가격판 배치와 지형(판 자리 모래·풀 없음)이 **같은 식**을 쓴다.
|
/// 그 칸 가장자리에서 `expansionPadInset` 안쪽 지점을 돌려준다. 가격판 배치와 지형(판 자리 모래·풀 없음)이 **같은 식**을 쓴다.
|
||||||
/// 2026-09-17 PD 「가격판은 이동 가능한 범위 안에서 최대한 가장자리」 → inset 기본 1.0 m.
|
/// 2026-09-17 PD 「가격판은 이동 가능한 범위 안에서 최대한 가장자리」 → inset 기본 1.0 m.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
// 🔴 2026-09-17 PD 「다리 위 가격판 자리가 저절로 바뀌지 않게」 ① — 잠긴 칸마다 처음 정한 자리를 기억한다(더 좋은 이웃이 나중에 열려도 · 던전에서 돌아와
|
||||||
|
// FI 가 세이브에서 가격판을 다시 만들어도 그대로). PlayerPrefs 에 같이 적어 두고, 세이브 초기화(WLIslandStart) 때 함께 비운다.
|
||||||
|
const string kSpotPrefsKey = "WL_Island_PadSpots";
|
||||||
|
static readonly Dictionary<Vector3Int, Vector3> s_spotByTile = new Dictionary<Vector3Int, Vector3>();
|
||||||
|
static readonly Dictionary<Vector3Int, Vector3> s_neighborByTile = new Dictionary<Vector3Int, Vector3>();
|
||||||
|
static bool s_spotsLoaded;
|
||||||
|
public static int SpotMemory { get { return s_spotByTile.Count; } }
|
||||||
|
|
||||||
|
public static void ClearSpotMemory() { s_spotByTile.Clear(); s_neighborByTile.Clear(); s_spotsLoaded = true; PlayerPrefs.DeleteKey(kSpotPrefsKey); }
|
||||||
|
|
||||||
|
static void LoadSpots()
|
||||||
|
{
|
||||||
|
if (s_spotsLoaded) return;
|
||||||
|
s_spotsLoaded = true;
|
||||||
|
var s = PlayerPrefs.GetString(kSpotPrefsKey, "");
|
||||||
|
if (string.IsNullOrEmpty(s)) return;
|
||||||
|
var ci = System.Globalization.CultureInfo.InvariantCulture;
|
||||||
|
var entries = s.Split(';');
|
||||||
|
for (int i = 0; i < entries.Length; i++)
|
||||||
|
{
|
||||||
|
var f = entries[i].Split(',');
|
||||||
|
if (f.Length != 6) continue;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var key = new Vector3Int(int.Parse(f[0], ci), 0, int.Parse(f[1], ci));
|
||||||
|
s_spotByTile[key] = new Vector3(float.Parse(f[2], ci), 0f, float.Parse(f[3], ci));
|
||||||
|
s_neighborByTile[key] = new Vector3(float.Parse(f[4], ci), 0f, float.Parse(f[5], ci));
|
||||||
|
}
|
||||||
|
catch (System.Exception) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void SaveSpots()
|
||||||
|
{
|
||||||
|
var ci = System.Globalization.CultureInfo.InvariantCulture;
|
||||||
|
var sb = new System.Text.StringBuilder();
|
||||||
|
foreach (var kv in s_spotByTile)
|
||||||
|
{
|
||||||
|
Vector3 n; if (!s_neighborByTile.TryGetValue(kv.Key, out n)) n = kv.Value;
|
||||||
|
if (sb.Length > 0) sb.Append(';');
|
||||||
|
sb.Append(kv.Key.x.ToString(ci)).Append(',').Append(kv.Key.z.ToString(ci)).Append(',')
|
||||||
|
.Append(kv.Value.x.ToString("F2", ci)).Append(',').Append(kv.Value.z.ToString("F2", ci)).Append(',')
|
||||||
|
.Append(n.x.ToString("F2", ci)).Append(',').Append(n.z.ToString("F2", ci));
|
||||||
|
}
|
||||||
|
PlayerPrefs.SetString(kSpotPrefsKey, sb.ToString());
|
||||||
|
PlayerPrefs.Save();
|
||||||
|
}
|
||||||
|
|
||||||
public static bool ComputePadSpot(WLIslandSettings cfg, Vector3 lockedPos, FIIsland[] islands, float size, out Vector3 spot, out Vector3 neighborCenter)
|
public static bool ComputePadSpot(WLIslandSettings cfg, Vector3 lockedPos, FIIsland[] islands, float size, out Vector3 spot, out Vector3 neighborCenter)
|
||||||
{
|
{
|
||||||
spot = lockedPos; neighborCenter = lockedPos;
|
spot = lockedPos; neighborCenter = lockedPos;
|
||||||
|
LoadSpots();
|
||||||
|
var key = Vector3Int.RoundToInt(lockedPos);
|
||||||
|
if (s_spotByTile.TryGetValue(key, out spot)) { neighborCenter = s_neighborByTile[key]; return true; }
|
||||||
float half = size * 0.5f;
|
float half = size * 0.5f;
|
||||||
Vector3[] dirs = { Vector3.forward, Vector3.right, Vector3.back, Vector3.left };
|
Vector3[] dirs = { Vector3.forward, Vector3.right, Vector3.back, Vector3.left };
|
||||||
FIIsland best = null; Vector3 bestDir = Vector3.zero; int bestRank = int.MaxValue;
|
FIIsland best = null; Vector3 bestDir = Vector3.zero; int bestRank = int.MaxValue;
|
||||||
|
|
@ -585,10 +643,93 @@ namespace WL.Island
|
||||||
}
|
}
|
||||||
if (best == null) return false;
|
if (best == null) return false;
|
||||||
neighborCenter = best.transform.position;
|
neighborCenter = best.transform.position;
|
||||||
spot = neighborCenter + bestDir * (half - Mathf.Clamp(cfg.expansionPadInset, 0.3f, half));
|
// 🔴 2026-09-17 PD ③ 「확장 방향에 맞게 · 십자 방향 판끼리 안 겹치게 간격·크기」
|
||||||
|
// 자리 = 확장 방향 가장자리 **중앙**, 안쪽 거리 = max(설정, 판 반지름 + 0.15)(판·트리거가 칸 밖으로 안 나간다).
|
||||||
|
// 그 자리가 텐트·모닥불·던전 문·발판·다른 확장 판·소품과 겹치면 가장자리를 따라 옆으로(±1 · ±2 · 끝) 비킨다.
|
||||||
|
float ps = cfg.expansionPadScale > 0.05f ? cfg.expansionPadScale : 1f;
|
||||||
|
float padR = 2.56f * ps;
|
||||||
|
float inset = Mathf.Clamp(Mathf.Max(cfg.expansionPadInset, padR + 0.15f), 0.3f, half);
|
||||||
|
var basePos = neighborCenter + bestDir * (half - inset);
|
||||||
|
var perp = new Vector3(bestDir.z, 0f, bestDir.x);
|
||||||
|
float maxSlide = Mathf.Max(0f, half - padR - 0.25f);
|
||||||
|
float[] cands = { 0f, 1f, -1f, 2f, -2f, maxSlide, -maxSlide };
|
||||||
|
spot = basePos; LastSlide = 0f; bool found = false;
|
||||||
|
for (int k = 0; k < cands.Length; k++)
|
||||||
|
{
|
||||||
|
float s = Mathf.Clamp(cands[k], -maxSlide, maxSlide);
|
||||||
|
var cand = basePos + perp * s;
|
||||||
|
if (IsSpotBlocked(cfg, cand, padR, key, islands)) continue;
|
||||||
|
spot = cand; LastSlide = s; found = true; break;
|
||||||
|
}
|
||||||
|
if (!found) BlockedSpots++;
|
||||||
|
s_spotByTile[key] = new Vector3(spot.x, 0f, spot.z); s_neighborByTile[key] = new Vector3(neighborCenter.x, 0f, neighborCenter.z);
|
||||||
|
SaveSpots();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static float LastSlide; public static int BlockedSpots;
|
||||||
|
|
||||||
|
/// <summary>판 자리가 막혔는가 — 다른 확장 판(잠긴 칸 기억 자리) · 캠프 장식 · 던전 문 · 우리 발판(렌더러 경계) · 콜라이더 있는 섬 소품(동물 제외).</summary>
|
||||||
|
static bool IsSpotBlocked(WLIslandSettings cfg, Vector3 c, float padR, Vector3Int selfKey, FIIsland[] islands)
|
||||||
|
{
|
||||||
|
float sep = Mathf.Max(2f * padR, cfg.expansionPadTriggerSize) + 0.4f;
|
||||||
|
foreach (var kv in s_spotByTile)
|
||||||
|
{
|
||||||
|
if (kv.Key == selfKey || IsTileUnlocked(kv.Key, islands)) continue;
|
||||||
|
float dx = kv.Value.x - c.x, dz = kv.Value.z - c.z;
|
||||||
|
if (dx * dx + dz * dz < sep * sep) return true;
|
||||||
|
}
|
||||||
|
float r = padR + 0.2f;
|
||||||
|
var made = WLIslandCampDecor.Made;
|
||||||
|
for (int i = 0; i < made.Count; i++)
|
||||||
|
{
|
||||||
|
if (made[i] == null) continue;
|
||||||
|
var t = made[i].transform;
|
||||||
|
if (t.childCount == 0) { if (RectHitsCircle(RendererBounds(made[i]), c, r)) return true; continue; }
|
||||||
|
for (int j = 0; j < t.childCount; j++) if (RectHitsCircle(RendererBounds(t.GetChild(j).gameObject), c, r)) return true; // 텐트·모닥불 하나씩
|
||||||
|
}
|
||||||
|
var gates = Object.FindObjectsByType<WLDungeonGate>(FindObjectsSortMode.None);
|
||||||
|
for (int i = 0; i < gates.Length; i++) if (gates[i] != null && RectHitsCircle(RendererBounds(gates[i].gameObject), c, r)) return true;
|
||||||
|
var plats = Object.FindObjectsByType<WLGoldPlatform>(FindObjectsSortMode.None);
|
||||||
|
for (int i = 0; i < plats.Length; i++) if (plats[i] != null && RectHitsCircle(RendererBounds(plats[i].gameObject), c, r)) return true;
|
||||||
|
var cols = Physics.OverlapSphere(new Vector3(c.x, c.y + 0.5f, c.z), r, ~0, QueryTriggerInteraction.Collide);
|
||||||
|
for (int i = 0; i < cols.Length; i++)
|
||||||
|
{
|
||||||
|
var col = cols[i]; if (col == null) continue;
|
||||||
|
if (col.GetComponentInParent<CryingSnow.FarmingIsland.IProp>() == null) continue;
|
||||||
|
if (col.GetComponentInParent<CryingSnow.FarmingIsland.AnimalController>() != null) continue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool IsTileUnlocked(Vector3Int key, FIIsland[] islands)
|
||||||
|
{
|
||||||
|
if (islands == null) return false;
|
||||||
|
for (int i = 0; i < islands.Length; i++)
|
||||||
|
{
|
||||||
|
var isl = islands[i]; if (isl == null) continue;
|
||||||
|
if (Vector3Int.RoundToInt(isl.transform.position) == key) return isl.IsUnlocked;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Bounds RendererBounds(GameObject go)
|
||||||
|
{
|
||||||
|
var rs = go.GetComponentsInChildren<Renderer>(false);
|
||||||
|
if (rs.Length == 0) return new Bounds(go.transform.position, Vector3.zero);
|
||||||
|
var b = rs[0].bounds;
|
||||||
|
for (int i = 1; i < rs.Length; i++) b.Encapsulate(rs[i].bounds);
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool RectHitsCircle(Bounds b, Vector3 c, float r)
|
||||||
|
{
|
||||||
|
if (b.size.x <= 0f && b.size.z <= 0f) return false;
|
||||||
|
float dx = Mathf.Max(b.min.x - c.x, 0f, c.x - b.max.x), dz = Mathf.Max(b.min.z - c.z, 0f, c.z - b.max.z);
|
||||||
|
return dx * dx + dz * dz < r * r;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 지금 가격판이 놓일(놓인) 자리 전부 — 잠긴 칸 중 「직교 이웃이 열려 있고 선행 조건(Constraint)이 풀린」 칸(FI 가 판을 만드는 조건과 같음).
|
/// 지금 가격판이 놓일(놓인) 자리 전부 — 잠긴 칸 중 「직교 이웃이 열려 있고 선행 조건(Constraint)이 풀린」 칸(FI 가 판을 만드는 조건과 같음).
|
||||||
/// 지형이 이 자리에 모래를 깔아 풀을 비운다.
|
/// 지형이 이 자리에 모래를 깔아 풀을 비운다.
|
||||||
|
|
@ -613,6 +754,60 @@ namespace WL.Island
|
||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
// 🔴 2026-09-17 PD 「가격판이 경사에 가려지지 않게」
|
||||||
|
// 판 평면 = 중심 표면 높이 + 지형 법선 기울기(최대 expansionPadTiltMax°). 판 테두리 8점 어디서든 표면(지형·바닥 콜라이더)이
|
||||||
|
// 평면보다 높으면 그만큼 평면을 올린다 → 둔덕(안쪽) · 해변 경사(바깥쪽) 어느 쪽에도 판이 묻히지 않는다.
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
public static string LastFitLog = "";
|
||||||
|
|
||||||
|
static float PadSurfaceY(Vector3 center, float fallbackY, float r, float tiltMax, out Quaternion rot)
|
||||||
|
{
|
||||||
|
var dt = Object.FindFirstObjectByType<global::WL.Look.Farm.WLDemoTerrain>(FindObjectsInactive.Include);
|
||||||
|
var terrain = dt != null ? dt.TerrainComponent : null;
|
||||||
|
// 중심 + 테두리 8점의 표면 높이로 최소제곱 평면 y = a·x + b·z + c 를 맞춘다(테두리는 대칭 → Σx² = Σz² = 4r²) → 둔덕·해변 경사·다리 아치를 모두 따라 눕는다.
|
||||||
|
var os = new Vector3[9]; var ys = new float[9];
|
||||||
|
float sum = 0f, sx = 0f, sz = 0f;
|
||||||
|
for (int k = 0; k < 9; k++)
|
||||||
|
{
|
||||||
|
os[k] = k < 8 ? new Vector3(Mathf.Cos(k * Mathf.PI * 0.25f) * r, 0f, Mathf.Sin(k * Mathf.PI * 0.25f) * r) : Vector3.zero;
|
||||||
|
ys[k] = SurfaceAt(center + os[k], fallbackY, terrain);
|
||||||
|
sum += ys[k]; sx += ys[k] * os[k].x; sz += ys[k] * os[k].z;
|
||||||
|
}
|
||||||
|
float a = sx / (4f * r * r), b = sz / (4f * r * r), c = sum / 9f;
|
||||||
|
rot = PadTilt(new Vector3(-a, 1f, -b), tiltMax);
|
||||||
|
float raise = float.MinValue; // 어느 표본도 판 아래에 묻히지 않게 평면을 올린다
|
||||||
|
for (int k = 0; k < 9; k++) { float d = ys[k] - (c + (rot * os[k]).y); if (d > raise) raise = d; }
|
||||||
|
if (raise < 0f) raise = 0f;
|
||||||
|
LastFitLog = "평면 " + c.ToString("F2") + " 기울기 " + Quaternion.Angle(Quaternion.identity, rot).ToString("F1") + "° 올림 " + raise.ToString("F2");
|
||||||
|
return c + raise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>한 점의 표면 높이 = 바닥 콜라이더(레이 · 소품·플레이어·가격판·벽 제외)와 데모 지형 높이 중 큰 값.</summary>
|
||||||
|
static float SurfaceAt(Vector3 p, float fallbackY, Terrain terrain)
|
||||||
|
{
|
||||||
|
float h = PadGroundY(p, fallbackY);
|
||||||
|
if (terrain != null)
|
||||||
|
{
|
||||||
|
var td = terrain.terrainData; var tp = terrain.transform.position;
|
||||||
|
if (td != null && p.x >= tp.x && p.z >= tp.z && p.x <= tp.x + td.size.x && p.z <= tp.z + td.size.z)
|
||||||
|
{
|
||||||
|
float th = terrain.SampleHeight(p) + tp.y;
|
||||||
|
if (th > h) h = th;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Quaternion PadTilt(Vector3 normal, float maxDeg)
|
||||||
|
{
|
||||||
|
if (maxDeg <= 0f || normal.sqrMagnitude < 1e-6f) return Quaternion.identity;
|
||||||
|
var q = Quaternion.FromToRotation(Vector3.up, normal.normalized);
|
||||||
|
float ang = Quaternion.Angle(Quaternion.identity, q);
|
||||||
|
if (ang > maxDeg) q = Quaternion.Slerp(Quaternion.identity, q, maxDeg / ang);
|
||||||
|
return q;
|
||||||
|
}
|
||||||
|
|
||||||
static float PadGroundY(Vector3 p, float fallbackY)
|
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);
|
var hits = Physics.RaycastAll(new Vector3(p.x, fallbackY + 5f, p.z), Vector3.down, 20f, ~0, QueryTriggerInteraction.Ignore);
|
||||||
|
|
@ -621,11 +816,84 @@ namespace WL.Island
|
||||||
{
|
{
|
||||||
float y = hits[i].point.y;
|
float y = hits[i].point.y;
|
||||||
if (y > fallbackY + 1.5f) continue;
|
if (y > fallbackY + 1.5f) continue;
|
||||||
|
if (!IsGroundCollider(hits[i].collider)) continue;
|
||||||
if (float.IsNaN(best) || y > best) best = y;
|
if (float.IsNaN(best) || y > best) best = y;
|
||||||
}
|
}
|
||||||
return float.IsNaN(best) ? fallbackY : best;
|
return float.IsNaN(best) ? fallbackY : best;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>바닥으로 치는 콜라이더 — 지형 · 우리 둔덕 콜라이더 · 섬 칸(타일·다리 바닥). 소품·가장자리 벽(세로 얇은 상자)·플레이어·가격판·발판·설치물은 제외(판이 그 위로 올라가면 안 된다).</summary>
|
||||||
|
static bool IsGroundCollider(Collider c)
|
||||||
|
{
|
||||||
|
if (c == null || c.isTrigger) return false;
|
||||||
|
if (c is TerrainCollider) return true;
|
||||||
|
if (c.gameObject.name.StartsWith("~WL_Relief")) return true;
|
||||||
|
if (c.GetComponentInParent<FIIsland>() == null) return false;
|
||||||
|
if (c.GetComponentInParent<CryingSnow.FarmingIsland.IProp>() != null) return false;
|
||||||
|
var box = c as BoxCollider;
|
||||||
|
if (box != null)
|
||||||
|
{
|
||||||
|
var s = Vector3.Scale(box.size, c.transform.lossyScale);
|
||||||
|
if (s.y > 0.6f && Mathf.Min(Mathf.Abs(s.x), Mathf.Abs(s.z)) < 3f) return false; // 가장자리 벽(8×2×0.3)·다리 난간(2.6×2×8)
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
// 🔴 2026-09-17 PD 「다리 위 가격판 자리가 저절로 바뀌지 않게」 ②
|
||||||
|
// FI 는 칸을 연 순간(OnActivated) 새 가격판을 잠긴 칸 중앙에 만든다 → 3 s 훑기가 오기 전까지 원래 자리(바다 쪽)에 보였다가 옮겨졌다.
|
||||||
|
// → 칸이 열리는 **그 프레임**에 바로 옮긴다. 지형이 다시 구워지면(솟아오르기 끝) 높이·기울기만 다시 맞춘다(자리 x·z 고정).
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
static readonly HashSet<FIIsland> s_watched = new HashSet<FIIsland>();
|
||||||
|
static bool s_bakedHooked; static WLIslandSettings s_cfgForBaked;
|
||||||
|
public static int Refits, ActivationPlaces;
|
||||||
|
|
||||||
|
static void Watch(WLIslandSettings cfg, Scene scene)
|
||||||
|
{
|
||||||
|
s_watched.RemoveWhere(i => i == null);
|
||||||
|
var islands = Object.FindObjectsByType<FIIsland>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||||||
|
for (int i = 0; i < islands.Length; i++)
|
||||||
|
{
|
||||||
|
var isl = islands[i];
|
||||||
|
if (isl == null || s_watched.Contains(isl)) continue;
|
||||||
|
s_watched.Add(isl);
|
||||||
|
// 🔴 실측: OnActivated 는 DOScale 완료 콜백 — 같은 프레임의 DOJump 착지가 아직 안 끝나 칸이 ~9 cm 떠 있다 → 자리는 즉시, 높이는 다음 프레임에 한 번 더 맞춘다.
|
||||||
|
isl.OnActivated += () =>
|
||||||
|
{
|
||||||
|
if (cfg == null) return;
|
||||||
|
ActivationPlaces++;
|
||||||
|
PlacePurchasers(cfg, scene);
|
||||||
|
var runner = WLIslandBridge.Runner;
|
||||||
|
if (runner != null) runner.StartCoroutine(Co_RefitNextFrame(cfg));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
s_cfgForBaked = cfg;
|
||||||
|
if (!s_bakedHooked) { s_bakedHooked = true; global::WL.Look.Farm.WLDemoTerrain.Baked += OnTerrainBaked; }
|
||||||
|
}
|
||||||
|
|
||||||
|
static void OnTerrainBaked() { if (s_cfgForBaked != null) RefitPlaced(s_cfgForBaked); }
|
||||||
|
|
||||||
|
static IEnumerator Co_RefitNextFrame(WLIslandSettings cfg) { yield return null; RefitPlaced(cfg); }
|
||||||
|
|
||||||
|
/// <summary>이미 옮긴 판의 높이·기울기만 지형에 다시 맞춘다(자리는 그대로).</summary>
|
||||||
|
public static void RefitPlaced(WLIslandSettings cfg)
|
||||||
|
{
|
||||||
|
if (cfg == null || cfg.expansionPadPlace == 0) return;
|
||||||
|
var marks = Object.FindObjectsByType<WLPlacedPurchaser>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||||||
|
for (int i = 0; i < marks.Length; i++)
|
||||||
|
{
|
||||||
|
var m = marks[i]; if (m == null) continue;
|
||||||
|
var t = m.transform;
|
||||||
|
float ps = t.localScale.x > 0.05f ? t.localScale.x : 1f;
|
||||||
|
float plateDropLocal = 0f; var frame = t.Find("Frame"); if (frame != null) plateDropLocal = -frame.localPosition.y;
|
||||||
|
Quaternion rot; var pos = t.position;
|
||||||
|
float gy = PadSurfaceY(new Vector3(pos.x, m.original.y, pos.z), m.original.y, 2.56f * ps, cfg.expansionPadTiltMax, out rot);
|
||||||
|
t.rotation = rot;
|
||||||
|
t.position = new Vector3(pos.x, gy + plateDropLocal * ps + cfg.expansionPadLift, pos.z);
|
||||||
|
Refits++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static readonly List<Renderer> s_rends = new List<Renderer>(64);
|
static readonly List<Renderer> s_rends = new List<Renderer>(64);
|
||||||
static readonly List<Collider> s_cols = new List<Collider>(64);
|
static readonly List<Collider> s_cols = new List<Collider>(64);
|
||||||
|
|
||||||
|
|
@ -657,6 +925,7 @@ namespace WL.Island
|
||||||
{
|
{
|
||||||
if (cfg == null || cfg.gateFlowEnabled == 0 || cfg.expansionAfterFirstWin == 0) return;
|
if (cfg == null || cfg.gateFlowEnabled == 0 || cfg.expansionAfterFirstWin == 0) return;
|
||||||
|
|
||||||
|
Watch(cfg, scene); // 칸이 열리는 프레임에 바로 옮기기 · 지형 재굽기 뒤 높이 재맞춤
|
||||||
PlacePurchasers(cfg, scene); // 자리 옮기기(숨김 여부와 무관 · 1회)
|
PlacePurchasers(cfg, scene); // 자리 옮기기(숨김 여부와 무관 · 1회)
|
||||||
if (IsUnlocked(cfg))
|
if (IsUnlocked(cfg))
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -797,7 +797,7 @@ namespace WL.Island
|
||||||
float ps = cfg.platformScale > 0.001f ? cfg.platformScale : 1f;
|
float ps = cfg.platformScale > 0.001f ? cfg.platformScale : 1f;
|
||||||
AppliedScale = ps;
|
AppliedScale = ps;
|
||||||
if (ps != 1f) p.transform.localScale = p.transform.localScale * ps;
|
if (ps != 1f) p.transform.localScale = p.transform.localScale * ps;
|
||||||
if (cfg.expansionPadOverlay != 0) WLIslandExpansionGate.ApplyOverlay(p.gameObject); // 2026-09-17 PD — 우리 발판도 풀·소품 위에 보이게(같은 규칙)
|
if (cfg.expansionPadOverlay != 0) WLIslandExpansionGate.ApplyOverlay(p.gameObject, cfg.expansionPadOverlay >= 2); // 2026-09-17 PD — 우리 발판도 같은 규칙(1 = 깊이 검사 · 캐릭터가 가림)
|
||||||
|
|
||||||
// 🔴 FI 가격판은 프리팹 안에서 **루트보다 앞·아래**에 놓여 있다(실측 2026-09-13:
|
// 🔴 FI 가격판은 프리팹 안에서 **루트보다 앞·아래**에 놓여 있다(실측 2026-09-13:
|
||||||
// Frame/Price/Coin 이 전부 y = -0.49 · z = 루트+(-1/0/+1) — 잠긴 섬 자리의 **물 위**에 눕는 판이다).
|
// Frame/Price/Coin 이 전부 y = -0.49 · z = 루트+(-1/0/+1) — 잠긴 섬 자리의 **물 위**에 눕는 판이다).
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,7 @@ namespace WL.Island
|
||||||
// WL-816zc — 초기 플로우 단계·확장 개방도 같이 0 으로(시작마다 초기화 대상)
|
// WL-816zc — 초기 플로우 단계·확장 개방도 같이 0 으로(시작마다 초기화 대상)
|
||||||
if (!string.IsNullOrEmpty(cfg.flowStepPrefsKey)) PlayerPrefs.SetInt(cfg.flowStepPrefsKey, 0);
|
if (!string.IsNullOrEmpty(cfg.flowStepPrefsKey)) PlayerPrefs.SetInt(cfg.flowStepPrefsKey, 0);
|
||||||
if (!string.IsNullOrEmpty(cfg.expansionUnlockPrefsKey)) PlayerPrefs.SetInt(cfg.expansionUnlockPrefsKey, 0);
|
if (!string.IsNullOrEmpty(cfg.expansionUnlockPrefsKey)) PlayerPrefs.SetInt(cfg.expansionUnlockPrefsKey, 0);
|
||||||
|
WLIslandExpansionGate.ClearSpotMemory(); // 2026-09-17 — 확장 가격판 자리 기억도 세이브와 함께 초기화
|
||||||
PlayerPrefs.Save();
|
PlayerPrefs.Save();
|
||||||
WLIslandBridge.ClearPending(); // 보류 보상도 비운다(정적 값까지)
|
WLIslandBridge.ClearPending(); // 보류 보상도 비운다(정적 값까지)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -564,22 +564,25 @@ namespace WL.Island
|
||||||
[Tooltip("1 = FI 확장 가격판(잠긴 칸 중앙 · 물 위)을 확장 방향 이웃(열린) 칸 가장자리 안쪽 지면 위로 옮긴다(FI 격자·세이브 무변경 · 트리거도 함께). 0 = 원본 자리.")]
|
[Tooltip("1 = FI 확장 가격판(잠긴 칸 중앙 · 물 위)을 확장 방향 이웃(열린) 칸 가장자리 안쪽 지면 위로 옮긴다(FI 격자·세이브 무변경 · 트리거도 함께). 0 = 원본 자리.")]
|
||||||
public int expansionPadPlace = 1;
|
public int expansionPadPlace = 1;
|
||||||
|
|
||||||
[Tooltip("이웃 칸 가장자리에서 안쪽으로 이만큼(m) 들어온 자리에 놓는다. 2026-09-17 PD 「이동 가능한 범위 안에서 최대한 가장자리」 → 2.2 → 1.0(타일 콜라이더 끝 = 걸어갈 수 있는 끝).")]
|
[Tooltip("이웃 칸 가장자리에서 안쪽으로 이만큼(m) 들어온 자리에 놓는다. 2026-09-17 PD 「이동 가능한 범위 안에서 최대한 가장자리」 → 1.0 → 판이 칸 밖으로 안 나가는 최소값(판 반지름 2.56×배율 + 0.15 = 1.3)이 하한.")]
|
||||||
public float expansionPadInset = 1.0f;
|
public float expansionPadInset = 1.3f;
|
||||||
|
|
||||||
[Tooltip("가격판 자리(확장 판·현재 단계 발판)에 지형이 까는 모래 원 반지름(m) — 풀잎이 판을 덮지 않게(판은 깊이 검사로 캐릭터 아래에 그려진다). 0 = 안 깖.")]
|
[Tooltip("가격판 자리(확장 판·현재 단계 발판)에 지형이 까는 모래 원 반지름(m) — 풀잎이 판을 덮지 않게(판은 깊이 검사로 캐릭터 아래에 그려진다). 0 = 안 깖.")]
|
||||||
public float padClearRadius = 1.6f;
|
public float padClearRadius = 1.4f;
|
||||||
|
|
||||||
[Tooltip("판을 지면보다 이만큼(m) 띄운다(z-fighting 방지).")]
|
[Tooltip("판을 지면보다 이만큼(m) 띄운다(z-fighting 방지).")]
|
||||||
public float expansionPadLift = 0.06f;
|
public float expansionPadLift = 0.05f;
|
||||||
|
|
||||||
[Tooltip("옮긴 가격판의 트리거 한 변(m). 원본 십자(±5 m)는 가장자리로 옮긴 판끼리 겹쳐 두 구매가 동시에 시작돼 원본 활성화 경쟁 오류가 났다(2026-09-17). 0 = 원본 트리거 그대로.")]
|
[Tooltip("옮긴 가격판의 트리거 한 변(m). 원본 십자(±5 m)는 가장자리로 옮긴 판끼리 겹쳐 두 구매가 동시에 시작돼 원본 활성화 경쟁 오류가 났다(2026-09-17). 십자 방향 판(가장자리 중앙 · 서로 4.24 m)끼리 모서리에서 안 겹치려면 < 2×(4 − inset − 1.5) → 2.6. 0 = 원본 트리거 그대로.")]
|
||||||
public float expansionPadTriggerSize = 3.4f;
|
public float expansionPadTriggerSize = 2.6f;
|
||||||
|
|
||||||
[Tooltip("옮긴 확장 가격판의 크기 배율 — 2026-09-17 PD 「캠프파이어 발판과 같은 크기」 = platformScale 과 같은 0.5.")]
|
[Tooltip("옮긴 확장 가격판의 크기 배율 — 2026-09-17 PD 「캠프파이어 발판과 같은 크기」 = 0.5 → 「십자 방향 판끼리 겹치지 않게 간격·크기 조정」 0.45.")]
|
||||||
public float expansionPadScale = 0.5f;
|
public float expansionPadScale = 0.45f;
|
||||||
|
|
||||||
[Tooltip("1 = 가격판(프레임·코인 스프라이트·가격 글자)을 깊이 무시 셰이더로 그려 풀·소품에 가려지지 않게 한다(2026-09-17 PD). 0 = 원본 재질.")]
|
[Tooltip("판을 지형 법선에 맞춰 눕히는 최대 각도(°) — 해변 경사·둔덕에 묻히지 않게(2026-09-17 PD). 0 = 항상 수평.")]
|
||||||
|
public float expansionPadTiltMax = 20f;
|
||||||
|
|
||||||
|
[Tooltip("가격판 재질: 0 = 원본 · 1 = 프레임·코인 스프라이트를 깊이 검사+깊이 오프셋 셰이더(바닥에 붙고 캐릭터·소품이 앞이면 가려짐 · 2026-09-17 PD 「캐릭터를 가리지 않게 · 바닥에 붙은 느낌」) · 2 = 가격 글자까지 항상 위(옛 방식).")]
|
||||||
public int expansionPadOverlay = 1;
|
public int expansionPadOverlay = 1;
|
||||||
|
|
||||||
[Tooltip("한 줄 = 한 단계. 위에서부터 차례로 발판이 선다.")]
|
[Tooltip("한 줄 = 한 단계. 위에서부터 차례로 발판이 선다.")]
|
||||||
|
|
|
||||||
|
|
@ -20,19 +20,44 @@ namespace WL.Tools
|
||||||
public const string PrefKey = "WL_LeadAutoEnter";
|
public const string PrefKey = "WL_LeadAutoEnter";
|
||||||
public static string LastLog = "";
|
public static string LastLog = "";
|
||||||
public static bool Running;
|
public static bool Running;
|
||||||
|
/// <summary>로그인 완료(AllComplete) 뒤 인게임으로 넘기기까지 기다리는 시간(s) — 사람이 화면을 터치하는 간격.
|
||||||
|
/// 🔴 2026-09-17 실측: 완료 직후 바로 넘기면 원본 로그인 사슬의 **두 번째** 완료 응답이 타이틀 씬이 내려간 뒤 도착해
|
||||||
|
/// `TitleInfo.ServerComplete` 가 지워진 글자를 만져 MissingReferenceException 창이 떴다(재시도 0회에서도 재현).</summary>
|
||||||
|
public static float EnterDelay = 8f;
|
||||||
|
public static float AllCompleteAt = -1f;
|
||||||
|
|
||||||
|
// 🔴 2026-09-17 실측: 표식(Boot)으로 만든 인스턴스와 프로브가 직접 만든 인스턴스가 **둘 다** DevLogin 을 불러 로그인 사슬이 2개 →
|
||||||
|
// 두 번째 사슬의 토큰 저장이 씬 전환 뒤 실패해 「서버 에러 · 토큰 저장 실패」 창(원본 모달)이 인게임에 떴다. 인스턴스는 항상 하나만.
|
||||||
|
static WLLeadAutoEnter s_instance;
|
||||||
|
public static bool Exists { get { return s_instance != null; } }
|
||||||
|
|
||||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
||||||
static void Boot()
|
static void Boot()
|
||||||
{
|
{
|
||||||
if (PlayerPrefs.GetInt(PrefKey, 0) == 0) return;
|
if (PlayerPrefs.GetInt(PrefKey, 0) == 0) return;
|
||||||
PlayerPrefs.SetInt(PrefKey, 0); PlayerPrefs.Save(); // 1회 소비
|
PlayerPrefs.SetInt(PrefKey, 0); PlayerPrefs.Save(); // 1회 소비
|
||||||
|
Create();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>프로브가 직접 부를 때도 여기로 — 이미 있으면 만들지 않는다.</summary>
|
||||||
|
public static bool Create()
|
||||||
|
{
|
||||||
|
if (s_instance != null) return false;
|
||||||
var go = new GameObject("~WL_LeadAutoEnter");
|
var go = new GameObject("~WL_LeadAutoEnter");
|
||||||
go.hideFlags = HideFlags.DontSave;
|
go.hideFlags = HideFlags.DontSave;
|
||||||
DontDestroyOnLoad(go);
|
DontDestroyOnLoad(go);
|
||||||
go.AddComponent<WLLeadAutoEnter>();
|
s_instance = go.AddComponent<WLLeadAutoEnter>();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Start() { StartCoroutine(Run()); }
|
void Start()
|
||||||
|
{
|
||||||
|
if (s_instance != null && s_instance != this) { Destroy(gameObject); return; }
|
||||||
|
s_instance = this;
|
||||||
|
StartCoroutine(Run());
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnDestroy() { if (s_instance == this) s_instance = null; }
|
||||||
|
|
||||||
IEnumerator Run()
|
IEnumerator Run()
|
||||||
{
|
{
|
||||||
|
|
@ -51,8 +76,14 @@ namespace WL.Tools
|
||||||
bool all = fAll != null && (bool)fAll.GetValue(title);
|
bool all = fAll != null && (bool)fAll.GetValue(title);
|
||||||
if (all)
|
if (all)
|
||||||
{
|
{
|
||||||
|
if (AllCompleteAt < 0f)
|
||||||
|
{
|
||||||
|
AllCompleteAt = Time.realtimeSinceStartup;
|
||||||
|
LastLog = "로그인 완료(" + (AllCompleteAt - t0).ToString("F1") + " s) → " + EnterDelay.ToString("F0") + " s 뒤 진입";
|
||||||
|
}
|
||||||
|
if (Time.realtimeSinceStartup - AllCompleteAt < EnterDelay) { yield return new WaitForSeconds(0.5f); continue; }
|
||||||
title.OnClick_Screen();
|
title.OnClick_Screen();
|
||||||
LastLog = "화면 터치 → 인게임 로드(" + (Time.realtimeSinceStartup - t0).ToString("F1") + " s)";
|
LastLog = "로그인 완료 " + (AllCompleteAt - t0).ToString("F1") + " s · 화면 터치 → 인게임 로드(" + (Time.realtimeSinceStartup - t0).ToString("F1") + " s)";
|
||||||
yield return new WaitForSeconds(2f);
|
yield return new WaitForSeconds(2f);
|
||||||
if (SceneManager.GetActiveScene().name != "Title") break;
|
if (SceneManager.GetActiveScene().name != "Title") break;
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -61,9 +92,12 @@ namespace WL.Tools
|
||||||
bool active = fActive != null && (bool)fActive.GetValue(title);
|
bool active = fActive != null && (bool)fActive.GetValue(title);
|
||||||
int fail = fFail != null ? (int)fFail.GetValue(title) : 0;
|
int fail = fFail != null ? (int)fFail.GetValue(title) : 0;
|
||||||
// 🔴 실측: 스토어 정보 대기 중에는 activeButtons 가 오래 false 인데 OnClick_DevLogin 은 그와 무관하게 동작한다(이전 수동 진입과 동일)
|
// 🔴 실측: 스토어 정보 대기 중에는 activeButtons 가 오래 false 인데 OnClick_DevLogin 은 그와 무관하게 동작한다(이전 수동 진입과 동일)
|
||||||
// → 타이틀이 보이면 1 s 뒤 바로 호출하고, 완료가 안 되면 8 s 마다 다시 시도한다.
|
// → 타이틀이 보이면 1 s 뒤 바로 호출한다.
|
||||||
|
// 🔴 2026-09-17 실측: 8 s 마다 다시 부르면 서버 응답(ServerComplete)이 여러 번 오고, 마지막 응답이 인게임 전환 **뒤**에 도착해
|
||||||
|
// 원본 TitleInfo 가 지워진 타이틀 글자를 만져 MissingReferenceException 창이 떴다 → 재시도는 실패 표시가 있거나 90 s 무응답일 때만
|
||||||
|
// (실측: 개발 로그인 완료까지 40~45 s 걸린다 — 40 s 재시도도 같은 창을 띄웠다).
|
||||||
bool first = !loginCalled && Time.realtimeSinceStartup - t0 > 1f;
|
bool first = !loginCalled && Time.realtimeSinceStartup - t0 > 1f;
|
||||||
bool retry = loginCalled && (fail > 0 || Time.realtimeSinceStartup - lastLogin > 8f);
|
bool retry = loginCalled && (fail > 0 || Time.realtimeSinceStartup - lastLogin > 90f);
|
||||||
if (first || retry)
|
if (first || retry)
|
||||||
{
|
{
|
||||||
var input = fInput != null ? fInput.GetValue(title) as TMPro.TMP_InputField : null;
|
var input = fInput != null ? fInput.GetValue(title) as TMPro.TMP_InputField : null;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue