Merge branch 'rewrite/master' into feature/results-clear-rank

This commit is contained in:
Cameron Taylor 2024-05-28 00:56:18 -04:00
commit 827ad61602
25 changed files with 225 additions and 112 deletions

View file

@ -5,7 +5,7 @@ The Friday Night Funkin' source code is licensed under the Apache 2.0 license: (
Friday Night Funkin' Copyright 2020-2024 The Funkin' Crew Inc. Friday Night Funkin' Copyright 2020-2024 The Funkin' Crew Inc.
All Rights Reserved. "Friday Night Funkin'" and the "Friday Night Funkin'" logo are trademarks of The Funkin' Crew Inc. All Rights Reserved. "Friday Night Funkin'" and the "Friday Night Funkin'" logo are trademarks of The Funkin' Crew Inc.
You can view the `funkin-assets` license here: (https://github.com/FunkinCrew/funkin-assets/blob/main/LICENSE.md) You can view the `funkin-assets` license here: (https://github.com/FunkinCrew/funkin.assets/blob/main/LICENSE.md)
## Apache 2.0 License ## Apache 2.0 License

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,19 +1,17 @@
# Friday Night Funkin' &middot; [![GitHub license](https://img.shields.io/badge/license-Modified%20Apache%20V2-blue.svg)](https://github.com/ninjamuffin99/Funkin/blob/master/LICENSE.md) ![Repo size](https://img.shields.io/github/repo-size/ninjamuffin99/Funkin) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/ninjamuffin99/Funkin/pulls) # Friday Night Funkin'
Friday Night Funkin' is a rhythm game that doubles as a playable cartoon. 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.
## [Play for free on Newgrounds!](https://www.newgrounds.com/portal/view/770371) - [Playable web demo on Newgrounds!](https://www.newgrounds.com/portal/view/770371)
## [Download builds for Windows, Mac, and Linux from Itch.io!](https://ninja-muffin24.itch.io/funkin) - [Demo download builds for Windows, Mac, and Linux from Itch.io!](https://ninja-muffin24.itch.io/funkin)
![Friday Night Funkin' Logo](./art/thumbnailNewer.png)
# Getting Started # Getting Started
**PLEASE USE THE LINKS ABOVE IF YOU JUST WANT TO PLAY THE GAME** **PLEASE USE THE LINKS ABOVE IF YOU JUST WANT TO PLAY THE GAME**
To learn how to install the necessary dependencies and compile the game from source, please check out our [building the game](/docs/compiling.md) guide. To learn how to install the necessary dependencies and compile the game from source, please check out our [building the game](/docs/COMPILING.md) guide.
# Contributing # Contributing
@ -21,6 +19,8 @@ Please check out our [Contributor's guide](./CONTRIBUTORS.md) on how you can act
# Credits and Special Thanks # Credits and Special Thanks
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
- [EliteMasterEric](https://twitter.com/EliteMasterEric) - Programmer - [EliteMasterEric](https://twitter.com/EliteMasterEric) - Programmer

2
art

@ -1 +1 @@
Subproject commit f72947b65fe0555821f827dccd562f01d308486d Subproject commit 66572f85d826ce2ec1d45468c12733b161237ffa

2
assets

@ -1 +1 @@
Subproject commit 2a57e34061f6034236663851332319c5dedab259 Subproject commit 8a8239cb50b5277fb0cfce041b3d8a9dfc780c35

View file

@ -3,7 +3,7 @@
0. Setup 0. Setup
- Download Haxe from [Haxe.org](https://haxe.org) - Download Haxe from [Haxe.org](https://haxe.org)
1. Cloning the Repository: Make sure when you clone, you clone the submodules to get the assets repo: 1. Cloning the Repository: Make sure when you clone, you clone the submodules to get the assets repo:
- `git clone --recurse-submodules https://github.com/FunkinCrew/funkin-secret.git` - `git clone --recurse-submodules https://github.com/FunkinCrew/funkin.git`
- If you accidentally cloned without the `assets` submodule (aka didn't follow the step above), you can run `git submodule update --init --recursive` to get the assets in a foolproof way. - If you accidentally cloned without the `assets` submodule (aka didn't follow the step above), you can run `git submodule update --init --recursive` to get the assets in a foolproof way.
2. Install `hmm` (run `haxelib --global install hmm` and then `haxelib --global run hmm setup`) 2. Install `hmm` (run `haxelib --global install hmm` and then `haxelib --global run hmm setup`)
3. Install all haxelibs of the current branch by running `hmm install` 3. Install all haxelibs of the current branch by running `hmm install`
@ -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

@ -49,7 +49,7 @@
"name": "funkin.vis", "name": "funkin.vis",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "98c9db09f0bbfedfe67a84538a5814aaef80bdea", "ref": "2aa654b974507ab51ab1724d2d97e75726fd7d78",
"url": "https://github.com/FunkinCrew/funkVis" "url": "https://github.com/FunkinCrew/funkVis"
}, },
{ {
@ -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

@ -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();
if (!fromDeathState)
{
playerStrumline.vwooshNotes(); playerStrumline.vwooshNotes();
opponentStrumline.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,
@ -2548,6 +2572,13 @@ class PlayState extends MusicBeatSubState
// Redirect to the chart editor playing the current song. // Redirect to the chart editor playing the current song.
if (controls.DEBUG_CHART) if (controls.DEBUG_CHART)
{
if (isChartingMode)
{
if (FlxG.sound.music != null) FlxG.sound.music.pause(); // Don't reset song position!
this.close(); // This only works because PlayState is a substate!
}
else
{ {
disableKeys = true; disableKeys = true;
persistentUpdate = false; persistentUpdate = false;
@ -2556,6 +2587,7 @@ class PlayState extends MusicBeatSubState
targetSongId: currentSong.id, targetSongId: currentSong.id,
})); }));
} }
}
#end #end
#if (debug || FORCE_DEBUG_VERSION) #if (debug || FORCE_DEBUG_VERSION)

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

@ -31,25 +31,13 @@ class PlayAnimationSongEvent extends SongEvent
switch (targetName) switch (targetName)
{ {
case 'boyfriend': case 'boyfriend' | 'bf' | 'player':
trace('Playing animation $anim on boyfriend.'); trace('Playing animation $anim on boyfriend.');
target = PlayState.instance.currentStage.getBoyfriend(); target = PlayState.instance.currentStage.getBoyfriend();
case 'bf': case 'dad' | 'opponent':
trace('Playing animation $anim on boyfriend.');
target = PlayState.instance.currentStage.getBoyfriend();
case 'player':
trace('Playing animation $anim on boyfriend.');
target = PlayState.instance.currentStage.getBoyfriend();
case 'dad':
trace('Playing animation $anim on dad.'); trace('Playing animation $anim on dad.');
target = PlayState.instance.currentStage.getDad(); target = PlayState.instance.currentStage.getDad();
case 'opponent': case 'girlfriend' | 'gf':
trace('Playing animation $anim on dad.');
target = PlayState.instance.currentStage.getDad();
case 'girlfriend':
trace('Playing animation $anim on girlfriend.');
target = PlayState.instance.currentStage.getGirlfriend();
case 'gf':
trace('Playing animation $anim on girlfriend.'); trace('Playing animation $anim on girlfriend.');
target = PlayState.instance.currentStage.getGirlfriend(); target = PlayState.instance.currentStage.getGirlfriend();
default: default:

View file

@ -576,6 +576,8 @@ class Strumline extends FlxSpriteGroup
note.holdNoteSprite.hitNote = true; note.holdNoteSprite.hitNote = true;
note.holdNoteSprite.missedNote = false; note.holdNoteSprite.missedNote = false;
note.holdNoteSprite.alpha = 1.0; note.holdNoteSprite.alpha = 1.0;
note.holdNoteSprite.sustainLength = (note.holdNoteSprite.strumTime + note.holdNoteSprite.fullSustainLength) - conductorInUse.songPosition;
} }
} }

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.
@ -78,6 +77,9 @@ class Save
levels: [], levels: [],
songs: [], songs: [],
}, },
favoriteSongs: [],
options: options:
{ {
// Reasonable defaults. // Reasonable defaults.
@ -555,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)
@ -741,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;
/** /**

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

@ -67,7 +67,7 @@ class ChartEditorCharacterIconSelectorMenu extends ChartEditorBaseMenu
var charGrid = new Grid(); var charGrid = new Grid();
charGrid.columns = 5; charGrid.columns = 5;
charGrid.width = 100; charGrid.width = this.width;
charSelectScroll.addComponent(charGrid); charSelectScroll.addComponent(charGrid);
var charIds:Array<String> = CharacterDataParser.listCharacterIds(); var charIds:Array<String> = CharacterDataParser.listCharacterIds();

View file

@ -699,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,
{ {
@ -724,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))
{ {
@ -880,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)
{ {
@ -901,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();
@ -1398,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);

View file

@ -139,10 +139,6 @@ class MainMenuState extends MusicBeatState
resetCamStuff(); resetCamStuff();
subStateClosed.add(_ -> {
resetCamStuff();
});
subStateOpened.add(sub -> { subStateOpened.add(sub -> {
if (Type.getClass(sub) == FreeplayState) if (Type.getClass(sub) == FreeplayState)
{ {

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

@ -283,6 +283,7 @@ class TitleState extends MusicBeatState
if (FlxG.keys.justPressed.Y) if (FlxG.keys.justPressed.Y)
{ {
FlxTween.cancelTweensOf(FlxG.stage.window, ['x', 'y']);
FlxTween.tween(FlxG.stage.window, {x: FlxG.stage.window.x + 300}, 1.4, {ease: FlxEase.quadInOut, type: PINGPONG, startDelay: 0.35}); FlxTween.tween(FlxG.stage.window, {x: FlxG.stage.window.x + 300}, 1.4, {ease: FlxEase.quadInOut, type: PINGPONG, startDelay: 0.35});
FlxTween.tween(FlxG.stage.window, {y: FlxG.stage.window.y + 100}, 0.7, {ease: FlxEase.quadInOut, type: PINGPONG}); FlxTween.tween(FlxG.stage.window, {y: FlxG.stage.window.y + 100}, 0.7, {ease: FlxEase.quadInOut, type: PINGPONG});
} }

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

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