feat: 스킬 레지스트리 도입으로 빌드 스킬 포함 보장 및 기본 장착 구조 개편

- SkillRegistry ScriptableObject 추가: _Skill_Player_ 에셋을 자동 수집하여 빌드 포함 보장
- PlayerSkillInput에 Registry 참조 추가 및 AutoRegisterPlayerSkills 개편
  - 슬롯 0~5는 Inspector에서 디버그용 수동 설정 가능 (AutoRegister 불개입)
  - 슬롯 6(회피)에 구르기 스킬 자동 유지
  - OnValidate 호출 순서 변경: SyncLegacySkillsToLoadoutEntries → AutoRegisterPlayerSkills
- 플레이어 프리팹에 SkillRegistry 에셋 할당 (14개 플레이어 스킬 수집됨)
- 에디터 전용 EditorAllPlayerSkills 정적 프로퍼티로 전체 스킬 목록 접근 가능
This commit is contained in:
2026-04-02 10:58:23 +09:00
parent 5c2b9ccb69
commit eadf9a7bde
6 changed files with 293 additions and 12 deletions

View File

@@ -0,0 +1,146 @@
using System;
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using System.IO;
using System.Text.RegularExpressions;
using UnityEditor;
#endif
namespace Colosseum.Skills
{
/// <summary>
/// 프로젝트의 모든 스킬 데이터를 중앙에서 관리하는 ScriptableObject.
/// 이 에셋을 프리팹이나 씬에서 참조하면, 등록된 모든 스킬이 빌드에 자동 포함됩니다.
/// </summary>
[CreateAssetMenu(fileName = "SkillRegistry", menuName = "Colosseum/SkillRegistry")]
public class SkillRegistry : ScriptableObject
{
private const string PlayerSkillSearchFolder = "Assets/_Game/Data/Skills";
private const string PlayerSkillNameFilter = "_Skill_Player_";
[Header("Player Skills")]
[Tooltip("모든 플레이어 스킬 데이터 (에디터에서 자동 수집됨)")]
[SerializeField] private List<SkillData> playerSkills = new();
/// <summary>
/// 등록된 모든 플레이어 스킬 목록
/// </summary>
public IReadOnlyList<SkillData> PlayerSkills => playerSkills;
/// <summary>
/// 스킬 이름으로 플레이어 스킬을 조회합니다.
/// </summary>
public SkillData GetPlayerSkill(string skillName)
{
if (string.IsNullOrEmpty(skillName))
return null;
for (int i = 0; i < playerSkills.Count; i++)
{
SkillData skill = playerSkills[i];
if (skill != null && string.Equals(skill.SkillName, skillName, StringComparison.Ordinal))
return skill;
}
return null;
}
/// <summary>
/// 에셋 이름으로 플레이어 스킬을 조회합니다.
/// </summary>
public SkillData GetPlayerSkillByAssetName(string assetName)
{
if (string.IsNullOrEmpty(assetName))
return null;
for (int i = 0; i < playerSkills.Count; i++)
{
SkillData skill = playerSkills[i];
if (skill != null && string.Equals(skill.name, assetName, StringComparison.Ordinal))
return skill;
}
return null;
}
/// <summary>
/// 이름에 지정 문자열이 포함된 첫 번째 플레이어 스킬을 조회합니다.
/// </summary>
public SkillData FindPlayerSkillByNameContains(string fragment)
{
if (string.IsNullOrEmpty(fragment))
return null;
for (int i = 0; i < playerSkills.Count; i++)
{
SkillData skill = playerSkills[i];
if (skill != null && skill.name.IndexOf(fragment, StringComparison.OrdinalIgnoreCase) >= 0)
return skill;
}
return null;
}
#if UNITY_EDITOR
/// <summary>
/// "_Skill_Player_" 이름이 포함된 모든 SkillData를 에디터에서 자동 수집합니다.
/// OnValidate에서 호출되어 에셋 변경 시 자동 동기화됩니다.
/// </summary>
public void AutoCollectPlayerSkills()
{
if (!AssetDatabase.IsValidFolder(PlayerSkillSearchFolder))
{
Debug.LogWarning($"[SkillRegistry] 스킬 검색 폴더가 없습니다: {PlayerSkillSearchFolder}", this);
return;
}
string[] guids = AssetDatabase.FindAssets("t:SkillData", new[] { PlayerSkillSearchFolder });
var skills = new List<SkillData>();
foreach (string guid in guids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
string assetName = Path.GetFileNameWithoutExtension(path);
if (assetName.IndexOf(PlayerSkillNameFilter, StringComparison.OrdinalIgnoreCase) >= 0)
{
SkillData skill = AssetDatabase.LoadAssetAtPath<SkillData>(path);
if (skill != null)
skills.Add(skill);
}
}
skills.Sort((a, b) => string.Compare(a.name, b.name, StringComparison.Ordinal));
bool changed = playerSkills.Count != skills.Count;
if (!changed)
{
for (int i = 0; i < skills.Count; i++)
{
if (playerSkills[i] != skills[i])
{
changed = true;
break;
}
}
}
if (changed)
{
playerSkills.Clear();
playerSkills.AddRange(skills);
Debug.Log($"[SkillRegistry] 자동 수집: {skills.Count}개 Player 스킬", this);
}
}
private void OnValidate()
{
AutoCollectPlayerSkills();
}
#endif
}
}