Compare commits

...

45 Commits

Author SHA1 Message Date
fumiko
1d97e66612 Update README.md 2025-03-17 12:48:44 +08:00
fumiko
3355747f2e Update README.md 2025-03-16 01:23:33 +08:00
fumiko
c07804f26c Update README.md 2025-03-16 01:16:47 +08:00
fumiko
b2727924fb Update README.md 2025-03-16 01:10:28 +08:00
fumiko
0381abf602 Update README.md 2025-03-16 00:34:55 +08:00
fumiko
1ceec57422 Merge pull request #178 from Himori-0/main
Update README.md wiki link
2025-02-17 16:25:52 +08:00
Himori
7b20754920 Update README.md 2025-02-15 19:26:50 +07:00
Himori
e6c75c444e Update README.md 2025-02-15 18:32:34 +07:00
Soneoylys
4609498ced GROUP E 2025-01-08 04:20:45 -08:00
Soneoylys
e16c6e2847 typo 2025-01-05 14:32:03 -08:00
Soneoylys
74cb2e815b Merge pull request #141 from I21b/main
Change issue templates to yaml version
2025-01-05 11:10:15 -08:00
Soneoylys
ca170d2a95 Update disclaimer 2025-01-05 11:09:17 -08:00
fumiko
1ee6f00314 Merge pull request #155 from DavidScann/patch-3
Grammatical errors, rephrasing, added legal disclaimer.
2024-10-22 10:39:59 +08:00
DavidScann
0a2b69f970 Fixed more grammar. Added link to TestFlight Group D
Around line 32 -> 40
2024-10-20 23:44:35 -06:00
DavidScann
de631fa648 Grammatical errors, rephrasing, added legal disclaimer.
Line 9, 10 has been updated to include a legal disclaimer regarding our association with SEGA. Which is none.
2024-10-20 23:31:24 -06:00
92
c009cfdcbb Merge branch '2394425147:main' into main 2024-09-30 15:59:17 +09:00
fumiko
66244a15cc Update README.md 2024-09-28 01:36:09 +08:00
92
e25ebfb358 Remove markdown version issue template 2024-09-14 13:41:24 +09:00
92
9fc39c0eb2 Add yaml version issue template 2024-09-14 13:13:46 +09:00
Soneoylys
cf42114966 Update README.md 2024-06-29 20:34:18 +00:00
fumiko
2c1dd0b1f2 Update README.md
Added collection installation notice in the Q&A section
2024-06-27 00:55:35 +08:00
fumiko
fae214f6d3 Merge pull request #111 from VenB304/VenB304-patch-1
Update README.md
2024-06-27 00:52:28 +08:00
fumiko
23fb548e11 Update README.md 2024-06-24 16:09:45 +08:00
Karl Zyrele Palomo
dab1b8fd75 Update README.md
grammar, under Desktop version, and controller support.
2024-06-21 21:09:38 +08:00
Soneoylys
3c40b8da25 Test group update 2024-06-20 18:43:03 -07:00
Soneoylys
5e7932a825 Status update 2024-06-18 22:20:27 -07:00
Soneoylys
d8efe9c1f1 Update README.md 2024-05-13 10:15:51 -07:00
Soneoylys
866eefabd3 Merge pull request #98 from DavidScann/saltcute-readme-testflight
Update README.md
2024-05-13 09:34:34 -07:00
DavidScann
7c0cc48734 Update README.md 2024-05-13 09:20:10 -06:00
DavidScann
f3c735936a Update README.md
- Changed phrasing of first header
- Added disclaimer about iOS TestFlight
2024-05-13 09:13:38 -06:00
fumiko
d657fafab5 Merge pull request #95 from DavidScann/main
Changed around a bit of the English and updated the stable version number
2024-05-04 17:53:47 +08:00
DavidScann
af2a5478f1 Fixed up several odd English texts and version name 2024-05-04 03:27:59 -06:00
DavidScann
840ac79e38 Update README.md
Fixed up a couple of grammatical errors, as well as adding a few miscellaneous changes
2024-04-29 08:34:28 -06:00
Soneoylys
cf45ac7697 Make banner smaller 2024-04-29 06:31:39 -07:00
Soneoylys
2ecf542087 Update README.md 2024-04-29 06:29:02 -07:00
Soneoylys
7f6a1b01f8 B... Browser? 2024-04-16 03:43:42 -07:00
Soneoy_Mac
14db91ea0f ~~English (Traditional)~~ 2024-04-16 03:41:43 -07:00
Soneoy_Mac
b776940c48 Update scripts 2024-04-16 03:40:27 -07:00
Soneoylys
d26e1bba1f Group C NOW! 2024-02-22 08:41:29 -08:00
Soneoylys
37bd37a850 Update README.md 2024-02-02 13:03:16 -08:00
Soneoylys
04d6db6ec4 Update... a translation mistake 2024-02-02 07:16:16 -08:00
Soneoylys
b80a535c65 Add README translation 2024-02-02 05:42:53 -08:00
fumiko
de387958fa Updated Discord badge styles 2023-12-08 22:45:20 +08:00
fumiko
c94206f398 Updated Discord badges 2023-12-08 22:44:23 +08:00
Soneoylys
8ddffec227 Shiny Smily Discord invitation links~ 2023-11-17 07:20:08 -08:00
45 changed files with 1285 additions and 87 deletions

View File

@@ -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
View 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

View File

@@ -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.

View 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
View File

@@ -0,0 +1,2 @@
# Macintosh index files
.DS_Store

View File

@@ -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
![GitHub release (latest by date)](https://img.shields.io/github/v/release/2394425147/maipaddx?label=stable)
[![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/2394425147/maipaddx?include_prereleases)](https://github.com/2394425147/maipaddx/releases/latest)
[![Discord](https://img.shields.io/discord/892807792996536453)](https://discord.gg/6fpETgpvjZ)
![Downloads](https://img.shields.io/github/downloads/2394425147/maipaddx/total?label=Android)
![GitHub release (latest by date)](https://img.shields.io/github/v/release/2394425147/astrodx?label=Stable)
[![Discord](https://dcbadge.vercel.app/api/server/6fpETgpvjZ?style=flat)](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?
[![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/2394425147/maipaddx?include_prereleases)](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.
![GitHub release (latest by date)](https://img.shields.io/github/v/release/2394425147/maipaddx?label=stable)
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.
![TestFlight](https://img.shields.io/github/downloads/2394425147/maipaddx/total?label=TestFlight)
## 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.
## 問題が発生した場合
[![Discord](https://img.shields.io/discord/892807792996536453)](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)上一起討論。

View File

@@ -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; }
}
}

View File

@@ -0,0 +1,9 @@
using SimaiSharp.Structures;
namespace AstroDX.Models.Scoring.Metrics
{
public interface IReactiveStatistic
{
void Push(in NoteType type, in JudgeData data);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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++;
}
}
}

View 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));
}
}
}

View 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()
};
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6a0d1a0a74b14c94af6f7d4d943075d8
timeCreated: 1710933820

View 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);
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5843eb5b52bc4cd085d670e17ff8be63
timeCreated: 1710933385

View 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();
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2f1b83f97d7847728b5f776276799111
timeCreated: 1710933857

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d313c27e4a974b3baea75f0d80917046
timeCreated: 1710933392

View 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));
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 163b88140e634e719f1a2e8ae214523f
timeCreated: 1710933419

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6421e225fef64262bce238235e6f6e0a
timeCreated: 1711013891