Project_WL/Assets/WL/Island/WLIslandSettings.cs

698 lines
44 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ─────────────────────────────────────────────────────────────────────────────
// WLIslandSettings.cs — 섬(FarmingIsland) × 던전(아레나 복사본) 왕복의 단일 출처(SOT · C45)
//
// PD 지시 #816 (2026-09-13) · 발주서 WL-816d §1 / §1-B + Lead 추가 지시 2건
// SOT = `공유/기획팀_산출물/WL/게임구조_2026-09-13/FarmingIsland_기반_전환설계_v1.md`(816c 실측)
//
// ■ 이 에셋 하나로 되돌린다(C8)
// enabled_ = 0 → 섬·던전 진입이 전부 사라지고 지금 게임 그대로(원본 1줄 훅도 -1 을 그대로 돌려준다).
// startOnIsland = 0 → 로그인 후 로비(WL_Nature)로 간다 = 현재 동작 100 %.
//
// ■ 던전을 늘리는 법 = **표 한 줄**
// dungeons[] 에 행 하나(씬 이름 · 맵 ID · 스테이지 인덱스 · 섬 위의 입구 좌표)를 추가하면
// 섬에 입구 더미가 하나 더 생기고, 그 문으로 들어가면 그 스테이지가 돈다. 코드 수정 0.
//
// 🔴 원본(Assets/Script/** · Assets/FarmingIsland/**)은 이 기능을 위해 고치지 않는다.
// 유일한 예외 = `NewGameUI.cs:36` **1줄**(발주서가 허용한 1줄 훅 상한) — StartMapId() 하나만 부른다.
// 🔴 Debug.LogError / Debug.LogException 을 쓰지 않는다(ErrorLogHookManager 가 플레이어 팝업을 띄운다).
// 🔴 어셈블리 주의: Assets/WL/Island/ 에 .asmdef 를 만들지 말 것.
// ─────────────────────────────────────────────────────────────────────────────
using System;
using UnityEngine;
namespace WL.Island
{
/// <summary>던전 1개 = 이 표의 한 행. 씬·스테이지·입구 위치가 전부 값이다.</summary>
[Serializable]
public class WLDungeonDef
{
[Tooltip("0 이면 이 행은 없는 것으로 친다(입구도 안 생긴다).")]
public int enabled_ = 1;
[Tooltip("던전 번호(로그·이름표용).")]
public int id = 1;
[Tooltip("입구 위에 뜨는 이름.")]
public string displayName = "던전 1";
[Tooltip("던전 씬 이름(빌드 씬 목록에 등록돼 있어야 한다). 예: WL_Dungeon01")]
public string sceneName = "WL_Dungeon01";
[Tooltip("BattleMapConfig.json 의 n_MapID — Load_Map(이 값) 으로 들어간다.")]
public int mapId = 901;
[Tooltip("WLStageTable 의 표 인덱스(0부터). 던전마다 달라야 보상·몹 수가 갈린다.")]
public int stageIndex = 10;
[Tooltip("섬 위 입구 더미의 월드 좌표.")]
public Vector3 gatePosition = Vector3.zero;
[Tooltip("입구 더미의 Y 회전(도).")]
public float gateYaw;
[Tooltip("입구 더미 크기 배수.")]
public float gateScale = 1f;
[Tooltip("입구 더미 색(더미라서 색으로만 구분한다 · 추후 프리팹으로 교체).")]
public Color gateColor = new Color(0.45f, 0.32f, 0.62f, 1f);
[Tooltip("비우면 내장 더미(문틀+포털판)를 만든다. 채우면 그 프리팹을 대신 놓는다(추후 교체용).")]
public string gatePrefabPath = "";
// ── WL-816g: 이 던전을 여는 「골드 납품 발판」 (한 던전당 한 자리 · 지불하면 그 자리는 영영 비워진다)
[Tooltip("1 이면 이 던전의 발판을 **지금 밭이 있는 자리**에 놓는다(첫 던전용). 0 이면 아래 좌표.")]
public int platformUseFarm;
[Tooltip("이 던전을 여는 발판의 월드 좌표(높이는 지면으로 자동 보정한다).")]
public Vector3 platformPosition = Vector3.zero;
}
/// <summary>
/// WL-816zc — 「섬 초기 플로우」 한 단계 = 이 표의 한 행.
/// 발판 1개가 이 행의 값(문구·가격·자리)으로 서고, 값을 치르면 이 행의 설치물이 그 자리에 남는다.
/// 코드 수정 0 — 순서·가격·설치물은 전부 여기 값이다(C45).
/// </summary>
[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
{
public const string ResourcePath = "WL/WLIslandSettings";
static WLIslandSettings s_inst;
static bool s_tried;
/// <summary>설정 에셋(없으면 null = 기능 전체 무동작).</summary>
public static WLIslandSettings Instance
{
get
{
if (!s_tried) { s_inst = Resources.Load<WLIslandSettings>(ResourcePath); s_tried = true; }
return s_inst;
}
}
public static void Invalidate() { s_inst = null; s_tried = false; }
/// <summary>기능이 살아 있는가.</summary>
public static bool Enabled { get { var c = Instance; return c != null && c.enabled_ != 0; } }
// ───────────────────────────────── 전체 스위치
[Header("되돌리기 (PD 1순위)")]
[Tooltip("0 이면 섬·던전이 전부 사라진다 = 지금 게임 그대로.")]
public int enabled_ = 1;
[Tooltip("1 이면 진행 상황을 Console 에 남긴다.")]
public int verboseLog = 1;
// ───────────────────────────────── 섬
[Header("섬 (FarmingIsland Level01)")]
[Tooltip("1 이면 로그인 후 로비 대신 섬에서 시작한다(PD 2026-09-13). 0 이면 지금처럼 로비.")]
public int startOnIsland = 1;
[Tooltip("BattleMapConfig.json 에 넣은 섬 행의 n_MapID.")]
public int islandMapId = 900;
[Tooltip("섬 씬 이름(BattleMapConfig 의 s_Scene 과 같아야 한다).")]
public string islandSceneName = "Level01";
[Tooltip("섬에서 WL 전투 PC(Actor)를 숨긴다 — 섬은 FI 플레이어가 걷는다(플레이어 2명 방지).")]
public int hideWlPcOnIsland = 1;
[Tooltip("섬 씬의 FI 카메라/AudioListener 중복을 정리한다(Additive 로 붙었을 때만).")]
public int fixIslandDuplicates = 1;
[Tooltip("섬에서 어느 카메라가 화면을 그릴지. 0 = 아무것도 안 건드림(카메라 2대 = 겹쳐 그려짐) · " +
"1 = FI 카메라가 이긴다(섬을 FarmingIsland 처럼 · 기본) · 2 = WL 카메라가 이긴다. " +
"🔴 Additive 경로는 로그인이 필요해 실측하지 못했다 — 실기 첫 실행에서 확인할 값.")]
public int islandCameraOwner = 1;
[Tooltip("WL 전투 PC 는 Addressables 로 비동기 생성된다 — 이 초 동안 지켜보며 나타나면 숨긴다.")]
public float hidePcWatchSeconds = 6f;
[Tooltip("FI 세이브 파일 이름을 이 값으로 고정한다(빈 문자열 = 원본 그대로 = 활성 씬 이름). " +
"816c §A-3: 섹터마다 세이브가 갈리는 문제의 최소 수정 — 다만 원본 0줄 원칙 때문에 이번엔 기본 빈칸(미적용).")]
public string islandSaveKey = "";
// ───────────────────────────────── 섬의 플레이어 = 우리 전투 캐릭터
[Header("섬 캐릭터 교체 (PD: 전투 캐릭터로 통일)")]
[Tooltip("1 이면 FI 농부(Stickman) 대신 우리 치비 캐릭터가 섬을 걷는다. 조작·상호작용은 FI 것 그대로.")]
public int swapIslandPlayer = 1;
[Tooltip("섬에서 쓸 PC 프리팹 경로(Addressables 주소 겸 에디터 경로).")]
public string pcPrefabPath = "Assets/Res_Addr/PC/LH_M05.prefab";
[Tooltip("치비 루트에 넣을 스케일. 프리팹의 WLPcScaleCompensator 가 ×1.5038 해서 " +
"0.7 × 1.5038 = 1.0527 → 월드 높이 1.1912 m(= 인게임 PC 와 동일 · 814n 실측).")]
public float islandPcScale = 0.7f;
[Tooltip("1 이면 FI 애니메이터의 파라미터를 치비 애니메이터로 그대로 흘린다(둘 다 Humanoid · 리타깃). " +
"FI 컨트롤러를 그대로 쓰므로 걷기·농사 동작이 전부 따라온다.")]
public int mirrorFiAnimator = 1;
[Tooltip("FI 농부 메시를 숨길 때 쓰는 자식 이름(대소문자 무시 · 비우면 SkinnedMeshRenderer 를 전부 끈다).")]
public string fiPlayerModelName = "Stickman";
// ───────────────────────────────── 던전 입구
[Header("던전 입구(더미)")]
[Tooltip("플레이어가 이 거리(m) 안에 들어오면 「들어가기」가 뜬다.")]
public float interactRadius = 3.5f;
[Tooltip("안내 문구 — {0} = 던전 이름.")]
public string promptFormat = "{0}\n들어가기";
[Tooltip("버튼 문구.")]
public string enterButtonText = "들어가기";
[Tooltip("키보드로도 들어갈 수 있게(에디터 검증용).")]
public KeyCode enterKey = KeyCode.E;
[Tooltip("입구 더미 기본 높이(m).")]
public float gateHeight = 3.2f;
[Tooltip("입구 더미 기본 폭(m).")]
public float gateWidth = 2.4f;
[Tooltip("이름표·버튼에 쓸 TMP 폰트(한글 포함). 비우면 TMP 기본 폰트 — 기본 폰트에는 한글 글리프가 없어 □ 로 나온다.")]
public string fontAssetPath = "Assets/ThirdParty/TextMesh Pro/Addressables/Fonts & Materials/Font SDF.asset";
// ───────────────────────────────── 보상
[Header("보상 (던전 클리어 → 섬 코인)")]
[Tooltip("0 이면 보상을 주지 않는다.")]
public int rewardEnabled = 1;
[Tooltip("처치 1마리당 코인.")]
public int goldPerKill = 12;
[Tooltip("클리어 기본 코인.")]
public int goldClearBonus = 60;
[Tooltip("보스 스테이지 추가 코인.")]
public int goldBossBonus = 120;
[Tooltip("1 이면 위 합계에 StageDef.goldMultiplier(WLStageTable.cs:103)를 곱한다.")]
public int applyGoldMultiplier = 1;
[Tooltip("실패(사망·시간 초과) 시 주는 코인. PD 지시 = 0.")]
public int goldOnFail = 0;
[Tooltip("보류 보상을 남기는 PlayerPrefs 키(플레이 중 씬 전환을 건너 살아남게).")]
public string pendingPrefsKey = "WL_Island_PendingGold";
// ───────────────────────────────── 던전 씬 런타임 준비
[Header("던전 씬 준비(런타임)")]
[Tooltip("1 이면 던전 씬에 NavMesh 를 런타임 베이크한다(아레나 복사본에는 베이크 데이터가 없다).")]
public int bakeDungeonNavMesh = 1;
[Tooltip("1 이면 스테이지 표가 요구하는 SpawnerID 로 스포너를 런타임 생성한다.")]
public int spawnDungeonSpawners = 1;
[Tooltip("생성한 스포너의 MobGenRange(m).")]
public float dungeonSpawnerRange = 6f;
[Tooltip("1 이면 아레나 복사본의 데모 리그(ArenaWalkerRig)·여분 카메라를 끈다(플레이어·카메라 중복 방지).")]
public int fixDungeonDuplicates = 1;
[Tooltip("던전 입장 후 StageDirector.Enter 를 부르기까지 기다리는 초(맵·PC 로딩 여유).")]
public float dungeonEnterDelay = 1.5f;
// ───────────────────────────────── 초기 상태 · 골드 납품 발판 (WL-816g · PD 2026-09-13)
[Header("초기 상태 — 밭·던전 숨김 + 발판 1개 (WL-816g)")]
[Tooltip("0 이면 **지금 상태로 100 % 복귀**한다 — 밭도 던전도 처음부터 전부 보인다(C8 되돌리기).")]
public int gateFlowEnabled = 1;
[Tooltip("1 이면 작물을 심는 밭(Farm·Soil)을 화면에서 숨긴다. " +
"🔴 오브젝트를 끄지 않고 **렌더러·콜라이더만** 끈다 — 끄면 섬 확장 때 FI 가 " +
"꺼진 오브젝트에서 코루틴을 시작해 콘솔 에러가 난다.")]
public int hideFarms = 1;
[Tooltip("섬이 나중에 열렸을 때 그 밭을 다시 숨기고 타일 재질을 맞추기까지 기다리는 초 " +
"(확장 연출 1초 + 흙칸 연출 + 룩 재적용 1.4초가 끝날 시간).")]
public float farmReHideDelaySeconds = 3.0f;
[Tooltip("🔴 WL-816w ③ — 1 이면 밭을 숨길 때 **흙칸(Soil) 렌더러는 남긴다** = 밭 자리가 " +
"초록 풀밭이 아니라 **흙**으로 보인다(PD 지시). 콜라이더는 그대로 꺼지므로 " +
"농사·상호작용은 계속 비활성이다. 0 = 816g 동작(흙칸까지 통째로 숨김).")]
public int showFarmSoil = 1;
[Tooltip("🔴 WL-816w ② — 1 이면 FI 섬 타일의 장식 **울타리**(Island/Fence*) 렌더러를 끈다. " +
"인게임이 아닌 섬에 말뚝 울타리가 서 있는 것을 PD 가 지적했다. " +
"밭(Farm) 자식이 아니라 **섬 타일 자식**이라 816g 밭 숨김이 훑지 못했다(실측).")]
public int hideIslandFences = 1;
[Tooltip("울타리로 판정할 오브젝트 이름 접두어(대소문자 무시).")]
public string[] islandFenceNamePrefixes = new[] { "Fence" };
[Tooltip("2026-09-15 PD 「흙 지역도 라운딩」 — 밭 사각형 모서리에서 이 거리(m · 맨해튼 dx+dz) 안의 흙칸은 숨긴다. 0 = 무동작(사각형 그대로).")]
public float soilCornerRadius = 1.6f;
[Tooltip("🔴 2026-09-15 Lead 실측 — 남은 울타리는 별도 오브젝트가 아니라 **FI 타일 메시에 구워진 난간**이다. 1 이면 타일 메시를 복제해 윗면 위로 솟은 삼각형을 잘라낸다(원본 메시 무수정 · WLIslandTileFenceStrip). 0 = 원본 메시 그대로.")]
public int stripBakedFences = 1;
[Tooltip("이 높이(m · 타일 로컬 y · 윗면 = 0)보다 위에 정점이 하나라도 있는 삼각형을 난간으로 보고 버린다.")]
public float fenceStripMinY = 0.15f;
[Tooltip("🔴 §1 — 1 이면 **새로 열린 타일**의 머티리얼 배열을 이미 열려 있는 타일과 **같게** 만든다. " +
"실측: 바닥 색 자체는 이미 같지만(main b1e58d795), 새 타일만 외곽선 패스(WL/HullOutline)를 " +
"못 받아 경계에서 테두리 유무가 갈린다. 새 머티리얼을 만들지 않고 기존 배열을 물려준다.")]
public int matchNewTileMaterials = 1;
[Tooltip("해금된 던전 수를 적어 두는 PlayerPrefs 키(FI 세이브 파일은 건드리지 않는다).")]
public string dungeonUnlockPrefsKey = "WL_Island_DungeonsUnlocked";
[Tooltip("던전을 1개·2개·3개…로 늘릴 때 내야 하는 골드(PD 「2개+@」). " +
"표보다 더 많이 열면 마지막 값을 이어 쓴다.")]
public int[] dungeonUnlockPrices = new int[] { 10, 30, 60, 100 };
[Tooltip("이 섬에서 열 수 있는 던전 수 상한(0 = dungeons 표 전체).")]
public int maxDungeons = 4;
[Header("골드 납품 발판(더미)")]
[Tooltip("1 이면 **지금 밭이 있는 자리**에 발판을 놓는다. 0 이면 아래 좌표를 쓴다.")]
public int platformUseFarmPosition = 1;
[Tooltip("platformUseFarmPosition = 0 일 때 쓸 월드 좌표.")]
public Vector3 platformPosition = Vector3.zero;
[Tooltip("밭 자리에서 얼마나 옮길지(미세 조정용).")]
public Vector3 platformOffset = Vector3.zero;
[Tooltip("비우면 **FI 원본 가격판(Purchaser 프리팹)만** 놓는다(PD 2026-09-13 정정 · 새 모델 금지). " +
"채우면 그 프리팹이 대신 놓인다 — 추후 교체는 이 값 하나.")]
public string platformPrefabPath = "";
[Tooltip("🔴 기본 0 — 1 로 켜면 FI 가격판 아래에 얇은 원판(발 디딜 자리)을 하나 더 깐다. " +
"FI 가격판이 **바닥에 눕는 판**이라 두꺼운 원판을 깔면 그 판을 덮는다(816g 실측).")]
public int platformShowPad = 0;
[Tooltip("1 이면 FI 가격판 전체의 렌더 범위를 재서 **밭 한가운데 · 바닥 바로 위**로 맞춘다. " +
"원본 프리팹은 물 위(y≈-0.5)에 눕도록 만들어져 있어 섬 위에 그대로 놓으면 지면 아래로 들어간다(816g 실측).")]
public int platformCenterOnPlate = 1;
[Tooltip("가격판을 바닥에서 얼마나 띄울지(m). 너무 낮으면 지면과 z-파이팅이 난다.")]
public float platformPlateLift = 0.03f;
[Tooltip("1 이면 「무엇이 열리는지」 알리는 글자를 발판 위에 띄운다(모델이 아니라 글자). 0 = FI 원본 모습 100 %.")]
public int platformShowLabel = 1;
[Tooltip("platformShowPad = 1 일 때 원판 반지름(m).")]
public float platformRadius = 1.7f;
[Tooltip("발판 색(더미).")]
public Color platformColor = new Color(0.92f, 0.72f, 0.24f, 1f);
[Tooltip("1 이면 아레나와 같은 Toon 셰이더를 먼저 찾고, 없으면 URP/Lit 로 만든다. " +
"🔴 0 이면 던전 입구 더미와 같은 URP/Lit 만 쓴다(안 보이면 0 으로 되돌릴 것).")]
public int platformUseToon = 1;
[Tooltip("안내 글 — {0} = 지금 내야 하는 골드. 🔴 2026-09-16 PD 「발판 글자에서 비용은 빼라(바닥 가격판이 이미 보여 준다)」 → 가격 없이 이름만.")]
public string platformLabelFormat = "던전 열기";
[Tooltip("안내 글 높이(m).")]
public float platformLabelHeight = 2.2f;
[Tooltip("1 = 글자를 바닥 판과 90° 로 **세워서** 카메라(플레이어 정면) 쪽 방향만 따라 돈다(기울이지 않음 · 2026-09-16 PD). " +
"0 = 예전처럼 카메라를 정면으로 바라보게 기울인다.")]
public int platformLabelUpright = 1;
[Tooltip("발판 트리거 한 변(m) — FI Purchaser 원본은 섬 1칸(8 m)을 덮는 十자라 발판 크기로 줄인다.")]
public float platformTriggerSize = 3.4f;
[Tooltip("발판 트리거 높이(m).")]
public float platformTriggerHeight = 3f;
[Tooltip("지불 완료 뒤 발판을 갈아 끼우기까지 기다리는 초(물보라·해금 연출이 끝날 시간).")]
public float platformRespawnDelaySeconds = 1.3f;
[Tooltip("1 이면 발판 자리를 지면 높이에 맞춘다(위에서 아래로 레이).")]
public int platformSnapToGround = 1;
// ── WL-816u (2026-09-15 PD) 「열림 발판의 크기는 열림 구역으로 설정 된 위치의 정중앙에 배치하고,
// 프레임과 글자 아이콘 크기 모두 50%로 줄여줘 (현재는 너무 커)」
[Tooltip("🔴 WL-816u — 발판(FI 가격판 프레임 + 가격 글자 + 코인 아이콘)의 인스턴스 스케일 배수. " +
"PD 「50 %」 = 0.5. **되돌리기 = 1**(= 816t 까지의 크기). 프리팹 원본은 한 글자도 고치지 않는다. " +
"트리거 한 변(platformTriggerSize)은 이 값과 무관하게 **월드 미터 그대로** 유지된다(스케일 역보정).")]
public float platformScale = 0.5f;
[Tooltip("🔴 WL-816u — 1 이면 발판을 「열림 구역」 **전체**의 XZ 정중앙에 놓는다. " +
"열림 구역 = 이 발판이 여는 자리(던전 1 = 숨겨 둔 밭 구역 전부 · 그 외 = 표의 platformPosition). " +
"0 이면 816g 동작(= 처음 만난 밭 **하나**의 중심).")]
public int platformCenterOnZone = 1;
[Tooltip("🔴 WL-816u — 1 이면 「보이는 프레임의 중심」을 맞춘다: 파티클·트레일·꺼진 렌더러를 " +
"중심 계산에서 제외한다(FI Purchaser 프리팹에는 물보라 파티클이 들어 있어 전부 세면 중심이 밀린다). " +
"0 이면 816g 동작(= 모든 렌더러 바운즈).")]
public int platformCenterVisibleOnly = 1;
[Tooltip("1 이면 새 발판은 플레이어가 그 자리에서 **내려온 뒤에** 생긴다 — " +
"골드가 많을 때 가만히 서서 던전이 연달아 팔리는 것을 막는다(816g 실측).")]
public int platformRequireStepOff = 1;
[Tooltip("위 대기의 상한(초). 넘으면 그냥 놓는다.")]
public float platformStepOffTimeoutSeconds = 120f;
// ── WL-816y (2026-09-15 PD) 「처음 골드로 지역을 개방하는 위치에 "캠프파이어 설치"로 명명하고,
// "Fireplace_01" 캠프파이어를 중앙에, 가장자리에 "Tent_01" 텐트를 배치해 캠프를 차린 것처럼」
[Header("캠프 장식 (WL-816y · 첫 개방 자리)")]
[Tooltip("🔴 WL-816y — 1 이면 첫 자리(던전 1 발판 자리)를 열었을 때 캠프 장식을 놓는다. " +
"**되돌리기 = 0**. 장식은 콜라이더를 전부 끈다(이동 방해 0 · NavMesh 재베이크 없음).")]
public int campDecorEnabled = 1;
[Tooltip("구역 중앙에 놓을 모닥불 프리팹 경로(비우면 모닥불 없음). " +
"🔴 FantasyForest 원본은 Built-in(Standard) 셰이더라 URP 에서 분홍으로 나온다 — " +
"원본을 고치지 않고 WL 쪽 URP 사본 머티리얼을 물린 래퍼 프리팹을 쓴다(WL-816y).")]
public string campFirePrefabPath = "Assets/WL/Island/Prefabs/WL_Camp_Fireplace.prefab";
[Tooltip("구역 가장자리에 놓을 텐트 프리팹 경로(비우면 텐트 없음 · 위와 같은 이유로 WL 래퍼).")]
public string campTentPrefabPath = "Assets/WL/Island/Prefabs/WL_Camp_Tent.prefab";
[Tooltip("텐트 개수(PD: 2개).")]
public int campTentCount = 2;
[Tooltip("모닥불 중심에서 텐트까지의 거리(m). campTentUseZone = 1 이면 구역 반경이 우선한다.")]
public float campTentRadius = 2.6f;
[Tooltip("1 이면 텐트 거리를 「열림 구역」 바운즈의 짧은 쪽 반지름 × campTentZoneScale 로 잡는다(= 가장자리).")]
public int campTentUseZone = 1;
[Tooltip("위 구역 반지름에 곱하는 값(1 = 구역 경계선 · 0.85 = 살짝 안쪽).")]
public float campTentZoneScale = 0.85f;
[Tooltip("첫 텐트의 방위각(도 · 0 = +Z). 나머지는 360/개수 만큼 돌아가며 선다.")]
public float campTentStartYaw = 35f;
[Tooltip("텐트가 불을 바라보는 각도 보정(도). 텐트 프리팹의 정면이 -Z 면 180 을 넣는다.")]
public float campTentFaceYawOffset;
[Tooltip("모닥불 인스턴스 스케일 배수(원본 지름 ≈ 0.9 m · 섬 캐릭터 1.19 m 기준).")]
public float campFireScale = 1.2f;
[Tooltip("텐트 인스턴스 스케일 배수(원본 0.78 × 0.69 × 1.09 m — 캐릭터 1.19 m 옆에서는 작다 · 2.0 ≈ 높이 1.38 m).")]
public float campTentScale = 1.6f;
[Tooltip("1 이면 장식 하나하나를 위에서 아래로 쏘아 지면 높이에 맞춘다.")]
public int campDecorSnapToGround = 1;
[Header("시작 골드 (PD: 초기 10 골드)")]
[Tooltip("1 이면 **새 게임**의 시작 골드를 아래 값으로 바꾼다(FI 기본값 500 · IslandManager.cs:30).")]
public int applyStartCoin = 1;
[Tooltip("새 게임 시작 골드.")]
public int startCoin = 10;
[Tooltip("이 문자열이 바뀌면 섬 세이브를 **한 번** 지우고 처음부터 시작한다(해금 수도 0 으로). " +
"비우면 아무것도 지우지 않는다. PD 에게 새 상태를 보여 줄 때 값을 바꾼다.")]
public string saveResetToken = "816g-1";
[Tooltip("🔴 1 이면 **게임을 시작할 때마다**(Play/앱 실행 1회당 1번 · 던전에서 섬으로 돌아올 때는 아님) 섬 세이브·던전 해금·보류 보상을 " +
"전부 지우고 시작 골드부터 다시 시작한다 — 2026-09-15 PD 「내가 얘기하기 전까지는 게임 시작 시 매번 초기화된 상태에서 시작」" +
"(초반 Flow 구성 예정). 0 = 세이브 유지(토큰 1회 초기화만).")]
public int resetSaveEveryStart = 1;
// ───────────────────────────────── WL-816i · 카메라 구도 고정
// PD 「카메라 구도는 섬에서 조작할 때의 각도와 뷰를 말한거야. 전장에 진입해도 바뀌지 않아야 하기 때문」
// → 기준은 **섬**. 아래 기본값은 섬(FI) 카메라 실측값 그대로다.
// Level01.unity `PlayerVirtualCamera` — 회전 오일러 (45,0,0) · FramingTransposer m_CameraDistance 22
// · 렌즈 FOV 60 · 원근(orthographic 0) · Near 0.1 / Far 500 · ScreenX/Y 0.5(대상 화면 중앙)
[Header("카메라 구도 (WL-816i · 섬 기준을 던전에 그대로)")]
[Tooltip("1 이면 던전(전장)의 카메라를 아래 **섬 구도** 값으로 맞춘다. 0 이면 던전은 힘민지 기본 전투 카메라 그대로(= 816g 까지의 동작).")]
public int dungeonCameraMatchIsland = 1;
[Tooltip("섬 카메라 내려보는 각(도). 섬 실측 = 45.")]
public float camPitch = 45f;
[Tooltip("섬 카메라 수평 방향(도). 섬은 플레이어를 따라 돌지 않는 고정 구도라 0.")]
public float camYaw = 0f;
[Tooltip("섬 카메라와 대상 사이 거리(m). 섬 실측 = 22 (CinemachineFramingTransposer.m_CameraDistance). " +
"🔴 섬은 이 값을 쓰지 않는다(FI Cinemachine 이 스스로 잡는다) — 기준 기록용이자 아래 던전 값의 기본값이다.")]
public float camDistance = 22f;
[Tooltip("🔴 **섬 전용** 카메라 거리(m) — 2026-09-15 PD 「섬과 던전 모두 좀 더 가까이」. FI `PlayerVirtualCamera` 의 " +
"CinemachineFramingTransposer.m_CameraDistance(씬 값 22)를 섬 로드 때 이 값으로 덮는다(FI 씬·코드 0줄). " +
"0 이하면 무동작(= 씬 값 22 그대로).")]
public float islandCamDistance = 15f;
[Tooltip("위 섬 거리를 적용할 FI 가상 카메라 이름. 비우면 섬 씬의 FramingTransposer 전부.")]
public string islandVcamName = "PlayerVirtualCamera";
[Tooltip("섬 카메라 내려다보는 각도(도). 씬 값 45 · 데모 캡처는 거의 수직(≈90). 0 이하면 무동작(= 씬 값 45 그대로). " +
"2026-09-15 PD 「60도로 올려서 비교」.")]
public float islandCamPitch = 0f;
[Tooltip("🔴 **던전 전용** 카메라 거리(m) — WL-816i2 · PD 「던전에서는 카메라를 조금만 더 당기자」. " +
"0 이하면 위 섬 값(22)을 그대로 쓴다. 각도 45°·yaw 0°·FOV 60 은 섬과 같게 유지하고 **거리만** 줄인다. " +
"섬은 이 값에 전혀 영향받지 않는다.")]
public float dungeonCamDistance = 18f;
[Tooltip("섬 카메라 화각(도). 섬 실측 = 60 · 원근 투영.")]
public float camFov = 60f;
[Tooltip("카메라가 바라보는 점의 높이(대상 발밑 기준 m). 섬은 대상 원점을 화면 중앙에 두므로 0.")]
public float camLookHeight = 0f;
[Tooltip("1 이면 던전에서 카메라를 드래그로 돌릴 수 없다(섬과 같은 고정 구도). 0 이면 회전 허용.")]
public int camLockYaw = 1;
[Tooltip("던전에서 「이동 방향 리드」(#755 · CameraLeadSettings)를 얼마나 쓸지. " +
"1 = 지금 그대로(리드 살아 있음 · 움직이면 화면 중심이 최대 1.5 m 흔들린다) · " +
"0 = 리드 끔(섬과 완전히 같은 고정 구도). 음수면 만지지 않는다. " +
"🔴 후진 피치(backwardPitchAdd 6°)는 이 값으로 꺼지지 않는다 — 그건 CameraLeadSettings 소관.")]
public float camLeadMultiplier = 1f;
[Tooltip("1 이면 투영이 깨진 카메라(near==far · 직교 size ≤ 0 · aspect ≤ 0)를 찾아 되살린다. " +
"이 상태의 카메라에 ScreenToWorldPoint 를 쓰면 유니티가 " +
"\"Screen position out of view frustum\" 에러를 낸다(WL-816i 실측). 0 이면 감시도 수리도 안 한다.")]
public int fixDegenerateCameras = 1;
[Tooltip("위 감시 주기(초).")]
public float cameraGuardInterval = 0.5f;
[Tooltip("1 이면 `RealCamera.list_tfCamactionTarget` 에 남은 **죽은 연출 대상**을 걷어낸다(WL-816i2). " +
"이게 남아 있으면 재입장 때 RealCamera.LateUpdate 가 매 프레임 예외로 끊겨 " +
"`Start_Game()`(= ActorInfo.All_Stop(false) · DungeonInfo.Start_Dungeon)이 불리지 않아 **전투가 시작되지 않는다**. " +
"0 이면 무동작(= 816i 까지의 동작).")]
public int fixCamActionTargets = 1;
[Tooltip("1 이면 던전 입장 연출에서 카메라가 **보간 없이 곧바로 최종 구도**에 놓인다(WL-816l · PD 「휙휙 바뀐다」). " +
"원본은 ⓐ 입장 프레임에 위치만 순간이동하고 회전은 초당 2.5 비율로만 돌려서 그 한 프레임이 엉뚱한 방향을 비추고 " +
"ⓑ 다음 프레임 `transform.LookAt` 이 회전을 튕기듯 맞춘다(RealCamera.cs:199·283 실측). " +
"0 이면 무동작(= 816i2 까지의 동작).")]
public int dungeonCamInstantSettle = 1;
[Tooltip("추적 대상이 한 프레임에 이 거리(m) 이상 움직이면 **순간이동**으로 보고 카메라를 즉시 따라 붙인다. " +
"던전 입장의 `StageDirector.Enter → WarpPlayerToAnchor`(앵커 뒤 startRadius m)가 여기 걸린다. " +
"걸어서는 한 프레임에 이만큼 못 움직이므로 평소 추적(부드러운 보간)에는 영향이 없다. 0 이하 = 끔.")]
public float dungeonCamTeleportSnap = 1f;
[Tooltip("1 이면 던전 로딩 화면을 **카메라가 최종 구도에 앉고 스테이지가 입장할 때까지** 닫지 않는다(WL-816l). " +
"0 이면 씬이 붙자마자 칸을 채운다(= 816g 동작) — 그러면 카메라가 자리를 잡기 전에 화면이 열려 튐이 그대로 보인다.")]
public int dungeonLoadingWaitForCamera = 1;
[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} = 가격. 🔴 2026-09-16 PD 「비용(예: 60 골드)은 빼라 — 바닥 가격판이 이미 보여 준다」 → 「{0}」 만.")]
public string flowLabelFormat = "{0}";
[Tooltip("1 = 설치물(좌판·나무 등)이 생길 때 그 범위 안에 플레이어가 있으면 범위 밖으로 밀어낸다(2026-09-16 PD 「가판대에 캐릭터가 끼어버림」). 0 = 안 밀어냄.")]
public int installPushPlayer = 1;
[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) 들어온 자리에 놓는다. 2026-09-17 PD 「이동 가능한 범위 안에서 최대한 가장자리」 → 1.0 → 판이 칸 밖으로 안 나가는 최소값(판 반지름 2.56×배율 + 0.15 = 1.3)이 하한.")]
public float expansionPadInset = 1.3f;
[Tooltip("가격판 자리(확장 판·현재 단계 발판)에 지형이 까는 모래 원 반지름(m) — 풀잎이 판을 덮지 않게(판은 깊이 검사로 캐릭터 아래에 그려진다). 0 = 안 깖.")]
public float padClearRadius = 1.4f;
[Tooltip("판을 지면보다 이만큼(m) 띄운다(z-fighting 방지).")]
public float expansionPadLift = 0.05f;
[Tooltip("옮긴 가격판의 트리거 한 변(m). 원본 십자(±5 m)는 가장자리로 옮긴 판끼리 겹쳐 두 구매가 동시에 시작돼 원본 활성화 경쟁 오류가 났다(2026-09-17). 십자 방향 판(가장자리 중앙 · 서로 4.24 m)끼리 모서리에서 안 겹치려면 < 2×(4 inset 1.5) → 2.6. 0 = 원본 트리거 그대로.")]
public float expansionPadTriggerSize = 2.6f;
[Tooltip("옮긴 확장 가격판의 크기 배율 — 2026-09-17 PD 「캠프파이어 발판과 같은 크기」 = 0.5 → 「십자 방향 판끼리 겹치지 않게 간격·크기 조정」 0.45.")]
public float expansionPadScale = 0.45f;
[Tooltip("판을 지형 법선에 맞춰 눕히는 최대 각도(°) — 해변 경사·둔덕에 묻히지 않게(2026-09-17 PD). 0 = 항상 수평.")]
public float expansionPadTiltMax = 20f;
[Tooltip("가격판 재질: 0 = 원본 · 1 = 프레임·코인 스프라이트를 깊이 검사+깊이 오프셋 셰이더(바닥에 붙고 캐릭터·소품이 앞이면 가려짐 · 2026-09-17 PD 「캐릭터를 가리지 않게 · 바닥에 붙은 느낌」) · 2 = 가격 글자까지 항상 위(옛 방식).")]
public int expansionPadOverlay = 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",
scale = 0.6f, // 2026-09-16 PD 「상인 프랍 크기 50 %」 → 「20 % 더 키워」 = 0.6
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,
keepCollider = 1, // 2026-09-15 PD — 좌판 트리거가 있어야 판매가 된다
},
new WLFlowStepDef
{
label = "사과나무 설치", price = 60,
platformFromDungeonRow = 2, // 기존 발판 #3 좌표
kind = "prefab",
prefabPath = "Assets/FarmingIsland/Prefabs/FruitTrees/Tree_Apple.prefab",
keepCollider = 1, // 2026-09-15 PD 「과일 나무 수집이 안 된다」 — 콜라이더를 지우면 FI 수확 트리거가 안 걸린다
},
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("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";
[Tooltip("확장 가격판 재숨김 주기(초) — FI 가 나중에 만들어 내는 가격판까지 잡는다.")]
public float expansionHideInterval = 0.5f;
[Tooltip("위 재숨김을 계속하는 시간(초). 0 이면 처음 1회만 숨긴다.")]
public float expansionHideSeconds = 20f;
// ───────────────────────────────── 던전 표
[Header("던전 표 — 한 줄 = 던전 하나")]
public WLDungeonDef[] dungeons = new WLDungeonDef[0];
// ─────────────────────────────────────────────────────────────────
// 원본 1줄 훅이 부르는 유일한 진입점 (NewGameUI.cs:36)
// ─────────────────────────────────────────────────────────────────
/// <summary>
/// 로그인 후 처음 들어갈 맵 ID. 기능이 꺼져 있으면 <paramref name="original"/>(= -1 로비)를 그대로 돌려준다.
/// 🔴 원본 수정은 이 호출 **1줄**이 전부다.
/// </summary>
public static int StartMapId(int original)
{
var c = Instance;
if (c == null || c.enabled_ == 0 || c.startOnIsland == 0) return original;
return c.islandMapId;
}
/// <summary>맵 ID 로 던전 행을 찾는다(없으면 null).</summary>
public WLDungeonDef FindByMapId(int mapId)
{
if (dungeons == null) return null;
for (int i = 0; i < dungeons.Length; i++)
if (dungeons[i] != null && dungeons[i].enabled_ != 0 && dungeons[i].mapId == mapId) return dungeons[i];
return null;
}
/// <summary>던전 표의 **줄 번호**로 그 행을 그대로 준다(enabled_ 와 무관 — 좌표만 빌려 쓸 때 쓴다 · WL-816zc).</summary>
public WLDungeonDef DungeonRow(int rawIndex)
{
if (dungeons == null || rawIndex < 0 || rawIndex >= dungeons.Length) return null;
return dungeons[rawIndex];
}
/// <summary>씬 이름으로 던전 행을 찾는다(같은 씬을 여러 던전이 써도 되므로 맵 ID 우선).</summary>
public WLDungeonDef FindBySceneName(string sceneName)
{
if (dungeons == null || string.IsNullOrEmpty(sceneName)) return null;
for (int i = 0; i < dungeons.Length; i++)
if (dungeons[i] != null && dungeons[i].enabled_ != 0 && sceneName.Equals(dungeons[i].sceneName)) return dungeons[i];
return null;
}
public void Log(string msg)
{
if (verboseLog != 0) Debug.Log("[WL-816d] " + msg);
}
}
}