Merge branch 'rewrite/master' into downscroll

This commit is contained in:
Cameron Taylor 2024-05-28 02:24:40 -04:00
commit 9ccee66b47
39 changed files with 1192 additions and 353 deletions

4
.gitmodules vendored
View file

@ -1,6 +1,6 @@
[submodule "assets"] [submodule "assets"]
path = assets path = assets
url = https://github.com/FunkinCrew/funkin.assets url = https://github.com/FunkinCrew/Funkin-assets-secret
[submodule "art"] [submodule "art"]
path = art path = art
url = https://github.com/FunkinCrew/funkin.art url = https://github.com/FunkinCrew/Funkin-art-secret

9
.vscode/launch.json vendored
View file

@ -3,10 +3,17 @@
"configurations": [ "configurations": [
{ {
// Launch in native/CPP on Windows/OSX/Linux // Launch in native/CPP on Windows/OSX/Linux
"name": "Lime", "name": "Lime Build+Debug",
"type": "lime", "type": "lime",
"request": "launch" "request": "launch"
}, },
{
// Launch in native/CPP on Windows/OSX/Linux
"name": "Lime Debug (No Build)",
"type": "lime",
"request": "launch",
"preLaunchTask": null
},
{ {
// Launch in browser // Launch in browser
"name": "HTML5 Debug", "name": "HTML5 Debug",

View file

@ -155,6 +155,11 @@
"target": "hl", "target": "hl",
"args": ["-debug", "-DDIALOGUE"] "args": ["-debug", "-DDIALOGUE"]
}, },
{
"label": "Windows / Debug (Results Screen Test)",
"target": "windows",
"args": ["-debug", "-DRESULTS"]
},
{ {
"label": "Windows / Debug (Straight to Chart Editor)", "label": "Windows / Debug (Straight to Chart Editor)",
"target": "windows", "target": "windows",

View file

@ -4,6 +4,46 @@ All notable changes will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.4.0] - 2024-05-??
### Added
- 2 new Erect remixes, Eggnog and Satin Panties. Check them out from
- Improvements to the Freeplay screen, with song difficulty ratings and player rank displays.
- Reworked the Results screen, with additional animations and audio based on your performance.
- Added a Charter field to the chart format, to allow for crediting the creator of a level's chart.
- You can see who charted a song from the Pause menu.
### Changed
- Tweaked the charts for several songs:
- Winter Horrorland
- Stress
- Lit Up
- Custom note styles are now properly supported for songs; add new notestyles via JSON, then select it for use from the Chart Editor Metadata toolbox. (thanks Keoiki!)
- Health icons now support a Winning frame without requiring a spritesheet, simply include a third frame in the icon file. (thanks gamerbross!)
- Remember that for more complex behaviors such as animations or transitions, you should use an XML file to define each frame.
### Fixed
- Fixed a bug where pressing the volume keys would stop the Toy commercial (thanks gamerbross!)
- Fixed a bug where the Chart Editor would crash when losing (thanks gamerbross!)
- Made improvements to compiling documentation (thanks gedehari!)
- Fixed a crash on Linux caused by an old version of hxCodec (thanks Noobz4Life!)
- Optimized animation handling for characters (thanks richTrash21!)
## [0.3.3] - 2024-05-14
### Changed
- Cleaned up some code in `PlayAnimationSongEvent.hx` (thanks BurgerBalls!)
### Fixed
- Fix Web Loading Bar (thanks lemz1!)
- Don't allow any more inputs when exiting freeplay (thanks gamerbros!)
- Fixed using mouse wheel to scroll on freeplay (thanks JugieNoob!)
- Fixed the reset's of the health icons, score, and notes when re-entering gameplay from gameover (thanks ImCodist!)
- Fixed the chart editor character selector's hitbox width (thanks MadBear422!)
- Fixed camera stutter once a wipe transition to the Main Menu completes (thanks ImCodist!)
- Fixed an issue where hold note would be invisible for a single frame (thanks ImCodist!)
- Fix tween accumulation on title screen when pressing Y multiple times (thanks TheGaloXx!)
- Fix for a game over easter egg so you don't accidentally exit it when viewing
- Fix a crash when querying FlxG.state in the crash handler
- Fix an issue where the Freeplay menu never displays 100% clear
- Chart debug key now properly returns you to the previous chart editor session if you were playtesting a chart (thanks nebulazorua!)
- Hopefully fixed Freeplay crashes on AMD gpu's
## [0.3.2] - 2024-05-03 ## [0.3.2] - 2024-05-03
### Added ### Added
- Added `,` and `.` keybinds to the Chart Editor. These place Focus Camera events at the playhead, for the opponent and player respectively. - Added `,` and `.` keybinds to the Chart Editor. These place Focus Camera events at the playhead, for the opponent and player respectively.

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<project> <project>
<!-- _________________________ Application Settings _________________________ --> <!-- _________________________ Application Settings _________________________ -->
<app title="Friday Night Funkin'" file="Funkin" packageName="com.funkin.fnf" package="com.funkin.fnf" main="Main" version="0.3.2" company="ninjamuffin99" /> <app title="Friday Night Funkin'" file="Funkin" packageName="com.funkin.fnf" package="com.funkin.fnf" main="Main" version="0.3.3" company="ninjamuffin99" />
<!--Switch Export with Unique ApplicationID and Icon--> <!--Switch Export with Unique ApplicationID and Icon-->
<set name="APP_ID" value="0x0100f6c013bbc000" /> <set name="APP_ID" value="0x0100f6c013bbc000" />

View file

@ -1,6 +1,6 @@
# Friday Night Funkin' # Friday Night Funkin'
Friday Night Funkin' is a rhythm game. Built using HaxeFlixel for Ludem Dare 47. Friday Night Funkin' is a rhythm game. Built using HaxeFlixel for Ludum Dare 47.
This game was made with love to Newgrounds and it's community. Extra love to Tom Fulp. This game was made with love to Newgrounds and it's community. Extra love to Tom Fulp.
@ -23,7 +23,7 @@ Full credits can be found in-game, or wherever the credits.json file is.
## Programming ## Programming
- [ninjamuffin99](https://twitter.com/ninja_muffin99) - Lead Programmer - [ninjamuffin99](https://twitter.com/ninja_muffin99) - Lead Programmer
- [MasterEric](https://twitter.com/EliteMasterEric) - Programmer - [EliteMasterEric](https://twitter.com/EliteMasterEric) - Programmer
- [MtH](https://twitter.com/emmnyaa) - Charting and Additional Programming - [MtH](https://twitter.com/emmnyaa) - Charting and Additional Programming
- [GeoKureli](https://twitter.com/Geokureli/) - Additional Programming - [GeoKureli](https://twitter.com/Geokureli/) - Additional Programming
- Our contributors on GitHub - Our contributors on GitHub

2
assets

@ -1 +1 @@
Subproject commit 962130b2243a839106607d08a11599b1857bf8b3 Subproject commit 8a8239cb50b5277fb0cfce041b3d8a9dfc780c35

View file

@ -18,3 +18,7 @@
- HTML5: Compiles without any extra setup - HTML5: Compiles without any extra setup
6. If you are targeting for native, you may need to run `lime rebuild PLATFORM` and `lime rebuild PLATFORM -debug` 6. If you are targeting for native, you may need to run `lime rebuild PLATFORM` and `lime rebuild PLATFORM -debug`
7. `lime test PLATFORM` ! Add `-debug` to enable several debug features such as time travel (`PgUp`/`PgDn` in Play State). 7. `lime test PLATFORM` ! Add `-debug` to enable several debug features such as time travel (`PgUp`/`PgDn` in Play State).
# Troubleshooting
- During the cloning process, you may experience an error along the lines of `error: RPC failed; curl 92 HTTP/2 stream 0 was not closed cleanly: PROTOCOL_ERROR (err 1)` due to poor connectivity. A common fix is to run ` git config --global http.postBuffer 4096M`.

View file

@ -1,31 +1,31 @@
# Funkin' Debug Hotkeys # Funkin' Debug Hotkeys
`F4` (EVERYWHERE) - Leave Current State and move to Main Menu Most of this functionality is only available on debug builds of the game!
`F5` (EVERYWHERE) - Hot Reload Data Files
`Y` (Title Screen) - WOAH ## Any State
- `F2`: ***OVERLAY***: Enables the Flixel debug overlay, which has partial support for scripting.
- `F3`: ***SCREENSHOT***: Takes a screenshot of the game and saves it to the local `screenshots` directory. Works outside of debug builds too!
- `F4`: ***EJECT***: Forcibly switch state to the Main Menu (with no extra transition). Useful if you're stuck in a level and you need to get out!
- `F5`: ***HOT RELOAD***: Forcibly reload the game's scripts and data files, then restart the current state. If any files in the `assets` folder have been modified, the game should process the changes for you! NOTE: Known bug, this does not reset song charts or song scripts, but it should reset everything else (such as stage layout data and character animation data).
- `CTRL-SHIFT-L`: ***FORCE CRASH***: Immediately crash the game with a detailed crash log and a stack trace.
`~` (Main Menu) - Access Debug Menu ## **Play State**
- `H`: ***HIDE UI***: Makes the user interface invisible. Works in Pause Menu, great for screenshots.
- `1`: ***END SONG***: Immediately ends the song and moves to Results Screen on Freeplay, or next song on Story Mode.
- `2`: ***GAIN HEALTH***: Debug function, add 10% to the player's health.
- `3`: ***LOSE HEALTH***: Debug function, subtract 5% to the player's health.
- `9`: NEATO!
- `PAGEUP` (MacOS: `Fn-Up`): ***FORWARDS TIME TRAVEL****: Move forward by 2 sections. Hold SHIFT to move forward by 20 sections instead.
- `PAGEDOWN` (MacOS: `Fn-Down`): ***BACKWARDS TIME TRAVEL****: Move backward by 2 sections. Hold SHIFT to move backward by 20 sections instead.
`U` (Play) - Open Stage Editor State ## **Freeplay State**
`H` (Play) - Show/Hide HUD - `F` (Freeplay Menu) - Move to Favorites
`1` (Play) - End Song - `Q` (Freeplay Menu) - Back one category
`2` (Play) - Add 10% Health - `E` (Freeplay Menu) - Forward one category
`3` (Play) - Subtract 5% Health
`7` (Play) - (NOT WORKING) Open Chart Editor
`8` (Play) - Open Animation Editor
`9` (Play) - (Easter Egg) Classic Health Icon
`PGUP`/`Fn+Up` (Play) - Skip Forward In Time
`PGDN`/`Fn+Down` (Play) - 🦃 That's right, we're going to go BACK IN TIME
`F` (Freeplay Menu) - Move to Favorites ## **Title State**
`P` (Freeplay Menu) - Switch to Pico (probably doesn't work) - `Y` - WOAH
`T` (Freeplay Menu) - Start typing in search bar
`Q` (Freeplay Menu) - Back one letter
`E` (Freeplay Menu) - Forward one letter
`Arrows` (Stage Editor) - Move Prop ## **Main Menu**
`Ctrl-Z` (Stage Editor) - Undo - `~`: ***DEBUG****: Opens a menu to access the Chart Editor and other work-in-progress editors. Rebindable in the options menu.
`Y` (Stage Editor) - Leave Stage Editor - `CTRL-ALT-SHIFT-W`: ***ALL ACCESS***: Unlocks all songs in Freeplay. Only available on debug builds.
`H` (Pause Menu) - Hide the Pause Menu UI (good for screenshots!)

View file

@ -3,7 +3,7 @@
"description": "An introductory mod.", "description": "An introductory mod.",
"contributors": [ "contributors": [
{ {
"name": "MasterEric" "name": "EliteMasterEric"
} }
], ],
"api_version": "0.1.0", "api_version": "0.1.0",

View file

@ -3,7 +3,7 @@
"description": "Newgrounds? More like OLDGROUNDS lol.", "description": "Newgrounds? More like OLDGROUNDS lol.",
"contributors": [ "contributors": [
{ {
"name": "MasterEric" "name": "EliteMasterEric"
} }
], ],
"api_version": "0.1.0", "api_version": "0.1.0",

View file

@ -80,7 +80,7 @@
"name": "hxCodec", "name": "hxCodec",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "c0c7f2680cc190c932a549c2e2fdd9b0ba2bd10e", "ref": "61b98a7a353b7f529a8fec84ed9afc919a2dffdd",
"url": "https://github.com/FunkinCrew/hxCodec" "url": "https://github.com/FunkinCrew/hxCodec"
}, },
{ {

View file

@ -430,7 +430,7 @@ class Conductor
else if (currentTimeChange != null && this.songPosition > 0.0) else if (currentTimeChange != null && this.songPosition > 0.0)
{ {
// roundDecimal prevents representing 8 as 7.9999999 // roundDecimal prevents representing 8 as 7.9999999
this.currentStepTime = FlxMath.roundDecimal((currentTimeChange.beatTime * 4) + (this.songPosition - currentTimeChange.timeStamp) / stepLengthMs, 6); this.currentStepTime = FlxMath.roundDecimal((currentTimeChange.beatTime * Constants.STEPS_PER_BEAT) + (this.songPosition - currentTimeChange.timeStamp) / stepLengthMs, 6);
this.currentBeatTime = currentStepTime / Constants.STEPS_PER_BEAT; this.currentBeatTime = currentStepTime / Constants.STEPS_PER_BEAT;
this.currentMeasureTime = currentStepTime / stepsPerMeasure; this.currentMeasureTime = currentStepTime / stepsPerMeasure;
this.currentStep = Math.floor(currentStepTime); this.currentStep = Math.floor(currentStepTime);
@ -564,7 +564,7 @@ class Conductor
if (ms >= timeChange.timeStamp) if (ms >= timeChange.timeStamp)
{ {
lastTimeChange = timeChange; lastTimeChange = timeChange;
resultStep = lastTimeChange.beatTime * 4; resultStep = lastTimeChange.beatTime * Constants.STEPS_PER_BEAT;
} }
else else
{ {
@ -600,7 +600,7 @@ class Conductor
var lastTimeChange:SongTimeChange = timeChanges[0]; var lastTimeChange:SongTimeChange = timeChanges[0];
for (timeChange in timeChanges) for (timeChange in timeChanges)
{ {
if (stepTime >= timeChange.beatTime * 4) if (stepTime >= timeChange.beatTime * Constants.STEPS_PER_BEAT)
{ {
lastTimeChange = timeChange; lastTimeChange = timeChange;
resultMs = lastTimeChange.timeStamp; resultMs = lastTimeChange.timeStamp;
@ -613,7 +613,7 @@ class Conductor
} }
var lastStepLengthMs:Float = ((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) / timeSignatureNumerator; var lastStepLengthMs:Float = ((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) / timeSignatureNumerator;
resultMs += (stepTime - lastTimeChange.beatTime * 4) * lastStepLengthMs; resultMs += (stepTime - lastTimeChange.beatTime * Constants.STEPS_PER_BEAT) * lastStepLengthMs;
return resultMs; return resultMs;
} }

View file

@ -214,6 +214,30 @@ class InitState extends FlxState
#elseif STAGEBUILD #elseif STAGEBUILD
// -DSTAGEBUILD // -DSTAGEBUILD
FlxG.switchState(() -> new funkin.ui.debug.stage.StageBuilderState()); FlxG.switchState(() -> new funkin.ui.debug.stage.StageBuilderState());
#elseif RESULTS
// -DRESULTS
FlxG.switchState(() -> new funkin.play.ResultState(
{
storyMode: false,
title: "CUM SONG",
isNewHighscore: true,
scoreData:
{
score: 1_234_567,
tallies:
{
sick: 130,
good: 25,
bad: 69,
shit: 69,
missed: 69,
combo: 69,
maxCombo: 69,
totalNotesHit: 140,
totalNotes: 200 // 0,
}
},
}));
#elseif ANIMDEBUG #elseif ANIMDEBUG
// -DANIMDEBUG // -DANIMDEBUG
FlxG.switchState(() -> new funkin.ui.debug.anim.DebugBoundingState()); FlxG.switchState(() -> new funkin.ui.debug.anim.DebugBoundingState());

View file

@ -40,8 +40,8 @@ class StrokeShader extends FlxShader
void main() void main()
{ {
vec4 sample = flixel_texture2D(bitmap, openfl_TextureCoordv); vec4 gay = flixel_texture2D(bitmap, openfl_TextureCoordv);
if (sample.a == 0.) { if (gay.a == 0.) {
float w = size.x / openfl_TextureSize.x; float w = size.x / openfl_TextureSize.x;
float h = size.y / openfl_TextureSize.y; float h = size.y / openfl_TextureSize.y;
@ -49,9 +49,9 @@ class StrokeShader extends FlxShader
|| flixel_texture2D(bitmap, vec2(openfl_TextureCoordv.x - w, openfl_TextureCoordv.y)).a != 0. || flixel_texture2D(bitmap, vec2(openfl_TextureCoordv.x - w, openfl_TextureCoordv.y)).a != 0.
|| flixel_texture2D(bitmap, vec2(openfl_TextureCoordv.x, openfl_TextureCoordv.y + h)).a != 0. || flixel_texture2D(bitmap, vec2(openfl_TextureCoordv.x, openfl_TextureCoordv.y + h)).a != 0.
|| flixel_texture2D(bitmap, vec2(openfl_TextureCoordv.x, openfl_TextureCoordv.y - h)).a != 0.) || flixel_texture2D(bitmap, vec2(openfl_TextureCoordv.x, openfl_TextureCoordv.y - h)).a != 0.)
sample = color; gay = color;
} }
gl_FragColor = sample; gl_FragColor = gay;
} }
') ')
public function new(color:FlxColor = 0xFFFFFFFF, width:Float = 1, height:Float = 1) public function new(color:FlxColor = 0xFFFFFFFF, width:Float = 1, height:Float = 1)

View file

@ -527,6 +527,14 @@ class Controls extends FlxActionSet
action.inputs[i].inputID = toAdd; action.inputs[i].inputID = toAdd;
} }
hasReplaced = true; hasReplaced = true;
} else if (input.device == KEYBOARD && input.inputID == toAdd) {
// This key is already bound!
if (hasReplaced) {
// Remove the duplicate keybind, don't replace.
action.inputs.remove(input);
} else {
hasReplaced = true;
}
} }
} }
@ -707,7 +715,7 @@ class Controls extends FlxActionSet
case Control.VOLUME_UP: return [PLUS, NUMPADPLUS]; case Control.VOLUME_UP: return [PLUS, NUMPADPLUS];
case Control.VOLUME_DOWN: return [MINUS, NUMPADMINUS]; case Control.VOLUME_DOWN: return [MINUS, NUMPADMINUS];
case Control.VOLUME_MUTE: return [ZERO, NUMPADZERO]; case Control.VOLUME_MUTE: return [ZERO, NUMPADZERO];
case Control.FULLSCREEN: return [FlxKey.F]; case Control.FULLSCREEN: return [FlxKey.F11]; // We use F for other things LOL.
} }
case Duo(true): case Duo(true):
@ -989,6 +997,7 @@ class Controls extends FlxActionSet
for (control in Control.createAll()) for (control in Control.createAll())
{ {
var inputs:Array<Int> = Reflect.field(data, control.getName()); var inputs:Array<Int> = Reflect.field(data, control.getName());
inputs = inputs.unique();
if (inputs != null) if (inputs != null)
{ {
if (inputs.length == 0) { if (inputs.length == 0) {
@ -1038,7 +1047,11 @@ class Controls extends FlxActionSet
var inputs = getInputsFor(control, device); var inputs = getInputsFor(control, device);
isEmpty = isEmpty && inputs.length == 0; isEmpty = isEmpty && inputs.length == 0;
if (inputs.length == 0) inputs = [FlxKey.NONE]; if (inputs.length == 0) {
inputs = [FlxKey.NONE];
} else {
inputs = inputs.unique();
}
Reflect.setField(data, control.getName(), inputs); Reflect.setField(data, control.getName(), inputs);
} }

View file

@ -83,6 +83,8 @@ class GameOverSubState extends MusicBeatSubState
var isChartingMode:Bool = false; var isChartingMode:Bool = false;
var mustNotExit:Bool = false;
var transparent:Bool; var transparent:Bool;
static final CAMERA_ZOOM_DURATION:Float = 0.5; static final CAMERA_ZOOM_DURATION:Float = 0.5;
@ -160,6 +162,8 @@ class GameOverSubState extends MusicBeatSubState
@:nullSafety(Off) @:nullSafety(Off)
function setCameraTarget():Void function setCameraTarget():Void
{ {
if (PlayState.instance.isMinimalMode || boyfriend == null) return;
// Assign a camera follow point to the boyfriend's position. // Assign a camera follow point to the boyfriend's position.
cameraFollowPoint = new FlxObject(PlayState.instance.cameraFollowPoint.x, PlayState.instance.cameraFollowPoint.y, 1, 1); cameraFollowPoint = new FlxObject(PlayState.instance.cameraFollowPoint.x, PlayState.instance.cameraFollowPoint.y, 1, 1);
cameraFollowPoint.x = boyfriend.getGraphicMidpoint().x; cameraFollowPoint.x = boyfriend.getGraphicMidpoint().x;
@ -240,7 +244,7 @@ class GameOverSubState extends MusicBeatSubState
} }
// KEYBOARD ONLY: Return to the menu when pressing the assigned key. // KEYBOARD ONLY: Return to the menu when pressing the assigned key.
if (controls.BACK) if (controls.BACK && !mustNotExit)
{ {
blueballed = false; blueballed = false;
PlayState.instance.deathCounter = 0; PlayState.instance.deathCounter = 0;
@ -252,6 +256,7 @@ class GameOverSubState extends MusicBeatSubState
this.close(); this.close();
if (FlxG.sound.music != null) FlxG.sound.music.pause(); // Don't reset song position! if (FlxG.sound.music != null) FlxG.sound.music.pause(); // Don't reset song position!
PlayState.instance.close(); // This only works because PlayState is a substate! PlayState.instance.close(); // This only works because PlayState is a substate!
return;
} }
else if (PlayStatePlaylist.isStoryMode) else if (PlayStatePlaylist.isStoryMode)
{ {

View file

@ -826,6 +826,8 @@ class PlayState extends MusicBeatSubState
resetCamera(); resetCamera();
var fromDeathState = isPlayerDying;
persistentUpdate = true; persistentUpdate = true;
persistentDraw = true; persistentDraw = true;
@ -863,8 +865,11 @@ class PlayState extends MusicBeatSubState
if (currentStage != null) currentStage.resetStage(); if (currentStage != null) currentStage.resetStage();
playerStrumline.vwooshNotes(); if (!fromDeathState)
opponentStrumline.vwooshNotes(); {
playerStrumline.vwooshNotes();
opponentStrumline.vwooshNotes();
}
playerStrumline.clean(); playerStrumline.clean();
opponentStrumline.clean(); opponentStrumline.clean();
@ -1075,6 +1080,25 @@ class PlayState extends MusicBeatSubState
function moveToGameOver():Void function moveToGameOver():Void
{ {
// Reset and update a bunch of values in advance for the transition back from the game over substate.
playerStrumline.clean();
opponentStrumline.clean();
songScore = 0;
updateScoreText();
health = Constants.HEALTH_STARTING;
healthLerp = health;
healthBar.value = healthLerp;
if (!isMinimalMode)
{
iconP1.updatePosition();
iconP2.updatePosition();
}
// Transition to the game over substate.
var gameOverSubState = new GameOverSubState( var gameOverSubState = new GameOverSubState(
{ {
isChartingMode: isChartingMode, isChartingMode: isChartingMode,
@ -2785,6 +2809,7 @@ class PlayState extends MusicBeatSubState
deathCounter = 0; deathCounter = 0;
var isNewHighscore = false; var isNewHighscore = false;
var prevScoreData:Null<SaveScoreData> = Save.instance.getSongScore(currentSong.id, currentDifficulty);
if (currentSong != null && currentSong.validScore) if (currentSong != null && currentSong.validScore)
{ {
@ -2804,7 +2829,6 @@ class PlayState extends MusicBeatSubState
totalNotesHit: Highscore.tallies.totalNotesHit, totalNotesHit: Highscore.tallies.totalNotesHit,
totalNotes: Highscore.tallies.totalNotes, totalNotes: Highscore.tallies.totalNotes,
}, },
accuracy: Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes,
}; };
// adds current song data into the tallies for the level (story levels) // adds current song data into the tallies for the level (story levels)
@ -2841,7 +2865,7 @@ class PlayState extends MusicBeatSubState
score: PlayStatePlaylist.campaignScore, score: PlayStatePlaylist.campaignScore,
tallies: tallies:
{ {
// TODO: Sum up the values for the whole level! // TODO: Sum up the values for the whole week!
sick: 0, sick: 0,
good: 0, good: 0,
bad: 0, bad: 0,
@ -2852,7 +2876,6 @@ class PlayState extends MusicBeatSubState
totalNotesHit: 0, totalNotesHit: 0,
totalNotes: 0, totalNotes: 0,
}, },
accuracy: Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes,
}; };
if (Save.instance.isLevelHighScore(PlayStatePlaylist.campaignId, PlayStatePlaylist.campaignDifficulty, data)) if (Save.instance.isLevelHighScore(PlayStatePlaylist.campaignId, PlayStatePlaylist.campaignDifficulty, data))
@ -2938,11 +2961,11 @@ class PlayState extends MusicBeatSubState
{ {
if (rightGoddamnNow) if (rightGoddamnNow)
{ {
moveToResultsScreen(isNewHighscore); moveToResultsScreen(isNewHighscore, prevScoreData);
} }
else else
{ {
zoomIntoResultsScreen(isNewHighscore); zoomIntoResultsScreen(isNewHighscore, prevScoreData);
} }
} }
} }
@ -3016,7 +3039,7 @@ class PlayState extends MusicBeatSubState
/** /**
* Play the camera zoom animation and then move to the results screen once it's done. * Play the camera zoom animation and then move to the results screen once it's done.
*/ */
function zoomIntoResultsScreen(isNewHighscore:Bool):Void function zoomIntoResultsScreen(isNewHighscore:Bool, ?prevScoreData:SaveScoreData):Void
{ {
trace('WENT TO RESULTS SCREEN!'); trace('WENT TO RESULTS SCREEN!');
@ -3056,7 +3079,7 @@ class PlayState extends MusicBeatSubState
FlxTween.tween(camHUD, {alpha: 0}, 0.6, FlxTween.tween(camHUD, {alpha: 0}, 0.6,
{ {
onComplete: function(_) { onComplete: function(_) {
moveToResultsScreen(isNewHighscore); moveToResultsScreen(isNewHighscore, prevScoreData);
} }
}); });
@ -3089,7 +3112,7 @@ class PlayState extends MusicBeatSubState
/** /**
* Move to the results screen right goddamn now. * Move to the results screen right goddamn now.
*/ */
function moveToResultsScreen(isNewHighscore:Bool):Void function moveToResultsScreen(isNewHighscore:Bool, ?prevScoreData:SaveScoreData):Void
{ {
persistentUpdate = false; persistentUpdate = false;
vocals.stop(); vocals.stop();
@ -3101,6 +3124,8 @@ class PlayState extends MusicBeatSubState
{ {
storyMode: PlayStatePlaylist.isStoryMode, storyMode: PlayStatePlaylist.isStoryMode,
title: PlayStatePlaylist.isStoryMode ? ('${PlayStatePlaylist.campaignTitle}') : ('${currentChart.songName} by ${currentChart.songArtist}'), title: PlayStatePlaylist.isStoryMode ? ('${PlayStatePlaylist.campaignTitle}') : ('${currentChart.songName} by ${currentChart.songArtist}'),
prevScoreData: prevScoreData,
difficultyId: currentDifficulty,
scoreData: scoreData:
{ {
score: PlayStatePlaylist.isStoryMode ? PlayStatePlaylist.campaignScore : songScore, score: PlayStatePlaylist.isStoryMode ? PlayStatePlaylist.campaignScore : songScore,
@ -3116,7 +3141,6 @@ class PlayState extends MusicBeatSubState
totalNotesHit: talliesToUse.totalNotesHit, totalNotesHit: talliesToUse.totalNotesHit,
totalNotes: talliesToUse.totalNotes, totalNotes: talliesToUse.totalNotes,
}, },
accuracy: Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes,
}, },
isNewHighscore: isNewHighscore isNewHighscore: isNewHighscore
}); });

View file

@ -12,6 +12,8 @@ import funkin.ui.MusicBeatSubState;
import flixel.math.FlxRect; import flixel.math.FlxRect;
import flixel.text.FlxBitmapText; import flixel.text.FlxBitmapText;
import funkin.ui.freeplay.FreeplayScore; import funkin.ui.freeplay.FreeplayScore;
import flixel.text.FlxText;
import flixel.util.FlxColor;
import flixel.tweens.FlxEase; import flixel.tweens.FlxEase;
import funkin.ui.freeplay.FreeplayState; import funkin.ui.freeplay.FreeplayState;
import flixel.tweens.FlxTween; import flixel.tweens.FlxTween;
@ -22,153 +24,196 @@ import funkin.save.Save;
import funkin.save.Save.SaveScoreData; import funkin.save.Save.SaveScoreData;
import funkin.graphics.shaders.LeftMaskShader; import funkin.graphics.shaders.LeftMaskShader;
import funkin.play.components.TallyCounter; import funkin.play.components.TallyCounter;
import funkin.play.components.ClearPercentCounter;
/** /**
* The state for the results screen after a song or week is finished. * The state for the results screen after a song or week is finished.
*/ */
@:nullSafety
class ResultState extends MusicBeatSubState class ResultState extends MusicBeatSubState
{ {
final params:ResultsStateParams; final params:ResultsStateParams;
var resultsVariation:ResultVariations; final rank:ResultRank;
var songName:FlxBitmapText; final songName:FlxBitmapText;
var difficulty:FlxSprite; final difficulty:FlxSprite;
final clearPercentSmall:ClearPercentCounter;
var maskShaderSongName:LeftMaskShader = new LeftMaskShader(); final maskShaderSongName:LeftMaskShader = new LeftMaskShader();
var maskShaderDifficulty:LeftMaskShader = new LeftMaskShader(); final maskShaderDifficulty:LeftMaskShader = new LeftMaskShader();
final resultsAnim:FunkinSprite;
final ratingsPopin:FunkinSprite;
final scorePopin:FunkinSprite;
final bgFlash:FlxSprite;
final highscoreNew:FlxSprite;
final score:ResultScore;
var bfPerfect:Null<FlxAtlasSprite> = null;
var bfExcellent:Null<FlxAtlasSprite> = null;
var bfGreat:Null<FlxAtlasSprite> = null;
var bfGood:Null<FlxSprite> = null;
var gfGood:Null<FlxSprite> = null;
var bfShit:Null<FlxAtlasSprite> = null;
public function new(params:ResultsStateParams) public function new(params:ResultsStateParams)
{ {
super(); super();
this.params = params; this.params = params;
}
override function create():Void rank = calculateRank(params);
{ // rank = SHIT;
/*
if (params.scoreData.sick == params.scoreData.totalNotesHit
&& params.scoreData.maxCombo == params.scoreData.totalNotesHit) resultsVariation = PERFECT;
else if (params.scoreData.missed + params.scoreData.bad + params.scoreData.shit >= params.scoreData.totalNotes * 0.50)
resultsVariation = SHIT; // if more than half of your song was missed, bad, or shit notes, you get shit ending!
else
resultsVariation = NORMAL;
*/
resultsVariation = NORMAL;
FunkinSound.playMusic('results$resultsVariation', // We build a lot of this stuff in the constructor, then place it in create().
{ // This prevents having to do `null` checks everywhere.
startingVolume: 1.0,
overrideExisting: true,
restartTrack: true,
loop: resultsVariation != SHIT
});
// Reset the camera zoom on the results screen.
FlxG.camera.zoom = 1.0;
// TEMP-ish, just used to sorta "cache" the 3000x3000 image!
var cacheBullShit:FlxSprite = new FlxSprite().loadGraphic(Paths.image("resultScreen/soundSystem"));
add(cacheBullShit);
var dumb:FlxSprite = new FlxSprite().loadGraphic(Paths.image("resultScreen/scorePopin"));
add(dumb);
var bg:FlxSprite = FlxGradient.createGradientFlxSprite(FlxG.width, FlxG.height, [0xFFFECC5C, 0xFFFDC05C], 90);
bg.scrollFactor.set();
add(bg);
var bgFlash:FlxSprite = FlxGradient.createGradientFlxSprite(FlxG.width, FlxG.height, [0xFFFFEB69, 0xFFFFE66A], 90);
bgFlash.scrollFactor.set();
bgFlash.visible = false;
add(bgFlash);
// var bfGfExcellent:FlxAtlasSprite = new FlxAtlasSprite(380, -170, Paths.animateAtlas("resultScreen/resultsBoyfriendExcellent", "shared"));
// bfGfExcellent.visible = false;
// add(bfGfExcellent);
//
// var bfPerfect:FlxAtlasSprite = new FlxAtlasSprite(370, -180, Paths.animateAtlas("resultScreen/resultsBoyfriendPerfect", "shared"));
// bfPerfect.visible = false;
// add(bfPerfect);
//
// var bfSHIT:FlxAtlasSprite = new FlxAtlasSprite(0, 20, Paths.animateAtlas("resultScreen/resultsBoyfriendSHIT", "shared"));
// bfSHIT.visible = false;
// add(bfSHIT);
//
// bfGfExcellent.anim.onComplete = () -> {
// bfGfExcellent.anim.curFrame = 28;
// bfGfExcellent.anim.play(); // unpauses this anim, since it's on PlayOnce!
// };
//
// bfPerfect.anim.onComplete = () -> {
// bfPerfect.anim.curFrame = 136;
// bfPerfect.anim.play(); // unpauses this anim, since it's on PlayOnce!
// };
//
// bfSHIT.anim.onComplete = () -> {
// bfSHIT.anim.curFrame = 150;
// bfSHIT.anim.play(); // unpauses this anim, since it's on PlayOnce!
// };
var gf:FlxSprite = FunkinSprite.createSparrow(625, 325, 'resultScreen/resultGirlfriendGOOD');
gf.animation.addByPrefix("clap", "Girlfriend Good Anim", 24, false);
gf.visible = false;
gf.animation.finishCallback = _ -> {
gf.animation.play('clap', true, false, 9);
};
add(gf);
var boyfriend:FlxSprite = FunkinSprite.createSparrow(640, -200, 'resultScreen/resultBoyfriendGOOD');
boyfriend.animation.addByPrefix("fall", "Boyfriend Good Anim0", 24, false);
boyfriend.visible = false;
boyfriend.animation.finishCallback = function(_) {
boyfriend.animation.play('fall', true, false, 14);
};
add(boyfriend);
var soundSystem:FlxSprite = FunkinSprite.createSparrow(-15, -180, 'resultScreen/soundSystem');
soundSystem.animation.addByPrefix("idle", "sound system", 24, false);
soundSystem.visible = false;
new FlxTimer().start(0.4, _ -> {
soundSystem.animation.play("idle");
soundSystem.visible = true;
});
add(soundSystem);
difficulty = new FlxSprite(555);
var diffSpr:String = switch (PlayState.instance.currentDifficulty)
{
case 'easy':
'difEasy';
case 'normal':
'difNormal';
case 'hard':
'difHard';
case 'erect':
'difErect';
case 'nightmare':
'difNightmare';
case _:
'difNormal';
}
difficulty.loadGraphic(Paths.image("resultScreen/" + diffSpr));
add(difficulty);
var fontLetters:String = "AaBbCcDdEeFfGgHhiIJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz:1234567890"; var fontLetters:String = "AaBbCcDdEeFfGgHhiIJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz:1234567890";
songName = new FlxBitmapText(FlxBitmapFont.fromMonospace(Paths.image("resultScreen/tardlingSpritesheet"), fontLetters, FlxPoint.get(49, 62))); songName = new FlxBitmapText(FlxBitmapFont.fromMonospace(Paths.image("resultScreen/tardlingSpritesheet"), fontLetters, FlxPoint.get(49, 62)));
songName.text = params.title; songName.text = params.title;
songName.letterSpacing = -15; songName.letterSpacing = -15;
songName.angle = -4.4; songName.angle = -4.4;
songName.zIndex = 1000;
difficulty = new FlxSprite(555);
difficulty.zIndex = 1000;
clearPercentSmall = new ClearPercentCounter(FlxG.width / 2 + 300, FlxG.height / 2 - 100, 100, true);
clearPercentSmall.zIndex = 1000;
clearPercentSmall.visible = false;
bgFlash = FlxGradient.createGradientFlxSprite(FlxG.width, FlxG.height, [0xFFFFEB69, 0xFFFFE66A], 90);
resultsAnim = FunkinSprite.createSparrow(-200, -10, "resultScreen/results");
ratingsPopin = FunkinSprite.createSparrow(-150, 120, "resultScreen/ratingsPopin");
scorePopin = FunkinSprite.createSparrow(-180, 520, "resultScreen/scorePopin");
highscoreNew = new FlxSprite(310, 570);
score = new ResultScore(35, 305, 10, params.scoreData.score);
}
override function create():Void
{
// Reset the camera zoom on the results screen.
FlxG.camera.zoom = 1.0;
var bg:FlxSprite = FlxGradient.createGradientFlxSprite(FlxG.width, FlxG.height, [0xFFFECC5C, 0xFFFDC05C], 90);
bg.scrollFactor.set();
bg.zIndex = 10;
add(bg);
bgFlash.scrollFactor.set();
bgFlash.visible = false;
bgFlash.zIndex = 20;
add(bgFlash);
// The sound system which falls into place behind the score text. Plays every time!
var soundSystem:FlxSprite = FunkinSprite.createSparrow(-15, -180, 'resultScreen/soundSystem');
soundSystem.animation.addByPrefix("idle", "sound system", 24, false);
soundSystem.visible = false;
new FlxTimer().start(0.3, _ -> {
soundSystem.animation.play("idle");
soundSystem.visible = true;
});
soundSystem.zIndex = 1100;
add(soundSystem);
switch (rank)
{
case PERFECT | PERFECT_GOLD:
bfPerfect = new FlxAtlasSprite(370, -180, Paths.animateAtlas("resultScreen/results-bf/resultsPERFECT", "shared"));
bfPerfect.visible = false;
bfPerfect.zIndex = 500;
add(bfPerfect);
bfPerfect.anim.onComplete = () -> {
if (bfPerfect != null)
{
bfPerfect.anim.curFrame = 137;
bfPerfect.anim.play(); // unpauses this anim, since it's on PlayOnce!
}
};
case EXCELLENT:
bfExcellent = new FlxAtlasSprite(380, -170, Paths.animateAtlas("resultScreen/results-bf/resultsEXCELLENT", "shared"));
bfExcellent.visible = false;
bfExcellent.zIndex = 500;
add(bfExcellent);
bfExcellent.onAnimationFinish.add((animName) -> {
if (bfExcellent != null)
{
bfExcellent.playAnimation('Loop Start');
}
});
case GREAT:
bfGreat = new FlxAtlasSprite(640, 200, Paths.animateAtlas("resultScreen/results-bf/resultsGREAT", "shared"));
bfGreat.visible = false;
bfGreat.zIndex = 500;
add(bfGreat);
bfGreat.onAnimationFinish.add((animName) -> {
if (bfGreat != null)
{
bfGreat.playAnimation('Loop Start');
}
});
case GOOD:
gfGood = FunkinSprite.createSparrow(625, 325, 'resultScreen/results-bf/resultsGOOD/resultGirlfriendGOOD');
gfGood.animation.addByPrefix("clap", "Girlfriend Good Anim", 24, false);
gfGood.visible = false;
gfGood.zIndex = 500;
gfGood.animation.finishCallback = _ -> {
if (gfGood != null)
{
gfGood.animation.play('clap', true, false, 9);
}
};
add(gfGood);
bfGood = FunkinSprite.createSparrow(640, -200, 'resultScreen/results-bf/resultsGOOD/resultBoyfriendGOOD');
bfGood.animation.addByPrefix("fall", "Boyfriend Good Anim0", 24, false);
bfGood.visible = false;
bfGood.zIndex = 501;
bfGood.animation.finishCallback = function(_) {
if (bfGood != null)
{
bfGood.animation.play('fall', true, false, 14);
}
};
add(bfGood);
case SHIT:
bfShit = new FlxAtlasSprite(0, 20, Paths.animateAtlas("resultScreen/results-bf/resultsSHIT", "shared"));
bfShit.visible = false;
bfShit.zIndex = 500;
add(bfShit);
bfShit.onAnimationFinish.add((animName) -> {
if (bfShit != null)
{
bfShit.playAnimation('Loop Start');
}
});
}
var diffSpr:String = 'dif${params?.difficultyId ?? 'Normal'}';
difficulty.loadGraphic(Paths.image("resultScreen/" + diffSpr));
add(difficulty);
add(songName); add(songName);
var angleRad = songName.angle * Math.PI / 180; var angleRad = songName.angle * Math.PI / 180;
speedOfTween.x = -1.0 * Math.cos(angleRad); speedOfTween.x = -1.0 * Math.cos(angleRad);
speedOfTween.y = -1.0 * Math.sin(angleRad); speedOfTween.y = -1.0 * Math.sin(angleRad);
timerThenSongName(); timerThenSongName(1.0, false);
songName.shader = maskShaderSongName; songName.shader = maskShaderSongName;
difficulty.shader = maskShaderDifficulty; difficulty.shader = maskShaderDifficulty;
@ -178,35 +223,53 @@ class ResultState extends MusicBeatSubState
var blackTopBar:FlxSprite = new FlxSprite().loadGraphic(Paths.image("resultScreen/topBarBlack")); var blackTopBar:FlxSprite = new FlxSprite().loadGraphic(Paths.image("resultScreen/topBarBlack"));
blackTopBar.y = -blackTopBar.height; blackTopBar.y = -blackTopBar.height;
FlxTween.tween(blackTopBar, {y: 0}, 0.4, {ease: FlxEase.quartOut, startDelay: 0.5}); FlxTween.tween(blackTopBar, {y: 0}, 0.4, {ease: FlxEase.quartOut});
blackTopBar.zIndex = 1010;
add(blackTopBar); add(blackTopBar);
var resultsAnim:FunkinSprite = FunkinSprite.createSparrow(-200, -10, "resultScreen/results");
resultsAnim.animation.addByPrefix("result", "results instance 1", 24, false); resultsAnim.animation.addByPrefix("result", "results instance 1", 24, false);
resultsAnim.animation.play("result"); resultsAnim.visible = false;
resultsAnim.zIndex = 1200;
add(resultsAnim); add(resultsAnim);
new FlxTimer().start(0.3, _ -> {
resultsAnim.visible = true;
resultsAnim.animation.play("result");
});
var ratingsPopin:FunkinSprite = FunkinSprite.createSparrow(-150, 120, "resultScreen/ratingsPopin");
ratingsPopin.animation.addByPrefix("idle", "Categories", 24, false); ratingsPopin.animation.addByPrefix("idle", "Categories", 24, false);
ratingsPopin.visible = false; ratingsPopin.visible = false;
ratingsPopin.zIndex = 1200;
add(ratingsPopin); add(ratingsPopin);
new FlxTimer().start(1.0, _ -> {
ratingsPopin.visible = true;
ratingsPopin.animation.play("idle");
});
var scorePopin:FunkinSprite = FunkinSprite.createSparrow(-180, 520, "resultScreen/scorePopin");
scorePopin.animation.addByPrefix("score", "tally score", 24, false); scorePopin.animation.addByPrefix("score", "tally score", 24, false);
scorePopin.visible = false; scorePopin.visible = false;
scorePopin.zIndex = 1200;
add(scorePopin); add(scorePopin);
new FlxTimer().start(1.0, _ -> {
scorePopin.visible = true;
scorePopin.animation.play("score");
scorePopin.animation.finishCallback = anim -> {
score.visible = true;
score.animateNumbers();
};
});
var highscoreNew:FlxSprite = new FlxSprite(310, 570);
highscoreNew.frames = Paths.getSparrowAtlas("resultScreen/highscoreNew"); highscoreNew.frames = Paths.getSparrowAtlas("resultScreen/highscoreNew");
highscoreNew.animation.addByPrefix("new", "NEW HIGHSCORE", 24); highscoreNew.animation.addByPrefix("new", "NEW HIGHSCORE", 24);
highscoreNew.visible = false; highscoreNew.visible = false;
highscoreNew.setGraphicSize(Std.int(highscoreNew.width * 0.8)); highscoreNew.setGraphicSize(Std.int(highscoreNew.width * 0.8));
highscoreNew.updateHitbox(); highscoreNew.updateHitbox();
highscoreNew.zIndex = 1200;
add(highscoreNew); add(highscoreNew);
var hStuf:Int = 50; var hStuf:Int = 50;
var ratingGrp:FlxTypedGroup<TallyCounter> = new FlxTypedGroup<TallyCounter>(); var ratingGrp:FlxTypedGroup<TallyCounter> = new FlxTypedGroup<TallyCounter>();
ratingGrp.zIndex = 1200;
add(ratingGrp); add(ratingGrp);
/** /**
@ -236,32 +299,115 @@ class ResultState extends MusicBeatSubState
var tallyMissed:TallyCounter = new TallyCounter(260, (hStuf * 9) + extraYOffset, params.scoreData.tallies.missed, 0xFFC68AE6); var tallyMissed:TallyCounter = new TallyCounter(260, (hStuf * 9) + extraYOffset, params.scoreData.tallies.missed, 0xFFC68AE6);
ratingGrp.add(tallyMissed); ratingGrp.add(tallyMissed);
var score:ResultScore = new ResultScore(35, 305, 10, params.scoreData.score);
score.visible = false; score.visible = false;
score.zIndex = 1200;
add(score); add(score);
for (ind => rating in ratingGrp.members) for (ind => rating in ratingGrp.members)
{ {
rating.visible = false; rating.visible = false;
new FlxTimer().start((0.3 * ind) + 0.55, _ -> { new FlxTimer().start((0.3 * ind) + 1.20, _ -> {
rating.visible = true; rating.visible = true;
FlxTween.tween(rating, {curNumber: rating.neededNumber}, 0.5, {ease: FlxEase.quartOut}); FlxTween.tween(rating, {curNumber: rating.neededNumber}, 0.5, {ease: FlxEase.quartOut});
}); });
} }
new FlxTimer().start(0.5, _ -> { ratingsPopin.animation.finishCallback = anim -> {
ratingsPopin.animation.play("idle"); startRankTallySequence();
ratingsPopin.visible = true;
if (params.isNewHighscore ?? false)
{
highscoreNew.visible = true;
highscoreNew.animation.play("new");
FlxTween.tween(highscoreNew, {y: highscoreNew.y + 10}, 0.8, {ease: FlxEase.quartOut});
}
else
{
highscoreNew.visible = false;
}
};
refresh();
super.create();
}
var rankTallyTimer:Null<FlxTimer> = null;
var clearPercentTarget:Int = 100;
var clearPercentLerp:Int = 0;
function startRankTallySequence():Void
{
var clearPercentFloat = (params.scoreData.tallies.sick + params.scoreData.tallies.good) / params.scoreData.tallies.totalNotes * 100;
clearPercentTarget = Math.floor(clearPercentFloat);
// Prevent off-by-one errors.
clearPercentLerp = Std.int(Math.max(0, clearPercentTarget - 36));
trace('Clear percent target: ' + clearPercentFloat + ', round: ' + clearPercentTarget);
var clearPercentCounter:ClearPercentCounter = new ClearPercentCounter(FlxG.width / 2 + 300, FlxG.height / 2 - 100, clearPercentLerp);
FlxTween.tween(clearPercentCounter, {curNumber: clearPercentTarget}, 1.5,
{
ease: FlxEase.quartOut,
onUpdate: _ -> {
// Only play the tick sound if the number increased.
if (clearPercentLerp != clearPercentCounter.curNumber)
{
clearPercentLerp = clearPercentCounter.curNumber;
FunkinSound.playOnce(Paths.sound('scrollMenu'));
}
},
onComplete: _ -> {
// Play confirm sound.
FunkinSound.playOnce(Paths.sound('confirmMenu'));
// Flash background.
bgFlash.visible = true;
FlxTween.tween(bgFlash, {alpha: 0}, 0.4);
// Just to be sure that the lerp didn't mess things up.
clearPercentCounter.curNumber = clearPercentTarget;
clearPercentCounter.flash(true);
new FlxTimer().start(0.4, _ -> {
clearPercentCounter.flash(false);
});
displayRankText();
new FlxTimer().start(2.0, _ -> {
FlxTween.tween(clearPercentCounter, {alpha: 0}, 0.5,
{
startDelay: 0.5,
ease: FlxEase.quartOut,
onComplete: _ -> {
remove(clearPercentCounter);
}
});
afterRankTallySequence();
});
}
});
clearPercentCounter.zIndex = 450;
add(clearPercentCounter);
if (ratingsPopin == null)
{
trace("Could not build ratingsPopin!");
}
else
{
// ratingsPopin.animation.play("idle");
// ratingsPopin.visible = true;
ratingsPopin.animation.finishCallback = anim -> { ratingsPopin.animation.finishCallback = anim -> {
scorePopin.animation.play("score"); // scorePopin.animation.play("score");
scorePopin.animation.finishCallback = anim -> {
score.visible = true;
score.animateNumbers();
};
scorePopin.visible = true;
if (params.isNewHighscore) // scorePopin.visible = true;
if (params.isNewHighscore ?? false)
{ {
highscoreNew.visible = true; highscoreNew.visible = true;
highscoreNew.animation.play("new"); highscoreNew.animation.play("new");
@ -272,47 +418,128 @@ class ResultState extends MusicBeatSubState
highscoreNew.visible = false; highscoreNew.visible = false;
} }
}; };
}
switch (resultsVariation) refresh();
}
function displayRankText():Void
{
var rankTextVert:FunkinSprite = FunkinSprite.create(FlxG.width - 64, 100, rank.getVerTextAsset());
rankTextVert.zIndex = 2000;
add(rankTextVert);
for (i in 0...10)
{
var rankTextBack:FunkinSprite = FunkinSprite.create(FlxG.width / 2 - 80, 50, rank.getHorTextAsset());
rankTextBack.y += (rankTextBack.height * i / 2) + 10;
rankTextBack.zIndex = 100;
add(rankTextBack);
}
refresh();
}
function afterRankTallySequence():Void
{
showSmallClearPercent();
FunkinSound.playMusic(rank.getMusicPath(),
{ {
// case SHIT: startingVolume: 1.0,
// bfSHIT.visible = true; overrideExisting: true,
// bfSHIT.playAnimation(""); restartTrack: true,
loop: rank.shouldMusicLoop()
});
case NORMAL: FlxG.sound.music.onComplete = () -> {
boyfriend.animation.play('fall'); if (rank == SHIT)
boyfriend.visible = true; {
FunkinSound.playMusic('bluu',
new FlxTimer().start((1 / 24) * 12, _ -> { {
bgFlash.visible = true; startingVolume: 0.0,
FlxTween.tween(bgFlash, {alpha: 0}, 0.4); overrideExisting: true,
new FlxTimer().start((1 / 24) * 2, _ -> restartTrack: true,
{ loop: true
// bgFlash.alpha = 0.5;
// bgFlash.visible = false;
});
}); });
FlxG.sound.music.fadeIn(10.0, 0.0, 1.0);
}
}
switch (rank)
{
case PERFECT | PERFECT_GOLD:
if (bfPerfect == null)
{
trace("Could not build PERFECT animation!");
}
else
{
bfPerfect.visible = true;
bfPerfect.playAnimation('');
}
case EXCELLENT:
if (bfExcellent == null)
{
trace("Could not build EXCELLENT animation!");
}
else
{
bfExcellent.visible = true;
bfExcellent.playAnimation('Intro');
}
case GREAT:
if (bfGreat == null)
{
trace("Could not build GREAT animation!");
}
else
{
bfGreat.visible = true;
bfGreat.playAnimation('Intro');
}
case SHIT:
if (bfShit == null)
{
trace("Could not build SHIT animation!");
}
else
{
bfShit.visible = true;
bfShit.playAnimation('Intro');
}
case GOOD:
if (bfGood == null)
{
trace("Could not build GOOD animation!");
}
else
{
bfGood.animation.play('fall');
bfGood.visible = true;
new FlxTimer().start((1 / 24) * 22, _ -> { new FlxTimer().start((1 / 24) * 22, _ -> {
// plays about 22 frames (at 24fps timing) after bf spawns in // plays about 22 frames (at 24fps timing) after bf spawns in
gf.animation.play('clap', true); if (gfGood != null)
gf.visible = true; {
gfGood.animation.play('clap', true);
gfGood.visible = true;
}
else
{
trace("Could not build GOOD animation!");
}
}); });
// case PERFECT: }
// bfPerfect.visible = true; default:
// bfPerfect.playAnimation(""); }
// bfGfExcellent.visible = true;
// bfGfExcellent.playAnimation("");
default:
}
});
super.create();
} }
function timerThenSongName():Void function timerThenSongName(timerLength:Float = 3.0, autoScroll:Bool = true):Void
{ {
movingSongStuff = false; movingSongStuff = false;
@ -323,21 +550,47 @@ class ResultState extends MusicBeatSubState
difficulty.y = -difficulty.height; difficulty.y = -difficulty.height;
FlxTween.tween(difficulty, {y: diffYTween}, 0.5, {ease: FlxEase.expoOut, startDelay: 0.8}); FlxTween.tween(difficulty, {y: diffYTween}, 0.5, {ease: FlxEase.expoOut, startDelay: 0.8});
if (clearPercentSmall != null)
{
clearPercentSmall.x = (difficulty.x + difficulty.width) + 60;
clearPercentSmall.y = -clearPercentSmall.height;
FlxTween.tween(clearPercentSmall, {y: 122 - 5}, 0.5, {ease: FlxEase.expoOut, startDelay: 0.8});
}
songName.y = -songName.height; songName.y = -songName.height;
var fuckedupnumber = (10) * (songName.text.length / 15); var fuckedupnumber = (10) * (songName.text.length / 15);
FlxTween.tween(songName, {y: diffYTween - 35 - fuckedupnumber}, 0.5, {ease: FlxEase.expoOut, startDelay: 0.9}); FlxTween.tween(songName, {y: diffYTween - 25 - fuckedupnumber}, 0.5, {ease: FlxEase.expoOut, startDelay: 0.9});
songName.x = (difficulty.x + difficulty.width) + 20; songName.x = clearPercentSmall.x + clearPercentSmall.width - 30;
new FlxTimer().start(3, _ -> { new FlxTimer().start(timerLength, _ -> {
var tempSpeed = FlxPoint.get(speedOfTween.x, speedOfTween.y); var tempSpeed = FlxPoint.get(speedOfTween.x, speedOfTween.y);
speedOfTween.set(0, 0); speedOfTween.set(0, 0);
FlxTween.tween(speedOfTween, {x: tempSpeed.x, y: tempSpeed.y}, 0.7, {ease: FlxEase.quadIn}); FlxTween.tween(speedOfTween, {x: tempSpeed.x, y: tempSpeed.y}, 0.7, {ease: FlxEase.quadIn});
movingSongStuff = true; movingSongStuff = (autoScroll);
}); });
} }
function showSmallClearPercent():Void
{
if (clearPercentSmall != null)
{
add(clearPercentSmall);
clearPercentSmall.visible = true;
clearPercentSmall.flash(true);
new FlxTimer().start(0.4, _ -> {
clearPercentSmall.flash(false);
});
clearPercentSmall.curNumber = clearPercentTarget;
clearPercentSmall.zIndex = 1000;
refresh();
}
movingSongStuff = true;
}
var movingSongStuff:Bool = false; var movingSongStuff:Bool = false;
var speedOfTween:FlxPoint = FlxPoint.get(-1, 1); var speedOfTween:FlxPoint = FlxPoint.get(-1, 1);
@ -345,11 +598,9 @@ class ResultState extends MusicBeatSubState
{ {
super.draw(); super.draw();
if (songName != null) songName.clipRect = FlxRect.get(Math.max(0, 520 - songName.x), 0, FlxG.width, songName.height);
{
songName.clipRect = FlxRect.get(Math.max(0, 540 - songName.x), 0, FlxG.width, songName.height); // PROBABLY SHOULD FIX MEMORY FREE OR WHATEVER THE PUT() FUNCTION DOES !!!! FEELS LIKE IT STUTTERS!!!
// PROBABLY SHOULD FIX MEMORY FREE OR WHATEVER THE PUT() FUNCTION DOES !!!! FEELS LIKE IT STUTTERS!!!
}
// if (songName != null && songName.frame != null) // if (songName != null && songName.frame != null)
// maskShaderSongName.frameUV = songName.frame.uv; // maskShaderSongName.frameUV = songName.frame.uv;
@ -364,8 +615,10 @@ class ResultState extends MusicBeatSubState
{ {
songName.x += speedOfTween.x; songName.x += speedOfTween.x;
difficulty.x += speedOfTween.x; difficulty.x += speedOfTween.x;
clearPercentSmall.x += speedOfTween.x;
songName.y += speedOfTween.y; songName.y += speedOfTween.y;
difficulty.y += speedOfTween.y; difficulty.y += speedOfTween.y;
clearPercentSmall.y += speedOfTween.y;
if (songName.x + songName.width < 100) if (songName.x + songName.width < 100)
{ {
@ -401,14 +654,135 @@ class ResultState extends MusicBeatSubState
super.update(elapsed); super.update(elapsed);
} }
public static function calculateRank(params:ResultsStateParams):ResultRank
{
// Perfect (Platinum) is a Sick Full Clear
var isPerfectGold = params.scoreData.tallies.sick == params.scoreData.tallies.totalNotes;
if (isPerfectGold) return ResultRank.PERFECT_GOLD;
// Else, use the standard grades
// Grade % (only good and sick), 1.00 is a full combo
var grade = (params.scoreData.tallies.sick + params.scoreData.tallies.good) / params.scoreData.tallies.totalNotes;
// Clear % (including bad and shit). 1.00 is a full clear but not a full combo
var clear = (params.scoreData.tallies.totalNotesHit) / params.scoreData.tallies.totalNotes;
if (grade == Constants.RANK_PERFECT_THRESHOLD)
{
return ResultRank.PERFECT;
}
else if (grade >= Constants.RANK_EXCELLENT_THRESHOLD)
{
return ResultRank.EXCELLENT;
}
else if (grade >= Constants.RANK_GREAT_THRESHOLD)
{
return ResultRank.GREAT;
}
else if (grade >= Constants.RANK_GOOD_THRESHOLD)
{
return ResultRank.GOOD;
}
else
{
return ResultRank.SHIT;
}
}
} }
enum abstract ResultVariations(String) enum abstract ResultRank(String)
{ {
var PERFECT_GOLD;
var PERFECT; var PERFECT;
var EXCELLENT; var EXCELLENT;
var NORMAL; var GREAT;
var GOOD;
var SHIT; var SHIT;
public function getMusicPath():String
{
switch (abstract)
{
case PERFECT_GOLD:
return 'resultsPERFECT';
case PERFECT:
return 'resultsPERFECT';
case EXCELLENT:
return 'resultsNORMAL';
case GREAT:
return 'resultsNORMAL';
case GOOD:
return 'resultsNORMAL';
case SHIT:
return 'resultsSHIT';
default:
return 'resultsNORMAL';
}
}
public function shouldMusicLoop():Bool
{
switch (abstract)
{
case PERFECT_GOLD:
return true;
case PERFECT:
return true;
case EXCELLENT:
return true;
case GREAT:
return true;
case GOOD:
return true;
case SHIT:
return false;
default:
return false;
}
}
public function getHorTextAsset()
{
switch (abstract)
{
case PERFECT_GOLD:
return 'resultScreen/rankText/rankScrollPERFECT';
case PERFECT:
return 'resultScreen/rankText/rankScrollPERFECT';
case EXCELLENT:
return 'resultScreen/rankText/rankScrollEXCELLENT';
case GREAT:
return 'resultScreen/rankText/rankScrollGREAT';
case GOOD:
return 'resultScreen/rankText/rankScrollGOOD';
case SHIT:
return 'resultScreen/rankText/rankScrollLOSS';
default:
return 'resultScreen/rankText/rankScrollGOOD';
}
}
public function getVerTextAsset()
{
switch (abstract)
{
case PERFECT_GOLD:
return 'resultScreen/rankText/rankTextPERFECT';
case PERFECT:
return 'resultScreen/rankText/rankTextPERFECT';
case EXCELLENT:
return 'resultScreen/rankText/rankTextEXCELLENT';
case GREAT:
return 'resultScreen/rankText/rankTextGREAT';
case GOOD:
return 'resultScreen/rankText/rankTextGOOD';
case SHIT:
return 'resultScreen/rankText/rankTextLOSS';
default:
return 'resultScreen/rankText/rankTextGOOD';
}
}
} }
typedef ResultsStateParams = typedef ResultsStateParams =
@ -426,10 +800,21 @@ typedef ResultsStateParams =
/** /**
* Whether the displayed score is a new highscore * Whether the displayed score is a new highscore
*/ */
var isNewHighscore:Bool; var ?isNewHighscore:Bool;
/**
* The difficulty ID of the song/week we just played.
* @default Normal
*/
var ?difficultyId:String;
/** /**
* The score, accuracy, and judgements. * The score, accuracy, and judgements.
*/ */
var scoreData:SaveScoreData; var scoreData:SaveScoreData;
/**
* The previous score data, used for rank comparision.
*/
var ?prevScoreData:SaveScoreData;
}; };

View file

@ -420,7 +420,8 @@ class BaseCharacter extends Bopper
{ {
if (isSinging()) return; if (isSinging()) return;
if (['hey', 'cheer'].contains(getCurrentAnimation()) && !isAnimationFinished()) return; var currentAnimation:String = getCurrentAnimation();
if ((currentAnimation == 'hey' || currentAnimation == 'cheer') && !isAnimationFinished()) return;
} }
// Prevent dancing while another animation is playing. // Prevent dancing while another animation is playing.
@ -441,19 +442,15 @@ class BaseCharacter extends Bopper
switch (player) switch (player)
{ {
case 1: case 1:
return [ return PlayerSettings.player1.controls.NOTE_LEFT_P
PlayerSettings.player1.controls.NOTE_LEFT_P, || PlayerSettings.player1.controls.NOTE_DOWN_P
PlayerSettings.player1.controls.NOTE_DOWN_P, || PlayerSettings.player1.controls.NOTE_UP_P
PlayerSettings.player1.controls.NOTE_UP_P, || PlayerSettings.player1.controls.NOTE_RIGHT_P;
PlayerSettings.player1.controls.NOTE_RIGHT_P,
].contains(true);
case 2: case 2:
return [ return PlayerSettings.player2.controls.NOTE_LEFT_P
PlayerSettings.player2.controls.NOTE_LEFT_P, || PlayerSettings.player2.controls.NOTE_DOWN_P
PlayerSettings.player2.controls.NOTE_DOWN_P, || PlayerSettings.player2.controls.NOTE_UP_P
PlayerSettings.player2.controls.NOTE_UP_P, || PlayerSettings.player2.controls.NOTE_RIGHT_P;
PlayerSettings.player2.controls.NOTE_RIGHT_P,
].contains(true);
} }
return false; return false;
} }
@ -469,19 +466,15 @@ class BaseCharacter extends Bopper
switch (player) switch (player)
{ {
case 1: case 1:
return [ return PlayerSettings.player1.controls.NOTE_LEFT
PlayerSettings.player1.controls.NOTE_LEFT, || PlayerSettings.player1.controls.NOTE_DOWN
PlayerSettings.player1.controls.NOTE_DOWN, || PlayerSettings.player1.controls.NOTE_UP
PlayerSettings.player1.controls.NOTE_UP, || PlayerSettings.player1.controls.NOTE_RIGHT;
PlayerSettings.player1.controls.NOTE_RIGHT,
].contains(true);
case 2: case 2:
return [ return PlayerSettings.player2.controls.NOTE_LEFT
PlayerSettings.player2.controls.NOTE_LEFT, || PlayerSettings.player2.controls.NOTE_DOWN
PlayerSettings.player2.controls.NOTE_DOWN, || PlayerSettings.player2.controls.NOTE_UP
PlayerSettings.player2.controls.NOTE_UP, || PlayerSettings.player2.controls.NOTE_RIGHT;
PlayerSettings.player2.controls.NOTE_RIGHT,
].contains(true);
} }
return false; return false;
} }

View file

@ -0,0 +1,137 @@
package funkin.play.components;
import funkin.graphics.FunkinSprite;
import funkin.graphics.shaders.PureColor;
import flixel.FlxSprite;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import flixel.math.FlxMath;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.text.FlxText.FlxTextAlign;
import funkin.util.MathUtil;
import flixel.util.FlxColor;
/**
* Numerical counters used to display the clear percent.
*/
class ClearPercentCounter extends FlxTypedSpriteGroup<FlxSprite>
{
public var curNumber(default, set):Int = 0;
var numberChanged:Bool = false;
function set_curNumber(val:Int):Int
{
numberChanged = true;
return curNumber = val;
}
var small:Bool = false;
var flashShader:PureColor;
public function new(x:Float, y:Float, startingNumber:Int = 0, small:Bool = false)
{
super(x, y);
flashShader = new PureColor(FlxColor.WHITE);
flashShader.colorSet = true;
curNumber = startingNumber;
this.small = small;
var clearPercentText:FunkinSprite = FunkinSprite.create(0, 0, 'resultScreen/clearPercent/clearPercentText${small ? 'Small' : ''}');
clearPercentText.x = small ? 40 : 0;
add(clearPercentText);
drawNumbers();
}
/**
* Make the counter flash turn white or stop being all white.
* @param enabled Whether the counter should be white.
*/
public function flash(enabled:Bool):Void
{
for (member in members)
{
member.shader = enabled ? flashShader : null;
}
}
var tmr:Float = 0;
override function update(elapsed:Float)
{
super.update(elapsed);
if (numberChanged) drawNumbers();
}
function drawNumbers()
{
var seperatedScore:Array<Int> = [];
var tempCombo:Int = Math.round(curNumber);
while (tempCombo != 0)
{
seperatedScore.push(tempCombo % 10);
tempCombo = Math.floor(tempCombo / 10);
}
if (seperatedScore.length == 0) seperatedScore.push(0);
seperatedScore.reverse();
for (ind => num in seperatedScore)
{
var digitIndex = ind + 1;
// If there's only one digit, move it to the right
// If there's three digits, move them all to the left
var digitOffset = (seperatedScore.length == 1) ? 1 : (seperatedScore.length == 3) ? -1 : 0;
var digitSize = small ? 32 : 72;
var digitHeightOffset = small ? -4 : 0;
var xPos = (digitIndex - 1 + digitOffset) * (digitSize * this.scale.x);
xPos += small ? -24 : 0;
var yPos = (digitIndex - 1 + digitOffset) * (digitHeightOffset * this.scale.y);
yPos += small ? 0 : 72;
if (digitIndex >= members.length)
{
// Three digits = LLR because the 1 and 0 won't be the same anyway.
var variant:Bool = (seperatedScore.length == 3) ? (digitIndex >= 2) : (digitIndex >= 1);
// var variant:Bool = (seperatedScore.length % 2 != 0) ? (digitIndex % 2 == 0) : (digitIndex % 2 == 1);
var numb:ClearPercentNumber = new ClearPercentNumber(xPos, yPos, num, variant, this.small);
numb.scale.set(this.scale.x, this.scale.y);
add(numb);
}
else
{
members[digitIndex].animation.play(Std.string(num));
// Reset the position of the number
members[digitIndex].x = xPos + this.x;
members[digitIndex].y = yPos + this.y;
}
}
}
}
class ClearPercentNumber extends FlxSprite
{
public function new(x:Float, y:Float, digit:Int, variant:Bool, small:Bool)
{
super(x, y);
frames = Paths.getSparrowAtlas('resultScreen/clearPercent/clearPercentNumber${small ? 'Small' : variant ? 'Right' : 'Left'}');
for (i in 0...10)
{
animation.addByPrefix('$i', 'number $i 0', 24, false);
}
animation.play('$digit');
updateHitbox();
}
}

View file

@ -24,7 +24,7 @@ import funkin.util.MathUtil;
* - i.e. `PlayState.instance.iconP1.playAnimation("losing")` * - i.e. `PlayState.instance.iconP1.playAnimation("losing")`
* - Scripts can also utilize all functionality that a normal FlxSprite would have access to, such as adding supplimental animations. * - Scripts can also utilize all functionality that a normal FlxSprite would have access to, such as adding supplimental animations.
* - i.e. `PlayState.instance.iconP1.animation.addByPrefix("jumpscare", "jumpscare", 24, false);` * - i.e. `PlayState.instance.iconP1.animation.addByPrefix("jumpscare", "jumpscare", 24, false);`
* @author MasterEric * @author EliteMasterEric
*/ */
@:nullSafety @:nullSafety
class HealthIcon extends FunkinSprite class HealthIcon extends FunkinSprite

View file

@ -399,6 +399,27 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
return null; return null;
} }
/**
* Given that this character is selected in the Freeplay menu,
* which variations should be available?
* @param charId The character ID to query.
* @return An array of available variations.
*/
public function getVariationsByCharId(?charId:String):Array<String>
{
if (charId == null) charId = Constants.DEFAULT_CHARACTER;
if (variations.contains(charId))
{
return [charId];
}
else
{
// TODO: How to exclude character variations while keeping other custom variations?
return variations;
}
}
/** /**
* List all the difficulties in this song. * List all the difficulties in this song.
* *

View file

@ -14,8 +14,7 @@ import funkin.util.SerializerUtil;
@:nullSafety @:nullSafety
class Save class Save
{ {
// Version 2.0.2 adds attributes to `optionsChartEditor`, that should return default values if they are null. public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.4";
public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.3";
public static final SAVE_DATA_VERSION_RULE:thx.semver.VersionRule = "2.0.x"; public static final SAVE_DATA_VERSION_RULE:thx.semver.VersionRule = "2.0.x";
// We load this version's saves from a new save path, to maintain SOME level of backwards compatibility. // We load this version's saves from a new save path, to maintain SOME level of backwards compatibility.
@ -53,7 +52,8 @@ class Save
public function new(?data:RawSaveData) public function new(?data:RawSaveData)
{ {
if (data == null) this.data = Save.getDefault(); if (data == null) this.data = Save.getDefault();
else this.data = data; else
this.data = data;
} }
public static function getDefault():RawSaveData public static function getDefault():RawSaveData
@ -77,6 +77,9 @@ class Save
levels: [], levels: [],
songs: [], songs: [],
}, },
favoriteSongs: [],
options: options:
{ {
// Reasonable defaults. // Reasonable defaults.
@ -554,6 +557,35 @@ class Save
return false; return false;
} }
public function isSongFavorited(id:String):Bool
{
if (data.favoriteSongs == null)
{
data.favoriteSongs = [];
flush();
};
return data.favoriteSongs.contains(id);
}
public function favoriteSong(id:String):Void
{
if (!isSongFavorited(id))
{
data.favoriteSongs.push(id);
flush();
}
}
public function unfavoriteSong(id:String):Void
{
if (isSongFavorited(id))
{
data.favoriteSongs.remove(id);
flush();
}
}
public function getControls(playerId:Int, inputType:Device):Null<SaveControlsData> public function getControls(playerId:Int, inputType:Device):Null<SaveControlsData>
{ {
switch (inputType) switch (inputType)
@ -740,6 +772,12 @@ typedef RawSaveData =
*/ */
var options:SaveDataOptions; var options:SaveDataOptions;
/**
* The user's favorited songs in the Freeplay menu,
* as a list of song IDs.
*/
var favoriteSongs:Array<String>;
var mods:SaveDataMods; var mods:SaveDataMods;
/** /**
@ -809,11 +847,6 @@ typedef SaveScoreData =
* The count of each judgement hit. * The count of each judgement hit.
*/ */
var tallies:SaveScoreTallyData; var tallies:SaveScoreTallyData;
/**
* The accuracy percentage.
*/
var accuracy:Float;
} }
typedef SaveScoreTallyData = typedef SaveScoreTallyData =

View file

@ -5,6 +5,9 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2.0.4] - 2024-05-21
### Added
- `favoriteSongs:Array<String>` to `Save`
## [2.0.3] - 2024-01-09 ## [2.0.3] - 2024-01-09
### Added ### Added

View file

@ -118,7 +118,7 @@ class SaveDataMigrator
var scoreDataEasy:SaveScoreData = var scoreDataEasy:SaveScoreData =
{ {
score: inputSaveData.songScores.get('${levelId}-easy') ?? 0, score: inputSaveData.songScores.get('${levelId}-easy') ?? 0,
accuracy: inputSaveData.songCompletion.get('${levelId}-easy') ?? 0.0, // accuracy: inputSaveData.songCompletion.get('${levelId}-easy') ?? 0.0,
tallies: tallies:
{ {
sick: 0, sick: 0,
@ -137,7 +137,7 @@ class SaveDataMigrator
var scoreDataNormal:SaveScoreData = var scoreDataNormal:SaveScoreData =
{ {
score: inputSaveData.songScores.get('${levelId}') ?? 0, score: inputSaveData.songScores.get('${levelId}') ?? 0,
accuracy: inputSaveData.songCompletion.get('${levelId}') ?? 0.0, // accuracy: inputSaveData.songCompletion.get('${levelId}') ?? 0.0,
tallies: tallies:
{ {
sick: 0, sick: 0,
@ -156,7 +156,7 @@ class SaveDataMigrator
var scoreDataHard:SaveScoreData = var scoreDataHard:SaveScoreData =
{ {
score: inputSaveData.songScores.get('${levelId}-hard') ?? 0, score: inputSaveData.songScores.get('${levelId}-hard') ?? 0,
accuracy: inputSaveData.songCompletion.get('${levelId}-hard') ?? 0.0, // accuracy: inputSaveData.songCompletion.get('${levelId}-hard') ?? 0.0,
tallies: tallies:
{ {
sick: 0, sick: 0,
@ -178,7 +178,6 @@ class SaveDataMigrator
var scoreDataEasy:SaveScoreData = var scoreDataEasy:SaveScoreData =
{ {
score: 0, score: 0,
accuracy: 0,
tallies: tallies:
{ {
sick: 0, sick: 0,
@ -196,14 +195,13 @@ class SaveDataMigrator
for (songId in songIds) for (songId in songIds)
{ {
scoreDataEasy.score = Std.int(Math.max(scoreDataEasy.score, inputSaveData.songScores.get('${songId}-easy') ?? 0)); scoreDataEasy.score = Std.int(Math.max(scoreDataEasy.score, inputSaveData.songScores.get('${songId}-easy') ?? 0));
scoreDataEasy.accuracy = Math.max(scoreDataEasy.accuracy, inputSaveData.songCompletion.get('${songId}-easy') ?? 0.0); // scoreDataEasy.accuracy = Math.max(scoreDataEasy.accuracy, inputSaveData.songCompletion.get('${songId}-easy') ?? 0.0);
} }
result.setSongScore(songIds[0], 'easy', scoreDataEasy); result.setSongScore(songIds[0], 'easy', scoreDataEasy);
var scoreDataNormal:SaveScoreData = var scoreDataNormal:SaveScoreData =
{ {
score: 0, score: 0,
accuracy: 0,
tallies: tallies:
{ {
sick: 0, sick: 0,
@ -221,14 +219,13 @@ class SaveDataMigrator
for (songId in songIds) for (songId in songIds)
{ {
scoreDataNormal.score = Std.int(Math.max(scoreDataNormal.score, inputSaveData.songScores.get('${songId}') ?? 0)); scoreDataNormal.score = Std.int(Math.max(scoreDataNormal.score, inputSaveData.songScores.get('${songId}') ?? 0));
scoreDataNormal.accuracy = Math.max(scoreDataNormal.accuracy, inputSaveData.songCompletion.get('${songId}') ?? 0.0); // scoreDataNormal.accuracy = Math.max(scoreDataNormal.accuracy, inputSaveData.songCompletion.get('${songId}') ?? 0.0);
} }
result.setSongScore(songIds[0], 'normal', scoreDataNormal); result.setSongScore(songIds[0], 'normal', scoreDataNormal);
var scoreDataHard:SaveScoreData = var scoreDataHard:SaveScoreData =
{ {
score: 0, score: 0,
accuracy: 0,
tallies: tallies:
{ {
sick: 0, sick: 0,
@ -246,7 +243,7 @@ class SaveDataMigrator
for (songId in songIds) for (songId in songIds)
{ {
scoreDataHard.score = Std.int(Math.max(scoreDataHard.score, inputSaveData.songScores.get('${songId}-hard') ?? 0)); scoreDataHard.score = Std.int(Math.max(scoreDataHard.score, inputSaveData.songScores.get('${songId}-hard') ?? 0));
scoreDataHard.accuracy = Math.max(scoreDataHard.accuracy, inputSaveData.songCompletion.get('${songId}-hard') ?? 0.0); // scoreDataHard.accuracy = Math.max(scoreDataHard.accuracy, inputSaveData.songCompletion.get('${songId}-hard') ?? 0.0);
} }
result.setSongScore(songIds[0], 'hard', scoreDataHard); result.setSongScore(songIds[0], 'hard', scoreDataHard);
} }

View file

@ -137,7 +137,7 @@ using Lambda;
* *
* Some functionality is split into handler classes to help maintain my sanity. * Some functionality is split into handler classes to help maintain my sanity.
* *
* @author MasterEric * @author EliteMasterEric
*/ */
// @:nullSafety // @:nullSafety

View file

@ -38,7 +38,7 @@ class AlbumRoll extends FlxSpriteGroup
var newAlbumArt:FlxAtlasSprite; var newAlbumArt:FlxAtlasSprite;
// var difficultyStars:DifficultyStars; var difficultyStars:DifficultyStars;
var _exitMovers:Null<FreeplayState.ExitMoverData>; var _exitMovers:Null<FreeplayState.ExitMoverData>;
var albumData:Album; var albumData:Album;
@ -65,9 +65,9 @@ class AlbumRoll extends FlxSpriteGroup
add(newAlbumArt); add(newAlbumArt);
// difficultyStars = new DifficultyStars(140, 39); difficultyStars = new DifficultyStars(140, 39);
// difficultyStars.stars.visible = false; difficultyStars.stars.visible = false;
// add(difficultyStars); add(difficultyStars);
} }
function onAlbumFinish(animName:String):Void function onAlbumFinish(animName:String):Void
@ -86,9 +86,14 @@ class AlbumRoll extends FlxSpriteGroup
{ {
if (albumId == null) if (albumId == null)
{ {
// difficultyStars.stars.visible = false; this.visible = false;
difficultyStars.stars.visible = false;
return; return;
} }
else
{
this.visible = true;
}
albumData = AlbumRegistry.instance.fetchEntry(albumId); albumData = AlbumRegistry.instance.fetchEntry(albumId);
@ -144,10 +149,10 @@ class AlbumRoll extends FlxSpriteGroup
newAlbumArt.visible = true; newAlbumArt.visible = true;
newAlbumArt.playAnimation(animNames.get('$albumId-active'), false, false, false); newAlbumArt.playAnimation(animNames.get('$albumId-active'), false, false, false);
// difficultyStars.stars.visible = false; difficultyStars.stars.visible = false;
new FlxTimer().start(0.75, function(_) { new FlxTimer().start(0.75, function(_) {
// showTitle(); // showTitle();
// showStars(); showStars();
}); });
} }
@ -156,16 +161,17 @@ class AlbumRoll extends FlxSpriteGroup
newAlbumArt.playAnimation(animNames.get('$albumId-trans'), false, false, false); newAlbumArt.playAnimation(animNames.get('$albumId-trans'), false, false, false);
} }
// public function setDifficultyStars(?difficulty:Int):Void public function setDifficultyStars(?difficulty:Int):Void
// { {
// if (difficulty == null) return; if (difficulty == null) return;
// difficultyStars.difficulty = difficulty; difficultyStars.difficulty = difficulty;
// } }
// /**
// * Make the album stars visible. /**
// */ * Make the album stars visible.
// public function showStars():Void */
// { public function showStars():Void
// difficultyStars.stars.visible = false; // true; {
// } difficultyStars.stars.visible = true; // true;
}
} }

View file

@ -0,0 +1,106 @@
package funkin.ui.freeplay;
import flixel.group.FlxSpriteGroup;
import funkin.graphics.adobeanimate.FlxAtlasSprite;
import funkin.graphics.shaders.HSVShader;
class DifficultyStars extends FlxSpriteGroup
{
/**
* Internal handler var for difficulty... ranges from 0... to 15
* 0 is 1 star... 15 is 0 stars!
*/
var curDifficulty(default, set):Int = 0;
/**
* Range between 0 and 15
*/
public var difficulty(default, set):Int = 1;
public var stars:FlxAtlasSprite;
var flames:FreeplayFlames;
var hsvShader:HSVShader;
public function new(x:Float, y:Float)
{
super(x, y);
hsvShader = new HSVShader();
flames = new FreeplayFlames(0, 0);
add(flames);
stars = new FlxAtlasSprite(0, 0, Paths.animateAtlas("freeplay/freeplayStars"));
stars.anim.play("diff stars");
add(stars);
stars.shader = hsvShader;
for (memb in flames.members)
memb.shader = hsvShader;
}
override function update(elapsed:Float):Void
{
super.update(elapsed);
// "loops" the current animation
// for clarity, the animation file looks like
// frame : stars
// 0-99: 1 star
// 100-199: 2 stars
// ......
// 1300-1499: 15 stars
// 1500 : 0 stars
if (curDifficulty < 15 && stars.anim.curFrame >= (curDifficulty + 1) * 100)
{
stars.anim.play("diff stars", true, false, curDifficulty * 100);
}
}
function set_difficulty(value:Int):Int
{
difficulty = value;
if (difficulty <= 0)
{
difficulty = 0;
curDifficulty = 15;
}
else if (difficulty <= 15)
{
difficulty = value;
curDifficulty = difficulty - 1;
}
else
{
difficulty = 15;
curDifficulty = difficulty - 1;
}
if (difficulty > 10) flames.flameCount = difficulty - 10;
else
flames.flameCount = 0;
return difficulty;
}
function set_curDifficulty(value:Int):Int
{
curDifficulty = value;
if (curDifficulty == 15)
{
stars.anim.play("diff stars", true, false, 1500);
stars.anim.pause();
}
else
{
stars.anim.curFrame = Std.int(curDifficulty * 100);
stars.anim.play("diff stars", true, false, curDifficulty * 100);
}
return curDifficulty;
}
}

View file

@ -50,8 +50,19 @@ class FreeplayFlames extends FlxSpriteGroup
} }
} }
var timers:Array<FlxTimer> = [];
function set_flameCount(value:Int):Int function set_flameCount(value:Int):Int
{ {
// Stop all existing timers.
// This fixes a bug where quickly switching difficulties would show flames.
for (timer in timers)
{
timer.active = false;
timer.destroy();
timers.remove(timer);
}
this.flameCount = value; this.flameCount = value;
var visibleCount:Int = 0; var visibleCount:Int = 0;
for (i in 0...5) for (i in 0...5)
@ -62,10 +73,18 @@ class FreeplayFlames extends FlxSpriteGroup
{ {
if (!flame.visible) if (!flame.visible)
{ {
new FlxTimer().start(flameTimer * visibleCount, function(_) { var nextTimer:FlxTimer = new FlxTimer().start(flameTimer * visibleCount, function(currentTimer:FlxTimer) {
if (i >= this.flameCount)
{
trace('EARLY EXIT');
return;
}
timers.remove(currentTimer);
flame.animation.play("flame", true); flame.animation.play("flame", true);
flame.visible = true; flame.visible = true;
}); });
timers.push(nextTimer);
visibleCount++; visibleCount++;
} }
} }

View file

@ -120,8 +120,6 @@ class FreeplayState extends MusicBeatSubState
var curCapsule:SongMenuItem; var curCapsule:SongMenuItem;
var curPlaying:Bool = false; var curPlaying:Bool = false;
var displayedVariations:Array<String>;
var dj:DJBoyfriend; var dj:DJBoyfriend;
var ostName:FlxText; var ostName:FlxText;
@ -184,10 +182,6 @@ class FreeplayState extends MusicBeatSubState
// Add a null entry that represents the RANDOM option // Add a null entry that represents the RANDOM option
songs.push(null); songs.push(null);
// TODO: This makes custom variations disappear from Freeplay. Figure out a better solution later.
// Default character (BF) shows default and Erect variations. Pico shows only Pico variations.
displayedVariations = (currentCharacter == 'bf') ? [Constants.DEFAULT_VARIATION, 'erect'] : [currentCharacter];
// programmatically adds the songs via LevelRegistry and SongRegistry // programmatically adds the songs via LevelRegistry and SongRegistry
for (levelId in LevelRegistry.instance.listSortedLevelIds()) for (levelId in LevelRegistry.instance.listSortedLevelIds())
{ {
@ -195,7 +189,8 @@ class FreeplayState extends MusicBeatSubState
{ {
var song:Song = SongRegistry.instance.fetchEntry(songId); var song:Song = SongRegistry.instance.fetchEntry(songId);
// Only display songs which actually have available charts for the current character. // Only display songs which actually have available difficulties for the current character.
var displayedVariations = song.getVariationsByCharId(currentCharacter);
var availableDifficultiesForSong:Array<String> = song.listDifficulties(displayedVariations, false); var availableDifficultiesForSong:Array<String> = song.listDifficulties(displayedVariations, false);
if (availableDifficultiesForSong.length == 0) continue; if (availableDifficultiesForSong.length == 0) continue;
@ -488,10 +483,6 @@ class FreeplayState extends MusicBeatSubState
albumRoll.playIntro(); albumRoll.playIntro();
new FlxTimer().start(0.75, function(_) {
// albumRoll.showTitle();
});
FlxTween.tween(grpDifficulties, {x: 90}, 0.6, {ease: FlxEase.quartOut}); FlxTween.tween(grpDifficulties, {x: 90}, 0.6, {ease: FlxEase.quartOut});
diffSelLeft.visible = true; diffSelLeft.visible = true;
@ -708,8 +699,8 @@ class FreeplayState extends MusicBeatSubState
if (targetSong != null) if (targetSong != null)
{ {
var realShit:Int = curSelected; var realShit:Int = curSelected;
targetSong.isFav = !targetSong.isFav; var isFav = targetSong.toggleFavorite();
if (targetSong.isFav) if (isFav)
{ {
FlxTween.tween(grpCapsules.members[realShit], {angle: 360}, 0.4, FlxTween.tween(grpCapsules.members[realShit], {angle: 360}, 0.4,
{ {
@ -733,8 +724,8 @@ class FreeplayState extends MusicBeatSubState
} }
} }
lerpScore = MathUtil.coolLerp(lerpScore, intendedScore, 0.2); lerpScore = MathUtil.smoothLerp(lerpScore, intendedScore, elapsed, 0.5);
lerpCompletion = MathUtil.coolLerp(lerpCompletion, intendedCompletion, 0.9); lerpCompletion = MathUtil.smoothLerp(lerpCompletion, intendedCompletion, elapsed, 0.5);
if (Math.isNaN(lerpScore)) if (Math.isNaN(lerpScore))
{ {
@ -889,11 +880,24 @@ class FreeplayState extends MusicBeatSubState
spamTimer = 0; spamTimer = 0;
} }
#if !html5
if (FlxG.mouse.wheel != 0) if (FlxG.mouse.wheel != 0)
{ {
dj.resetAFKTimer(); dj.resetAFKTimer();
changeSelection(-Math.round(FlxG.mouse.wheel / 4)); changeSelection(-Math.round(FlxG.mouse.wheel));
} }
#else
if (FlxG.mouse.wheel < 0)
{
dj.resetAFKTimer();
changeSelection(-Math.round(FlxG.mouse.wheel / 8));
}
else if (FlxG.mouse.wheel > 0)
{
dj.resetAFKTimer();
changeSelection(-Math.round(FlxG.mouse.wheel / 8));
}
#end
if (controls.UI_LEFT_P && !FlxG.keys.pressed.CONTROL) if (controls.UI_LEFT_P && !FlxG.keys.pressed.CONTROL)
{ {
@ -910,6 +914,7 @@ class FreeplayState extends MusicBeatSubState
if (controls.BACK) if (controls.BACK)
{ {
busy = true;
FlxTween.globalManager.clear(); FlxTween.globalManager.clear();
FlxTimer.globalManager.clear(); FlxTimer.globalManager.clear();
dj.onIntroDone.removeAll(); dj.onIntroDone.removeAll();
@ -1007,7 +1012,7 @@ class FreeplayState extends MusicBeatSubState
{ {
var songScore:SaveScoreData = Save.instance.getSongScore(grpCapsules.members[curSelected].songData.songId, currentDifficulty); var songScore:SaveScoreData = Save.instance.getSongScore(grpCapsules.members[curSelected].songData.songId, currentDifficulty);
intendedScore = songScore?.score ?? 0; intendedScore = songScore?.score ?? 0;
intendedCompletion = songScore?.accuracy ?? 0.0; intendedCompletion = songScore == null ? 0.0 : ((songScore.tallies.sick + songScore.tallies.good) / songScore.tallies.totalNotes);
rememberedDifficulty = currentDifficulty; rememberedDifficulty = currentDifficulty;
} }
else else
@ -1072,6 +1077,9 @@ class FreeplayState extends MusicBeatSubState
albumRoll.albumId = newAlbumId; albumRoll.albumId = newAlbumId;
albumRoll.skipIntro(); albumRoll.skipIntro();
} }
// Set difficulty star count.
albumRoll.setDifficultyStars(daSong?.difficultyRating);
} }
// Clears the cache of songs, frees up memory, they' ll have to be loaded in later tho function clearDaCache(actualSongTho:String) // Clears the cache of songs, frees up memory, they' ll have to be loaded in later tho function clearDaCache(actualSongTho:String)
@ -1202,7 +1210,7 @@ class FreeplayState extends MusicBeatSubState
{ {
var songScore:SaveScoreData = Save.instance.getSongScore(daSongCapsule.songData.songId, currentDifficulty); var songScore:SaveScoreData = Save.instance.getSongScore(daSongCapsule.songData.songId, currentDifficulty);
intendedScore = songScore?.score ?? 0; intendedScore = songScore?.score ?? 0;
intendedCompletion = songScore?.accuracy ?? 0.0; intendedCompletion = songScore == null ? 0.0 : ((songScore.tallies.sick + songScore.tallies.good) / songScore.tallies.totalNotes);
diffIdsCurrent = daSongCapsule.songData.songDifficulties; diffIdsCurrent = daSongCapsule.songData.songDifficulties;
rememberedSongId = daSongCapsule.songData.songId; rememberedSongId = daSongCapsule.songData.songId;
changeDiff(); changeDiff();
@ -1383,11 +1391,12 @@ class FreeplaySongData
public var songName(default, null):String = ''; public var songName(default, null):String = '';
public var songCharacter(default, null):String = ''; public var songCharacter(default, null):String = '';
public var songRating(default, null):Int = 0; public var difficultyRating(default, null):Int = 0;
public var albumId(default, null):Null<String> = null; public var albumId(default, null):Null<String> = null;
public var currentDifficulty(default, set):String = Constants.DEFAULT_DIFFICULTY; public var currentDifficulty(default, set):String = Constants.DEFAULT_DIFFICULTY;
public var displayedVariations(default, null):Array<String> = [Constants.DEFAULT_VARIATION];
var displayedVariations:Array<String> = [Constants.DEFAULT_VARIATION];
function set_currentDifficulty(value:String):String function set_currentDifficulty(value:String):String
{ {
@ -1403,11 +1412,32 @@ class FreeplaySongData
this.levelId = levelId; this.levelId = levelId;
this.songId = songId; this.songId = songId;
this.song = song; this.song = song;
this.isFav = Save.instance.isSongFavorited(songId);
if (displayedVariations != null) this.displayedVariations = displayedVariations; if (displayedVariations != null) this.displayedVariations = displayedVariations;
updateValues(displayedVariations); updateValues(displayedVariations);
} }
/**
* Toggle whether or not the song is favorited, then flush to save data.
* @return Whether or not the song is now favorited.
*/
public function toggleFavorite():Bool
{
isFav = !isFav;
if (isFav)
{
Save.instance.favoriteSong(this.songId);
}
else
{
Save.instance.unfavoriteSong(this.songId);
}
return isFav;
}
function updateValues(variations:Array<String>):Void function updateValues(variations:Array<String>):Void
{ {
this.songDifficulties = song.listDifficulties(variations, false, false); this.songDifficulties = song.listDifficulties(variations, false, false);
@ -1417,7 +1447,7 @@ class FreeplaySongData
if (songDifficulty == null) return; if (songDifficulty == null) return;
this.songName = songDifficulty.songName; this.songName = songDifficulty.songName;
this.songCharacter = songDifficulty.characters.opponent; this.songCharacter = songDifficulty.characters.opponent;
this.songRating = songDifficulty.difficultyRating; this.difficultyRating = songDifficulty.difficultyRating;
if (songDifficulty.album == null) if (songDifficulty.album == null)
{ {
FlxG.log.warn('No album for: ${songDifficulty.songName}'); FlxG.log.warn('No album for: ${songDifficulty.songName}');

View file

@ -168,7 +168,7 @@ class SongMenuItem extends FlxSpriteGroup
songText.text = songData?.songName ?? 'Random'; songText.text = songData?.songName ?? 'Random';
// Update capsule character. // Update capsule character.
if (songData?.songCharacter != null) setCharacter(songData.songCharacter); if (songData?.songCharacter != null) setCharacter(songData.songCharacter);
updateDifficultyRating(songData?.songRating ?? 0); updateDifficultyRating(songData?.difficultyRating ?? 0);
// Update opacity, offsets, etc. // Update opacity, offsets, etc.
updateSelected(); updateSelected();
} }

View file

@ -351,8 +351,7 @@ class MainMenuState extends MusicBeatState
maxCombo: 0, maxCombo: 0,
totalNotesHit: 0, totalNotesHit: 0,
totalNotes: 0, totalNotes: 0,
}, }
accuracy: 0,
}); });
} }
#end #end

View file

@ -89,7 +89,7 @@ class AttractState extends MusicBeatState
super.update(elapsed); super.update(elapsed);
// If the user presses any button, skip the video. // If the user presses any button, skip the video.
if (FlxG.keys.justPressed.ANY) if (FlxG.keys.justPressed.ANY && !controls.VOLUME_MUTE && !controls.VOLUME_UP && !controls.VOLUME_DOWN)
{ {
onAttractEnd(); onAttractEnd();
} }

View file

@ -162,7 +162,9 @@ class LoadingState extends MusicBeatSubState
{ {
targetShit = FlxMath.remapToRange(callbacks.numRemaining / callbacks.length, 1, 0, 0, 1); targetShit = FlxMath.remapToRange(callbacks.numRemaining / callbacks.length, 1, 0, 0, 1);
loadBar.scale.x = FlxMath.lerp(loadBar.scale.x, targetShit, 0.50); var lerpWidth:Int = Std.int(FlxMath.lerp(loadBar.width, FlxG.width * targetShit, 0.2));
loadBar.setGraphicSize(lerpWidth, loadBar.height);
loadBar.updateHitbox();
FlxG.watch.addQuick('percentage?', callbacks.numRemaining / callbacks.length); FlxG.watch.addQuick('percentage?', callbacks.numRemaining / callbacks.length);
} }

View file

@ -455,6 +455,17 @@ class Constants
public static final JUDGEMENT_BAD_COMBO_BREAK:Bool = true; public static final JUDGEMENT_BAD_COMBO_BREAK:Bool = true;
public static final JUDGEMENT_SHIT_COMBO_BREAK:Bool = true; public static final JUDGEMENT_SHIT_COMBO_BREAK:Bool = true;
// % Sick
public static final RANK_PERFECT_PLAT_THRESHOLD:Float = 1.0; // % Sick
public static final RANK_PERFECT_GOLD_THRESHOLD:Float = 0.85; // % Sick
// % Hit
public static final RANK_PERFECT_THRESHOLD:Float = 1.00;
public static final RANK_EXCELLENT_THRESHOLD:Float = 0.90;
public static final RANK_GREAT_THRESHOLD:Float = 0.75;
public static final RANK_GOOD_THRESHOLD:Float = 0.60;
// public static final RANK_SHIT_THRESHOLD:Float = 0.00;
/** /**
* FILE EXTENSIONS * FILE EXTENSIONS
*/ */

View file

@ -141,7 +141,9 @@ class CrashHandler
fullContents += '\n'; fullContents += '\n';
fullContents += 'Flixel Current State: ${Type.getClassName(Type.getClass(FlxG.state))}\n'; var currentState = FlxG.state != null ? Type.getClassName(Type.getClass(FlxG.state)) : 'No state loaded';
fullContents += 'Flixel Current State: ${currentState}\n';
fullContents += '\n'; fullContents += '\n';

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

View file

@ -1,27 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<TextureAtlas imagePath="arrows.png">
<SubTexture name="staticLeft0001" x="0" y="0" width="17" height="17" />
<SubTexture name="staticDown0001" x="17" y="0" width="17" height="17" />
<SubTexture name="staticUp0001" x="34" y="0" width="17" height="17" />
<SubTexture name="staticRight0001" x="51" y="0" width="17" height="17" />
<SubTexture name="noteLeft0001" x="0" y="17" width="17" height="17" />
<SubTexture name="noteDown0001" x="17" y="17" width="17" height="17" />
<SubTexture name="noteUp0001" x="34" y="17" width="17" height="17" />
<SubTexture name="noteRight0001" x="51" y="17" width="17" height="17" />
<SubTexture name="pressedLeft0001" x="0" y="17" width="17" height="17" />
<SubTexture name="pressedDown0001" x="17" y="17" width="17" height="17" />
<SubTexture name="pressedUp0001" x="34" y="17" width="17" height="17" />
<SubTexture name="pressedRight0001" x="51" y="17" width="17" height="17" />
<SubTexture name="pressedLeft0002" x="0" y="34" width="17" height="17" />
<SubTexture name="pressedDown0002" x="17" y="34" width="17" height="17" />
<SubTexture name="pressedUp0002" x="34" y="34" width="17" height="17" />
<SubTexture name="pressedRight0002" x="51" y="34" width="17" height="17" />
<SubTexture name="confirmLeft0001" x="0" y="51" width="17" height="17" />
<SubTexture name="confirmDown0001" x="17" y="51" width="17" height="17" />
<SubTexture name="confirmUp0001" x="34" y="51" width="17" height="17" />
<SubTexture name="confirmRight0001" x="51" y="51" width="17" height="17" />
<SubTexture name="confirmLeft0002" x="0" y="68" width="17" height="17" />
<SubTexture name="confirmDown0002" x="17" y="68" width="17" height="17" />
<SubTexture name="confirmUp0002" x="34" y="68" width="17" height="17" />
<SubTexture name="confirmRight0002" x="51" y="68" width="17" height="17" />
</TextureAtlas>