From 047661cc495181a474df961a2c7b1c5c5eb58a3f Mon Sep 17 00:00:00 2001 From: swrring Date: Fri, 8 May 2026 17:53:39 +0900 Subject: [PATCH] =?UTF-8?q?BT12-MVP-A=20Phase=202-A:=20=EA=B2=BD=ED=97=98?= =?UTF-8?q?=EC=B9=98=C2=B7=EB=A0=88=EB=B2=A8=EC=97=85=20=EC=8B=9C=EC=8A=A4?= =?UTF-8?q?=ED=85=9C=20=EC=BD=94=EB=93=9C=20+=20JSON=20=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=B8=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PD 직접 지시 2026-05-08 — (b) 채택 + JSON 테이블 영역 관리. 신규 영역 (8 파일): - Assets/Resources/Progression/level_xp_table.json — Lv 1~30 EXP 테이블 (balance-designer SOT) - Assets/Scripts/Progression/LevelXPTableLoader.cs — Resources.Load + JsonUtility 캐시 - Assets/Scripts/Progression/PlayerProgression.cs — Level·EXP 진행도 (BT12-Dev v1 PlayerStats와 직무 분리) - Assets/Scripts/Progression/ExperienceSystem.cs — EXP 발급 정적 게이트웨이 - Assets/Scripts/Progression/SkillCardPlaceholder.cs — placeholder ScriptableObject - Assets/Scripts/Progression/SkillCardPlaceholderPool.cs — 카드 풀·Draw3Random - Assets/Scripts/Progression/LevelUpManager.cs — 레벨업 발화·일시정지·UI placeholder (Phase 2-B 통합) 기존 파일 수정 (2 파일): - EnemyDeath.cs Execute 마지막 영역 ExperienceSystem.OnEnemyKilled 호출 - PlayerController.cs Awake PlayerProgression 자동 부착 회귀 위험: - BT5-Dev 발판/몬스터 영역 영향 X (EnemyDeath 호출 마지막·PlayerController 자동 부착) - BT7-Dev VS 순수형 영향 X (Schedule 영역 변경 X) - BT12-Dev v1 영역 충돌 X (PlayerStats 분리·신규 namespace EerieVillage.Progression) Phase 2-A 영역 검증: - 적 처치 → EXP 누적 → Lv 임계점 → Console [LevelUpManager] 영역 출력 확증 - Phase 2-B 영역 = SkillSelectionUI prefab + 5 placeholder asset + Scene 통합 --- .../Resources/Progression/level_xp_table.json | 38 +++++++ Assets/Scripts/Gameplay/EnemyDeath.cs | 4 + Assets/Scripts/Mechanics/PlayerController.cs | 4 + .../Scripts/Progression/ExperienceSystem.cs | 30 ++++++ Assets/Scripts/Progression/LevelUpManager.cs | 100 ++++++++++++++++++ .../Scripts/Progression/LevelXPTableLoader.cs | 97 +++++++++++++++++ .../Scripts/Progression/PlayerProgression.cs | 38 +++++++ .../Progression/SkillCardPlaceholder.cs | 33 ++++++ .../Progression/SkillCardPlaceholderPool.cs | 31 ++++++ 9 files changed, 375 insertions(+) create mode 100644 Assets/Resources/Progression/level_xp_table.json create mode 100644 Assets/Scripts/Progression/ExperienceSystem.cs create mode 100644 Assets/Scripts/Progression/LevelUpManager.cs create mode 100644 Assets/Scripts/Progression/LevelXPTableLoader.cs create mode 100644 Assets/Scripts/Progression/PlayerProgression.cs create mode 100644 Assets/Scripts/Progression/SkillCardPlaceholder.cs create mode 100644 Assets/Scripts/Progression/SkillCardPlaceholderPool.cs diff --git a/Assets/Resources/Progression/level_xp_table.json b/Assets/Resources/Progression/level_xp_table.json new file mode 100644 index 0000000..41d1529 --- /dev/null +++ b/Assets/Resources/Progression/level_xp_table.json @@ -0,0 +1,38 @@ +{ + "version": "0.1", + "description": "BT12-MVP-A 레벨별 다음 레벨 도달 EXP 테이블. balance-designer SOT. 코드 산식 X·JSON 직접 편집.", + "fallback_formula": "level <= 0 시 100 / table 미정의 시 last_level + 20 × (level - max)", + "max_level_in_table": 30, + "table": [ + { "level": 1, "xp_to_next": 100 }, + { "level": 2, "xp_to_next": 120 }, + { "level": 3, "xp_to_next": 140 }, + { "level": 4, "xp_to_next": 160 }, + { "level": 5, "xp_to_next": 180 }, + { "level": 6, "xp_to_next": 200 }, + { "level": 7, "xp_to_next": 220 }, + { "level": 8, "xp_to_next": 240 }, + { "level": 9, "xp_to_next": 260 }, + { "level": 10, "xp_to_next": 280 }, + { "level": 11, "xp_to_next": 320 }, + { "level": 12, "xp_to_next": 360 }, + { "level": 13, "xp_to_next": 400 }, + { "level": 14, "xp_to_next": 440 }, + { "level": 15, "xp_to_next": 480 }, + { "level": 16, "xp_to_next": 520 }, + { "level": 17, "xp_to_next": 560 }, + { "level": 18, "xp_to_next": 600 }, + { "level": 19, "xp_to_next": 640 }, + { "level": 20, "xp_to_next": 680 }, + { "level": 21, "xp_to_next": 760 }, + { "level": 22, "xp_to_next": 840 }, + { "level": 23, "xp_to_next": 920 }, + { "level": 24, "xp_to_next": 1000 }, + { "level": 25, "xp_to_next": 1080 }, + { "level": 26, "xp_to_next": 1160 }, + { "level": 27, "xp_to_next": 1240 }, + { "level": 28, "xp_to_next": 1320 }, + { "level": 29, "xp_to_next": 1400 }, + { "level": 30, "xp_to_next": 1480 } + ] +} diff --git a/Assets/Scripts/Gameplay/EnemyDeath.cs b/Assets/Scripts/Gameplay/EnemyDeath.cs index c5ac0d1..a60c279 100644 --- a/Assets/Scripts/Gameplay/EnemyDeath.cs +++ b/Assets/Scripts/Gameplay/EnemyDeath.cs @@ -35,6 +35,10 @@ namespace Platformer.Gameplay // 1초 후 GameObject 영역 Destroy (사망 애니메이션 시간) Object.Destroy(enemy.gameObject, 1f); + + // BT12-MVP-A 영역 신규 (2026-05-08) — 적 처치 시 EXP 발급 + var player = Object.FindFirstObjectByType(); + EerieVillage.Progression.ExperienceSystem.OnEnemyKilled(enemy, player); } } } \ No newline at end of file diff --git a/Assets/Scripts/Mechanics/PlayerController.cs b/Assets/Scripts/Mechanics/PlayerController.cs index 0308796..0b1137e 100644 --- a/Assets/Scripts/Mechanics/PlayerController.cs +++ b/Assets/Scripts/Mechanics/PlayerController.cs @@ -92,6 +92,10 @@ namespace Platformer.Mechanics if (GetComponent() == null) gameObject.AddComponent(); if (GetComponent() == null) gameObject.AddComponent(); + // BT12-MVP-A 영역 신규 (2026-05-08) — PlayerProgression 자동 부착 (레벨업 영역) + if (GetComponent() == null) + gameObject.AddComponent(); + // 사망 시 입력 차단 / 부활 시 입력 복원 if (health != null) { diff --git a/Assets/Scripts/Progression/ExperienceSystem.cs b/Assets/Scripts/Progression/ExperienceSystem.cs new file mode 100644 index 0000000..d60fc67 --- /dev/null +++ b/Assets/Scripts/Progression/ExperienceSystem.cs @@ -0,0 +1,30 @@ +using UnityEngine; +using Platformer.Mechanics; + +namespace EerieVillage.Progression +{ + /// + /// EXP 발급 정적 영역. EnemyDeath 영역 단일 호출 통로. + /// 차기 BT12-Dev 영역 P19 XPMultiplier 영역 적용 hook. + /// + public static class ExperienceSystem + { + const int DEFAULT_XP_REWARD = 5; + + /// 적 처치 시 호출 — Player 영역 PlayerProgression 갱신. + public static void OnEnemyKilled(EnemyController enemy, PlayerController player) + { + if (player == null) return; + var prog = player.GetComponent(); + if (prog == null) return; + int xp = ComputeXPReward(enemy); + prog.GainXP(xp); + } + + /// placeholder — 적 종류·등급별 XP 영역 차기 BT12-Dev 영역 (enemy_xp_reward.json 분리). + static int ComputeXPReward(EnemyController enemy) + { + return DEFAULT_XP_REWARD; + } + } +} diff --git a/Assets/Scripts/Progression/LevelUpManager.cs b/Assets/Scripts/Progression/LevelUpManager.cs new file mode 100644 index 0000000..eb05b35 --- /dev/null +++ b/Assets/Scripts/Progression/LevelUpManager.cs @@ -0,0 +1,100 @@ +using System.Collections.Generic; +using UnityEngine; +using Platformer.Mechanics; + +namespace EerieVillage.Progression +{ + /// + /// 레벨업 발화 시 일시정지 + UI 호출 + 카드 선택 결과 수령. + /// PlayerProgression.OnLevelUp 구독. + /// + /// Phase 2-A 영역 — UI 호출 placeholder (Debug.Log). + /// Phase 2-B 영역 — SkillSelectionUI 영역 통합. + /// + public class LevelUpManager : MonoBehaviour + { + public static LevelUpManager Instance { get; private set; } + + [SerializeField] SkillCardPlaceholderPool _pool; + + // Phase 2-B 영역 — SkillSelectionUI 영역 부착 + // [SerializeField] SkillSelectionUI _ui; + + PlayerController _player; + PlayerProgression _progression; + bool _isLevelUpActive = false; + + void Awake() + { + if (Instance != null && Instance != this) + { + Destroy(gameObject); + return; + } + Instance = this; + } + + void Start() + { + _player = Object.FindFirstObjectByType(); + if (_player == null) + { + Debug.LogWarning("[LevelUpManager] PlayerController 영역 부재 — Start 시점 참조 X"); + return; + } + + _progression = _player.GetComponent(); + if (_progression == null) + { + _progression = _player.gameObject.AddComponent(); + } + _progression.OnLevelUp += HandleLevelUp; + + if (_pool == null) _pool = GetComponent(); + } + + void OnDestroy() + { + if (_progression != null) _progression.OnLevelUp -= HandleLevelUp; + if (Instance == this) Instance = null; + } + + void HandleLevelUp(int newLevel) + { + if (_isLevelUpActive) return; + _isLevelUpActive = true; + + // 일시정지 + 입력 차단 + Time.timeScale = 0f; + if (_player != null) _player.controlEnabled = false; + + // 카드 3장 무작위 추출 + List cards = _pool != null + ? _pool.Draw3Random() + : new List(); + + // Phase 2-A 영역 placeholder — UI 호출 영역 Phase 2-B 영역 통합 + Debug.Log($"[LevelUpManager] LevelUp Lv.{newLevel} — 카드 {cards.Count}장 영역 (UI placeholder·Phase 2-B 통합 예정)"); + for (int i = 0; i < cards.Count; i++) + { + var c = cards[i]; + Debug.Log($" [{i + 1}] {c.displayName} ({c.rarity}·Lv.{c.currentLevel}{(c.IsMaxLevel ? "·최대" : "")})"); + } + + // Phase 2-A 영역 임시 — 즉시 첫 카드 자동 확인 + 게임 재개 + // Phase 2-B 영역 = SkillSelectionUI.Show + 사용자 클릭·확인 후 콜백 + HandleCardConfirmed(cards.Count > 0 ? cards[0] : null); + } + + void HandleCardConfirmed(SkillCardPlaceholder selected) + { + // 차기 BT12-Dev 영역 = PlayerSkillInventory.AddSkillByCardId(selected.id) + // BT12-MVP-A 영역 = UI 닫기·게임 재개만 + Debug.Log($"[LevelUpManager] 카드 확정 영역 — {(selected != null ? selected.displayName : "NONE")} (효과 적용 X·BT12-Dev 본격 영역)"); + + Time.timeScale = 1f; + if (_player != null) _player.controlEnabled = true; + _isLevelUpActive = false; + } + } +} diff --git a/Assets/Scripts/Progression/LevelXPTableLoader.cs b/Assets/Scripts/Progression/LevelXPTableLoader.cs new file mode 100644 index 0000000..6dc4d8c --- /dev/null +++ b/Assets/Scripts/Progression/LevelXPTableLoader.cs @@ -0,0 +1,97 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace EerieVillage.Progression +{ + /// + /// JSON 영역 레벨업 EXP 테이블 로더. Resources.Load + JsonUtility 활용. + /// 정적 캐시 — 첫 호출 시점 1회 로드 (게임 영역 1회). + /// SOT: Assets/Resources/Progression/level_xp_table.json (balance-designer SOT). + /// PD 직접 지시 2026-05-08 — 코드 산식 폐기·JSON 테이블 영역 관리. + /// + public static class LevelXPTableLoader + { + [System.Serializable] + public class LevelXPEntry + { + public int level; + public int xp_to_next; + } + + [System.Serializable] + public class LevelXPTable + { + public string version; + public string description; + public string fallback_formula; + public int max_level_in_table; + public LevelXPEntry[] table; + } + + const string RESOURCE_PATH = "Progression/level_xp_table"; + const int FALLBACK_BASE = 100; + const int FALLBACK_INCREMENT = 20; + + static Dictionary _cache; + static int _maxLevelInTable; + static int _lastTableXP; + + public static void EnsureLoaded() + { + if (_cache != null) return; + + _cache = new Dictionary(); + _maxLevelInTable = 0; + _lastTableXP = FALLBACK_BASE; + + var ta = Resources.Load(RESOURCE_PATH); + if (ta == null) + { + Debug.LogWarning($"[LevelXPTableLoader] Resources/{RESOURCE_PATH}.json 부재 — fallback 활성"); + return; + } + + try + { + var data = JsonUtility.FromJson(ta.text); + if (data == null || data.table == null) + { + Debug.LogWarning("[LevelXPTableLoader] JSON 파싱 실패 — fallback 활성"); + return; + } + _maxLevelInTable = data.max_level_in_table; + foreach (var entry in data.table) + { + _cache[entry.level] = entry.xp_to_next; + if (entry.level == _maxLevelInTable) _lastTableXP = entry.xp_to_next; + } + } + catch (System.Exception e) + { + Debug.LogError($"[LevelXPTableLoader] 파싱 예외 — {e.Message}"); + } + } + + /// + /// 지정 level → 다음 레벨 도달 EXP. 테이블 미정의 영역 = fallback (last_xp + 20 × overflow). + /// + public static int GetXPToNextLevel(int level) + { + EnsureLoaded(); + if (level <= 0) return FALLBACK_BASE; + if (_cache.TryGetValue(level, out int xp)) return xp; + if (_maxLevelInTable > 0 && level > _maxLevelInTable) + { + return _lastTableXP + FALLBACK_INCREMENT * (level - _maxLevelInTable); + } + return FALLBACK_BASE + level * FALLBACK_INCREMENT; + } + + /// 테이블 재로드 (Editor 영역 hot-reload 용). + public static void Reload() + { + _cache = null; + EnsureLoaded(); + } + } +} diff --git a/Assets/Scripts/Progression/PlayerProgression.cs b/Assets/Scripts/Progression/PlayerProgression.cs new file mode 100644 index 0000000..348381e --- /dev/null +++ b/Assets/Scripts/Progression/PlayerProgression.cs @@ -0,0 +1,38 @@ +using UnityEngine; + +namespace EerieVillage.Progression +{ + /// + /// 플레이어 레벨업 진행도. BT12-Dev v1 PlayerStats(패시브 보정)와 직무 분리 (Single Responsibility). + /// EXP는 적 처치 시 누적. 임계점 도달 시 OnLevelUp 발화. + /// EXP 곡선 = LevelXPTableLoader 영역 JSON SOT. + /// + public class PlayerProgression : MonoBehaviour + { + public int Level { get; private set; } = 1; + public int CurrentXP { get; private set; } = 0; + public int XPToNextLevel { get; private set; } = 100; + + /// 레벨업 발화 — int = new Level. LevelUpManager 구독 hook. + public event System.Action OnLevelUp; + + void Awake() + { + XPToNextLevel = LevelXPTableLoader.GetXPToNextLevel(Level); + } + + /// 적 처치 시 호출 — XP 획득. amount <= 0 시 무시. + public void GainXP(int amount) + { + if (amount <= 0) return; + CurrentXP += amount; + while (CurrentXP >= XPToNextLevel) + { + CurrentXP -= XPToNextLevel; + Level++; + XPToNextLevel = LevelXPTableLoader.GetXPToNextLevel(Level); + OnLevelUp?.Invoke(Level); + } + } + } +} diff --git a/Assets/Scripts/Progression/SkillCardPlaceholder.cs b/Assets/Scripts/Progression/SkillCardPlaceholder.cs new file mode 100644 index 0000000..ca5ddf3 --- /dev/null +++ b/Assets/Scripts/Progression/SkillCardPlaceholder.cs @@ -0,0 +1,33 @@ +using UnityEngine; + +namespace EerieVillage.Progression +{ + public enum CardRarity { Common, Rare, Max } + + /// + /// BT12-MVP-A 영역 placeholder 카드 데이터 ScriptableObject. + /// BT12-Dev v1 영역 ActiveSkillData·PassiveSkillData·AwakeningSkillData 영역과 별도 (효과 정의 X·표시 영역만). + /// 차기 BT12-Dev 본격 영역 = 본 ScriptableObject 영역 deprecate + 60종 카드 ScriptableObject 활용. + /// + [CreateAssetMenu(menuName = "EerieVillage/SkillCardPlaceholder")] + public class SkillCardPlaceholder : ScriptableObject + { + [Header("식별 영역")] + public string id; // 고유 ID (예: "A01_jineonbu") + public string displayName; // 한글 카드명 (예: "진언부") + + [Header("표시 영역 (PD 예시 정합)")] + public Sprite icon; // 원형 아이콘 + public CardRarity rarity = CardRarity.Common; + + [Range(1, 5)] + public int currentLevel = 1; // PD 예시 영역 "레벨 N" + [Range(1, 5)] + public int maxLevel = 5; // PD 예시 영역 "최대" 표시 + + [TextArea(2, 4)] + public string description; // 효과 설명 (3~4 라인 placeholder) + + public bool IsMaxLevel => currentLevel >= maxLevel; + } +} diff --git a/Assets/Scripts/Progression/SkillCardPlaceholderPool.cs b/Assets/Scripts/Progression/SkillCardPlaceholderPool.cs new file mode 100644 index 0000000..a86918c --- /dev/null +++ b/Assets/Scripts/Progression/SkillCardPlaceholderPool.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace EerieVillage.Progression +{ + /// + /// BT12-MVP-A 영역 placeholder 카드 풀. Inspector 영역 SkillCardPlaceholder 5~8장 등록. + /// 차기 BT12-Dev 본격 영역 = SkillRuntimeFactory 영역 활용. + /// + public class SkillCardPlaceholderPool : MonoBehaviour + { + [SerializeField] List _allCards = new List(); + + public IReadOnlyList AllCards => _allCards; + + /// 5~8장에서 무작위 3장 추출 (중복 X). 카드 영역 부족 시 가능 영역만. + public List Draw3Random() + { + var copy = new List(_allCards); + var result = new List(); + int draw = Mathf.Min(3, copy.Count); + for (int i = 0; i < draw; i++) + { + int idx = Random.Range(0, copy.Count); + result.Add(copy[idx]); + copy.RemoveAt(idx); + } + return result; + } + } +}