diff --git a/.gitignore b/.gitignore
index 9f24f450e..8a4d131df 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,7 +2,4 @@ export/
.vscode/
APIStuff.hx
.DS_STORE
-
-example_mods/enaSkin/
-
-example_mods/tricky/
+RECOVER_*.fla
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 4afcd170e..9a277f85c 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -12,6 +12,21 @@
"name": "Haxe Eval",
"type": "haxe-eval",
"request": "launch"
- }
+ },
+ {
+ "name": "HTML5 Debug",
+ "type": "chrome",
+ "request": "launch",
+ "url": "http://127.0.0.1:3001",
+ "sourceMaps": true,
+ "webRoot": "${workspaceFolder}",
+ "preLaunchTask": "debug: html"
+ },
+ {
+ "name": "Mac (Debug)",
+ "type": "hxcpp",
+ "request": "launch",
+ "program": "${workspaceRoot}/export/debug/macos/bin/Funkin.app/Contents/MacOS/Funkin",
+ "preLaunchTask": "debug: mac"
]
}
diff --git a/Project.xml b/Project.xml
index 1382f5f71..89402c2eb 100644
--- a/Project.xml
+++ b/Project.xml
@@ -96,7 +96,13 @@
-
+
@@ -122,7 +128,7 @@
-
+
@@ -191,7 +197,7 @@
-
+
diff --git a/example_mods/introMod/_polymod_meta.json b/example_mods/introMod/_polymod_meta.json
index ccd7405d8..4c7fd742e 100644
--- a/example_mods/introMod/_polymod_meta.json
+++ b/example_mods/introMod/_polymod_meta.json
@@ -1,7 +1,9 @@
{
"title": "Intro Mod",
"description": "An introductory mod.",
- "author": "MasterEric",
+ "contributors": [{
+ "name": "MasterEric"
+ }],
"api_version": "0.1.0",
"mod_version": "1.0.0",
"license": "Apache-2.0"
diff --git a/example_mods/testing123/_polymod_meta.json b/example_mods/testing123/_polymod_meta.json
index 0c134dd9f..1a7766820 100644
--- a/example_mods/testing123/_polymod_meta.json
+++ b/example_mods/testing123/_polymod_meta.json
@@ -1,7 +1,9 @@
{
"title": "Testing123",
"description": "Newgrounds? More like OLDGROUNDS lol.",
- "author": "MasterEric",
+ "contributors": [{
+ "name": "MasterEric"
+ }],
"api_version": "0.1.0",
"mod_version": "1.0.0",
"license": "Apache-2.0"
diff --git a/source/Main.hx b/source/Main.hx
index fb2a01a69..191ebef53 100644
--- a/source/Main.hx
+++ b/source/Main.hx
@@ -1,9 +1,9 @@
package;
-import funkin.InitState;
-import funkin.MemoryCounter;
import flixel.FlxGame;
import flixel.FlxState;
+import funkin.InitState;
+import funkin.MemoryCounter;
import openfl.Lib;
import openfl.display.FPS;
import openfl.display.Sprite;
@@ -37,17 +37,9 @@ class Main extends Sprite
{
super();
- // TODO: Ideally this should change to utilize a user interface.
- // 1. Call PolymodHandler.getAllMods(). This gives you an array of ModMetadata items,
- // each of which contains information about the mod including an icon.
- // 2. Provide an interface to enable, disable, and reorder enabled mods.
- // A design similar to that of Minecraft resource packs would be intuitive.
- // 3. The interface should save (to the save file) and output an ordered array of mod IDs.
- // 4. Replace the call to PolymodHandler.loadAllMods() with a call to PolymodHandler.loadModsById(ids:Array).
+ // TODO: Replace this with loadEnabledMods().
funkin.modding.PolymodHandler.loadAllMods();
- funkin.i18n.FireTongueHandler.init();
-
if (stage != null)
{
init();
diff --git a/source/funkin/Conductor.hx b/source/funkin/Conductor.hx
index 765686740..331dd46f7 100644
--- a/source/funkin/Conductor.hx
+++ b/source/funkin/Conductor.hx
@@ -15,9 +15,31 @@ typedef BPMChangeEvent =
class Conductor
{
+ /**
+ * Beats per minute of the song.
+ */
public static var bpm:Float = 100;
- public static var crochet:Float = ((60 / bpm) * 1000); // beats in milliseconds
- public static var stepCrochet:Float = crochet / 4; // steps in milliseconds
+
+ /**
+ * Duration of a beat in millisecond.
+ */
+ public static var crochet(get, null):Float;
+
+ static function get_crochet():Float
+ {
+ return ((60 / bpm) * 1000);
+ }
+
+ /**
+ * Duration of a step in milliseconds.
+ */
+ public static var stepCrochet(get, null):Float;
+
+ static function get_stepCrochet():Float
+ {
+ return crochet / 4;
+ }
+
public static var songPosition:Float;
public static var lastSongPos:Float;
public static var offset:Float = 0;
@@ -52,12 +74,4 @@ class Conductor
}
trace("new BPM map BUDDY " + bpmChangeMap);
}
-
- public static function changeBPM(newBpm:Float)
- {
- bpm = newBpm;
-
- crochet = ((60 / bpm) * 1000);
- stepCrochet = crochet / 4;
- }
}
diff --git a/source/funkin/GameOverSubstate.hx b/source/funkin/GameOverSubstate.hx
index 3c397ee78..40cf9c0ca 100644
--- a/source/funkin/GameOverSubstate.hx
+++ b/source/funkin/GameOverSubstate.hx
@@ -1,14 +1,11 @@
package funkin;
import flixel.FlxObject;
-import flixel.FlxSubState;
-import flixel.math.FlxPoint;
import flixel.system.FlxSound;
import flixel.util.FlxColor;
import flixel.util.FlxTimer;
-import haxe.display.Display;
-import funkin.ui.PreferencesMenu;
import funkin.play.PlayState;
+import funkin.ui.PreferencesMenu;
class GameOverSubstate extends MusicBeatSubstate
{
@@ -57,7 +54,7 @@ class GameOverSubstate extends MusicBeatSubstate
add(camFollow);
FlxG.sound.play(Paths.sound('fnf_loss_sfx' + stageSuffix));
- // Conductor.changeBPM(100);
+ // Conductor.bpm = 100;
switch (PlayState.currentSong.player1)
{
diff --git a/source/funkin/LatencyState.hx b/source/funkin/LatencyState.hx
index 4e0ca07af..0239bea53 100644
--- a/source/funkin/LatencyState.hx
+++ b/source/funkin/LatencyState.hx
@@ -31,7 +31,7 @@ class LatencyState extends FlxState
strumLine = new FlxSprite(FlxG.width / 2, 100).makeGraphic(FlxG.width, 5);
add(strumLine);
- Conductor.changeBPM(120);
+ Conductor.bpm = 120;
super.create();
}
diff --git a/source/funkin/LoadingState.hx b/source/funkin/LoadingState.hx
index b163b49d0..ad649bb03 100644
--- a/source/funkin/LoadingState.hx
+++ b/source/funkin/LoadingState.hx
@@ -2,9 +2,9 @@ package funkin;
import flixel.FlxSprite;
import flixel.FlxState;
-import flixel.graphics.frames.FlxAtlasFrames;
import flixel.math.FlxMath;
import flixel.util.FlxTimer;
+import funkin.play.PlayState;
import haxe.io.Path;
import lime.app.Future;
import lime.app.Promise;
@@ -12,7 +12,6 @@ import lime.utils.AssetLibrary;
import lime.utils.AssetManifest;
import lime.utils.Assets as LimeAssets;
import openfl.utils.Assets;
-import funkin.play.PlayState;
class LoadingState extends MusicBeatState
{
@@ -21,7 +20,6 @@ class LoadingState extends MusicBeatState
var target:FlxState;
var stopMusic = false;
var callbacks:MultiCallback;
-
var danceLeft = false;
var loadBar:FlxSprite;
@@ -117,17 +115,15 @@ class LoadingState extends MusicBeatState
}
}
- override function beatHit()
+ override function beatHit():Bool
{
- super.beatHit();
+ // super.beatHit() returns false if a module cancelled the event.
+ if (!super.beatHit())
+ return false;
- // logo.animation.play('bump');
danceLeft = !danceLeft;
- /*
- if (danceLeft)
- gfDance.animation.play('danceRight');
- else
- gfDance.animation.play('danceLeft'); */
+
+ return true;
}
var targetShit:Float = 0;
diff --git a/source/funkin/MusicBeatState.hx b/source/funkin/MusicBeatState.hx
index 2dce0272b..0fca6fb84 100644
--- a/source/funkin/MusicBeatState.hx
+++ b/source/funkin/MusicBeatState.hx
@@ -1,13 +1,21 @@
package funkin;
-import flixel.util.FlxColor;
+import flixel.FlxState;
+import flixel.FlxSubState;
+import flixel.addons.ui.FlxUIState;
import flixel.text.FlxText;
+import flixel.util.FlxColor;
+import flixel.util.FlxSort;
+import funkin.Conductor.BPMChangeEvent;
+import funkin.modding.PolymodHandler;
import funkin.modding.events.ScriptEvent;
import funkin.modding.module.ModuleHandler;
-import funkin.modding.events.ScriptEvent.UpdateScriptEvent;
-import funkin.Conductor.BPMChangeEvent;
-import flixel.addons.ui.FlxUIState;
+import funkin.util.SortUtil;
+/**
+ * MusicBeatState actually represents the core utility FlxState of the game.
+ * It includes functionality for event handling, as well as maintaining BPM-based update events.
+ */
class MusicBeatState extends FlxUIState
{
private var curStep:Int = 0;
@@ -21,13 +29,23 @@ class MusicBeatState extends FlxUIState
public var leftWatermarkText:FlxText = null;
public var rightWatermarkText:FlxText = null;
+ public function new()
+ {
+ super();
+
+ initCallbacks();
+ }
+
+ function initCallbacks()
+ {
+ subStateOpened.add(onOpenSubstateComplete);
+ subStateClosed.add(onCloseSubstateComplete);
+ }
+
override function create()
{
super.create();
- if (transIn != null)
- trace('reg ' + transIn.region);
-
createWatermarkText();
}
@@ -35,6 +53,10 @@ class MusicBeatState extends FlxUIState
{
super.update(elapsed);
+ // This can now be used in EVERY STATE YAY!
+ if (FlxG.keys.justPressed.F5)
+ debug_refreshModules();
+
// everyStep();
var oldStep:Int = curStep;
@@ -44,6 +66,8 @@ class MusicBeatState extends FlxUIState
if (oldStep != curStep && curStep >= 0)
stepHit();
+ FlxG.watch.addQuick("songPos", Conductor.songPosition);
+
dispatchEvent(new UpdateScriptEvent(elapsed));
}
@@ -71,6 +95,14 @@ class MusicBeatState extends FlxUIState
ModuleHandler.callEvent(event);
}
+ function debug_refreshModules()
+ {
+ PolymodHandler.forceReloadAssets();
+
+ // Restart the current state, so old data is cleared.
+ FlxG.resetState();
+ }
+
private function updateBeat():Void
{
curBeat = Math.floor(curStep / 4);
@@ -92,15 +124,97 @@ class MusicBeatState extends FlxUIState
curStep = lastChange.stepTime + Math.floor((Conductor.songPosition - lastChange.songTime) / Conductor.stepCrochet);
}
- public function stepHit():Void
+ public function stepHit():Bool
{
+ var event = new SongTimeScriptEvent(ScriptEvent.SONG_STEP_HIT, curBeat, curStep);
+
+ dispatchEvent(event);
+
+ if (event.eventCanceled)
+ {
+ return false;
+ }
+
if (curStep % 4 == 0)
beatHit();
+
+ return true;
}
- public function beatHit():Void
+ public function beatHit():Bool
{
+ var event = new SongTimeScriptEvent(ScriptEvent.SONG_BEAT_HIT, curBeat, curStep);
+
+ dispatchEvent(event);
+
+ if (event.eventCanceled)
+ {
+ return false;
+ }
+
lastBeatHitTime = Conductor.songPosition;
- // do literally nothing dumbass
+
+ return true;
+ }
+
+ /**
+ * Refreshes the state, by redoing the render order of all sprites.
+ * It does this based on the `zIndex` of each prop.
+ */
+ public function refresh()
+ {
+ sort(SortUtil.byZIndex, FlxSort.ASCENDING);
+ }
+
+ override function switchTo(nextState:FlxState):Bool
+ {
+ var event = new StateChangeScriptEvent(ScriptEvent.STATE_CHANGE_BEGIN, nextState, true);
+
+ dispatchEvent(event);
+
+ if (event.eventCanceled)
+ {
+ return false;
+ }
+
+ return super.switchTo(nextState);
+ }
+
+ public override function openSubState(targetSubstate:FlxSubState):Void
+ {
+ var event = new SubStateScriptEvent(ScriptEvent.SUBSTATE_OPEN_BEGIN, targetSubstate, true);
+
+ dispatchEvent(event);
+
+ if (event.eventCanceled)
+ {
+ return;
+ }
+
+ super.openSubState(targetSubstate);
+ }
+
+ function onOpenSubstateComplete(targetState:FlxSubState):Void
+ {
+ dispatchEvent(new SubStateScriptEvent(ScriptEvent.SUBSTATE_OPEN_END, targetState, true));
+ }
+
+ public override function closeSubState():Void
+ {
+ var event = new SubStateScriptEvent(ScriptEvent.SUBSTATE_CLOSE_BEGIN, this.subState, true);
+
+ dispatchEvent(event);
+
+ if (event.eventCanceled)
+ {
+ return;
+ }
+
+ super.closeSubState();
+ }
+
+ function onCloseSubstateComplete(targetState:FlxSubState):Void
+ {
+ dispatchEvent(new SubStateScriptEvent(ScriptEvent.SUBSTATE_CLOSE_END, targetState, true));
}
}
diff --git a/source/funkin/MusicBeatSubstate.hx b/source/funkin/MusicBeatSubstate.hx
index 8374848e4..db20bce34 100644
--- a/source/funkin/MusicBeatSubstate.hx
+++ b/source/funkin/MusicBeatSubstate.hx
@@ -1,8 +1,13 @@
package funkin;
-import funkin.Conductor.BPMChangeEvent;
import flixel.FlxSubState;
+import funkin.Conductor.BPMChangeEvent;
+import funkin.modding.events.ScriptEvent;
+import funkin.modding.module.ModuleHandler;
+/**
+ * MusicBeatSubstate reincorporates the functionality of MusicBeatState into an FlxSubState.
+ */
class MusicBeatSubstate extends FlxSubState
{
public function new()
@@ -53,6 +58,11 @@ class MusicBeatSubstate extends FlxSubState
beatHit();
}
+ function dispatchEvent(event:ScriptEvent)
+ {
+ ModuleHandler.callEvent(event);
+ }
+
public function beatHit():Void
{
// do literally nothing dumbass
diff --git a/source/funkin/TitleState.hx b/source/funkin/TitleState.hx
index e5ce1d21c..6dfd5c8b7 100644
--- a/source/funkin/TitleState.hx
+++ b/source/funkin/TitleState.hx
@@ -4,13 +4,7 @@ import flixel.FlxObject;
import flixel.FlxSprite;
import flixel.FlxState;
import flixel.group.FlxGroup;
-import flixel.input.android.FlxAndroidKey;
-import flixel.input.android.FlxAndroidKeys;
import flixel.input.gamepad.FlxGamepad;
-import flixel.input.gamepad.id.SwitchJoyconLeftID;
-import flixel.math.FlxPoint;
-import flixel.math.FlxRect;
-import flixel.text.FlxText;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.util.FlxColor;
@@ -19,29 +13,20 @@ import funkin.audiovis.SpectogramSprite;
import funkin.shaderslmfao.BuildingShaders;
import funkin.shaderslmfao.ColorSwap;
import funkin.shaderslmfao.TitleOutline;
-import funkin.ui.PreferencesMenu;
-import lime.app.Application;
-import lime.graphics.Image;
-import lime.media.AudioContext;
-import lime.ui.Window;
+import funkin.ui.AtlasText;
+import funkin.util.Constants;
import openfl.Assets;
import openfl.display.Sprite;
import openfl.events.AsyncErrorEvent;
-import openfl.events.Event;
import openfl.events.MouseEvent;
import openfl.events.NetStatusEvent;
import openfl.media.Video;
-import openfl.net.NetConnection;
import openfl.net.NetStream;
using StringTools;
#if desktop
-import sys.FileSystem;
-import sys.io.File;
-import sys.thread.Thread;
#end
-
class TitleState extends MusicBeatState
{
public static var initialized:Bool = false;
@@ -73,33 +58,33 @@ class TitleState extends MusicBeatState
super.create();
/*
- #elseif web
+ #elseif web
- if (!initialized)
- {
+ if (!initialized)
+ {
- video = new Video();
- FlxG.stage.addChild(video);
+ video = new Video();
+ FlxG.stage.addChild(video);
- var netConnection = new NetConnection();
- netConnection.connect(null);
+ var netConnection = new NetConnection();
+ netConnection.connect(null);
- netStream = new NetStream(netConnection);
- netStream.client = {onMetaData: client_onMetaData};
- netStream.addEventListener(AsyncErrorEvent.ASYNC_ERROR, netStream_onAsyncError);
- netConnection.addEventListener(NetStatusEvent.NET_STATUS, netConnection_onNetStatus);
- // netStream.addEventListener(NetStatusEvent.NET_STATUS) // netStream.play(Paths.file('music/kickstarterTrailer.mp4'));
+ netStream = new NetStream(netConnection);
+ netStream.client = {onMetaData: client_onMetaData};
+ netStream.addEventListener(AsyncErrorEvent.ASYNC_ERROR, netStream_onAsyncError);
+ netConnection.addEventListener(NetStatusEvent.NET_STATUS, netConnection_onNetStatus);
+ // netStream.addEventListener(NetStatusEvent.NET_STATUS) // netStream.play(Paths.file('music/kickstarterTrailer.mp4'));
- overlay = new Sprite();
- overlay.graphics.beginFill(0, 0.5);
- overlay.graphics.drawRect(0, 0, 1280, 720);
- overlay.addEventListener(MouseEvent.MOUSE_DOWN, overlay_onMouseDown);
+ overlay = new Sprite();
+ overlay.graphics.beginFill(0, 0.5);
+ overlay.graphics.drawRect(0, 0, 1280, 720);
+ overlay.addEventListener(MouseEvent.MOUSE_DOWN, overlay_onMouseDown);
- overlay.buttonMode = true;
- // FlxG.stage.addChild(overlay);
+ overlay.buttonMode = true;
+ // FlxG.stage.addChild(overlay);
- }
+ }
*/
// netConnection.addEventListener(MouseEvent.MOUSE_DOWN, overlay_onMouseDown);
@@ -158,9 +143,9 @@ class TitleState extends MusicBeatState
{
FlxG.sound.playMusic(Paths.music('freakyMenu'), 0);
FlxG.sound.music.fadeIn(4, 0, 0.7);
+ Conductor.bpm = Constants.FREAKY_MENU_BPM;
}
- Conductor.changeBPM(102);
persistentUpdate = true;
var bg:FlxSprite = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, FlxColor.BLACK);
@@ -189,8 +174,6 @@ class TitleState extends MusicBeatState
gfDance.antialiasing = true;
add(gfDance);
- trace('MACRO TEST: ${gfDance.zIndex}');
-
// alphaShader.shader.funnyShit.input = gfDance.pixels; // old shit
logoBl.shader = alphaShader.shader;
@@ -220,6 +203,7 @@ class TitleState extends MusicBeatState
blackScreen = bg.clone();
credGroup.add(blackScreen);
+ credGroup.add(textGroup);
// var atlasBullShit:FlxSprite = new FlxSprite();
// atlasBullShit.frames = CoolUtil.fromAnimate(Paths.image('money'), Paths.file('images/money.json'));
@@ -290,13 +274,13 @@ class TitleState extends MusicBeatState
#end
/* if (FlxG.onMobile)
+ {
+ if (gfDance != null)
{
- if (gfDance != null)
- {
- gfDance.x = (FlxG.width / 2) + (FlxG.accelerometer.x * (FlxG.width / 2));
- // gfDance.y = (FlxG.height / 2) + (FlxG.accelerometer.y * (FlxG.height / 2));
- }
+ gfDance.x = (FlxG.width / 2) + (FlxG.accelerometer.x * (FlxG.width / 2));
+ // gfDance.y = (FlxG.height / 2) + (FlxG.accelerometer.y * (FlxG.height / 2));
}
+ }
*/
if (FlxG.keys.justPressed.I)
{
@@ -313,37 +297,37 @@ class TitleState extends MusicBeatState
}
/*
- FlxG.watch.addQuick('cur display', FlxG.stage.window.display.id);
- if (FlxG.keys.justPressed.Y)
+ FlxG.watch.addQuick('cur display', FlxG.stage.window.display.id);
+ if (FlxG.keys.justPressed.Y)
+ {
+ // trace(FlxG.stage.window.display.name);
+
+ if (FlxG.gamepads.firstActive != null)
{
- // trace(FlxG.stage.window.display.name);
-
- if (FlxG.gamepads.firstActive != null)
- {
- trace(FlxG.gamepads.firstActive.model);
- FlxG.gamepads.firstActive.id
- }
- else
- trace('gamepad null');
-
- // FlxG.stage.window.title = Std.string(FlxG.random.int(0, 20000));
- // FlxG.stage.window.setIcon(Image.fromFile('assets/images/icon16.png'));
- // FlxG.stage.window.readPixels;
-
- if (FlxG.stage.window.width == Std.int(FlxG.stage.window.display.bounds.width))
- {
- FlxG.stage.window.width = 1280;
- FlxG.stage.window.height = 720;
- FlxG.stage.window.y = 30;
- }
- else
- {
- FlxG.stage.window.width = Std.int(FlxG.stage.window.display.bounds.width);
- FlxG.stage.window.height = Std.int(FlxG.stage.window.display.bounds.height);
- FlxG.stage.window.x = Std.int(FlxG.stage.window.display.bounds.x);
- FlxG.stage.window.y = Std.int(FlxG.stage.window.display.bounds.y);
- }
+ trace(FlxG.gamepads.firstActive.model);
+ FlxG.gamepads.firstActive.id
}
+ else
+ trace('gamepad null');
+
+ // FlxG.stage.window.title = Std.string(FlxG.random.int(0, 20000));
+ // FlxG.stage.window.setIcon(Image.fromFile('assets/images/icon16.png'));
+ // FlxG.stage.window.readPixels;
+
+ if (FlxG.stage.window.width == Std.int(FlxG.stage.window.display.bounds.width))
+ {
+ FlxG.stage.window.width = 1280;
+ FlxG.stage.window.height = 720;
+ FlxG.stage.window.y = 30;
+ }
+ else
+ {
+ FlxG.stage.window.width = Std.int(FlxG.stage.window.display.bounds.width);
+ FlxG.stage.window.height = Std.int(FlxG.stage.window.display.bounds.height);
+ FlxG.stage.window.x = Std.int(FlxG.stage.window.display.bounds.x);
+ FlxG.stage.window.y = Std.int(FlxG.stage.window.display.bounds.y);
+ }
+ }
*/
#if debug
@@ -382,13 +366,6 @@ class TitleState extends MusicBeatState
pressedEnter = true;
#end
}
-
- // a faster intro thing lol!
- if (pressedEnter && transitioning && skippedIntro)
- {
- FlxG.switchState(new MainMenuState());
- }
-
if (pressedEnter && !transitioning && skippedIntro)
{
if (FlxG.sound.music != null)
@@ -434,22 +411,23 @@ class TitleState extends MusicBeatState
Assets.cache.clear(Paths.image('logoBumpin'));
Assets.cache.clear(Paths.image('titleEnter'));
// ngSpr??
+ FlxG.switchState(targetState);
});
// FlxG.sound.play(Paths.music('titleShoot'), 0.7);
}
if (pressedEnter && !skippedIntro && initialized)
skipIntro();
/*
- #if web
- if (!initialized && controls.ACCEPT)
- {
- // netStream.dispose();
- // FlxG.stage.removeChild(video);
+ #if web
+ if (!initialized && controls.ACCEPT)
+ {
+ // netStream.dispose();
+ // FlxG.stage.removeChild(video);
- startIntro();
- skipIntro();
- }
- #end
+ startIntro();
+ skipIntro();
+ }
+ #end
*/
if (controls.UI_LEFT)
@@ -503,39 +481,47 @@ class TitleState extends MusicBeatState
var spec:SpectogramSprite = new SpectogramSprite(FlxG.sound.music);
add(spec);
- Conductor.changeBPM(190);
+ Conductor.bpm = 190;
FlxG.camera.flash(FlxColor.WHITE, 1);
FlxG.sound.play(Paths.sound('confirmMenu'), 0.7);
}
function createCoolText(textArray:Array)
{
+ if (credGroup == null || textGroup == null)
+ return;
+
for (i in 0...textArray.length)
{
- var money:Alphabet = new Alphabet(0, 0, textArray[i], true, false);
+ var money:AtlasText = new AtlasText(0, 0, textArray[i], AtlasFont.BOLD);
money.screenCenter(X);
money.y += (i * 60) + 200;
- credGroup.add(money);
+ // credGroup.add(money);
textGroup.add(money);
}
}
function addMoreText(text:String)
{
+ if (credGroup == null || textGroup == null)
+ return;
+
lime.ui.Haptic.vibrate(100, 100);
- var coolText:Alphabet = new Alphabet(0, 0, text, true, false);
+ var coolText:AtlasText = new AtlasText(0, 0, text, AtlasFont.BOLD);
coolText.screenCenter(X);
coolText.y += (textGroup.length * 60) + 200;
- credGroup.add(coolText);
textGroup.add(coolText);
}
function deleteCoolText()
{
+ if (credGroup == null || textGroup == null)
+ return;
+
while (textGroup.members.length > 0)
{
- credGroup.remove(textGroup.members[0], true);
+ // credGroup.remove(textGroup.members[0], true);
textGroup.remove(textGroup.members[0], true);
}
}
@@ -543,9 +529,11 @@ class TitleState extends MusicBeatState
var isRainbow:Bool = false;
var skippedIntro:Bool = false;
- override function beatHit()
+ override function beatHit():Bool
{
- super.beatHit();
+ // super.beatHit() returns false if a module cancelled the event.
+ if (!super.beatHit())
+ return false;
if (!skippedIntro)
{
@@ -605,6 +593,8 @@ class TitleState extends MusicBeatState
else
gfDance.animation.play('danceLeft');
}
+
+ return true;
}
function skipIntro():Void
diff --git a/source/funkin/charting/ChartingState.hx b/source/funkin/charting/ChartingState.hx
index e8aea9da1..eb4b13663 100644
--- a/source/funkin/charting/ChartingState.hx
+++ b/source/funkin/charting/ChartingState.hx
@@ -1,12 +1,5 @@
package funkin.charting;
-import funkin.Conductor.BPMChangeEvent;
-import funkin.Note.NoteData;
-import funkin.Section.SwagSection;
-import funkin.SongLoad.SwagSong;
-import funkin.audiovis.ABotVis;
-import funkin.audiovis.PolygonSpectogram;
-import funkin.audiovis.SpectogramSprite;
import flixel.FlxSprite;
import flixel.addons.display.FlxGridOverlay;
import flixel.addons.transition.FlxTransitionableState;
@@ -23,14 +16,21 @@ import flixel.system.FlxSound;
import flixel.text.FlxText;
import flixel.ui.FlxButton;
import flixel.util.FlxColor;
+import funkin.Conductor.BPMChangeEvent;
+import funkin.Note.NoteData;
+import funkin.Section.SwagSection;
+import funkin.SongLoad.SwagSong;
+import funkin.audiovis.ABotVis;
+import funkin.audiovis.PolygonSpectogram;
+import funkin.audiovis.SpectogramSprite;
+import funkin.play.PlayState;
+import funkin.rendering.MeshRender;
import haxe.Json;
import lime.media.AudioBuffer;
import lime.utils.Assets;
import openfl.events.Event;
import openfl.events.IOErrorEvent;
import openfl.net.FileReference;
-import funkin.rendering.MeshRender;
-import funkin.play.PlayState;
using Lambda;
using StringTools;
@@ -144,7 +144,7 @@ class ChartingState extends MusicBeatState
updateGrid();
loadSong(_song.song);
- Conductor.changeBPM(_song.bpm);
+ Conductor.bpm = _song.bpm;
Conductor.mapBPMChanges(_song);
bpmTxt = new FlxText(1000, 50, 0, "", 16);
@@ -545,7 +545,7 @@ class ChartingState extends MusicBeatState
{
tempBpm = nums.value;
Conductor.mapBPMChanges(_song);
- Conductor.changeBPM(nums.value);
+ Conductor.bpm = nums.value;
}
else if (wname == 'note_susLength')
{
@@ -975,9 +975,9 @@ class ChartingState extends MusicBeatState
_song.bpm = tempBpm;
/* if (FlxG.keys.justPressed.UP)
- Conductor.changeBPM(Conductor.bpm + 1);
+ Conductor.bpm = Conductor.bpm + 1;
if (FlxG.keys.justPressed.DOWN)
- Conductor.changeBPM(Conductor.bpm - 1); */
+ Conductor.bpm = Conductor.bpm - 1; */
var shiftThing:Int = 1;
if (FlxG.keys.pressed.SHIFT)
@@ -1213,7 +1213,7 @@ class ChartingState extends MusicBeatState
if (SongLoad.getSong()[curSection].changeBPM && SongLoad.getSong()[curSection].bpm > 0)
{
- Conductor.changeBPM(SongLoad.getSong()[curSection].bpm);
+ Conductor.bpm = SongLoad.getSong()[curSection].bpm;
FlxG.log.add('CHANGED BPM!');
}
else
@@ -1223,7 +1223,7 @@ class ChartingState extends MusicBeatState
for (i in 0...curSection)
if (SongLoad.getSong()[i].changeBPM)
daBPM = SongLoad.getSong()[i].bpm;
- Conductor.changeBPM(daBPM);
+ Conductor.bpm = daBPM;
}
/* // PORT BULLSHIT, INCASE THERE'S NO SUSTAIN DATA FOR A NOTE
diff --git a/source/funkin/i18n/FireTongueHandler.hx b/source/funkin/i18n/FireTongueHandler.hx
deleted file mode 100644
index fdb13e41c..000000000
--- a/source/funkin/i18n/FireTongueHandler.hx
+++ /dev/null
@@ -1,114 +0,0 @@
-package funkin.i18n;
-
-import firetongue.FireTongue;
-
-class FireTongueHandler
-{
- static final DEFAULT_LOCALE = 'en-US';
- // static final DEFAULT_LOCALE = 'pt-BR';
- static final LOCALE_DIR = 'assets/locales/';
-
- static var tongue:FireTongue;
-
- /**
- * Initialize the FireTongue instance.
- * This will automatically start with the default locale for you.
- */
- public static function init():Void
- {
- tongue = new FireTongue(OPENFL, // Haxe framework being used.
- // This should really have been a parameterized object...
- null, // Function to check if a file exists. Specify null to use the one from the framework.
- null, // Function to retrieve the text of a file. Specify null to use the one from the framework.
- null, // Function to get a list of files in a directory. Specify null to use the one from the framework.
- firetongue.FireTongue.Case.Upper);
-
- // TODO: Make this use the language from the user's preferences.
- setLanguage(DEFAULT_LOCALE);
-
- trace('[FIRETONGUE] Initialized. Available locales: ${tongue.locales.join(', ')}');
- }
-
- /**
- * Switch the language used by FireTongue.
- * @param locale The name of the locale to use, such as `en-US`.
- */
- public static function setLanguage(locale:String):Void
- {
- tongue.initialize({
- locale: locale, // The locale to load.
-
- finishedCallback: onFinishLoad, // Function run when the locale is loaded.
- directory: LOCALE_DIR, // Folder (relative to assets/) to load data from.
- replaceMissing: false, // If true, missing flags fallback to the default locale.
- checkMissing: true, // If true, check for and store the list of missing flags for this locale.
- });
- }
-
- /**
- * Function called when FireTongue finishes loading a language.
- */
- static function onFinishLoad()
- {
- if (tongue == null)
- return;
-
- trace('[FIRETONGUE] Finished loading locale: ${tongue.locale}');
- if (tongue.missingFlags != null)
- {
- if (tongue.missingFlags.get(tongue.locale) != null && tongue.missingFlags.get(tongue.locale).length != 0)
- {
- trace('[FIRETONGUE] Missing flags: ${tongue.missingFlags.get(tongue.locale).join(', ')}');
- }
- else
- {
- trace('[FIRETONGUE] No missing flags for this locale. (Note: Another locale has missing flags.)');
- }
- }
- else
- {
- trace('[FIRETONGUE] No missing flags.');
- }
-
- trace('[FIRETONGUE] HELLO_WORLD = ${t("HELLO_WORLD")}');
- }
-
- /**
- * Retrieve a localized string based on the given key.
- *
- * Example:
- * import i18n.FiretongueHandler.t;
- * trace(t('HELLO')); // Prints "Hello!"
- *
- * @param key The key to use to retrieve the localized string.
- * @param context The data file to load the key from.
- * @return The localized string.
- */
- public static function t(key:String, context:String = 'data'):String
- {
- // The localization strings can be stored all in one file,
- // or split into several contexts.
- return tongue.get(key, context);
- }
-
- /**
- * Retrieve a localized string while replacing specific values.
- * In this way, you can use the same invocation call to properly localize
- * a variety of different languages with distinct grammar.
- *
- * Example:
- * import i18n.FiretongueHandler.f;
- * trace(f('COLLECT_X_APPLES', 'data', [''], ['10']); // Prints "Collect 10 apples!"
- *
- * @param key The key to use to retrieve the localized string.
- * @param context The data file to load the key from.
- * @param flags The flags to replace in the string.
- * @param values The values to replace those flags with.
- * @return The localized string.
- */
- public static function f(key:String, context:String = 'data', flags:Array = null, values:Array = null):String
- {
- var str = t(key, context);
- return firetongue.Replace.flags(str, flags, values);
- }
-}
diff --git a/source/funkin/i18n/README.md b/source/funkin/i18n/README.md
deleted file mode 100644
index d55a8f41c..000000000
--- a/source/funkin/i18n/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# i18n
-
-This package contains functions used for internationalization (i18n).
diff --git a/source/funkin/modding/IScriptedClass.hx b/source/funkin/modding/IScriptedClass.hx
index 9691c1417..4261032a2 100644
--- a/source/funkin/modding/IScriptedClass.hx
+++ b/source/funkin/modding/IScriptedClass.hx
@@ -23,6 +23,20 @@ interface IStateChangingScriptedClass extends IScriptedClass
{
public function onStateChangeBegin(event:StateChangeScriptEvent):Void;
public function onStateChangeEnd(event:StateChangeScriptEvent):Void;
+
+ public function onSubstateOpenBegin(event:SubStateScriptEvent):Void;
+ public function onSubstateOpenEnd(event:SubStateScriptEvent):Void;
+ public function onSubstateCloseBegin(event:SubStateScriptEvent):Void;
+ public function onSubstateCloseEnd(event:SubStateScriptEvent):Void;
+}
+
+/**
+ * Defines a set of callbacks available to scripted classes which represent notes.
+ */
+interface INoteScriptedClass extends IScriptedClass
+{
+ public function onNoteHit(event:NoteScriptEvent):Void;
+ public function onNoteMiss(event:NoteScriptEvent):Void;
}
/**
@@ -40,18 +54,18 @@ interface IStateChangingScriptedClass extends IScriptedClass
*/
interface IPlayStateScriptedClass extends IScriptedClass
{
- public function onPause(event:ScriptEvent):Void;
+ public function onPause(event:PauseScriptEvent):Void;
public function onResume(event:ScriptEvent):Void;
public function onSongLoaded(eent:SongLoadScriptEvent):Void;
public function onSongStart(event:ScriptEvent):Void;
public function onSongEnd(event:ScriptEvent):Void;
- public function onSongReset(event:ScriptEvent):Void;
public function onGameOver(event:ScriptEvent):Void;
- public function onGameRetry(event:ScriptEvent):Void;
+ public function onSongRetry(event:ScriptEvent):Void;
public function onNoteHit(event:NoteScriptEvent):Void;
public function onNoteMiss(event:NoteScriptEvent):Void;
+ public function onNoteGhostMiss(event:GhostMissNoteScriptEvent):Void;
public function onStepHit(event:SongTimeScriptEvent):Void;
public function onBeatHit(event:SongTimeScriptEvent):Void;
diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx
index 1f6d149cb..407f3ec85 100644
--- a/source/funkin/modding/PolymodHandler.hx
+++ b/source/funkin/modding/PolymodHandler.hx
@@ -1,10 +1,9 @@
package funkin.modding;
-import polymod.Polymod.ModMetadata;
+import funkin.modding.module.ModuleHandler;
+import funkin.play.stage.StageData;
import polymod.Polymod;
-import polymod.backends.OpenFLBackend;
import polymod.backends.PolymodAssets.PolymodAssetType;
-import polymod.format.ParseRules.LinesParseFormat;
import polymod.format.ParseRules.TextFileFormat;
class PolymodHandler
@@ -31,6 +30,15 @@ class PolymodHandler
loadModsById(getAllModIds());
}
+ /**
+ * Loads the game with configured mods enabled with Polymod.
+ */
+ public static function loadEnabledMods()
+ {
+ trace("Initializing Polymod (using configured mods)...");
+ loadModsById(getEnabledModIds());
+ }
+
/**
* Loads the game without any mods enabled with Polymod.
*/
@@ -146,8 +154,8 @@ class PolymodHandler
{
return {
assetLibraryPaths: [
- "songs" => "songs", "shared" => "", "tutorial" => "tutorial", "scripts" => "scripts", "week1" => "week1", "week2" => "week2",
- "week3" => "week3", "week4" => "week4", "week5" => "week5", "week6" => "week6", "week7" => "week7", "week8" => "week8",
+ "songs" => "songs", "shared" => "", "tutorial" => "tutorial", "scripts" => "scripts", "week1" => "week1", "week2" => "week2",
+ "week3" => "week3", "week4" => "week4", "week5" => "week5", "week6" => "week6", "week7" => "week7", "week8" => "week8",
]
}
}
@@ -165,4 +173,61 @@ class PolymodHandler
var modIds = [for (i in getAllMods()) i.id];
return modIds;
}
+
+ public static function setEnabledMods(newModList:Array):Void
+ {
+ FlxG.save.data.enabledMods = newModList;
+ // Make sure to COMMIT the changes.
+ FlxG.save.flush();
+ }
+
+ /**
+ * Returns the list of enabled mods.
+ * @return Array
+ */
+ public static function getEnabledModIds():Array
+ {
+ if (FlxG.save.data.enabledMods == null)
+ {
+ // NOTE: If the value is null, the enabled mod list is unconfigured.
+ // Currently, we default to disabling newly installed mods.
+ // If we want to auto-enable new mods, but otherwise leave the configured list in place,
+ // we will need some custom logic.
+ FlxG.save.data.enabledMods = [];
+ }
+ return FlxG.save.data.enabledMods;
+ }
+
+ public static function getEnabledMods():Array
+ {
+ var modIds = getEnabledModIds();
+ var modMetadata = getAllMods();
+ var enabledMods = [];
+ for (item in modMetadata)
+ {
+ if (modIds.indexOf(item.id) != -1)
+ {
+ enabledMods.push(item);
+ }
+ }
+ return enabledMods;
+ }
+
+ public static function forceReloadAssets()
+ {
+ // Forcibly clear scripts so that scripts can be edited.
+ ModuleHandler.clearModuleCache();
+ polymod.hscript.PolymodScriptClass.clearScriptClasses();
+
+ // Forcibly reload Polymod so it finds any new files.
+ loadEnabledMods();
+
+ // Reload scripted classes so stages and modules will update.
+ polymod.hscript.PolymodScriptClass.registerAllScriptClasses();
+
+ // Reload the stages in cache.
+ // TODO: Currently this causes lag since you're reading a lot of files, how to fix?
+ StageDataParser.loadStageCache();
+ ModuleHandler.loadModuleCache();
+ }
}
diff --git a/source/funkin/modding/events/ScriptEvent.hx b/source/funkin/modding/events/ScriptEvent.hx
index ce4c08743..ef8fc3a06 100644
--- a/source/funkin/modding/events/ScriptEvent.hx
+++ b/source/funkin/modding/events/ScriptEvent.hx
@@ -1,5 +1,8 @@
package funkin.modding.events;
+import flixel.FlxState;
+import flixel.FlxSubState;
+import funkin.Note.NoteDir;
import funkin.play.Countdown.CountdownStep;
import openfl.events.EventType;
import openfl.events.KeyboardEvent;
@@ -83,6 +86,15 @@ class ScriptEvent
*/
public static inline final NOTE_MISS:ScriptEventType = "NOTE_MISS";
+ /**
+ * Called when a character presses a note when there was none there, causing them to lose health.
+ * Important information such as direction pressed, etc. are all provided.
+ *
+ * This event IS cancelable! Canceling this event prevents the note from being considered missed,
+ * avoiding lost health/score and preventing the miss animation.
+ */
+ public static inline final NOTE_GHOST_MISS:ScriptEventType = "NOTE_GHOST_MISS";
+
/**
* Called when the song starts. This occurs as the countdown ends and the instrumental and vocals begin.
*
@@ -97,13 +109,6 @@ class ScriptEvent
*/
public static inline final SONG_END:ScriptEventType = "SONG_END";
- /**
- * Called when the song is reset. This can happen from the pause menu or the game over screen.
- *
- * This event is not cancelable.
- */
- public static inline final SONG_RESET:ScriptEventType = "SONG_RESET";
-
/**
* Called when the countdown begins. This occurs before the song starts.
*
@@ -130,18 +135,19 @@ class ScriptEvent
public static inline final COUNTDOWN_END:ScriptEventType = "COUNTDOWN_END";
/**
- * Called when the game over screen triggers and the death animation plays.
+ * Called before the game over screen triggers and the death animation plays.
*
* This event is not cancelable.
*/
public static inline final GAME_OVER:ScriptEventType = "GAME_OVER";
/**
- * Called when the player presses a key to restart the game after the death animation.
+ * Called when the player presses a key to restart the game.
+ * This can happen from the pause menu or the game over screen.
*
* This event IS cancelable! Canceling this event will prevent the game from restarting.
*/
- public static inline final GAME_RETRY:ScriptEventType = "GAME_RETRY";
+ public static inline final SONG_RETRY:ScriptEventType = "SONG_RETRY";
/**
* Called when the player pushes down any key on the keyboard.
@@ -166,11 +172,46 @@ class ScriptEvent
public static inline final SONG_LOADED:ScriptEventType = "SONG_LOADED";
/**
- * Called when the game is entering the current FlxState.
+ * Called when the game is about to switch the current FlxState.
*
* This event is not cancelable.
*/
- public static inline final STATE_ENTER:ScriptEventType = "STATE_ENTER";
+ public static inline final STATE_CHANGE_BEGIN:ScriptEventType = "STATE_CHANGE_BEGIN";
+
+ /**
+ * Called when the game has finished switching the current FlxState.
+ *
+ * This event is not cancelable.
+ */
+ public static inline final STATE_CHANGE_END:ScriptEventType = "STATE_CHANGE_END";
+
+ /**
+ * Called when the game is about to open a new FlxSubState.
+ *
+ * This event is not cancelable.
+ */
+ public static inline final SUBSTATE_OPEN_BEGIN:ScriptEventType = "SUBSTATE_OPEN_BEGIN";
+
+ /**
+ * Called when the game has finished opening a new FlxSubState.
+ *
+ * This event is not cancelable.
+ */
+ public static inline final SUBSTATE_OPEN_END:ScriptEventType = "SUBSTATE_OPEN_END";
+
+ /**
+ * Called when the game is about to close the current FlxSubState.
+ *
+ * This event is not cancelable.
+ */
+ public static inline final SUBSTATE_CLOSE_BEGIN:ScriptEventType = "SUBSTATE_CLOSE_BEGIN";
+
+ /**
+ * Called when the game has finished closing the current FlxSubState.
+ *
+ * This event is not cancelable.
+ */
+ public static inline final SUBSTATE_CLOSE_END:ScriptEventType = "SUBSTATE_CLOSE_END";
/**
* Called when the game is exiting the current FlxState.
@@ -179,7 +220,7 @@ class ScriptEvent
*/
/**
* If true, the behavior associated with this event can be prevented.
- * For example, cancelling COUNTDOWN_BEGIN should prevent the countdown from starting,
+ * For example, cancelling COUNTDOWN_START should prevent the countdown from starting,
* until another script restarts it, or cancelling NOTE_HIT should cause the note to be missed.
*/
public var cancelable(default, null):Bool;
@@ -209,7 +250,7 @@ class ScriptEvent
/**
* Call this function on a cancelable event to cancel the associated behavior.
- * For example, cancelling COUNTDOWN_BEGIN will prevent the countdown from starting.
+ * For example, cancelling COUNTDOWN_START will prevent the countdown from starting.
*/
public function cancelEvent():Void
{
@@ -265,6 +306,59 @@ class NoteScriptEvent extends ScriptEvent
}
}
+/**
+ * An event that is fired when you press a key with no note present.
+ */
+class GhostMissNoteScriptEvent extends ScriptEvent
+{
+ /**
+ * The direction that was mistakenly pressed.
+ */
+ public var dir(default, null):NoteDir;
+
+ /**
+ * Whether there was a note within judgement range when this ghost note was pressed.
+ */
+ public var hasPossibleNotes(default, null):Bool;
+
+ /**
+ * How much health should be lost when this ghost note is pressed.
+ * Remember that max health is 2.00.
+ */
+ public var healthChange(default, default):Float;
+
+ /**
+ * How much score should be lost when this ghost note is pressed.
+ */
+ public var scoreChange(default, default):Int;
+
+ /**
+ * Whether to play the record scratch sound.
+ */
+ public var playSound(default, default):Bool;
+
+ /**
+ * Whether to play the miss animation on the player.
+ */
+ public var playAnim(default, default):Bool;
+
+ public function new(dir:NoteDir, hasPossibleNotes:Bool, healthChange:Float, scoreChange:Int):Void
+ {
+ super(ScriptEvent.NOTE_GHOST_MISS, true);
+ this.dir = dir;
+ this.hasPossibleNotes = hasPossibleNotes;
+ this.healthChange = healthChange;
+ this.scoreChange = scoreChange;
+ this.playSound = true;
+ this.playAnim = true;
+ }
+
+ public override function toString():String
+ {
+ return 'GhostMissNoteScriptEvent(dir=' + dir + ', hasPossibleNotes=' + hasPossibleNotes + ')';
+ }
+}
+
/**
* An event that is fired during the update loop.
*/
@@ -306,7 +400,7 @@ class SongTimeScriptEvent extends ScriptEvent
public function new(type:ScriptEventType, beat:Int, step:Int):Void
{
- super(type, false);
+ super(type, true);
this.beat = beat;
this.step = step;
}
@@ -403,13 +497,58 @@ class SongLoadScriptEvent extends ScriptEvent
*/
class StateChangeScriptEvent extends ScriptEvent
{
- public function new(type:ScriptEventType):Void
+ /**
+ * The state the game is moving into.
+ */
+ public var targetState(default, null):FlxState;
+
+ public function new(type:ScriptEventType, targetState:FlxState, cancelable:Bool = false):Void
{
- super(type, false);
+ super(type, cancelable);
+ this.targetState = targetState;
}
public override function toString():String
{
- return 'StateChangeScriptEvent(type=' + type + ')';
+ return 'StateChangeScriptEvent(type=' + type + ', targetState=' + targetState + ')';
+ }
+}
+
+/**
+ * An event that is fired when moving out of or into an FlxSubState.
+ */
+class SubStateScriptEvent extends ScriptEvent
+{
+ /**
+ * The state the game is moving into.
+ */
+ public var targetState(default, null):FlxSubState;
+
+ public function new(type:ScriptEventType, targetState:FlxSubState, cancelable:Bool = false):Void
+ {
+ super(type, cancelable);
+ this.targetState = targetState;
+ }
+
+ public override function toString():String
+ {
+ return 'SubStateScriptEvent(type=' + type + ', targetState=' + targetState + ')';
+ }
+}
+
+/**
+ * An event which is called when the player attempts to pause the game.
+ */
+class PauseScriptEvent extends ScriptEvent
+{
+ /**
+ * Whether to use the Gitaroo Man pause.
+ */
+ public var gitaroo(default, default):Bool;
+
+ public function new(gitaroo:Bool):Void
+ {
+ super(ScriptEvent.PAUSE, true);
+ this.gitaroo = gitaroo;
}
}
diff --git a/source/funkin/modding/events/ScriptEventDispatcher.hx b/source/funkin/modding/events/ScriptEventDispatcher.hx
index ecb97a846..474d1a18c 100644
--- a/source/funkin/modding/events/ScriptEventDispatcher.hx
+++ b/source/funkin/modding/events/ScriptEventDispatcher.hx
@@ -1,7 +1,7 @@
package funkin.modding.events;
-import funkin.modding.IScriptedClass;
import funkin.modding.IScriptedClass.IPlayStateScriptedClass;
+import funkin.modding.IScriptedClass;
/**
* Utility functions to assist with handling scripted classes.
@@ -35,18 +35,6 @@ class ScriptEventDispatcher
return;
}
- if (Std.isOfType(target, IStateChangingScriptedClass))
- {
- var t = cast(target, IStateChangingScriptedClass);
- var t = cast(target, IPlayStateScriptedClass);
- switch (event.type)
- {
- case ScriptEvent.NOTE_HIT:
- t.onNoteHit(cast event);
- return;
- }
- }
-
if (Std.isOfType(target, IPlayStateScriptedClass))
{
var t = cast(target, IPlayStateScriptedClass);
@@ -58,6 +46,9 @@ class ScriptEventDispatcher
case ScriptEvent.NOTE_MISS:
t.onNoteMiss(cast event);
return;
+ case ScriptEvent.NOTE_GHOST_MISS:
+ t.onNoteGhostMiss(cast event);
+ return;
case ScriptEvent.SONG_BEAT_HIT:
t.onBeatHit(cast event);
return;
@@ -70,11 +61,14 @@ class ScriptEventDispatcher
case ScriptEvent.SONG_END:
t.onSongEnd(event);
return;
- case ScriptEvent.SONG_RESET:
- t.onSongReset(event);
+ case ScriptEvent.SONG_RETRY:
+ t.onSongRetry(event);
+ return;
+ case ScriptEvent.GAME_OVER:
+ t.onGameOver(event);
return;
case ScriptEvent.PAUSE:
- t.onPause(event);
+ t.onPause(cast event);
return;
case ScriptEvent.RESUME:
t.onResume(event);
@@ -94,7 +88,38 @@ class ScriptEventDispatcher
}
}
- throw "No helper for event type: " + event.type;
+ if (Std.isOfType(target, IStateChangingScriptedClass))
+ {
+ var t = cast(target, IStateChangingScriptedClass);
+ switch (event.type)
+ {
+ case ScriptEvent.STATE_CHANGE_BEGIN:
+ t.onStateChangeBegin(cast event);
+ return;
+ case ScriptEvent.STATE_CHANGE_END:
+ t.onStateChangeEnd(cast event);
+ return;
+ case ScriptEvent.SUBSTATE_OPEN_BEGIN:
+ t.onSubstateOpenBegin(cast event);
+ return;
+ case ScriptEvent.SUBSTATE_OPEN_END:
+ t.onSubstateOpenEnd(cast event);
+ return;
+ case ScriptEvent.SUBSTATE_CLOSE_BEGIN:
+ t.onSubstateCloseBegin(cast event);
+ return;
+ case ScriptEvent.SUBSTATE_CLOSE_END:
+ t.onSubstateCloseEnd(cast event);
+ return;
+ }
+ }
+ else
+ {
+ // Prevent "NO HELPER error."
+ return;
+ }
+
+ throw "No function called for event type: " + event.type;
}
public static function callEventOnAllTargets(targets:Iterator, event:ScriptEvent):Void
diff --git a/source/funkin/modding/module/Module.hx b/source/funkin/modding/module/Module.hx
index 3f499c5e1..d337ad719 100644
--- a/source/funkin/modding/module/Module.hx
+++ b/source/funkin/modding/module/Module.hx
@@ -1,13 +1,8 @@
package funkin.modding.module;
-import funkin.modding.events.ScriptEvent;
-import funkin.modding.events.ScriptEvent.UpdateScriptEvent;
-import funkin.modding.events.ScriptEvent.KeyboardInputScriptEvent;
-import funkin.modding.events.ScriptEvent.NoteScriptEvent;
-import funkin.modding.events.ScriptEvent.SongTimeScriptEvent;
-import funkin.modding.events.ScriptEvent.CountdownScriptEvent;
import funkin.modding.IScriptedClass.IPlayStateScriptedClass;
import funkin.modding.IScriptedClass.IStateChangingScriptedClass;
+import funkin.modding.events.ScriptEvent;
/**
* A module is a scripted class which receives all events without requiring a specific context.
@@ -18,7 +13,7 @@ class Module implements IPlayStateScriptedClass implements IStateChangingScripte
/**
* Whether the module is currently active.
*/
- public var active(default, set):Bool = false;
+ public var active(default, set):Bool = true;
function set_active(value:Bool):Bool
{
@@ -48,14 +43,11 @@ class Module implements IPlayStateScriptedClass implements IStateChangingScripte
* Called when the module is initialized.
* It may not be safe to reference other modules here since they may not be loaded yet.
*
- * @param startActive Whether to start with the module active.
- * If false, the module will be inactive and must be enabled by another script,
- * such as a stage or another module.
+ * NOTE: To make the module start inactive, call `this.active = false` in the constructor.
*/
- public function new(moduleId:String, active:Bool = true, priority:Int = 1000):Void
+ public function new(moduleId:String, priority:Int = 1000):Void
{
this.moduleId = moduleId;
- this.active = active;
this.priority = priority;
}
@@ -82,7 +74,7 @@ class Module implements IPlayStateScriptedClass implements IStateChangingScripte
public function onUpdate(event:UpdateScriptEvent) {}
- public function onPause(event:ScriptEvent) {}
+ public function onPause(event:PauseScriptEvent) {}
public function onResume(event:ScriptEvent) {}
@@ -90,16 +82,14 @@ class Module implements IPlayStateScriptedClass implements IStateChangingScripte
public function onSongEnd(event:ScriptEvent) {}
- public function onSongReset(event:ScriptEvent) {}
-
public function onGameOver(event:ScriptEvent) {}
- public function onGameRetry(event:ScriptEvent) {}
-
public function onNoteHit(event:NoteScriptEvent) {}
public function onNoteMiss(event:NoteScriptEvent) {}
+ public function onNoteGhostMiss(event:GhostMissNoteScriptEvent) {}
+
public function onStepHit(event:SongTimeScriptEvent) {}
public function onBeatHit(event:SongTimeScriptEvent) {}
@@ -110,9 +100,19 @@ class Module implements IPlayStateScriptedClass implements IStateChangingScripte
public function onCountdownEnd(event:CountdownScriptEvent) {}
- public function onSongLoaded(eent:SongLoadScriptEvent) {}
+ public function onSongLoaded(event:SongLoadScriptEvent) {}
public function onStateChangeBegin(event:StateChangeScriptEvent) {}
public function onStateChangeEnd(event:StateChangeScriptEvent) {}
+
+ public function onSubstateOpenBegin(event:SubStateScriptEvent) {}
+
+ public function onSubstateOpenEnd(event:SubStateScriptEvent) {}
+
+ public function onSubstateCloseBegin(event:SubStateScriptEvent) {}
+
+ public function onSubstateCloseEnd(event:SubStateScriptEvent) {}
+
+ public function onSongRetry(event:ScriptEvent) {}
}
diff --git a/source/funkin/play/AnimationData.hx b/source/funkin/play/AnimationData.hx
new file mode 100644
index 000000000..597495345
--- /dev/null
+++ b/source/funkin/play/AnimationData.hx
@@ -0,0 +1,63 @@
+package funkin.play;
+
+typedef AnimationData =
+{
+ /**
+ * The name for the animation.
+ * This should match the animation name queried by the game;
+ * for example, characters need animations with names `idle`, `singDOWN`, `singUPmiss`, etc.
+ */
+ var name:String;
+
+ /**
+ * The prefix for the frames of the animation as defined by the XML file.
+ * This will may or may not differ from the `name` of the animation,
+ * depending on how your animator organized their FLA or whatever.
+ */
+ var prefix:String;
+
+ /**
+ * Optionally specify an asset path to use for this specific animation.
+ * ONLY for use by MultiSparrow characters.
+ * @default The assetPath of the parent sprite
+ */
+ var assetPath:Null;
+
+ /**
+ * Offset the character's position by this amount when playing this animation.
+ * @default [0, 0]
+ */
+ var offsets:Null>;
+
+ /**
+ * Whether the animation should loop when it finishes.
+ * @default false
+ */
+ var looped:Null;
+
+ /**
+ * Whether the animation's sprites should be flipped horizontally.
+ * @default false
+ */
+ var flipX:Null;
+
+ /**
+ * Whether the animation's sprites should be flipped vertically.
+ * @default false
+ */
+ var flipY:Null;
+
+ /**
+ * The frame rate of the animation.
+ * @default 24
+ */
+ var frameRate:Null;
+
+ /**
+ * If you want this animation to use only certain frames of an animation with a given prefix,
+ * select them here.
+ * @example [0, 1, 2, 3] (use only the first four frames)
+ * @default [] (all frames)
+ */
+ var frameIndices:Null>;
+}
diff --git a/source/funkin/play/PicoFight.hx b/source/funkin/play/PicoFight.hx
index 1ef8a1fa2..f37bcb08f 100644
--- a/source/funkin/play/PicoFight.hx
+++ b/source/funkin/play/PicoFight.hx
@@ -1,14 +1,13 @@
package funkin.play;
-import funkin.Note.NoteData;
-import funkin.audiovis.PolygonSpectogram;
import flixel.FlxObject;
import flixel.FlxSprite;
import flixel.addons.effects.FlxTrail;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.math.FlxMath;
import flixel.util.FlxColor;
-import flixel.util.FlxTimer;
+import funkin.Note.NoteData;
+import funkin.audiovis.PolygonSpectogram;
class PicoFight extends MusicBeatState
{
@@ -37,7 +36,7 @@ class PicoFight extends MusicBeatState
FlxG.sound.playMusic(Paths.inst("blazin"));
SongLoad.loadFromJson('blazin', "blazin");
- Conductor.changeBPM(SongLoad.songData.bpm);
+ Conductor.bpm = SongLoad.songData.bpm;
for (dumbassSection in SongLoad.songData.noteMap['hard'])
{
@@ -184,13 +183,15 @@ class PicoFight extends MusicBeatState
super.update(elapsed);
}
- override function stepHit()
+ override function stepHit():Bool
{
- super.stepHit();
+ return super.stepHit();
}
- override function beatHit()
+ override function beatHit():Bool
{
+ if (!super.beatHit())
+ return false;
funnyWave.thickness = 10;
funnyWave.waveAmplitude = 300;
funnyWave.realtimeVisLenght = 0.1;
@@ -198,7 +199,6 @@ class PicoFight extends MusicBeatState
picoHealth += 1;
makeNotes();
- // trace(picoHealth);
- super.beatHit();
+ return true;
}
}
diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index 61ede4246..4cb5f1397 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -1,13 +1,12 @@
package funkin.play;
-import funkin.play.Strumline.StrumlineArrow;
-import flixel.addons.effects.FlxTrail;
-import flixel.addons.transition.FlxTransitionableState;
import flixel.FlxCamera;
import flixel.FlxObject;
import flixel.FlxSprite;
import flixel.FlxState;
import flixel.FlxSubState;
+import flixel.addons.effects.FlxTrail;
+import flixel.addons.transition.FlxTransitionableState;
import flixel.group.FlxGroup;
import flixel.math.FlxMath;
import flixel.math.FlxPoint;
@@ -19,19 +18,17 @@ import flixel.ui.FlxBar;
import flixel.util.FlxColor;
import flixel.util.FlxSort;
import flixel.util.FlxTimer;
-import funkin.charting.ChartingState;
-import funkin.modding.events.ScriptEvent;
-import funkin.modding.events.ScriptEvent.SongTimeScriptEvent;
-import funkin.modding.events.ScriptEvent.UpdateScriptEvent;
-import funkin.modding.events.ScriptEventDispatcher;
-import funkin.modding.IHook;
-import funkin.modding.module.ModuleHandler;
import funkin.Note;
-import funkin.play.stage.Stage;
-import funkin.play.stage.StageData;
-import funkin.play.Strumline.StrumlineStyle;
import funkin.Section.SwagSection;
import funkin.SongLoad.SwagSong;
+import funkin.charting.ChartingState;
+import funkin.modding.IHook;
+import funkin.modding.events.ScriptEvent;
+import funkin.modding.events.ScriptEventDispatcher;
+import funkin.play.Strumline.StrumlineArrow;
+import funkin.play.Strumline.StrumlineStyle;
+import funkin.play.stage.Stage;
+import funkin.play.stage.StageData;
import funkin.ui.PopUpStuff;
import funkin.ui.PreferencesMenu;
import funkin.util.Constants;
@@ -287,7 +284,7 @@ class PlayState extends MusicBeatState implements IHook
currentSong = SongLoad.loadFromJson('tutorial');
Conductor.mapBPMChanges(currentSong);
- Conductor.changeBPM(currentSong.bpm);
+ Conductor.bpm = currentSong.bpm;
switch (currentSong.song.toLowerCase())
{
@@ -592,7 +589,7 @@ class PlayState extends MusicBeatState implements IHook
*
* Call this by pressing F5 on a debug build.
*/
- function debug_refreshStages()
+ override function debug_refreshModules()
{
// Remove the current stage. If the stage gets deleted while it's still in use,
// it'll probably crash the game or something.
@@ -604,18 +601,7 @@ class PlayState extends MusicBeatState implements IHook
currentStage = null;
}
- ModuleHandler.clearModuleCache();
-
- // Forcibly reload scripts so that scripted stages can be edited.
- polymod.hscript.PolymodScriptClass.clearScriptClasses();
- polymod.hscript.PolymodScriptClass.registerAllScriptClasses();
-
- // Reload the stages in cache. This might cause a lag spike but who cares this is a debug utility.
- StageDataParser.loadStageCache();
- ModuleHandler.loadModuleCache();
-
- // Reload the level. This should use new data from the assets folder.
- LoadingState.loadAndSwitchState(new PlayState());
+ super.debug_refreshModules();
}
/**
@@ -783,7 +769,7 @@ class PlayState extends MusicBeatState implements IHook
{
// FlxG.log.add(ChartParser.parse());
- Conductor.changeBPM(currentSong.bpm);
+ Conductor.bpm = currentSong.bpm;
currentSong.song = currentSong.song;
@@ -849,7 +835,9 @@ class PlayState extends MusicBeatState implements IHook
oldNote = null;
var swagNote:Note = new Note(daStrumTime, daNoteData, oldNote);
- swagNote.data = songNotes;
+ // swagNote.data = songNotes;
+ swagNote.data.sustainLength = songNotes.sustainLength;
+ swagNote.data.altNote = songNotes.altNote;
swagNote.scrollFactor.set(0, 0);
var susLength:Float = swagNote.data.sustainLength;
@@ -938,6 +926,8 @@ class PlayState extends MusicBeatState implements IHook
if (needsReset)
{
+ dispatchEvent(new ScriptEvent(ScriptEvent.SONG_RETRY));
+
resetCamera();
persistentUpdate = true;
@@ -948,11 +938,10 @@ class PlayState extends MusicBeatState implements IHook
FlxG.sound.music.pause();
vocals.pause();
- var event:ScriptEvent = new ScriptEvent(ScriptEvent.SONG_RESET, false);
-
FlxG.sound.music.time = 0;
regenNoteData(); // loads the note data from start
health = 1;
+ songScore = 0;
Countdown.performCountdown(currentStageId.startsWith('school'));
needsReset = false;
@@ -1004,28 +993,35 @@ class PlayState extends MusicBeatState implements IHook
if ((controls.PAUSE || androidPause) && isInCountdown && mayPauseGame)
{
- persistentUpdate = false;
- persistentDraw = true;
+ var event = new PauseScriptEvent(FlxG.random.bool(1 / 1000));
- // There is a 1/1000 change to use a special pause menu.
- // This prevents the player from resuming, but that's the point.
- // It's a reference to Gitaroo Man, which doesn't let you pause the game.
- if (FlxG.random.bool(1 / 1000))
- {
- FlxG.switchState(new GitarooPause());
- }
- else
- {
- var boyfriendPos = currentStage.getBoyfriend().getScreenPosition();
- var pauseSubState = new PauseSubState(boyfriendPos.x, boyfriendPos.y);
- openSubState(pauseSubState);
- pauseSubState.camera = camHUD;
- boyfriendPos.put();
- }
+ dispatchEvent(event);
- #if discord_rpc
- DiscordClient.changePresence(detailsPausedText, currentSong.song + " (" + storyDifficultyText + ")", iconRPC);
- #end
+ if (!event.eventCanceled)
+ {
+ persistentUpdate = false;
+ persistentDraw = true;
+
+ // There is a 1/1000 change to use a special pause menu.
+ // This prevents the player from resuming, but that's the point.
+ // It's a reference to Gitaroo Man, which doesn't let you pause the game.
+ if (event.gitaroo)
+ {
+ FlxG.switchState(new GitarooPause());
+ }
+ else
+ {
+ var boyfriendPos = currentStage.getBoyfriend().getScreenPosition();
+ var pauseSubState = new PauseSubState(boyfriendPos.x, boyfriendPos.y);
+ openSubState(pauseSubState);
+ pauseSubState.camera = camHUD;
+ boyfriendPos.put();
+ }
+
+ #if discord_rpc
+ DiscordClient.changePresence(detailsPausedText, currentSong.song + " (" + storyDifficultyText + ")", iconRPC);
+ #end
+ }
}
if (FlxG.keys.justPressed.SEVEN)
@@ -1040,9 +1036,6 @@ class PlayState extends MusicBeatState implements IHook
if (FlxG.keys.justPressed.EIGHT)
FlxG.switchState(new funkin.ui.animDebugShit.DebugBoundingState());
- if (FlxG.keys.justPressed.F5)
- debug_refreshStages();
-
if (FlxG.keys.justPressed.NINE)
iconP1.swapOldIcon();
@@ -1656,7 +1649,7 @@ class PlayState extends MusicBeatState implements IHook
}
}
- if (PlayState.instance.currentStage == null)
+ if (PlayState.instance == null || PlayState.instance.currentStage == null)
return;
if (PlayState.instance.currentStage.getBoyfriend().holdTimer > Conductor.stepCrochet * 4 * 0.001 && !holdArray.contains(true))
{
@@ -1724,9 +1717,12 @@ class PlayState extends MusicBeatState implements IHook
}
}
- override function stepHit()
+ override function stepHit():Bool
{
- super.stepHit();
+ // super.stepHit() returns false if a module cancelled the event.
+ if (!super.stepHit())
+ return false;
+
if (Math.abs(FlxG.sound.music.time - (Conductor.songPosition - Conductor.offset)) > 20
|| (currentSong.needsVoices && Math.abs(vocals.time - (Conductor.songPosition - Conductor.offset)) > 20))
{
@@ -1734,11 +1730,15 @@ class PlayState extends MusicBeatState implements IHook
}
dispatchEvent(new SongTimeScriptEvent(ScriptEvent.SONG_STEP_HIT, curBeat, curStep));
+
+ return true;
}
- override function beatHit()
+ override function beatHit():Bool
{
- super.beatHit();
+ // super.beatHit() returns false if a module cancelled the event.
+ if (!super.beatHit())
+ return false;
if (generatedMusic)
{
@@ -1749,7 +1749,7 @@ class PlayState extends MusicBeatState implements IHook
{
if (SongLoad.getSong()[Math.floor(curStep / 16)].changeBPM)
{
- Conductor.changeBPM(SongLoad.getSong()[Math.floor(curStep / 16)].bpm);
+ Conductor.bpm = SongLoad.getSong()[Math.floor(curStep / 16)].bpm;
FlxG.log.add('CHANGED BPM!');
}
}
@@ -1780,8 +1780,7 @@ class PlayState extends MusicBeatState implements IHook
// Make the characters dance on the beat
danceOnBeat();
- // Call any relevant event handlers.
- dispatchEvent(new SongTimeScriptEvent(ScriptEvent.SONG_BEAT_HIT, curBeat, curStep));
+ return true;
}
/**
@@ -1897,13 +1896,6 @@ class PlayState extends MusicBeatState implements IHook
Countdown.pauseCountdown();
}
- var event:ScriptEvent = new ScriptEvent(ScriptEvent.PAUSE, true);
-
- dispatchEvent(event);
-
- if (event.eventCanceled)
- return;
-
super.openSubState(subState);
}
@@ -1915,6 +1907,13 @@ class PlayState extends MusicBeatState implements IHook
{
if (isGamePaused)
{
+ var event:ScriptEvent = new ScriptEvent(ScriptEvent.RESUME, true);
+
+ dispatchEvent(event);
+
+ if (event.eventCanceled)
+ return;
+
if (FlxG.sound.music != null && !startingSong)
resyncVocals();
@@ -1930,13 +1929,6 @@ class PlayState extends MusicBeatState implements IHook
#end
}
- var event:ScriptEvent = new ScriptEvent(ScriptEvent.RESUME, true);
-
- dispatchEvent(event);
-
- if (event.eventCanceled)
- return;
-
super.closeSubState();
}
@@ -1957,12 +1949,16 @@ class PlayState extends MusicBeatState implements IHook
override function dispatchEvent(event:ScriptEvent):Void
{
+ // ORDER: Module, Stage, Character, Song, Note
+ // Modules should get the first chance to cancel the event.
+
+ // super.dispatchEvent(event) dispatches event to module scripts.
+ super.dispatchEvent(event);
+
+ // Dispatch event to stage script.
ScriptEventDispatcher.callEvent(currentStage, event);
// TODO: Dispatch event to song script
- // TODO: Dispatch events to character scripts
-
- super.dispatchEvent(event);
}
/**
@@ -2014,16 +2010,6 @@ class PlayState extends MusicBeatState implements IHook
instance = null;
}
- /**
- * Refreshes the state, by redoing the render order of all elements.
- * It does this based on the `zIndex` of each element.
- */
- public function refresh()
- {
- sort(SortUtil.byZIndex, FlxSort.ASCENDING);
- trace('Stage sorted by z-index');
- }
-
/**
* This function is called whenever Flixel switches switching to a new FlxState.
*/
diff --git a/source/funkin/play/stage/Bopper.hx b/source/funkin/play/stage/Bopper.hx
index ca1d26ce0..1c8fdce5d 100644
--- a/source/funkin/play/stage/Bopper.hx
+++ b/source/funkin/play/stage/Bopper.hx
@@ -1,12 +1,8 @@
package funkin.play.stage;
-import funkin.modding.events.ScriptEvent;
-import funkin.modding.events.ScriptEvent.UpdateScriptEvent;
-import funkin.modding.events.ScriptEvent.NoteScriptEvent;
-import funkin.modding.events.ScriptEvent.SongTimeScriptEvent;
-import funkin.modding.events.ScriptEvent.CountdownScriptEvent;
-import funkin.modding.IScriptedClass.IPlayStateScriptedClass;
import flixel.FlxSprite;
+import funkin.modding.IScriptedClass.IPlayStateScriptedClass;
+import funkin.modding.events.ScriptEvent;
/**
* A Bopper is a stage prop which plays a dance animation.
@@ -16,6 +12,7 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
{
/**
* The bopper plays the dance animation once every `danceEvery` beats.
+ * Set to 0 to disable idle animation.
*/
public var danceEvery:Int = 1;
@@ -29,16 +26,14 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
public var shouldAlternate:Null = null;
/**
- * Set this value to define an additional horizontal offset to this sprite's position.
+ * Offset the character's sprite by this much when playing each animation.
*/
- public var xOffset:Float = 0;
-
- override function set_x(value:Float):Float
- {
- this.x = value + this.xOffset;
- return value;
- }
+ public var animationOffsets:Map> = new Map>();
+ /**
+ * Add a suffix to the `idle` animation (or `danceLeft` and `danceRight` animations)
+ * that this bopper will play.
+ */
public var idleSuffix(default, set):String = "";
function set_idleSuffix(value:String):String
@@ -49,14 +44,26 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
}
/**
- * Set this value to define an additional vertical offset to this sprite's position.
+ * The offset of the character relative to the position specified by the stage.
*/
- public var yOffset:Float = 0;
+ public var globalOffsets(default, null):Array = [0, 0];
- override function set_y(value:Float):Float
+ private var animOffsets(default, set):Array = [0, 0];
+
+ function set_animOffsets(value:Array)
{
- this.y = value + this.yOffset;
- return value;
+ if (animOffsets == null)
+ animOffsets = [0, 0];
+ if (animOffsets == value)
+ return value;
+
+ var xDiff = animOffsets[0] - value[0];
+ var yDiff = animOffsets[1] - value[1];
+
+ this.x += xDiff;
+ this.y += yDiff;
+
+ return animOffsets = value;
}
/**
@@ -73,7 +80,7 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
function update_shouldAlternate():Void
{
- if (this.animation.getByName('danceLeft') != null)
+ if (hasAnimation('danceLeft'))
{
this.shouldAlternate = true;
}
@@ -84,16 +91,16 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
*/
public function onBeatHit(event:SongTimeScriptEvent):Void
{
- if (event.beat % danceEvery == 0)
+ if (danceEvery > 0 && event.beat % danceEvery == 0)
{
- dance();
+ dance(true);
}
}
/**
* Called every `danceEvery` beats of the song.
*/
- function dance():Void
+ public function dance(force:Bool = false):Void
{
if (this.animation == null)
{
@@ -109,20 +116,113 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
{
if (hasDanced)
{
- this.animation.play('danceRight$idleSuffix');
+ playAnimation('danceRight$idleSuffix', true);
}
else
{
- this.animation.play('danceLeft$idleSuffix');
+ playAnimation('danceLeft$idleSuffix', true);
}
hasDanced = !hasDanced;
}
else
{
- this.animation.play('idle$idleSuffix');
+ playAnimation('idle$idleSuffix', true);
}
}
+ public function hasAnimation(id:String):Bool
+ {
+ if (this.animation == null)
+ return false;
+
+ return this.animation.getByName(id) != null;
+ }
+
+ /**
+ * Ensure that a given animation exists before playing it.
+ * Will gracefully check for name, then name with stripped suffixes, then 'idle', then fail to play.
+ * @param name
+ */
+ function correctAnimationName(name:String)
+ {
+ // If the animation exists, we're good.
+ if (hasAnimation(name))
+ return name;
+
+ trace('[BOPPER] Animation "$name" does not exist!');
+
+ // Attempt to strip a `-alt` suffix, if it exists.
+ if (name.lastIndexOf('-') != -1)
+ {
+ var correctName = name.substring(0, name.lastIndexOf('-'));
+ trace('[BOPPER] Attempting to fallback to "$correctName"');
+ return correctAnimationName(correctName);
+ }
+ else
+ {
+ if (name != 'idle')
+ {
+ trace('[BOPPER] Attempting to fallback to "idle"');
+ return correctAnimationName('idle');
+ }
+ else
+ {
+ trace('[BOPPER] Failing animation playback.');
+ return null;
+ }
+ }
+ }
+
+ /**
+ * @param name The name of the animation to play.
+ * @param restart Whether to restart the animation if it is already playing.
+ */
+ public function playAnimation(name:String, restart:Bool = false):Void
+ {
+ var correctName = correctAnimationName(name);
+ if (correctName == null)
+ return;
+
+ this.animation.play(correctName, restart, false, 0);
+
+ applyAnimationOffsets(correctName);
+ }
+
+ function applyAnimationOffsets(name:String)
+ {
+ var offsets = animationOffsets.get(name);
+ if (offsets != null)
+ {
+ this.animOffsets = [offsets[0] + globalOffsets[0], offsets[1] + globalOffsets[1]];
+ }
+ else
+ {
+ this.animOffsets = globalOffsets;
+ }
+ }
+
+ public function isAnimationFinished():Bool
+ {
+ return this.animation.finished;
+ }
+
+ public function setAnimationOffsets(name:String, xOffset:Float, yOffset:Float):Void
+ {
+ animationOffsets.set(name, [xOffset, yOffset]);
+ }
+
+ /**
+ * Returns the name of the animation that is currently playing.
+ * If no animation is playing (usually this means the character is BROKEN!),
+ * returns an empty string to prevent NPEs.
+ */
+ public function getCurrentAnimation():String
+ {
+ if (this.animation == null || this.animation.curAnim == null)
+ return "";
+ return this.animation.curAnim.name;
+ }
+
public function onScriptEvent(event:ScriptEvent) {}
public function onCreate(event:ScriptEvent) {}
@@ -131,7 +231,7 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
public function onUpdate(event:UpdateScriptEvent) {}
- public function onPause(event:ScriptEvent) {}
+ public function onPause(event:PauseScriptEvent) {}
public function onResume(event:ScriptEvent) {}
@@ -139,16 +239,14 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
public function onSongEnd(event:ScriptEvent) {}
- public function onSongReset(event:ScriptEvent) {}
-
public function onGameOver(event:ScriptEvent) {}
- public function onGameRetry(event:ScriptEvent) {}
-
public function onNoteHit(event:NoteScriptEvent) {}
public function onNoteMiss(event:NoteScriptEvent) {}
+ public function onNoteGhostMiss(event:GhostMissNoteScriptEvent) {}
+
public function onStepHit(event:SongTimeScriptEvent) {}
public function onCountdownStart(event:CountdownScriptEvent) {}
@@ -158,4 +256,6 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
public function onCountdownEnd(event:CountdownScriptEvent) {}
public function onSongLoaded(eent:SongLoadScriptEvent) {}
+
+ public function onSongRetry(event:ScriptEvent) {}
}
diff --git a/source/funkin/play/stage/Stage.hx b/source/funkin/play/stage/Stage.hx
index a17f340d4..8d77e374a 100644
--- a/source/funkin/play/stage/Stage.hx
+++ b/source/funkin/play/stage/Stage.hx
@@ -1,18 +1,16 @@
package funkin.play.stage;
-import funkin.modding.events.ScriptEventDispatcher;
-import funkin.modding.events.ScriptEvent;
-import funkin.modding.events.ScriptEvent.CountdownScriptEvent;
-import funkin.modding.events.ScriptEvent.KeyboardInputScriptEvent;
-import funkin.modding.IScriptedClass;
import flixel.FlxSprite;
import flixel.group.FlxSpriteGroup;
-import flixel.math.FlxPoint;
import flixel.util.FlxSort;
import funkin.modding.IHook;
+import funkin.modding.IScriptedClass;
+import funkin.modding.events.ScriptEvent;
+import funkin.modding.events.ScriptEventDispatcher;
import funkin.play.character.Character.CharacterType;
import funkin.play.stage.StageData.StageDataParser;
import funkin.util.SortUtil;
+import funkin.util.assets.FlxAnimationUtil;
/**
* A Stage is a group of objects rendered in the PlayState.
@@ -143,19 +141,19 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte
for (propAnim in dataProp.animations)
{
propSprite.animation.add(propAnim.name, propAnim.frameIndices);
+
+ if (Std.isOfType(propSprite, Bopper))
+ {
+ cast(propSprite, Bopper).setAnimationOffsets(propAnim.name, propAnim.offsets[0], propAnim.offsets[1]);
+ }
}
default: // "sparrow"
- for (propAnim in dataProp.animations)
+ FlxAnimationUtil.addAtlasAnimations(propSprite, dataProp.animations);
+ if (Std.isOfType(propSprite, Bopper))
{
- if (propAnim.frameIndices.length == 0)
+ for (propAnim in dataProp.animations)
{
- propSprite.animation.addByPrefix(propAnim.name, propAnim.prefix, propAnim.frameRate, propAnim.loop, propAnim.flipX,
- propAnim.flipY);
- }
- else
- {
- propSprite.animation.addByIndices(propAnim.name, propAnim.prefix, propAnim.frameIndices, "", propAnim.frameRate, propAnim.loop,
- propAnim.flipX, propAnim.flipY);
+ cast(propSprite, Bopper).setAnimationOffsets(propAnim.name, propAnim.offsets[0], propAnim.offsets[1]);
}
}
}
@@ -377,7 +375,7 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte
public function onScriptEvent(event:ScriptEvent) {}
- public function onPause(event:ScriptEvent) {}
+ public function onPause(event:PauseScriptEvent) {}
public function onResume(event:ScriptEvent) {}
@@ -385,29 +383,23 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte
public function onSongEnd(event:ScriptEvent) {}
- /**
- * Resets the stage and its props.
- */
- public function onSongReset(event:ScriptEvent) {}
-
public function onGameOver(event:ScriptEvent) {}
- public function onGameRetry(event:ScriptEvent) {}
-
public function onCountdownStart(event:CountdownScriptEvent) {}
public function onCountdownStep(event:CountdownScriptEvent) {}
public function onCountdownEnd(event:CountdownScriptEvent) {}
- /**
- * A function that should get called every frame.
- */
public function onUpdate(event:UpdateScriptEvent) {}
public function onNoteHit(event:NoteScriptEvent) {}
public function onNoteMiss(event:NoteScriptEvent) {}
+ public function onNoteGhostMiss(event:GhostMissNoteScriptEvent) {}
+
public function onSongLoaded(eent:SongLoadScriptEvent) {}
+
+ public function onSongRetry(event:ScriptEvent) {}
}
diff --git a/source/funkin/play/stage/StageData.hx b/source/funkin/play/stage/StageData.hx
index 2e9058a87..dc5538bac 100644
--- a/source/funkin/play/stage/StageData.hx
+++ b/source/funkin/play/stage/StageData.hx
@@ -1,9 +1,10 @@
package funkin.play.stage;
-import openfl.Assets;
+import flixel.util.typeLimit.OneOfTwo;
+import funkin.util.VersionUtil;
import funkin.util.assets.DataAssets;
import haxe.Json;
-import flixel.util.typeLimit.OneOfTwo;
+import openfl.Assets;
using StringTools;
@@ -17,7 +18,12 @@ class StageDataParser
* Handle breaking changes by incrementing this value
* and adding migration to the `migrateStageData()` function.
*/
- public static final STAGE_DATA_VERSION:String = "1.0";
+ public static final STAGE_DATA_VERSION:String = "1.0.0";
+
+ /**
+ * The current version rule check for the stage data format.
+ */
+ public static final STAGE_DATA_VERSION_RULE:String = "1.0.x";
static final stageCache:Map = new Map();
@@ -163,20 +169,21 @@ class StageDataParser
}
}
- static final DEFAULT_NAME:String = "Untitled Stage";
- static final DEFAULT_CAMERAZOOM:Float = 1.0;
- static final DEFAULT_ZINDEX:Int = 0;
- static final DEFAULT_DANCEEVERY:Int = 0;
- static final DEFAULT_SCALE:Float = 1.0;
- static final DEFAULT_ISPIXEL:Bool = false;
- static final DEFAULT_POSITION:Array = [0, 0];
- static final DEFAULT_SCROLL:Array = [0, 0];
- static final DEFAULT_FRAMEINDICES:Array = [];
static final DEFAULT_ANIMTYPE:String = "sparrow";
+ static final DEFAULT_CAMERAZOOM:Float = 1.0;
+ static final DEFAULT_DANCEEVERY:Int = 0;
+ static final DEFAULT_ISPIXEL:Bool = false;
+ static final DEFAULT_NAME:String = "Untitled Stage";
+ static final DEFAULT_OFFSETS:Array = [0, 0];
+ static final DEFAULT_POSITION:Array = [0, 0];
+ static final DEFAULT_SCALE:Float = 1.0;
+ static final DEFAULT_SCROLL:Array = [0, 0];
+ static final DEFAULT_ZINDEX:Int = 0;
static final DEFAULT_CHARACTER_DATA:StageDataCharacter = {
zIndex: DEFAULT_ZINDEX,
position: DEFAULT_POSITION,
+ cameraOffsets: DEFAULT_OFFSETS,
}
/**
@@ -194,12 +201,18 @@ class StageDataParser
return null;
}
- if (input.version != STAGE_DATA_VERSION)
+ if (input.version == null)
{
trace('[STAGEDATA] ERROR: Could not load stage data for "$id": missing version');
return null;
}
+ if (!VersionUtil.validateVersion(input.version, STAGE_DATA_VERSION_RULE))
+ {
+ trace('[STAGEDATA] ERROR: Could not load stage data for "$id": bad version (got ${input.version}, expected ${STAGE_DATA_VERSION_RULE})');
+ return null;
+ }
+
if (input.name == null)
{
trace('[STAGEDATA] WARN: Stage data for "$id" missing name');
@@ -211,10 +224,9 @@ class StageDataParser
input.cameraZoom = DEFAULT_CAMERAZOOM;
}
- if (input.props == null || input.props.length == 0)
+ if (input.props == null)
{
- trace('[STAGEDATA] ERROR: Could not load stage data for "$id": missing props');
- return null;
+ input.props = [];
}
for (inputProp in input.props)
@@ -296,14 +308,14 @@ class StageDataParser
inputAnimation.frameRate = 24;
}
- if (inputAnimation.frameIndices == null)
+ if (inputAnimation.offsets == null)
{
- inputAnimation.frameIndices = DEFAULT_FRAMEINDICES;
+ inputAnimation.offsets = DEFAULT_OFFSETS;
}
- if (inputAnimation.loop == null)
+ if (inputAnimation.looped == null)
{
- inputAnimation.loop = true;
+ inputAnimation.looped = true;
}
if (inputAnimation.flipX == null)
@@ -347,6 +359,10 @@ class StageDataParser
{
inputCharacter.position = [0, 0];
}
+ if (inputCharacter.cameraOffsets == null || inputCharacter.cameraOffsets.length != 2)
+ {
+ inputCharacter.cameraOffsets = [0, 0];
+ }
}
// All good!
@@ -356,8 +372,12 @@ class StageDataParser
typedef StageData =
{
- // Uses semantic versioning.
+ /**
+ * The sematic version number of the stage data JSON format.
+ * Supports fancy comparisons like NPM does it's neat.
+ */
var version:String;
+
var name:String;
var cameraZoom:Null;
var props:Array;
@@ -432,7 +452,7 @@ typedef StageDataProp =
* An optional array of animations which the prop can play.
* @default Prop has no animations.
*/
- var animations:Array;
+ var animations:Array;
/**
* If animations are used, this is the name of the animation to play first.
@@ -448,52 +468,6 @@ typedef StageDataProp =
var animType:String;
};
-typedef StageDataPropAnimation =
-{
- /**
- * The name of the animation.
- */
- var name:String;
-
- /**
- * The common beginning of image names in atlas for this animation's frames.
- * For example, if the frames are named "test0001.png", "test0002.png", etc., use "test".
- */
- var prefix:String;
-
- /**
- * If you want this animation to use only certain frames of an animation with a given prefix,
- * select them here.
- * @example [0, 1, 2, 3] (use only the first four frames)
- * @default [] (all frames)
- */
- var frameIndices:Array;
-
- /**
- * The speed of the animation in frames per second.
- * @default 24
- */
- var frameRate:Null;
-
- /**
- * Whether the animation should loop.
- * @default false
- */
- var loop:Null;
-
- /**
- * Whether to flip the sprite horizontally while animating.
- * @default false
- */
- var flipX:Null;
-
- /**
- * Whether to flip the sprite vertically while animating.
- * @default false
- */
- var flipY:Null;
-};
-
typedef StageDataCharacter =
{
/**
@@ -505,5 +479,12 @@ typedef StageDataCharacter =
/**
* The position to render the character at.
- */ position:Array
+ */
+ position:Array,
+
+ /**
+ * The camera offsets to apply when focusing on the character on this stage.
+ * @default [0, 0]
+ */
+ cameraOffsets:Array,
};
diff --git a/source/funkin/ui/AtlasText.hx b/source/funkin/ui/AtlasText.hx
index 6df5f9e22..b07aca240 100644
--- a/source/funkin/ui/AtlasText.hx
+++ b/source/funkin/ui/AtlasText.hx
@@ -1,21 +1,13 @@
package funkin.ui;
import flixel.FlxSprite;
-import flixel.group.FlxSpriteGroup;
import flixel.graphics.frames.FlxAtlasFrames;
+import flixel.group.FlxSpriteGroup;
import flixel.util.FlxStringUtil;
-@:forward
-abstract BoldText(AtlasText) from AtlasText to AtlasText
-{
- inline public function new(x = 0.0, y = 0.0, text:String)
- {
- this = new AtlasText(x, y, text, Bold);
- }
-}
-
/**
- * Alphabet.hx has a ton of bugs and does a bunch of stuff I don't need, fuck that class
+ * AtlasText is an improved version of Alphabet and FlxBitmapText.
+ * It supports animations on the letters, and is less buggy than Alphabet.
*/
class AtlasText extends FlxTypedSpriteGroup
{
@@ -41,7 +33,7 @@ class AtlasText extends FlxTypedSpriteGroup
inline function get_maxHeight()
return font.maxHeight;
- public function new(x = 0.0, y = 0.0, text:String, fontName:AtlasFont = Default)
+ public function new(x = 0.0, y = 0.0, text:String, fontName:AtlasFont = AtlasFont.DEFAULT)
{
if (!fonts.exists(fontName))
fonts[fontName] = new AtlasFontData(fontName);
@@ -246,7 +238,14 @@ private class AtlasFontData
public function new(name:AtlasFont)
{
- atlas = Paths.getSparrowAtlas("fonts/" + name.getName().toLowerCase());
+ var fontName:String = name;
+ atlas = Paths.getSparrowAtlas('fonts/${fontName.toLowerCase()}');
+ if (atlas == null)
+ {
+ FlxG.log.warn('Could not find font atlas for font "${fontName}".');
+ return;
+ }
+
atlas.parent.destroyOnNoUse = false;
atlas.parent.persist = true;
@@ -276,8 +275,8 @@ enum Case
Lower;
}
-enum AtlasFont
+enum abstract AtlasFont(String) from String to String
{
- Default;
- Bold;
+ var DEFAULT = "default";
+ var BOLD = "bold";
}
diff --git a/source/funkin/ui/ControlsMenu.hx b/source/funkin/ui/ControlsMenu.hx
index fb4144b5b..e5e3e44b0 100644
--- a/source/funkin/ui/ControlsMenu.hx
+++ b/source/funkin/ui/ControlsMenu.hx
@@ -1,6 +1,5 @@
package funkin.ui;
-import funkin.Controls;
import flixel.FlxCamera;
import flixel.FlxObject;
import flixel.FlxSprite;
@@ -8,6 +7,7 @@ import flixel.group.FlxGroup;
import flixel.input.actions.FlxActionInput;
import flixel.input.gamepad.FlxGamepadInputID;
import flixel.input.keyboard.FlxKey;
+import funkin.Controls;
import funkin.ui.AtlasText;
import funkin.ui.MenuList;
import funkin.ui.TextMenuList;
@@ -66,11 +66,11 @@ class ControlsMenu extends funkin.ui.OptionsState.Page
var item;
- item = deviceList.createItem("Keyboard", Bold, selectDevice.bind(Keys));
+ item = deviceList.createItem("Keyboard", AtlasFont.BOLD, selectDevice.bind(Keys));
item.x = FlxG.width / 2 - item.width - 30;
item.y = (devicesBg.height - item.height) / 2;
- item = deviceList.createItem("Gamepad", Bold, selectDevice.bind(Gamepad(FlxG.gamepads.firstActive.id)));
+ item = deviceList.createItem("Gamepad", AtlasFont.BOLD, selectDevice.bind(Gamepad(FlxG.gamepads.firstActive.id)));
item.x = FlxG.width / 2 + 30;
item.y = (devicesBg.height - item.height) / 2;
}
@@ -87,20 +87,20 @@ class ControlsMenu extends funkin.ui.OptionsState.Page
if (currentHeader != "UI_" && name.indexOf("UI_") == 0)
{
currentHeader = "UI_";
- headers.add(new BoldText(0, y, "UI")).screenCenter(X);
+ headers.add(new AtlasText(0, y, "UI", AtlasFont.BOLD)).screenCenter(X);
y += spacer;
}
else if (currentHeader != "NOTE_" && name.indexOf("NOTE_") == 0)
{
currentHeader = "NOTE_";
- headers.add(new BoldText(0, y, "NOTES")).screenCenter(X);
+ headers.add(new AtlasText(0, y, "NOTES", AtlasFont.BOLD)).screenCenter(X);
y += spacer;
}
if (currentHeader != null && name.indexOf(currentHeader) == 0)
name = name.substr(currentHeader.length);
- var label = labels.add(new BoldText(150, y, name));
+ var label = labels.add(new AtlasText(150, y, name, AtlasFont.BOLD));
label.alpha = 0.6;
for (i in 0...COLUMNS)
createItem(label.x + 400 + i * 300, y, control, i);
@@ -317,7 +317,7 @@ class InputItem extends TextMenuItem
this.index = index;
this.input = getInput();
- super(x, y, getLabel(input), Default, callback);
+ super(x, y, getLabel(input), DEFAULT, callback);
}
public function updateDevice(device:Device)
diff --git a/source/funkin/ui/OptionsState.hx b/source/funkin/ui/OptionsState.hx
index 1b6d03e93..3a74ef3f8 100644
--- a/source/funkin/ui/OptionsState.hx
+++ b/source/funkin/ui/OptionsState.hx
@@ -5,10 +5,9 @@ import flixel.FlxSubState;
import flixel.addons.transition.FlxTransitionableState;
import flixel.group.FlxGroup;
import flixel.util.FlxSignal;
-import funkin.i18n.FireTongueHandler.t;
+import funkin.util.Constants;
+import funkin.util.WindowUtil;
-// typedef OptionsState = OptionsMenu_old;
-// class OptionsState_new extends MusicBeatState
class OptionsState extends MusicBeatState
{
var pages = new Map();
@@ -31,17 +30,12 @@ class OptionsState extends MusicBeatState
var options = addPage(Options, new OptionsMenu(false));
var preferences = addPage(Preferences, new PreferencesMenu());
var controls = addPage(Controls, new ControlsMenu());
- // var colors = addPage(Colors, new ColorsMenu());
-
- var mods = addPage(Mods, new ModMenu());
if (options.hasMultipleOptions())
{
options.onExit.add(exitToMainMenu);
controls.onExit.add(switchPage.bind(Options));
- // colors.onExit.add(switchPage.bind(Options));
preferences.onExit.add(switchPage.bind(Options));
- mods.onExit.add(switchPage.bind(Options));
}
else
{
@@ -67,12 +61,18 @@ class OptionsState extends MusicBeatState
function setPage(name:PageName)
{
if (pages.exists(currentName))
+ {
currentPage.exists = false;
+ currentPage.visible = false;
+ }
currentName = name;
if (pages.exists(currentName))
+ {
currentPage.exists = true;
+ currentPage.visible = true;
+ }
}
override function finishTransIn()
@@ -91,7 +91,7 @@ class OptionsState extends MusicBeatState
function exitToMainMenu()
{
currentPage.enabled = false;
- // Todo animate?
+ // TODO: Animate this transition?
FlxG.switchState(new MainMenuState());
}
}
@@ -172,30 +172,29 @@ class OptionsMenu extends Page
super();
add(items = new TextMenuList());
- createItem(t("PREFERENCES"), function() switchPage(Preferences));
- createItem(t("CONTROLS"), function() switchPage(Controls));
- // createItem(t("COLORS"), function() switchPage(Colors));
- createItem(t("MODS"), function() switchPage(Mods));
+ createItem("PREFERENCES", function() switchPage(Preferences));
+ createItem("CONTROLS", function() switchPage(Controls));
+ // createItem("COLORS", function() switchPage(Colors));
#if CAN_OPEN_LINKS
if (showDonate)
{
var hasPopupBlocker = #if web true #else false #end;
- createItem(t("DONATE"), selectDonate, hasPopupBlocker);
+ createItem("DONATE", selectDonate, hasPopupBlocker);
}
#end
#if newgrounds
if (NGio.isLoggedIn)
- createItem(t("LOGOUT"), selectLogout);
+ createItem("LOGOUT", selectLogout);
else
- createItem(t("LOGIN"), selectLogin);
+ createItem("LOGIN", selectLogin);
#end
- createItem(t("EXIT"), exit);
+ createItem("EXIT", exit);
}
function createItem(name:String, callback:Void->Void, fireInstantly = false)
{
- var item = items.createItem(0, 100 + items.length * 100, name, Bold, callback);
+ var item = items.createItem(0, 100 + items.length * 100, name, BOLD, callback);
item.fireInstantly = fireInstantly;
item.screenCenter(X);
return item;
@@ -219,11 +218,7 @@ class OptionsMenu extends Page
#if CAN_OPEN_LINKS
function selectDonate()
{
- #if linux
- Sys.command('/usr/bin/xdg-open', ["https://ninja-muffin24.itch.io/funkin", "&"]);
- #else
- FlxG.openURL('https://ninja-muffin24.itch.io/funkin');
- #end
+ WindowUtil.openURL(Constants.URL_ITCH);
}
#end
diff --git a/source/funkin/ui/PreferencesMenu.hx b/source/funkin/ui/PreferencesMenu.hx
index 6fcf7b57a..94158bcce 100644
--- a/source/funkin/ui/PreferencesMenu.hx
+++ b/source/funkin/ui/PreferencesMenu.hx
@@ -4,8 +4,8 @@ import flixel.FlxCamera;
import flixel.FlxObject;
import flixel.FlxSprite;
import funkin.ui.AtlasText.AtlasFont;
-import funkin.ui.TextMenuList.TextMenuItem;
import funkin.ui.OptionsState.Page;
+import funkin.ui.TextMenuList.TextMenuItem;
class PreferencesMenu extends Page
{
@@ -84,7 +84,7 @@ class PreferencesMenu extends Page
private function createPrefItem(prefName:String, prefString:String, prefValue:Dynamic):Void
{
- items.createItem(120, (120 * items.length) + 30, prefName, AtlasFont.Bold, function()
+ items.createItem(120, (120 * items.length) + 30, prefName, AtlasFont.BOLD, function()
{
preferenceCheck(prefString, prefValue);
@@ -157,16 +157,17 @@ class PreferencesMenu extends Page
});
}
- private static function preferenceCheck(prefString:String, prefValue:Dynamic):Void
+ private static function preferenceCheck(prefString:String, defaultValue:Dynamic):Void
{
if (preferences.get(prefString) == null)
{
- preferences.set(prefString, prefValue);
- trace('set preference!');
+ // Set the value to default.
+ preferences.set(prefString, defaultValue);
+ trace('Set preference to default: ${prefString} = ${defaultValue}');
}
else
{
- trace('found preference: ' + preferences.get(prefString));
+ trace('Found preference: ${prefString} = ${preferences.get(prefString)}');
}
}
}
diff --git a/source/funkin/ui/Prompt.hx b/source/funkin/ui/Prompt.hx
index 1679d3886..76d85744b 100644
--- a/source/funkin/ui/Prompt.hx
+++ b/source/funkin/ui/Prompt.hx
@@ -1,12 +1,12 @@
package funkin.ui;
import flixel.FlxSprite;
-import flixel.graphics.frames.FlxAtlasFrames;
-import flixel.text.FlxText;
-import flixel.util.FlxColor;
-import funkin.ui.AtlasText;
+import funkin.ui.AtlasText.AtlasFont;
import funkin.ui.MenuList;
+/**
+ * Opens a yes/no dialog box as a substate over the current state.
+ */
class Prompt extends flixel.FlxSubState
{
inline static var MARGIN = 100;
@@ -26,7 +26,7 @@ class Prompt extends flixel.FlxSubState
buttons = new TextMenuList(Horizontal);
- field = new BoldText(text);
+ field = new AtlasText(text, AtlasFont.BOLD);
field.scrollFactor.set(0, 0);
}
diff --git a/source/funkin/ui/TextMenuList.hx b/source/funkin/ui/TextMenuList.hx
index ae3ed7bff..fe1a13a7c 100644
--- a/source/funkin/ui/TextMenuList.hx
+++ b/source/funkin/ui/TextMenuList.hx
@@ -10,7 +10,7 @@ class TextMenuList extends MenuTypedList
super(navControls, wrapMode);
}
- public function createItem(x = 0.0, y = 0.0, name:String, font:AtlasFont = Bold, callback, fireInstantly = false)
+ public function createItem(x = 0.0, y = 0.0, name:String, font:AtlasFont = BOLD, callback, fireInstantly = false)
{
var item = new TextMenuItem(x, y, name, font, callback);
item.fireInstantly = fireInstantly;
@@ -20,7 +20,7 @@ class TextMenuList extends MenuTypedList
class TextMenuItem extends TextTypedMenuItem
{
- public function new(x = 0.0, y = 0.0, name:String, font:AtlasFont = Bold, callback)
+ public function new(x = 0.0, y = 0.0, name:String, font:AtlasFont = BOLD, callback)
{
super(x, y, new AtlasText(0, 0, name, font), name, callback);
setEmptyBackground();
diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx
index fb121e144..a93578c20 100644
--- a/source/funkin/util/Constants.hx
+++ b/source/funkin/util/Constants.hx
@@ -1,7 +1,7 @@
package funkin.util;
-import lime.app.Application;
import flixel.util.FlxColor;
+import lime.app.Application;
class Constants
{
@@ -18,6 +18,8 @@ class Constants
public static final VERSION_SUFFIX = ' PROTOTYPE';
public static var VERSION(get, null):String;
+ public static final FREAKY_MENU_BPM = 102;
+
#if debug
public static final GIT_HASH = funkin.util.macro.GitCommit.getGitCommitHash();
@@ -31,4 +33,7 @@ class Constants
return 'v${Application.current.meta.get('version')}' + VERSION_SUFFIX;
}
#end
+
+ public static final URL_KICKSTARTER:String = "https://www.kickstarter.com/projects/funkin/friday-night-funkin-the-full-ass-game/";
+ public static final URL_ITCH:String = "https://ninja-muffin24.itch.io/funkin";
}
diff --git a/source/funkin/util/VersionUtil.hx b/source/funkin/util/VersionUtil.hx
new file mode 100644
index 000000000..57e1f80ed
--- /dev/null
+++ b/source/funkin/util/VersionUtil.hx
@@ -0,0 +1,31 @@
+package funkin.util;
+
+import thx.semver.Version;
+import thx.semver.VersionRule;
+
+/**
+ * Remember, increment the patch version (1.0.x) if you make a bugfix,
+ * increment the minor version (1.x.0) if you make a new feature (but previous content is still compatible),
+ * and increment the major version (x.0.0) if you make a breaking change (e.g. new API or reorganized file format).
+ */
+class VersionUtil
+{
+ /**
+ * Checks that a given verison number satisisfies a given version rule.
+ * Version rule can be complex, e.g. "1.0.x" or ">=1.0.0,<1.1.0", or anything NPM supports.
+ */
+ public static function validateVersion(version:String, versionRule:String):Bool
+ {
+ try
+ {
+ var v:Version = version; // Perform a cast.
+ var vr:VersionRule = versionRule; // Perform a cast.
+ return v.satisfies(vr);
+ }
+ catch (e)
+ {
+ trace('[VERSIONUTIL] Invalid semantic version: ${version}');
+ return false;
+ }
+ }
+}
diff --git a/source/funkin/util/WindowUtil.hx b/source/funkin/util/WindowUtil.hx
new file mode 100644
index 000000000..fe27ed252
--- /dev/null
+++ b/source/funkin/util/WindowUtil.hx
@@ -0,0 +1,18 @@
+package funkin.util;
+
+class WindowUtil
+{
+ public static function openURL(targetUrl:String)
+ {
+ #if CAN_OPEN_LINKS
+ #if linux
+ // Sys.command('/usr/bin/xdg-open', [, "&"]);
+ Sys.command('/usr/bin/xdg-open', [targetUrl, "&"]);
+ #else
+ FlxG.openURL(targetUrl);
+ #end
+ #else
+ trace('Cannot open')
+ #end
+ }
+}
diff --git a/source/funkin/util/assets/FlxAnimationUtil.hx b/source/funkin/util/assets/FlxAnimationUtil.hx
new file mode 100644
index 000000000..d40dfa73c
--- /dev/null
+++ b/source/funkin/util/assets/FlxAnimationUtil.hx
@@ -0,0 +1,42 @@
+package funkin.util.assets;
+
+import flixel.FlxSprite;
+import funkin.play.AnimationData;
+
+class FlxAnimationUtil
+{
+ /**
+ * Properly adds an animation to a sprite based on JSON data.
+ */
+ public static function addAtlasAnimation(target:FlxSprite, anim:AnimationData)
+ {
+ var frameRate = anim.frameRate == null ? 24 : anim.frameRate;
+ var looped = anim.looped == null ? false : anim.looped;
+ var flipX = anim.flipX == null ? false : anim.flipX;
+ var flipY = anim.flipY == null ? false : anim.flipY;
+
+ if (anim.frameIndices != null && anim.frameIndices.length > 0)
+ {
+ // trace('addByIndices(${anim.name}, ${anim.prefix}, ${anim.frameIndices}, ${frameRate}, ${looped}, ${flipX}, ${flipY})');
+ target.animation.addByIndices(anim.name, anim.prefix, anim.frameIndices, "", frameRate, looped, flipX, flipY);
+ // trace('RESULT:${target.animation.getAnimationList()}');
+ }
+ else
+ {
+ // trace('addByPrefix(${anim.name}, ${anim.prefix}, ${frameRate}, ${looped}, ${flipX}, ${flipY})');
+ target.animation.addByPrefix(anim.name, anim.prefix, frameRate, looped, flipX, flipY);
+ // trace('RESULT:${target.animation.getAnimationList()}');
+ }
+ }
+
+ /**
+ * Properly adds multiple animations to a sprite based on JSON data.
+ */
+ public static function addAtlasAnimations(target:FlxSprite, animations:Array)
+ {
+ for (anim in animations)
+ {
+ addAtlasAnimation(target, anim);
+ }
+ }
+}