Compare commits

...

55 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
fumiko
47d52d2653 Update README.md
Fixed Q&A format
2023-03-04 17:37:41 +08:00
fumiko
38c96ad522 Update README.md 2023-03-04 17:36:46 +08:00
fumiko
89d1617593 Update issue templates 2023-02-06 16:50:04 +08:00
52 changed files with 3487 additions and 61 deletions

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

@@ -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,31 +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 that builds on top of the traditional arcade experience and brings optimizations for mobile devices.
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.
# Project open-source status
# Open-source projects
Since AstroDX isn't published to any app stores, we wanted to make sure it's hard to be recompiled and sold unofficially (this game is free after all).
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.
- Simai serializer/deserializer: [SimaiSharp](https://github.com/reflektone-games/SimaiSharp)
- Gameplay logic: [AstroDX core-dump](https://github.com/2394425147/maipaddx/tree/main/core-dump)
We intended to open-source AstroDX after publishing to the iOS/Android app stores. Paid assets will be excluded to comply with licenses.
# Q&A
Q. Which version should I download?
## How do I download the iOS version?
> As of now, v1.1.1 is the latest stable version. You should use this if you prefer having less bugs.
> If you want more optimizations, a better navigation experience and reworked features (though unstable), you might want to give v2.0 a shot!
> [!IMPORTANT]
> Only 10k users can be in a group at any time. Don't join multiple groups if you joined one already.
**v2.0** is currently undergoing many changes, including a massive UI overhaul, runtime optimizations, vfx / audio system reworks, just to name a few.
However, new features are unstable and will eventually be updated as time progresses. If you live on cutting-edge technology, this is the version for you.
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.
Happy playing!
## I'm having some 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.
Open an issue [here](https://github.com/2394425147/astrodx/issues), or join our [Discord server](https://discord.gg/6fpETgpvjZ) to talk about it.
## 問題が発生した場合
> [!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