From a94daf79684487ba59865e918950792ba98a53b4 Mon Sep 17 00:00:00 2001 From: dal4segno Date: Thu, 26 Mar 2026 16:18:45 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=A0=AC=20=EB=B6=84=EB=A5=98=20?= =?UTF-8?q?=EC=82=AC=EC=96=91=EA=B3=BC=20=EC=9E=A5=EC=B0=A9=20UI=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 옵시디언 기준의 역할/발동 타입 분류를 스킬·젬 데이터와 장착 검증 로직에 반영 - 젬 보관 UI와 퀵슬롯 표시를 새 분류 및 실제 마나/쿨타임 계산 기준으로 갱신 - 테스트 스킬/젬 자산을 에디터 메뉴로 동기화하고 Unity 컴파일 및 플레이 검증 완료 --- .../Data_SkillGem_Player_강인함.asset | 4 +- .../SkillGems/Data_SkillGem_Player_관통.asset | 2 + .../Data_SkillGem_Player_도전자.asset | 4 +- .../SkillGems/Data_SkillGem_Player_수호.asset | 4 +- .../SkillGems/Data_SkillGem_Player_약화.asset | 4 +- .../SkillGems/Data_SkillGem_Player_연속.asset | 4 +- .../Data_SkillGem_Player_예리함.asset | 2 + .../SkillGems/Data_SkillGem_Player_충격.asset | 2 + .../SkillGems/Data_SkillGem_Player_파쇄.asset | 2 + .../Skills/Data_Skill_Player_광역치유.asset | 2 + .../Skills/Data_Skill_Player_구르기.asset | 4 +- .../Data/Skills/Data_Skill_Player_도발.asset | 4 +- .../Data/Skills/Data_Skill_Player_돌진.asset | 4 +- .../Skills/Data_Skill_Player_방어태세.asset | 2 + .../Skills/Data_Skill_Player_보호막.asset | 4 +- .../Data/Skills/Data_Skill_Player_철벽.asset | 4 +- .../Data/Skills/Data_Skill_Player_치유.asset | 2 + Assets/_Game/Prefabs/UI/UI_ActionBar.prefab | 45 + .../Scripts/Editor/PlayerSkillDebugMenu.cs | 103 +- Assets/_Game/Scripts/Player/PlayerMovement.cs | 32 +- .../_Game/Scripts/Player/PlayerSkillInput.cs | 27 +- Assets/_Game/Scripts/Skills/SkillData.cs | 137 +- Assets/_Game/Scripts/Skills/SkillGemData.cs | 33 +- .../_Game/Scripts/Skills/SkillLoadoutEntry.cs | 10 +- .../_Game/Scripts/UI/SkillGemInventoryUI.cs | 1348 +++++++++++++++++ .../Scripts/UI/SkillGemInventoryUI.cs.meta | 2 + Assets/_Game/Scripts/UI/SkillQuickSlotUI.cs | 6 +- 27 files changed, 1738 insertions(+), 59 deletions(-) create mode 100644 Assets/_Game/Scripts/UI/SkillGemInventoryUI.cs create mode 100644 Assets/_Game/Scripts/UI/SkillGemInventoryUI.cs.meta diff --git a/Assets/_Game/Data/SkillGems/Data_SkillGem_Player_강인함.asset b/Assets/_Game/Data/SkillGems/Data_SkillGem_Player_강인함.asset index dd8907e9..38bea85f 100644 --- a/Assets/_Game/Data/SkillGems/Data_SkillGem_Player_강인함.asset +++ b/Assets/_Game/Data/SkillGems/Data_SkillGem_Player_강인함.asset @@ -17,7 +17,9 @@ MonoBehaviour: \uBA74\uC5ED\uC744 \uBD80\uC5EC\uD558\uB294 \uD14C\uC2A4\uD2B8\uC6A9 \uBC29\uC5B4 \uC82C" icon: {fileID: 0} - category: 3 + category: 2 + allowedSkillRoles: 7 + allowedSkillActivationTypes: 3 allowedSkillTypes: 63 incompatibleCategories: incompatibleGems: [] diff --git a/Assets/_Game/Data/SkillGems/Data_SkillGem_Player_관통.asset b/Assets/_Game/Data/SkillGems/Data_SkillGem_Player_관통.asset index cd98d9c5..8012767b 100644 --- a/Assets/_Game/Data/SkillGems/Data_SkillGem_Player_관통.asset +++ b/Assets/_Game/Data/SkillGems/Data_SkillGem_Player_관통.asset @@ -17,6 +17,8 @@ MonoBehaviour: \uD14C\uC2A4\uD2B8\uC6A9 \uACF5\uACA9 \uC82C" icon: {fileID: 0} category: 1 + allowedSkillRoles: 1 + allowedSkillActivationTypes: 1 allowedSkillTypes: 1 incompatibleCategories: incompatibleGems: [] diff --git a/Assets/_Game/Data/SkillGems/Data_SkillGem_Player_도전자.asset b/Assets/_Game/Data/SkillGems/Data_SkillGem_Player_도전자.asset index 29e8643e..9d700585 100644 --- a/Assets/_Game/Data/SkillGems/Data_SkillGem_Player_도전자.asset +++ b/Assets/_Game/Data/SkillGems/Data_SkillGem_Player_도전자.asset @@ -16,7 +16,9 @@ MonoBehaviour: description: "\uACE0\uC704\uB825 \uAE30\uC220\uC5D0 \uC704\uD611 \uC120\uC810 \uAE30\uB2A5\uC744 \uC5B9\uB294 \uD14C\uC2A4\uD2B8\uC6A9 \uC82C" icon: {fileID: 0} - category: 2 + category: 4 + allowedSkillRoles: 3 + allowedSkillActivationTypes: 1 allowedSkillTypes: 1 incompatibleCategories: incompatibleGems: [] diff --git a/Assets/_Game/Data/SkillGems/Data_SkillGem_Player_수호.asset b/Assets/_Game/Data/SkillGems/Data_SkillGem_Player_수호.asset index 3251a56e..73183571 100644 --- a/Assets/_Game/Data/SkillGems/Data_SkillGem_Player_수호.asset +++ b/Assets/_Game/Data/SkillGems/Data_SkillGem_Player_수호.asset @@ -16,7 +16,9 @@ MonoBehaviour: description: "\uACE0\uC704\uB825 \uAE30\uC220\uC5D0 \uBCF4\uD638\uB9C9 \uBCF4\uC870\uB97C \uC5B9\uB294 \uD14C\uC2A4\uD2B8\uC6A9 \uC82C" icon: {fileID: 0} - category: 4 + category: 2 + allowedSkillRoles: 7 + allowedSkillActivationTypes: 1 allowedSkillTypes: 1 incompatibleCategories: incompatibleGems: [] diff --git a/Assets/_Game/Data/SkillGems/Data_SkillGem_Player_약화.asset b/Assets/_Game/Data/SkillGems/Data_SkillGem_Player_약화.asset index 8422b532..7a3c2619 100644 --- a/Assets/_Game/Data/SkillGems/Data_SkillGem_Player_약화.asset +++ b/Assets/_Game/Data/SkillGems/Data_SkillGem_Player_약화.asset @@ -16,7 +16,9 @@ MonoBehaviour: description: "\uC2A4\uD0AC \uC801\uC911 \uB300\uC0C1\uC5D0\uAC8C \uD14C\uC2A4\uD2B8 \uB514\uBC84\uD504\uB97C \uBD80\uC5EC\uD558\uB294 \uC81C\uC5B4 \uC82C" icon: {fileID: 0} - category: 5 + category: 4 + allowedSkillRoles: 7 + allowedSkillActivationTypes: 1 allowedSkillTypes: 63 incompatibleCategories: incompatibleGems: [] diff --git a/Assets/_Game/Data/SkillGems/Data_SkillGem_Player_연속.asset b/Assets/_Game/Data/SkillGems/Data_SkillGem_Player_연속.asset index 2d471f15..ae5dd6fd 100644 --- a/Assets/_Game/Data/SkillGems/Data_SkillGem_Player_연속.asset +++ b/Assets/_Game/Data/SkillGems/Data_SkillGem_Player_연속.asset @@ -16,7 +16,9 @@ MonoBehaviour: description: "\uBD99\uC740 \uC2A4\uD0AC\uC744 \uD55C \uBC88 \uB354 \uBC18\uBCF5 \uC2DC\uC804\uD558\uB294 \uD14C\uC2A4\uD2B8\uC6A9 \uC82C" icon: {fileID: 0} - category: 6 + category: 4 + allowedSkillRoles: 1 + allowedSkillActivationTypes: 1 allowedSkillTypes: 1 incompatibleCategories: incompatibleGems: [] diff --git a/Assets/_Game/Data/SkillGems/Data_SkillGem_Player_예리함.asset b/Assets/_Game/Data/SkillGems/Data_SkillGem_Player_예리함.asset index 1bb19bbb..d0f2756f 100644 --- a/Assets/_Game/Data/SkillGems/Data_SkillGem_Player_예리함.asset +++ b/Assets/_Game/Data/SkillGems/Data_SkillGem_Player_예리함.asset @@ -17,6 +17,8 @@ MonoBehaviour: \uD14C\uC2A4\uD2B8\uC6A9 \uACF5\uACA9 \uC82C" icon: {fileID: 0} category: 1 + allowedSkillRoles: 1 + allowedSkillActivationTypes: 1 allowedSkillTypes: 1 incompatibleCategories: incompatibleGems: [] diff --git a/Assets/_Game/Data/SkillGems/Data_SkillGem_Player_충격.asset b/Assets/_Game/Data/SkillGems/Data_SkillGem_Player_충격.asset index cd1e2ce8..6d677cdb 100644 --- a/Assets/_Game/Data/SkillGems/Data_SkillGem_Player_충격.asset +++ b/Assets/_Game/Data/SkillGems/Data_SkillGem_Player_충격.asset @@ -17,6 +17,8 @@ MonoBehaviour: \uD14C\uC2A4\uD2B8\uC6A9 \uACF5\uACA9 \uC82C" icon: {fileID: 0} category: 1 + allowedSkillRoles: 1 + allowedSkillActivationTypes: 1 allowedSkillTypes: 1 incompatibleCategories: incompatibleGems: [] diff --git a/Assets/_Game/Data/SkillGems/Data_SkillGem_Player_파쇄.asset b/Assets/_Game/Data/SkillGems/Data_SkillGem_Player_파쇄.asset index 4368a92a..6def735c 100644 --- a/Assets/_Game/Data/SkillGems/Data_SkillGem_Player_파쇄.asset +++ b/Assets/_Game/Data/SkillGems/Data_SkillGem_Player_파쇄.asset @@ -17,6 +17,8 @@ MonoBehaviour: \uAC15\uD654\uD558\uB294 \uD14C\uC2A4\uD2B8\uC6A9 \uC82C" icon: {fileID: 0} category: 1 + allowedSkillRoles: 1 + allowedSkillActivationTypes: 1 allowedSkillTypes: 1 incompatibleCategories: incompatibleGems: [] diff --git a/Assets/_Game/Data/Skills/Data_Skill_Player_광역치유.asset b/Assets/_Game/Data/Skills/Data_Skill_Player_광역치유.asset index 86a159fb..a39549d7 100644 --- a/Assets/_Game/Data/Skills/Data_Skill_Player_광역치유.asset +++ b/Assets/_Game/Data/Skills/Data_Skill_Player_광역치유.asset @@ -16,6 +16,8 @@ MonoBehaviour: description: "\uC8FC\uBCC0 \uC544\uAD70\uACFC \uC790\uC2E0\uC758 \uCCB4\uB825\uC744 \uD568\uAED8 \uD68C\uBCF5\uD55C\uB2E4." icon: {fileID: 0} + skillRole: 4 + activationType: 1 baseTypes: 4 skillClip: {fileID: -8689311932429934276, guid: 836c26605050496b9fd07dd456e6ea82, type: 3} endClip: {fileID: 0} diff --git a/Assets/_Game/Data/Skills/Data_Skill_Player_구르기.asset b/Assets/_Game/Data/Skills/Data_Skill_Player_구르기.asset index bb2d2aa8..d63cc093 100644 --- a/Assets/_Game/Data/Skills/Data_Skill_Player_구르기.asset +++ b/Assets/_Game/Data/Skills/Data_Skill_Player_구르기.asset @@ -15,7 +15,9 @@ MonoBehaviour: skillName: "\uAD6C\uB974\uAE30" description: icon: {fileID: 21300000, guid: eafcc94eae3865944b93e64c4e281aa0, type: 3} - baseTypes: 16 + skillRole: 2 + activationType: 1 + baseTypes: 2 skillClip: {fileID: -14460799136228694, guid: d6d51384d6dd17a419c1d8e2a1c0c875, type: 3} endClip: {fileID: 0} animationSpeed: 1 diff --git a/Assets/_Game/Data/Skills/Data_Skill_Player_도발.asset b/Assets/_Game/Data/Skills/Data_Skill_Player_도발.asset index f2ba0edb..d3da7937 100644 --- a/Assets/_Game/Data/Skills/Data_Skill_Player_도발.asset +++ b/Assets/_Game/Data/Skills/Data_Skill_Player_도발.asset @@ -17,7 +17,9 @@ MonoBehaviour: \uB192\uC774\uACE0 \uC9E7\uC740 \uC2DC\uAC04 \uB3D9\uC548 \uC704\uD611 \uC0DD\uC131\uB7C9\uC744 \uC99D\uAC00\uC2DC\uD0A8\uB2E4." icon: {fileID: 0} - baseTypes: 40 + skillRole: 2 + activationType: 1 + baseTypes: 2 skillClip: {fileID: -4662563244894722208, guid: de4d0153716747cd9fc90c60f5efb1ae, type: 3} endClip: {fileID: 0} animationSpeed: 1 diff --git a/Assets/_Game/Data/Skills/Data_Skill_Player_돌진.asset b/Assets/_Game/Data/Skills/Data_Skill_Player_돌진.asset index a918f3a1..e7a545da 100644 --- a/Assets/_Game/Data/Skills/Data_Skill_Player_돌진.asset +++ b/Assets/_Game/Data/Skills/Data_Skill_Player_돌진.asset @@ -15,7 +15,9 @@ MonoBehaviour: skillName: "\uB3CC\uC9C4" description: icon: {fileID: 0} - baseTypes: 16 + skillRole: 1 + activationType: 1 + baseTypes: 1 skillClip: {fileID: 0} endClip: {fileID: 0} animationSpeed: 1 diff --git a/Assets/_Game/Data/Skills/Data_Skill_Player_방어태세.asset b/Assets/_Game/Data/Skills/Data_Skill_Player_방어태세.asset index 6a2e8381..c1e8b7cb 100644 --- a/Assets/_Game/Data/Skills/Data_Skill_Player_방어태세.asset +++ b/Assets/_Game/Data/Skills/Data_Skill_Player_방어태세.asset @@ -16,6 +16,8 @@ MonoBehaviour: description: "\uC9E7\uC740 \uC2DC\uAC04 \uB3D9\uC548 \uBC1B\uB294 \uD53C\uD574\uB97C \uC904\uC774\uACE0 \uC704\uD611 \uC0DD\uC131\uB7C9\uC744 \uB192\uC778\uB2E4." icon: {fileID: 0} + skillRole: 2 + activationType: 2 baseTypes: 2 skillClip: {fileID: -592826573199220879, guid: 52e14756abda46499f4739d811043b3d, type: 3} endClip: {fileID: 0} diff --git a/Assets/_Game/Data/Skills/Data_Skill_Player_보호막.asset b/Assets/_Game/Data/Skills/Data_Skill_Player_보호막.asset index adf4559e..63bca74d 100644 --- a/Assets/_Game/Data/Skills/Data_Skill_Player_보호막.asset +++ b/Assets/_Game/Data/Skills/Data_Skill_Player_보호막.asset @@ -16,7 +16,9 @@ MonoBehaviour: description: "\uC8FC\uBCC0 \uC544\uAD70\uACFC \uC790\uC2E0\uC5D0\uAC8C \uD53C\uD574\uB97C \uD761\uC218\uD558\uB294 \uBCF4\uD638\uB9C9\uC744 \uBD80\uC5EC\uD55C\uB2E4." icon: {fileID: 0} - baseTypes: 6 + skillRole: 4 + activationType: 1 + baseTypes: 4 skillClip: {fileID: -1185230921767219677, guid: f2d90cfa60b04630af1dde00f4d29320, type: 3} endClip: {fileID: 0} animationSpeed: 1 diff --git a/Assets/_Game/Data/Skills/Data_Skill_Player_철벽.asset b/Assets/_Game/Data/Skills/Data_Skill_Player_철벽.asset index efb88303..c09c8d6c 100644 --- a/Assets/_Game/Data/Skills/Data_Skill_Player_철벽.asset +++ b/Assets/_Game/Data/Skills/Data_Skill_Player_철벽.asset @@ -16,7 +16,9 @@ MonoBehaviour: description: "\uC9E7\uC740 \uC2DC\uAC04 \uB3D9\uC548 \uBB34\uC801\uC774 \uB418\uBA70 \uC704\uD611 \uC0DD\uC131\uB7C9\uC774 \uC57D\uAC04 \uC99D\uAC00\uD55C\uB2E4." icon: {fileID: 0} - baseTypes: 6 + skillRole: 2 + activationType: 2 + baseTypes: 2 skillClip: {fileID: -7313196749698736815, guid: 95764ba490b24918b73fc1553e34dc1e, type: 3} endClip: {fileID: 0} animationSpeed: 1 diff --git a/Assets/_Game/Data/Skills/Data_Skill_Player_치유.asset b/Assets/_Game/Data/Skills/Data_Skill_Player_치유.asset index ba467c0f..76e76e9e 100644 --- a/Assets/_Game/Data/Skills/Data_Skill_Player_치유.asset +++ b/Assets/_Game/Data/Skills/Data_Skill_Player_치유.asset @@ -15,6 +15,8 @@ MonoBehaviour: skillName: "\uCE58\uC720" description: "\uC790\uC2E0\uC758 \uCCB4\uB825\uC744 \uBE60\uB974\uAC8C \uD68C\uBCF5\uD55C\uB2E4." icon: {fileID: 0} + skillRole: 4 + activationType: 1 baseTypes: 4 skillClip: {fileID: -8689311932429934276, guid: 4450ee0d92144ade9f63dd601432d3bf, type: 3} endClip: {fileID: 0} diff --git a/Assets/_Game/Prefabs/UI/UI_ActionBar.prefab b/Assets/_Game/Prefabs/UI/UI_ActionBar.prefab index 68b0d8c1..8a5e2950 100644 --- a/Assets/_Game/Prefabs/UI/UI_ActionBar.prefab +++ b/Assets/_Game/Prefabs/UI/UI_ActionBar.prefab @@ -12,6 +12,7 @@ GameObject: - component: {fileID: 9156608660900377980} - component: {fileID: 3790117282943784127} - component: {fileID: 7777120333480524073} + - component: {fileID: 110599025774604826} m_Layer: 5 m_Name: UI_ActionBar m_TagString: Untagged @@ -115,6 +116,50 @@ MonoBehaviour: - 2 - 3 - 4 +--- !u!114 &110599025774604826 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 628750841697537993} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: ca98554fbe3918040881b21c34fbb373, type: 3} + m_Name: + m_EditorClassIdentifier: Colosseum.Game::Colosseum.UI.SkillGemInventoryUI + toggleKey: 21 + toggleButtonLabel: "\uC82C" + toggleButtonAnchoredPosition: {x: -48, y: 164} + ownedGemEntries: + - gem: {fileID: 11400000, guid: e020eee86f6c97f4393672759d73602e, type: 2} + quantity: 1 + - gem: {fileID: 11400000, guid: 47a98aa9a30748a4da49455ac0fbd142, type: 2} + quantity: 1 + - gem: {fileID: 11400000, guid: e86536592f45d2b49b9d25abbad1b184, type: 2} + quantity: 1 + - gem: {fileID: 11400000, guid: de5e48980eba8794c93ea7168d592f8f, type: 2} + quantity: 1 + - gem: {fileID: 11400000, guid: 2edf7687dc6caa0489ae2111499fcfab, type: 2} + quantity: 1 + - gem: {fileID: 11400000, guid: 863dcd9e10827f94ab4574b529ffe683, type: 2} + quantity: 1 + - gem: {fileID: 11400000, guid: 66ccf80cf9c50614dbe13ea7f24a6f19, type: 2} + quantity: 1 + - gem: {fileID: 11400000, guid: cf3e3e1f9f1f42f499196fa819263dc1, type: 2} + quantity: 1 + - gem: {fileID: 11400000, guid: 2c42bf0e90f5dd9488d534c337a44eed, type: 2} + quantity: 1 + autoCollectOwnedGemsInEditor: 1 + gemSearchFolder: Assets/_Game/Data/SkillGems + autoCollectedGemQuantity: 1 + panelBackgroundColor: {r: 0.08, g: 0.08, b: 0.11, a: 0.96} + sectionBackgroundColor: {r: 0.14, g: 0.14, b: 0.18, a: 0.95} + buttonNormalColor: {r: 0.19, g: 0.19, b: 0.24, a: 0.96} + buttonSelectedColor: {r: 0.48, g: 0.32, b: 0.16, a: 0.96} + buttonDisabledColor: {r: 0.12, g: 0.12, b: 0.15, a: 0.65} + statusNormalColor: {r: 0.86, g: 0.85, b: 0.78, a: 1} + statusErrorColor: {r: 1, g: 0.52, b: 0.45, a: 1} --- !u!1001 &3885202253629243258 PrefabInstance: m_ObjectHideFlags: 0 diff --git a/Assets/_Game/Scripts/Editor/PlayerSkillDebugMenu.cs b/Assets/_Game/Scripts/Editor/PlayerSkillDebugMenu.cs index e5cd0458..0b42669a 100644 --- a/Assets/_Game/Scripts/Editor/PlayerSkillDebugMenu.cs +++ b/Assets/_Game/Scripts/Editor/PlayerSkillDebugMenu.cs @@ -615,7 +615,7 @@ namespace Colosseum.Editor CrushGemPath, "파쇄", "고위력 기술의 단일 피해를 강화하는 테스트용 젬", - SkillGemCategory.Attack, + SkillGemCategory.Damage, 1.15f, 1.1f, 1f, @@ -625,13 +625,15 @@ namespace Colosseum.Editor 1f, 0, damageEffect, + allowedSkillRoles: SkillRoleType.Attack, + allowedSkillActivationTypes: SkillActivationType.Instant, allowedSkillTypes: SkillBaseType.Attack); CreateOrUpdateGemAsset( ChallengerGemPath, "도전자", "고위력 기술에 위협 선점 기능을 얹는 테스트용 젬", - SkillGemCategory.Threat, + SkillGemCategory.Special, 1f, 1f, 1f, @@ -641,13 +643,15 @@ namespace Colosseum.Editor 1.5f, 0, tauntEffect, + allowedSkillRoles: SkillRoleType.Attack | SkillRoleType.Defense, + allowedSkillActivationTypes: SkillActivationType.Instant, allowedSkillTypes: SkillBaseType.Attack); CreateOrUpdateGemAsset( GuardianGemPath, "수호", "고위력 기술에 보호막 보조를 얹는 테스트용 젬", - SkillGemCategory.Support, + SkillGemCategory.Survival, 1.05f, 1.1f, 1f, @@ -657,13 +661,15 @@ namespace Colosseum.Editor 1f, 0, shieldEffect, + allowedSkillRoles: SkillRoleType.All, + allowedSkillActivationTypes: SkillActivationType.Instant, allowedSkillTypes: SkillBaseType.Attack); CreateOrUpdateGemAsset( RepeatGemPath, "연속", "붙은 스킬을 한 번 더 반복 시전하는 테스트용 젬", - SkillGemCategory.Efficiency, + SkillGemCategory.Special, 1.2f, 1.15f, 1.1f, @@ -673,13 +679,15 @@ namespace Colosseum.Editor 1f, 1, null, + allowedSkillRoles: SkillRoleType.Attack, + allowedSkillActivationTypes: SkillActivationType.Instant, allowedSkillTypes: SkillBaseType.Attack); CreateOrUpdateGemAsset( FortitudeGemPath, "강인함", "스킬 사용 시 자신에게 경직 면역을 부여하는 테스트용 방어 젬", - SkillGemCategory.Defense, + SkillGemCategory.Survival, 1.05f, 1.05f, 1f, @@ -689,13 +697,15 @@ namespace Colosseum.Editor 1f, 0, null, - new[] { hitReactionImmuneAbnormality }); + new[] { hitReactionImmuneAbnormality }, + allowedSkillRoles: SkillRoleType.All, + allowedSkillActivationTypes: SkillActivationType.All); CreateOrUpdateGemAsset( WitherGemPath, "약화", "스킬 적중 대상에게 테스트 디버프를 부여하는 제어 젬", - SkillGemCategory.Control, + SkillGemCategory.Special, 1.05f, 1.05f, 1f, @@ -707,13 +717,15 @@ namespace Colosseum.Editor null, null, 0, - new[] { testDebuffAbnormality }); + new[] { testDebuffAbnormality }, + allowedSkillRoles: SkillRoleType.All, + allowedSkillActivationTypes: SkillActivationType.Instant); CreateOrUpdateGemAsset( EdgeGemPath, "예리함", "고정 추가 피해를 부여하는 테스트용 공격 젬", - SkillGemCategory.Attack, + SkillGemCategory.Damage, 1f, 1f, 1f, @@ -723,13 +735,15 @@ namespace Colosseum.Editor 1f, 0, edgeDamageEffect, + allowedSkillRoles: SkillRoleType.Attack, + allowedSkillActivationTypes: SkillActivationType.Instant, allowedSkillTypes: SkillBaseType.Attack); CreateOrUpdateGemAsset( ImpactGemPath, "충격", "중간 고정 추가 피해를 부여하는 테스트용 공격 젬", - SkillGemCategory.Attack, + SkillGemCategory.Damage, 1f, 1f, 1f, @@ -739,13 +753,15 @@ namespace Colosseum.Editor 1f, 0, impactDamageEffect, + allowedSkillRoles: SkillRoleType.Attack, + allowedSkillActivationTypes: SkillActivationType.Instant, allowedSkillTypes: SkillBaseType.Attack); CreateOrUpdateGemAsset( BreachGemPath, "관통", "높은 고정 추가 피해를 부여하는 테스트용 공격 젬", - SkillGemCategory.Attack, + SkillGemCategory.Damage, 1f, 1f, 1f, @@ -755,6 +771,8 @@ namespace Colosseum.Editor 1f, 0, breachDamageEffect, + allowedSkillRoles: SkillRoleType.Attack, + allowedSkillActivationTypes: SkillActivationType.Instant, allowedSkillTypes: SkillBaseType.Attack); AssetDatabase.SaveAssets(); @@ -793,19 +811,19 @@ namespace Colosseum.Editor SkillGemData impactGem = AssetDatabase.LoadAssetAtPath(ImpactGemPath); SkillGemData breachGem = AssetDatabase.LoadAssetAtPath(BreachGemPath); - SetSkillBaseTypes(slashSkill, SkillBaseType.Attack); - SetSkillBaseTypes(tauntSkill, SkillBaseType.Control | SkillBaseType.Utility); - SetSkillBaseTypes(guardSkill, SkillBaseType.Defense); - SetSkillBaseTypes(dashSkill, SkillBaseType.Mobility); - SetSkillBaseTypes(ironWallSkill, SkillBaseType.Defense | SkillBaseType.Support); - SetSkillBaseTypes(pierceSkill, SkillBaseType.Attack); - SetSkillBaseTypes(gemTestSkill, SkillBaseType.Attack); - SetSkillBaseTypes(healSkill, SkillBaseType.Support); - SetSkillBaseTypes(areaHealSkill, SkillBaseType.Support); - SetSkillBaseTypes(shieldSkill, SkillBaseType.Defense | SkillBaseType.Support); - SetSkillBaseTypes(projectileSkill, SkillBaseType.Attack); - SetSkillBaseTypes(spinSkill, SkillBaseType.Attack); - SetSkillBaseTypes(evadeSkill, SkillBaseType.Mobility); + SetSkillClassification(slashSkill, SkillRoleType.Attack, SkillActivationType.Instant, SkillBaseType.Attack); + SetSkillClassification(tauntSkill, SkillRoleType.Defense, SkillActivationType.Instant, SkillBaseType.Defense); + SetSkillClassification(guardSkill, SkillRoleType.Defense, SkillActivationType.Buff, SkillBaseType.Defense); + SetSkillClassification(dashSkill, SkillRoleType.Attack, SkillActivationType.Instant, SkillBaseType.Attack); + SetSkillClassification(ironWallSkill, SkillRoleType.Defense, SkillActivationType.Buff, SkillBaseType.Defense); + SetSkillClassification(pierceSkill, SkillRoleType.Attack, SkillActivationType.Instant, SkillBaseType.Attack); + SetSkillClassification(gemTestSkill, SkillRoleType.Attack, SkillActivationType.Instant, SkillBaseType.Attack); + SetSkillClassification(healSkill, SkillRoleType.Support, SkillActivationType.Instant, SkillBaseType.Support); + SetSkillClassification(areaHealSkill, SkillRoleType.Support, SkillActivationType.Instant, SkillBaseType.Support); + SetSkillClassification(shieldSkill, SkillRoleType.Support, SkillActivationType.Instant, SkillBaseType.Support); + SetSkillClassification(projectileSkill, SkillRoleType.Attack, SkillActivationType.Instant, SkillBaseType.Attack); + SetSkillClassification(spinSkill, SkillRoleType.Attack, SkillActivationType.Instant, SkillBaseType.Attack); + SetSkillClassification(evadeSkill, SkillRoleType.Defense, SkillActivationType.Instant, SkillBaseType.Defense); EnsureGemTestSkillSlotCount(gemTestSkill, 3); @@ -1336,6 +1354,8 @@ namespace Colosseum.Editor AbnormalityData[] selfAbnormalities = null, int triggeredAbnormalityIndex = -1, AbnormalityData[] onHitAbnormalities = null, + SkillRoleType allowedSkillRoles = SkillRoleType.All, + SkillActivationType allowedSkillActivationTypes = SkillActivationType.All, SkillBaseType allowedSkillTypes = SkillBaseType.All, SkillGemCategory[] incompatibleCategories = null, SkillGemData[] incompatibleGems = null) @@ -1356,6 +1376,8 @@ namespace Colosseum.Editor serializedGem.FindProperty("gemName").stringValue = gemName; serializedGem.FindProperty("description").stringValue = description; serializedGem.FindProperty("category").enumValueIndex = (int)category; + serializedGem.FindProperty("allowedSkillRoles").intValue = (int)allowedSkillRoles; + serializedGem.FindProperty("allowedSkillActivationTypes").intValue = (int)allowedSkillActivationTypes; serializedGem.FindProperty("manaCostMultiplier").floatValue = manaCostMultiplier; serializedGem.FindProperty("cooldownMultiplier").floatValue = cooldownMultiplier; serializedGem.FindProperty("castSpeedMultiplier").floatValue = castSpeedMultiplier; @@ -1424,17 +1446,42 @@ namespace Colosseum.Editor EditorUtility.SetDirty(gem); } - private static void SetSkillBaseTypes(SkillData skill, SkillBaseType baseTypes) + private static void SetSkillClassification( + SkillData skill, + SkillRoleType skillRole, + SkillActivationType activationType, + SkillBaseType baseTypes) { if (skill == null) return; SerializedObject serializedSkill = new SerializedObject(skill); + bool hasChanges = false; + + SerializedProperty skillRoleProperty = serializedSkill.FindProperty("skillRole"); + if (skillRoleProperty != null && skillRoleProperty.intValue != (int)skillRole) + { + skillRoleProperty.intValue = (int)skillRole; + hasChanges = true; + } + + SerializedProperty activationTypeProperty = serializedSkill.FindProperty("activationType"); + if (activationTypeProperty != null && activationTypeProperty.intValue != (int)activationType) + { + activationTypeProperty.intValue = (int)activationType; + hasChanges = true; + } + SerializedProperty baseTypesProperty = serializedSkill.FindProperty("baseTypes"); - if (baseTypesProperty == null || baseTypesProperty.intValue == (int)baseTypes) + if (baseTypesProperty != null && baseTypesProperty.intValue != (int)baseTypes) + { + baseTypesProperty.intValue = (int)baseTypes; + hasChanges = true; + } + + if (!hasChanges) return; - baseTypesProperty.intValue = (int)baseTypes; serializedSkill.ApplyModifiedPropertiesWithoutUndo(); EditorUtility.SetDirty(skill); } @@ -1571,7 +1618,7 @@ namespace Colosseum.Editor if (hasGem) categoryBuilder.Append(", "); - categoryBuilder.Append(gem.Category); + categoryBuilder.Append(SkillClassificationUtility.GetGemCategoryLabel(gem.Category)); hasGem = true; } diff --git a/Assets/_Game/Scripts/Player/PlayerMovement.cs b/Assets/_Game/Scripts/Player/PlayerMovement.cs index cf10f556..a514287d 100644 --- a/Assets/_Game/Scripts/Player/PlayerMovement.cs +++ b/Assets/_Game/Scripts/Player/PlayerMovement.cs @@ -32,6 +32,7 @@ namespace Colosseum.Player private Vector3 velocity; private Vector2 moveInput; // 로컬 원시 입력 (IsOwner 전용) private InputSystem_Actions inputActions; + private bool gameplayInputEnabled = true; private bool isJumping; private bool wasGrounded; private Vector3 forcedMovementVelocity; @@ -97,10 +98,10 @@ namespace Colosseum.Player private void InitializeInputActions() { inputActions = new InputSystem_Actions(); - inputActions.Player.Enable(); inputActions.Player.Move.performed += OnMovePerformed; inputActions.Player.Move.canceled += OnMoveCanceled; inputActions.Player.Jump.performed += OnJumpPerformed; + SetGameplayInputEnabled(true); } private void CleanupInputActions() @@ -125,10 +126,7 @@ namespace Colosseum.Player { if (IsOwner && inputActions != null) { - inputActions.Player.Enable(); - inputActions.Player.Move.performed += OnMovePerformed; - inputActions.Player.Move.canceled += OnMoveCanceled; - inputActions.Player.Jump.performed += OnJumpPerformed; + SetGameplayInputEnabled(gameplayInputEnabled); } } @@ -153,9 +151,33 @@ namespace Colosseum.Player private void OnMovePerformed(InputAction.CallbackContext context) => moveInput = context.ReadValue(); private void OnMoveCanceled(InputAction.CallbackContext context) => moveInput = Vector2.zero; + /// + /// 로컬 플레이어의 전투 입력을 일시적으로 차단하거나 복구합니다. + /// + public void SetGameplayInputEnabled(bool enabled) + { + gameplayInputEnabled = enabled; + + if (!IsOwner || inputActions == null) + return; + + if (enabled) + { + inputActions.Player.Enable(); + return; + } + + moveInput = Vector2.zero; + if (netMoveInput.Value != Vector2.zero) + netMoveInput.Value = Vector2.zero; + + inputActions.Player.Disable(); + } + private void OnJumpPerformed(InputAction.CallbackContext context) { if (!IsOwner) return; + if (!gameplayInputEnabled) return; if (actionState != null && !actionState.CanJump) return; JumpRequestRpc(); diff --git a/Assets/_Game/Scripts/Player/PlayerSkillInput.cs b/Assets/_Game/Scripts/Player/PlayerSkillInput.cs index 50752d82..7fe503e3 100644 --- a/Assets/_Game/Scripts/Player/PlayerSkillInput.cs +++ b/Assets/_Game/Scripts/Player/PlayerSkillInput.cs @@ -77,6 +77,7 @@ namespace Colosseum.Player [SerializeField] private PlayerActionState actionState; private InputSystem_Actions inputActions; + private bool gameplayInputEnabled = true; public SkillData[] SkillSlots => skillSlots; public SkillLoadoutEntry[] SkillLoadoutEntries => skillLoadoutEntries; @@ -117,7 +118,7 @@ namespace Colosseum.Player inputActions.Player.Evade.performed += OnEvadePerformed; } - inputActions.Player.Enable(); + SetGameplayInputEnabled(true); } public override void OnNetworkDespawn() @@ -148,10 +149,29 @@ namespace Colosseum.Player { if (IsOwner && inputActions != null) { - inputActions.Player.Enable(); + SetGameplayInputEnabled(gameplayInputEnabled); } } + /// + /// 로컬 플레이어의 스킬 입력을 일시적으로 차단하거나 복구합니다. + /// + public void SetGameplayInputEnabled(bool enabled) + { + gameplayInputEnabled = enabled; + + if (!IsOwner || inputActions == null) + return; + + if (enabled) + { + inputActions.Player.Enable(); + return; + } + + inputActions.Player.Disable(); + } + /// /// 기존 프리팹이나 씬 직렬화 데이터가 6칸으로 남아 있어도 /// 긴급 회피 슬롯까지 포함한 7칸 구성을 항상 보장합니다. @@ -244,6 +264,9 @@ namespace Colosseum.Player /// private void OnSkillInput(int slotIndex) { + if (!gameplayInputEnabled) + return; + if (slotIndex < 0 || slotIndex >= skillSlots.Length) return; diff --git a/Assets/_Game/Scripts/Skills/SkillData.cs b/Assets/_Game/Scripts/Skills/SkillData.cs index b3873196..00fdb7fb 100644 --- a/Assets/_Game/Scripts/Skills/SkillData.cs +++ b/Assets/_Game/Scripts/Skills/SkillData.cs @@ -22,6 +22,32 @@ namespace Colosseum.Skills All = Attack | Defense | Support | Control | Mobility | Utility, } + /// + /// 스킬의 역할 분류입니다. + /// 젬 장착 조건에는 비트 마스크 형태로도 사용합니다. + /// + [Flags] + public enum SkillRoleType + { + None = 0, + Attack = 1 << 0, + Defense = 1 << 1, + Support = 1 << 2, + All = Attack | Defense | Support, + } + + /// + /// 스킬의 발동 타입 분류입니다. + /// + [Flags] + public enum SkillActivationType + { + None = 0, + Instant = 1 << 0, + Buff = 1 << 1, + All = Instant | Buff, + } + /// /// 스킬 데이터. 스킬의 기본 정보와 효과 목록을 관리합니다. /// @@ -34,8 +60,14 @@ namespace Colosseum.Skills [SerializeField] private string description; [SerializeField] private Sprite icon; - [Header("기반 스킬 분류")] - [Tooltip("젬 장착 가능 조건에 사용하는 기반 스킬 분류")] + [Header("스킬 분류")] + [Tooltip("이 스킬의 주 역할입니다.")] + [SerializeField] private SkillRoleType skillRole = SkillRoleType.Attack; + [Tooltip("이 스킬의 발동 타입입니다.")] + [SerializeField] private SkillActivationType activationType = SkillActivationType.Instant; + + [Header("레거시 기반 스킬 분류")] + [Tooltip("기존 테스트 데이터와의 호환을 위한 기반 분류입니다.")] [SerializeField] private SkillBaseType baseTypes = SkillBaseType.None; [Header("애니메이션")] @@ -82,6 +114,8 @@ namespace Colosseum.Skills public string SkillName => skillName; public string Description => description; public Sprite Icon => icon; + public SkillRoleType SkillRole => skillRole; + public SkillActivationType ActivationType => activationType; public SkillBaseType BaseTypes => baseTypes; public AnimationClip SkillClip => skillClip; public AnimationClip EndClip => endClip; @@ -97,5 +131,104 @@ namespace Colosseum.Skills public bool BlockOtherSkillsWhileCasting => blockOtherSkillsWhileCasting; public IReadOnlyList CastStartEffects => castStartEffects; public IReadOnlyList Effects => effects; + + /// + /// 지정한 장착 조건과 현재 스킬 분류가 맞는지 확인합니다. + /// + public bool MatchesClassification(SkillRoleType allowedRoles, SkillActivationType allowedActivationTypes) + { + bool matchesRole = allowedRoles == SkillRoleType.None || + allowedRoles == SkillRoleType.All || + (allowedRoles & skillRole) != 0; + + bool matchesActivationType = allowedActivationTypes == SkillActivationType.None || + allowedActivationTypes == SkillActivationType.All || + (allowedActivationTypes & activationType) != 0; + + return matchesRole && matchesActivationType; + } + } + + /// + /// 스킬/젬 분류를 UI 친화적인 문자열로 변환하는 유틸리티입니다. + /// + public static class SkillClassificationUtility + { + public static string GetRoleLabel(SkillRoleType role) + { + return role switch + { + SkillRoleType.Attack => "공격", + SkillRoleType.Defense => "방어", + SkillRoleType.Support => "지원", + SkillRoleType.All => "전체", + _ => "미분류", + }; + } + + public static string GetActivationTypeLabel(SkillActivationType activationType) + { + return activationType switch + { + SkillActivationType.Instant => "즉발", + SkillActivationType.Buff => "버프", + SkillActivationType.All => "전체", + _ => "미분류", + }; + } + + public static string GetSkillClassificationLabel(SkillData skill) + { + if (skill == null) + return "미분류"; + + return $"{GetRoleLabel(skill.SkillRole)}/{GetActivationTypeLabel(skill.ActivationType)}"; + } + + public static string GetGemCategoryLabel(SkillGemCategory category) + { + return category switch + { + SkillGemCategory.Damage => "데미지", + SkillGemCategory.Survival => "생존", + SkillGemCategory.Mana => "마나", + SkillGemCategory.Special => "특수", + SkillGemCategory.BuffPower => "효과 강화", + SkillGemCategory.Duration => "지속시간", + SkillGemCategory.Area => "범위", + SkillGemCategory.Cost => "비용", + _ => "공용", + }; + } + + public static string GetAllowedRoleSummary(SkillRoleType roles) + { + if (roles == SkillRoleType.None || roles == SkillRoleType.All) + return "전체"; + + List labels = new List(); + if ((roles & SkillRoleType.Attack) != 0) + labels.Add("공격"); + if ((roles & SkillRoleType.Defense) != 0) + labels.Add("방어"); + if ((roles & SkillRoleType.Support) != 0) + labels.Add("지원"); + + return labels.Count > 0 ? string.Join(" + ", labels) : "미분류"; + } + + public static string GetAllowedActivationSummary(SkillActivationType activationTypes) + { + if (activationTypes == SkillActivationType.None || activationTypes == SkillActivationType.All) + return "전체"; + + List labels = new List(); + if ((activationTypes & SkillActivationType.Instant) != 0) + labels.Add("즉발"); + if ((activationTypes & SkillActivationType.Buff) != 0) + labels.Add("버프"); + + return labels.Count > 0 ? string.Join(" + ", labels) : "미분류"; + } } } diff --git a/Assets/_Game/Scripts/Skills/SkillGemData.cs b/Assets/_Game/Scripts/Skills/SkillGemData.cs index c41f52e2..3f0e4108 100644 --- a/Assets/_Game/Scripts/Skills/SkillGemData.cs +++ b/Assets/_Game/Scripts/Skills/SkillGemData.cs @@ -13,12 +13,14 @@ namespace Colosseum.Skills public enum SkillGemCategory { Common, - Attack, - Threat, - Defense, - Support, - Control, - Efficiency, + Damage, + Survival, + Mana, + Special, + BuffPower, + Duration, + Area, + Cost, } /// @@ -66,9 +68,13 @@ namespace Colosseum.Skills [SerializeField] private SkillGemCategory category = SkillGemCategory.Common; [Header("장착 제약")] - [Tooltip("장착 가능한 기반 스킬 분류입니다. None 또는 All이면 제한하지 않습니다.")] + [Tooltip("장착 가능한 스킬 역할 조합입니다. None 또는 All이면 역할 제한을 두지 않습니다.")] + [SerializeField] private SkillRoleType allowedSkillRoles = SkillRoleType.All; + [Tooltip("장착 가능한 스킬 발동 타입 조합입니다. None 또는 All이면 타입 제한을 두지 않습니다.")] + [SerializeField] private SkillActivationType allowedSkillActivationTypes = SkillActivationType.All; + [Tooltip("기존 테스트 자산과의 호환을 위한 기반 스킬 분류입니다. 새 사양에서는 역할/발동 타입을 우선 사용합니다.")] [SerializeField] private SkillBaseType allowedSkillTypes = SkillBaseType.All; - [Tooltip("함께 장착할 수 없는 젬 분류")] + [Tooltip("함께 장착할 수 없는 젬 효과 분류")] [SerializeField] private SkillGemCategory[] incompatibleCategories = Array.Empty(); [Tooltip("함께 장착할 수 없는 특정 젬")] [SerializeField] private List incompatibleGems = new(); @@ -107,6 +113,8 @@ namespace Colosseum.Skills public string Description => description; public Sprite Icon => icon; public SkillGemCategory Category => category; + public SkillRoleType AllowedSkillRoles => allowedSkillRoles; + public SkillActivationType AllowedSkillActivationTypes => allowedSkillActivationTypes; public SkillBaseType AllowedSkillTypes => allowedSkillTypes; public float ManaCostMultiplier => manaCostMultiplier; public float CooldownMultiplier => cooldownMultiplier; @@ -131,6 +139,15 @@ namespace Colosseum.Skills if (skill == null) return false; + bool usesRoleRestriction = allowedSkillRoles != SkillRoleType.None && allowedSkillRoles != SkillRoleType.All; + bool usesActivationRestriction = allowedSkillActivationTypes != SkillActivationType.None && + allowedSkillActivationTypes != SkillActivationType.All; + + if (usesRoleRestriction || usesActivationRestriction) + { + return skill.MatchesClassification(allowedSkillRoles, allowedSkillActivationTypes); + } + if (allowedSkillTypes == SkillBaseType.None || allowedSkillTypes == SkillBaseType.All) return true; diff --git a/Assets/_Game/Scripts/Skills/SkillLoadoutEntry.cs b/Assets/_Game/Scripts/Skills/SkillLoadoutEntry.cs index 4cfff9e3..4f0990cf 100644 --- a/Assets/_Game/Scripts/Skills/SkillLoadoutEntry.cs +++ b/Assets/_Game/Scripts/Skills/SkillLoadoutEntry.cs @@ -425,7 +425,11 @@ namespace Colosseum.Skills if (!gem.CanAttachToSkill(baseSkill)) { - reason = $"기반 스킬 분류 제약을 만족하지 않습니다. Skill={baseSkill.BaseTypes}, Allowed={gem.AllowedSkillTypes}"; + string skillClassification = SkillClassificationUtility.GetSkillClassificationLabel(baseSkill); + string allowedClassification = + $"{SkillClassificationUtility.GetAllowedRoleSummary(gem.AllowedSkillRoles)}/" + + $"{SkillClassificationUtility.GetAllowedActivationSummary(gem.AllowedSkillActivationTypes)}"; + reason = $"장착 가능한 스킬 조합이 아닙니다. Skill={skillClassification}, Allowed={allowedClassification}"; return false; } @@ -476,7 +480,9 @@ namespace Colosseum.Skills if (gem.IsCategoryIncompatible(otherGem.Category) || otherGem.IsCategoryIncompatible(gem.Category)) { - reason = $"{gem.Category} / {otherGem.Category} 분류 조합은 허용되지 않습니다."; + reason = + $"{SkillClassificationUtility.GetGemCategoryLabel(gem.Category)} / " + + $"{SkillClassificationUtility.GetGemCategoryLabel(otherGem.Category)} 효과 분류 조합은 허용되지 않습니다."; return false; } diff --git a/Assets/_Game/Scripts/UI/SkillGemInventoryUI.cs b/Assets/_Game/Scripts/UI/SkillGemInventoryUI.cs new file mode 100644 index 00000000..87c90e8e --- /dev/null +++ b/Assets/_Game/Scripts/UI/SkillGemInventoryUI.cs @@ -0,0 +1,1348 @@ +using System; +using System.Collections.Generic; +using System.Text; + +using TMPro; + +using UnityEngine; +using UnityEngine.InputSystem; +using UnityEngine.UI; + +using Colosseum.Player; +using Colosseum.Skills; + +#if UNITY_EDITOR +using UnityEditor; +#endif + +namespace Colosseum.UI +{ + /// + /// 젬 보관함과 스킬별 장착/탈착 UI를 관리합니다. + /// + public class SkillGemInventoryUI : MonoBehaviour + { + private const string DefaultGemSearchFolder = "Assets/_Game/Data/SkillGems"; + + [Serializable] + private class SkillGemStorageEntry + { + [SerializeField] private SkillGemData gem; + [Min(0)] [SerializeField] private int quantity = 1; + + public SkillGemData Gem + { + get => gem; + set => gem = value; + } + + public int Quantity + { + get => quantity; + set => quantity = Mathf.Max(0, value); + } + } + + [Header("Toggle")] + [Tooltip("젬 UI를 여닫는 키")] + [SerializeField] private Key toggleKey = Key.G; + [Tooltip("토글 버튼에 표시할 텍스트")] + [SerializeField] private string toggleButtonLabel = "젬"; + [Tooltip("토글 버튼의 캔버스 기준 위치")] + [SerializeField] private Vector2 toggleButtonAnchoredPosition = new Vector2(-48f, 164f); + + [Header("Storage")] + [Tooltip("젬 보관 수량")] + [SerializeField] private List ownedGemEntries = new(); + + [Header("Debug")] + [Tooltip("플레이 모드 시작 시 패널을 자동으로 엽니다.")] + [SerializeField] private bool openOnStartInPlayMode = false; + + #if UNITY_EDITOR + [Header("Editor Auto Collect")] + [Tooltip("에디터에서 젬 자산을 자동으로 수집합니다.")] + [SerializeField] private bool autoCollectOwnedGemsInEditor = true; + [Tooltip("자동 수집할 젬 자산 폴더")] + [SerializeField] private string gemSearchFolder = DefaultGemSearchFolder; + [Tooltip("자동 수집 시 신규 젬 기본 수량")] + [Min(0)] [SerializeField] private int autoCollectedGemQuantity = 1; + #endif + + [Header("Style")] + [SerializeField] private Color panelBackgroundColor = new Color(0.08f, 0.08f, 0.11f, 0.96f); + [SerializeField] private Color sectionBackgroundColor = new Color(0.14f, 0.14f, 0.18f, 0.95f); + [SerializeField] private Color buttonNormalColor = new Color(0.19f, 0.19f, 0.24f, 0.96f); + [SerializeField] private Color buttonSelectedColor = new Color(0.48f, 0.32f, 0.16f, 0.96f); + [SerializeField] private Color buttonDisabledColor = new Color(0.12f, 0.12f, 0.15f, 0.65f); + [SerializeField] private Color statusNormalColor = new Color(0.86f, 0.85f, 0.78f, 1f); + [SerializeField] private Color statusErrorColor = new Color(1f, 0.52f, 0.45f, 1f); + + private static readonly string[] SlotDisplayNames = + { + "좌클릭", + "우클릭", + "기술 1", + "기술 2", + "기술 3", + "기술 4", + "긴급 회피", + }; + + private PlayerSkillInput playerSkillInput; + private PlayerMovement playerMovement; + private Canvas parentCanvas; + private RectTransform canvasRectTransform; + private TMP_FontAsset sharedFont; + + private GameObject overlayRoot; + private RectTransform panelRectTransform; + + private RectTransform skillListContent; + private RectTransform socketListContent; + private RectTransform gemListContent; + + private TextMeshProUGUI storageSummaryText; + private TextMeshProUGUI skillSummaryText; + private TextMeshProUGUI gemDetailText; + private TextMeshProUGUI statusText; + private Button equipButton; + private Button unequipButton; + private TextMeshProUGUI equipButtonLabel; + private TextMeshProUGUI unequipButtonLabel; + + private bool isPanelVisible; + private bool uiInitialized; + private int selectedSkillSlotIndex; + private int selectedSocketIndex; + private SkillGemData selectedGem; + private bool previousCursorVisible; + private CursorLockMode previousCursorLockState; + + private void Awake() + { + EnsureUi(); + HidePanelImmediate(); + } + + private void Start() + { + EnsureUi(); + FindLocalPlayer(); + RefreshAll(); + + if (Application.isPlaying && openOnStartInPlayMode) + { + SetPanelVisible(true); + } + } + + private void Update() + { + if (playerSkillInput == null) + { + FindLocalPlayer(); + } + + if (Keyboard.current == null) + return; + + if (Keyboard.current[toggleKey].wasPressedThisFrame) + { + TogglePanelVisibility(); + } + else if (isPanelVisible && Keyboard.current.escapeKey.wasPressedThisFrame) + { + SetPanelVisible(false); + } + } + + private void OnDestroy() + { + UnsubscribeFromPlayer(); + SetGameplayInputBlocked(false); + RestoreCursorState(); + } + + #if UNITY_EDITOR + private void OnValidate() + { + if (Application.isPlaying) + return; + + AutoCollectOwnedGemsInEditor(); + } + #endif + + private void FindLocalPlayer() + { + PlayerSkillInput[] players = FindObjectsByType(FindObjectsSortMode.None); + for (int i = 0; i < players.Length; i++) + { + if (!players[i].IsOwner) + continue; + + if (playerSkillInput == players[i]) + return; + + SetTarget(players[i]); + return; + } + } + + private void SetTarget(PlayerSkillInput target) + { + UnsubscribeFromPlayer(); + playerSkillInput = target; + playerMovement = playerSkillInput != null ? playerSkillInput.GetComponent() : null; + + if (playerSkillInput != null) + { + playerSkillInput.OnSkillSlotsChanged += HandleSkillSlotsChanged; + } + + if (isPanelVisible) + { + SetGameplayInputBlocked(true); + } + + RefreshAll(); + } + + private void UnsubscribeFromPlayer() + { + if (playerSkillInput != null) + { + playerSkillInput.OnSkillSlotsChanged -= HandleSkillSlotsChanged; + } + } + + private void HandleSkillSlotsChanged() + { + RefreshAll(); + } + + private void TogglePanelVisibility() + { + SetPanelVisible(!isPanelVisible); + } + + private void SetPanelVisible(bool visible) + { + EnsureUi(); + isPanelVisible = visible; + UpdatePanelSize(); + + if (overlayRoot != null) + { + overlayRoot.SetActive(visible); + } + + if (visible) + { + RememberCursorState(); + Cursor.visible = true; + Cursor.lockState = CursorLockMode.None; + SetGameplayInputBlocked(true); + RefreshAll(); + } + else + { + SetGameplayInputBlocked(false); + RestoreCursorState(); + } + } + + private void HidePanelImmediate() + { + isPanelVisible = false; + if (overlayRoot != null) + { + overlayRoot.SetActive(false); + } + } + + private void RememberCursorState() + { + previousCursorVisible = Cursor.visible; + previousCursorLockState = Cursor.lockState; + } + + private void RestoreCursorState() + { + Cursor.visible = previousCursorVisible; + Cursor.lockState = previousCursorLockState; + } + + private void SetGameplayInputBlocked(bool blocked) + { + if (playerMovement != null) + { + playerMovement.SetGameplayInputEnabled(!blocked); + } + + if (playerSkillInput != null) + { + playerSkillInput.SetGameplayInputEnabled(!blocked); + } + } + + private void EnsureUi() + { + if (uiInitialized) + return; + + parentCanvas = GetComponentInParent(); + canvasRectTransform = parentCanvas != null ? parentCanvas.GetComponent() : null; + + if (parentCanvas == null || canvasRectTransform == null) + { + Debug.LogWarning("[SkillGemInventoryUI] Canvas를 찾지 못했습니다."); + return; + } + + TextMeshProUGUI referenceText = GetComponentInChildren(true); + sharedFont = referenceText != null ? referenceText.font : TMP_Settings.defaultFontAsset; + CreateToggleButton(); + CreateOverlay(); + UpdatePanelSize(); + uiInitialized = true; + } + + private void UpdatePanelSize() + { + if (panelRectTransform == null || canvasRectTransform == null) + return; + + Rect canvasRect = canvasRectTransform.rect; + float width = Mathf.Clamp(canvasRect.width - 120f, 920f, 1380f); + float height = Mathf.Clamp(canvasRect.height - 120f, 620f, 820f); + panelRectTransform.sizeDelta = new Vector2(width, height); + } + + private void CreateToggleButton() + { + GameObject buttonObject = CreateUiObject("Button_GemInventory", canvasRectTransform); + RectTransform buttonRect = buttonObject.AddComponent(); + buttonRect.anchorMin = new Vector2(1f, 0f); + buttonRect.anchorMax = new Vector2(1f, 0f); + buttonRect.pivot = new Vector2(1f, 0f); + buttonRect.anchoredPosition = toggleButtonAnchoredPosition; + buttonRect.sizeDelta = new Vector2(72f, 34f); + + Image buttonImage = buttonObject.AddComponent(); + buttonImage.color = buttonNormalColor; + Button toggleButton = buttonObject.AddComponent