mirror of
https://github.com/2394425147/astrodx.git
synced 2025-12-13 23:56:20 +08:00
Update scripts
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# Macintosh index files
|
||||
.DS_Store
|
||||
@@ -0,0 +1,20 @@
|
||||
using SimaiSharp.Structures;
|
||||
|
||||
namespace AstroDX.Models.Scoring.Metrics.Extended
|
||||
{
|
||||
public class HealthStats : IReactiveStatistic
|
||||
{
|
||||
public HealthStats(int maxHealth)
|
||||
{
|
||||
MaxHealth = maxHealth;
|
||||
}
|
||||
|
||||
public void Push(in NoteType type, in JudgeData data)
|
||||
{
|
||||
Loss += GameSettings.Settings.Profile.Mods.LifeDrain.GetHealthLoss(type, data);
|
||||
}
|
||||
|
||||
public int MaxHealth { get; }
|
||||
public int Loss { get; private set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using SimaiSharp.Structures;
|
||||
|
||||
namespace AstroDX.Models.Scoring.Metrics
|
||||
{
|
||||
public interface IReactiveStatistic
|
||||
{
|
||||
void Push(in NoteType type, in JudgeData data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
using System;
|
||||
|
||||
namespace AstroDX.Models.Scoring.Metrics.Internal
|
||||
{
|
||||
public sealed class AchievementStats
|
||||
{
|
||||
private readonly Statistics _statistics;
|
||||
private readonly double _baseScore;
|
||||
|
||||
public AchievementStats(Statistics statistics)
|
||||
{
|
||||
_statistics = statistics;
|
||||
|
||||
var totalPointCount = statistics.JudgementStats.MaxTapCount +
|
||||
statistics.JudgementStats.MaxTouchCount +
|
||||
statistics.JudgementStats.MaxHoldCount * 2 +
|
||||
statistics.JudgementStats.MaxSlideCount * 3 +
|
||||
statistics.JudgementStats.MaxBreakCount * 5;
|
||||
|
||||
_baseScore = totalPointCount > 0 ? 100.00 / totalPointCount : 0;
|
||||
}
|
||||
|
||||
/// <param name="precise">Used when displaying achievement as text to avoid double rounding</param>
|
||||
public double GetAchievement(bool precise = false)
|
||||
{
|
||||
var baseAchievement = GetBaseAchievement();
|
||||
var extraAchievement = _statistics.JudgementStats.BreakRecord.MaxCount > 0
|
||||
? _statistics.JudgementStats.BreakRecord.Extras /
|
||||
_statistics.JudgementStats.BreakRecord.MaxCount
|
||||
: 0;
|
||||
|
||||
var totalAchievement = baseAchievement +
|
||||
extraAchievement;
|
||||
|
||||
return precise ? totalAchievement : Math.Round(totalAchievement, 4);
|
||||
}
|
||||
|
||||
private double GetBaseAchievement()
|
||||
{
|
||||
if (_baseScore == 0)
|
||||
return 0;
|
||||
|
||||
var tapAchievement = _statistics.JudgementStats.TapRecord.Points
|
||||
* _baseScore;
|
||||
|
||||
var touchAchievement = _statistics.JudgementStats.TouchRecord.Points
|
||||
* _baseScore;
|
||||
|
||||
var holdAchievement = _statistics.JudgementStats.HoldRecord.Points
|
||||
* _baseScore * 2;
|
||||
|
||||
var slideAchievement = _statistics.JudgementStats.SlideRecord.Points
|
||||
* _baseScore * 3;
|
||||
|
||||
var breakAchievement = _statistics.JudgementStats.BreakRecord.Points
|
||||
* _baseScore * 5;
|
||||
|
||||
return tapAchievement + touchAchievement + holdAchievement + slideAchievement + breakAchievement;
|
||||
}
|
||||
|
||||
public double CalculateMaxAchievable(bool includeExtras)
|
||||
{
|
||||
var tapAchievement = _statistics.JudgementStats.TapRecord.PassedCount * _baseScore;
|
||||
var touchAchievement = _statistics.JudgementStats.TouchRecord.PassedCount * _baseScore;
|
||||
var holdAchievement = _statistics.JudgementStats.HoldRecord.PassedCount * _baseScore * 2;
|
||||
var slideAchievement = _statistics.JudgementStats.SlideRecord.PassedCount * _baseScore * 3;
|
||||
var breakAchievement = _statistics.JudgementStats.BreakRecord.PassedCount * _baseScore * 5;
|
||||
|
||||
return includeExtras && _statistics.JudgementStats.BreakRecord.MaxCount > 0
|
||||
? tapAchievement + touchAchievement + holdAchievement + slideAchievement + breakAchievement +
|
||||
(double)_statistics.JudgementStats.BreakRecord.PassedCount /
|
||||
_statistics.JudgementStats.BreakRecord.MaxCount
|
||||
: tapAchievement + touchAchievement + holdAchievement + slideAchievement + breakAchievement;
|
||||
}
|
||||
|
||||
public ClearType EvaluateClearType(bool whilePlaying = false)
|
||||
{
|
||||
var achievement = GetAchievement();
|
||||
|
||||
var cleared = achievement >= 80 && _statistics.JudgementStats.AllNotesPassed;
|
||||
var missed = _statistics.JudgementStats.JudgedMissCount >= 1;
|
||||
var anyGood = _statistics.JudgementStats.JudgedGoodCount >= 1;
|
||||
var anyGreat = _statistics.JudgementStats.JudgedGreatCount >= 1;
|
||||
|
||||
var chartHasBreaks = _statistics.JudgementStats.MaxBreakCount > 0;
|
||||
|
||||
if (!whilePlaying && !cleared)
|
||||
return ClearType.Failed;
|
||||
|
||||
if (missed)
|
||||
return ClearType.Clear;
|
||||
|
||||
if (anyGood)
|
||||
return ClearType.FullCombo;
|
||||
|
||||
if (anyGreat)
|
||||
return ClearType.FullComboPlus;
|
||||
|
||||
if (!chartHasBreaks)
|
||||
return ClearType.AllPerfect;
|
||||
|
||||
var maxBreak = _statistics.JudgementStats.BreakRecord.CriticalCount ==
|
||||
_statistics.JudgementStats.BreakRecord.PassedCount;
|
||||
|
||||
return maxBreak
|
||||
? ClearType.AllPerfectPlus
|
||||
: ClearType.AllPerfect;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using SimaiSharp.Structures;
|
||||
|
||||
namespace AstroDX.Models.Scoring.Metrics.Internal
|
||||
{
|
||||
public sealed class ComboStats : IReactiveStatistic
|
||||
{
|
||||
public long lastCombo { get; private set; }
|
||||
public long maxCombo { get; private set; }
|
||||
|
||||
public void Push(in NoteType type, in JudgeData data = default)
|
||||
{
|
||||
if (data.grade == JudgeGrade.Miss)
|
||||
{
|
||||
lastCombo = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
lastCombo++;
|
||||
maxCombo = Math.Max(lastCombo, maxCombo);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System.Linq;
|
||||
|
||||
namespace AstroDX.Models.Scoring.Metrics.Internal
|
||||
{
|
||||
public sealed class DxScoreStats
|
||||
{
|
||||
private readonly Statistics _statistics;
|
||||
|
||||
private const int CriticalWeight = 3;
|
||||
|
||||
private static readonly int[] Weights =
|
||||
{
|
||||
CriticalWeight, 2, 1, 0, 0
|
||||
};
|
||||
|
||||
public DxScoreStats(Statistics statistics) => _statistics = statistics;
|
||||
|
||||
public uint TotalDxScore => _statistics.JudgementStats.MaxNoteCount * CriticalWeight;
|
||||
|
||||
/// <summary>
|
||||
/// This is used to generate max health or max dx score.
|
||||
/// </summary>
|
||||
public uint MaxAchievableDxScore => _statistics.JudgementStats.JudgedNoteCount * CriticalWeight;
|
||||
|
||||
/// <summary>
|
||||
/// This is used to generate the current health or dx score.
|
||||
/// </summary>
|
||||
public uint DxScore =>
|
||||
(uint)(_statistics.JudgementStats.TapRecord.Judged.Sum(c => Weights[(int)c.grade]) +
|
||||
_statistics.JudgementStats.TouchRecord.Judged.Sum(c => Weights[(int)c.grade]) +
|
||||
_statistics.JudgementStats.HoldRecord.Judged.Sum(c => Weights[(int)c.grade]) +
|
||||
_statistics.JudgementStats.SlideRecord.Judged.Sum(c => Weights[(int)c.grade]) +
|
||||
_statistics.JudgementStats.BreakRecord.Judged.Sum(c => Weights[(int)c.grade]));
|
||||
|
||||
public int GetGrade()
|
||||
{
|
||||
var dxScorePercentage = (float)DxScore / TotalDxScore;
|
||||
|
||||
// // add 1 for every passed threshold
|
||||
for (var i = 0; i < DxGradeThresholds.Thresholds.Length; i++)
|
||||
{
|
||||
if (dxScorePercentage < DxGradeThresholds.Thresholds[i])
|
||||
return i;
|
||||
}
|
||||
|
||||
return DxGradeThresholds.Thresholds.Length;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using AstroDX.Utilities.Extensions;
|
||||
using SimaiSharp.Structures;
|
||||
|
||||
namespace AstroDX.Models.Scoring.Metrics.Internal
|
||||
{
|
||||
public sealed class FluctuationStats : IReactiveStatistic
|
||||
{
|
||||
public long EarlyCount { get; private set; }
|
||||
public long LateCount { get; private set; }
|
||||
public double TotalDeviation { get; private set; }
|
||||
public long NotesCounted { get; private set; }
|
||||
public double Deviation => TotalDeviation / NotesCounted;
|
||||
|
||||
public void Push(in NoteType type, in JudgeData data)
|
||||
{
|
||||
if ((GameSettings.Settings.Profile.Metrics.fluctuationVisibility & data.grade.AsFlag()) == 0)
|
||||
return;
|
||||
|
||||
if (type == NoteType.Slide)
|
||||
return;
|
||||
|
||||
NotesCounted++;
|
||||
|
||||
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
|
||||
switch (data.state)
|
||||
{
|
||||
case JudgeState.Early:
|
||||
EarlyCount++;
|
||||
break;
|
||||
case JudgeState.Late:
|
||||
LateCount++;
|
||||
break;
|
||||
}
|
||||
|
||||
TotalDeviation += data.offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
using System;
|
||||
using SimaiSharp.Structures;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AstroDX.Models.Scoring.Metrics.Internal
|
||||
{
|
||||
public sealed class JudgementStats : IReactiveStatistic
|
||||
{
|
||||
public NoteRecord HoldRecord { get; }
|
||||
public NoteRecord SlideRecord { get; }
|
||||
public NoteRecord TapRecord { get; }
|
||||
public NoteRecord TouchRecord { get; }
|
||||
public NoteRecord BreakRecord { get; }
|
||||
|
||||
public uint MaxTapCount { get; }
|
||||
public uint MaxTouchCount { get; }
|
||||
public uint MaxHoldCount { get; }
|
||||
public uint MaxSlideCount { get; }
|
||||
public uint MaxBreakCount { get; }
|
||||
|
||||
public uint JudgedCriticalCount { get; private set; }
|
||||
public uint JudgedPerfectCount { get; private set; }
|
||||
public uint JudgedGreatCount { get; private set; }
|
||||
public uint JudgedGoodCount { get; private set; }
|
||||
public uint JudgedMissCount { get; private set; }
|
||||
public uint JudgedNoteCount { get; private set; }
|
||||
|
||||
public uint MaxNoteCount =>
|
||||
TapRecord.MaxCount + TouchRecord.MaxCount +
|
||||
HoldRecord.MaxCount + SlideRecord.MaxCount +
|
||||
BreakRecord.MaxCount;
|
||||
|
||||
public bool AllNotesPassed =>
|
||||
TapRecord.MaxCount == TapRecord.PassedCount &&
|
||||
TouchRecord.MaxCount == TouchRecord.PassedCount &&
|
||||
HoldRecord.MaxCount == HoldRecord.PassedCount &&
|
||||
SlideRecord.MaxCount == SlideRecord.PassedCount &&
|
||||
BreakRecord.MaxCount == BreakRecord.PassedCount;
|
||||
|
||||
public JudgementStats(ReadOnlyMemory<NoteCollection> noteCollections)
|
||||
{
|
||||
foreach (var noteCollection in noteCollections.Span)
|
||||
{
|
||||
foreach (var note in noteCollection)
|
||||
{
|
||||
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
|
||||
switch (note.type)
|
||||
{
|
||||
case NoteType.Tap:
|
||||
MaxTapCount++;
|
||||
break;
|
||||
case NoteType.Touch:
|
||||
MaxTouchCount++;
|
||||
break;
|
||||
case NoteType.Hold:
|
||||
MaxHoldCount++;
|
||||
break;
|
||||
case NoteType.Break:
|
||||
MaxBreakCount++;
|
||||
break;
|
||||
}
|
||||
|
||||
foreach (var slide in note.slidePaths)
|
||||
{
|
||||
if (slide.type == NoteType.Break)
|
||||
MaxBreakCount++;
|
||||
else
|
||||
MaxSlideCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TapRecord = new NoteRecord(MaxTapCount);
|
||||
TouchRecord = new NoteRecord(MaxTouchCount);
|
||||
HoldRecord = new NoteRecord(MaxHoldCount);
|
||||
SlideRecord = new NoteRecord(MaxSlideCount);
|
||||
BreakRecord = new NoteRecord(MaxBreakCount);
|
||||
}
|
||||
|
||||
public void Push(in NoteType type, in JudgeData data)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case NoteType.Tap:
|
||||
TapRecord.Push(in data);
|
||||
break;
|
||||
case NoteType.Touch:
|
||||
TouchRecord.Push(in data);
|
||||
break;
|
||||
case NoteType.Hold:
|
||||
HoldRecord.Push(in data);
|
||||
break;
|
||||
case NoteType.Slide:
|
||||
SlideRecord.Push(in data);
|
||||
break;
|
||||
case NoteType.Break:
|
||||
BreakRecord.Push(in data);
|
||||
BreakRecord.Extras += data.grade switch
|
||||
{
|
||||
JudgeGrade.CriticalPerfect => 1,
|
||||
JudgeGrade.Perfect =>
|
||||
Mathf.Abs((float)data.offset) <= 0.033335f ? 0.75 : 0.5,
|
||||
JudgeGrade.Great => 0.4,
|
||||
JudgeGrade.Good => 0.3,
|
||||
JudgeGrade.Miss => 0,
|
||||
_ => 0
|
||||
};
|
||||
|
||||
break;
|
||||
case NoteType.ForceInvalidate:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
switch (data.grade)
|
||||
{
|
||||
case JudgeGrade.CriticalPerfect:
|
||||
JudgedCriticalCount++;
|
||||
break;
|
||||
case JudgeGrade.Perfect:
|
||||
JudgedPerfectCount++;
|
||||
break;
|
||||
case JudgeGrade.Great:
|
||||
JudgedGreatCount++;
|
||||
break;
|
||||
case JudgeGrade.Good:
|
||||
JudgedGoodCount++;
|
||||
break;
|
||||
case JudgeGrade.Miss:
|
||||
JudgedMissCount++;
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
JudgedNoteCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
55
core-dump/Scripts/Models/Scoring/Metrics/Statistics.cs
Normal file
55
core-dump/Scripts/Models/Scoring/Metrics/Statistics.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AstroDX.Models.Scoring.Metrics.Internal;
|
||||
using Sigtrap.Relays;
|
||||
using SimaiSharp.Structures;
|
||||
|
||||
namespace AstroDX.Models.Scoring.Metrics
|
||||
{
|
||||
[Serializable]
|
||||
public sealed class Statistics
|
||||
{
|
||||
public Relay<NoteType, JudgeData> OnJudgementReceived { get; } = new();
|
||||
|
||||
public FluctuationStats FluctuationStats { get; }
|
||||
public ComboStats ComboStats { get; }
|
||||
public JudgementStats JudgementStats { get; }
|
||||
public AchievementStats AchievementStats { get; }
|
||||
public DxScoreStats DxScoreStats { get; }
|
||||
private HashSet<IReactiveStatistic> Extensions { get; }
|
||||
|
||||
public Statistics(ReadOnlyMemory<NoteCollection> chart)
|
||||
{
|
||||
JudgementStats = new JudgementStats(chart);
|
||||
FluctuationStats = new FluctuationStats();
|
||||
ComboStats = new ComboStats();
|
||||
AchievementStats = new AchievementStats(this);
|
||||
DxScoreStats = new DxScoreStats(this);
|
||||
Extensions = new HashSet<IReactiveStatistic>();
|
||||
}
|
||||
|
||||
public void Push(in NoteType type, in JudgeData data)
|
||||
{
|
||||
JudgementStats.Push(in type, in data);
|
||||
ComboStats.Push(in type, in data);
|
||||
FluctuationStats.Push(in type, in data);
|
||||
|
||||
foreach (var statistic in Extensions)
|
||||
statistic.Push(in type, in data);
|
||||
|
||||
OnJudgementReceived.Dispatch(type, data);
|
||||
}
|
||||
|
||||
public void RegisterExtension(IReactiveStatistic extension)
|
||||
{
|
||||
Extensions.Add(extension);
|
||||
}
|
||||
|
||||
public IReactiveStatistic GetExtensionOrDefault<T>()
|
||||
where T : IReactiveStatistic
|
||||
{
|
||||
return Extensions.FirstOrDefault(e => e.GetType() == typeof(T));
|
||||
}
|
||||
}
|
||||
}
|
||||
72
core-dump/Scripts/Models/Scoring/NoteRecord.cs
Normal file
72
core-dump/Scripts/Models/Scoring/NoteRecord.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace AstroDX.Models.Scoring
|
||||
{
|
||||
public class NoteRecord
|
||||
{
|
||||
public IReadOnlyList<JudgeData> Judged { get; }
|
||||
|
||||
public uint MaxCount { get; }
|
||||
public uint PassedCount { get; private set; }
|
||||
public uint CriticalCount { get; private set; }
|
||||
public uint PerfectCount { get; private set; }
|
||||
public uint GreatCount { get; private set; }
|
||||
public uint GoodCount { get; private set; }
|
||||
public uint MissCount { get; private set; }
|
||||
public double Extras { get; set; }
|
||||
|
||||
public NoteRecord(uint maxCount)
|
||||
{
|
||||
Judged = new List<JudgeData>();
|
||||
MaxCount = maxCount;
|
||||
PassedCount = 0;
|
||||
Extras = 0;
|
||||
}
|
||||
|
||||
public void Push(in JudgeData data)
|
||||
{
|
||||
((List<JudgeData>)Judged).Add(data);
|
||||
|
||||
PassedCount++;
|
||||
switch (data.grade)
|
||||
{
|
||||
case JudgeGrade.CriticalPerfect:
|
||||
CriticalCount++;
|
||||
break;
|
||||
case JudgeGrade.Perfect:
|
||||
PerfectCount++;
|
||||
break;
|
||||
case JudgeGrade.Great:
|
||||
GreatCount++;
|
||||
break;
|
||||
case JudgeGrade.Good:
|
||||
GoodCount++;
|
||||
break;
|
||||
case JudgeGrade.Miss:
|
||||
MissCount++;
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
Points += GetScore(data.grade);
|
||||
}
|
||||
|
||||
public double Points { get; private set; }
|
||||
|
||||
private static double GetScore(JudgeGrade grade)
|
||||
{
|
||||
return grade switch
|
||||
{
|
||||
JudgeGrade.CriticalPerfect => 1,
|
||||
JudgeGrade.Perfect => 1,
|
||||
JudgeGrade.Great => 0.8,
|
||||
JudgeGrade.Good => 0.5,
|
||||
JudgeGrade.Miss => 0,
|
||||
_ => throw new
|
||||
ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
3
core-dump/Scripts/UI/RecyclingScrollRect/Interfaces.meta
Executable file
3
core-dump/Scripts/UI/RecyclingScrollRect/Interfaces.meta
Executable file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6a0d1a0a74b14c94af6f7d4d943075d8
|
||||
timeCreated: 1710933820
|
||||
101
core-dump/Scripts/UI/RecyclingScrollRect/Interfaces/ScrollRectRecyclingBase.cs
Executable file
101
core-dump/Scripts/UI/RecyclingScrollRect/Interfaces/ScrollRectRecyclingBase.cs
Executable file
@@ -0,0 +1,101 @@
|
||||
using System.Collections.Generic;
|
||||
using DG.Tweening;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.Pool;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace AstroDX.UI.RecyclingScrollRect.Interfaces
|
||||
{
|
||||
[RequireComponent(typeof(ScrollRect))]
|
||||
public abstract class ScrollRectRecyclingBase<TData> : MonoBehaviour,
|
||||
IDragHandler,
|
||||
IBeginDragHandler,
|
||||
IInitializePotentialDragHandler,
|
||||
IEndDragHandler
|
||||
{
|
||||
protected IList<TData> DataSource { get; private set; } = new List<TData>();
|
||||
|
||||
protected ObjectPool<ScrollViewItem<TData>> itemsPool;
|
||||
|
||||
[field: Disable, SerializeField]
|
||||
protected List<ScrollViewItem<TData>> ShownItemsUnordered { get; private set; } = new();
|
||||
|
||||
[BeginGroup("List View")]
|
||||
[NotNull, SerializeField, Tooltip("Where the items will be created.")]
|
||||
protected ScrollRect scrollRect;
|
||||
|
||||
[NotNull, SerializeField, Tooltip("The visual representation of each item.")]
|
||||
[EndGroup]
|
||||
protected ScrollViewItem<TData> itemPrefab;
|
||||
|
||||
private bool _initialized;
|
||||
|
||||
protected virtual void OnEnable()
|
||||
{
|
||||
if (_initialized)
|
||||
return;
|
||||
|
||||
itemsPool = new ObjectPool<ScrollViewItem<TData>>(() =>
|
||||
{
|
||||
var item = Instantiate(itemPrefab, scrollRect.content);
|
||||
item.DisableItem();
|
||||
return item;
|
||||
},
|
||||
item => item.EnableItem(),
|
||||
item => item.DisableItem());
|
||||
}
|
||||
|
||||
public void SetDataSource(IList<TData> dataSource)
|
||||
{
|
||||
DataSource = dataSource;
|
||||
ReloadItems();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reloads the scroll rect to match up with the data source.
|
||||
/// </summary>
|
||||
protected abstract void ReloadItems();
|
||||
|
||||
/// <summary>
|
||||
/// Sets the current visible range, load and discards items depending on the previous range.
|
||||
/// <param name="resetActiveItems">Set the item data again to items that can be recycled. Triggered when data source changes.</param>
|
||||
/// </summary>
|
||||
protected abstract void UpdateVisibleRange(bool resetActiveItems);
|
||||
|
||||
/// <summary>
|
||||
/// Jumps to the item in the scroll rect.
|
||||
/// </summary>
|
||||
public void JumpToItem(TData item)
|
||||
{
|
||||
// We don't do any error handling here so that the error is thrown and developers using this can debug what's happening.
|
||||
JumpToIndex(DataSource.IndexOf(item));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scrolls to the item in the scroll rect.
|
||||
/// </summary>
|
||||
public void ScrollToItem(TData item, float duration, Ease easing = default)
|
||||
{
|
||||
// We don't do any error handling here so that the error is thrown and developers using this can debug what's happening.
|
||||
ScrollToIndex(DataSource.IndexOf(item), duration, easing);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Jumps to the index provided by the data source in the scroll rect.
|
||||
/// </summary>
|
||||
public abstract void JumpToIndex(int index);
|
||||
|
||||
public abstract void ScrollToIndex(int index, float duration, Ease easing = default);
|
||||
|
||||
protected virtual void OnDestroy() => itemsPool.Dispose();
|
||||
|
||||
public abstract void OnDrag(PointerEventData eventData);
|
||||
|
||||
public abstract void OnBeginDrag(PointerEventData eventData);
|
||||
|
||||
public abstract void OnInitializePotentialDrag(PointerEventData eventData);
|
||||
|
||||
public abstract void OnEndDrag(PointerEventData eventData);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5843eb5b52bc4cd085d670e17ff8be63
|
||||
timeCreated: 1710933385
|
||||
34
core-dump/Scripts/UI/RecyclingScrollRect/Interfaces/ScrollViewItem.cs
Executable file
34
core-dump/Scripts/UI/RecyclingScrollRect/Interfaces/ScrollViewItem.cs
Executable file
@@ -0,0 +1,34 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace AstroDX.UI.RecyclingScrollRect.Interfaces
|
||||
{
|
||||
public abstract class ScrollViewItem<T> : MonoBehaviour
|
||||
{
|
||||
public T Data { get; private set; }
|
||||
public int Index { get; private set; }
|
||||
|
||||
public void SetData(T data, int index)
|
||||
{
|
||||
Data = data;
|
||||
Index = index;
|
||||
|
||||
OnDataUpdated();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is called whenever this instance is requested to render a new item with a different index.
|
||||
/// </summary>
|
||||
protected abstract void OnDataUpdated();
|
||||
|
||||
/// <summary>
|
||||
/// When this object instance scrolls out of view, this is called to disable relevant components;
|
||||
/// Should be as lightweight as possible since disabled items might get reused instantly.
|
||||
/// </summary>
|
||||
internal abstract void DisableItem();
|
||||
|
||||
/// <summary>
|
||||
/// Called when an item is retrieved from the pool.
|
||||
/// </summary>
|
||||
internal abstract void EnableItem();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2f1b83f97d7847728b5f776276799111
|
||||
timeCreated: 1710933857
|
||||
3
core-dump/Scripts/UI/RecyclingScrollRect/ScrollRects.meta
Executable file
3
core-dump/Scripts/UI/RecyclingScrollRect/ScrollRects.meta
Executable file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d313c27e4a974b3baea75f0d80917046
|
||||
timeCreated: 1710933392
|
||||
236
core-dump/Scripts/UI/RecyclingScrollRect/ScrollRects/RecyclingGridView.cs
Executable file
236
core-dump/Scripts/UI/RecyclingScrollRect/ScrollRects/RecyclingGridView.cs
Executable file
@@ -0,0 +1,236 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AstroDX.GameSettings;
|
||||
using AstroDX.UI.RecyclingScrollRect.Interfaces;
|
||||
using AstroDX.Utilities.Extensions;
|
||||
using DG.Tweening;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
|
||||
namespace AstroDX.UI.RecyclingScrollRect.ScrollRects
|
||||
{
|
||||
[Serializable]
|
||||
public class RecyclingGridView<TData> : ScrollRectRecyclingBase<TData>
|
||||
{
|
||||
[BeginGroup("Scrolling")]
|
||||
[SerializeField]
|
||||
private float gap;
|
||||
|
||||
[SerializeField]
|
||||
private float marginTop;
|
||||
|
||||
[SerializeField]
|
||||
private float marginBottom;
|
||||
|
||||
[Disable, SerializeField]
|
||||
private Vector2 itemSize;
|
||||
|
||||
[Disable, SerializeField]
|
||||
private int visibleStart;
|
||||
|
||||
[Disable, SerializeField]
|
||||
private int columnCount;
|
||||
|
||||
[Disable, SerializeField]
|
||||
private int rowCount;
|
||||
|
||||
[SerializeField, Disable]
|
||||
private float usableWidth;
|
||||
|
||||
[Disable, SerializeField]
|
||||
private int maxVisibleCount;
|
||||
|
||||
[Disable, SerializeField]
|
||||
private float contentHeight;
|
||||
|
||||
private Tweener _positionTween;
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
itemSize = itemPrefab?.transform.AsRectTransform().sizeDelta ?? Vector2.zero;
|
||||
|
||||
scrollRect.content.anchorMin = new Vector2(0, 1);
|
||||
scrollRect.content.anchorMax = new Vector2(1, 1);
|
||||
scrollRect.content.pivot = new Vector2(0.5f, 1);
|
||||
scrollRect.content.localScale = Vector3.one;
|
||||
|
||||
var dt = new DrivenRectTransformTracker();
|
||||
dt.Clear();
|
||||
|
||||
//Object to drive the transform
|
||||
dt.Add(this, scrollRect.content, DrivenTransformProperties.All);
|
||||
}
|
||||
|
||||
protected override void OnEnable()
|
||||
{
|
||||
base.OnEnable();
|
||||
Settings.Profile.Graphics.scaleFactorChanged.AddListener(ReloadItems);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (scrollRect.velocity.y == 0)
|
||||
return;
|
||||
|
||||
UpdateVisibleRange(false);
|
||||
}
|
||||
|
||||
protected override void ReloadItems()
|
||||
{
|
||||
columnCount = Mathf.FloorToInt((scrollRect.viewport.rect.width + gap) / (itemSize.x + gap));
|
||||
usableWidth = columnCount * itemSize.x + (columnCount - 1) * gap;
|
||||
|
||||
rowCount = Mathf.CeilToInt((float)DataSource.Count / columnCount);
|
||||
|
||||
maxVisibleCount = Mathf.CeilToInt((scrollRect.viewport.rect.size.y + gap) / (itemSize.y + gap)) * columnCount;
|
||||
|
||||
contentHeight = marginTop + rowCount * (itemSize.y + gap) + marginBottom;
|
||||
scrollRect.content.sizeDelta = new Vector2(scrollRect.content.sizeDelta.x, contentHeight);
|
||||
|
||||
UpdateVisibleRange(true);
|
||||
}
|
||||
|
||||
private readonly HashSet<int> _itemsToAdd = new();
|
||||
|
||||
private int _previousStartIndexInclusive;
|
||||
private int _previousEndIndexExclusive;
|
||||
|
||||
protected override void UpdateVisibleRange(bool resetActiveItems)
|
||||
{
|
||||
visibleStart = scrollRect.content.anchoredPosition.y <= marginTop
|
||||
? 0
|
||||
: columnCount *
|
||||
Mathf.FloorToInt((scrollRect.content.anchoredPosition.y - marginTop) / (itemSize.y + gap));
|
||||
|
||||
var startIndexInclusive = visibleStart;
|
||||
var endIndexExclusive = Mathf.Min(startIndexInclusive + maxVisibleCount, DataSource.Count);
|
||||
|
||||
// If the visible range hasn't changed, just update the positions of all items in range
|
||||
if (startIndexInclusive == _previousStartIndexInclusive &&
|
||||
endIndexExclusive == _previousEndIndexExclusive)
|
||||
{
|
||||
foreach (var item in ShownItemsUnordered)
|
||||
{
|
||||
item.GetRectTransform().anchoredPosition = AbsolutePositionAtIndex(item.Index);
|
||||
|
||||
if (resetActiveItems)
|
||||
item.SetData(DataSource[item.Index], item.Index);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_previousStartIndexInclusive = startIndexInclusive;
|
||||
_previousEndIndexExclusive = endIndexExclusive;
|
||||
|
||||
// Populate the list of items that should be shown by the end of this procedure
|
||||
for (var i = startIndexInclusive; i < endIndexExclusive; i++)
|
||||
_itemsToAdd.Add(i);
|
||||
|
||||
// Find and skip items that are already shown
|
||||
// Since the shown items are unordered (for example, we may request to show 1 after 2 and 3 are shown, resulting in [2, 3, 1]),
|
||||
// We can't make any assumptions about items not appearing later in the list (e.g. [1, 2, 4, 5, 6, 3])
|
||||
for (var i = 0; i < ShownItemsUnordered.Count; i++)
|
||||
{
|
||||
var item = ShownItemsUnordered[i];
|
||||
|
||||
if (_itemsToAdd.Contains(item.Index))
|
||||
{
|
||||
item.GetRectTransform().anchoredPosition = AbsolutePositionAtIndex(item.Index);
|
||||
|
||||
if (resetActiveItems)
|
||||
item.SetData(DataSource[item.Index], item.Index);
|
||||
|
||||
_itemsToAdd.Remove(item.Index);
|
||||
}
|
||||
else
|
||||
{
|
||||
// We don't need to render this item in the new visible range, so we can safely release it
|
||||
itemsPool.Release(item);
|
||||
|
||||
// Modifying a list while enumerating it isn't recommended
|
||||
// But we're doing it here for performance
|
||||
ShownItemsUnordered.RemoveAt(i);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
// Lastly, generate the actual missing items
|
||||
foreach (var index in _itemsToAdd)
|
||||
{
|
||||
var itemInstance = itemsPool.Get();
|
||||
ShownItemsUnordered.Add(itemInstance);
|
||||
itemInstance.SetData(DataSource[index], index);
|
||||
itemInstance.GetRectTransform().anchoredPosition = AbsolutePositionAtIndex(index);
|
||||
}
|
||||
|
||||
_itemsToAdd.Clear();
|
||||
}
|
||||
|
||||
public override void JumpToIndex(int index)
|
||||
{
|
||||
scrollRect.velocity = new Vector3(0, 0);
|
||||
scrollRect.verticalNormalizedPosition = GetScrollPosition(index);
|
||||
UpdateVisibleRange(false);
|
||||
}
|
||||
|
||||
public override void ScrollToIndex(int index, float duration, Ease easing = default)
|
||||
{
|
||||
scrollRect.velocity = new Vector3(0, 0);
|
||||
_positionTween?.Kill();
|
||||
|
||||
_positionTween = DOTween.To(() => scrollRect.verticalNormalizedPosition,
|
||||
p =>
|
||||
{
|
||||
scrollRect.verticalNormalizedPosition = p;
|
||||
UpdateVisibleRange(false);
|
||||
},
|
||||
GetScrollPosition(index),
|
||||
duration).SetEase(easing);
|
||||
}
|
||||
|
||||
private float GetScrollPosition(int index)
|
||||
{
|
||||
// absolute position at index starts 0 at the top, and decreases when descending
|
||||
var normalizedPosition = 1 + AbsolutePositionAtIndex(index).y / contentHeight;
|
||||
normalizedPosition -= ((itemSize.y + marginBottom) * (1 - normalizedPosition) - marginTop * normalizedPosition) /
|
||||
contentHeight;
|
||||
|
||||
return normalizedPosition;
|
||||
}
|
||||
|
||||
public override void OnInitializePotentialDrag(PointerEventData eventData)
|
||||
{
|
||||
if (eventData.button != PointerEventData.InputButton.Left)
|
||||
return;
|
||||
|
||||
_positionTween?.Kill();
|
||||
}
|
||||
|
||||
public override void OnBeginDrag(PointerEventData eventData)
|
||||
{
|
||||
}
|
||||
|
||||
public override void OnEndDrag(PointerEventData eventData)
|
||||
{
|
||||
}
|
||||
|
||||
public override void OnDrag(PointerEventData eventData)
|
||||
{
|
||||
if (eventData.button != PointerEventData.InputButton.Left)
|
||||
return;
|
||||
|
||||
UpdateVisibleRange(false);
|
||||
}
|
||||
|
||||
private Vector2 AbsolutePositionAtIndex(int index)
|
||||
{
|
||||
var column = index % columnCount;
|
||||
var fullContainerWidth = scrollRect.content.rect.size.x;
|
||||
var row = Mathf.FloorToInt((float)index / columnCount);
|
||||
|
||||
return new Vector2(column * (itemSize.x + gap) + (fullContainerWidth - usableWidth) / 2,
|
||||
-marginTop - row * (itemSize.y + gap));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 163b88140e634e719f1a2e8ae214523f
|
||||
timeCreated: 1710933419
|
||||
@@ -0,0 +1,207 @@
|
||||
using System.Collections.Generic;
|
||||
using AstroDX.UI.RecyclingScrollRect.Interfaces;
|
||||
using AstroDX.Utilities.Extensions;
|
||||
using DG.Tweening;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
|
||||
namespace AstroDX.UI.RecyclingScrollRect.ScrollRects
|
||||
{
|
||||
public class RecyclingVerticalListView<TData> : ScrollRectRecyclingBase<TData>
|
||||
{
|
||||
[BeginGroup("Scrolling")]
|
||||
[SerializeField]
|
||||
private float gap;
|
||||
|
||||
[SerializeField]
|
||||
private float marginTop;
|
||||
|
||||
[SerializeField]
|
||||
[EndGroup]
|
||||
private float marginBottom;
|
||||
|
||||
[Disable, SerializeField]
|
||||
private float itemHeight;
|
||||
|
||||
[Disable, SerializeField]
|
||||
private int maxVisibleCount;
|
||||
|
||||
[Disable, SerializeField]
|
||||
private float contentHeight;
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
itemHeight = itemPrefab?.GetRectTransform().sizeDelta.y ?? 0;
|
||||
|
||||
scrollRect.content.anchorMin = new Vector2(0, 1);
|
||||
scrollRect.content.anchorMax = new Vector2(1, 1);
|
||||
scrollRect.content.pivot = new Vector2(0.5f, 1);
|
||||
scrollRect.content.localScale = Vector3.one;
|
||||
|
||||
var dt = new DrivenRectTransformTracker();
|
||||
dt.Clear();
|
||||
|
||||
//Object to drive the transform
|
||||
dt.Add(this, scrollRect.content, DrivenTransformProperties.All);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (scrollRect.velocity.y == 0)
|
||||
return;
|
||||
|
||||
UpdateVisibleRange(false);
|
||||
}
|
||||
|
||||
protected override void ReloadItems()
|
||||
{
|
||||
maxVisibleCount = Mathf.CeilToInt((scrollRect.viewport.rect.size.y + gap) / (itemHeight + gap));
|
||||
|
||||
contentHeight = marginTop + (itemHeight + gap) + marginBottom;
|
||||
scrollRect.content.sizeDelta = new Vector2(scrollRect.content.sizeDelta.x, contentHeight);
|
||||
|
||||
UpdateVisibleRange(true);
|
||||
}
|
||||
|
||||
private readonly HashSet<int> _itemsToAdd = new();
|
||||
|
||||
private int _previousStartIndexInclusive;
|
||||
private int _previousEndIndexExclusive;
|
||||
|
||||
protected override void UpdateVisibleRange(bool resetActiveItems)
|
||||
{
|
||||
var startIndexInclusive = scrollRect.content.anchoredPosition.y <= marginTop
|
||||
? 0
|
||||
: Mathf.FloorToInt((scrollRect.content.anchoredPosition.y - marginTop) /
|
||||
(itemHeight + gap));
|
||||
var endIndexExclusive = Mathf.Min(startIndexInclusive + maxVisibleCount, DataSource.Count);
|
||||
|
||||
// If the visible range hasn't changed, just update the positions of all items in range
|
||||
if (startIndexInclusive == _previousStartIndexInclusive &&
|
||||
endIndexExclusive == _previousEndIndexExclusive)
|
||||
{
|
||||
foreach (var item in ShownItemsUnordered)
|
||||
{
|
||||
var position = item.GetRectTransform().anchoredPosition;
|
||||
position.y = AbsolutePositionAtIndex(item.Index);
|
||||
item.GetRectTransform().anchoredPosition = position;
|
||||
|
||||
if (resetActiveItems)
|
||||
item.SetData(DataSource[item.Index], item.Index);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_previousStartIndexInclusive = startIndexInclusive;
|
||||
_previousEndIndexExclusive = endIndexExclusive;
|
||||
|
||||
// Populate the list of items that should be shown by the end of this procedure
|
||||
for (var i = startIndexInclusive; i < endIndexExclusive; i++)
|
||||
_itemsToAdd.Add(i);
|
||||
|
||||
// Find and skip items that are already shown
|
||||
// Since the shown items are unordered (for example, we may request to show 1 after 2 and 3 are shown, resulting in [2, 3, 1]),
|
||||
// We can't make any assumptions about items not appearing later in the list (e.g. [1, 2, 4, 5, 6, 3])
|
||||
for (var i = 0; i < ShownItemsUnordered.Count; i++)
|
||||
{
|
||||
var item = ShownItemsUnordered[i];
|
||||
|
||||
if (_itemsToAdd.Contains(item.Index))
|
||||
{
|
||||
var position = item.GetRectTransform().anchoredPosition;
|
||||
position.y = AbsolutePositionAtIndex(item.Index);
|
||||
item.GetRectTransform().anchoredPosition = position;
|
||||
|
||||
if (resetActiveItems)
|
||||
item.SetData(DataSource[item.Index], item.Index);
|
||||
|
||||
_itemsToAdd.Remove(item.Index);
|
||||
}
|
||||
else
|
||||
{
|
||||
// We don't need to render this item in the new visible range, so we can safely release it
|
||||
itemsPool.Release(item);
|
||||
|
||||
// Modifying a list while enumerating it isn't recommended
|
||||
// But we're doing it here for performance
|
||||
ShownItemsUnordered.RemoveAt(i);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
// Lastly, generate the actual missing items
|
||||
foreach (var index in _itemsToAdd)
|
||||
{
|
||||
var itemInstance = itemsPool.Get();
|
||||
ShownItemsUnordered.Add(itemInstance);
|
||||
itemInstance.SetData(DataSource[index], index);
|
||||
|
||||
var position = itemInstance.GetRectTransform().anchoredPosition;
|
||||
position.y = AbsolutePositionAtIndex(index);
|
||||
itemInstance.GetRectTransform().anchoredPosition = position;
|
||||
}
|
||||
|
||||
_itemsToAdd.Clear();
|
||||
}
|
||||
|
||||
public override void JumpToIndex(int index)
|
||||
{
|
||||
scrollRect.velocity = new Vector3(0, 0);
|
||||
scrollRect.verticalNormalizedPosition = GetScrollPosition(index);
|
||||
UpdateVisibleRange(false);
|
||||
}
|
||||
|
||||
private Tweener _positionTween;
|
||||
|
||||
public override void ScrollToIndex(int index, float duration, Ease easing = default)
|
||||
{
|
||||
scrollRect.velocity = Vector2.zero;
|
||||
_positionTween?.Kill();
|
||||
_positionTween = DOTween.To(() => scrollRect.verticalNormalizedPosition, p =>
|
||||
{
|
||||
scrollRect.verticalNormalizedPosition = p;
|
||||
UpdateVisibleRange(false);
|
||||
}, GetScrollPosition(index), duration).SetEase(easing);
|
||||
}
|
||||
|
||||
private float GetScrollPosition(int index)
|
||||
{
|
||||
// absolute position at index starts 0 at the top, and decreases when descending
|
||||
var normalizedPosition = 1 + AbsolutePositionAtIndex(index) / contentHeight;
|
||||
normalizedPosition -= ((itemHeight + marginBottom) * (1 - normalizedPosition) - marginTop * normalizedPosition) /
|
||||
contentHeight;
|
||||
|
||||
return normalizedPosition;
|
||||
}
|
||||
|
||||
public override void OnInitializePotentialDrag(PointerEventData eventData)
|
||||
{
|
||||
if (eventData.button != PointerEventData.InputButton.Left)
|
||||
return;
|
||||
|
||||
_positionTween?.Kill();
|
||||
}
|
||||
|
||||
public override void OnBeginDrag(PointerEventData eventData)
|
||||
{
|
||||
}
|
||||
|
||||
public override void OnEndDrag(PointerEventData eventData)
|
||||
{
|
||||
}
|
||||
|
||||
public override void OnDrag(PointerEventData eventData)
|
||||
{
|
||||
if (eventData.button != PointerEventData.InputButton.Left)
|
||||
return;
|
||||
|
||||
UpdateVisibleRange(false);
|
||||
}
|
||||
|
||||
private float AbsolutePositionAtIndex(int index)
|
||||
{
|
||||
return -index * (itemHeight + gap) - marginTop;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6421e225fef64262bce238235e6f6e0a
|
||||
timeCreated: 1711013891
|
||||
Reference in New Issue
Block a user