Compare commits
36 Commits
v2.0.0.bet
...
v2.0.0.bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c1dd0b1f2 | ||
|
|
fae214f6d3 | ||
|
|
23fb548e11 | ||
|
|
dab1b8fd75 | ||
|
|
3c40b8da25 | ||
|
|
5e7932a825 | ||
|
|
d8efe9c1f1 | ||
|
|
866eefabd3 | ||
|
|
7c0cc48734 | ||
|
|
f3c735936a | ||
|
|
d657fafab5 | ||
|
|
af2a5478f1 | ||
|
|
840ac79e38 | ||
|
|
cf45ac7697 | ||
|
|
2ecf542087 | ||
|
|
7f6a1b01f8 | ||
|
|
14db91ea0f | ||
|
|
b776940c48 | ||
|
|
d26e1bba1f | ||
|
|
37bd37a850 | ||
|
|
04d6db6ec4 | ||
|
|
b80a535c65 | ||
|
|
de387958fa | ||
|
|
c94206f398 | ||
|
|
8ddffec227 | ||
|
|
adc4d7ef56 | ||
|
|
870a2b7b32 | ||
|
|
3d47031bd2 | ||
|
|
2d6aba81fa | ||
|
|
787f3014d9 | ||
|
|
7fa93aab21 | ||
|
|
490db467f0 | ||
|
|
47d52d2653 | ||
|
|
38c96ad522 | ||
|
|
89d1617593 | ||
|
|
7089b3e722 |
29
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: '2394425147'
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1.
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Device specs**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: '2394425147'
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# Macintosh index files
|
||||
.DS_Store
|
||||
87
README.md
@@ -1,39 +1,90 @@
|
||||
# AstroDX
|
||||

|
||||

|
||||

|
||||
| Latest (Development) | Latest (Stable) |
|
||||
|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| [](https://github.com/2394425147/maipaddx/releases/latest) | [](https://github.com/2394425147/maipaddx/releases/latest) |
|
||||
|
||||
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.
|
||||
|
||||
# Project open-source status
|
||||
Join our Discord! [](https://discord.gg/6fpETgpvjZ)
|
||||
|
||||
To ensure safety, we're not planning on making the project open-source. This repository serves solely as a knowledgebase for the game.
|
||||
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.
|
||||
# Open-source status
|
||||
|
||||
We initially intended for AstroDX to be fully open-source after it has been uploaded to official app stores, but as the game contains paid assets, we may only be able to partially open-source AstroDX.
|
||||
|
||||
However, if you have issues, please don't hesitate to point it out in issues and we'll try to answer them as best as we can.
|
||||
|
||||
This game is a clean-room implementation of maimai, and has been developed without using any original arcade data.
|
||||
|
||||
- For simai interpreter and serializer used in AstroDX: [SimaiSharp](https://github.com/reflektone-games/SimaiSharp)
|
||||
- For game open-source parts: [AstroDX core-dump](https://github.com/2394425147/maipaddx/tree/main/core-dump)
|
||||
|
||||
# Q&A
|
||||
|
||||
## Which version should I download?
|
||||
## Seems like my collections aren't getting installed properly in Beta
|
||||
|
||||
### 1.x
|
||||
We went through a redesign for the level storage structure. You can read more here: https://github.com/2394425147/astrodx/issues/115
|
||||
|
||||
As of now, **v1.1** is the latest stable release of AstroDX.
|
||||
Use this version if you want a bug-less experience.
|
||||
## Desktop version, and controller support
|
||||
|
||||
### 2.x
|
||||
Currently, AstroDX hasn't released a desktop version(Windows/Linux/macOS), we also have not released any official controller support yet.
|
||||
|
||||
> You should always update to the latest version whenever possible.
|
||||
> 2.x is constantly updated and it's hard to manage multiple versions of the game algorithms at the same time,
|
||||
> so we suggest you to keep up-to-date.
|
||||
Products such as "ADX controller", "BDX", etc., some of whose names may sound similar to AstroDX, are not associated with us.
|
||||
|
||||
**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.
|
||||
We are currently developing our controller API for the desktop version, and our API will be made public along with the desktop version. You may contact the original manufacturers of the controllers to request their support for AstroDX so that you can enjoy our game.
|
||||
|
||||
## Wait, is there a version for iOS?
|
||||
Well... It's a bit finnicky. There's a major change happening regarding TestFlight (which you can read more on our Discord server [here](https://discord.com/channels/892807792996536453/1210127565986205726/1238882652040200373)).
|
||||
|
||||
**TL;DR: AstroDX for iOS is migrating to a new developer account, and during this period we won't free up more tester slots. However, we hope to finish this transition soon.**
|
||||
|
||||
In the meantime, you will still be able to join the tests as per usual.
|
||||
|
||||
You can join the test at [TestFlight Group A](https://testflight.apple.com/join/rACTLjPL) or [TestFlight Group B](https://testflight.apple.com/join/ocj3yptn) or [TestFlight Group C](https://testflight.apple.com/join/CuMxZE2M) or [TestFlight Group D](https://testflight.apple.com/join/T6qKfV6f).
|
||||
|
||||

|
||||
|
||||
*Each public link only can hold 10k players (they counted by Apple ID, not devices), so please DO NOT rejoin the test if you already have AstroDX installed.*
|
||||
|
||||
## Are there any tutorials on importing?
|
||||
|
||||
Importing guides are available in the wiki of this repo.
|
||||
Detailed guides for Android and iOS are available here:
|
||||
- [Installation Guide for Android](https://sht.moe/adx-android)
|
||||
- [Installation Guide for iOS/iPadOS](https://rentry.org/adx_ios)
|
||||
|
||||
## Can I use charts transcribed from the official arcade game maimai?
|
||||
|
||||
We **don't recommend** doing this, as it violates SEGA's policies.
|
||||
|
||||
## I'm having some issues...
|
||||
|
||||
You can open an issue [here](https://github.com/2394425147/maipaddx/issues).
|
||||
|
||||
**As of May 2024, an attached English translation of the issue is mandatory.**
|
||||
|
||||
We welcome issues written in Chinese, Japanese and English. However, it would be strongly suggested to provide translations (even using online translator) to English when submitting them, thus other people could understand as well.
|
||||
|
||||
When submitting issues, please always ensure that you are running the latest released version. We also recommend reviewing existing issues to avoid duplication of questions or concerns.
|
||||
|
||||
Alternatively, on our [Discord server](https://discord.gg/6fpETgpvjZ), we also have a help forum dedicated for issues, an faq, as well as a suggestions channel for feedback.
|
||||
|
||||
## 質問がある場合
|
||||
|
||||
イシューは[こちら](https://github.com/2394425147/maipaddx/issues)から提出できます。
|
||||
|
||||
中国語、日本語、英語でのイシュー投稿を大歓迎します。ただし、他の方も理解できるように、イシューを提出する際には英文への翻訳を(オンライン翻訳を使用しても)お願いいたします。
|
||||
|
||||
イシューを提出する際には、最新バージョンを使用していることチェックしてください。また、重複を避けるため、既存のイシューを確認することをおすすめします。
|
||||
|
||||
また、AstroDXの[Discordサーバー](https://discord.gg/6fpETgpvjZ)には、質問用、FAQ、フィードバックのための提案チャンネルもご用意しています。
|
||||
|
||||
## 當遇到了問題的時候
|
||||
|
||||
可以在[這裏](https://github.com/2394425147/maipaddx/issues)提出issue。
|
||||
|
||||
我們歡迎使用中文、日文和英文編寫的issue。然而,在提交問題時,強烈建議提供英文翻譯(甚至使用線上翻譯),以便其他人也能理解。
|
||||
|
||||
提交issue時,請務必確保您正在執行的是最新發布的版本。 我們也建議審查現有issue,以避免重複或疑慮。
|
||||
|
||||
此外,在我們的[Discord伺服器](https://discord.gg/6fpETgpvjZ)上,還有一個專門解決問題的幫助論壇、常見問題解答以及回饋及建議頻道。
|
||||
|
||||
Happy playing!
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
190
core-dump/Scripts/Contexts/Gameplay/Behaviours/SlideBehaviour.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using SimaiSharp.Structures;
|
||||
|
||||
namespace AstroDX.Models.Scoring.Metrics.Extended
|
||||
{
|
||||
public class HealthStats : IReactiveStatistic
|
||||
{
|
||||
public HealthStats(int maxHealth)
|
||||
{
|
||||
MaxHealth = maxHealth;
|
||||
}
|
||||
|
||||
public void Push(in NoteType type, in JudgeData data)
|
||||
{
|
||||
Loss += GameSettings.Settings.Profile.Mods.LifeDrain.GetHealthLoss(type, data);
|
||||
}
|
||||
|
||||
public int MaxHealth { get; }
|
||||
public int Loss { get; private set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using SimaiSharp.Structures;
|
||||
|
||||
namespace AstroDX.Models.Scoring.Metrics
|
||||
{
|
||||
public interface IReactiveStatistic
|
||||
{
|
||||
void Push(in NoteType type, in JudgeData data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
using System;
|
||||
|
||||
namespace AstroDX.Models.Scoring.Metrics.Internal
|
||||
{
|
||||
public sealed class AchievementStats
|
||||
{
|
||||
private readonly Statistics _statistics;
|
||||
private readonly double _baseScore;
|
||||
|
||||
public AchievementStats(Statistics statistics)
|
||||
{
|
||||
_statistics = statistics;
|
||||
|
||||
var totalPointCount = statistics.JudgementStats.MaxTapCount +
|
||||
statistics.JudgementStats.MaxTouchCount +
|
||||
statistics.JudgementStats.MaxHoldCount * 2 +
|
||||
statistics.JudgementStats.MaxSlideCount * 3 +
|
||||
statistics.JudgementStats.MaxBreakCount * 5;
|
||||
|
||||
_baseScore = totalPointCount > 0 ? 100.00 / totalPointCount : 0;
|
||||
}
|
||||
|
||||
/// <param name="precise">Used when displaying achievement as text to avoid double rounding</param>
|
||||
public double GetAchievement(bool precise = false)
|
||||
{
|
||||
var baseAchievement = GetBaseAchievement();
|
||||
var extraAchievement = _statistics.JudgementStats.BreakRecord.MaxCount > 0
|
||||
? _statistics.JudgementStats.BreakRecord.Extras /
|
||||
_statistics.JudgementStats.BreakRecord.MaxCount
|
||||
: 0;
|
||||
|
||||
var totalAchievement = baseAchievement +
|
||||
extraAchievement;
|
||||
|
||||
return precise ? totalAchievement : Math.Round(totalAchievement, 4);
|
||||
}
|
||||
|
||||
private double GetBaseAchievement()
|
||||
{
|
||||
if (_baseScore == 0)
|
||||
return 0;
|
||||
|
||||
var tapAchievement = _statistics.JudgementStats.TapRecord.Points
|
||||
* _baseScore;
|
||||
|
||||
var touchAchievement = _statistics.JudgementStats.TouchRecord.Points
|
||||
* _baseScore;
|
||||
|
||||
var holdAchievement = _statistics.JudgementStats.HoldRecord.Points
|
||||
* _baseScore * 2;
|
||||
|
||||
var slideAchievement = _statistics.JudgementStats.SlideRecord.Points
|
||||
* _baseScore * 3;
|
||||
|
||||
var breakAchievement = _statistics.JudgementStats.BreakRecord.Points
|
||||
* _baseScore * 5;
|
||||
|
||||
return tapAchievement + touchAchievement + holdAchievement + slideAchievement + breakAchievement;
|
||||
}
|
||||
|
||||
public double CalculateMaxAchievable(bool includeExtras)
|
||||
{
|
||||
var tapAchievement = _statistics.JudgementStats.TapRecord.PassedCount * _baseScore;
|
||||
var touchAchievement = _statistics.JudgementStats.TouchRecord.PassedCount * _baseScore;
|
||||
var holdAchievement = _statistics.JudgementStats.HoldRecord.PassedCount * _baseScore * 2;
|
||||
var slideAchievement = _statistics.JudgementStats.SlideRecord.PassedCount * _baseScore * 3;
|
||||
var breakAchievement = _statistics.JudgementStats.BreakRecord.PassedCount * _baseScore * 5;
|
||||
|
||||
return includeExtras && _statistics.JudgementStats.BreakRecord.MaxCount > 0
|
||||
? tapAchievement + touchAchievement + holdAchievement + slideAchievement + breakAchievement +
|
||||
(double)_statistics.JudgementStats.BreakRecord.PassedCount /
|
||||
_statistics.JudgementStats.BreakRecord.MaxCount
|
||||
: tapAchievement + touchAchievement + holdAchievement + slideAchievement + breakAchievement;
|
||||
}
|
||||
|
||||
public ClearType EvaluateClearType(bool whilePlaying = false)
|
||||
{
|
||||
var achievement = GetAchievement();
|
||||
|
||||
var cleared = achievement >= 80 && _statistics.JudgementStats.AllNotesPassed;
|
||||
var missed = _statistics.JudgementStats.JudgedMissCount >= 1;
|
||||
var anyGood = _statistics.JudgementStats.JudgedGoodCount >= 1;
|
||||
var anyGreat = _statistics.JudgementStats.JudgedGreatCount >= 1;
|
||||
|
||||
var chartHasBreaks = _statistics.JudgementStats.MaxBreakCount > 0;
|
||||
|
||||
if (!whilePlaying && !cleared)
|
||||
return ClearType.Failed;
|
||||
|
||||
if (missed)
|
||||
return ClearType.Clear;
|
||||
|
||||
if (anyGood)
|
||||
return ClearType.FullCombo;
|
||||
|
||||
if (anyGreat)
|
||||
return ClearType.FullComboPlus;
|
||||
|
||||
if (!chartHasBreaks)
|
||||
return ClearType.AllPerfect;
|
||||
|
||||
var maxBreak = _statistics.JudgementStats.BreakRecord.CriticalCount ==
|
||||
_statistics.JudgementStats.BreakRecord.PassedCount;
|
||||
|
||||
return maxBreak
|
||||
? ClearType.AllPerfectPlus
|
||||
: ClearType.AllPerfect;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using SimaiSharp.Structures;
|
||||
|
||||
namespace AstroDX.Models.Scoring.Metrics.Internal
|
||||
{
|
||||
public sealed class ComboStats : IReactiveStatistic
|
||||
{
|
||||
public long lastCombo { get; private set; }
|
||||
public long maxCombo { get; private set; }
|
||||
|
||||
public void Push(in NoteType type, in JudgeData data = default)
|
||||
{
|
||||
if (data.grade == JudgeGrade.Miss)
|
||||
{
|
||||
lastCombo = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
lastCombo++;
|
||||
maxCombo = Math.Max(lastCombo, maxCombo);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System.Linq;
|
||||
|
||||
namespace AstroDX.Models.Scoring.Metrics.Internal
|
||||
{
|
||||
public sealed class DxScoreStats
|
||||
{
|
||||
private readonly Statistics _statistics;
|
||||
|
||||
private const int CriticalWeight = 3;
|
||||
|
||||
private static readonly int[] Weights =
|
||||
{
|
||||
CriticalWeight, 2, 1, 0, 0
|
||||
};
|
||||
|
||||
public DxScoreStats(Statistics statistics) => _statistics = statistics;
|
||||
|
||||
public uint TotalDxScore => _statistics.JudgementStats.MaxNoteCount * CriticalWeight;
|
||||
|
||||
/// <summary>
|
||||
/// This is used to generate max health or max dx score.
|
||||
/// </summary>
|
||||
public uint MaxAchievableDxScore => _statistics.JudgementStats.JudgedNoteCount * CriticalWeight;
|
||||
|
||||
/// <summary>
|
||||
/// This is used to generate the current health or dx score.
|
||||
/// </summary>
|
||||
public uint DxScore =>
|
||||
(uint)(_statistics.JudgementStats.TapRecord.Judged.Sum(c => Weights[(int)c.grade]) +
|
||||
_statistics.JudgementStats.TouchRecord.Judged.Sum(c => Weights[(int)c.grade]) +
|
||||
_statistics.JudgementStats.HoldRecord.Judged.Sum(c => Weights[(int)c.grade]) +
|
||||
_statistics.JudgementStats.SlideRecord.Judged.Sum(c => Weights[(int)c.grade]) +
|
||||
_statistics.JudgementStats.BreakRecord.Judged.Sum(c => Weights[(int)c.grade]));
|
||||
|
||||
public int GetGrade()
|
||||
{
|
||||
var dxScorePercentage = (float)DxScore / TotalDxScore;
|
||||
|
||||
// // add 1 for every passed threshold
|
||||
for (var i = 0; i < DxGradeThresholds.Thresholds.Length; i++)
|
||||
{
|
||||
if (dxScorePercentage < DxGradeThresholds.Thresholds[i])
|
||||
return i;
|
||||
}
|
||||
|
||||
return DxGradeThresholds.Thresholds.Length;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using AstroDX.Utilities.Extensions;
|
||||
using SimaiSharp.Structures;
|
||||
|
||||
namespace AstroDX.Models.Scoring.Metrics.Internal
|
||||
{
|
||||
public sealed class FluctuationStats : IReactiveStatistic
|
||||
{
|
||||
public long EarlyCount { get; private set; }
|
||||
public long LateCount { get; private set; }
|
||||
public double TotalDeviation { get; private set; }
|
||||
public long NotesCounted { get; private set; }
|
||||
public double Deviation => TotalDeviation / NotesCounted;
|
||||
|
||||
public void Push(in NoteType type, in JudgeData data)
|
||||
{
|
||||
if ((GameSettings.Settings.Profile.Metrics.fluctuationVisibility & data.grade.AsFlag()) == 0)
|
||||
return;
|
||||
|
||||
if (type == NoteType.Slide)
|
||||
return;
|
||||
|
||||
NotesCounted++;
|
||||
|
||||
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
|
||||
switch (data.state)
|
||||
{
|
||||
case JudgeState.Early:
|
||||
EarlyCount++;
|
||||
break;
|
||||
case JudgeState.Late:
|
||||
LateCount++;
|
||||
break;
|
||||
}
|
||||
|
||||
TotalDeviation += data.offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
using System;
|
||||
using SimaiSharp.Structures;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AstroDX.Models.Scoring.Metrics.Internal
|
||||
{
|
||||
public sealed class JudgementStats : IReactiveStatistic
|
||||
{
|
||||
public NoteRecord HoldRecord { get; }
|
||||
public NoteRecord SlideRecord { get; }
|
||||
public NoteRecord TapRecord { get; }
|
||||
public NoteRecord TouchRecord { get; }
|
||||
public NoteRecord BreakRecord { get; }
|
||||
|
||||
public uint MaxTapCount { get; }
|
||||
public uint MaxTouchCount { get; }
|
||||
public uint MaxHoldCount { get; }
|
||||
public uint MaxSlideCount { get; }
|
||||
public uint MaxBreakCount { get; }
|
||||
|
||||
public uint JudgedCriticalCount { get; private set; }
|
||||
public uint JudgedPerfectCount { get; private set; }
|
||||
public uint JudgedGreatCount { get; private set; }
|
||||
public uint JudgedGoodCount { get; private set; }
|
||||
public uint JudgedMissCount { get; private set; }
|
||||
public uint JudgedNoteCount { get; private set; }
|
||||
|
||||
public uint MaxNoteCount =>
|
||||
TapRecord.MaxCount + TouchRecord.MaxCount +
|
||||
HoldRecord.MaxCount + SlideRecord.MaxCount +
|
||||
BreakRecord.MaxCount;
|
||||
|
||||
public bool AllNotesPassed =>
|
||||
TapRecord.MaxCount == TapRecord.PassedCount &&
|
||||
TouchRecord.MaxCount == TouchRecord.PassedCount &&
|
||||
HoldRecord.MaxCount == HoldRecord.PassedCount &&
|
||||
SlideRecord.MaxCount == SlideRecord.PassedCount &&
|
||||
BreakRecord.MaxCount == BreakRecord.PassedCount;
|
||||
|
||||
public JudgementStats(ReadOnlyMemory<NoteCollection> noteCollections)
|
||||
{
|
||||
foreach (var noteCollection in noteCollections.Span)
|
||||
{
|
||||
foreach (var note in noteCollection)
|
||||
{
|
||||
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
|
||||
switch (note.type)
|
||||
{
|
||||
case NoteType.Tap:
|
||||
MaxTapCount++;
|
||||
break;
|
||||
case NoteType.Touch:
|
||||
MaxTouchCount++;
|
||||
break;
|
||||
case NoteType.Hold:
|
||||
MaxHoldCount++;
|
||||
break;
|
||||
case NoteType.Break:
|
||||
MaxBreakCount++;
|
||||
break;
|
||||
}
|
||||
|
||||
foreach (var slide in note.slidePaths)
|
||||
{
|
||||
if (slide.type == NoteType.Break)
|
||||
MaxBreakCount++;
|
||||
else
|
||||
MaxSlideCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TapRecord = new NoteRecord(MaxTapCount);
|
||||
TouchRecord = new NoteRecord(MaxTouchCount);
|
||||
HoldRecord = new NoteRecord(MaxHoldCount);
|
||||
SlideRecord = new NoteRecord(MaxSlideCount);
|
||||
BreakRecord = new NoteRecord(MaxBreakCount);
|
||||
}
|
||||
|
||||
public void Push(in NoteType type, in JudgeData data)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case NoteType.Tap:
|
||||
TapRecord.Push(in data);
|
||||
break;
|
||||
case NoteType.Touch:
|
||||
TouchRecord.Push(in data);
|
||||
break;
|
||||
case NoteType.Hold:
|
||||
HoldRecord.Push(in data);
|
||||
break;
|
||||
case NoteType.Slide:
|
||||
SlideRecord.Push(in data);
|
||||
break;
|
||||
case NoteType.Break:
|
||||
BreakRecord.Push(in data);
|
||||
BreakRecord.Extras += data.grade switch
|
||||
{
|
||||
JudgeGrade.CriticalPerfect => 1,
|
||||
JudgeGrade.Perfect =>
|
||||
Mathf.Abs((float)data.offset) <= 0.033335f ? 0.75 : 0.5,
|
||||
JudgeGrade.Great => 0.4,
|
||||
JudgeGrade.Good => 0.3,
|
||||
JudgeGrade.Miss => 0,
|
||||
_ => 0
|
||||
};
|
||||
|
||||
break;
|
||||
case NoteType.ForceInvalidate:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
switch (data.grade)
|
||||
{
|
||||
case JudgeGrade.CriticalPerfect:
|
||||
JudgedCriticalCount++;
|
||||
break;
|
||||
case JudgeGrade.Perfect:
|
||||
JudgedPerfectCount++;
|
||||
break;
|
||||
case JudgeGrade.Great:
|
||||
JudgedGreatCount++;
|
||||
break;
|
||||
case JudgeGrade.Good:
|
||||
JudgedGoodCount++;
|
||||
break;
|
||||
case JudgeGrade.Miss:
|
||||
JudgedMissCount++;
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
JudgedNoteCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
55
core-dump/Scripts/Models/Scoring/Metrics/Statistics.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AstroDX.Models.Scoring.Metrics.Internal;
|
||||
using Sigtrap.Relays;
|
||||
using SimaiSharp.Structures;
|
||||
|
||||
namespace AstroDX.Models.Scoring.Metrics
|
||||
{
|
||||
[Serializable]
|
||||
public sealed class Statistics
|
||||
{
|
||||
public Relay<NoteType, JudgeData> OnJudgementReceived { get; } = new();
|
||||
|
||||
public FluctuationStats FluctuationStats { get; }
|
||||
public ComboStats ComboStats { get; }
|
||||
public JudgementStats JudgementStats { get; }
|
||||
public AchievementStats AchievementStats { get; }
|
||||
public DxScoreStats DxScoreStats { get; }
|
||||
private HashSet<IReactiveStatistic> Extensions { get; }
|
||||
|
||||
public Statistics(ReadOnlyMemory<NoteCollection> chart)
|
||||
{
|
||||
JudgementStats = new JudgementStats(chart);
|
||||
FluctuationStats = new FluctuationStats();
|
||||
ComboStats = new ComboStats();
|
||||
AchievementStats = new AchievementStats(this);
|
||||
DxScoreStats = new DxScoreStats(this);
|
||||
Extensions = new HashSet<IReactiveStatistic>();
|
||||
}
|
||||
|
||||
public void Push(in NoteType type, in JudgeData data)
|
||||
{
|
||||
JudgementStats.Push(in type, in data);
|
||||
ComboStats.Push(in type, in data);
|
||||
FluctuationStats.Push(in type, in data);
|
||||
|
||||
foreach (var statistic in Extensions)
|
||||
statistic.Push(in type, in data);
|
||||
|
||||
OnJudgementReceived.Dispatch(type, data);
|
||||
}
|
||||
|
||||
public void RegisterExtension(IReactiveStatistic extension)
|
||||
{
|
||||
Extensions.Add(extension);
|
||||
}
|
||||
|
||||
public IReactiveStatistic GetExtensionOrDefault<T>()
|
||||
where T : IReactiveStatistic
|
||||
{
|
||||
return Extensions.FirstOrDefault(e => e.GetType() == typeof(T));
|
||||
}
|
||||
}
|
||||
}
|
||||
72
core-dump/Scripts/Models/Scoring/NoteRecord.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace AstroDX.Models.Scoring
|
||||
{
|
||||
public class NoteRecord
|
||||
{
|
||||
public IReadOnlyList<JudgeData> Judged { get; }
|
||||
|
||||
public uint MaxCount { get; }
|
||||
public uint PassedCount { get; private set; }
|
||||
public uint CriticalCount { get; private set; }
|
||||
public uint PerfectCount { get; private set; }
|
||||
public uint GreatCount { get; private set; }
|
||||
public uint GoodCount { get; private set; }
|
||||
public uint MissCount { get; private set; }
|
||||
public double Extras { get; set; }
|
||||
|
||||
public NoteRecord(uint maxCount)
|
||||
{
|
||||
Judged = new List<JudgeData>();
|
||||
MaxCount = maxCount;
|
||||
PassedCount = 0;
|
||||
Extras = 0;
|
||||
}
|
||||
|
||||
public void Push(in JudgeData data)
|
||||
{
|
||||
((List<JudgeData>)Judged).Add(data);
|
||||
|
||||
PassedCount++;
|
||||
switch (data.grade)
|
||||
{
|
||||
case JudgeGrade.CriticalPerfect:
|
||||
CriticalCount++;
|
||||
break;
|
||||
case JudgeGrade.Perfect:
|
||||
PerfectCount++;
|
||||
break;
|
||||
case JudgeGrade.Great:
|
||||
GreatCount++;
|
||||
break;
|
||||
case JudgeGrade.Good:
|
||||
GoodCount++;
|
||||
break;
|
||||
case JudgeGrade.Miss:
|
||||
MissCount++;
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
Points += GetScore(data.grade);
|
||||
}
|
||||
|
||||
public double Points { get; private set; }
|
||||
|
||||
private static double GetScore(JudgeGrade grade)
|
||||
{
|
||||
return grade switch
|
||||
{
|
||||
JudgeGrade.CriticalPerfect => 1,
|
||||
JudgeGrade.Perfect => 1,
|
||||
JudgeGrade.Great => 0.8,
|
||||
JudgeGrade.Good => 0.5,
|
||||
JudgeGrade.Miss => 0,
|
||||
_ => throw new
|
||||
ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
3
core-dump/Scripts/UI/RecyclingScrollRect/Interfaces.meta
Executable file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6a0d1a0a74b14c94af6f7d4d943075d8
|
||||
timeCreated: 1710933820
|
||||
101
core-dump/Scripts/UI/RecyclingScrollRect/Interfaces/ScrollRectRecyclingBase.cs
Executable file
@@ -0,0 +1,101 @@
|
||||
using System.Collections.Generic;
|
||||
using DG.Tweening;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.Pool;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace AstroDX.UI.RecyclingScrollRect.Interfaces
|
||||
{
|
||||
[RequireComponent(typeof(ScrollRect))]
|
||||
public abstract class ScrollRectRecyclingBase<TData> : MonoBehaviour,
|
||||
IDragHandler,
|
||||
IBeginDragHandler,
|
||||
IInitializePotentialDragHandler,
|
||||
IEndDragHandler
|
||||
{
|
||||
protected IList<TData> DataSource { get; private set; } = new List<TData>();
|
||||
|
||||
protected ObjectPool<ScrollViewItem<TData>> itemsPool;
|
||||
|
||||
[field: Disable, SerializeField]
|
||||
protected List<ScrollViewItem<TData>> ShownItemsUnordered { get; private set; } = new();
|
||||
|
||||
[BeginGroup("List View")]
|
||||
[NotNull, SerializeField, Tooltip("Where the items will be created.")]
|
||||
protected ScrollRect scrollRect;
|
||||
|
||||
[NotNull, SerializeField, Tooltip("The visual representation of each item.")]
|
||||
[EndGroup]
|
||||
protected ScrollViewItem<TData> itemPrefab;
|
||||
|
||||
private bool _initialized;
|
||||
|
||||
protected virtual void OnEnable()
|
||||
{
|
||||
if (_initialized)
|
||||
return;
|
||||
|
||||
itemsPool = new ObjectPool<ScrollViewItem<TData>>(() =>
|
||||
{
|
||||
var item = Instantiate(itemPrefab, scrollRect.content);
|
||||
item.DisableItem();
|
||||
return item;
|
||||
},
|
||||
item => item.EnableItem(),
|
||||
item => item.DisableItem());
|
||||
}
|
||||
|
||||
public void SetDataSource(IList<TData> dataSource)
|
||||
{
|
||||
DataSource = dataSource;
|
||||
ReloadItems();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reloads the scroll rect to match up with the data source.
|
||||
/// </summary>
|
||||
protected abstract void ReloadItems();
|
||||
|
||||
/// <summary>
|
||||
/// Sets the current visible range, load and discards items depending on the previous range.
|
||||
/// <param name="resetActiveItems">Set the item data again to items that can be recycled. Triggered when data source changes.</param>
|
||||
/// </summary>
|
||||
protected abstract void UpdateVisibleRange(bool resetActiveItems);
|
||||
|
||||
/// <summary>
|
||||
/// Jumps to the item in the scroll rect.
|
||||
/// </summary>
|
||||
public void JumpToItem(TData item)
|
||||
{
|
||||
// We don't do any error handling here so that the error is thrown and developers using this can debug what's happening.
|
||||
JumpToIndex(DataSource.IndexOf(item));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scrolls to the item in the scroll rect.
|
||||
/// </summary>
|
||||
public void ScrollToItem(TData item, float duration, Ease easing = default)
|
||||
{
|
||||
// We don't do any error handling here so that the error is thrown and developers using this can debug what's happening.
|
||||
ScrollToIndex(DataSource.IndexOf(item), duration, easing);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Jumps to the index provided by the data source in the scroll rect.
|
||||
/// </summary>
|
||||
public abstract void JumpToIndex(int index);
|
||||
|
||||
public abstract void ScrollToIndex(int index, float duration, Ease easing = default);
|
||||
|
||||
protected virtual void OnDestroy() => itemsPool.Dispose();
|
||||
|
||||
public abstract void OnDrag(PointerEventData eventData);
|
||||
|
||||
public abstract void OnBeginDrag(PointerEventData eventData);
|
||||
|
||||
public abstract void OnInitializePotentialDrag(PointerEventData eventData);
|
||||
|
||||
public abstract void OnEndDrag(PointerEventData eventData);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5843eb5b52bc4cd085d670e17ff8be63
|
||||
timeCreated: 1710933385
|
||||
34
core-dump/Scripts/UI/RecyclingScrollRect/Interfaces/ScrollViewItem.cs
Executable file
@@ -0,0 +1,34 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace AstroDX.UI.RecyclingScrollRect.Interfaces
|
||||
{
|
||||
public abstract class ScrollViewItem<T> : MonoBehaviour
|
||||
{
|
||||
public T Data { get; private set; }
|
||||
public int Index { get; private set; }
|
||||
|
||||
public void SetData(T data, int index)
|
||||
{
|
||||
Data = data;
|
||||
Index = index;
|
||||
|
||||
OnDataUpdated();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is called whenever this instance is requested to render a new item with a different index.
|
||||
/// </summary>
|
||||
protected abstract void OnDataUpdated();
|
||||
|
||||
/// <summary>
|
||||
/// When this object instance scrolls out of view, this is called to disable relevant components;
|
||||
/// Should be as lightweight as possible since disabled items might get reused instantly.
|
||||
/// </summary>
|
||||
internal abstract void DisableItem();
|
||||
|
||||
/// <summary>
|
||||
/// Called when an item is retrieved from the pool.
|
||||
/// </summary>
|
||||
internal abstract void EnableItem();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2f1b83f97d7847728b5f776276799111
|
||||
timeCreated: 1710933857
|
||||
3
core-dump/Scripts/UI/RecyclingScrollRect/ScrollRects.meta
Executable file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d313c27e4a974b3baea75f0d80917046
|
||||
timeCreated: 1710933392
|
||||
236
core-dump/Scripts/UI/RecyclingScrollRect/ScrollRects/RecyclingGridView.cs
Executable file
@@ -0,0 +1,236 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AstroDX.GameSettings;
|
||||
using AstroDX.UI.RecyclingScrollRect.Interfaces;
|
||||
using AstroDX.Utilities.Extensions;
|
||||
using DG.Tweening;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
|
||||
namespace AstroDX.UI.RecyclingScrollRect.ScrollRects
|
||||
{
|
||||
[Serializable]
|
||||
public class RecyclingGridView<TData> : ScrollRectRecyclingBase<TData>
|
||||
{
|
||||
[BeginGroup("Scrolling")]
|
||||
[SerializeField]
|
||||
private float gap;
|
||||
|
||||
[SerializeField]
|
||||
private float marginTop;
|
||||
|
||||
[SerializeField]
|
||||
private float marginBottom;
|
||||
|
||||
[Disable, SerializeField]
|
||||
private Vector2 itemSize;
|
||||
|
||||
[Disable, SerializeField]
|
||||
private int visibleStart;
|
||||
|
||||
[Disable, SerializeField]
|
||||
private int columnCount;
|
||||
|
||||
[Disable, SerializeField]
|
||||
private int rowCount;
|
||||
|
||||
[SerializeField, Disable]
|
||||
private float usableWidth;
|
||||
|
||||
[Disable, SerializeField]
|
||||
private int maxVisibleCount;
|
||||
|
||||
[Disable, SerializeField]
|
||||
private float contentHeight;
|
||||
|
||||
private Tweener _positionTween;
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
itemSize = itemPrefab?.transform.AsRectTransform().sizeDelta ?? Vector2.zero;
|
||||
|
||||
scrollRect.content.anchorMin = new Vector2(0, 1);
|
||||
scrollRect.content.anchorMax = new Vector2(1, 1);
|
||||
scrollRect.content.pivot = new Vector2(0.5f, 1);
|
||||
scrollRect.content.localScale = Vector3.one;
|
||||
|
||||
var dt = new DrivenRectTransformTracker();
|
||||
dt.Clear();
|
||||
|
||||
//Object to drive the transform
|
||||
dt.Add(this, scrollRect.content, DrivenTransformProperties.All);
|
||||
}
|
||||
|
||||
protected override void OnEnable()
|
||||
{
|
||||
base.OnEnable();
|
||||
Settings.Profile.Graphics.scaleFactorChanged.AddListener(ReloadItems);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (scrollRect.velocity.y == 0)
|
||||
return;
|
||||
|
||||
UpdateVisibleRange(false);
|
||||
}
|
||||
|
||||
protected override void ReloadItems()
|
||||
{
|
||||
columnCount = Mathf.FloorToInt((scrollRect.viewport.rect.width + gap) / (itemSize.x + gap));
|
||||
usableWidth = columnCount * itemSize.x + (columnCount - 1) * gap;
|
||||
|
||||
rowCount = Mathf.CeilToInt((float)DataSource.Count / columnCount);
|
||||
|
||||
maxVisibleCount = Mathf.CeilToInt((scrollRect.viewport.rect.size.y + gap) / (itemSize.y + gap)) * columnCount;
|
||||
|
||||
contentHeight = marginTop + rowCount * (itemSize.y + gap) + marginBottom;
|
||||
scrollRect.content.sizeDelta = new Vector2(scrollRect.content.sizeDelta.x, contentHeight);
|
||||
|
||||
UpdateVisibleRange(true);
|
||||
}
|
||||
|
||||
private readonly HashSet<int> _itemsToAdd = new();
|
||||
|
||||
private int _previousStartIndexInclusive;
|
||||
private int _previousEndIndexExclusive;
|
||||
|
||||
protected override void UpdateVisibleRange(bool resetActiveItems)
|
||||
{
|
||||
visibleStart = scrollRect.content.anchoredPosition.y <= marginTop
|
||||
? 0
|
||||
: columnCount *
|
||||
Mathf.FloorToInt((scrollRect.content.anchoredPosition.y - marginTop) / (itemSize.y + gap));
|
||||
|
||||
var startIndexInclusive = visibleStart;
|
||||
var endIndexExclusive = Mathf.Min(startIndexInclusive + maxVisibleCount, DataSource.Count);
|
||||
|
||||
// If the visible range hasn't changed, just update the positions of all items in range
|
||||
if (startIndexInclusive == _previousStartIndexInclusive &&
|
||||
endIndexExclusive == _previousEndIndexExclusive)
|
||||
{
|
||||
foreach (var item in ShownItemsUnordered)
|
||||
{
|
||||
item.GetRectTransform().anchoredPosition = AbsolutePositionAtIndex(item.Index);
|
||||
|
||||
if (resetActiveItems)
|
||||
item.SetData(DataSource[item.Index], item.Index);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_previousStartIndexInclusive = startIndexInclusive;
|
||||
_previousEndIndexExclusive = endIndexExclusive;
|
||||
|
||||
// Populate the list of items that should be shown by the end of this procedure
|
||||
for (var i = startIndexInclusive; i < endIndexExclusive; i++)
|
||||
_itemsToAdd.Add(i);
|
||||
|
||||
// Find and skip items that are already shown
|
||||
// Since the shown items are unordered (for example, we may request to show 1 after 2 and 3 are shown, resulting in [2, 3, 1]),
|
||||
// We can't make any assumptions about items not appearing later in the list (e.g. [1, 2, 4, 5, 6, 3])
|
||||
for (var i = 0; i < ShownItemsUnordered.Count; i++)
|
||||
{
|
||||
var item = ShownItemsUnordered[i];
|
||||
|
||||
if (_itemsToAdd.Contains(item.Index))
|
||||
{
|
||||
item.GetRectTransform().anchoredPosition = AbsolutePositionAtIndex(item.Index);
|
||||
|
||||
if (resetActiveItems)
|
||||
item.SetData(DataSource[item.Index], item.Index);
|
||||
|
||||
_itemsToAdd.Remove(item.Index);
|
||||
}
|
||||
else
|
||||
{
|
||||
// We don't need to render this item in the new visible range, so we can safely release it
|
||||
itemsPool.Release(item);
|
||||
|
||||
// Modifying a list while enumerating it isn't recommended
|
||||
// But we're doing it here for performance
|
||||
ShownItemsUnordered.RemoveAt(i);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
// Lastly, generate the actual missing items
|
||||
foreach (var index in _itemsToAdd)
|
||||
{
|
||||
var itemInstance = itemsPool.Get();
|
||||
ShownItemsUnordered.Add(itemInstance);
|
||||
itemInstance.SetData(DataSource[index], index);
|
||||
itemInstance.GetRectTransform().anchoredPosition = AbsolutePositionAtIndex(index);
|
||||
}
|
||||
|
||||
_itemsToAdd.Clear();
|
||||
}
|
||||
|
||||
public override void JumpToIndex(int index)
|
||||
{
|
||||
scrollRect.velocity = new Vector3(0, 0);
|
||||
scrollRect.verticalNormalizedPosition = GetScrollPosition(index);
|
||||
UpdateVisibleRange(false);
|
||||
}
|
||||
|
||||
public override void ScrollToIndex(int index, float duration, Ease easing = default)
|
||||
{
|
||||
scrollRect.velocity = new Vector3(0, 0);
|
||||
_positionTween?.Kill();
|
||||
|
||||
_positionTween = DOTween.To(() => scrollRect.verticalNormalizedPosition,
|
||||
p =>
|
||||
{
|
||||
scrollRect.verticalNormalizedPosition = p;
|
||||
UpdateVisibleRange(false);
|
||||
},
|
||||
GetScrollPosition(index),
|
||||
duration).SetEase(easing);
|
||||
}
|
||||
|
||||
private float GetScrollPosition(int index)
|
||||
{
|
||||
// absolute position at index starts 0 at the top, and decreases when descending
|
||||
var normalizedPosition = 1 + AbsolutePositionAtIndex(index).y / contentHeight;
|
||||
normalizedPosition -= ((itemSize.y + marginBottom) * (1 - normalizedPosition) - marginTop * normalizedPosition) /
|
||||
contentHeight;
|
||||
|
||||
return normalizedPosition;
|
||||
}
|
||||
|
||||
public override void OnInitializePotentialDrag(PointerEventData eventData)
|
||||
{
|
||||
if (eventData.button != PointerEventData.InputButton.Left)
|
||||
return;
|
||||
|
||||
_positionTween?.Kill();
|
||||
}
|
||||
|
||||
public override void OnBeginDrag(PointerEventData eventData)
|
||||
{
|
||||
}
|
||||
|
||||
public override void OnEndDrag(PointerEventData eventData)
|
||||
{
|
||||
}
|
||||
|
||||
public override void OnDrag(PointerEventData eventData)
|
||||
{
|
||||
if (eventData.button != PointerEventData.InputButton.Left)
|
||||
return;
|
||||
|
||||
UpdateVisibleRange(false);
|
||||
}
|
||||
|
||||
private Vector2 AbsolutePositionAtIndex(int index)
|
||||
{
|
||||
var column = index % columnCount;
|
||||
var fullContainerWidth = scrollRect.content.rect.size.x;
|
||||
var row = Mathf.FloorToInt((float)index / columnCount);
|
||||
|
||||
return new Vector2(column * (itemSize.x + gap) + (fullContainerWidth - usableWidth) / 2,
|
||||
-marginTop - row * (itemSize.y + gap));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 163b88140e634e719f1a2e8ae214523f
|
||||
timeCreated: 1710933419
|
||||
@@ -0,0 +1,207 @@
|
||||
using System.Collections.Generic;
|
||||
using AstroDX.UI.RecyclingScrollRect.Interfaces;
|
||||
using AstroDX.Utilities.Extensions;
|
||||
using DG.Tweening;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
|
||||
namespace AstroDX.UI.RecyclingScrollRect.ScrollRects
|
||||
{
|
||||
public class RecyclingVerticalListView<TData> : ScrollRectRecyclingBase<TData>
|
||||
{
|
||||
[BeginGroup("Scrolling")]
|
||||
[SerializeField]
|
||||
private float gap;
|
||||
|
||||
[SerializeField]
|
||||
private float marginTop;
|
||||
|
||||
[SerializeField]
|
||||
[EndGroup]
|
||||
private float marginBottom;
|
||||
|
||||
[Disable, SerializeField]
|
||||
private float itemHeight;
|
||||
|
||||
[Disable, SerializeField]
|
||||
private int maxVisibleCount;
|
||||
|
||||
[Disable, SerializeField]
|
||||
private float contentHeight;
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
itemHeight = itemPrefab?.GetRectTransform().sizeDelta.y ?? 0;
|
||||
|
||||
scrollRect.content.anchorMin = new Vector2(0, 1);
|
||||
scrollRect.content.anchorMax = new Vector2(1, 1);
|
||||
scrollRect.content.pivot = new Vector2(0.5f, 1);
|
||||
scrollRect.content.localScale = Vector3.one;
|
||||
|
||||
var dt = new DrivenRectTransformTracker();
|
||||
dt.Clear();
|
||||
|
||||
//Object to drive the transform
|
||||
dt.Add(this, scrollRect.content, DrivenTransformProperties.All);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (scrollRect.velocity.y == 0)
|
||||
return;
|
||||
|
||||
UpdateVisibleRange(false);
|
||||
}
|
||||
|
||||
protected override void ReloadItems()
|
||||
{
|
||||
maxVisibleCount = Mathf.CeilToInt((scrollRect.viewport.rect.size.y + gap) / (itemHeight + gap));
|
||||
|
||||
contentHeight = marginTop + (itemHeight + gap) + marginBottom;
|
||||
scrollRect.content.sizeDelta = new Vector2(scrollRect.content.sizeDelta.x, contentHeight);
|
||||
|
||||
UpdateVisibleRange(true);
|
||||
}
|
||||
|
||||
private readonly HashSet<int> _itemsToAdd = new();
|
||||
|
||||
private int _previousStartIndexInclusive;
|
||||
private int _previousEndIndexExclusive;
|
||||
|
||||
protected override void UpdateVisibleRange(bool resetActiveItems)
|
||||
{
|
||||
var startIndexInclusive = scrollRect.content.anchoredPosition.y <= marginTop
|
||||
? 0
|
||||
: Mathf.FloorToInt((scrollRect.content.anchoredPosition.y - marginTop) /
|
||||
(itemHeight + gap));
|
||||
var endIndexExclusive = Mathf.Min(startIndexInclusive + maxVisibleCount, DataSource.Count);
|
||||
|
||||
// If the visible range hasn't changed, just update the positions of all items in range
|
||||
if (startIndexInclusive == _previousStartIndexInclusive &&
|
||||
endIndexExclusive == _previousEndIndexExclusive)
|
||||
{
|
||||
foreach (var item in ShownItemsUnordered)
|
||||
{
|
||||
var position = item.GetRectTransform().anchoredPosition;
|
||||
position.y = AbsolutePositionAtIndex(item.Index);
|
||||
item.GetRectTransform().anchoredPosition = position;
|
||||
|
||||
if (resetActiveItems)
|
||||
item.SetData(DataSource[item.Index], item.Index);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_previousStartIndexInclusive = startIndexInclusive;
|
||||
_previousEndIndexExclusive = endIndexExclusive;
|
||||
|
||||
// Populate the list of items that should be shown by the end of this procedure
|
||||
for (var i = startIndexInclusive; i < endIndexExclusive; i++)
|
||||
_itemsToAdd.Add(i);
|
||||
|
||||
// Find and skip items that are already shown
|
||||
// Since the shown items are unordered (for example, we may request to show 1 after 2 and 3 are shown, resulting in [2, 3, 1]),
|
||||
// We can't make any assumptions about items not appearing later in the list (e.g. [1, 2, 4, 5, 6, 3])
|
||||
for (var i = 0; i < ShownItemsUnordered.Count; i++)
|
||||
{
|
||||
var item = ShownItemsUnordered[i];
|
||||
|
||||
if (_itemsToAdd.Contains(item.Index))
|
||||
{
|
||||
var position = item.GetRectTransform().anchoredPosition;
|
||||
position.y = AbsolutePositionAtIndex(item.Index);
|
||||
item.GetRectTransform().anchoredPosition = position;
|
||||
|
||||
if (resetActiveItems)
|
||||
item.SetData(DataSource[item.Index], item.Index);
|
||||
|
||||
_itemsToAdd.Remove(item.Index);
|
||||
}
|
||||
else
|
||||
{
|
||||
// We don't need to render this item in the new visible range, so we can safely release it
|
||||
itemsPool.Release(item);
|
||||
|
||||
// Modifying a list while enumerating it isn't recommended
|
||||
// But we're doing it here for performance
|
||||
ShownItemsUnordered.RemoveAt(i);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
// Lastly, generate the actual missing items
|
||||
foreach (var index in _itemsToAdd)
|
||||
{
|
||||
var itemInstance = itemsPool.Get();
|
||||
ShownItemsUnordered.Add(itemInstance);
|
||||
itemInstance.SetData(DataSource[index], index);
|
||||
|
||||
var position = itemInstance.GetRectTransform().anchoredPosition;
|
||||
position.y = AbsolutePositionAtIndex(index);
|
||||
itemInstance.GetRectTransform().anchoredPosition = position;
|
||||
}
|
||||
|
||||
_itemsToAdd.Clear();
|
||||
}
|
||||
|
||||
public override void JumpToIndex(int index)
|
||||
{
|
||||
scrollRect.velocity = new Vector3(0, 0);
|
||||
scrollRect.verticalNormalizedPosition = GetScrollPosition(index);
|
||||
UpdateVisibleRange(false);
|
||||
}
|
||||
|
||||
private Tweener _positionTween;
|
||||
|
||||
public override void ScrollToIndex(int index, float duration, Ease easing = default)
|
||||
{
|
||||
scrollRect.velocity = Vector2.zero;
|
||||
_positionTween?.Kill();
|
||||
_positionTween = DOTween.To(() => scrollRect.verticalNormalizedPosition, p =>
|
||||
{
|
||||
scrollRect.verticalNormalizedPosition = p;
|
||||
UpdateVisibleRange(false);
|
||||
}, GetScrollPosition(index), duration).SetEase(easing);
|
||||
}
|
||||
|
||||
private float GetScrollPosition(int index)
|
||||
{
|
||||
// absolute position at index starts 0 at the top, and decreases when descending
|
||||
var normalizedPosition = 1 + AbsolutePositionAtIndex(index) / contentHeight;
|
||||
normalizedPosition -= ((itemHeight + marginBottom) * (1 - normalizedPosition) - marginTop * normalizedPosition) /
|
||||
contentHeight;
|
||||
|
||||
return normalizedPosition;
|
||||
}
|
||||
|
||||
public override void OnInitializePotentialDrag(PointerEventData eventData)
|
||||
{
|
||||
if (eventData.button != PointerEventData.InputButton.Left)
|
||||
return;
|
||||
|
||||
_positionTween?.Kill();
|
||||
}
|
||||
|
||||
public override void OnBeginDrag(PointerEventData eventData)
|
||||
{
|
||||
}
|
||||
|
||||
public override void OnEndDrag(PointerEventData eventData)
|
||||
{
|
||||
}
|
||||
|
||||
public override void OnDrag(PointerEventData eventData)
|
||||
{
|
||||
if (eventData.button != PointerEventData.InputButton.Left)
|
||||
return;
|
||||
|
||||
UpdateVisibleRange(false);
|
||||
}
|
||||
|
||||
private float AbsolutePositionAtIndex(int index)
|
||||
{
|
||||
return -index * (itemHeight + gap) - marginTop;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6421e225fef64262bce238235e6f6e0a
|
||||
timeCreated: 1711013891
|
||||
69
core-dump/Scripts/Utilities/Deprecated/Timing/GameTime.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
84
core-dump/Scripts/Utilities/Trigonometry.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
122
core-dump/Shader/Note.shader
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
core-dump/Sprites/IMG_GAME_HOLD_0.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
core-dump/Sprites/IMG_GAME_HOLD_1.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
core-dump/Sprites/IMG_GAME_STAR_0.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
core-dump/Sprites/IMG_GAME_STAR_1.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
core-dump/Sprites/IMG_GAME_TAP_0.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
core-dump/Sprites/IMG_GAME_TAP_1.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
core-dump/Sprites/IMG_GAME_TOUCH.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
core-dump/Sprites/IMG_GAME_TOUCH_STAR.png
Normal file
|
After Width: | Height: | Size: 18 KiB |