1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2024-11-15 11:22:55 +00:00

Merge branch 'rewrite/master' into bugfix/character-offset-fixes

This commit is contained in:
Cameron Taylor 2024-07-29 21:25:33 -04:00
commit f4a2ac4dde
22 changed files with 674 additions and 313 deletions

View file

@ -45,7 +45,11 @@ jobs:
uses: ./.github/actions/setup-haxe uses: ./.github/actions/setup-haxe
with: with:
gh-token: ${{ steps.app_token.outputs.token }} gh-token: ${{ steps.app_token.outputs.token }}
- name: Setup HXCPP dev commit
run: |
cd .haxelib/hxcpp/git/tools/hxcpp
haxe compile.hxml
cd ../../../../..
- name: Build game - name: Build game
if: ${{ matrix.target == 'windows' }} if: ${{ matrix.target == 'windows' }}
run: | run: |

2
assets

@ -1 +1 @@
Subproject commit 4af95a506fc62cd683422dfb9c599877b26c27db Subproject commit 005c96f85f4304865acb196e7cc4d6d83f9d76d8

View file

@ -11,14 +11,14 @@
"name": "flixel", "name": "flixel",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "a7d8e3bad89a0a3506a4714121f73d8e34522c49", "ref": "10c2a203c43a78ff1ff26b8368fd736576829d8d",
"url": "https://github.com/FunkinCrew/flixel" "url": "https://github.com/FunkinCrew/flixel"
}, },
{ {
"name": "flixel-addons", "name": "flixel-addons",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "a523c3b56622f0640933944171efed46929e360e", "ref": "9c6fb47968e894eb36bf10e94725cd7640c49281",
"url": "https://github.com/FunkinCrew/flixel-addons" "url": "https://github.com/FunkinCrew/flixel-addons"
}, },
{ {
@ -30,7 +30,7 @@
"name": "flixel-ui", "name": "flixel-ui",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "719b4f10d94186ed55f6fef1b6618d32abec8c15", "ref": "d0afed7293c71ffdb1184751317fc709b44c9056",
"url": "https://github.com/HaxeFlixel/flixel-ui" "url": "https://github.com/HaxeFlixel/flixel-ui"
}, },
{ {
@ -99,8 +99,10 @@
}, },
{ {
"name": "hxcpp", "name": "hxcpp",
"type": "haxelib", "type": "git",
"version": "4.3.2" "dir": null,
"url": "https://github.com/HaxeFoundation/hxcpp",
"ref": "01cfee282a9a783e10c5a7774a3baaf547e6b0a7"
}, },
{ {
"name": "hxcpp-debug-server", "name": "hxcpp-debug-server",
@ -121,6 +123,20 @@
"ref": "a8c26f18463c98da32f744c214fe02273e1823fa", "ref": "a8c26f18463c98da32f744c214fe02273e1823fa",
"url": "https://github.com/FunkinCrew/json2object" "url": "https://github.com/FunkinCrew/json2object"
}, },
{
"name": "jsonpatch",
"type": "git",
"dir": null,
"ref": "f9b83215acd586dc28754b4ae7f69d4c06c3b4d3",
"url": "https://github.com/EliteMasterEric/jsonpatch"
},
{
"name": "jsonpath",
"type": "git",
"dir": null,
"ref": "7a24193717b36393458c15c0435bb7c4470ecdda",
"url": "https://github.com/EliteMasterEric/jsonpath"
},
{ {
"name": "lime", "name": "lime",
"type": "git", "type": "git",
@ -167,7 +183,7 @@
"name": "polymod", "name": "polymod",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "bfbe30d81601b3543d80dce580108ad6b7e182c7", "ref": "98945c6c7f5ecde01a32c4623d3515bf012a023a",
"url": "https://github.com/larsiusprime/polymod" "url": "https://github.com/larsiusprime/polymod"
}, },
{ {

View file

@ -46,7 +46,7 @@ class SongEventRegistry
if (event != null) if (event != null)
{ {
trace(' Loaded built-in song event: (${event.id})'); trace(' Loaded built-in song event: ${event.id}');
eventCache.set(event.id, event); eventCache.set(event.id, event);
} }
else else
@ -59,9 +59,9 @@ class SongEventRegistry
static function registerScriptedEvents() static function registerScriptedEvents()
{ {
var scriptedEventClassNames:Array<String> = ScriptedSongEvent.listScriptClasses(); var scriptedEventClassNames:Array<String> = ScriptedSongEvent.listScriptClasses();
trace('Instantiating ${scriptedEventClassNames.length} scripted song events...');
if (scriptedEventClassNames == null || scriptedEventClassNames.length == 0) return; if (scriptedEventClassNames == null || scriptedEventClassNames.length == 0) return;
trace('Instantiating ${scriptedEventClassNames.length} scripted song events...');
for (eventCls in scriptedEventClassNames) for (eventCls in scriptedEventClassNames)
{ {
var event:SongEvent = ScriptedSongEvent.init(eventCls, "UKNOWN"); var event:SongEvent = ScriptedSongEvent.init(eventCls, "UKNOWN");

View file

@ -5,6 +5,13 @@ 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.2.4]
### Added
- Added `playData.characters.opponentVocals` to specify which vocal track(s) to play for the opponent.
- If the value isn't present, it will use the `playData.characters.opponent`, but if it is present, it will be used (even if it's empty, in which case no vocals will be used for the opponent)
- Added `playData.characters.playerVocals` to specify which vocal track(s) to play for the player.
- If the value isn't present, it will use the `playData.characters.player`, but if it is present, it will be used (even if it's empty, in which case no vocals will be used for the player)
## [2.2.3] ## [2.2.3]
### Added ### Added
- Added `charter` field to denote authorship of a chart. - Added `charter` field to denote authorship of a chart.

View file

@ -529,12 +529,26 @@ class SongCharacterData implements ICloneable<SongCharacterData>
@:default([]) @:default([])
public var altInstrumentals:Array<String> = []; public var altInstrumentals:Array<String> = [];
public function new(player:String = '', girlfriend:String = '', opponent:String = '', instrumental:String = '') @:optional
public var opponentVocals:Null<Array<String>> = null;
@:optional
public var playerVocals:Null<Array<String>> = null;
public function new(player:String = '', girlfriend:String = '', opponent:String = '', instrumental:String = '', ?altInstrumentals:Array<String>,
?opponentVocals:Array<String>, ?playerVocals:Array<String>)
{ {
this.player = player; this.player = player;
this.girlfriend = girlfriend; this.girlfriend = girlfriend;
this.opponent = opponent; this.opponent = opponent;
this.instrumental = instrumental; this.instrumental = instrumental;
this.altInstrumentals = altInstrumentals;
this.opponentVocals = opponentVocals;
this.playerVocals = playerVocals;
if (opponentVocals == null) this.opponentVocals = [opponent];
if (playerVocals == null) this.playerVocals = [player];
} }
public function clone():SongCharacterData public function clone():SongCharacterData
@ -722,18 +736,6 @@ class SongEventDataRaw implements ICloneable<SongEventDataRaw>
{ {
return new SongEventDataRaw(this.time, this.eventKind, this.value); return new SongEventDataRaw(this.time, this.eventKind, this.value);
} }
}
/**
* Wrap SongEventData in an abstract so we can overload operators.
*/
@:forward(time, eventKind, value, activated, getStepTime, clone)
abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataRaw
{
public function new(time:Float, eventKind:String, value:Dynamic = null)
{
this = new SongEventDataRaw(time, eventKind, value);
}
public function valueAsStruct(?defaultKey:String = "key"):Dynamic public function valueAsStruct(?defaultKey:String = "key"):Dynamic
{ {
@ -757,27 +759,27 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
} }
} }
public inline function getHandler():Null<SongEvent> public function getHandler():Null<SongEvent>
{ {
return SongEventRegistry.getEvent(this.eventKind); return SongEventRegistry.getEvent(this.eventKind);
} }
public inline function getSchema():Null<SongEventSchema> public function getSchema():Null<SongEventSchema>
{ {
return SongEventRegistry.getEventSchema(this.eventKind); return SongEventRegistry.getEventSchema(this.eventKind);
} }
public inline function getDynamic(key:String):Null<Dynamic> public function getDynamic(key:String):Null<Dynamic>
{ {
return this.value == null ? null : Reflect.field(this.value, key); return this.value == null ? null : Reflect.field(this.value, key);
} }
public inline function getBool(key:String):Null<Bool> public function getBool(key:String):Null<Bool>
{ {
return this.value == null ? null : cast Reflect.field(this.value, key); return this.value == null ? null : cast Reflect.field(this.value, key);
} }
public inline function getInt(key:String):Null<Int> public function getInt(key:String):Null<Int>
{ {
if (this.value == null) return null; if (this.value == null) return null;
var result = Reflect.field(this.value, key); var result = Reflect.field(this.value, key);
@ -787,7 +789,7 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
return cast result; return cast result;
} }
public inline function getFloat(key:String):Null<Float> public function getFloat(key:String):Null<Float>
{ {
if (this.value == null) return null; if (this.value == null) return null;
var result = Reflect.field(this.value, key); var result = Reflect.field(this.value, key);
@ -797,17 +799,17 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
return cast result; return cast result;
} }
public inline function getString(key:String):String public function getString(key:String):String
{ {
return this.value == null ? null : cast Reflect.field(this.value, key); return this.value == null ? null : cast Reflect.field(this.value, key);
} }
public inline function getArray(key:String):Array<Dynamic> public function getArray(key:String):Array<Dynamic>
{ {
return this.value == null ? null : cast Reflect.field(this.value, key); return this.value == null ? null : cast Reflect.field(this.value, key);
} }
public inline function getBoolArray(key:String):Array<Bool> public function getBoolArray(key:String):Array<Bool>
{ {
return this.value == null ? null : cast Reflect.field(this.value, key); return this.value == null ? null : cast Reflect.field(this.value, key);
} }
@ -839,6 +841,19 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
return result; return result;
} }
}
/**
* Wrap SongEventData in an abstract so we can overload operators.
*/
@:forward(time, eventKind, value, activated, getStepTime, clone, getHandler, getSchema, getDynamic, getBool, getInt, getFloat, getString, getArray,
getBoolArray, buildTooltip, valueAsStruct)
abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataRaw
{
public function new(time:Float, eventKind:String, value:Dynamic = null)
{
this = new SongEventDataRaw(time, eventKind, value);
}
public function clone():SongEventData public function clone():SongEventData
{ {

View file

@ -234,6 +234,8 @@ class PolymodHandler
// NOTE: Scripted classes are automatically aliased to their parent class. // NOTE: Scripted classes are automatically aliased to their parent class.
Polymod.addImportAlias('flixel.math.FlxPoint', flixel.math.FlxPoint.FlxBasePoint); Polymod.addImportAlias('flixel.math.FlxPoint', flixel.math.FlxPoint.FlxBasePoint);
Polymod.addImportAlias('funkin.data.event.SongEventSchema', funkin.data.event.SongEventSchema.SongEventSchemaRaw);
// Add blacklisting for prohibited classes and packages. // Add blacklisting for prohibited classes and packages.
// `Sys` // `Sys`
@ -252,6 +254,10 @@ class PolymodHandler
// Lib.load() can load malicious DLLs // Lib.load() can load malicious DLLs
Polymod.blacklistImport('cpp.Lib'); Polymod.blacklistImport('cpp.Lib');
// `Unserializer`
// Unserializerr.DEFAULT_RESOLVER.resolveClass() can access blacklisted packages
Polymod.blacklistImport('Unserializer');
// `polymod.*` // `polymod.*`
// You can probably unblacklist a module // You can probably unblacklist a module
for (cls in ClassMacro.listClassesInPackage('polymod')) for (cls in ClassMacro.listClassesInPackage('polymod'))

View file

@ -504,7 +504,7 @@ class PlayState extends MusicBeatSubState
public var camGame:FlxCamera; public var camGame:FlxCamera;
/** /**
* The camera which contains, and controls visibility of, a video cutscene. * The camera which contains, and controls visibility of, a video cutscene, dialogue, pause menu and sticker transition.
*/ */
public var camCutscene:FlxCamera; public var camCutscene:FlxCamera;
@ -579,7 +579,8 @@ class PlayState extends MusicBeatSubState
// TODO: Refactor or document // TODO: Refactor or document
var generatedMusic:Bool = false; var generatedMusic:Bool = false;
var perfectMode:Bool = false;
var skipEndingTransition:Bool = false;
static final BACKGROUND_COLOR:FlxColor = FlxColor.BLACK; static final BACKGROUND_COLOR:FlxColor = FlxColor.BLACK;
@ -976,7 +977,7 @@ class PlayState extends MusicBeatSubState
FlxTransitionableState.skipNextTransIn = true; FlxTransitionableState.skipNextTransIn = true;
FlxTransitionableState.skipNextTransOut = true; FlxTransitionableState.skipNextTransOut = true;
pauseSubState.camera = camHUD; pauseSubState.camera = camCutscene;
openSubState(pauseSubState); openSubState(pauseSubState);
// boyfriendPos.put(); // TODO: Why is this here? // boyfriendPos.put(); // TODO: Why is this here?
} }
@ -1350,64 +1351,13 @@ class PlayState extends MusicBeatSubState
} }
/** /**
* Removes any references to the current stage, then clears the stage cache,
* then reloads all the stages.
*
* This is useful for when you want to edit a stage without reloading the whole game.
* Reloading works on both the JSON and the HXC, if applicable.
*
* Call this by pressing F5 on a debug build. * Call this by pressing F5 on a debug build.
*/ */
override function debug_refreshModules():Void override function reloadAssets():Void
{ {
// Prevent further gameplay updates, which will try to reference dead objects. funkin.modding.PolymodHandler.forceReloadAssets();
criticalFailure = true; lastParams.targetSong = SongRegistry.instance.fetchEntry(currentSong.id);
LoadingState.loadPlayState(lastParams);
// Remove the current stage. If the stage gets deleted while it's still in use,
// it'll probably crash the game or something.
if (this.currentStage != null)
{
remove(currentStage);
var event:ScriptEvent = new ScriptEvent(DESTROY, false);
ScriptEventDispatcher.callEvent(currentStage, event);
currentStage = null;
}
if (!overrideMusic)
{
// Stop the instrumental.
if (FlxG.sound.music != null)
{
FlxG.sound.music.destroy();
FlxG.sound.music = null;
}
// Stop the vocals.
if (vocals != null && vocals.exists)
{
vocals.destroy();
vocals = null;
}
}
else
{
// Stop the instrumental.
if (FlxG.sound.music != null)
{
FlxG.sound.music.stop();
}
// Stop the vocals.
if (vocals != null && vocals.exists)
{
vocals.stop();
}
}
super.debug_refreshModules();
var event:ScriptEvent = new ScriptEvent(CREATE, false);
ScriptEventDispatcher.callEvent(currentSong, event);
} }
override function stepHit():Bool override function stepHit():Bool
@ -1419,17 +1369,6 @@ class PlayState extends MusicBeatSubState
if (isGamePaused) return false; if (isGamePaused) return false;
if (!startingSong
&& FlxG.sound.music != null
&& (Math.abs(FlxG.sound.music.time - (Conductor.instance.songPosition + Conductor.instance.instrumentalOffset)) > 200
|| Math.abs(vocals.checkSyncError(Conductor.instance.songPosition + Conductor.instance.instrumentalOffset)) > 200))
{
trace("VOCALS NEED RESYNC");
if (vocals != null) trace(vocals.checkSyncError(Conductor.instance.songPosition + Conductor.instance.instrumentalOffset));
trace(FlxG.sound.music.time - (Conductor.instance.songPosition + Conductor.instance.instrumentalOffset));
resyncVocals();
}
if (iconP1 != null) iconP1.onStepHit(Std.int(Conductor.instance.currentStep)); if (iconP1 != null) iconP1.onStepHit(Std.int(Conductor.instance.currentStep));
if (iconP2 != null) iconP2.onStepHit(Std.int(Conductor.instance.currentStep)); if (iconP2 != null) iconP2.onStepHit(Std.int(Conductor.instance.currentStep));
@ -1451,6 +1390,17 @@ class PlayState extends MusicBeatSubState
// activeNotes.sort(SortUtil.byStrumtime, FlxSort.DESCENDING); // activeNotes.sort(SortUtil.byStrumtime, FlxSort.DESCENDING);
} }
if (!startingSong
&& FlxG.sound.music != null
&& (Math.abs(FlxG.sound.music.time - (Conductor.instance.songPosition + Conductor.instance.instrumentalOffset)) > 100
|| Math.abs(vocals.checkSyncError(Conductor.instance.songPosition + Conductor.instance.instrumentalOffset)) > 100))
{
trace("VOCALS NEED RESYNC");
if (vocals != null) trace(vocals.checkSyncError(Conductor.instance.songPosition + Conductor.instance.instrumentalOffset));
trace(FlxG.sound.music.time - (Conductor.instance.songPosition + Conductor.instance.instrumentalOffset));
resyncVocals();
}
// Only bop camera if zoom level is below 135% // Only bop camera if zoom level is below 135%
if (Preferences.zoomCamera if (Preferences.zoomCamera
&& FlxG.camera.zoom < (1.35 * FlxCamera.defaultZoom) && FlxG.camera.zoom < (1.35 * FlxCamera.defaultZoom)
@ -1503,9 +1453,6 @@ class PlayState extends MusicBeatSubState
if (playerStrumline != null) playerStrumline.onBeatHit(); if (playerStrumline != null) playerStrumline.onBeatHit();
if (opponentStrumline != null) opponentStrumline.onBeatHit(); if (opponentStrumline != null) opponentStrumline.onBeatHit();
// Make the characters dance on the beat
danceOnBeat();
return true; return true;
} }
@ -1516,26 +1463,6 @@ class PlayState extends MusicBeatSubState
super.destroy(); super.destroy();
} }
/**
* Handles characters dancing to the beat of the current song.
*
* TODO: Move some of this logic into `Bopper.hx`, or individual character scripts.
*/
function danceOnBeat():Void
{
if (currentStage == null) return;
// TODO: Add HEY! song events to Tutorial.
if (Conductor.instance.currentBeat % 16 == 15
&& currentStage.getDad().characterId == 'gf'
&& Conductor.instance.currentBeat > 16
&& Conductor.instance.currentBeat < 48)
{
currentStage.getBoyfriend().playAnimation('hey', true);
currentStage.getDad().playAnimation('cheer', true);
}
}
/** /**
* Initializes the game and HUD cameras. * Initializes the game and HUD cameras.
*/ */
@ -1936,7 +1863,6 @@ class PlayState extends MusicBeatSubState
if (!result) return; if (!result) return;
isInCutscene = false; isInCutscene = false;
camCutscene.visible = false;
// TODO: Maybe tween in the camera after any cutscenes. // TODO: Maybe tween in the camera after any cutscenes.
camHUD.visible = true; camHUD.visible = true;
@ -2002,7 +1928,9 @@ class PlayState extends MusicBeatSubState
return; return;
} }
FlxG.sound.music.onComplete = endSong.bind(false); FlxG.sound.music.onComplete = function() {
endSong(skipEndingTransition);
};
// A negative instrumental offset means the song skips the first few milliseconds of the track. // A negative instrumental offset means the song skips the first few milliseconds of the track.
// This just gets added into the startTimestamp behavior so we don't need to do anything extra. // This just gets added into the startTimestamp behavior so we don't need to do anything extra.
FlxG.sound.music.play(true, startTimestamp - Conductor.instance.instrumentalOffset); FlxG.sound.music.play(true, startTimestamp - Conductor.instance.instrumentalOffset);
@ -2042,13 +1970,15 @@ class PlayState extends MusicBeatSubState
// Skip this if the music is paused (GameOver, Pause menu, start-of-song offset, etc.) // Skip this if the music is paused (GameOver, Pause menu, start-of-song offset, etc.)
if (!FlxG.sound.music.playing) return; if (!FlxG.sound.music.playing) return;
var timeToPlayAt:Float = Conductor.instance.songPosition - Conductor.instance.instrumentalOffset;
FlxG.sound.music.pause();
vocals.pause(); vocals.pause();
FlxG.sound.music.play(FlxG.sound.music.time); FlxG.sound.music.time = timeToPlayAt;
FlxG.sound.music.play(false, timeToPlayAt);
vocals.time = FlxG.sound.music.time; vocals.time = timeToPlayAt;
vocals.play(false, FlxG.sound.music.time); vocals.play(false, timeToPlayAt);
} }
/** /**
@ -2612,12 +2542,6 @@ class PlayState extends MusicBeatSubState
*/ */
function debugKeyShit():Void function debugKeyShit():Void
{ {
#if !debug
perfectMode = false;
#else
if (FlxG.keys.justPressed.H) camHUD.visible = !camHUD.visible;
#end
#if CHART_EDITOR_SUPPORTED #if CHART_EDITOR_SUPPORTED
// Open the stage editor overlaying the current state. // Open the stage editor overlaying the current state.
if (controls.DEBUG_STAGE) if (controls.DEBUG_STAGE)
@ -2649,6 +2573,9 @@ class PlayState extends MusicBeatSubState
#end #end
#if (debug || FORCE_DEBUG_VERSION) #if (debug || FORCE_DEBUG_VERSION)
// H: Hide the HUD.
if (FlxG.keys.justPressed.H) camHUD.visible = !camHUD.visible;
// 1: End the song immediately. // 1: End the song immediately.
if (FlxG.keys.justPressed.ONE) endSong(true); if (FlxG.keys.justPressed.ONE) endSong(true);

View file

@ -81,7 +81,6 @@ class VideoCutscene
// Trigger the cutscene. Don't play the song in the background. // Trigger the cutscene. Don't play the song in the background.
PlayState.instance.isInCutscene = true; PlayState.instance.isInCutscene = true;
PlayState.instance.camHUD.visible = false; PlayState.instance.camHUD.visible = false;
PlayState.instance.camCutscene.visible = true;
// Display a black screen to hide the game while the video is playing. // Display a black screen to hide the game while the video is playing.
blackScreen = new FlxSprite(-200, -200).makeGraphic(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK); blackScreen = new FlxSprite(-200, -200).makeGraphic(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK);
@ -305,7 +304,6 @@ class VideoCutscene
vid = null; vid = null;
#end #end
PlayState.instance.camCutscene.visible = true;
PlayState.instance.camHUD.visible = true; PlayState.instance.camHUD.visible = true;
FlxTween.tween(blackScreen, {alpha: 0}, transitionTime, FlxTween.tween(blackScreen, {alpha: 0}, transitionTime,

View file

@ -495,6 +495,24 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
return diffFiltered; return diffFiltered;
} }
public function listSuffixedDifficulties(variationIds:Array<String>, ?showLocked:Bool, ?showHidden:Bool):Array<String>
{
var result = [];
for (variation in variationIds)
{
var difficulties = listDifficulties(variation, null, showLocked, showHidden);
for (difficulty in difficulties)
{
var suffixedDifficulty = (variation != Constants.DEFAULT_VARIATION
&& variation != 'erect') ? '$difficulty-${variation}' : difficulty;
result.push(suffixedDifficulty);
}
}
return result;
}
public function hasDifficulty(diffId:String, ?variationId:String, ?variationIds:Array<String>):Bool public function hasDifficulty(diffId:String, ?variationId:String, ?variationIds:Array<String>):Bool
{ {
if (variationIds == null) variationIds = []; if (variationIds == null) variationIds = [];
@ -707,10 +725,11 @@ class SongDifficulty
* Cache the vocals for a given character. * Cache the vocals for a given character.
* @param id The character we are about to play. * @param id The character we are about to play.
*/ */
public inline function cacheVocals():Void public function cacheVocals():Void
{ {
for (voice in buildVoiceList()) for (voice in buildVoiceList())
{ {
trace('Caching vocal track: $voice');
FlxG.sound.cache(voice); FlxG.sound.cache(voice);
} }
} }
@ -722,6 +741,20 @@ class SongDifficulty
* @param id The character we are about to play. * @param id The character we are about to play.
*/ */
public function buildVoiceList():Array<String> public function buildVoiceList():Array<String>
{
var result:Array<String> = [];
result = result.concat(buildPlayerVoiceList());
result = result.concat(buildOpponentVoiceList());
if (result.length == 0)
{
var suffix:String = (variation != null && variation != '' && variation != 'default') ? '-$variation' : '';
// Try to use `Voices.ogg` if no other voices are found.
if (Assets.exists(Paths.voices(this.song.id, ''))) result.push(Paths.voices(this.song.id, '$suffix'));
}
return result;
}
public function buildPlayerVoiceList():Array<String>
{ {
var suffix:String = (variation != null && variation != '' && variation != 'default') ? '-$variation' : ''; var suffix:String = (variation != null && variation != '' && variation != 'default') ? '-$variation' : '';
@ -729,62 +762,88 @@ class SongDifficulty
// For example, if `Voices-bf-car-erect.ogg` does not exist, check for `Voices-bf-erect.ogg`. // For example, if `Voices-bf-car-erect.ogg` does not exist, check for `Voices-bf-erect.ogg`.
// Then, check for `Voices-bf-car.ogg`, then `Voices-bf.ogg`. // Then, check for `Voices-bf-car.ogg`, then `Voices-bf.ogg`.
var playerId:String = characters.player; if (characters.playerVocals == null)
var voicePlayer:String = Paths.voices(this.song.id, '-$playerId$suffix');
while (voicePlayer != null && !Assets.exists(voicePlayer))
{ {
// Remove the last suffix. var playerId:String = characters.player;
// For example, bf-car becomes bf. var playerVoice:String = Paths.voices(this.song.id, '-${playerId}$suffix');
playerId = playerId.split('-').slice(0, -1).join('-');
// Try again. while (playerVoice != null && !Assets.exists(playerVoice))
voicePlayer = playerId == '' ? null : Paths.voices(this.song.id, '-${playerId}$suffix');
}
if (voicePlayer == null)
{
// Try again without $suffix.
playerId = characters.player;
voicePlayer = Paths.voices(this.song.id, '-${playerId}');
while (voicePlayer != null && !Assets.exists(voicePlayer))
{ {
// Remove the last suffix. // Remove the last suffix.
// For example, bf-car becomes bf.
playerId = playerId.split('-').slice(0, -1).join('-'); playerId = playerId.split('-').slice(0, -1).join('-');
// Try again. // Try again.
voicePlayer = playerId == '' ? null : Paths.voices(this.song.id, '-${playerId}$suffix'); playerVoice = playerId == '' ? null : Paths.voices(this.song.id, '-${playerId}$suffix');
}
if (playerVoice == null)
{
// Try again without $suffix.
playerId = characters.player;
playerVoice = Paths.voices(this.song.id, '-${playerId}');
while (playerVoice != null && !Assets.exists(playerVoice))
{
// Remove the last suffix.
playerId = playerId.split('-').slice(0, -1).join('-');
// Try again.
playerVoice = playerId == '' ? null : Paths.voices(this.song.id, '-${playerId}$suffix');
}
} }
}
var opponentId:String = characters.opponent; return playerVoice != null ? [playerVoice] : [];
var voiceOpponent:String = Paths.voices(this.song.id, '-${opponentId}$suffix');
while (voiceOpponent != null && !Assets.exists(voiceOpponent))
{
// Remove the last suffix.
opponentId = opponentId.split('-').slice(0, -1).join('-');
// Try again.
voiceOpponent = opponentId == '' ? null : Paths.voices(this.song.id, '-${opponentId}$suffix');
} }
if (voiceOpponent == null) else
{ {
// Try again without $suffix. // The metadata explicitly defines the list of voices.
opponentId = characters.opponent; var playerIds:Array<String> = characters?.playerVocals ?? [characters.player];
voiceOpponent = Paths.voices(this.song.id, '-${opponentId}'); var playerVoices:Array<String> = playerIds.map((id) -> Paths.voices(this.song.id, '-$id$suffix'));
while (voiceOpponent != null && !Assets.exists(voiceOpponent))
return playerVoices;
}
}
public function buildOpponentVoiceList():Array<String>
{
var suffix:String = (variation != null && variation != '' && variation != 'default') ? '-$variation' : '';
// Automatically resolve voices by removing suffixes.
// For example, if `Voices-bf-car-erect.ogg` does not exist, check for `Voices-bf-erect.ogg`.
// Then, check for `Voices-bf-car.ogg`, then `Voices-bf.ogg`.
if (characters.opponentVocals == null)
{
var opponentId:String = characters.opponent;
var opponentVoice:String = Paths.voices(this.song.id, '-${opponentId}$suffix');
while (opponentVoice != null && !Assets.exists(opponentVoice))
{ {
// Remove the last suffix. // Remove the last suffix.
opponentId = opponentId.split('-').slice(0, -1).join('-'); opponentId = opponentId.split('-').slice(0, -1).join('-');
// Try again. // Try again.
voiceOpponent = opponentId == '' ? null : Paths.voices(this.song.id, '-${opponentId}$suffix'); opponentVoice = opponentId == '' ? null : Paths.voices(this.song.id, '-${opponentId}$suffix');
}
if (opponentVoice == null)
{
// Try again without $suffix.
opponentId = characters.opponent;
opponentVoice = Paths.voices(this.song.id, '-${opponentId}');
while (opponentVoice != null && !Assets.exists(opponentVoice))
{
// Remove the last suffix.
opponentId = opponentId.split('-').slice(0, -1).join('-');
// Try again.
opponentVoice = opponentId == '' ? null : Paths.voices(this.song.id, '-${opponentId}$suffix');
}
} }
}
var result:Array<String> = []; return opponentVoice != null ? [opponentVoice] : [];
if (voicePlayer != null) result.push(voicePlayer); }
if (voiceOpponent != null) result.push(voiceOpponent); else
if (voicePlayer == null && voiceOpponent == null) {
{ // The metadata explicitly defines the list of voices.
// Try to use `Voices.ogg` if no other voices are found. var opponentIds:Array<String> = characters?.opponentVocals ?? [characters.opponent];
if (Assets.exists(Paths.voices(this.song.id, ''))) result.push(Paths.voices(this.song.id, '$suffix')); var opponentVoices:Array<String> = opponentIds.map((id) -> Paths.voices(this.song.id, '-$id$suffix'));
return opponentVoices;
} }
return result;
} }
/** /**
@ -796,26 +855,19 @@ class SongDifficulty
{ {
var result:VoicesGroup = new VoicesGroup(); var result:VoicesGroup = new VoicesGroup();
var voiceList:Array<String> = buildVoiceList(); var playerVoiceList:Array<String> = this.buildPlayerVoiceList();
var opponentVoiceList:Array<String> = this.buildOpponentVoiceList();
if (voiceList.length == 0)
{
trace('Could not find any voices for song ${this.song.id}');
return result;
}
// Add player vocals. // Add player vocals.
if (voiceList[0] != null) result.addPlayerVoice(FunkinSound.load(voiceList[0])); for (playerVoice in playerVoiceList)
// Add opponent vocals.
if (voiceList[1] != null) result.addOpponentVoice(FunkinSound.load(voiceList[1]));
// Add additional vocals.
if (voiceList.length > 2)
{ {
for (i in 2...voiceList.length) result.addPlayerVoice(FunkinSound.load(playerVoice));
{ }
result.add(FunkinSound.load(Assets.getSound(voiceList[i])));
} // Add opponent vocals.
for (opponentVoice in opponentVoiceList)
{
result.addOpponentVoice(FunkinSound.load(opponentVoice));
} }
result.playerVoicesOffset = offsets.getVocalOffset(characters.player); result.playerVoicesOffset = offsets.getVocalOffset(characters.player);

View file

@ -152,6 +152,32 @@ class AtlasText extends FlxTypedSpriteGroup<AtlasChar>
} }
} }
public function getWidth():Int
{
var width = 0;
for (char in this.text.split(""))
{
switch (char)
{
case " ":
{
width += 40;
}
case "\n":
{}
case char:
{
var sprite = new AtlasChar(atlas, char);
sprite.revive();
sprite.char = char;
sprite.alpha = 1;
width += Std.int(sprite.width);
}
}
}
return width;
}
override function toString() override function toString()
{ {
return "InputItem, " + FlxStringUtil.getDebugString([ return "InputItem, " + FlxStringUtil.getDebugString([

View file

@ -78,9 +78,6 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
{ {
// Emergency exit button. // Emergency exit button.
if (FlxG.keys.justPressed.F4) FlxG.switchState(() -> new MainMenuState()); if (FlxG.keys.justPressed.F4) FlxG.switchState(() -> new MainMenuState());
// This can now be used in EVERY STATE YAY!
if (FlxG.keys.justPressed.F5) debug_refreshModules();
} }
override function update(elapsed:Float) override function update(elapsed:Float)
@ -114,12 +111,10 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
ModuleHandler.callEvent(event); ModuleHandler.callEvent(event);
} }
function debug_refreshModules() function reloadAssets()
{ {
PolymodHandler.forceReloadAssets(); PolymodHandler.forceReloadAssets();
this.destroy();
// Create a new instance of the current state, so old data is cleared. // Create a new instance of the current state, so old data is cleared.
FlxG.resetState(); FlxG.resetState();
} }

View file

@ -72,9 +72,6 @@ class MusicBeatSubState extends FlxSubState implements IEventHandler
// Emergency exit button. // Emergency exit button.
if (FlxG.keys.justPressed.F4) FlxG.switchState(() -> new MainMenuState()); if (FlxG.keys.justPressed.F4) FlxG.switchState(() -> new MainMenuState());
// This can now be used in EVERY STATE YAY!
if (FlxG.keys.justPressed.F5) debug_refreshModules();
// Display Conductor info in the watch window. // Display Conductor info in the watch window.
FlxG.watch.addQuick("musicTime", FlxG.sound.music?.time ?? 0.0); FlxG.watch.addQuick("musicTime", FlxG.sound.music?.time ?? 0.0);
Conductor.watchQuick(conductorInUse); Conductor.watchQuick(conductorInUse);
@ -82,7 +79,7 @@ class MusicBeatSubState extends FlxSubState implements IEventHandler
dispatchEvent(new UpdateScriptEvent(elapsed)); dispatchEvent(new UpdateScriptEvent(elapsed));
} }
function debug_refreshModules() function reloadAssets()
{ {
PolymodHandler.forceReloadAssets(); PolymodHandler.forceReloadAssets();

View file

@ -284,6 +284,21 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/ */
public static final WELCOME_MUSIC_FADE_IN_DURATION:Float = 10.0; public static final WELCOME_MUSIC_FADE_IN_DURATION:Float = 10.0;
/**
* A map of the keys for every live input style.
*/
public static final LIVE_INPUT_KEYS:Map<ChartEditorLiveInputStyle, Array<FlxKey>> = [
NumberKeys => [
FIVE, SIX, SEVEN, EIGHT,
ONE, TWO, THREE, FOUR
],
WASDKeys => [
LEFT, DOWN, UP, RIGHT,
A, S, W, D
],
None => []
];
/** /**
* INSTANCE DATA * INSTANCE DATA
*/ */
@ -5146,46 +5161,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
function handlePlayhead():Void function handlePlayhead():Void
{ {
// Place notes at the playhead with the keyboard. // Place notes at the playhead with the keyboard.
switch (currentLiveInputStyle) for (note => key in LIVE_INPUT_KEYS[currentLiveInputStyle])
{ {
case ChartEditorLiveInputStyle.WASDKeys: if (FlxG.keys.checkStatus(key, JUST_PRESSED)) placeNoteAtPlayhead(note)
if (FlxG.keys.justPressed.A) placeNoteAtPlayhead(4); else if (FlxG.keys.checkStatus(key, JUST_RELEASED)) finishPlaceNoteAtPlayhead(note);
if (FlxG.keys.justReleased.A) finishPlaceNoteAtPlayhead(4);
if (FlxG.keys.justPressed.S) placeNoteAtPlayhead(5);
if (FlxG.keys.justReleased.S) finishPlaceNoteAtPlayhead(5);
if (FlxG.keys.justPressed.W) placeNoteAtPlayhead(6);
if (FlxG.keys.justReleased.W) finishPlaceNoteAtPlayhead(6);
if (FlxG.keys.justPressed.D) placeNoteAtPlayhead(7);
if (FlxG.keys.justReleased.D) finishPlaceNoteAtPlayhead(7);
if (FlxG.keys.justPressed.LEFT) placeNoteAtPlayhead(0);
if (FlxG.keys.justReleased.LEFT) finishPlaceNoteAtPlayhead(0);
if (FlxG.keys.justPressed.DOWN) placeNoteAtPlayhead(1);
if (FlxG.keys.justReleased.DOWN) finishPlaceNoteAtPlayhead(1);
if (FlxG.keys.justPressed.UP) placeNoteAtPlayhead(2);
if (FlxG.keys.justReleased.UP) finishPlaceNoteAtPlayhead(2);
if (FlxG.keys.justPressed.RIGHT) placeNoteAtPlayhead(3);
if (FlxG.keys.justReleased.RIGHT) finishPlaceNoteAtPlayhead(3);
case ChartEditorLiveInputStyle.NumberKeys:
// Flipped because Dad is on the left but represents data 0-3.
if (FlxG.keys.justPressed.ONE) placeNoteAtPlayhead(4);
if (FlxG.keys.justReleased.ONE) finishPlaceNoteAtPlayhead(4);
if (FlxG.keys.justPressed.TWO) placeNoteAtPlayhead(5);
if (FlxG.keys.justReleased.TWO) finishPlaceNoteAtPlayhead(5);
if (FlxG.keys.justPressed.THREE) placeNoteAtPlayhead(6);
if (FlxG.keys.justReleased.THREE) finishPlaceNoteAtPlayhead(6);
if (FlxG.keys.justPressed.FOUR) placeNoteAtPlayhead(7);
if (FlxG.keys.justReleased.FOUR) finishPlaceNoteAtPlayhead(7);
if (FlxG.keys.justPressed.FIVE) placeNoteAtPlayhead(0);
if (FlxG.keys.justReleased.FIVE) finishPlaceNoteAtPlayhead(0);
if (FlxG.keys.justPressed.SIX) placeNoteAtPlayhead(1);
if (FlxG.keys.justPressed.SEVEN) placeNoteAtPlayhead(2);
if (FlxG.keys.justReleased.SEVEN) finishPlaceNoteAtPlayhead(2);
if (FlxG.keys.justPressed.EIGHT) placeNoteAtPlayhead(3);
if (FlxG.keys.justReleased.EIGHT) finishPlaceNoteAtPlayhead(3);
case ChartEditorLiveInputStyle.None:
// Do nothing.
} }
// Place events at playhead. // Place events at playhead.

View file

@ -339,7 +339,7 @@ class FreeplayState extends MusicBeatSubState
// Only display songs which actually have available difficulties for the current character. // Only display songs which actually have available difficulties for the current character.
var displayedVariations = song.getVariationsByCharacter(currentCharacter); var displayedVariations = song.getVariationsByCharacter(currentCharacter);
trace('Displayed Variations (${songId}): $displayedVariations'); trace('Displayed Variations (${songId}): $displayedVariations');
var availableDifficultiesForSong:Array<String> = song.listDifficulties(displayedVariations, false); var availableDifficultiesForSong:Array<String> = song.listSuffixedDifficulties(displayedVariations, false, false);
trace('Available Difficulties: $availableDifficultiesForSong'); trace('Available Difficulties: $availableDifficultiesForSong');
if (availableDifficultiesForSong.length == 0) continue; if (availableDifficultiesForSong.length == 0) continue;
@ -1120,7 +1120,7 @@ class FreeplayState extends MusicBeatSubState
// NOW we can interact with the menu // NOW we can interact with the menu
busy = false; busy = false;
grpCapsules.members[curSelected].sparkle.alpha = 0.7; capsule.sparkle.alpha = 0.7;
playCurSongPreview(capsule); playCurSongPreview(capsule);
}, null); }, null);
@ -1674,6 +1674,9 @@ class FreeplayState extends MusicBeatSubState
songCapsule.init(null, null, null); songCapsule.init(null, null, null);
} }
} }
// Reset the song preview in case we changed variations (normal->erect etc)
playCurSongPreview();
} }
// Set the album graphic and play the animation if relevant. // Set the album graphic and play the animation if relevant.
@ -1912,8 +1915,10 @@ class FreeplayState extends MusicBeatSubState
} }
} }
public function playCurSongPreview(daSongCapsule:SongMenuItem):Void public function playCurSongPreview(?daSongCapsule:SongMenuItem):Void
{ {
if (daSongCapsule == null) daSongCapsule = grpCapsules.members[curSelected];
if (curSelected == 0) if (curSelected == 0)
{ {
FunkinSound.playMusic('freeplayRandom', FunkinSound.playMusic('freeplayRandom',
@ -2145,7 +2150,7 @@ class FreeplaySongData
function updateValues(variations:Array<String>):Void function updateValues(variations:Array<String>):Void
{ {
this.songDifficulties = song.listDifficulties(null, variations, false, false); this.songDifficulties = song.listSuffixedDifficulties(variations, false, false);
if (!this.songDifficulties.contains(currentDifficulty)) currentDifficulty = Constants.DEFAULT_DIFFICULTY; if (!this.songDifficulties.contains(currentDifficulty)) currentDifficulty = Constants.DEFAULT_DIFFICULTY;
var songDifficulty:SongDifficulty = song.getDifficulty(currentDifficulty, null, variations); var songDifficulty:SongDifficulty = song.getDifficulty(currentDifficulty, null, variations);
@ -2207,15 +2212,26 @@ class DifficultySprite extends FlxSprite
difficultyId = diffId; difficultyId = diffId;
if (Assets.exists(Paths.file('images/freeplay/freeplay${diffId}.xml'))) var assetDiffId:String = diffId;
while (!Assets.exists(Paths.image('freeplay/freeplay${assetDiffId}')))
{ {
this.frames = Paths.getSparrowAtlas('freeplay/freeplay${diffId}'); // Remove the last suffix of the difficulty id until we find an asset or there are no more suffixes.
var assetDiffIdParts:Array<String> = assetDiffId.split('-');
assetDiffIdParts.pop();
if (assetDiffIdParts.length == 0) break;
assetDiffId = assetDiffIdParts.join('-');
}
// Check for an XML to use an animation instead of an image.
if (Assets.exists(Paths.file('images/freeplay/freeplay${assetDiffId}.xml')))
{
this.frames = Paths.getSparrowAtlas('freeplay/freeplay${assetDiffId}');
this.animation.addByPrefix('idle', 'idle0', 24, true); this.animation.addByPrefix('idle', 'idle0', 24, true);
if (Preferences.flashingLights) this.animation.play('idle'); if (Preferences.flashingLights) this.animation.play('idle');
} }
else else
{ {
this.loadGraphic(Paths.image('freeplay/freeplay' + diffId)); this.loadGraphic(Paths.image('freeplay/freeplay' + assetDiffId));
} }
} }
} }

View file

@ -162,7 +162,7 @@ class SongMenuItem extends FlxSpriteGroup
sparkle = new FlxSprite(ranking.x, ranking.y); sparkle = new FlxSprite(ranking.x, ranking.y);
sparkle.frames = Paths.getSparrowAtlas('freeplay/sparkle'); sparkle.frames = Paths.getSparrowAtlas('freeplay/sparkle');
sparkle.animation.addByPrefix('sparkle', 'sparkle', 24, false); sparkle.animation.addByPrefix('sparkle', 'sparkle Export0', 24, false);
sparkle.animation.play('sparkle', true); sparkle.animation.play('sparkle', true);
sparkle.scale.set(0.8, 0.8); sparkle.scale.set(0.8, 0.8);
sparkle.blend = BlendMode.ADD; sparkle.blend = BlendMode.ADD;
@ -523,7 +523,6 @@ class SongMenuItem extends FlxSpriteGroup
checkWeek(songData?.songId); checkWeek(songData?.songId);
} }
var frameInTicker:Float = 0; var frameInTicker:Float = 0;
var frameInTypeBeat:Int = 0; var frameInTypeBeat:Int = 0;

View file

@ -0,0 +1,10 @@
package funkin.ui.options;
// Add enums for use with `EnumPreferenceItem` here!
/* Example:
class MyOptionEnum
{
public static inline var YuhUh = "true"; // "true" is the value's ID
public static inline var NuhUh = "false";
}
*/

View file

@ -8,6 +8,11 @@ import funkin.ui.AtlasText.AtlasFont;
import funkin.ui.options.OptionsState.Page; import funkin.ui.options.OptionsState.Page;
import funkin.graphics.FunkinCamera; import funkin.graphics.FunkinCamera;
import funkin.ui.TextMenuList.TextMenuItem; import funkin.ui.TextMenuList.TextMenuItem;
import funkin.audio.FunkinSound;
import funkin.ui.options.MenuItemEnums;
import funkin.ui.options.items.CheckboxPreferenceItem;
import funkin.ui.options.items.NumberPreferenceItem;
import funkin.ui.options.items.EnumPreferenceItem;
class PreferencesMenu extends Page class PreferencesMenu extends Page
{ {
@ -69,11 +74,51 @@ class PreferencesMenu extends Page
}, Preferences.autoPause); }, Preferences.autoPause);
} }
override function update(elapsed:Float):Void
{
super.update(elapsed);
// Indent the selected item.
items.forEach(function(daItem:TextMenuItem) {
var thyOffset:Int = 0;
// Initializing thy text width (if thou text present)
var thyTextWidth:Int = 0;
if (Std.isOfType(daItem, EnumPreferenceItem)) thyTextWidth = cast(daItem, EnumPreferenceItem).lefthandText.getWidth();
else if (Std.isOfType(daItem, NumberPreferenceItem)) thyTextWidth = cast(daItem, NumberPreferenceItem).lefthandText.getWidth();
if (thyTextWidth != 0)
{
// Magic number because of the weird offset thats being added by default
thyOffset += thyTextWidth - 75;
}
if (items.selectedItem == daItem)
{
thyOffset += 150;
}
else
{
thyOffset += 120;
}
daItem.x = thyOffset;
});
}
// - Preference item creation methods -
// Should be moved into a separate PreferenceItems class but you can't access PreferencesMenu.items and PreferencesMenu.preferenceItems from outside.
/**
* Creates a pref item that works with booleans
* @param onChange Gets called every time the player changes the value; use this to apply the value
* @param defaultValue The value that is loaded in when the pref item is created (usually your Preferences.settingVariable)
*/
function createPrefItemCheckbox(prefName:String, prefDesc:String, onChange:Bool->Void, defaultValue:Bool):Void function createPrefItemCheckbox(prefName:String, prefDesc:String, onChange:Bool->Void, defaultValue:Bool):Void
{ {
var checkbox:CheckboxPreferenceItem = new CheckboxPreferenceItem(0, 120 * (items.length - 1 + 1), defaultValue); var checkbox:CheckboxPreferenceItem = new CheckboxPreferenceItem(0, 120 * (items.length - 1 + 1), defaultValue);
items.createItem(120, (120 * items.length) + 30, prefName, AtlasFont.BOLD, function() { items.createItem(0, (120 * items.length) + 30, prefName, AtlasFont.BOLD, function() {
var value = !checkbox.currentValue; var value = !checkbox.currentValue;
onChange(value); onChange(value);
checkbox.currentValue = value; checkbox.currentValue = value;
@ -82,62 +127,54 @@ class PreferencesMenu extends Page
preferenceItems.add(checkbox); preferenceItems.add(checkbox);
} }
override function update(elapsed:Float) /**
* Creates a pref item that works with general numbers
* @param onChange Gets called every time the player changes the value; use this to apply the value
* @param valueFormatter Will get called every time the game needs to display the float value; use this to change how the displayed value looks
* @param defaultValue The value that is loaded in when the pref item is created (usually your Preferences.settingVariable)
* @param min Minimum value (example: 0)
* @param max Maximum value (example: 10)
* @param step The value to increment/decrement by (default = 0.1)
* @param precision Rounds decimals up to a `precision` amount of digits (ex: 4 -> 0.1234, 2 -> 0.12)
*/
function createPrefItemNumber(prefName:String, prefDesc:String, onChange:Float->Void, ?valueFormatter:Float->String, defaultValue:Int, min:Int, max:Int,
step:Float = 0.1, precision:Int):Void
{ {
super.update(elapsed); var item = new NumberPreferenceItem(0, (120 * items.length) + 30, prefName, defaultValue, min, max, step, precision, onChange, valueFormatter);
items.addItem(prefName, item);
preferenceItems.add(item.lefthandText);
}
// Indent the selected item. /**
// TODO: Only do this on menu change? * Creates a pref item that works with number percentages
items.forEach(function(daItem:TextMenuItem) { * @param onChange Gets called every time the player changes the value; use this to apply the value
if (items.selectedItem == daItem) daItem.x = 150; * @param defaultValue The value that is loaded in when the pref item is created (usually your Preferences.settingVariable)
else * @param min Minimum value (default = 0)
daItem.x = 120; * @param max Maximum value (default = 100)
}); */
} function createPrefItemPercentage(prefName:String, prefDesc:String, onChange:Int->Void, defaultValue:Int, min:Int = 0, max:Int = 100):Void
} {
var newCallback = function(value:Float) {
class CheckboxPreferenceItem extends FlxSprite onChange(Std.int(value));
{ };
public var currentValue(default, set):Bool; var formatter = function(value:Float) {
return '${value}%';
public function new(x:Float, y:Float, defaultValue:Bool = false) };
{ var item = new NumberPreferenceItem(0, (120 * items.length) + 30, prefName, defaultValue, min, max, 10, 0, newCallback, formatter);
super(x, y); items.addItem(prefName, item);
preferenceItems.add(item.lefthandText);
frames = Paths.getSparrowAtlas('checkboxThingie'); }
animation.addByPrefix('static', 'Check Box unselected', 24, false);
animation.addByPrefix('checked', 'Check Box selecting animation', 24, false); /**
* Creates a pref item that works with enums
setGraphicSize(Std.int(width * 0.7)); * @param values Maps enum values to display strings _(ex: `NoteHitSoundType.PingPong => "Ping pong"`)_
updateHitbox(); * @param onChange Gets called every time the player changes the value; use this to apply the value
* @param defaultValue The value that is loaded in when the pref item is created (usually your Preferences.settingVariable)
this.currentValue = defaultValue; */
} function createPrefItemEnum(prefName:String, prefDesc:String, values:Map<String, String>, onChange:String->Void, defaultValue:String):Void
{
override function update(elapsed:Float) var item = new EnumPreferenceItem(0, (120 * items.length) + 30, prefName, values, defaultValue, onChange);
{ items.addItem(prefName, item);
super.update(elapsed); preferenceItems.add(item.lefthandText);
switch (animation.curAnim.name)
{
case 'static':
offset.set();
case 'checked':
offset.set(17, 70);
}
}
function set_currentValue(value:Bool):Bool
{
if (value)
{
animation.play('checked', true);
}
else
{
animation.play('static');
}
return currentValue = value;
} }
} }

View file

@ -0,0 +1,49 @@
package funkin.ui.options.items;
import flixel.FlxSprite.FlxSprite;
class CheckboxPreferenceItem extends FlxSprite
{
public var currentValue(default, set):Bool;
public function new(x:Float, y:Float, defaultValue:Bool = false)
{
super(x, y);
frames = Paths.getSparrowAtlas('checkboxThingie');
animation.addByPrefix('static', 'Check Box unselected', 24, false);
animation.addByPrefix('checked', 'Check Box selecting animation', 24, false);
setGraphicSize(Std.int(width * 0.7));
updateHitbox();
this.currentValue = defaultValue;
}
override function update(elapsed:Float)
{
super.update(elapsed);
switch (animation.curAnim.name)
{
case 'static':
offset.set();
case 'checked':
offset.set(17, 70);
}
}
function set_currentValue(value:Bool):Bool
{
if (value)
{
animation.play('checked', true);
}
else
{
animation.play('static');
}
return currentValue = value;
}
}

View file

@ -0,0 +1,84 @@
package funkin.ui.options.items;
import funkin.ui.TextMenuList;
import funkin.ui.AtlasText;
import funkin.input.Controls;
import funkin.ui.options.MenuItemEnums;
import haxe.EnumTools;
/**
* Preference item that allows the player to pick a value from an enum (list of values)
*/
class EnumPreferenceItem extends TextMenuItem
{
function controls():Controls
{
return PlayerSettings.player1.controls;
}
public var lefthandText:AtlasText;
public var currentValue:String;
public var onChangeCallback:Null<String->Void>;
public var map:Map<String, String>;
public var keys:Array<String> = [];
var index = 0;
public function new(x:Float, y:Float, name:String, map:Map<String, String>, defaultValue:String, ?callback:String->Void)
{
super(x, y, name, function() {
callback(this.currentValue);
});
updateHitbox();
this.map = map;
this.currentValue = defaultValue;
this.onChangeCallback = callback;
var i:Int = 0;
for (key in map.keys())
{
this.keys.push(key);
if (this.currentValue == key) index = i;
i += 1;
}
lefthandText = new AtlasText(15, y, formatted(defaultValue), AtlasFont.DEFAULT);
}
override function update(elapsed:Float):Void
{
super.update(elapsed);
// var fancyTextFancyColor:Color;
if (selected)
{
var shouldDecrease:Bool = controls().UI_LEFT_P;
var shouldIncrease:Bool = controls().UI_RIGHT_P;
if (shouldDecrease) index -= 1;
if (shouldIncrease) index += 1;
if (index > keys.length - 1) index = 0;
if (index < 0) index = keys.length - 1;
currentValue = keys[index];
if (onChangeCallback != null && (shouldIncrease || shouldDecrease))
{
onChangeCallback(currentValue);
}
}
lefthandText.text = formatted(currentValue);
}
function formatted(value:String):String
{
// FIXME: Can't add arrows around the text because the font doesn't support < >
// var leftArrow:String = selected ? '<' : '';
// var rightArrow:String = selected ? '>' : '';
return '${map.get(value) ?? value}';
}
}

View file

@ -0,0 +1,136 @@
package funkin.ui.options.items;
import funkin.ui.TextMenuList;
import funkin.ui.AtlasText;
import funkin.input.Controls;
/**
* Preference item that allows the player to pick a value between min and max
*/
class NumberPreferenceItem extends TextMenuItem
{
function controls():Controls
{
return PlayerSettings.player1.controls;
}
// Widgets
public var lefthandText:AtlasText;
// Constants
static final HOLD_DELAY:Float = 0.3; // seconds
static final CHANGE_RATE:Float = 0.08; // seconds
// Constructor-initialized variables
public var currentValue:Float;
public var min:Float;
public var max:Float;
public var step:Float;
public var precision:Int;
public var onChangeCallback:Null<Float->Void>;
public var valueFormatter:Null<Float->String>;
// Variables
var holdDelayTimer:Float = HOLD_DELAY; // seconds
var changeRateTimer:Float = 0.0; // seconds
/**
* @param min Minimum value (example: 0)
* @param max Maximum value (example: 100)
* @param step The value to increment/decrement by (example: 10)
* @param callback Will get called every time the user changes the setting; use this to apply/save the setting.
* @param valueFormatter Will get called every time the game needs to display the float value; use this to change how the displayed string looks
*/
public function new(x:Float, y:Float, name:String, defaultValue:Float, min:Float, max:Float, step:Float, precision:Int, ?callback:Float->Void,
?valueFormatter:Float->String):Void
{
super(x, y, name, function() {
callback(this.currentValue);
});
lefthandText = new AtlasText(15, y, formatted(defaultValue), AtlasFont.DEFAULT);
updateHitbox();
this.currentValue = defaultValue;
this.min = min;
this.max = max;
this.step = step;
this.precision = precision;
this.onChangeCallback = callback;
this.valueFormatter = valueFormatter;
}
override function update(elapsed:Float):Void
{
super.update(elapsed);
// var fancyTextFancyColor:Color;
if (selected)
{
holdDelayTimer -= elapsed;
if (holdDelayTimer <= 0.0)
{
changeRateTimer -= elapsed;
}
var jpLeft:Bool = controls().UI_LEFT_P;
var jpRight:Bool = controls().UI_RIGHT_P;
if (jpLeft || jpRight)
{
holdDelayTimer = HOLD_DELAY;
changeRateTimer = 0.0;
}
var shouldDecrease:Bool = jpLeft;
var shouldIncrease:Bool = jpRight;
if (controls().UI_LEFT && holdDelayTimer <= 0.0 && changeRateTimer <= 0.0)
{
shouldDecrease = true;
changeRateTimer = CHANGE_RATE;
}
else if (controls().UI_RIGHT && holdDelayTimer <= 0.0 && changeRateTimer <= 0.0)
{
shouldIncrease = true;
changeRateTimer = CHANGE_RATE;
}
// Actually increasing/decreasing the value
if (shouldDecrease)
{
var isBelowMin:Bool = currentValue - step < min;
currentValue = (currentValue - step).clamp(min, max);
if (onChangeCallback != null && !isBelowMin) onChangeCallback(currentValue);
}
else if (shouldIncrease)
{
var isAboveMax:Bool = currentValue + step > max;
currentValue = (currentValue + step).clamp(min, max);
if (onChangeCallback != null && !isAboveMax) onChangeCallback(currentValue);
}
}
lefthandText.text = formatted(currentValue);
}
/** Turns the float into a string */
function formatted(value:Float):String
{
var float:Float = toFixed(value);
if (valueFormatter != null)
{
return valueFormatter(float);
}
else
{
return '${float}';
}
}
function toFixed(value:Float):Float
{
var multiplier:Float = Math.pow(10, precision);
return Math.floor(value * multiplier) / multiplier;
}
}

View file

@ -1,6 +1,9 @@
package funkin.util.plugins; package funkin.util.plugins;
import flixel.FlxG;
import flixel.FlxBasic; import flixel.FlxBasic;
import funkin.ui.MusicBeatState;
import funkin.ui.MusicBeatSubState;
/** /**
* A plugin which adds functionality to press `F5` to reload all game assets, then reload the current state. * A plugin which adds functionality to press `F5` to reload all game assets, then reload the current state.
@ -28,10 +31,15 @@ class ReloadAssetsDebugPlugin extends FlxBasic
if (FlxG.keys.justPressed.F5) if (FlxG.keys.justPressed.F5)
#end #end
{ {
funkin.modding.PolymodHandler.forceReloadAssets(); var state:Dynamic = FlxG.state;
if (state is MusicBeatState || state is MusicBeatSubState) state.reloadAssets();
else
{
funkin.modding.PolymodHandler.forceReloadAssets();
// Create a new instance of the current state, so old data is cleared. // Create a new instance of the current state, so old data is cleared.
FlxG.resetState(); FlxG.resetState();
}
} }
} }