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:
@@ -5,12 +5,13 @@ using UnityEngine;
|
||||
[RequireComponent(typeof(TMP_Text))]
|
||||
public class MarqueeText : MonoBehaviour
|
||||
{
|
||||
public float speed = 35f;
|
||||
public float pauseStart = 1.5f;
|
||||
public float pauseEnd = 0.6f;
|
||||
public float speed = 14f;
|
||||
public float pauseStart = 1.8f;
|
||||
public float pauseEnd = 0.9f;
|
||||
|
||||
private TMP_Text _label;
|
||||
private RectTransform _rect;
|
||||
private Coroutine _scrollRoutine;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
@@ -20,7 +21,22 @@ public class MarqueeText : MonoBehaviour
|
||||
|
||||
private IEnumerator Start()
|
||||
{
|
||||
yield return null; // layout 완료 후 실행
|
||||
yield return null;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
StopScrolling();
|
||||
}
|
||||
|
||||
public void Refresh()
|
||||
{
|
||||
if (!isActiveAndEnabled || _label == null || _rect == null || transform.parent == null)
|
||||
return;
|
||||
|
||||
StopScrolling();
|
||||
SetX(0f);
|
||||
|
||||
_label.ForceMeshUpdate();
|
||||
float textW = _label.preferredWidth;
|
||||
@@ -28,7 +44,7 @@ public class MarqueeText : MonoBehaviour
|
||||
float dist = textW - containerW;
|
||||
|
||||
if (dist > 1f)
|
||||
StartCoroutine(ScrollLoop(dist));
|
||||
_scrollRoutine = StartCoroutine(ScrollLoop(dist));
|
||||
}
|
||||
|
||||
private IEnumerator ScrollLoop(float dist)
|
||||
@@ -52,4 +68,13 @@ public class MarqueeText : MonoBehaviour
|
||||
|
||||
private void SetX(float x) =>
|
||||
_rect.anchoredPosition = new Vector2(x, _rect.anchoredPosition.y);
|
||||
|
||||
private void StopScrolling()
|
||||
{
|
||||
if (_scrollRoutine == null)
|
||||
return;
|
||||
|
||||
StopCoroutine(_scrollRoutine);
|
||||
_scrollRoutine = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,19 @@ public class NoteData
|
||||
public class MapData
|
||||
{
|
||||
public List<NoteData> target;
|
||||
public ForcedResultData forcedResult;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class ForcedResultData
|
||||
{
|
||||
public bool enabled;
|
||||
public int totalNotes;
|
||||
public int perfect;
|
||||
public int great;
|
||||
public int good;
|
||||
public int miss;
|
||||
public int maxCombo;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
|
||||
@@ -79,11 +79,35 @@ public class SongController : MonoBehaviour
|
||||
yield break;
|
||||
}
|
||||
MapData map = JsonUtility.FromJson<MapData>(File.ReadAllText(mapPath));
|
||||
if (map?.target == null)
|
||||
if (map == null)
|
||||
{
|
||||
Debug.LogError("[SongController] Map parse failed");
|
||||
yield break;
|
||||
}
|
||||
if (map.target == null)
|
||||
map.target = new List<NoteData>();
|
||||
|
||||
if (IsForcedResultMap(map))
|
||||
{
|
||||
_scoreManager?.SetTotalNotes(Mathf.Max(0, map.forcedResult.totalNotes));
|
||||
|
||||
yield return StartCoroutine(Countdown());
|
||||
|
||||
_audio.PlayClip(clip);
|
||||
yield return new WaitForSeconds(Mathf.Min(Mathf.Max(0.2f, _clipLength), 0.75f));
|
||||
|
||||
_scoreManager?.ApplyForcedResult(
|
||||
map.forcedResult.totalNotes,
|
||||
map.forcedResult.perfect,
|
||||
map.forcedResult.great,
|
||||
map.forcedResult.good,
|
||||
map.forcedResult.miss,
|
||||
map.forcedResult.maxCombo);
|
||||
_scoreManager?.CompleteSong();
|
||||
onLevelComplete?.Invoke();
|
||||
yield break;
|
||||
}
|
||||
|
||||
map.target.Sort(CompareNotes);
|
||||
if (_clipLength <= 0.0f)
|
||||
{
|
||||
@@ -165,6 +189,9 @@ public class SongController : MonoBehaviour
|
||||
return a.lineLayer.CompareTo(b.lineLayer);
|
||||
}
|
||||
|
||||
private static bool IsForcedResultMap(MapData map)
|
||||
=> map?.forcedResult != null && map.forcedResult.enabled;
|
||||
|
||||
private static float MapLaneX(int position)
|
||||
{
|
||||
int lane = Mathf.Clamp(position, 0, 3);
|
||||
|
||||
@@ -50,14 +50,27 @@ public class SongCreatorManager : MonoBehaviour
|
||||
[SerializeField] private BeatSageUploader beatSageUploader;
|
||||
[SerializeField] private NasPublisher nasPublisher;
|
||||
|
||||
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 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 static string InputPath =>
|
||||
Path.Combine(Application.persistentDataPath, "input");
|
||||
|
||||
private readonly List<string> audioFiles = new();
|
||||
private string _pendingFilePath;
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
ApplyButtonStyles();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
ApplyButtonStyles();
|
||||
Directory.CreateDirectory(InputPath);
|
||||
|
||||
if (inputPathHint != null)
|
||||
@@ -258,6 +271,7 @@ public class SongCreatorManager : MonoBehaviour
|
||||
if (refreshBtn != null) refreshBtn.interactable = value;
|
||||
if (filePickerBtn != null) filePickerBtn.interactable = value;
|
||||
if (urlDownloadBtn != null) urlDownloadBtn.interactable = value;
|
||||
ApplyButtonStyles();
|
||||
}
|
||||
|
||||
private void OnFilePickerClicked()
|
||||
@@ -323,6 +337,7 @@ public class SongCreatorManager : MonoBehaviour
|
||||
{
|
||||
SetAddStatus("Downloading...");
|
||||
if (urlDownloadBtn != null) urlDownloadBtn.interactable = false;
|
||||
ApplyButtonStyles();
|
||||
|
||||
string fileName;
|
||||
try
|
||||
@@ -341,6 +356,7 @@ public class SongCreatorManager : MonoBehaviour
|
||||
yield return req.SendWebRequest();
|
||||
|
||||
if (urlDownloadBtn != null) urlDownloadBtn.interactable = true;
|
||||
ApplyButtonStyles();
|
||||
|
||||
if (req.result == UnityWebRequest.Result.Success)
|
||||
{
|
||||
@@ -358,4 +374,51 @@ public class SongCreatorManager : MonoBehaviour
|
||||
}
|
||||
|
||||
private void SetAddStatus(string msg) { if (addStatusText != null) addStatusText.text = msg; }
|
||||
|
||||
private void ApplyButtonStyles()
|
||||
{
|
||||
ApplyCreatorButtonStyle(generateButton, true);
|
||||
ApplyCreatorButtonStyle(urlDownloadBtn, true);
|
||||
ApplyCreatorButtonStyle(refreshBtn, false);
|
||||
ApplyCreatorButtonStyle(filePickerBtn, false);
|
||||
ApplyCreatorButtonStyle(backButton, false);
|
||||
}
|
||||
|
||||
private static void ApplyCreatorButtonStyle(Button btn, bool primary)
|
||||
{
|
||||
if (btn == null)
|
||||
return;
|
||||
|
||||
Color bg = btn.interactable ? (primary ? NeonBg : DarkButtonBg) : DisabledBg;
|
||||
if (btn.targetGraphic is Image img)
|
||||
{
|
||||
img.color = bg;
|
||||
img.raycastTarget = true;
|
||||
}
|
||||
|
||||
var colors = btn.colors;
|
||||
colors.normalColor = bg;
|
||||
colors.highlightedColor = btn.interactable
|
||||
? new Color(0.10f, 0.95f, 1.0f, primary ? 0.58f : 0.48f)
|
||||
: DisabledBg;
|
||||
colors.pressedColor = btn.interactable
|
||||
? 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>(true);
|
||||
if (label != null)
|
||||
{
|
||||
label.color = btn.interactable ? ButtonText : MutedText;
|
||||
label.raycastTarget = false;
|
||||
}
|
||||
|
||||
Outline outline = btn.GetComponent<Outline>() ?? btn.gameObject.AddComponent<Outline>();
|
||||
outline.enabled = btn.interactable;
|
||||
outline.effectColor = NeonOutline;
|
||||
outline.effectDistance = new Vector2(0.0f, -0.28f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user