Project_WL/Assets/WL/Island/WLIslandSettings.cs

412 lines
24 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;
}
[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("🔴 §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;
[Tooltip("1 이면 새 발판은 플레이어가 그 자리에서 **내려온 뒤에** 생긴다 — " +
"골드가 많을 때 가만히 서서 던전이 연달아 팔리는 것을 막는다(816g 실측).")]
public int platformRequireStepOff = 1;
[Tooltip("위 대기의 상한(초). 넘으면 그냥 놓는다.")]
public float platformStepOffTimeoutSeconds = 120f;
[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";
// ───────────────────────────────── 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) — 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;
// ───────────────────────────────── 던전 표
[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>씬 이름으로 던전 행을 찾는다(같은 씬을 여러 던전이 써도 되므로 맵 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);
}
}
}