- 옵시디언 기준의 역할/발동 타입 분류를 스킬·젬 데이터와 장착 검증 로직에 반영 - 젬 보관 UI와 퀵슬롯 표시를 새 분류 및 실제 마나/쿨타임 계산 기준으로 갱신 - 테스트 스킬/젬 자산을 에디터 메뉴로 동기화하고 Unity 컴파일 및 플레이 검증 완료
1349 lines
52 KiB
C#
1349 lines
52 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// 젬 보관함과 스킬별 장착/탈착 UI를 관리합니다.
|
|
/// </summary>
|
|
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<SkillGemStorageEntry> 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<PlayerSkillInput>(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<PlayerMovement>() : 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<Canvas>();
|
|
canvasRectTransform = parentCanvas != null ? parentCanvas.GetComponent<RectTransform>() : null;
|
|
|
|
if (parentCanvas == null || canvasRectTransform == null)
|
|
{
|
|
Debug.LogWarning("[SkillGemInventoryUI] Canvas를 찾지 못했습니다.");
|
|
return;
|
|
}
|
|
|
|
TextMeshProUGUI referenceText = GetComponentInChildren<TextMeshProUGUI>(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<RectTransform>();
|
|
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<Image>();
|
|
buttonImage.color = buttonNormalColor;
|
|
Button toggleButton = buttonObject.AddComponent<Button>();
|
|
toggleButton.targetGraphic = buttonImage;
|
|
toggleButton.onClick.AddListener(TogglePanelVisibility);
|
|
|
|
TextMeshProUGUI label = CreateFillLabel(buttonRect, "Label", toggleButtonLabel, 18f, TextAlignmentOptions.Center);
|
|
label.textWrappingMode = TextWrappingModes.NoWrap;
|
|
}
|
|
|
|
private void CreateOverlay()
|
|
{
|
|
overlayRoot = CreateUiObject("Overlay_GemInventory", canvasRectTransform);
|
|
RectTransform overlayRect = overlayRoot.AddComponent<RectTransform>();
|
|
StretchToParent(overlayRect);
|
|
|
|
GameObject backdropObject = CreateUiObject("Backdrop", overlayRect);
|
|
RectTransform backdropRect = backdropObject.AddComponent<RectTransform>();
|
|
StretchToParent(backdropRect);
|
|
Image backdropImage = backdropObject.AddComponent<Image>();
|
|
backdropImage.color = new Color(0f, 0f, 0f, 0.38f);
|
|
Button backdropButton = backdropObject.AddComponent<Button>();
|
|
backdropButton.targetGraphic = backdropImage;
|
|
backdropButton.onClick.AddListener(() => SetPanelVisible(false));
|
|
|
|
GameObject panelObject = CreateUiObject("Panel", overlayRect);
|
|
panelRectTransform = panelObject.AddComponent<RectTransform>();
|
|
panelRectTransform.anchorMin = new Vector2(0.5f, 0.5f);
|
|
panelRectTransform.anchorMax = new Vector2(0.5f, 0.5f);
|
|
panelRectTransform.pivot = new Vector2(0.5f, 0.5f);
|
|
panelRectTransform.anchoredPosition = Vector2.zero;
|
|
panelRectTransform.sizeDelta = new Vector2(1160f, 680f);
|
|
|
|
Image panelImage = panelObject.AddComponent<Image>();
|
|
panelImage.color = panelBackgroundColor;
|
|
|
|
VerticalLayoutGroup panelLayout = panelObject.AddComponent<VerticalLayoutGroup>();
|
|
panelLayout.padding = new RectOffset(18, 18, 18, 18);
|
|
panelLayout.spacing = 14f;
|
|
panelLayout.childAlignment = TextAnchor.UpperLeft;
|
|
panelLayout.childControlWidth = true;
|
|
panelLayout.childControlHeight = true;
|
|
panelLayout.childForceExpandWidth = true;
|
|
panelLayout.childForceExpandHeight = false;
|
|
|
|
ContentSizeFitter fitter = panelObject.AddComponent<ContentSizeFitter>();
|
|
fitter.verticalFit = ContentSizeFitter.FitMode.Unconstrained;
|
|
fitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained;
|
|
|
|
CreateHeader(panelRectTransform);
|
|
CreateBody(panelRectTransform);
|
|
CreateFooter(panelRectTransform);
|
|
}
|
|
|
|
private void CreateHeader(RectTransform parent)
|
|
{
|
|
RectTransform header = CreateContainer(parent, "Header", 96f);
|
|
HorizontalLayoutGroup layout = header.gameObject.AddComponent<HorizontalLayoutGroup>();
|
|
layout.childAlignment = TextAnchor.MiddleLeft;
|
|
layout.childControlWidth = true;
|
|
layout.childControlHeight = true;
|
|
layout.childForceExpandWidth = false;
|
|
layout.childForceExpandHeight = true;
|
|
layout.spacing = 12f;
|
|
|
|
RectTransform titleGroup = CreateUiObject("TitleGroup", header).AddComponent<RectTransform>();
|
|
LayoutElement titleLayout = titleGroup.gameObject.AddComponent<LayoutElement>();
|
|
titleLayout.flexibleWidth = 1f;
|
|
VerticalLayoutGroup titleGroupLayout = titleGroup.gameObject.AddComponent<VerticalLayoutGroup>();
|
|
titleGroupLayout.spacing = 4f;
|
|
titleGroupLayout.childAlignment = TextAnchor.MiddleLeft;
|
|
titleGroupLayout.childControlWidth = true;
|
|
titleGroupLayout.childControlHeight = true;
|
|
titleGroupLayout.childForceExpandWidth = true;
|
|
titleGroupLayout.childForceExpandHeight = false;
|
|
|
|
TextMeshProUGUI title = CreateAutoLabel(titleGroup, "Title", "젬 보관함", 30f, TextAlignmentOptions.Left);
|
|
title.fontStyle = FontStyles.Bold;
|
|
title.textWrappingMode = TextWrappingModes.NoWrap;
|
|
LayoutElement titleTextLayout = title.gameObject.AddComponent<LayoutElement>();
|
|
titleTextLayout.minHeight = 36f;
|
|
|
|
TextMeshProUGUI subtitle = CreateAutoLabel(titleGroup, "Subtitle", "G 키 또는 버튼으로 열고 닫을 수 있습니다.", 18f, TextAlignmentOptions.Left);
|
|
subtitle.color = new Color(0.82f, 0.8f, 0.72f, 0.92f);
|
|
subtitle.textWrappingMode = TextWrappingModes.NoWrap;
|
|
LayoutElement subtitleTextLayout = subtitle.gameObject.AddComponent<LayoutElement>();
|
|
subtitleTextLayout.minHeight = 24f;
|
|
|
|
Button closeButton = CreateButton(header, "Button_Close", "닫기", new Vector2(90f, 42f), () => SetPanelVisible(false), out _);
|
|
LayoutElement closeButtonLayout = closeButton.gameObject.AddComponent<LayoutElement>();
|
|
closeButtonLayout.minWidth = 90f;
|
|
closeButtonLayout.preferredWidth = 90f;
|
|
closeButton.image.color = new Color(0.32f, 0.18f, 0.18f, 0.95f);
|
|
}
|
|
|
|
private void CreateBody(RectTransform parent)
|
|
{
|
|
RectTransform body = CreateContainer(parent, "Body", 0f);
|
|
LayoutElement bodyLayout = body.GetComponent<LayoutElement>();
|
|
bodyLayout.flexibleHeight = 1f;
|
|
bodyLayout.minHeight = 0f;
|
|
|
|
HorizontalLayoutGroup layout = body.gameObject.AddComponent<HorizontalLayoutGroup>();
|
|
layout.childAlignment = TextAnchor.UpperLeft;
|
|
layout.spacing = 14f;
|
|
layout.childControlWidth = true;
|
|
layout.childControlHeight = true;
|
|
layout.childForceExpandWidth = true;
|
|
layout.childForceExpandHeight = true;
|
|
|
|
CreateSkillSection(body);
|
|
CreateSocketSection(body);
|
|
CreateStorageSection(body);
|
|
}
|
|
|
|
private void CreateFooter(RectTransform parent)
|
|
{
|
|
RectTransform footer = CreateContainer(parent, "Footer", 64f);
|
|
Image footerImage = footer.gameObject.AddComponent<Image>();
|
|
footerImage.color = sectionBackgroundColor;
|
|
|
|
HorizontalLayoutGroup layout = footer.gameObject.AddComponent<HorizontalLayoutGroup>();
|
|
layout.padding = new RectOffset(14, 14, 12, 12);
|
|
layout.spacing = 10f;
|
|
layout.childControlWidth = true;
|
|
layout.childControlHeight = true;
|
|
layout.childForceExpandWidth = false;
|
|
layout.childForceExpandHeight = true;
|
|
layout.childAlignment = TextAnchor.MiddleLeft;
|
|
|
|
statusText = CreateAutoLabel(footer, "Label_Status", "로컬 플레이어를 찾는 중입니다.", 19f, TextAlignmentOptions.MidlineLeft);
|
|
LayoutElement statusLayout = statusText.gameObject.AddComponent<LayoutElement>();
|
|
statusLayout.flexibleWidth = 1f;
|
|
statusText.color = statusNormalColor;
|
|
}
|
|
|
|
private void CreateSkillSection(RectTransform parent)
|
|
{
|
|
RectTransform section = CreateSectionRoot(parent, "Section_Skills");
|
|
LayoutElement sectionLayout = section.GetComponent<LayoutElement>();
|
|
sectionLayout.preferredWidth = 300f;
|
|
CreateSectionTitle(section, "스킬 슬롯");
|
|
skillSummaryText = CreateInfoText(section, "Label_SkillSummary", 100f);
|
|
RectTransform scroll = CreateScrollView(section, "Scroll_Skills", out skillListContent);
|
|
LayoutElement scrollLayout = scroll.gameObject.AddComponent<LayoutElement>();
|
|
scrollLayout.flexibleHeight = 1f;
|
|
scrollLayout.minHeight = 180f;
|
|
}
|
|
|
|
private void CreateSocketSection(RectTransform parent)
|
|
{
|
|
RectTransform section = CreateSectionRoot(parent, "Section_Sockets");
|
|
LayoutElement sectionLayout = section.GetComponent<LayoutElement>();
|
|
sectionLayout.preferredWidth = 300f;
|
|
CreateSectionTitle(section, "장착 소켓");
|
|
|
|
RectTransform commandRow = CreateContainer(section, "Commands", 48f);
|
|
HorizontalLayoutGroup commandLayout = commandRow.gameObject.AddComponent<HorizontalLayoutGroup>();
|
|
commandLayout.spacing = 8f;
|
|
commandLayout.childControlWidth = true;
|
|
commandLayout.childControlHeight = true;
|
|
commandLayout.childForceExpandWidth = true;
|
|
commandLayout.childForceExpandHeight = true;
|
|
|
|
equipButton = CreateButton(commandRow, "Button_Equip", "선택한 젬 장착", Vector2.zero, TryEquipSelectedGem, out equipButtonLabel);
|
|
LayoutElement equipLayout = equipButton.gameObject.AddComponent<LayoutElement>();
|
|
equipLayout.flexibleWidth = 1f;
|
|
|
|
unequipButton = CreateButton(commandRow, "Button_Unequip", "선택한 젬 탈착", Vector2.zero, TryUnequipSelectedGem, out unequipButtonLabel);
|
|
LayoutElement unequipLayout = unequipButton.gameObject.AddComponent<LayoutElement>();
|
|
unequipLayout.flexibleWidth = 1f;
|
|
|
|
RectTransform scroll = CreateScrollView(section, "Scroll_Sockets", out socketListContent);
|
|
LayoutElement scrollLayout = scroll.gameObject.AddComponent<LayoutElement>();
|
|
scrollLayout.flexibleHeight = 1f;
|
|
scrollLayout.minHeight = 240f;
|
|
}
|
|
|
|
private void CreateStorageSection(RectTransform parent)
|
|
{
|
|
RectTransform section = CreateSectionRoot(parent, "Section_Storage");
|
|
LayoutElement sectionLayout = section.GetComponent<LayoutElement>();
|
|
sectionLayout.preferredWidth = 380f;
|
|
CreateSectionTitle(section, "젬 보관함");
|
|
storageSummaryText = CreateInfoText(section, "Label_StorageSummary", 72f);
|
|
|
|
RectTransform scroll = CreateScrollView(section, "Scroll_Gems", out gemListContent);
|
|
LayoutElement scrollLayout = scroll.gameObject.AddComponent<LayoutElement>();
|
|
scrollLayout.flexibleHeight = 1f;
|
|
scrollLayout.minHeight = 150f;
|
|
|
|
gemDetailText = CreateInfoText(section, "Label_GemDetail", 120f);
|
|
}
|
|
|
|
private RectTransform CreateSectionRoot(RectTransform parent, string name)
|
|
{
|
|
RectTransform section = CreateContainer(parent, name, 0f);
|
|
LayoutElement layout = section.GetComponent<LayoutElement>();
|
|
layout.flexibleWidth = 1f;
|
|
layout.flexibleHeight = 1f;
|
|
layout.minWidth = 0f;
|
|
|
|
Image image = section.gameObject.AddComponent<Image>();
|
|
image.color = sectionBackgroundColor;
|
|
|
|
VerticalLayoutGroup verticalLayout = section.gameObject.AddComponent<VerticalLayoutGroup>();
|
|
verticalLayout.padding = new RectOffset(14, 14, 14, 14);
|
|
verticalLayout.spacing = 10f;
|
|
verticalLayout.childAlignment = TextAnchor.UpperLeft;
|
|
verticalLayout.childControlWidth = true;
|
|
verticalLayout.childControlHeight = true;
|
|
verticalLayout.childForceExpandWidth = true;
|
|
verticalLayout.childForceExpandHeight = false;
|
|
return section;
|
|
}
|
|
|
|
private TextMeshProUGUI CreateSectionTitle(RectTransform parent, string text)
|
|
{
|
|
TextMeshProUGUI label = CreateAutoLabel(parent, "Title", text, 24f, TextAlignmentOptions.Left);
|
|
label.fontStyle = FontStyles.Bold;
|
|
LayoutElement layout = label.gameObject.AddComponent<LayoutElement>();
|
|
layout.minHeight = 34f;
|
|
return label;
|
|
}
|
|
|
|
private TextMeshProUGUI CreateInfoText(RectTransform parent, string name, float minHeight)
|
|
{
|
|
RectTransform container = CreateContainer(parent, $"{name}_Container", minHeight);
|
|
Image image = container.gameObject.AddComponent<Image>();
|
|
image.color = new Color(0f, 0f, 0f, 0.16f);
|
|
LayoutElement layout = container.GetComponent<LayoutElement>();
|
|
layout.minHeight = minHeight;
|
|
VerticalLayoutGroup containerLayout = container.gameObject.AddComponent<VerticalLayoutGroup>();
|
|
containerLayout.padding = new RectOffset(12, 12, 10, 10);
|
|
containerLayout.childAlignment = TextAnchor.UpperLeft;
|
|
containerLayout.childControlWidth = true;
|
|
containerLayout.childControlHeight = true;
|
|
containerLayout.childForceExpandWidth = true;
|
|
containerLayout.childForceExpandHeight = false;
|
|
|
|
ContentSizeFitter containerFitter = container.gameObject.AddComponent<ContentSizeFitter>();
|
|
containerFitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained;
|
|
containerFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
|
|
|
TextMeshProUGUI label = CreateAutoLabel(container, name, string.Empty, 18f, TextAlignmentOptions.TopLeft);
|
|
label.textWrappingMode = TextWrappingModes.Normal;
|
|
label.overflowMode = TextOverflowModes.Ellipsis;
|
|
return label;
|
|
}
|
|
|
|
private RectTransform CreateScrollView(RectTransform parent, string name, out RectTransform content)
|
|
{
|
|
GameObject rootObject = CreateUiObject(name, parent);
|
|
RectTransform rootRect = rootObject.AddComponent<RectTransform>();
|
|
|
|
Image rootImage = rootObject.AddComponent<Image>();
|
|
rootImage.color = new Color(0f, 0f, 0f, 0.12f);
|
|
|
|
ScrollRect scrollRect = rootObject.AddComponent<ScrollRect>();
|
|
scrollRect.horizontal = false;
|
|
scrollRect.vertical = true;
|
|
scrollRect.scrollSensitivity = 24f;
|
|
|
|
GameObject viewportObject = CreateUiObject("Viewport", rootRect);
|
|
RectTransform viewportRect = viewportObject.AddComponent<RectTransform>();
|
|
StretchToParent(viewportRect);
|
|
Image viewportImage = viewportObject.AddComponent<Image>();
|
|
viewportImage.color = new Color(1f, 1f, 1f, 0.01f);
|
|
Mask viewportMask = viewportObject.AddComponent<Mask>();
|
|
viewportMask.showMaskGraphic = false;
|
|
|
|
GameObject contentObject = CreateUiObject("Content", viewportRect);
|
|
content = contentObject.AddComponent<RectTransform>();
|
|
content.anchorMin = new Vector2(0f, 1f);
|
|
content.anchorMax = new Vector2(1f, 1f);
|
|
content.pivot = new Vector2(0.5f, 1f);
|
|
content.anchoredPosition = Vector2.zero;
|
|
content.sizeDelta = Vector2.zero;
|
|
|
|
VerticalLayoutGroup contentLayout = contentObject.AddComponent<VerticalLayoutGroup>();
|
|
contentLayout.padding = new RectOffset(8, 8, 8, 8);
|
|
contentLayout.spacing = 6f;
|
|
contentLayout.childAlignment = TextAnchor.UpperLeft;
|
|
contentLayout.childControlWidth = true;
|
|
contentLayout.childControlHeight = true;
|
|
contentLayout.childForceExpandWidth = true;
|
|
contentLayout.childForceExpandHeight = false;
|
|
|
|
ContentSizeFitter contentFitter = contentObject.AddComponent<ContentSizeFitter>();
|
|
contentFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
|
contentFitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained;
|
|
|
|
scrollRect.viewport = viewportRect;
|
|
scrollRect.content = content;
|
|
return rootRect;
|
|
}
|
|
|
|
private RectTransform CreateContainer(RectTransform parent, string name, float minHeight)
|
|
{
|
|
GameObject containerObject = CreateUiObject(name, parent);
|
|
RectTransform rectTransform = containerObject.AddComponent<RectTransform>();
|
|
LayoutElement layout = containerObject.AddComponent<LayoutElement>();
|
|
layout.minHeight = minHeight;
|
|
return rectTransform;
|
|
}
|
|
|
|
private Button CreateButton(RectTransform parent, string name, string labelText, Vector2 size, Action onClick, out TextMeshProUGUI label)
|
|
{
|
|
GameObject buttonObject = CreateUiObject(name, parent);
|
|
RectTransform rectTransform = buttonObject.AddComponent<RectTransform>();
|
|
if (size.sqrMagnitude > 0f)
|
|
{
|
|
rectTransform.sizeDelta = size;
|
|
}
|
|
|
|
Image image = buttonObject.AddComponent<Image>();
|
|
image.color = buttonNormalColor;
|
|
|
|
Button button = buttonObject.AddComponent<Button>();
|
|
button.targetGraphic = image;
|
|
button.onClick.AddListener(() => onClick?.Invoke());
|
|
|
|
label = CreateFillLabel(rectTransform, "Label", labelText, 18f, TextAlignmentOptions.Center);
|
|
label.textWrappingMode = TextWrappingModes.Normal;
|
|
label.margin = new Vector4(12f, 6f, 12f, 6f);
|
|
return button;
|
|
}
|
|
|
|
private TextMeshProUGUI CreateAutoLabel(RectTransform parent, string name, string text, float fontSize, TextAlignmentOptions alignment)
|
|
{
|
|
GameObject labelObject = CreateUiObject(name, parent);
|
|
RectTransform rectTransform = labelObject.AddComponent<RectTransform>();
|
|
rectTransform.anchorMin = new Vector2(0f, 1f);
|
|
rectTransform.anchorMax = new Vector2(1f, 1f);
|
|
rectTransform.pivot = new Vector2(0.5f, 1f);
|
|
rectTransform.sizeDelta = Vector2.zero;
|
|
|
|
TextMeshProUGUI label = labelObject.AddComponent<TextMeshProUGUI>();
|
|
label.font = sharedFont;
|
|
label.fontSize = fontSize;
|
|
label.color = new Color(0.94f, 0.92f, 0.86f, 1f);
|
|
label.text = text;
|
|
label.alignment = alignment;
|
|
label.textWrappingMode = TextWrappingModes.Normal;
|
|
ContentSizeFitter fitter = labelObject.AddComponent<ContentSizeFitter>();
|
|
fitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained;
|
|
fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
|
return label;
|
|
}
|
|
|
|
private TextMeshProUGUI CreateFillLabel(RectTransform parent, string name, string text, float fontSize, TextAlignmentOptions alignment)
|
|
{
|
|
TextMeshProUGUI label = CreateAutoLabel(parent, name, text, fontSize, alignment);
|
|
StretchToParent(label.rectTransform);
|
|
return label;
|
|
}
|
|
|
|
private static GameObject CreateUiObject(string name, RectTransform parent)
|
|
{
|
|
GameObject gameObject = new GameObject(name);
|
|
gameObject.transform.SetParent(parent, false);
|
|
return gameObject;
|
|
}
|
|
|
|
private static void StretchToParent(RectTransform rectTransform)
|
|
{
|
|
rectTransform.anchorMin = Vector2.zero;
|
|
rectTransform.anchorMax = Vector2.one;
|
|
rectTransform.pivot = new Vector2(0.5f, 0.5f);
|
|
rectTransform.offsetMin = Vector2.zero;
|
|
rectTransform.offsetMax = Vector2.zero;
|
|
}
|
|
|
|
private void RefreshAll()
|
|
{
|
|
if (!uiInitialized)
|
|
return;
|
|
|
|
UpdatePanelSize();
|
|
NormalizeSelection();
|
|
RefreshSkillSummary();
|
|
RefreshStorageSummary();
|
|
RebuildSkillButtons();
|
|
RebuildSocketButtons();
|
|
RebuildGemButtons();
|
|
RefreshGemDetail();
|
|
RefreshCommandButtons();
|
|
}
|
|
|
|
private void NormalizeSelection()
|
|
{
|
|
if (playerSkillInput == null)
|
|
{
|
|
selectedSkillSlotIndex = 0;
|
|
selectedSocketIndex = 0;
|
|
return;
|
|
}
|
|
|
|
int slotCount = playerSkillInput.SkillLoadoutEntries != null ? playerSkillInput.SkillLoadoutEntries.Length : 0;
|
|
if (slotCount <= 0)
|
|
{
|
|
selectedSkillSlotIndex = 0;
|
|
selectedSocketIndex = 0;
|
|
return;
|
|
}
|
|
|
|
selectedSkillSlotIndex = Mathf.Clamp(selectedSkillSlotIndex, 0, slotCount - 1);
|
|
|
|
SkillLoadoutEntry selectedEntry = playerSkillInput.GetSkillLoadout(selectedSkillSlotIndex);
|
|
int socketCount = selectedEntry?.SocketedGems != null ? selectedEntry.SocketedGems.Count : 0;
|
|
if (socketCount <= 0)
|
|
{
|
|
selectedSocketIndex = 0;
|
|
}
|
|
else
|
|
{
|
|
selectedSocketIndex = Mathf.Clamp(selectedSocketIndex, 0, socketCount - 1);
|
|
}
|
|
|
|
if (selectedGem != null && FindStorageEntry(selectedGem) == null)
|
|
{
|
|
selectedGem = null;
|
|
}
|
|
}
|
|
|
|
private void RefreshSkillSummary()
|
|
{
|
|
if (skillSummaryText == null)
|
|
return;
|
|
|
|
if (playerSkillInput == null)
|
|
{
|
|
skillSummaryText.text = "로컬 플레이어를 찾는 중입니다.\n플레이어가 생성되면 스킬과 젬 소켓을 표시합니다.";
|
|
return;
|
|
}
|
|
|
|
SkillLoadoutEntry entry = playerSkillInput.GetSkillLoadout(selectedSkillSlotIndex);
|
|
SkillData skill = entry?.BaseSkill;
|
|
|
|
if (skill == null)
|
|
{
|
|
skillSummaryText.text = $"{GetSlotDisplayName(selectedSkillSlotIndex)}\n비어 있는 슬롯입니다.";
|
|
return;
|
|
}
|
|
|
|
StringBuilder builder = new StringBuilder();
|
|
builder.AppendLine(GetSlotDisplayName(selectedSkillSlotIndex));
|
|
builder.AppendLine($"기반 기술: {skill.SkillName}");
|
|
builder.AppendLine($"분류: {SkillClassificationUtility.GetSkillClassificationLabel(skill)}");
|
|
builder.AppendLine($"젬 슬롯: {skill.MaxGemSlotCount}칸");
|
|
builder.AppendLine($"마나: {entry.GetResolvedManaCost():0.##} | 쿨타임: {entry.GetResolvedCooldown():0.##}초");
|
|
builder.Append($"장착 현황: {BuildSocketSummary(entry)}");
|
|
skillSummaryText.text = builder.ToString();
|
|
}
|
|
|
|
private void RefreshStorageSummary()
|
|
{
|
|
if (storageSummaryText == null)
|
|
return;
|
|
|
|
int gemTypeCount = 0;
|
|
int totalOwnedCount = 0;
|
|
int totalEquippedCount = 0;
|
|
|
|
for (int i = 0; i < ownedGemEntries.Count; i++)
|
|
{
|
|
SkillGemStorageEntry entry = ownedGemEntries[i];
|
|
if (entry == null || entry.Gem == null)
|
|
continue;
|
|
|
|
gemTypeCount++;
|
|
totalOwnedCount += entry.Quantity;
|
|
totalEquippedCount += GetEquippedCount(entry.Gem);
|
|
}
|
|
|
|
storageSummaryText.text =
|
|
$"보유 종류: {gemTypeCount}\n" +
|
|
$"총 보유 수량: {totalOwnedCount}\n" +
|
|
$"현재 장착 수량: {totalEquippedCount}";
|
|
}
|
|
|
|
private void RebuildSkillButtons()
|
|
{
|
|
if (skillListContent == null)
|
|
return;
|
|
|
|
ClearChildren(skillListContent);
|
|
|
|
if (playerSkillInput == null)
|
|
{
|
|
CreateListMessage(skillListContent, "플레이어 스킬 정보를 기다리는 중입니다.");
|
|
return;
|
|
}
|
|
|
|
int slotCount = playerSkillInput.SkillLoadoutEntries != null ? playerSkillInput.SkillLoadoutEntries.Length : 0;
|
|
for (int i = 0; i < slotCount; i++)
|
|
{
|
|
int capturedIndex = i;
|
|
SkillLoadoutEntry entry = playerSkillInput.GetSkillLoadout(i);
|
|
SkillData skill = entry?.BaseSkill;
|
|
|
|
string label =
|
|
$"{GetSlotDisplayName(i)}\n" +
|
|
$"{(skill != null ? skill.SkillName : "비어 있음")}\n" +
|
|
$"장착: {BuildSocketSummary(entry)}";
|
|
|
|
Button button = CreateListButton(skillListContent, $"Button_Skill_{i}", label, () =>
|
|
{
|
|
selectedSkillSlotIndex = capturedIndex;
|
|
selectedSocketIndex = 0;
|
|
RefreshAll();
|
|
});
|
|
|
|
ApplyButtonState(button, i == selectedSkillSlotIndex, true);
|
|
}
|
|
}
|
|
|
|
private void RebuildSocketButtons()
|
|
{
|
|
if (socketListContent == null)
|
|
return;
|
|
|
|
ClearChildren(socketListContent);
|
|
|
|
if (playerSkillInput == null)
|
|
{
|
|
CreateListMessage(socketListContent, "플레이어가 준비되면 소켓을 표시합니다.");
|
|
return;
|
|
}
|
|
|
|
SkillLoadoutEntry entry = playerSkillInput.GetSkillLoadout(selectedSkillSlotIndex);
|
|
SkillData skill = entry?.BaseSkill;
|
|
if (skill == null)
|
|
{
|
|
CreateListMessage(socketListContent, "비어 있는 슬롯입니다.");
|
|
return;
|
|
}
|
|
|
|
if (skill.MaxGemSlotCount <= 0)
|
|
{
|
|
CreateListMessage(socketListContent, "이 기술은 젬 슬롯이 없습니다.");
|
|
return;
|
|
}
|
|
|
|
for (int i = 0; i < skill.MaxGemSlotCount; i++)
|
|
{
|
|
int capturedIndex = i;
|
|
SkillGemData equippedGem = entry.GetGem(i);
|
|
string label =
|
|
$"소켓 {i + 1}\n" +
|
|
$"{(equippedGem != null ? equippedGem.GemName : "비어 있음")}";
|
|
|
|
Button button = CreateListButton(socketListContent, $"Button_Socket_{i}", label, () =>
|
|
{
|
|
selectedSocketIndex = capturedIndex;
|
|
RefreshAll();
|
|
});
|
|
|
|
ApplyButtonState(button, i == selectedSocketIndex, true);
|
|
}
|
|
}
|
|
|
|
private void RebuildGemButtons()
|
|
{
|
|
if (gemListContent == null)
|
|
return;
|
|
|
|
ClearChildren(gemListContent);
|
|
|
|
int createdCount = 0;
|
|
for (int i = 0; i < ownedGemEntries.Count; i++)
|
|
{
|
|
SkillGemStorageEntry entry = ownedGemEntries[i];
|
|
if (entry == null || entry.Gem == null)
|
|
continue;
|
|
|
|
int equippedCount = GetEquippedCount(entry.Gem);
|
|
int remainingCount = Mathf.Max(0, entry.Quantity - equippedCount);
|
|
SkillGemData gem = entry.Gem;
|
|
string label =
|
|
$"{gem.GemName}\n" +
|
|
$"효과: {SkillClassificationUtility.GetGemCategoryLabel(gem.Category)} | " +
|
|
$"장착: {GetGemAllowedSummary(gem)}\n" +
|
|
$"남은 수량 {remainingCount}/{entry.Quantity}";
|
|
|
|
Button button = CreateListButton(gemListContent, $"Button_Gem_{i}", label, () =>
|
|
{
|
|
selectedGem = gem;
|
|
RefreshAll();
|
|
});
|
|
|
|
ApplyButtonState(button, selectedGem == gem, entry.Quantity > 0);
|
|
createdCount++;
|
|
}
|
|
|
|
if (createdCount <= 0)
|
|
{
|
|
CreateListMessage(gemListContent, "보유한 젬이 없습니다.");
|
|
}
|
|
}
|
|
|
|
private void RefreshGemDetail()
|
|
{
|
|
if (gemDetailText == null)
|
|
return;
|
|
|
|
if (selectedGem == null)
|
|
{
|
|
gemDetailText.text = "보관함에서 젬을 선택하면 설명과 수치 정보를 표시합니다.";
|
|
return;
|
|
}
|
|
|
|
SkillGemStorageEntry storageEntry = FindStorageEntry(selectedGem);
|
|
int totalQuantity = storageEntry != null ? storageEntry.Quantity : 0;
|
|
int equippedCount = GetEquippedCount(selectedGem);
|
|
int remainingCount = Mathf.Max(0, totalQuantity - equippedCount);
|
|
|
|
StringBuilder builder = new StringBuilder();
|
|
builder.AppendLine(selectedGem.GemName);
|
|
builder.AppendLine($"효과 분류: {SkillClassificationUtility.GetGemCategoryLabel(selectedGem.Category)}");
|
|
builder.AppendLine($"장착 가능: {GetGemAllowedSummary(selectedGem)}");
|
|
builder.AppendLine($"보관: {remainingCount}/{totalQuantity}");
|
|
|
|
if (!string.IsNullOrWhiteSpace(selectedGem.Description))
|
|
{
|
|
builder.AppendLine();
|
|
builder.AppendLine(selectedGem.Description);
|
|
}
|
|
|
|
builder.AppendLine();
|
|
builder.AppendLine(BuildGemStatSummary(selectedGem));
|
|
|
|
string incompatibleSummary = BuildIncompatibleSummary(selectedGem);
|
|
if (!string.IsNullOrEmpty(incompatibleSummary))
|
|
{
|
|
builder.AppendLine();
|
|
builder.AppendLine(incompatibleSummary);
|
|
}
|
|
|
|
gemDetailText.text = builder.ToString().TrimEnd();
|
|
}
|
|
|
|
private void RefreshCommandButtons()
|
|
{
|
|
if (equipButton == null || unequipButton == null)
|
|
return;
|
|
|
|
string equipReason = GetEquipBlockedReason();
|
|
bool canEquip = string.IsNullOrEmpty(equipReason);
|
|
ApplyButtonState(equipButton, false, canEquip);
|
|
ApplyButtonState(unequipButton, false, CanUnequipSelectedSocket());
|
|
|
|
if (equipButtonLabel != null)
|
|
{
|
|
equipButtonLabel.text = canEquip ? "선택한 젬 장착" : "장착 불가";
|
|
}
|
|
|
|
if (unequipButtonLabel != null)
|
|
{
|
|
unequipButtonLabel.text = CanUnequipSelectedSocket() ? "선택한 젬 탈착" : "탈착할 젬 없음";
|
|
}
|
|
|
|
if (playerSkillInput == null)
|
|
{
|
|
SetStatusMessage("로컬 플레이어를 찾는 중입니다.", false);
|
|
}
|
|
else if (isPanelVisible)
|
|
{
|
|
SetStatusMessage(canEquip ? BuildSelectionHint() : equipReason, !canEquip);
|
|
}
|
|
}
|
|
|
|
private string BuildSelectionHint()
|
|
{
|
|
SkillLoadoutEntry entry = playerSkillInput != null ? playerSkillInput.GetSkillLoadout(selectedSkillSlotIndex) : null;
|
|
SkillData skill = entry?.BaseSkill;
|
|
|
|
if (skill == null)
|
|
return "젬을 장착할 기술 슬롯을 선택하세요.";
|
|
|
|
if (selectedGem == null)
|
|
return "보관함에서 장착할 젬을 선택하세요.";
|
|
|
|
return $"'{selectedGem.GemName}'을(를) {GetSlotDisplayName(selectedSkillSlotIndex)} 소켓 {selectedSocketIndex + 1}에 장착할 수 있습니다.";
|
|
}
|
|
|
|
private void TryEquipSelectedGem()
|
|
{
|
|
string blockedReason = GetEquipBlockedReason();
|
|
if (!string.IsNullOrEmpty(blockedReason))
|
|
{
|
|
SetStatusMessage(blockedReason, true);
|
|
RefreshAll();
|
|
return;
|
|
}
|
|
|
|
playerSkillInput.SetSkillGem(selectedSkillSlotIndex, selectedSocketIndex, selectedGem);
|
|
SetStatusMessage($"'{selectedGem.GemName}' 장착 완료", false);
|
|
RefreshAll();
|
|
}
|
|
|
|
private void TryUnequipSelectedGem()
|
|
{
|
|
if (playerSkillInput == null)
|
|
{
|
|
SetStatusMessage("로컬 플레이어를 찾지 못했습니다.", true);
|
|
return;
|
|
}
|
|
|
|
SkillLoadoutEntry entry = playerSkillInput.GetSkillLoadout(selectedSkillSlotIndex);
|
|
SkillGemData equippedGem = entry?.GetGem(selectedSocketIndex);
|
|
if (equippedGem == null)
|
|
{
|
|
SetStatusMessage("선택한 소켓에 장착된 젬이 없습니다.", true);
|
|
RefreshAll();
|
|
return;
|
|
}
|
|
|
|
playerSkillInput.SetSkillGem(selectedSkillSlotIndex, selectedSocketIndex, null);
|
|
SetStatusMessage($"'{equippedGem.GemName}' 탈착 완료", false);
|
|
RefreshAll();
|
|
}
|
|
|
|
private bool CanUnequipSelectedSocket()
|
|
{
|
|
if (playerSkillInput == null)
|
|
return false;
|
|
|
|
SkillLoadoutEntry entry = playerSkillInput.GetSkillLoadout(selectedSkillSlotIndex);
|
|
return entry?.GetGem(selectedSocketIndex) != null;
|
|
}
|
|
|
|
private string GetEquipBlockedReason()
|
|
{
|
|
if (playerSkillInput == null)
|
|
return "로컬 플레이어를 찾는 중입니다.";
|
|
|
|
SkillLoadoutEntry entry = playerSkillInput.GetSkillLoadout(selectedSkillSlotIndex);
|
|
SkillData skill = entry?.BaseSkill;
|
|
if (skill == null)
|
|
return "비어 있는 슬롯에는 젬을 장착할 수 없습니다.";
|
|
|
|
if (skill.MaxGemSlotCount <= 0)
|
|
return "이 기술은 젬 슬롯이 없습니다.";
|
|
|
|
if (selectedSocketIndex < 0 || selectedSocketIndex >= skill.MaxGemSlotCount)
|
|
return "유효한 소켓을 선택하세요.";
|
|
|
|
if (selectedGem == null)
|
|
return "보관함에서 장착할 젬을 선택하세요.";
|
|
|
|
SkillGemStorageEntry storageEntry = FindStorageEntry(selectedGem);
|
|
if (storageEntry == null || storageEntry.Quantity <= 0)
|
|
return "선택한 젬이 보관함에 없습니다.";
|
|
|
|
SkillGemData currentGem = entry.GetGem(selectedSocketIndex);
|
|
if (currentGem == selectedGem)
|
|
return "이미 같은 젬이 장착되어 있습니다.";
|
|
|
|
int remainingCount = storageEntry.Quantity - GetEquippedCount(selectedGem);
|
|
if (remainingCount <= 0)
|
|
return "보유한 수량을 모두 장착했습니다.";
|
|
|
|
SkillLoadoutEntry validationCopy = entry.CreateCopy();
|
|
if (!validationCopy.TrySetGem(selectedSocketIndex, selectedGem, out string reason))
|
|
return reason;
|
|
|
|
return string.Empty;
|
|
}
|
|
|
|
private SkillGemStorageEntry FindStorageEntry(SkillGemData gem)
|
|
{
|
|
if (gem == null)
|
|
return null;
|
|
|
|
for (int i = 0; i < ownedGemEntries.Count; i++)
|
|
{
|
|
SkillGemStorageEntry entry = ownedGemEntries[i];
|
|
if (entry != null && entry.Gem == gem)
|
|
return entry;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private int GetEquippedCount(SkillGemData gem)
|
|
{
|
|
if (gem == null || playerSkillInput == null || playerSkillInput.SkillLoadoutEntries == null)
|
|
return 0;
|
|
|
|
int count = 0;
|
|
for (int slotIndex = 0; slotIndex < playerSkillInput.SkillLoadoutEntries.Length; slotIndex++)
|
|
{
|
|
SkillLoadoutEntry entry = playerSkillInput.GetSkillLoadout(slotIndex);
|
|
if (entry?.SocketedGems == null)
|
|
continue;
|
|
|
|
for (int socketIndex = 0; socketIndex < entry.SocketedGems.Count; socketIndex++)
|
|
{
|
|
if (entry.GetGem(socketIndex) == gem)
|
|
{
|
|
count++;
|
|
}
|
|
}
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
private static string BuildSocketSummary(SkillLoadoutEntry entry)
|
|
{
|
|
if (entry == null || entry.SocketedGems == null || entry.SocketedGems.Count == 0)
|
|
return "없음";
|
|
|
|
StringBuilder builder = new StringBuilder();
|
|
for (int i = 0; i < entry.SocketedGems.Count; i++)
|
|
{
|
|
SkillGemData gem = entry.GetGem(i);
|
|
if (i > 0)
|
|
builder.Append(", ");
|
|
|
|
builder.Append(gem != null ? gem.GemName : "비어 있음");
|
|
}
|
|
|
|
return builder.ToString();
|
|
}
|
|
|
|
private static string BuildGemStatSummary(SkillGemData gem)
|
|
{
|
|
List<string> lines = new List<string>();
|
|
|
|
if (!Mathf.Approximately(gem.ManaCostMultiplier, 1f))
|
|
lines.Add($"마나 비용 x{gem.ManaCostMultiplier:0.##}");
|
|
if (!Mathf.Approximately(gem.CooldownMultiplier, 1f))
|
|
lines.Add($"쿨타임 x{gem.CooldownMultiplier:0.##}");
|
|
if (!Mathf.Approximately(gem.CastSpeedMultiplier, 1f))
|
|
lines.Add($"시전 속도 x{gem.CastSpeedMultiplier:0.##}");
|
|
if (!Mathf.Approximately(gem.DamageMultiplier, 1f))
|
|
lines.Add($"피해량 x{gem.DamageMultiplier:0.##}");
|
|
if (!Mathf.Approximately(gem.HealMultiplier, 1f))
|
|
lines.Add($"회복량 x{gem.HealMultiplier:0.##}");
|
|
if (!Mathf.Approximately(gem.ShieldMultiplier, 1f))
|
|
lines.Add($"보호막량 x{gem.ShieldMultiplier:0.##}");
|
|
if (!Mathf.Approximately(gem.ThreatMultiplier, 1f))
|
|
lines.Add($"위협량 x{gem.ThreatMultiplier:0.##}");
|
|
if (gem.AdditionalRepeatCount > 0)
|
|
lines.Add($"추가 반복 +{gem.AdditionalRepeatCount}");
|
|
|
|
return lines.Count <= 0 ? "추가 수치 보정 없음" : string.Join("\n", lines);
|
|
}
|
|
|
|
private static string BuildIncompatibleSummary(SkillGemData gem)
|
|
{
|
|
List<string> entries = new List<string>();
|
|
|
|
if (gem.IncompatibleCategories != null && gem.IncompatibleCategories.Count > 0)
|
|
{
|
|
StringBuilder categoryBuilder = new StringBuilder();
|
|
categoryBuilder.Append("함께 장착 불가 분류: ");
|
|
|
|
for (int i = 0; i < gem.IncompatibleCategories.Count; i++)
|
|
{
|
|
if (i > 0)
|
|
categoryBuilder.Append(", ");
|
|
|
|
categoryBuilder.Append(SkillClassificationUtility.GetGemCategoryLabel(gem.IncompatibleCategories[i]));
|
|
}
|
|
|
|
entries.Add(categoryBuilder.ToString());
|
|
}
|
|
|
|
if (gem.IncompatibleGems != null && gem.IncompatibleGems.Count > 0)
|
|
{
|
|
StringBuilder gemBuilder = new StringBuilder();
|
|
gemBuilder.Append("함께 장착 불가 젬: ");
|
|
bool hasGem = false;
|
|
|
|
for (int i = 0; i < gem.IncompatibleGems.Count; i++)
|
|
{
|
|
if (gem.IncompatibleGems[i] == null)
|
|
continue;
|
|
|
|
if (hasGem)
|
|
gemBuilder.Append(", ");
|
|
|
|
gemBuilder.Append(gem.IncompatibleGems[i].GemName);
|
|
hasGem = true;
|
|
}
|
|
|
|
if (hasGem)
|
|
{
|
|
entries.Add(gemBuilder.ToString());
|
|
}
|
|
}
|
|
|
|
return entries.Count > 0 ? string.Join("\n", entries) : string.Empty;
|
|
}
|
|
|
|
private static string GetSlotDisplayName(int slotIndex)
|
|
{
|
|
if (slotIndex >= 0 && slotIndex < SlotDisplayNames.Length)
|
|
return SlotDisplayNames[slotIndex];
|
|
|
|
return $"기술 {slotIndex + 1}";
|
|
}
|
|
|
|
private static string GetGemAllowedSummary(SkillGemData gem)
|
|
{
|
|
if (gem == null)
|
|
return "전체";
|
|
|
|
return
|
|
$"{SkillClassificationUtility.GetAllowedRoleSummary(gem.AllowedSkillRoles)}/" +
|
|
$"{SkillClassificationUtility.GetAllowedActivationSummary(gem.AllowedSkillActivationTypes)}";
|
|
}
|
|
|
|
private void ApplyButtonState(Button button, bool selected, bool interactable)
|
|
{
|
|
if (button == null)
|
|
return;
|
|
|
|
button.interactable = interactable;
|
|
Image image = button.image;
|
|
if (image == null)
|
|
return;
|
|
|
|
image.color = !interactable
|
|
? buttonDisabledColor
|
|
: selected ? buttonSelectedColor : buttonNormalColor;
|
|
}
|
|
|
|
private Button CreateListButton(RectTransform parent, string name, string label, Action onClick)
|
|
{
|
|
Button button = CreateButton(parent, name, label, Vector2.zero, onClick, out TextMeshProUGUI textLabel);
|
|
textLabel.alignment = TextAlignmentOptions.Left;
|
|
textLabel.margin = new Vector4(12f, 10f, 12f, 10f);
|
|
LayoutElement layout = button.gameObject.AddComponent<LayoutElement>();
|
|
layout.minHeight = 74f;
|
|
return button;
|
|
}
|
|
|
|
private void CreateListMessage(RectTransform parent, string message)
|
|
{
|
|
TextMeshProUGUI label = CreateAutoLabel(parent, "Label_Message", message, 18f, TextAlignmentOptions.TopLeft);
|
|
LayoutElement layout = label.gameObject.AddComponent<LayoutElement>();
|
|
layout.minHeight = 56f;
|
|
}
|
|
|
|
private static void ClearChildren(RectTransform parent)
|
|
{
|
|
for (int i = parent.childCount - 1; i >= 0; i--)
|
|
{
|
|
Destroy(parent.GetChild(i).gameObject);
|
|
}
|
|
}
|
|
|
|
private void SetStatusMessage(string message, bool isError)
|
|
{
|
|
if (statusText == null)
|
|
return;
|
|
|
|
statusText.text = message;
|
|
statusText.color = isError ? statusErrorColor : statusNormalColor;
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
private void AutoCollectOwnedGemsInEditor()
|
|
{
|
|
if (!autoCollectOwnedGemsInEditor)
|
|
return;
|
|
|
|
string searchFolder = string.IsNullOrWhiteSpace(gemSearchFolder) ? DefaultGemSearchFolder : gemSearchFolder.Trim();
|
|
if (!AssetDatabase.IsValidFolder(searchFolder))
|
|
return;
|
|
|
|
Dictionary<SkillGemData, int> existingQuantities = new Dictionary<SkillGemData, int>();
|
|
for (int i = 0; i < ownedGemEntries.Count; i++)
|
|
{
|
|
SkillGemStorageEntry entry = ownedGemEntries[i];
|
|
if (entry == null || entry.Gem == null)
|
|
continue;
|
|
|
|
if (!existingQuantities.ContainsKey(entry.Gem))
|
|
{
|
|
existingQuantities.Add(entry.Gem, entry.Quantity);
|
|
}
|
|
}
|
|
|
|
string[] guids = AssetDatabase.FindAssets("t:SkillGemData", new[] { searchFolder });
|
|
Array.Sort(guids, (left, right) =>
|
|
{
|
|
string leftPath = AssetDatabase.GUIDToAssetPath(left);
|
|
string rightPath = AssetDatabase.GUIDToAssetPath(right);
|
|
return string.Compare(leftPath, rightPath, StringComparison.Ordinal);
|
|
});
|
|
|
|
ownedGemEntries.Clear();
|
|
for (int i = 0; i < guids.Length; i++)
|
|
{
|
|
string assetPath = AssetDatabase.GUIDToAssetPath(guids[i]);
|
|
SkillGemData gem = AssetDatabase.LoadAssetAtPath<SkillGemData>(assetPath);
|
|
if (gem == null)
|
|
continue;
|
|
|
|
SkillGemStorageEntry entry = new SkillGemStorageEntry
|
|
{
|
|
Gem = gem,
|
|
Quantity = existingQuantities.TryGetValue(gem, out int quantity) ? quantity : autoCollectedGemQuantity,
|
|
};
|
|
|
|
ownedGemEntries.Add(entry);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
}
|