// ───────────────────────────────────────────────────────────────────────────── // 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 { /// 던전 1개 = 이 표의 한 행. 씬·스테이지·입구 위치가 전부 값이다. [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; } /// /// WL-816zc — 「섬 초기 플로우」 한 단계 = 이 표의 한 행. /// 발판 1개가 이 행의 값(문구·가격·자리)으로 서고, 값을 치르면 이 행의 설치물이 그 자리에 남는다. /// 코드 수정 0 — 순서·가격·설치물은 전부 여기 값이다(C45). /// [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; /// 설정 에셋(없으면 null = 기능 전체 무동작). public static WLIslandSettings Instance { get { if (!s_tried) { s_inst = Resources.Load(ResourcePath); s_tried = true; } return s_inst; } } public static void Invalidate() { s_inst = null; s_tried = false; } /// 기능이 살아 있는가. 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("🔴 §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} = 지금 내야 하는 골드.")] public string platformLabelFormat = "던전 열기\n{0} 골드"; [Tooltip("안내 글 높이(m).")] public float platformLabelHeight = 2.2f; [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("🔴 **던전 전용** 카메라 거리(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} = 가격.")] public string flowLabelFormat = "{0}\n{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", 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("첫 승리 여부를 적는 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) // ───────────────────────────────────────────────────────────────── /// /// 로그인 후 처음 들어갈 맵 ID. 기능이 꺼져 있으면 (= -1 로비)를 그대로 돌려준다. /// 🔴 원본 수정은 이 호출 **1줄**이 전부다. /// public static int StartMapId(int original) { var c = Instance; if (c == null || c.enabled_ == 0 || c.startOnIsland == 0) return original; return c.islandMapId; } /// 맵 ID 로 던전 행을 찾는다(없으면 null). 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; } /// 던전 표의 **줄 번호**로 그 행을 그대로 준다(enabled_ 와 무관 — 좌표만 빌려 쓸 때 쓴다 · WL-816zc). public WLDungeonDef DungeonRow(int rawIndex) { if (dungeons == null || rawIndex < 0 || rawIndex >= dungeons.Length) return null; return dungeons[rawIndex]; } /// 씬 이름으로 던전 행을 찾는다(같은 씬을 여러 던전이 써도 되므로 맵 ID 우선). 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); } } }