mirror of
https://github.com/2394425147/astrodx.git
synced 2026-02-04 23:57:27 +08:00
Compare commits
45 Commits
v2.0.0.bet
...
v2.0.0.rc.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d97e66612 | ||
|
|
3355747f2e | ||
|
|
c07804f26c | ||
|
|
b2727924fb | ||
|
|
0381abf602 | ||
|
|
1ceec57422 | ||
|
|
7b20754920 | ||
|
|
e6c75c444e | ||
|
|
4609498ced | ||
|
|
e16c6e2847 | ||
|
|
74cb2e815b | ||
|
|
ca170d2a95 | ||
|
|
1ee6f00314 | ||
|
|
0a2b69f970 | ||
|
|
de631fa648 | ||
|
|
c009cfdcbb | ||
|
|
66244a15cc | ||
|
|
e25ebfb358 | ||
|
|
9fc39c0eb2 | ||
|
|
cf42114966 | ||
|
|
2c1dd0b1f2 | ||
|
|
fae214f6d3 | ||
|
|
23fb548e11 | ||
|
|
dab1b8fd75 | ||
|
|
3c40b8da25 | ||
|
|
5e7932a825 | ||
|
|
d8efe9c1f1 | ||
|
|
866eefabd3 | ||
|
|
7c0cc48734 | ||
|
|
f3c735936a | ||
|
|
d657fafab5 | ||
|
|
af2a5478f1 | ||
|
|
840ac79e38 | ||
|
|
cf45ac7697 | ||
|
|
2ecf542087 | ||
|
|
7f6a1b01f8 | ||
|
|
14db91ea0f | ||
|
|
b776940c48 | ||
|
|
d26e1bba1f | ||
|
|
37bd37a850 | ||
|
|
04d6db6ec4 | ||
|
|
b80a535c65 | ||
|
|
de387958fa | ||
|
|
c94206f398 | ||
|
|
8ddffec227 |
30
.github/ISSUE_TEMPLATE/bug_report.md
vendored
30
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,30 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: '2394425147'
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1.
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Device specs**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
83
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
83
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
name: Bug report
|
||||
description: Create a report to help us address issues you are facing
|
||||
title: "[Bug] "
|
||||
labels: [bug]
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: duplication
|
||||
attributes:
|
||||
label: ⠀
|
||||
options:
|
||||
- label: This issue is not duplicated with any other open or closed issues
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
⠀
|
||||
Thanks for taking the time to make us better!
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: A clear and concise description of what the bug is
|
||||
placeholder: |
|
||||
App crashes on startup every time after changing settings.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: A clear and concise description of what you expected to happen
|
||||
placeholder: |
|
||||
App started normally, everything worked fine.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: Steps to reproduce the bug
|
||||
placeholder: |
|
||||
1. Turn on "Show hitbox" in "Judgement" settings
|
||||
2. Restart the app
|
||||
3. Crash
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: environment
|
||||
attributes:
|
||||
label: Device information
|
||||
description: Provide details about your system environment
|
||||
placeholder: |
|
||||
Device: [e.g. Pixel 8 Pro]
|
||||
System: [e.g. Android 15 (AP41.240823.009)]
|
||||
Version: [e.g. v2.0.0.beta.2.patch.5]
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots
|
||||
description: If applicable, add screenshots to help explain your problem
|
||||
placeholder: |
|
||||
modified_setting_items.jpg
|
||||
crashed_screen.jpg
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context about the problem here
|
||||
placeholder: |
|
||||
Crash report:
|
||||
validations:
|
||||
required: false
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: '2394425147'
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
51
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal file
51
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
name: Feature request
|
||||
description: Suggest features you want to add or suggest to modify existing features
|
||||
title: "[Feature] "
|
||||
labels: [enhancement]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to make us better!
|
||||
|
||||
- type: textarea
|
||||
id: related
|
||||
attributes:
|
||||
label: Is your feature request related to a problem?
|
||||
description: A clear and concise description of what the problem
|
||||
placeholder: |
|
||||
I'm always frustrated when ...
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe the feature
|
||||
description: A clear and concise description of what the feature is
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reasons
|
||||
attributes:
|
||||
label: Reason for adding
|
||||
description: Explain why this feature would be useful to you
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: examples
|
||||
attributes:
|
||||
label: Example(s)
|
||||
description: Post screenshots/drawings/links/etc of the feature request, or proof-of-concept images about the feature
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context about the feature here
|
||||
validations:
|
||||
required: false
|
||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# Macintosh index files
|
||||
.DS_Store
|
||||
75
README.md
75
README.md
@@ -1,60 +1,61 @@
|
||||
> [!WARNING]
|
||||
> ### This GitHub repository is the ONLY official source for downloading the game.
|
||||
> AstroDX is **100% free**, and will never require payment from you.
|
||||
> If you purchased this game from any third-party website, store, or seller, you have been misled. These entities are not authorized to distribute or monetize this work. Request a refund immediately through the platform/store where you made the purchase. Report the listing to the platform's support team to help prevent further scams.
|
||||
|
||||
# AstroDX
|
||||

|
||||
[](https://github.com/2394425147/maipaddx/releases/latest)
|
||||
[](https://discord.gg/6fpETgpvjZ)
|
||||

|
||||

|
||||
[](https://discord.gg/6fpETgpvjZ)
|
||||
|
||||
AstroDX (Formerly named MaipadDX), is a mobile maimai simulator intended for those who don't yet have access to a cabinet, those who want to practice, and everyone interested in maimai.
|
||||
AstroDX (Formerly MaipadDX) is a mobile maimai simulator intended for those who do not yet have access to a cabinet, those who want to practice, and everyone interested in maimai who otherwise could not play the arcade game.
|
||||
This game is a clean-room implementation of maimai, and has been developed without using any use of original arcade data.
|
||||
|
||||
# Open-source status
|
||||
# Open-source projects
|
||||
|
||||
We initially intended for AstroDX to be fully open-source after it's uploaded to official app stores, but as the game contains paid assets, we might be only able to partially open-source AstroDX.
|
||||
- Simai serializer/deserializer: [SimaiSharp](https://github.com/reflektone-games/SimaiSharp)
|
||||
- Gameplay logic: [AstroDX core-dump](https://github.com/2394425147/maipaddx/tree/main/core-dump)
|
||||
|
||||
However, If you have issues, please don't hesitate to point it out in issues and we'll try to answer them as best as we can.
|
||||
|
||||
This game is a clean-room implementation of maimai, and has been developed without using any original arcade data.
|
||||
|
||||
- For simai interpreter and serializer used in AstroDX: [SimaiSharp](https://github.com/reflektone-games/SimaiSharp)
|
||||
- For game open-source parts: [AstroDX core-dump](https://github.com/2394425147/maipaddx/tree/main/core-dump)
|
||||
We intended to open-source AstroDX after publishing to the iOS/Android app stores. Paid assets will be excluded to comply with licenses.
|
||||
|
||||
# Q&A
|
||||
|
||||
## Which version should I download?
|
||||
## How do I download the iOS version?
|
||||
|
||||
[](https://github.com/2394425147/maipaddx/releases/latest)
|
||||
v2.0 is the latest version cycle, with full support for festival features, and a superset of simai3 syntaxes (deserialized via [SimaiSharp](https://github.com/reflektone-games/SimaiSharp)).
|
||||
v2.0 also includes optimizations, better interfaces, and more customizations. However, it's still in its pre-beta cycle, which means features aren't fully settled, and is prone to changes.
|
||||
> [!IMPORTANT]
|
||||
> Only 10k users can be in a group at any time. Don't join multiple groups if you joined one already.
|
||||
|
||||

|
||||
If you prefer a stable build instead, v1.1.1 is the **latest stable version**. You should use this if you prefer a long-term support experience.
|
||||
Join the testing program through [TestFlight Group A](https://testflight.apple.com/join/rACTLjPL), [TestFlight Group B](https://testflight.apple.com/join/ocj3yptn), [TestFlight Group C](https://testflight.apple.com/join/CuMxZE2M), or [TestFlight Group D](https://testflight.apple.com/join/T6qKfV6f) or [TestFlight Group E](https://testflight.apple.com/join/sMm1MCYc).
|
||||
|
||||
## Wait, is there version for iOS?
|
||||
Yes (kind of)! You can join the test at [TestFlight Group A](https://testflight.apple.com/join/rACTLjPL) or [TestFlight Group B](https://testflight.apple.com/join/ocj3yptn).
|
||||
Our [Discord server](https://discord.com/channels/892807792996536453/1210127565986205726/1210428179001380946) also offers a live tracker of available seats.
|
||||
|
||||

|
||||
## How do I get songs/levels?
|
||||
|
||||
Due to restriction of Apple, developer must be 18 (or 19 depends on local law) to publish Applications on App Store. Currently, we borrow JiNALE TEAM's developer account for bringing TestFlight up.
|
||||
- [Installation Guide for Android](https://wiki.astrodx.com/install/android)
|
||||
- [Installation Guide for iOS/iPadOS](https://wiki.astrodx.com/install/ios)
|
||||
|
||||
Each public link only can hold 10k players (count by Apple ID), so please NOT duplicate join the test if you already have AstroDX installed.
|
||||
|
||||
Since both of those slots are full again, we are planning on create another one soon, but ETA is not guaranteed on this.
|
||||
|
||||
## Are there any tutorials on importing?
|
||||
|
||||
Yep, they should be on the [wiki](https://github.com/2394425147/maipaddx/wiki/Importing-levels) of this repo.
|
||||
|
||||
## Can I use charts transcribed from the official arcade game maimai?
|
||||
## Can I play converts from maimai?
|
||||
|
||||
We **don't recommend** doing this, as it violates SEGA's policies.
|
||||
|
||||
## I'm having some issues...
|
||||
|
||||
You can open an issue [here](https://github.com/2394425147/maipaddx/issues).
|
||||
> [!TIP]
|
||||
> Writing your issue in English allows more people to understand and help you.
|
||||
> We also recommend searching for other people's issues for solutions first.
|
||||
|
||||
We welcome issues written in Mandarin, Japanese and English. However, it would be strongly suggested to provide translations (even using online translator) to English when submitting them, thus other people could understand as well.
|
||||
Open an issue [here](https://github.com/2394425147/astrodx/issues), or join our [Discord server](https://discord.gg/6fpETgpvjZ) to talk about it.
|
||||
|
||||
When submitting issues, please always ensure that you are running the latest released version. We also recommend reviewing existing issues to avoid duplication of questions or concerns.
|
||||
## 問題が発生した場合
|
||||
|
||||
[](https://discord.gg/6fpETgpvjZ)
|
||||
Alternatively, on our Discord server, we also have a help forum dedicated for issues, an faq, as well as a suggestions channel for feedback.
|
||||
> [!TIP]
|
||||
> イシューを英語で記載すると、より多くの人が理解し、助けてくれるでしょう。
|
||||
|
||||
Happy playing!
|
||||
[こちらから](https://github.com/2394425147/astrodx/issues)イシューを提出するか、[Discordサーバー](https://discord.com/channels/892807792996536453/1210127565986205726/1210428179001380946)に参加して相談してください。
|
||||
|
||||
## 我遇到了問題!
|
||||
|
||||
> [!TIP]
|
||||
> 我們强烈建議提供英文翻譯(或綫上翻譯),以便其他人理解,并幫助到你。我們也建議搜索現有的issue,以避免重複或疑慮。
|
||||
|
||||
在[這裏](https://github.com/2394425147/astrodx/issues)提交你的issue,或在我們的[Discord伺服器](https://discord.gg/6fpETgpvjZ)上一起討論。
|
||||
|
||||
@@ -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