Compare commits

...

52 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
Soneoy
adc4d7ef56 Upload core-dump files 2023-10-27 08:26:33 -07:00
fumiko
870a2b7b32 Merge pull request #51 from xhayper/main
feat: add clickable badges
2023-10-08 11:28:47 +08:00
hayper
3d47031bd2 feat: miss one badge 2023-10-08 08:20:29 +07:00
hayper
2d6aba81fa feat: add clickable badges 2023-10-08 08:16:17 +07:00
fumiko
787f3014d9 Merge pull request #41 from huantianad/main
README.md: add discord server invite link to badge
2023-07-17 11:54:45 +08:00
huantian
7fa93aab21 README.md: add discord server invite link to badge 2023-07-14 12:43:57 -07:00
Soneoylys
490db467f0 Update iOS TestFlight info 2023-05-19 09:58:16 -07:00
54 changed files with 3482 additions and 115 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,40 +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)
![Discord](https://img.shields.io/discord/892807792996536453)
![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.
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)
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).
## Are there any tutorials on importing?
Our [Discord server](https://discord.com/channels/892807792996536453/1210127565986205726/1210428179001380946) also offers a live tracker of available seats.
Yep, they should be on the [wiki](https://github.com/2394425147/maipaddx/wiki/Importing-levels) of this repo.
## How do I get songs/levels?
## Can I use charts transcribed from the official arcade game maimai?
- [Installation Guide for Android](https://wiki.astrodx.com/install/android)
- [Installation Guide for iOS/iPadOS](https://wiki.astrodx.com/install/ios)
## 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.
![Discord](https://img.shields.io/discord/892807792996536453)
Alternatively, on our Discord server, we also have a help forum dedicated for issues, an faq, as well as a suggestions channel for feedback.
Open an issue [here](https://github.com/2394425147/astrodx/issues), or join our [Discord server](https://discord.gg/6fpETgpvjZ) to talk about it.
Happy playing!
## 問題が発生した場合
> [!TIP]
> イシューを英語で記載すると、より多くの人が理解し、助けてくれるでしょう。
[こちらから](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,327 @@
using System.Collections.Generic;
using System.Linq;
using AstroDX.Contexts.Gameplay.Behaviours.Slide.SlideMarkers;
using AstroDX.Contexts.Gameplay.Interactions;
using AstroDX.Contexts.Gameplay.PlayerScope;
using AstroDX.Contexts.Gameplay.SlideGenerators;
using AstroDX.Globals;
using Medicine;
using SimaiSharp.Structures;
using UnityEngine;
namespace AstroDX.Contexts.Gameplay.Behaviours.Slide.Handlers
{
public class FanSlideSegmentHandler : SlideSegmentHandler
{
private const int Margin = 2;
private const int ArrowCount = 11;
private readonly List<FanSlideArrow> _arrows = new();
private readonly Vector2[] _endPositions;
private readonly List<List<TouchInteractable>> _interactableGroups = new();
private readonly Vector2 _startPosition;
private readonly Vector2[] _ups;
private int _interactionReceivedState;
private SlideStarBehaviour[] _stars;
public FanSlideSegmentHandler(SlideBehaviour slideBehaviour,
SlideSegment segment,
Location startLocation) :
base(slideBehaviour)
{
var vertices = segment.vertices.ToList();
vertices.Insert(0, startLocation);
_startPosition = SlideGenerator.GetPosition(vertices[0]);
var ccwEndLocation = startLocation;
ccwEndLocation.index += 3;
var centerEndLocation = startLocation;
centerEndLocation.index += 4;
var cwEndLocation = startLocation;
cwEndLocation.index += 5;
_endPositions = new[]
{
SlideGenerator.GetPosition(ccwEndLocation),
SlideGenerator.GetPosition(centerEndLocation),
SlideGenerator.GetPosition(cwEndLocation)
};
_ups = new[]
{
_endPositions[0] - _startPosition,
_endPositions[1] - _startPosition,
_endPositions[2] - _startPosition
};
_stars = new[]
{
SlideManager.slideStars.Get(),
SlideManager.slideStars.Get(),
SlideManager.slideStars.Get()
};
foreach (var star in _stars) star.Initialize(ParentPath, ParentNote.parentCollection);
GenerateSensors();
GenerateArrows();
}
[Inject.Single]
private SlideManager SlideManager { get; }
[Inject.Single]
private TouchManager TouchManager { get; }
public override float GetLength()
{
return RenderManager.PlayFieldRadius * 2;
}
public override void OnUpdate(float segmentT)
{
if (IsJudgementTarget)
CheckInteraction();
UpdateSlideStarPosition(segmentT);
UpdateArrowOpacity();
}
private void UpdateArrowOpacity()
{
var timeSinceNoteStart = TimeSinceSlideStart + ParentPath.delay;
var alpha = 0.8f;
if (timeSinceNoteStart < 0)
{
var colorPoint = Mathf.InverseLerp(-Persistent.Settings.Gameplay.slideFadeInDuration,
0,
(float)timeSinceNoteStart);
alpha = Mathf.Lerp(0, 0.8f, colorPoint);
}
foreach (var arrow in _arrows.Select(a => a.spriteRenderer))
{
var color = arrow.color;
color.a = alpha;
arrow.color = color;
}
}
private void CheckInteraction()
{
if (_interactionReceivedState == 0)
{
if (!_interactableGroups[0][0].GetSlideJudgement())
return;
_interactionReceivedState = 1;
foreach (var slideArrow in _arrows
.Where(a => a.interactionGroupIndex == 0)
.ToList())
{
_arrows.Remove(slideArrow);
slideArrow.ExitSequence();
}
}
if (_interactionReceivedState >= 3)
{
Cleared = true;
return;
}
for (var groupIndex = _interactionReceivedState; groupIndex < 3; groupIndex++)
{
foreach (var interactable in
_interactableGroups[groupIndex]
.Where(interactable => interactable.GetSlideJudgement())
.ToList())
_interactableGroups[groupIndex].Remove(interactable);
if (_interactableGroups[groupIndex].Count > 0 ||
_interactionReceivedState < groupIndex)
continue;
foreach (var slideArrow in _arrows
.Where(a => a.interactionGroupIndex == _interactionReceivedState)
.ToList())
{
_arrows.Remove(slideArrow);
slideArrow.ExitSequence();
}
_interactionReceivedState++;
}
}
public override float GetRemainingLength()
{
return (3 - _interactionReceivedState) / 3f * GetLength();
}
public override void OnDestroy()
{
base.OnDestroy();
foreach (var slideArrow in _arrows)
SlideManager.fanSlideArrows.Release(slideArrow);
foreach (var star in _stars)
SlideManager.slideStars.Release(star);
}
private void UpdateSlideStarPosition(float segmentT)
{
if (disposed || _stars is null)
return;
for (var index = 0; index < _stars.Length; index++)
{
var star = _stars[index];
if (star == null)
continue;
var transform = star.transform;
if (segmentT >= 1 && indexInSlide != ParentPath.segments.Count - 1)
{
transform.localScale = Vector3.one * Persistent.Settings.Gameplay.noteScale;
star.Appearance.Color = new Color(1, 1, 1, 0);
continue;
}
var position = Vector3.Lerp(_startPosition,
_endPositions[index],
segmentT);
transform.localPosition = position;
transform.up = _ups[index];
if (TimeSinceSlideStart < 0 &&
indexInSlide == 0 &&
ParentNote.slideMorph != SlideMorph.SuddenIn)
{
var interpolation =
Mathf.InverseLerp(-ParentPath.delay, 0,
(float)TimeSinceSlideStart);
transform.localScale = new Vector3(interpolation, interpolation) *
Persistent.Settings.Gameplay.noteScale;
star.Appearance.Color = new Color(1, 1, 1, interpolation);
return;
}
if (segmentT < 0)
return;
transform.localScale = Vector3.one * Persistent.Settings.Gameplay.noteScale;
star.Appearance.Color = Color.white;
}
}
public override void GetJudgementVector(out Vector2 position, out float rotation)
{
position = _endPositions[1];
rotation = Mathf.Atan2(_ups[1].y, _ups[1].x);
}
private void GenerateSensors()
{
_interactableGroups.AddRange(new List<TouchInteractable>[]
{
new()
{
TouchManager.GetCollider(new Location
{
group = NoteGroup.ASensor,
index = ParentNote.location.index
})
},
new()
{
TouchManager.GetCollider(new Location
{
group = NoteGroup.BSensor,
index = (ParentNote.location.index + 2) %
8
}),
TouchManager.GetCollider(new Location
{
group = NoteGroup.BSensor,
index = (ParentNote.location.index + 6) %
8
})
},
new()
{
TouchManager.GetCollider(new Location
{
group = NoteGroup.ASensor,
index = (ParentNote.location.index + 5) %
8
}),
TouchManager.GetCollider(new Location
{
group = NoteGroup.ASensor,
index = (ParentNote.location.index + 3) %
8
}),
TouchManager.GetCollider(new Location
{
group = NoteGroup.ASensor,
index = (ParentNote.location.index + 4) %
8
})
}
}
);
}
private void GenerateArrows()
{
var halfMargin = Mathf.RoundToInt(Margin / 2f);
for (var i = 0;
i < ArrowCount;
i++)
{
var position = Vector3.Lerp(_startPosition,
_endPositions[1],
(float)(i + halfMargin) / (ArrowCount + Margin));
var arrow = SlideManager.fanSlideArrows.Get();
arrow.Initialize(position,
_ups[1],
ArrowCount,
i,
isBreak,
isEach);
AssignArrows(arrow, i);
}
}
private void AssignArrows(FanSlideArrow arrow, int index)
{
_arrows.Add(arrow);
if (index < ArrowCount / 3f)
arrow.interactionGroupIndex = 0;
else if (index < ArrowCount * 2 / 3f)
arrow.interactionGroupIndex = 1;
else
arrow.interactionGroupIndex = 2;
}
}
}

View File

@@ -0,0 +1,248 @@
using System.Collections.Generic;
using System.Linq;
using AstroDX.Contexts.Gameplay.Behaviours.Slide.SlideMarkers;
using AstroDX.Contexts.Gameplay.Interactions;
using AstroDX.Contexts.Gameplay.PlayerScope;
using AstroDX.Contexts.Gameplay.SlideGenerators;
using AstroDX.Globals;
using Medicine;
using SimaiSharp.Structures;
using UnityEngine;
namespace AstroDX.Contexts.Gameplay.Behaviours.Slide.Handlers
{
public class RegularSlideSegmentHandler : SlideSegmentHandler
{
private readonly SlideGenerator _generator;
private readonly
List<(TouchInteractable relevantSensor,
List<RegularSlideArrow> relevantArrows)> _interactionPath = new();
/// <summary>
/// Used in auto judgement
/// </summary>
private int _initialSensorCount;
/// <summary>
/// Used in auto judgement
/// </summary>
private int _lastCheckedClearIndex;
private readonly SlideStarBehaviour _slideStar;
public RegularSlideSegmentHandler(SlideBehaviour slideBehaviour,
SlideSegment segment,
Location startLocation) :
base(slideBehaviour)
{
var vertices = segment.vertices.ToList();
vertices.Insert(0, startLocation);
_generator = SlideManager.GetGenerator(segment.slideType, vertices);
_slideStar = SlideManager.slideStars.Get();
_slideStar.Initialize(ParentPath, ParentNote.parentCollection);
GenerateArrows();
}
[Inject.Single]
private SlideManager SlideManager { get; }
public override float GetLength()
{
return _generator.GetLength();
}
public override void OnUpdate(float segmentT)
{
if (IsJudgementTarget)
CheckInteraction(segmentT);
UpdateSlideStarPosition(segmentT);
UpdateArrowOpacity();
}
private float _arrowOpacity;
private void UpdateArrowOpacity()
{
var timeSinceNoteStart = TimeSinceSlideStart + ParentPath.delay;
if (timeSinceNoteStart < 0)
{
_arrowOpacity = Mathf.InverseLerp(-Persistent.Settings.Gameplay.slideFadeInDuration,
0,
(float)timeSinceNoteStart);
_arrowOpacity = Mathf.Lerp(0, 0.8f, _arrowOpacity);
}
else
{
if (_arrowOpacity >= 1)
return;
_arrowOpacity = 1;
}
foreach (var (_, relevantArrows) in _interactionPath)
{
foreach (var arrow in relevantArrows)
{
arrow.SetAlpha(_arrowOpacity);
}
}
}
private void CheckInteraction(float segmentT)
{
var clearCount = GetClearCount(segmentT);
for (; clearCount > 0 && _interactionPath.Count > 0; clearCount--)
{
foreach (var slideArrow in _interactionPath[0].relevantArrows)
slideArrow.ExitSequence();
_interactionPath.RemoveAt(0);
}
Cleared = _interactionPath.Count == 0;
}
public override float GetRemainingLength()
{
return (float)_interactionPath.Count / _initialSensorCount * GetLength();
}
public override void OnDestroy()
{
base.OnDestroy();
foreach (var (_, arrows) in _interactionPath)
foreach (var slideArrow in arrows)
SlideManager.regularSlideArrows.Release(slideArrow);
SlideManager.slideStars.Release(_slideStar);
}
public override void GetJudgementVector(out Vector2 position, out float rotation)
{
_generator.GetPoint(1, out position, out rotation);
}
private void UpdateSlideStarPosition(float segmentT)
{
if (disposed)
return;
var starTransform = _slideStar.transform;
if (segmentT >= 1 && IsLastSegment)
{
starTransform.localScale = Vector3.one * Persistent.Settings.Gameplay.noteScale;
_slideStar.Appearance.Color = new Color(1, 1, 1, 0);
return;
}
_generator.GetPoint(Mathf.Clamp01(segmentT), out var position, out var rotation);
starTransform.SetLocalPositionAndRotation(position, Quaternion.Euler(0, 0, rotation * Mathf.Rad2Deg - 90));
if (TimeSinceSlideStart < 0 &&
indexInSlide == 0 &&
ParentNote.slideMorph != SlideMorph.SuddenIn)
{
var interpolation =
Mathf.InverseLerp(-ParentPath.delay, 0,
(float)TimeSinceSlideStart);
starTransform.localScale = new Vector3(interpolation, interpolation) *
Persistent.Settings.Gameplay.noteScale;
_slideStar.Appearance.Color = new Color(1, 1, 1, interpolation);
}
if (segmentT < 0)
return;
starTransform.localScale = Vector3.one * Persistent.Settings.Gameplay.noteScale;
_slideStar.Appearance.Color = Color.white;
}
private void GenerateArrows()
{
var relevantArrows = new List<RegularSlideArrow>();
TouchInteractable relevantSensor = null;
var totalLength = GetLength();
var arrowCount = Mathf.FloorToInt(totalLength / SlideManager.ArrowDistance);
for (var (distance, index) = (SlideManager.ArrowDistance, 0);
distance <= totalLength;
distance += SlideManager.ArrowDistance)
{
_generator.GetPoint(distance / totalLength, out var position, out var rotation);
var slideArrow = SlideManager.regularSlideArrows.Get();
slideArrow.Initialize(position, rotation,
arrowCount, index, isBreak, isEach);
var sensor = slideArrow.GetClosestSensor();
if (relevantSensor != sensor)
{
if (relevantSensor != null)
_interactionPath.Add((relevantSensor, relevantArrows));
relevantArrows = new List<RegularSlideArrow>();
relevantSensor = sensor;
}
relevantArrows.Add(slideArrow);
index++;
}
if (relevantArrows.Count > 0) _interactionPath.Add((relevantSensor, relevantArrows));
_initialSensorCount = _interactionPath.Count;
}
private int GetClearCount(double segmentT)
{
if (Persistent.Settings.Mods.Auto && segmentT >= 0)
{
var clearCount = Mathf.FloorToInt((float)(segmentT * _initialSensorCount));
if (_lastCheckedClearIndex >= clearCount)
return 0;
var lastCheckedClearIndex = _lastCheckedClearIndex;
_lastCheckedClearIndex = clearCount;
return clearCount - lastCheckedClearIndex;
}
else
{
var clearCount = 0;
var scanIndex = 0;
var tolerance = Persistent.Settings.Judgement.slideTolerance;
while (tolerance >= 0 && scanIndex < _interactionPath.Count)
{
if (_interactionPath[scanIndex].relevantSensor.GetSlideJudgement())
{
tolerance = Persistent.Settings.Judgement.slideTolerance;
clearCount = scanIndex + 1;
}
else
{
tolerance--;
}
scanIndex++;
}
return clearCount;
}
}
}
}

View File

@@ -0,0 +1,70 @@
using System.Linq;
using SimaiSharp.Structures;
using UnityEngine;
namespace AstroDX.Contexts.Gameplay.Behaviours.Slide.Handlers
{
public abstract class SlideSegmentHandler
{
protected readonly bool isBreak;
protected readonly bool isEach;
protected bool disposed;
protected int indexInSlide = -1;
protected SlideSegmentHandler(SlideBehaviour slideBehaviour)
{
disposed = false;
ParentSlide = slideBehaviour;
isEach = ParentSlide.ParentNote.parentCollection
.Sum(n => n.slidePaths.Count) > 1;
isBreak = ParentSlide.Path.type == NoteType.Break;
}
public bool Cleared { get; protected set; }
protected SlideBehaviour ParentSlide { get; }
protected SlidePath ParentPath => ParentSlide.Path;
protected Note ParentNote => ParentSlide.ParentNote;
protected double TimeSinceSlideStart => ParentSlide.TimeSinceStart;
protected bool IsLastSegment => indexInSlide != ParentPath.segments.Count - 1;
public bool IsJudgementTarget { get; set; }
public void SetIndex(int index)
{
indexInSlide = index;
}
/// <summary>
/// Describes the position and the up vector for the judgement text.
/// </summary>
public abstract void GetJudgementVector(out Vector2 position, out float rotation);
public abstract void OnUpdate(float segmentT);
public virtual void OnDestroy()
{
disposed = true;
}
public abstract float GetLength();
public abstract float GetRemainingLength();
public static SlideSegmentHandler Recommend(SlideBehaviour slideBehaviour,
SlideSegment segment,
Location startLocation)
{
return segment.slideType switch
{
SlideType.Fan => new FanSlideSegmentHandler(slideBehaviour, segment, startLocation),
_ => new RegularSlideSegmentHandler(slideBehaviour, segment, startLocation)
};
}
}
}

View File

@@ -0,0 +1,190 @@
using System;
using System.Collections.Generic;
using AstroDX.Contexts.Gameplay.Behaviours.Slide.Handlers;
using AstroDX.Contexts.Gameplay.PlayerScope;
using AstroDX.Contexts.Gameplay.SceneScope;
using AstroDX.Globals;
using Medicine;
using SimaiSharp.Structures;
using UnityEngine;
using TimeSpan = AstroDX.Models.Scoring.TimeSpan;
namespace AstroDX.Contexts.Gameplay.Behaviours.Slide
{
public sealed class SlideBehaviour : MonoBehaviour, IDisposable
{
private readonly List<(float distance, SlideSegmentHandler handler)> _segments = new();
private float _endRotation;
private Vector2 _endPosition;
private SlideType _endSegmentType;
private bool _judged;
private int _judgedSegmentCount;
private float _length;
private float _slideStartTime;
[Inject.Single]
private StatisticsManager StatisticsManager { get; }
[Inject.Single]
private MusicManager MusicManager { get; }
[Inject.Single]
private SlideManager SlideManager { get; }
public SlidePath Path { get; private set; }
public Note ParentNote { get; private set; }
public double TimeSinceStart => MusicManager.Time - _slideStartTime;
private void Update()
{
UpdateSegments();
if (TimeSinceStart >= 0)
CheckInteraction();
if (Persistent.Settings.Mods.Auto &&
TimeSinceStart > Path.duration)
Judge();
else if (TimeSinceStart > Path.duration +
TimeSpan.SlideFinish.TimingWindows[^1].lateSpan)
Judge();
}
public void Init(in SlidePath path, in Note parent)
{
_judgedSegmentCount = 0;
_judged = false;
Path = path;
ParentNote = parent;
_slideStartTime = ParentNote.parentCollection.time + Path.delay;
GenerateHandlers(path);
}
private void UpdateSegments()
{
var t = (float)TimeSinceStart / Path.duration * _length;
var isJudgementTarget = TimeSinceStart + Path.delay >= 0;
for (var i = 0; i < _segments.Count; i++)
{
var (segmentStartT, handler) = _segments[i];
var segmentEndT = i + 1 < _segments.Count ? _segments[i + 1].distance : _length;
var segmentT = (t - segmentStartT) / (segmentEndT - segmentStartT);
handler.IsJudgementTarget = isJudgementTarget;
handler.OnUpdate(segmentT);
if (!handler.Cleared)
isJudgementTarget = false;
}
}
private void CheckInteraction()
{
while (_judgedSegmentCount < _segments.Count)
{
var currentHandler = _segments[_judgedSegmentCount].handler;
var segmentCleared = currentHandler.Cleared;
if (!segmentCleared)
return;
_judgedSegmentCount++;
}
Judge();
}
private void GenerateHandlers(in SlidePath slidePath)
{
if (_segments.Count > 0)
_segments.Clear();
var totalDistance = 0f;
var handlers = new List<SlideSegmentHandler>();
for (var i = 0; i < slidePath.segments.Count; i++)
{
var segment = slidePath.segments[i];
var startLocation = i > 0 ? slidePath.segments[i - 1].vertices[^1] : ParentNote.location;
var handler = SlideSegmentHandler.Recommend(this, segment, startLocation);
handlers.Add(handler);
}
var index = 0;
foreach (var segmentHandler in handlers)
{
segmentHandler.SetIndex(index);
_segments.Add((totalDistance, segmentHandler));
totalDistance += segmentHandler.GetLength();
index++;
}
_segments[^1].handler.GetJudgementVector(out _endPosition, out _endRotation);
_endSegmentType = Path.segments[^1].slideType;
_length = totalDistance;
}
private void Judge()
{
if (_judged)
return;
_judged = true;
var incomplete = _judgedSegmentCount < _segments.Count;
var startTime = ParentNote.parentCollection.time +
Path.delay;
var timeFromEnd = MusicManager.Time - (startTime + Path.duration);
var distanceToEnd = incomplete
? _segments[_judgedSegmentCount].handler.GetRemainingLength()
: _length - Mathf.InverseLerp(startTime,
startTime + Path.duration,
(float)MusicManager.Time) * _length;
if (incomplete)
{
var multipleSegmentsRemaining = _judgedSegmentCount + 1 < _segments.Count;
if (multipleSegmentsRemaining)
distanceToEnd += _length - _segments[_judgedSegmentCount + 1].distance;
}
StatisticsManager.TallySlide(Path,
distanceToEnd,
timeFromEnd,
_endPosition,
_endRotation,
_endSegmentType,
incomplete);
Dispose();
}
public void Dispose()
{
foreach (var segment in _segments)
segment.handler.OnDestroy();
SlideManager.slides.Release(this);
}
}
}

View File

@@ -0,0 +1,98 @@
using System.Collections.Generic;
using AstroDX.Contexts.Gameplay.PlayerScope;
using AstroDX.Utilities;
using SimaiSharp.Structures;
using UnityEngine;
namespace AstroDX.Contexts.Gameplay.SlideGenerators
{
public sealed class CurveCcwGenerator : SlideGenerator
{
private const float CurveRadius = RenderManager.CenterRadius;
private const float RingRadius = RenderManager.PlayFieldRadius;
private readonly float _curveLength;
private readonly float _endForward;
private readonly Vector2 _endPoint;
private readonly float _startForward;
private readonly float _startLength;
private readonly Vector2 _startPoint;
private readonly Vector2 _tangentInPoint;
private readonly float _tangentInRotation;
private readonly Vector2 _tangentOutPoint;
private readonly float _totalLength;
public CurveCcwGenerator(IReadOnlyList<Location> vertices)
{
var startRotation = GetRotation(vertices[0]);
var endRotation = GetRotation(vertices[1]);
_tangentInRotation = startRotation +
Trigonometry.GetTangentAngleDelta(CurveRadius, RingRadius, false);
var tangentOutRotation = endRotation +
Trigonometry.GetTangentAngleDelta(CurveRadius, RingRadius, true);
_startPoint = GetPositionRadial(startRotation);
_tangentInPoint = GetPositionRadial(_tangentInRotation, CurveRadius);
_tangentOutPoint = GetPositionRadial(tangentOutRotation, CurveRadius);
_endPoint = GetPositionRadial(endRotation);
var startSegment = _tangentInPoint - _startPoint;
_startLength = startSegment.magnitude;
_startForward = Mathf.Atan2(startSegment.y, startSegment.x);
_curveLength = Trigonometry.GetAngleSpan(_tangentInRotation, tangentOutRotation,
false) * CurveRadius;
var endSegment = _endPoint - _tangentOutPoint;
var endLength = endSegment.magnitude;
_endForward = Mathf.Atan2(endSegment.y, endSegment.x);
_totalLength = _startLength + _curveLength + endLength;
}
public override float GetLength()
{
return _totalLength;
}
public override void GetPoint(float t, out Vector2 position, out float rotation)
{
var distanceFromStart = t * _totalLength;
if (distanceFromStart < _startLength)
{
position = Vector2.Lerp(_startPoint,
_tangentInPoint,
Mathf.InverseLerp(0,
_startLength,
distanceFromStart));
rotation = _startForward;
}
else if (distanceFromStart < _startLength + _curveLength)
{
var localT = Mathf.InverseLerp(_startLength, _startLength + _curveLength, distanceFromStart);
position = new Vector2(Mathf.Cos(_tangentInRotation + _curveLength / CurveRadius * localT) *
CurveRadius,
Mathf.Sin(_tangentInRotation + _curveLength / CurveRadius * localT) *
CurveRadius);
var forward = position.Rotate(Trigonometry.Tau / 4);
rotation = Mathf.Atan2(forward.y, forward.x);
}
else
{
position = Vector2.Lerp(_tangentOutPoint,
_endPoint,
Mathf.InverseLerp(_startLength + _curveLength,
_totalLength,
distanceFromStart));
rotation = _endForward;
}
}
}
}

View File

@@ -0,0 +1,98 @@
using System.Collections.Generic;
using AstroDX.Contexts.Gameplay.PlayerScope;
using AstroDX.Utilities;
using SimaiSharp.Structures;
using UnityEngine;
namespace AstroDX.Contexts.Gameplay.SlideGenerators
{
public sealed class CurveCwGenerator : SlideGenerator
{
private const float CurveRadius = RenderManager.CenterRadius;
private const float RingRadius = RenderManager.PlayFieldRadius;
private readonly float _curveLength;
private readonly float _endForward;
private readonly Vector2 _endPoint;
private readonly float _startForward;
private readonly float _startLength;
private readonly Vector2 _startPoint;
private readonly Vector2 _tangentInPoint;
private readonly float _tangentInRotation;
private readonly Vector2 _tangentOutPoint;
private readonly float _totalLength;
public CurveCwGenerator(IReadOnlyList<Location> vertices)
{
var startRotation = GetRotation(vertices[0]);
var endRotation = GetRotation(vertices[1]);
_tangentInRotation = startRotation +
Trigonometry.GetTangentAngleDelta(CurveRadius, RingRadius, true);
var tangentOutRotation = endRotation +
Trigonometry.GetTangentAngleDelta(CurveRadius, RingRadius, false);
_startPoint = GetPositionRadial(startRotation);
_tangentInPoint = GetPositionRadial(_tangentInRotation, CurveRadius);
_tangentOutPoint = GetPositionRadial(tangentOutRotation, CurveRadius);
_endPoint = GetPositionRadial(endRotation);
var startSegment = _tangentInPoint - _startPoint;
_startLength = startSegment.magnitude;
_startForward = Mathf.Atan2(startSegment.y, startSegment.x);
_curveLength = Trigonometry.GetAngleSpan(_tangentInRotation, tangentOutRotation,
true) * CurveRadius;
var endSegment = _endPoint - _tangentOutPoint;
var endLength = endSegment.magnitude;
_endForward = Mathf.Atan2(endSegment.y, endSegment.x);
_totalLength = _startLength + _curveLength + endLength;
}
public override float GetLength()
{
return _totalLength;
}
public override void GetPoint(float t, out Vector2 position, out float rotation)
{
var distanceFromStart = t * _totalLength;
if (distanceFromStart < _startLength)
{
position = Vector2.Lerp(_startPoint,
_tangentInPoint,
Mathf.InverseLerp(0,
_startLength,
distanceFromStart));
rotation = _startForward;
}
else if (distanceFromStart < _startLength + _curveLength)
{
var localT = Mathf.InverseLerp(_startLength, _startLength + _curveLength, distanceFromStart);
position = new Vector2(Mathf.Cos(_tangentInRotation - _curveLength / CurveRadius * localT) *
CurveRadius,
Mathf.Sin(_tangentInRotation - _curveLength / CurveRadius * localT) *
CurveRadius);
var forward = position.Rotate(-Trigonometry.Tau / 4);
rotation = Mathf.Atan2(forward.y, forward.x);
}
else
{
position = Vector2.Lerp(_tangentOutPoint,
_endPoint,
Mathf.InverseLerp(_startLength + _curveLength,
_totalLength,
distanceFromStart));
rotation = _endForward;
}
}
}
}

View File

@@ -0,0 +1,116 @@
using System.Collections.Generic;
using AstroDX.Contexts.Gameplay.PlayerScope;
using AstroDX.Utilities;
using SimaiSharp.Structures;
using UnityEngine;
namespace AstroDX.Contexts.Gameplay.SlideGenerators
{
public sealed class EdgeCurveCcwGenerator : SlideGenerator
{
private const float CurveRadius = RenderManager.CenterRadius * 1.2f;
private const float CenterAngularOffset = Trigonometry.Tau / 4 - Trigonometry.Tau / 16;
private const float CenterRadialOffset = RenderManager.PlayFieldRadius * 0.4662f;
private readonly Vector2 _centerPosition;
private readonly float _curveLength;
private readonly Vector2 _endPoint;
private readonly float _startRotation;
private readonly float _endRotation;
private readonly float _startLength;
private readonly Vector2 _startPoint;
private readonly Vector2 _tangentInPoint;
private readonly float _tangentInRotation;
private readonly Vector2 _tangentOutPoint;
private readonly float _totalLength;
public EdgeCurveCcwGenerator(IReadOnlyList<Location> vertices)
{
var startRotation = GetRotation(vertices[0]);
var endRotation = GetRotation(vertices[1]);
var centerAngle = startRotation - CenterAngularOffset;
_centerPosition = new Vector2(CenterRadialOffset * Mathf.Cos(centerAngle),
CenterRadialOffset * Mathf.Sin(centerAngle));
_startPoint = GetPositionRadial(startRotation);
var relativeStartRotation = Trigonometry.ToPolarAngle(_startPoint, _centerPosition);
var magnitude = (_centerPosition - _startPoint).magnitude;
var startDelta = Trigonometry.GetTangentAngleDelta(CurveRadius, magnitude, false);
_tangentInRotation = relativeStartRotation + startDelta;
_tangentInPoint = GetPositionRadial(_tangentInRotation, CurveRadius) +
_centerPosition;
_endPoint = GetPositionRadial(endRotation);
var relativeEndRotation = Trigonometry.ToPolarAngle(_endPoint, _centerPosition);
var endMagnitude = (_endPoint - _centerPosition).magnitude;
var endDelta = Trigonometry.GetTangentAngleDelta(CurveRadius, endMagnitude, true);
var tangentOutRotation = relativeEndRotation + endDelta;
_tangentOutPoint = GetPositionRadial(tangentOutRotation, CurveRadius) +
_centerPosition;
var startSegment = _tangentInPoint - _startPoint;
_startLength = startSegment.magnitude;
_startRotation = Mathf.Atan2(startSegment.y, startSegment.x);
_curveLength = Trigonometry.GetAngleSpan(_tangentInRotation, tangentOutRotation,
false, Trigonometry.Tau / 4f) * CurveRadius;
var endSegment = _endPoint - _tangentOutPoint;
var endLength = endSegment.magnitude;
_endRotation = Mathf.Atan2(endSegment.y, endSegment.x);
_totalLength = _startLength + _curveLength + endLength;
}
public override float GetLength()
{
return _totalLength;
}
public override void GetPoint(float t, out Vector2 position, out float rotation)
{
var distanceFromStart = t * _totalLength;
if (distanceFromStart < _startLength)
{
position = Vector2.Lerp(_startPoint,
_tangentInPoint,
Mathf.InverseLerp(0,
_startLength,
distanceFromStart));
rotation = _startRotation;
}
else if (distanceFromStart < _startLength + _curveLength)
{
var localT = Mathf.InverseLerp(_startLength, _startLength + _curveLength, distanceFromStart);
position = new Vector2(Mathf.Cos(_tangentInRotation + _curveLength / CurveRadius * localT) *
CurveRadius,
Mathf.Sin(_tangentInRotation + _curveLength / CurveRadius * localT) *
CurveRadius);
var forward = position.Rotate(Trigonometry.Tau / 4);
rotation = Mathf.Atan2(forward.y, forward.x);
position += _centerPosition;
}
else
{
position = Vector2.Lerp(_tangentOutPoint,
_endPoint,
Mathf.InverseLerp(_startLength + _curveLength,
_totalLength,
distanceFromStart));
rotation = _endRotation;
}
}
}
}

View File

@@ -0,0 +1,116 @@
using System.Collections.Generic;
using AstroDX.Contexts.Gameplay.PlayerScope;
using AstroDX.Utilities;
using SimaiSharp.Structures;
using UnityEngine;
namespace AstroDX.Contexts.Gameplay.SlideGenerators
{
public sealed class EdgeCurveCwGenerator : SlideGenerator
{
private const float CurveRadius = RenderManager.CenterRadius * 1.2f;
private const float CenterAngularOffset = Trigonometry.Tau / 4 - Trigonometry.Tau / 16;
private const float CenterRadialOffset = RenderManager.PlayFieldRadius * 0.4662f;
private readonly Vector2 _centerPosition;
private readonly float _curveLength;
private readonly Vector2 _endPoint;
private readonly float _startRotation;
private readonly float _endRotation;
private readonly float _startLength;
private readonly Vector2 _startPoint;
private readonly Vector2 _tangentInPoint;
private readonly float _tangentInRotation;
private readonly Vector2 _tangentOutPoint;
private readonly float _totalLength;
public EdgeCurveCwGenerator(IReadOnlyList<Location> vertices)
{
var startRotation = GetRotation(vertices[0]);
var endRotation = GetRotation(vertices[1]);
var centerAngle = startRotation + CenterAngularOffset;
_centerPosition = new Vector2(CenterRadialOffset * Mathf.Cos(centerAngle),
CenterRadialOffset * Mathf.Sin(centerAngle));
_startPoint = GetPositionRadial(startRotation);
var relativeStartRotation = Trigonometry.ToPolarAngle(_startPoint, _centerPosition);
var magnitude = (_centerPosition - _startPoint).magnitude;
var startDelta = Trigonometry.GetTangentAngleDelta(CurveRadius, magnitude, true);
_tangentInRotation = relativeStartRotation + startDelta;
_tangentInPoint = GetPositionRadial(_tangentInRotation, CurveRadius) +
_centerPosition;
_endPoint = GetPositionRadial(endRotation);
var relativeEndRotation = Trigonometry.ToPolarAngle(_endPoint, _centerPosition);
var endMagnitude = (_endPoint - _centerPosition).magnitude;
var endDelta = Trigonometry.GetTangentAngleDelta(CurveRadius, endMagnitude, false);
var tangentOutRotation = relativeEndRotation + endDelta;
_tangentOutPoint = GetPositionRadial(tangentOutRotation, CurveRadius) +
_centerPosition;
var startSegment = _tangentInPoint - _startPoint;
_startLength = startSegment.magnitude;
_startRotation = Mathf.Atan2(startSegment.y, startSegment.x);
_curveLength = Trigonometry.GetAngleSpan(_tangentInRotation, tangentOutRotation,
true, Trigonometry.Tau / 4f) * CurveRadius;
var endSegment = _endPoint - _tangentOutPoint;
var endLength = endSegment.magnitude;
_endRotation = Mathf.Atan2(endSegment.y, endSegment.x);
_totalLength = _startLength + _curveLength + endLength;
}
public override float GetLength()
{
return _totalLength;
}
public override void GetPoint(float t, out Vector2 position, out float rotation)
{
var distanceFromStart = t * _totalLength;
if (distanceFromStart < _startLength)
{
position = Vector2.Lerp(_startPoint,
_tangentInPoint,
Mathf.InverseLerp(0,
_startLength,
distanceFromStart));
rotation = _startRotation;
}
else if (distanceFromStart < _startLength + _curveLength)
{
var localT = Mathf.InverseLerp(_startLength, _startLength + _curveLength, distanceFromStart);
position = new Vector2(Mathf.Cos(_tangentInRotation - _curveLength / CurveRadius * localT) *
CurveRadius,
Mathf.Sin(_tangentInRotation - _curveLength / CurveRadius * localT) *
CurveRadius);
var forward = position.Rotate(-Trigonometry.Tau / 4);
rotation = Mathf.Atan2(forward.y, forward.x);
position += _centerPosition;
}
else
{
position = Vector2.Lerp(_tangentOutPoint,
_endPoint,
Mathf.InverseLerp(_startLength + _curveLength,
_totalLength,
distanceFromStart));
rotation = _endRotation;
}
}
}
}

View File

@@ -0,0 +1,63 @@
using System.Collections.Generic;
using SimaiSharp.Structures;
using UnityEngine;
namespace AstroDX.Contexts.Gameplay.SlideGenerators
{
public sealed class EdgeFoldGenerator : SlideGenerator
{
private readonly Vector2 _endPoint;
private readonly Vector2 _midPoint;
private readonly Vector2 _startPoint;
private readonly float _startRotation;
private readonly float _endRotation;
private readonly float _startSegmentLength;
private readonly float _totalLength;
public EdgeFoldGenerator(IReadOnlyList<Location> vertices)
{
_startPoint = GetPosition(vertices[0]);
_midPoint = GetPosition(vertices[1]);
_endPoint = GetPosition(vertices[2]);
var startSegment = _midPoint - _startPoint;
_startSegmentLength = startSegment.magnitude;
_startRotation = Mathf.Atan2(startSegment.y, startSegment.x);
var endSegment = _endPoint - _midPoint;
var endSegmentSpan = endSegment.magnitude;
_endRotation = Mathf.Atan2(endSegment.y, endSegment.x);
_totalLength = _startSegmentLength + endSegmentSpan;
}
public override float GetLength()
{
return _totalLength;
}
public override void GetPoint(float t, out Vector2 position, out float rotation)
{
var distanceFromStart = t * _totalLength;
if (distanceFromStart < _startSegmentLength)
{
position = Vector2.Lerp(_startPoint, _midPoint,
Mathf.InverseLerp(0,
_startSegmentLength / _totalLength,
t));
rotation = _startRotation;
}
else
{
position = Vector2.Lerp(_midPoint, _endPoint,
Mathf.InverseLerp(_startSegmentLength / _totalLength,
1,
t));
rotation = _endRotation;
}
}
}
}

View File

@@ -0,0 +1,63 @@
using System.Collections.Generic;
using SimaiSharp.Structures;
using UnityEngine;
namespace AstroDX.Contexts.Gameplay.SlideGenerators
{
public sealed class FoldGenerator : SlideGenerator
{
private readonly Vector2 _endPoint;
private readonly float _endRotation;
private readonly Vector2 _midPoint;
private readonly Vector2 _startPoint;
private readonly float _startRotation;
private readonly float _startSegmentLength;
private readonly float _totalLength;
public FoldGenerator(IReadOnlyList<Location> vertices)
{
_startPoint = GetPosition(vertices[0]);
_endPoint = GetPosition(vertices[1]);
_midPoint = Vector2.zero;
var startSegment = _midPoint - _startPoint;
_startSegmentLength = startSegment.magnitude;
_startRotation = Mathf.Atan2(startSegment.y, startSegment.x);
var endSegment = _endPoint - _midPoint;
var endSegmentSpan = endSegment.magnitude;
_endRotation = Mathf.Atan2(endSegment.y, endSegment.x);
_totalLength = _startSegmentLength + endSegmentSpan;
}
public override float GetLength()
{
return _totalLength;
}
public override void GetPoint(float t, out Vector2 position, out float rotation)
{
var distanceFromStart = t * _totalLength;
if (distanceFromStart < _startSegmentLength)
{
position = Vector2.Lerp(_startPoint, _midPoint,
Mathf.InverseLerp(0,
_startSegmentLength / _totalLength,
t));
rotation = _startRotation;
}
else
{
position = Vector2.Lerp(_midPoint, _endPoint,
Mathf.InverseLerp(_startSegmentLength / _totalLength,
1,
t));
rotation = _endRotation;
}
}
}
}

View File

@@ -0,0 +1,52 @@
using System.Collections.Generic;
using AstroDX.Utilities;
using SimaiSharp.Structures;
using UnityEngine;
namespace AstroDX.Contexts.Gameplay.SlideGenerators
{
public sealed class RingCcwGenerator : SlideGenerator
{
private readonly float _angleSpan;
private readonly float _endRadius;
private readonly float _length;
private readonly float _startRadius;
private readonly float _startRotation;
public RingCcwGenerator(IReadOnlyList<Location> vertices)
{
var inPosition = GetPosition(vertices[0]);
var inRadians = Trigonometry.ToPolarAngle(inPosition);
var outPosition = GetPosition(vertices[1]);
var outRadians = Trigonometry.ToPolarAngle(outPosition);
_startRotation = inRadians;
_angleSpan = Trigonometry.GetAngleSpan(inRadians, outRadians, false);
_startRadius = GetRadiusFromCenter(vertices[0]);
_endRadius = GetRadiusFromCenter(vertices[1]);
_length = _angleSpan * (_startRadius + _endRadius) / 2;
}
public override float GetLength()
{
return _length;
}
public override void GetPoint(float t,
out Vector2 position,
out float rotation)
{
var radiusAtT = Mathf.Lerp(_startRadius, _endRadius, t);
var rotationAtT = _startRotation + _angleSpan * t;
position = new Vector2(Mathf.Cos(rotationAtT) * radiusAtT,
Mathf.Sin(rotationAtT) * radiusAtT);
rotation = rotationAtT + Trigonometry.Tau / 4;
}
}
}

View File

@@ -0,0 +1,52 @@
using System.Collections.Generic;
using AstroDX.Utilities;
using SimaiSharp.Structures;
using UnityEngine;
namespace AstroDX.Contexts.Gameplay.SlideGenerators
{
public sealed class RingCwGenerator : SlideGenerator
{
private readonly float _angleSpan;
private readonly float _endRadius;
private readonly float _length;
private readonly float _startRadius;
private readonly float _startRotation;
public RingCwGenerator(IReadOnlyList<Location> vertices)
{
var inPosition = GetPosition(vertices[0]);
var inRadians = Trigonometry.ToPolarAngle(inPosition);
var outPosition = GetPosition(vertices[1]);
var outRadians = Trigonometry.ToPolarAngle(outPosition);
_startRotation = inRadians;
_angleSpan = Trigonometry.GetAngleSpan(_startRotation, outRadians, true);
_startRadius = GetRadiusFromCenter(vertices[0]);
_endRadius = GetRadiusFromCenter(vertices[1]);
_length = _angleSpan * (_startRadius + _endRadius) / 2;
}
public override float GetLength()
{
return _length;
}
public override void GetPoint(float t,
out Vector2 position,
out float rotation)
{
var radiusAtT = Mathf.Lerp(_startRadius, _endRadius, t);
var rotationAtT = _startRotation - _angleSpan * t;
position = new Vector2(Mathf.Cos(rotationAtT) * radiusAtT,
Mathf.Sin(rotationAtT) * radiusAtT);
rotation = rotationAtT - Trigonometry.Tau / 4;
}
}
}

View File

@@ -0,0 +1,54 @@
using System;
using AstroDX.Contexts.Gameplay.PlayerScope;
using AstroDX.Utilities;
using SimaiSharp.Structures;
using UnityEngine;
namespace AstroDX.Contexts.Gameplay.SlideGenerators
{
public abstract class SlideGenerator
{
public abstract float GetLength();
public abstract void GetPoint(float t, out Vector2 position, out float rotation);
protected static float GetRotation(in Location location)
{
const float initialAngle = Trigonometry.Tau / 4f - Trigonometry.Tau / 16f;
var angle = initialAngle - Trigonometry.Tau / 8f * location.index;
if (location.group is NoteGroup.DSensor or NoteGroup.ESensor) angle += Trigonometry.Tau / 16f;
return angle;
}
public static Vector2 GetPosition(in Location location)
{
var radius = GetRadiusFromCenter(location);
return GetPositionRadial(GetRotation(location), radius);
}
protected static Vector2 GetPositionRadial(in float rotationRadians,
in float radius = RenderManager.PlayFieldRadius)
{
return new Vector2(Mathf.Cos(rotationRadians) * radius,
Mathf.Sin(rotationRadians) * radius);
}
protected static float GetRadiusFromCenter(Location location)
{
return location.group switch
{
NoteGroup.Tap => RenderManager.PlayFieldRadius,
NoteGroup.ASensor => RenderManager.AreaARadius,
NoteGroup.BSensor => RenderManager.AreaBRadius,
NoteGroup.CSensor => 0,
NoteGroup.DSensor => RenderManager.AreaDRadius,
NoteGroup.ESensor => RenderManager.AreaERadius,
_ => throw new ArgumentOutOfRangeException()
};
}
}
}

View File

@@ -0,0 +1,35 @@
using System.Collections.Generic;
using SimaiSharp.Structures;
using UnityEngine;
namespace AstroDX.Contexts.Gameplay.SlideGenerators
{
public sealed class StraightGenerator : SlideGenerator
{
private readonly Vector2 _endPoint;
private readonly float _rotation;
private readonly float _length;
private readonly Vector2 _startPoint;
public StraightGenerator(IReadOnlyList<Location> vertices)
{
_startPoint = GetPosition(vertices[0]);
_endPoint = GetPosition(vertices[1]);
var segment = _endPoint - _startPoint;
_length = segment.magnitude;
_rotation = Mathf.Atan2(segment.y, segment.x);
}
public override float GetLength()
{
return _length;
}
public override void GetPoint(float t, out Vector2 position, out float rotation)
{
position = Vector2.Lerp(_startPoint, _endPoint, t);
rotation = _rotation;
}
}
}

View File

@@ -0,0 +1,95 @@
using System.Collections.Generic;
using AstroDX.Contexts.Gameplay.PlayerScope;
using AstroDX.Utilities;
using SimaiSharp.Structures;
using UnityEngine;
namespace AstroDX.Contexts.Gameplay.SlideGenerators
{
public sealed class ZigZagSGenerator : SlideGenerator
{
private readonly Vector2 _endPoint;
private readonly Vector2 _endZagPoint;
private readonly float _midSegmentLength;
private readonly Vector2 _startPoint;
private readonly float _startRotation;
private readonly float _midRotation;
private readonly float _endRotation;
private readonly float _startSegmentLength;
private readonly Vector2 _startZagPoint;
private readonly float _totalLength;
public ZigZagSGenerator(IReadOnlyList<Location> vertices)
{
const float distance = RenderManager.PlayFieldRadius;
const float inner = RenderManager.CenterRadius;
var startRotation = GetRotation(vertices[0]);
var endRotation = GetRotation(vertices[1]);
var startZag = startRotation + Trigonometry.Tau / 4f;
var endZag = endRotation + Trigonometry.Tau / 4f;
_startPoint = new Vector2(distance * Mathf.Cos(startRotation),
distance * Mathf.Sin(startRotation));
_startZagPoint = new Vector2(inner * Mathf.Cos(startZag),
inner * Mathf.Sin(startZag));
var startSegment = _startZagPoint - _startPoint;
_startSegmentLength = startSegment.magnitude;
_startRotation = Mathf.Atan2(startSegment.y, startSegment.x);
_endZagPoint = new Vector2(inner * Mathf.Cos(endZag),
inner * Mathf.Sin(endZag));
var midSegment = _endZagPoint - _startZagPoint;
_midSegmentLength = midSegment.magnitude;
_midRotation = Mathf.Atan2(midSegment.y, midSegment.x);
_endPoint = new Vector2(distance * Mathf.Cos(endRotation),
distance * Mathf.Sin(endRotation));
var endSegment = _endPoint - _endZagPoint;
var endSegmentLength = endSegment.magnitude;
_endRotation = Mathf.Atan2(endSegment.y, endSegment.x);
_totalLength = _startSegmentLength + _midSegmentLength + endSegmentLength;
}
public override float GetLength()
{
return _totalLength;
}
public override void GetPoint(float t, out Vector2 position, out float rotation)
{
var distanceFromStart = t * _totalLength;
if (distanceFromStart < _startSegmentLength)
{
position = Vector2.Lerp(_startPoint, _startZagPoint,
Mathf.InverseLerp(0, _startSegmentLength, distanceFromStart));
rotation = _startRotation;
}
else if (distanceFromStart < _startSegmentLength + _midSegmentLength)
{
var midLength = _startSegmentLength + _midSegmentLength;
position = Vector2.Lerp(_startZagPoint, _endZagPoint,
Mathf.InverseLerp(_startSegmentLength, midLength, distanceFromStart));
rotation = _midRotation;
}
else
{
var midLength = _startSegmentLength + _midSegmentLength;
position = Vector2.Lerp(_endZagPoint, _endPoint,
Mathf.InverseLerp(midLength, _totalLength, distanceFromStart));
rotation = _endRotation;
}
}
}
}

View File

@@ -0,0 +1,95 @@
using System.Collections.Generic;
using AstroDX.Contexts.Gameplay.PlayerScope;
using AstroDX.Utilities;
using SimaiSharp.Structures;
using UnityEngine;
namespace AstroDX.Contexts.Gameplay.SlideGenerators
{
public sealed class ZigZagZGenerator : SlideGenerator
{
private readonly Vector2 _endPoint;
private readonly Vector2 _endZagPoint;
private readonly float _midSegmentLength;
private readonly Vector2 _startPoint;
private readonly float _startRotation;
private readonly float _midRotation;
private readonly float _endRotation;
private readonly float _startSegmentLength;
private readonly Vector2 _startZagPoint;
private readonly float _totalLength;
public ZigZagZGenerator(IReadOnlyList<Location> vertices)
{
const float distance = RenderManager.PlayFieldRadius;
const float inner = RenderManager.CenterRadius;
var startRotation = GetRotation(vertices[0]);
var endRotation = GetRotation(vertices[1]);
var startZag = startRotation - Trigonometry.Tau / 4f;
var endZag = endRotation - Trigonometry.Tau / 4f;
_startPoint = new Vector2(distance * Mathf.Cos(startRotation),
distance * Mathf.Sin(startRotation));
_startZagPoint = new Vector2(inner * Mathf.Cos(startZag),
inner * Mathf.Sin(startZag));
var startSegment = _startZagPoint - _startPoint;
_startSegmentLength = startSegment.magnitude;
_startRotation = Mathf.Atan2(startSegment.y, startSegment.x);
_endZagPoint = new Vector2(inner * Mathf.Cos(endZag),
inner * Mathf.Sin(endZag));
var midSegment = _endZagPoint - _startZagPoint;
_midSegmentLength = midSegment.magnitude;
_midRotation = Mathf.Atan2(midSegment.y, midSegment.x);
_endPoint = new Vector2(distance * Mathf.Cos(endRotation),
distance * Mathf.Sin(endRotation));
var endSegment = _endPoint - _endZagPoint;
var endSegmentLength = endSegment.magnitude;
_endRotation = Mathf.Atan2(endSegment.y, endSegment.x);
_totalLength = _startSegmentLength + _midSegmentLength + endSegmentLength;
}
public override float GetLength()
{
return _totalLength;
}
public override void GetPoint(float t, out Vector2 position, out float rotation)
{
var distanceFromStart = t * _totalLength;
if (distanceFromStart < _startSegmentLength)
{
position = Vector2.Lerp(_startPoint, _startZagPoint,
Mathf.InverseLerp(0, _startSegmentLength, distanceFromStart));
rotation = _startRotation;
}
else if (distanceFromStart < _startSegmentLength + _midSegmentLength)
{
var midLength = _startSegmentLength + _midSegmentLength;
position = Vector2.Lerp(_startZagPoint, _endZagPoint,
Mathf.InverseLerp(_startSegmentLength, midLength, distanceFromStart));
rotation = _midRotation;
}
else
{
var midLength = _startSegmentLength + _midSegmentLength;
position = Vector2.Lerp(_endZagPoint, _endPoint,
Mathf.InverseLerp(midLength, _totalLength, distanceFromStart));
rotation = _endRotation;
}
}
}
}

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

View File

@@ -0,0 +1,69 @@
using Unity.Mathematics;
using UnityEngine;
namespace Timing
{
public class GameTime : MonoBehaviour
{
private double _lastCapturedDspTime;
public static bool Active { get; private set; }
public static double StartTime { get; private set; }
public static double TimeSinceClipStart { get; private set; }
private static NativeLinearRegression TimePrediction { get; set; }
private void Update()
{
if (!Active) return;
#region Smoothen DSP buffer time
if (AudioSettings.dspTime > _lastCapturedDspTime)
{
TimePrediction.Sample(new double2(Time.realtimeSinceStartupAsDouble, AudioSettings.dspTime));
_lastCapturedDspTime = AudioSettings.dspTime;
}
var smoothDspTime = TimePrediction.SampleCount < 2
? AudioSettings.dspTime
: TimePrediction.Predict(Time.realtimeSinceStartupAsDouble);
#endregion
TimeSinceClipStart = smoothDspTime - StartTime;
}
private void OnDestroy()
{
TimePrediction.Dispose();
}
/// <summary>
/// Tells the time manager to start counting time
/// </summary>
public static void StartFrom(double startTime)
{
StartTime = startTime;
TimeSinceClipStart = AudioSettings.dspTime - startTime;
Active = true;
TimePrediction ??= new NativeLinearRegression();
TimePrediction.Clear();
}
public static void Pause()
{
Active = false;
}
public static void UnPause(double timeSinceClipStart)
{
TimePrediction.Clear();
StartTime = AudioSettings.dspTime - timeSinceClipStart;
TimeSinceClipStart = timeSinceClipStart;
Active = true;
}
}
}

View File

@@ -0,0 +1,102 @@
using System;
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
namespace Timing
{
public sealed class NativeLinearRegression : IDisposable
{
public int SampleCount { get; private set; }
public int MaxSampleCount { get; }
private NativeArray<double2> _samples;
public NativeLinearRegression(int maxSampleCount = 12)
{
MaxSampleCount = maxSampleCount;
_samples = new NativeArray<double2>(maxSampleCount, Allocator.Persistent);
}
public void Sample(double2 plot)
{
if (SampleCount < MaxSampleCount)
SampleCount++;
else
for (var i = 1; i < SampleCount; i++)
_samples[i - 1] = _samples[i];
_samples[SampleCount - 1] = plot;
}
public double Predict(in double x)
{
using var result = new NativeArray<double>(2, Allocator.TempJob);
var jobData = new LinearRegressionJob
{
samples = _samples.Slice(0, SampleCount),
yInterceptAndSlope = result
};
jobData.Schedule().Complete();
var yIntercept = result[0];
var slope = result[1];
return x * slope + yIntercept;
}
public void Clear()
{
SampleCount = 0;
}
public void Dispose()
{
_samples.Dispose();
}
}
[BurstCompile]
public struct LinearRegressionJob : IJob
{
[ReadOnly]
public NativeSlice<double2> samples;
[WriteOnly]
public NativeArray<double> yInterceptAndSlope;
public void Execute()
{
double sumOfX = 0;
double sumOfY = 0;
double sumOfXSq = 0;
double sumCoDeviates = 0;
var sampleCount = samples.Length;
for (var i = 0; i < sampleCount; i++)
{
var plot = samples[i];
sumCoDeviates += plot.x * plot.y;
sumOfX += plot.x;
sumOfY += plot.y;
sumOfXSq += plot.x * plot.x;
}
var ssX = sumOfXSq - sumOfX * sumOfX / sampleCount;
var sCo = sumCoDeviates - sumOfX * sumOfY / sampleCount;
var meanX = sumOfX / sampleCount;
var meanY = sumOfY / sampleCount;
// y-intercept
yInterceptAndSlope[0] = meanY - sCo / ssX * meanX;
// slope
yInterceptAndSlope[1] = sCo / ssX;
}
}
}

View File

@@ -1,45 +1,45 @@
using System.Collections;
using System.Collections.Generic;
using Helpers;
using UnityEngine;
using UnityEngine.UI;
namespace Title
{
public class TitleManager : MonoBehaviour
{
public Text anyKeyPressed;
public List<string> textList;
private int _textIndex;
private void Start()
{
textList = new List<string>
{
Localization.ParseAuto($"TITLE_ANYKEY"),
Localization.ParseAuto($"TITLE_THANKS"),
$"{Localization.ParseAuto($"TITLE_BUILDVERSION")} {Application.version}"
};
StartCoroutine(CycleThroughText());
}
private IEnumerator CycleThroughText()
{
while (true)
{
_textIndex = (_textIndex + 1) % textList.Count;
anyKeyPressed.text = textList[_textIndex];
yield return new WaitForSeconds(3);
}
// ReSharper disable once IteratorNeverReturns
}
public void ChangeScene()
{
SceneSwapper.LoadScene("ModeSelection");
}
}
}
using System.Collections;
using System.Collections.Generic;
using Helpers;
using UnityEngine;
using UnityEngine.UI;
namespace Title
{
public class TitleManager : MonoBehaviour
{
public Text anyKeyPressed;
public List<string> textList;
private int _textIndex;
private void Start()
{
textList = new List<string>
{
Localization.ParseAuto($"TITLE_ANYKEY"),
Localization.ParseAuto($"TITLE_THANKS"),
$"{Localization.ParseAuto($"TITLE_BUILDVERSION")} {Application.version}"
};
StartCoroutine(CycleThroughText());
}
private IEnumerator CycleThroughText()
{
while (true)
{
_textIndex = (_textIndex + 1) % textList.Count;
anyKeyPressed.text = textList[_textIndex];
yield return new WaitForSeconds(3);
}
// ReSharper disable once IteratorNeverReturns
}
public void ChangeScene()
{
SceneSwapper.LoadScene("ModeSelection");
}
}
}

View File

@@ -0,0 +1,84 @@
using UnityEngine;
namespace AstroDX.Utilities
{
public static class Trigonometry
{
public const float Tau = Mathf.PI * 2;
public static Vector2 Rotate(in this Vector2 v, in float degreesRad)
{
var magnitude = v.magnitude;
var originalDegrees = Mathf.Atan2(v.y, v.x);
var newDegrees = originalDegrees + degreesRad;
var newX = Mathf.Cos(newDegrees) * magnitude;
var newY = Mathf.Sin(newDegrees) * magnitude;
return new Vector2(newX, newY);
}
/// <summary>
/// Calculates a point's angle relative to a center point.
/// </summary>
/// <param name="position">The absolute position of a point.</param>
/// <param name="offset">The offset of the calculation's center point.</param>
/// <returns>The relative angle of a point from the given center point.</returns>
internal static float ToPolarAngle(in Vector2 position, in Vector2? offset = null)
{
if (!offset.HasValue)
return Mathf.Atan2(position.y, position.x);
var difference = position - offset.Value;
return Mathf.Atan2(difference.y, difference.x);
}
/// <summary>
/// Calculates the angle between a line to a point on a ring
/// and another line perpendicular from a tangent line of that point.
/// </summary>
/// <param name="adjacent"></param>
/// <param name="hypotenuse"></param>
/// <param name="clockwise"></param>
/// <returns></returns>
internal static float GetTangentAngleDelta(in float adjacent,
in float hypotenuse,
in bool clockwise)
{
var angleDiff = Mathf.Acos(adjacent / hypotenuse);
return clockwise ? -angleDiff : angleDiff;
}
/// <summary>
/// <para>
/// Calculates the angle between <c>startRotation</c> and <c>endRotation</c>,
/// given its traversing direction.
/// </para>
/// </summary>
/// <param name="startRotation">The starting rotation.</param>
/// <param name="endRotation">The ending rotation.</param>
/// <param name="clockwise">Traversing direction.</param>
/// <param name="wrapThreshold">
/// <para>Wraps to full circle for spans smaller than this value.</para>
/// <para><code>Tau / 4f</code> is recommended for offset circles</para>
/// </param>
/// <returns>
/// The span between the starting rotation and the ending rotation on a unit circle,
/// in radians.
/// </returns>
public static float GetAngleSpan(in float startRotation,
in float endRotation,
bool clockwise,
float wrapThreshold = Tau / 32f)
{
var span = clockwise
? (startRotation - endRotation + 2 * Tau) % Tau
: (endRotation - startRotation + 2 * Tau) % Tau;
if (span <= wrapThreshold)
span += Tau;
return span;
}
}
}

View File

@@ -0,0 +1,122 @@
Shader "AstroDX/Note"
{
Properties
{
[PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
_GradientTex ("Gradient Texture", 2D) = "white" {}
_Color ("Tint", Color) = (1,1,1,1)
_ShadowColor ("Shadow Tint", Color) = (0,0,0,1)
_Grayscale("Grayscale", Range(0, 1)) = 0
[MaterialToggle] _Shine ("Shine", Float) = 0
[MaterialToggle] PixelSnap ("Pixel snap", Float) = 0
}
SubShader
{
Tags
{
"Queue"="Transparent"
"IgnoreProjector"="True"
"RenderType"="Transparent"
"PreviewType"="Plane"
"CanUseSpriteAtlas"="True"
}
Cull Off
Lighting Off
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile _ PIXELSNAP_ON
#include "UnityCG.cginc"
struct appdata_t
{
fixed4 vertex : POSITION;
fixed4 color : COLOR;
fixed2 texcoord : TEXCOORD0;
};
struct v2f
{
fixed4 vertex : SV_POSITION;
fixed4 color : COLOR;
fixed2 texcoord : TEXCOORD0;
};
fixed4 _Color;
fixed _Shine;
v2f vert(appdata_t IN)
{
v2f OUT;
OUT.vertex = UnityObjectToClipPos(IN.vertex);
OUT.texcoord = IN.texcoord;
OUT.color = IN.color * _Color;
#ifdef PIXELSNAP_ON
OUT.vertex = UnityPixelSnap (OUT.vertex);
#endif
return OUT;
}
sampler2D _MainTex;
sampler2D _GradientTex;
sampler2D _AlphaTex;
fixed4 _ShadowColor;
fixed _AlphaSplitEnabled;
fixed _Grayscale;
fixed4 sample_sprite_texture(float2 uv)
{
const fixed4 color = tex2D(_MainTex, uv);
// setting to 0.004 and 0.996 prevents getting wrong texture edge colors
const fixed gradient_position = lerp(0.005, 0.995, saturate(color.r * 2 - 1));
const fixed4 gradient_map = tex2D(_GradientTex, fixed2(gradient_position, 0));
#if UNITY_TEXTURE_ALPHASPLIT_ALLOWED
if (_AlphaSplitEnabled)
{
color.a = tex2D (_AlphaTex, uv).r;
}
#endif //UNITY_TEXTURE_ALPHASPLIT_ALLOWED
// choke starts at 0.1 higher than the gradient map threshold
// to allow a smoother color blending (see touch)
const fixed red_choke = smoothstep(0, 0.5, color.r);
const fixed3 shadow_color = fixed3(color.b * _ShadowColor.rgb);
fixed3 sum = gradient_map * red_choke + shadow_color * _ShadowColor.a * color.b;
sum = color.g.xxxx + sum * (1 - color.g);
return fixed4(sum.rgb, color.a);
}
fixed4 frag(v2f IN) : SV_Target
{
fixed4 c = sample_sprite_texture(IN.texcoord);
//Rough human eye adjusted grayscale computation
const fixed mono_rgb = 0.299 * c.r + 0.587 * c.g + 0.114 * c.b;
c.rgb *= IN.color.rgb;
const fixed shine_multiplier = (sin(_Time.y * 16) * 0.5 + 0.5) * _Shine;
c.rgb += shine_multiplier.rrr * 0.2;
c.rgb *= 1 + shine_multiplier.rrr * 0.3;
const fixed3 out_color = lerp(c.rgb, mono_rgb.rrr, _Grayscale);
return fixed4(out_color, c.a * IN.color.a);
}
ENDCG
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB