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
+140 -40
View File
@@ -16,8 +16,11 @@ public class SongSelectManager : MonoBehaviour
[SerializeField] private TMP_Text errorText;
private static readonly Color TabActive = new Color(1f, 1f, 1f, 0.45f);
private static readonly Color TabInactive = new Color(1f, 1f, 1f, 0.12f);
private static readonly Color TabActiveBg = new Color(0.05f, 0.82f, 0.95f, 0.42f);
private static readonly Color TabInactiveBg = new Color(0.09f, 0.22f, 0.27f, 0.66f);
private static readonly Color TabActiveText = new Color(0.92f, 1.0f, 1.0f, 1.0f);
private static readonly Color TabInactiveText = new Color(0.72f, 0.86f, 0.90f, 0.82f);
private static readonly Color TabActiveOutline = new Color(0.25f, 0.96f, 1.0f, 0.55f);
private static string CachePath =>
Path.Combine(Application.persistentDataPath, "songs_cache.json");
@@ -49,17 +52,39 @@ public class SongSelectManager : MonoBehaviour
private void SetTabVisual(bool owned)
{
ApplyTabColor(tabAllBtn, owned ? TabInactive : TabActive);
ApplyTabColor(tabOwnedBtn, owned ? TabActive : TabInactive);
ApplyTabStyle(tabAllBtn, !owned);
ApplyTabStyle(tabOwnedBtn, owned);
}
private static void ApplyTabColor(Button btn, Color c)
private static void ApplyTabStyle(Button btn, bool active)
{
if (btn == null)
return;
Color bg = active ? TabActiveBg : TabInactiveBg;
if (btn.targetGraphic is Image img)
img.color = c;
img.color = bg;
var colors = btn.colors;
colors.normalColor = Color.white;
colors.normalColor = bg;
colors.highlightedColor = active
? new Color(0.10f, 0.95f, 1.0f, 0.58f)
: new Color(0.14f, 0.34f, 0.40f, 0.72f);
colors.pressedColor = active
? new Color(0.02f, 0.58f, 0.72f, 0.72f)
: new Color(0.08f, 0.20f, 0.24f, 0.82f);
colors.selectedColor = colors.highlightedColor;
colors.disabledColor = new Color(0.05f, 0.10f, 0.12f, 0.45f);
btn.colors = colors;
TMP_Text label = btn.GetComponentInChildren<TMP_Text>();
if (label != null)
label.color = active ? TabActiveText : TabInactiveText;
Outline outline = btn.GetComponent<Outline>() ?? btn.gameObject.AddComponent<Outline>();
outline.enabled = active;
outline.effectColor = TabActiveOutline;
outline.effectDistance = new Vector2(0.0f, -0.35f);
}
private void FetchSongs()
@@ -70,9 +95,10 @@ public class SongSelectManager : MonoBehaviour
downloadManager.FetchSongsList(
onSuccess: list =>
{
allSongs = list.songs;
SaveCache(list);
SongLibrary.Instance.ValidateWithFileSystem(downloadManager, list.songs);
allSongs = list.songs ?? new List<SongInfo>();
AddLocalForcedRankDummies(allSongs);
SaveCache(new SongsList { version = list.version, songs = allSongs });
SongLibrary.Instance.ValidateWithFileSystem(downloadManager, allSongs);
loadingOverlay.SetActive(false);
RefreshCards();
},
@@ -134,14 +160,16 @@ public class SongSelectManager : MonoBehaviour
bc.fadeDuration = 0.1f;
btn.colors = bc;
float textLeftInset = downloaded ? 12f : 5f;
// Title — RectMask2D 컨테이너 안에서 마퀴 스크롤
var titleMask = new GameObject("TitleMask");
titleMask.transform.SetParent(card.transform, false);
var tmr = titleMask.AddComponent<RectTransform>();
tmr.anchorMin = new Vector2(0f, 0.5f);
tmr.anchorMax = new Vector2(1f, 1f);
tmr.offsetMin = new Vector2(5f, 0f);
tmr.offsetMax = new Vector2(downloaded ? -20f : -3f, 0f);
tmr.offsetMin = new Vector2(textLeftInset, 0f);
tmr.offsetMax = new Vector2(-3f, 0f);
titleMask.AddComponent<RectMask2D>();
var titleGO = new GameObject("Title");
@@ -166,41 +194,37 @@ public class SongSelectManager : MonoBehaviour
var artistGO = new GameObject("Artist");
artistGO.transform.SetParent(card.transform, false);
var ar = artistGO.AddComponent<RectTransform>();
ar.anchorMin = new Vector2(0f, 0f);
ar.anchorMax = new Vector2(1f, 0.5f);
ar.offsetMin = new Vector2(5f, 1f);
ar.anchorMin = new Vector2(0f, 0.04f);
ar.anchorMax = new Vector2(1f, 0.48f);
ar.offsetMin = new Vector2(textLeftInset, 0f);
ar.offsetMax = new Vector2(-3f, 0f);
var aTmp = artistGO.AddComponent<TextMeshProUGUI>();
if (_cardFont != null) aTmp.font = _cardFont;
aTmp.text = song.artist;
aTmp.fontSize = 4f;
aTmp.color = new Color(1f, 1f, 1f, 0.6f);
aTmp.alignment = TextAlignmentOptions.MidlineLeft;
aTmp.text = song.artist;
aTmp.fontSize = 4f;
aTmp.enableAutoSizing = true;
aTmp.fontSizeMin = 2.8f;
aTmp.fontSizeMax = 4f;
aTmp.color = new Color(1f, 1f, 1f, 0.6f);
aTmp.alignment = TextAlignmentOptions.MidlineLeft;
aTmp.overflowMode = TextOverflowModes.Ellipsis;
aTmp.textWrappingMode = TextWrappingModes.NoWrap;
// Downloaded badge
// Downloaded check mark
if (downloaded)
{
var badge = new GameObject("Badge");
badge.transform.SetParent(card.transform, false);
var br = badge.AddComponent<RectTransform>();
br.anchorMin = new Vector2(1f, 0.5f);
br.anchorMax = new Vector2(1f, 0.5f);
br.pivot = new Vector2(1f, 0.5f);
br.anchoredPosition = new Vector2(-3f, 0f);
br.sizeDelta = new Vector2(14f, 5.5f);
badge.AddComponent<Image>().color = new Color(0.2f, 0.78f, 0.4f, 0.85f);
var checkGO = new GameObject("OwnedCheck");
checkGO.transform.SetParent(card.transform, false);
var cr = checkGO.AddComponent<RectTransform>();
cr.anchorMin = new Vector2(0f, 0f);
cr.anchorMax = new Vector2(0f, 1f);
cr.pivot = new Vector2(0f, 0.5f);
cr.anchoredPosition = new Vector2(3.0f, 0f);
cr.sizeDelta = new Vector2(6f, 0f);
var bl = new GameObject("Text");
bl.transform.SetParent(badge.transform, false);
var blr = bl.AddComponent<RectTransform>();
blr.anchorMin = Vector2.zero;
blr.anchorMax = Vector2.one;
blr.offsetMin = blr.offsetMax = Vector2.zero;
var blTmp = bl.AddComponent<TextMeshProUGUI>();
blTmp.text = "OWNED";
blTmp.fontSize = 3.5f;
blTmp.color = Color.white;
blTmp.alignment = TextAlignmentOptions.Center;
Color checkColor = new Color(0.36f, 1.0f, 0.58f, 0.95f);
CreateCheckStroke(checkGO.transform, "ShortStroke", new Vector2(1.8f, 7.1f), new Vector2(1.5f, 0.35f), 42.0f, checkColor);
CreateCheckStroke(checkGO.transform, "LongStroke", new Vector2(3.25f, 7.85f), new Vector2(3.7f, 0.35f), -45.0f, checkColor);
}
SongInfo captured = song;
@@ -213,6 +237,25 @@ public class SongSelectManager : MonoBehaviour
detailPanel.Show(song, downloadManager, this);
}
private static void CreateCheckStroke(Transform parent, string name, Vector2 anchoredPosition,
Vector2 size, float rotationZ, Color color)
{
var stroke = new GameObject(name);
stroke.transform.SetParent(parent, false);
var rect = stroke.AddComponent<RectTransform>();
rect.anchorMin = new Vector2(0f, 0f);
rect.anchorMax = new Vector2(0f, 0f);
rect.pivot = new Vector2(0.5f, 0.5f);
rect.anchoredPosition = anchoredPosition;
rect.sizeDelta = size;
rect.localRotation = Quaternion.Euler(0f, 0f, rotationZ);
var img = stroke.AddComponent<Image>();
img.color = color;
img.raycastTarget = false;
}
private static void SaveCache(SongsList list)
{
try { File.WriteAllText(CachePath, JsonUtility.ToJson(list, true)); }
@@ -225,4 +268,61 @@ public class SongSelectManager : MonoBehaviour
try { return JsonUtility.FromJson<SongsList>(File.ReadAllText(CachePath)); }
catch { return null; }
}
private static void AddLocalForcedRankDummies(List<SongInfo> songs)
{
string root = Path.Combine(Application.persistentDataPath, "beatsaber");
AddLocalForcedRankDummy(songs, root, "dummy_rank_m", "M", 10);
AddLocalForcedRankDummy(songs, root, "dummy_rank_splus", "S+", 100);
AddLocalForcedRankDummy(songs, root, "dummy_rank_s", "S", 100);
AddLocalForcedRankDummy(songs, root, "dummy_rank_a", "A", 100);
AddLocalForcedRankDummy(songs, root, "dummy_rank_b", "B", 100);
AddLocalForcedRankDummy(songs, root, "dummy_rank_c", "C", 100);
AddLocalForcedRankDummy(songs, root, "dummy_rank_d", "D", 100);
AddLocalForcedRankDummy(songs, root, "dummy_rank_f", "F", 100);
AddLocalForcedRankDummy(songs, root, "dummy_rank_splus_border", "S+ 98%", 100);
AddLocalForcedRankDummy(songs, root, "dummy_rank_f_zero", "F 0%", 20);
}
private static void AddLocalForcedRankDummy(List<SongInfo> songs, string root, string id, string title, int noteCount)
{
if (songs.Exists(song => song.id == id))
return;
string songDir = Path.Combine(root, id);
string audioPath = Path.Combine(songDir, $"{id}.mp3");
string mapFile = $"Map_{id}_forced.json";
string mapPath = Path.Combine(songDir, mapFile);
if (!File.Exists(audioPath) || !File.Exists(mapPath))
return;
long audioSize = new FileInfo(audioPath).Length;
long mapSize = new FileInfo(mapPath).Length;
DifficultyInfo info = new DifficultyInfo
{
mapFile = mapFile,
mapSize = mapSize,
noteCount = noteCount
};
songs.Insert(0, new SongInfo
{
id = id,
title = title,
artist = "Forced Rank Dummy",
bpm = 120.0f,
duration = 1,
audioFile = $"dummy/{id}.mp3",
audioSize = audioSize,
coverImage = "",
difficulties = new DifficultyMap
{
normal = info,
hard = info,
expert = info,
expertplus = info
},
addedAt = "2026-05-29"
});
}
}