feat: update song selection, score UI, and song creator features

- SongSelectManager/SongDetailPanel: 곡 선택 및 상세 패널 개선
- SongCreatorManager: 곡 생성 기능 추가
- FinalScoreLabel/ScoreManager: 결과 화면 점수 UI 업데이트
- MarqueeText: 마퀴 텍스트 컴포넌트 개선
- NoteData/SongController: 노트 데이터 및 컨트롤러 보완

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jongjae0305
2026-05-29 17:29:50 +09:00
parent 72dad1ce4c
commit c335995a9a
10 changed files with 686 additions and 171 deletions
+158 -11
View File
@@ -33,13 +33,20 @@ public class SongDetailPanel : MonoBehaviour
[Header("씬 이름")]
[SerializeField] private string gameSceneName = "Game";
private static readonly Color SelectedColor = new Color(0.2f, 0.78f, 0.4f);
private static readonly Color DeselectedImgColor = new Color(1f, 1f, 1f, 0.12f); // original button alpha
private static readonly Color NeonBg = new Color(0.05f, 0.82f, 0.95f, 0.42f);
private static readonly Color DarkButtonBg = new Color(0.09f, 0.22f, 0.27f, 0.66f);
private static readonly Color DisabledBg = new Color(0.06f, 0.12f, 0.15f, 0.48f);
private static readonly Color DangerBg = new Color(0.52f, 0.16f, 0.22f, 0.72f);
private static readonly Color ButtonText = new Color(0.92f, 1.0f, 1.0f, 1.0f);
private static readonly Color MutedText = new Color(0.66f, 0.80f, 0.84f, 0.76f);
private static readonly Color NeonOutline = new Color(0.25f, 0.96f, 1.0f, 0.42f);
private SongInfo currentSong;
private string selectedDifficulty;
private DownloadManager downloadManager;
private SongSelectManager selectManager;
private MarqueeText titleMarquee;
private MarqueeText artistMarquee;
private readonly (string key, Func<SongDetailPanel, Button> btn)[] diffSlots =
{
@@ -49,6 +56,22 @@ public class SongDetailPanel : MonoBehaviour
("expertplus", p => p.btnExpertPlus),
};
private void Awake()
{
HideDifficultyLabel();
titleMarquee = ConfigureMarqueeText(titleText, 5.0f, 7.2f);
artistMarquee = ConfigureMarqueeText(artistText, 3.4f, 4.4f);
ConfigureOneLineText(infoText, 3.2f, 4.2f, TextAlignmentOptions.MidlineLeft);
ConfigureButtonText(btnNormal, 3.2f, 4.0f);
ConfigureButtonText(btnHard, 3.2f, 4.0f);
ConfigureButtonText(btnExpert, 3.2f, 4.0f);
ConfigureButtonText(btnExpertPlus, 3.0f, 3.8f);
ConfigureButtonText(downloadButton, 3.5f, 4.4f);
ConfigureButtonText(deleteButton, 3.5f, 4.4f);
ConfigureButtonText(playButton, 3.5f, 4.4f);
ConfigureButtonText(closeButton, 5.2f, 6.4f);
}
// ── Public API ───────────────────────────────────────────
public void Show(SongInfo song, DownloadManager dm, SongSelectManager sm)
@@ -61,9 +84,11 @@ public class SongDetailPanel : MonoBehaviour
titleText.text = song.title;
artistText.text = song.artist;
infoText.text = song.duration > 0
? $"BPM {Mathf.RoundToInt(song.bpm)} | {FormatDuration(song.duration)}"
? $"BPM {Mathf.RoundToInt(song.bpm)} {FormatDuration(song.duration)}"
: $"BPM {Mathf.RoundToInt(song.bpm)}";
titleMarquee?.Refresh();
artistMarquee?.Refresh();
RefreshUI();
}
@@ -92,8 +117,11 @@ public class SongDetailPanel : MonoBehaviour
downloadButton.gameObject.SetActive(!downloaded);
deleteButton.gameObject.SetActive(downloaded);
downloadButton.interactable = !downloaded;
deleteButton.interactable = downloaded;
playButton.interactable = downloaded && selectedDifficulty != null;
progressGroup.SetActive(false);
UpdateActionButtonStyles(downloaded);
downloadButton.onClick.RemoveAllListeners();
downloadButton.onClick.AddListener(OnDownloadClicked);
@@ -116,6 +144,7 @@ public class SongDetailPanel : MonoBehaviour
selectedDifficulty = difficulty;
playButton.interactable = true;
UpdateDiffColors();
UpdateActionButtonStyles(true);
}
private void UpdateDiffColors()
@@ -125,14 +154,7 @@ public class SongDetailPanel : MonoBehaviour
Button btn = getBtn(this);
bool selected = key == selectedDifficulty;
if (btn.targetGraphic is Image img)
img.color = selected ? SelectedColor : DeselectedImgColor;
var cb = btn.colors;
cb.normalColor = Color.white;
cb.highlightedColor = selected ? new Color(0.3f, 0.95f, 0.55f) : new Color(1f, 1f, 1f, 0.25f);
cb.pressedColor = selected ? new Color(0.15f, 0.6f, 0.3f) : new Color(1f, 1f, 1f, 0.35f);
btn.colors = cb;
ApplyButtonStyle(btn, selected ? NeonBg : DarkButtonBg, selected, btn.interactable, false);
}
}
@@ -234,4 +256,129 @@ public class SongDetailPanel : MonoBehaviour
private static string FormatDuration(int seconds)
=> $"{seconds / 60}:{seconds % 60:D2}";
private void HideDifficultyLabel()
{
Transform label = transform.Find("LblDifficulty");
if (label != null)
label.gameObject.SetActive(false);
}
private void UpdateActionButtonStyles(bool downloaded)
{
ApplyButtonStyle(downloadButton, NeonBg, true, !downloaded, false);
ApplyButtonStyle(deleteButton, DangerBg, true, downloaded, true);
ApplyButtonStyle(playButton, NeonBg, true, playButton.interactable, false);
ApplyButtonStyle(closeButton, DarkButtonBg, false, true, false);
}
private static void ApplyButtonStyle(Button btn, Color activeBg, bool outlined, bool enabled, bool danger)
{
if (btn == null)
return;
Color bg = enabled ? activeBg : DisabledBg;
if (btn.targetGraphic is Image img)
img.color = bg;
var colors = btn.colors;
colors.normalColor = bg;
colors.highlightedColor = enabled
? (danger ? new Color(0.72f, 0.23f, 0.30f, 0.86f) : new Color(0.10f, 0.95f, 1.0f, 0.58f))
: DisabledBg;
colors.pressedColor = enabled
? (danger ? new Color(0.42f, 0.10f, 0.15f, 0.92f) : new Color(0.02f, 0.58f, 0.72f, 0.80f))
: DisabledBg;
colors.selectedColor = colors.highlightedColor;
colors.disabledColor = DisabledBg;
colors.fadeDuration = 0.08f;
btn.colors = colors;
TMP_Text label = btn.GetComponentInChildren<TMP_Text>();
if (label != null)
label.color = enabled ? ButtonText : MutedText;
Outline outline = btn.GetComponent<Outline>() ?? btn.gameObject.AddComponent<Outline>();
outline.enabled = outlined && enabled;
outline.effectColor = danger ? new Color(1.0f, 0.35f, 0.42f, 0.34f) : NeonOutline;
outline.effectDistance = new Vector2(0.0f, -0.28f);
}
private static MarqueeText ConfigureMarqueeText(TMP_Text text, float minSize, float maxSize)
{
if (text == null)
return null;
RectTransform textRect = text.rectTransform;
Transform originalParent = textRect.parent;
int siblingIndex = textRect.GetSiblingIndex();
string maskName = $"{text.name}Mask";
Transform existingMask = originalParent != null ? originalParent.Find(maskName) : null;
RectTransform maskRect;
if (existingMask != null)
{
maskRect = existingMask as RectTransform;
if (textRect.parent != existingMask)
textRect.SetParent(existingMask, false);
}
else
{
var mask = new GameObject(maskName);
mask.transform.SetParent(originalParent, false);
mask.transform.SetSiblingIndex(siblingIndex);
maskRect = mask.AddComponent<RectTransform>();
maskRect.anchorMin = textRect.anchorMin;
maskRect.anchorMax = textRect.anchorMax;
maskRect.pivot = textRect.pivot;
maskRect.anchoredPosition = textRect.anchoredPosition;
maskRect.sizeDelta = textRect.sizeDelta;
maskRect.localRotation = textRect.localRotation;
maskRect.localScale = textRect.localScale;
mask.AddComponent<RectMask2D>();
textRect.SetParent(mask.transform, false);
}
textRect.anchorMin = new Vector2(0f, 0f);
textRect.anchorMax = new Vector2(0f, 1f);
textRect.pivot = new Vector2(0f, 0.5f);
textRect.anchoredPosition = Vector2.zero;
textRect.localRotation = Quaternion.identity;
textRect.localScale = Vector3.one;
textRect.sizeDelta = new Vector2(260.0f, 0f);
ConfigureOneLineText(text, minSize, maxSize, TextAlignmentOptions.MidlineLeft);
text.overflowMode = TextOverflowModes.Overflow;
text.raycastTarget = false;
MarqueeText marquee = text.GetComponent<MarqueeText>() ?? text.gameObject.AddComponent<MarqueeText>();
marquee.speed = 9f;
marquee.pauseStart = 1.25f;
marquee.pauseEnd = 0.8f;
return marquee;
}
private static void ConfigureButtonText(Button btn, float minSize, float maxSize)
{
if (btn == null)
return;
TMP_Text label = btn.GetComponentInChildren<TMP_Text>();
ConfigureOneLineText(label, minSize, maxSize, TextAlignmentOptions.Center);
if (label != null)
label.raycastTarget = false;
}
private static void ConfigureOneLineText(TMP_Text text, float minSize, float maxSize, TextAlignmentOptions alignment)
{
if (text == null)
return;
text.enableAutoSizing = true;
text.fontSizeMin = minSize;
text.fontSizeMax = maxSize;
text.alignment = alignment;
text.overflowMode = TextOverflowModes.Ellipsis;
text.textWrappingMode = TextWrappingModes.NoWrap;
}
}