Fixed Week 4 boppers and car.

This commit is contained in:
Eric Myllyoja 2022-04-18 19:36:09 -04:00
parent 1485d27b07
commit b6da0b5e20
37 changed files with 1107 additions and 633 deletions

5
.gitignore vendored
View file

@ -2,7 +2,4 @@ export/
.vscode/ .vscode/
APIStuff.hx APIStuff.hx
.DS_STORE .DS_STORE
RECOVER_*.fla
example_mods/enaSkin/
example_mods/tricky/

17
.vscode/launch.json vendored
View file

@ -12,6 +12,21 @@
"name": "Haxe Eval", "name": "Haxe Eval",
"type": "haxe-eval", "type": "haxe-eval",
"request": "launch" "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"
] ]
} }

View file

@ -96,7 +96,13 @@
<!-- <assets path='example_mods' rename='mods' embed='false'/> --> <!-- <assets path='example_mods' rename='mods' embed='false'/> -->
<assets path='example_mods' rename='mods' embed='false' exclude="*.md" /> <!--
AUTOMATICALLY MOVING EXAMPLE MODS INTO THE BUILD CAUSES ISSUES
Currently, this line will add the mod files to the library manifest,
which causes issues if the mod is not enabled.
If we can exclude the `mods` folder from the manifest, we can re-enable this line.
<assets path='example_mods' rename='mods' embed='false' exclude="*.md" />
-->
<assets path='art/readme.txt' rename='do NOT readme.txt' /> <assets path='art/readme.txt' rename='do NOT readme.txt' />
<assets path="CHANGELOG.md" rename='changelog.txt' /> <assets path="CHANGELOG.md" rename='changelog.txt' />
@ -122,7 +128,7 @@
<!--haxelib name="newgrounds" unless="switch"/> --> <!--haxelib name="newgrounds" unless="switch"/> -->
<haxelib name="faxe" if='switch' /> <haxelib name="faxe" if='switch' />
<haxelib name="polymod" /> <haxelib name="polymod" />
<haxelib name="firetongue" /> <haxelib name="thx.semver" />
<!-- <haxelib name="colyseus"/> --> <!-- <haxelib name="colyseus"/> -->
<!-- <haxelib name="colyseus-websocket" /> --> <!-- <haxelib name="colyseus-websocket" /> -->
@ -191,7 +197,7 @@
<haxeflag name="--macro" value="include('funkin')" /> <haxeflag name="--macro" value="include('funkin')" />
<!-- Necessary to provide stack traces for HScript. --> <!-- Necessary to provide stack traces for HScript. -->
<haxedef name="hscriptPos" /> <haxedef name="hscriptPos" />
<haxedef name="HXCPP_CHECK_POINTER" /> <haxedef name="HXCPP_CHECK_POINTER" />
<haxedef name="HXCPP_STACK_LINE" /> <haxedef name="HXCPP_STACK_LINE" />

View file

@ -1,7 +1,9 @@
{ {
"title": "Intro Mod", "title": "Intro Mod",
"description": "An introductory mod.", "description": "An introductory mod.",
"author": "MasterEric", "contributors": [{
"name": "MasterEric"
}],
"api_version": "0.1.0", "api_version": "0.1.0",
"mod_version": "1.0.0", "mod_version": "1.0.0",
"license": "Apache-2.0" "license": "Apache-2.0"

View file

@ -1,7 +1,9 @@
{ {
"title": "Testing123", "title": "Testing123",
"description": "Newgrounds? More like OLDGROUNDS lol.", "description": "Newgrounds? More like OLDGROUNDS lol.",
"author": "MasterEric", "contributors": [{
"name": "MasterEric"
}],
"api_version": "0.1.0", "api_version": "0.1.0",
"mod_version": "1.0.0", "mod_version": "1.0.0",
"license": "Apache-2.0" "license": "Apache-2.0"

View file

@ -1,9 +1,9 @@
package; package;
import funkin.InitState;
import funkin.MemoryCounter;
import flixel.FlxGame; import flixel.FlxGame;
import flixel.FlxState; import flixel.FlxState;
import funkin.InitState;
import funkin.MemoryCounter;
import openfl.Lib; import openfl.Lib;
import openfl.display.FPS; import openfl.display.FPS;
import openfl.display.Sprite; import openfl.display.Sprite;
@ -37,17 +37,9 @@ class Main extends Sprite
{ {
super(); super();
// TODO: Ideally this should change to utilize a user interface. // TODO: Replace this with loadEnabledMods().
// 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<String>).
funkin.modding.PolymodHandler.loadAllMods(); funkin.modding.PolymodHandler.loadAllMods();
funkin.i18n.FireTongueHandler.init();
if (stage != null) if (stage != null)
{ {
init(); init();

View file

@ -15,9 +15,31 @@ typedef BPMChangeEvent =
class Conductor class Conductor
{ {
/**
* Beats per minute of the song.
*/
public static var bpm:Float = 100; 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 songPosition:Float;
public static var lastSongPos:Float; public static var lastSongPos:Float;
public static var offset:Float = 0; public static var offset:Float = 0;
@ -52,12 +74,4 @@ class Conductor
} }
trace("new BPM map BUDDY " + bpmChangeMap); trace("new BPM map BUDDY " + bpmChangeMap);
} }
public static function changeBPM(newBpm:Float)
{
bpm = newBpm;
crochet = ((60 / bpm) * 1000);
stepCrochet = crochet / 4;
}
} }

View file

@ -1,14 +1,11 @@
package funkin; package funkin;
import flixel.FlxObject; import flixel.FlxObject;
import flixel.FlxSubState;
import flixel.math.FlxPoint;
import flixel.system.FlxSound; import flixel.system.FlxSound;
import flixel.util.FlxColor; import flixel.util.FlxColor;
import flixel.util.FlxTimer; import flixel.util.FlxTimer;
import haxe.display.Display;
import funkin.ui.PreferencesMenu;
import funkin.play.PlayState; import funkin.play.PlayState;
import funkin.ui.PreferencesMenu;
class GameOverSubstate extends MusicBeatSubstate class GameOverSubstate extends MusicBeatSubstate
{ {
@ -57,7 +54,7 @@ class GameOverSubstate extends MusicBeatSubstate
add(camFollow); add(camFollow);
FlxG.sound.play(Paths.sound('fnf_loss_sfx' + stageSuffix)); FlxG.sound.play(Paths.sound('fnf_loss_sfx' + stageSuffix));
// Conductor.changeBPM(100); // Conductor.bpm = 100;
switch (PlayState.currentSong.player1) switch (PlayState.currentSong.player1)
{ {

View file

@ -31,7 +31,7 @@ class LatencyState extends FlxState
strumLine = new FlxSprite(FlxG.width / 2, 100).makeGraphic(FlxG.width, 5); strumLine = new FlxSprite(FlxG.width / 2, 100).makeGraphic(FlxG.width, 5);
add(strumLine); add(strumLine);
Conductor.changeBPM(120); Conductor.bpm = 120;
super.create(); super.create();
} }

View file

@ -2,9 +2,9 @@ package funkin;
import flixel.FlxSprite; import flixel.FlxSprite;
import flixel.FlxState; import flixel.FlxState;
import flixel.graphics.frames.FlxAtlasFrames;
import flixel.math.FlxMath; import flixel.math.FlxMath;
import flixel.util.FlxTimer; import flixel.util.FlxTimer;
import funkin.play.PlayState;
import haxe.io.Path; import haxe.io.Path;
import lime.app.Future; import lime.app.Future;
import lime.app.Promise; import lime.app.Promise;
@ -12,7 +12,6 @@ import lime.utils.AssetLibrary;
import lime.utils.AssetManifest; import lime.utils.AssetManifest;
import lime.utils.Assets as LimeAssets; import lime.utils.Assets as LimeAssets;
import openfl.utils.Assets; import openfl.utils.Assets;
import funkin.play.PlayState;
class LoadingState extends MusicBeatState class LoadingState extends MusicBeatState
{ {
@ -21,7 +20,6 @@ class LoadingState extends MusicBeatState
var target:FlxState; var target:FlxState;
var stopMusic = false; var stopMusic = false;
var callbacks:MultiCallback; var callbacks:MultiCallback;
var danceLeft = false; var danceLeft = false;
var loadBar:FlxSprite; 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; danceLeft = !danceLeft;
/*
if (danceLeft) return true;
gfDance.animation.play('danceRight');
else
gfDance.animation.play('danceLeft'); */
} }
var targetShit:Float = 0; var targetShit:Float = 0;

View file

@ -1,13 +1,21 @@
package funkin; package funkin;
import flixel.util.FlxColor; import flixel.FlxState;
import flixel.FlxSubState;
import flixel.addons.ui.FlxUIState;
import flixel.text.FlxText; 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.events.ScriptEvent;
import funkin.modding.module.ModuleHandler; import funkin.modding.module.ModuleHandler;
import funkin.modding.events.ScriptEvent.UpdateScriptEvent; import funkin.util.SortUtil;
import funkin.Conductor.BPMChangeEvent;
import flixel.addons.ui.FlxUIState;
/**
* 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 class MusicBeatState extends FlxUIState
{ {
private var curStep:Int = 0; private var curStep:Int = 0;
@ -21,13 +29,23 @@ class MusicBeatState extends FlxUIState
public var leftWatermarkText:FlxText = null; public var leftWatermarkText:FlxText = null;
public var rightWatermarkText:FlxText = null; public var rightWatermarkText:FlxText = null;
public function new()
{
super();
initCallbacks();
}
function initCallbacks()
{
subStateOpened.add(onOpenSubstateComplete);
subStateClosed.add(onCloseSubstateComplete);
}
override function create() override function create()
{ {
super.create(); super.create();
if (transIn != null)
trace('reg ' + transIn.region);
createWatermarkText(); createWatermarkText();
} }
@ -35,6 +53,10 @@ class MusicBeatState extends FlxUIState
{ {
super.update(elapsed); super.update(elapsed);
// This can now be used in EVERY STATE YAY!
if (FlxG.keys.justPressed.F5)
debug_refreshModules();
// everyStep(); // everyStep();
var oldStep:Int = curStep; var oldStep:Int = curStep;
@ -44,6 +66,8 @@ class MusicBeatState extends FlxUIState
if (oldStep != curStep && curStep >= 0) if (oldStep != curStep && curStep >= 0)
stepHit(); stepHit();
FlxG.watch.addQuick("songPos", Conductor.songPosition);
dispatchEvent(new UpdateScriptEvent(elapsed)); dispatchEvent(new UpdateScriptEvent(elapsed));
} }
@ -71,6 +95,14 @@ class MusicBeatState extends FlxUIState
ModuleHandler.callEvent(event); ModuleHandler.callEvent(event);
} }
function debug_refreshModules()
{
PolymodHandler.forceReloadAssets();
// Restart the current state, so old data is cleared.
FlxG.resetState();
}
private function updateBeat():Void private function updateBeat():Void
{ {
curBeat = Math.floor(curStep / 4); curBeat = Math.floor(curStep / 4);
@ -92,15 +124,97 @@ class MusicBeatState extends FlxUIState
curStep = lastChange.stepTime + Math.floor((Conductor.songPosition - lastChange.songTime) / Conductor.stepCrochet); 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) if (curStep % 4 == 0)
beatHit(); 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; 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));
} }
} }

View file

@ -1,8 +1,13 @@
package funkin; package funkin;
import funkin.Conductor.BPMChangeEvent;
import flixel.FlxSubState; 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 class MusicBeatSubstate extends FlxSubState
{ {
public function new() public function new()
@ -53,6 +58,11 @@ class MusicBeatSubstate extends FlxSubState
beatHit(); beatHit();
} }
function dispatchEvent(event:ScriptEvent)
{
ModuleHandler.callEvent(event);
}
public function beatHit():Void public function beatHit():Void
{ {
// do literally nothing dumbass // do literally nothing dumbass

View file

@ -4,13 +4,7 @@ import flixel.FlxObject;
import flixel.FlxSprite; import flixel.FlxSprite;
import flixel.FlxState; import flixel.FlxState;
import flixel.group.FlxGroup; import flixel.group.FlxGroup;
import flixel.input.android.FlxAndroidKey;
import flixel.input.android.FlxAndroidKeys;
import flixel.input.gamepad.FlxGamepad; 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.FlxEase;
import flixel.tweens.FlxTween; import flixel.tweens.FlxTween;
import flixel.util.FlxColor; import flixel.util.FlxColor;
@ -19,29 +13,20 @@ import funkin.audiovis.SpectogramSprite;
import funkin.shaderslmfao.BuildingShaders; import funkin.shaderslmfao.BuildingShaders;
import funkin.shaderslmfao.ColorSwap; import funkin.shaderslmfao.ColorSwap;
import funkin.shaderslmfao.TitleOutline; import funkin.shaderslmfao.TitleOutline;
import funkin.ui.PreferencesMenu; import funkin.ui.AtlasText;
import lime.app.Application; import funkin.util.Constants;
import lime.graphics.Image;
import lime.media.AudioContext;
import lime.ui.Window;
import openfl.Assets; import openfl.Assets;
import openfl.display.Sprite; import openfl.display.Sprite;
import openfl.events.AsyncErrorEvent; import openfl.events.AsyncErrorEvent;
import openfl.events.Event;
import openfl.events.MouseEvent; import openfl.events.MouseEvent;
import openfl.events.NetStatusEvent; import openfl.events.NetStatusEvent;
import openfl.media.Video; import openfl.media.Video;
import openfl.net.NetConnection;
import openfl.net.NetStream; import openfl.net.NetStream;
using StringTools; using StringTools;
#if desktop #if desktop
import sys.FileSystem;
import sys.io.File;
import sys.thread.Thread;
#end #end
class TitleState extends MusicBeatState class TitleState extends MusicBeatState
{ {
public static var initialized:Bool = false; public static var initialized:Bool = false;
@ -73,33 +58,33 @@ class TitleState extends MusicBeatState
super.create(); super.create();
/* /*
#elseif web #elseif web
if (!initialized) if (!initialized)
{ {
video = new Video(); video = new Video();
FlxG.stage.addChild(video); FlxG.stage.addChild(video);
var netConnection = new NetConnection(); var netConnection = new NetConnection();
netConnection.connect(null); netConnection.connect(null);
netStream = new NetStream(netConnection); netStream = new NetStream(netConnection);
netStream.client = {onMetaData: client_onMetaData}; netStream.client = {onMetaData: client_onMetaData};
netStream.addEventListener(AsyncErrorEvent.ASYNC_ERROR, netStream_onAsyncError); netStream.addEventListener(AsyncErrorEvent.ASYNC_ERROR, netStream_onAsyncError);
netConnection.addEventListener(NetStatusEvent.NET_STATUS, netConnection_onNetStatus); netConnection.addEventListener(NetStatusEvent.NET_STATUS, netConnection_onNetStatus);
// netStream.addEventListener(NetStatusEvent.NET_STATUS) // netStream.play(Paths.file('music/kickstarterTrailer.mp4')); // netStream.addEventListener(NetStatusEvent.NET_STATUS) // netStream.play(Paths.file('music/kickstarterTrailer.mp4'));
overlay = new Sprite(); overlay = new Sprite();
overlay.graphics.beginFill(0, 0.5); overlay.graphics.beginFill(0, 0.5);
overlay.graphics.drawRect(0, 0, 1280, 720); overlay.graphics.drawRect(0, 0, 1280, 720);
overlay.addEventListener(MouseEvent.MOUSE_DOWN, overlay_onMouseDown); overlay.addEventListener(MouseEvent.MOUSE_DOWN, overlay_onMouseDown);
overlay.buttonMode = true; overlay.buttonMode = true;
// FlxG.stage.addChild(overlay); // FlxG.stage.addChild(overlay);
} }
*/ */
// netConnection.addEventListener(MouseEvent.MOUSE_DOWN, overlay_onMouseDown); // netConnection.addEventListener(MouseEvent.MOUSE_DOWN, overlay_onMouseDown);
@ -158,9 +143,9 @@ class TitleState extends MusicBeatState
{ {
FlxG.sound.playMusic(Paths.music('freakyMenu'), 0); FlxG.sound.playMusic(Paths.music('freakyMenu'), 0);
FlxG.sound.music.fadeIn(4, 0, 0.7); FlxG.sound.music.fadeIn(4, 0, 0.7);
Conductor.bpm = Constants.FREAKY_MENU_BPM;
} }
Conductor.changeBPM(102);
persistentUpdate = true; persistentUpdate = true;
var bg:FlxSprite = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, FlxColor.BLACK); var bg:FlxSprite = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, FlxColor.BLACK);
@ -189,8 +174,6 @@ class TitleState extends MusicBeatState
gfDance.antialiasing = true; gfDance.antialiasing = true;
add(gfDance); add(gfDance);
trace('MACRO TEST: ${gfDance.zIndex}');
// alphaShader.shader.funnyShit.input = gfDance.pixels; // old shit // alphaShader.shader.funnyShit.input = gfDance.pixels; // old shit
logoBl.shader = alphaShader.shader; logoBl.shader = alphaShader.shader;
@ -220,6 +203,7 @@ class TitleState extends MusicBeatState
blackScreen = bg.clone(); blackScreen = bg.clone();
credGroup.add(blackScreen); credGroup.add(blackScreen);
credGroup.add(textGroup);
// var atlasBullShit:FlxSprite = new FlxSprite(); // var atlasBullShit:FlxSprite = new FlxSprite();
// atlasBullShit.frames = CoolUtil.fromAnimate(Paths.image('money'), Paths.file('images/money.json')); // atlasBullShit.frames = CoolUtil.fromAnimate(Paths.image('money'), Paths.file('images/money.json'));
@ -290,13 +274,13 @@ class TitleState extends MusicBeatState
#end #end
/* if (FlxG.onMobile) /* 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) if (FlxG.keys.justPressed.I)
{ {
@ -313,37 +297,37 @@ class TitleState extends MusicBeatState
} }
/* /*
FlxG.watch.addQuick('cur display', FlxG.stage.window.display.id); FlxG.watch.addQuick('cur display', FlxG.stage.window.display.id);
if (FlxG.keys.justPressed.Y) if (FlxG.keys.justPressed.Y)
{
// trace(FlxG.stage.window.display.name);
if (FlxG.gamepads.firstActive != null)
{ {
// trace(FlxG.stage.window.display.name); trace(FlxG.gamepads.firstActive.model);
FlxG.gamepads.firstActive.id
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);
}
} }
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 #if debug
@ -382,13 +366,6 @@ class TitleState extends MusicBeatState
pressedEnter = true; pressedEnter = true;
#end #end
} }
// a faster intro thing lol!
if (pressedEnter && transitioning && skippedIntro)
{
FlxG.switchState(new MainMenuState());
}
if (pressedEnter && !transitioning && skippedIntro) if (pressedEnter && !transitioning && skippedIntro)
{ {
if (FlxG.sound.music != null) if (FlxG.sound.music != null)
@ -434,22 +411,23 @@ class TitleState extends MusicBeatState
Assets.cache.clear(Paths.image('logoBumpin')); Assets.cache.clear(Paths.image('logoBumpin'));
Assets.cache.clear(Paths.image('titleEnter')); Assets.cache.clear(Paths.image('titleEnter'));
// ngSpr?? // ngSpr??
FlxG.switchState(targetState);
}); });
// FlxG.sound.play(Paths.music('titleShoot'), 0.7); // FlxG.sound.play(Paths.music('titleShoot'), 0.7);
} }
if (pressedEnter && !skippedIntro && initialized) if (pressedEnter && !skippedIntro && initialized)
skipIntro(); skipIntro();
/* /*
#if web #if web
if (!initialized && controls.ACCEPT) if (!initialized && controls.ACCEPT)
{ {
// netStream.dispose(); // netStream.dispose();
// FlxG.stage.removeChild(video); // FlxG.stage.removeChild(video);
startIntro(); startIntro();
skipIntro(); skipIntro();
} }
#end #end
*/ */
if (controls.UI_LEFT) if (controls.UI_LEFT)
@ -503,39 +481,47 @@ class TitleState extends MusicBeatState
var spec:SpectogramSprite = new SpectogramSprite(FlxG.sound.music); var spec:SpectogramSprite = new SpectogramSprite(FlxG.sound.music);
add(spec); add(spec);
Conductor.changeBPM(190); Conductor.bpm = 190;
FlxG.camera.flash(FlxColor.WHITE, 1); FlxG.camera.flash(FlxColor.WHITE, 1);
FlxG.sound.play(Paths.sound('confirmMenu'), 0.7); FlxG.sound.play(Paths.sound('confirmMenu'), 0.7);
} }
function createCoolText(textArray:Array<String>) function createCoolText(textArray:Array<String>)
{ {
if (credGroup == null || textGroup == null)
return;
for (i in 0...textArray.length) 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.screenCenter(X);
money.y += (i * 60) + 200; money.y += (i * 60) + 200;
credGroup.add(money); // credGroup.add(money);
textGroup.add(money); textGroup.add(money);
} }
} }
function addMoreText(text:String) function addMoreText(text:String)
{ {
if (credGroup == null || textGroup == null)
return;
lime.ui.Haptic.vibrate(100, 100); 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.screenCenter(X);
coolText.y += (textGroup.length * 60) + 200; coolText.y += (textGroup.length * 60) + 200;
credGroup.add(coolText);
textGroup.add(coolText); textGroup.add(coolText);
} }
function deleteCoolText() function deleteCoolText()
{ {
if (credGroup == null || textGroup == null)
return;
while (textGroup.members.length > 0) while (textGroup.members.length > 0)
{ {
credGroup.remove(textGroup.members[0], true); // credGroup.remove(textGroup.members[0], true);
textGroup.remove(textGroup.members[0], true); textGroup.remove(textGroup.members[0], true);
} }
} }
@ -543,9 +529,11 @@ class TitleState extends MusicBeatState
var isRainbow:Bool = false; var isRainbow:Bool = false;
var skippedIntro: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) if (!skippedIntro)
{ {
@ -605,6 +593,8 @@ class TitleState extends MusicBeatState
else else
gfDance.animation.play('danceLeft'); gfDance.animation.play('danceLeft');
} }
return true;
} }
function skipIntro():Void function skipIntro():Void

View file

@ -1,12 +1,5 @@
package funkin.charting; 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.FlxSprite;
import flixel.addons.display.FlxGridOverlay; import flixel.addons.display.FlxGridOverlay;
import flixel.addons.transition.FlxTransitionableState; import flixel.addons.transition.FlxTransitionableState;
@ -23,14 +16,21 @@ import flixel.system.FlxSound;
import flixel.text.FlxText; import flixel.text.FlxText;
import flixel.ui.FlxButton; import flixel.ui.FlxButton;
import flixel.util.FlxColor; 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 haxe.Json;
import lime.media.AudioBuffer; import lime.media.AudioBuffer;
import lime.utils.Assets; import lime.utils.Assets;
import openfl.events.Event; import openfl.events.Event;
import openfl.events.IOErrorEvent; import openfl.events.IOErrorEvent;
import openfl.net.FileReference; import openfl.net.FileReference;
import funkin.rendering.MeshRender;
import funkin.play.PlayState;
using Lambda; using Lambda;
using StringTools; using StringTools;
@ -144,7 +144,7 @@ class ChartingState extends MusicBeatState
updateGrid(); updateGrid();
loadSong(_song.song); loadSong(_song.song);
Conductor.changeBPM(_song.bpm); Conductor.bpm = _song.bpm;
Conductor.mapBPMChanges(_song); Conductor.mapBPMChanges(_song);
bpmTxt = new FlxText(1000, 50, 0, "", 16); bpmTxt = new FlxText(1000, 50, 0, "", 16);
@ -545,7 +545,7 @@ class ChartingState extends MusicBeatState
{ {
tempBpm = nums.value; tempBpm = nums.value;
Conductor.mapBPMChanges(_song); Conductor.mapBPMChanges(_song);
Conductor.changeBPM(nums.value); Conductor.bpm = nums.value;
} }
else if (wname == 'note_susLength') else if (wname == 'note_susLength')
{ {
@ -975,9 +975,9 @@ class ChartingState extends MusicBeatState
_song.bpm = tempBpm; _song.bpm = tempBpm;
/* if (FlxG.keys.justPressed.UP) /* if (FlxG.keys.justPressed.UP)
Conductor.changeBPM(Conductor.bpm + 1); Conductor.bpm = Conductor.bpm + 1;
if (FlxG.keys.justPressed.DOWN) if (FlxG.keys.justPressed.DOWN)
Conductor.changeBPM(Conductor.bpm - 1); */ Conductor.bpm = Conductor.bpm - 1; */
var shiftThing:Int = 1; var shiftThing:Int = 1;
if (FlxG.keys.pressed.SHIFT) if (FlxG.keys.pressed.SHIFT)
@ -1213,7 +1213,7 @@ class ChartingState extends MusicBeatState
if (SongLoad.getSong()[curSection].changeBPM && SongLoad.getSong()[curSection].bpm > 0) 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!'); FlxG.log.add('CHANGED BPM!');
} }
else else
@ -1223,7 +1223,7 @@ class ChartingState extends MusicBeatState
for (i in 0...curSection) for (i in 0...curSection)
if (SongLoad.getSong()[i].changeBPM) if (SongLoad.getSong()[i].changeBPM)
daBPM = SongLoad.getSong()[i].bpm; daBPM = SongLoad.getSong()[i].bpm;
Conductor.changeBPM(daBPM); Conductor.bpm = daBPM;
} }
/* // PORT BULLSHIT, INCASE THERE'S NO SUSTAIN DATA FOR A NOTE /* // PORT BULLSHIT, INCASE THERE'S NO SUSTAIN DATA FOR A NOTE

View file

@ -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', ['<X>'], ['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<String> = null, values:Array<String> = null):String
{
var str = t(key, context);
return firetongue.Replace.flags(str, flags, values);
}
}

View file

@ -1,3 +0,0 @@
# i18n
This package contains functions used for internationalization (i18n).

View file

@ -23,6 +23,20 @@ interface IStateChangingScriptedClass extends IScriptedClass
{ {
public function onStateChangeBegin(event:StateChangeScriptEvent):Void; public function onStateChangeBegin(event:StateChangeScriptEvent):Void;
public function onStateChangeEnd(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 interface IPlayStateScriptedClass extends IScriptedClass
{ {
public function onPause(event:ScriptEvent):Void; public function onPause(event:PauseScriptEvent):Void;
public function onResume(event:ScriptEvent):Void; public function onResume(event:ScriptEvent):Void;
public function onSongLoaded(eent:SongLoadScriptEvent):Void; public function onSongLoaded(eent:SongLoadScriptEvent):Void;
public function onSongStart(event:ScriptEvent):Void; public function onSongStart(event:ScriptEvent):Void;
public function onSongEnd(event:ScriptEvent):Void; public function onSongEnd(event:ScriptEvent):Void;
public function onSongReset(event:ScriptEvent):Void;
public function onGameOver(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 onNoteHit(event:NoteScriptEvent):Void;
public function onNoteMiss(event:NoteScriptEvent):Void; public function onNoteMiss(event:NoteScriptEvent):Void;
public function onNoteGhostMiss(event:GhostMissNoteScriptEvent):Void;
public function onStepHit(event:SongTimeScriptEvent):Void; public function onStepHit(event:SongTimeScriptEvent):Void;
public function onBeatHit(event:SongTimeScriptEvent):Void; public function onBeatHit(event:SongTimeScriptEvent):Void;

View file

@ -1,10 +1,9 @@
package funkin.modding; package funkin.modding;
import polymod.Polymod.ModMetadata; import funkin.modding.module.ModuleHandler;
import funkin.play.stage.StageData;
import polymod.Polymod; import polymod.Polymod;
import polymod.backends.OpenFLBackend;
import polymod.backends.PolymodAssets.PolymodAssetType; import polymod.backends.PolymodAssets.PolymodAssetType;
import polymod.format.ParseRules.LinesParseFormat;
import polymod.format.ParseRules.TextFileFormat; import polymod.format.ParseRules.TextFileFormat;
class PolymodHandler class PolymodHandler
@ -31,6 +30,15 @@ class PolymodHandler
loadModsById(getAllModIds()); 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. * Loads the game without any mods enabled with Polymod.
*/ */
@ -146,8 +154,8 @@ class PolymodHandler
{ {
return { return {
assetLibraryPaths: [ assetLibraryPaths: [
"songs" => "songs", "shared" => "", "tutorial" => "tutorial", "scripts" => "scripts", "week1" => "week1", "week2" => "week2", "songs" => "songs", "shared" => "", "tutorial" => "tutorial", "scripts" => "scripts", "week1" => "week1", "week2" => "week2",
"week3" => "week3", "week4" => "week4", "week5" => "week5", "week6" => "week6", "week7" => "week7", "week8" => "week8", "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]; var modIds = [for (i in getAllMods()) i.id];
return modIds; return modIds;
} }
public static function setEnabledMods(newModList:Array<String>):Void
{
FlxG.save.data.enabledMods = newModList;
// Make sure to COMMIT the changes.
FlxG.save.flush();
}
/**
* Returns the list of enabled mods.
* @return Array<String>
*/
public static function getEnabledModIds():Array<String>
{
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<ModMetadata>
{
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();
}
} }

View file

@ -1,5 +1,8 @@
package funkin.modding.events; package funkin.modding.events;
import flixel.FlxState;
import flixel.FlxSubState;
import funkin.Note.NoteDir;
import funkin.play.Countdown.CountdownStep; import funkin.play.Countdown.CountdownStep;
import openfl.events.EventType; import openfl.events.EventType;
import openfl.events.KeyboardEvent; import openfl.events.KeyboardEvent;
@ -83,6 +86,15 @@ class ScriptEvent
*/ */
public static inline final NOTE_MISS:ScriptEventType = "NOTE_MISS"; 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. * 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"; 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. * 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"; 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. * This event is not cancelable.
*/ */
public static inline final GAME_OVER:ScriptEventType = "GAME_OVER"; 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. * 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. * 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"; 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. * 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. * 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. * 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. * until another script restarts it, or cancelling NOTE_HIT should cause the note to be missed.
*/ */
public var cancelable(default, null):Bool; public var cancelable(default, null):Bool;
@ -209,7 +250,7 @@ class ScriptEvent
/** /**
* Call this function on a cancelable event to cancel the associated behavior. * 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 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. * 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 public function new(type:ScriptEventType, beat:Int, step:Int):Void
{ {
super(type, false); super(type, true);
this.beat = beat; this.beat = beat;
this.step = step; this.step = step;
} }
@ -403,13 +497,58 @@ class SongLoadScriptEvent extends ScriptEvent
*/ */
class StateChangeScriptEvent 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 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;
} }
} }

View file

@ -1,7 +1,7 @@
package funkin.modding.events; package funkin.modding.events;
import funkin.modding.IScriptedClass;
import funkin.modding.IScriptedClass.IPlayStateScriptedClass; import funkin.modding.IScriptedClass.IPlayStateScriptedClass;
import funkin.modding.IScriptedClass;
/** /**
* Utility functions to assist with handling scripted classes. * Utility functions to assist with handling scripted classes.
@ -35,18 +35,6 @@ class ScriptEventDispatcher
return; 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)) if (Std.isOfType(target, IPlayStateScriptedClass))
{ {
var t = cast(target, IPlayStateScriptedClass); var t = cast(target, IPlayStateScriptedClass);
@ -58,6 +46,9 @@ class ScriptEventDispatcher
case ScriptEvent.NOTE_MISS: case ScriptEvent.NOTE_MISS:
t.onNoteMiss(cast event); t.onNoteMiss(cast event);
return; return;
case ScriptEvent.NOTE_GHOST_MISS:
t.onNoteGhostMiss(cast event);
return;
case ScriptEvent.SONG_BEAT_HIT: case ScriptEvent.SONG_BEAT_HIT:
t.onBeatHit(cast event); t.onBeatHit(cast event);
return; return;
@ -70,11 +61,14 @@ class ScriptEventDispatcher
case ScriptEvent.SONG_END: case ScriptEvent.SONG_END:
t.onSongEnd(event); t.onSongEnd(event);
return; return;
case ScriptEvent.SONG_RESET: case ScriptEvent.SONG_RETRY:
t.onSongReset(event); t.onSongRetry(event);
return;
case ScriptEvent.GAME_OVER:
t.onGameOver(event);
return; return;
case ScriptEvent.PAUSE: case ScriptEvent.PAUSE:
t.onPause(event); t.onPause(cast event);
return; return;
case ScriptEvent.RESUME: case ScriptEvent.RESUME:
t.onResume(event); 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<IScriptedClass>, event:ScriptEvent):Void public static function callEventOnAllTargets(targets:Iterator<IScriptedClass>, event:ScriptEvent):Void

View file

@ -1,13 +1,8 @@
package funkin.modding.module; 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.IPlayStateScriptedClass;
import funkin.modding.IScriptedClass.IStateChangingScriptedClass; 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. * 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. * 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 function set_active(value:Bool):Bool
{ {
@ -48,14 +43,11 @@ class Module implements IPlayStateScriptedClass implements IStateChangingScripte
* Called when the module is initialized. * Called when the module is initialized.
* It may not be safe to reference other modules here since they may not be loaded yet. * 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. * NOTE: To make the module start inactive, call `this.active = false` in the constructor.
* If false, the module will be inactive and must be enabled by another script,
* such as a stage or another module.
*/ */
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.moduleId = moduleId;
this.active = active;
this.priority = priority; this.priority = priority;
} }
@ -82,7 +74,7 @@ class Module implements IPlayStateScriptedClass implements IStateChangingScripte
public function onUpdate(event:UpdateScriptEvent) {} public function onUpdate(event:UpdateScriptEvent) {}
public function onPause(event:ScriptEvent) {} public function onPause(event:PauseScriptEvent) {}
public function onResume(event:ScriptEvent) {} public function onResume(event:ScriptEvent) {}
@ -90,16 +82,14 @@ class Module implements IPlayStateScriptedClass implements IStateChangingScripte
public function onSongEnd(event:ScriptEvent) {} public function onSongEnd(event:ScriptEvent) {}
public function onSongReset(event:ScriptEvent) {}
public function onGameOver(event:ScriptEvent) {} public function onGameOver(event:ScriptEvent) {}
public function onGameRetry(event:ScriptEvent) {}
public function onNoteHit(event:NoteScriptEvent) {} public function onNoteHit(event:NoteScriptEvent) {}
public function onNoteMiss(event:NoteScriptEvent) {} public function onNoteMiss(event:NoteScriptEvent) {}
public function onNoteGhostMiss(event:GhostMissNoteScriptEvent) {}
public function onStepHit(event:SongTimeScriptEvent) {} public function onStepHit(event:SongTimeScriptEvent) {}
public function onBeatHit(event:SongTimeScriptEvent) {} public function onBeatHit(event:SongTimeScriptEvent) {}
@ -110,9 +100,19 @@ class Module implements IPlayStateScriptedClass implements IStateChangingScripte
public function onCountdownEnd(event:CountdownScriptEvent) {} public function onCountdownEnd(event:CountdownScriptEvent) {}
public function onSongLoaded(eent:SongLoadScriptEvent) {} public function onSongLoaded(event:SongLoadScriptEvent) {}
public function onStateChangeBegin(event:StateChangeScriptEvent) {} public function onStateChangeBegin(event:StateChangeScriptEvent) {}
public function onStateChangeEnd(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) {}
} }

View file

@ -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<String>;
/**
* Offset the character's position by this amount when playing this animation.
* @default [0, 0]
*/
var offsets:Null<Array<Float>>;
/**
* Whether the animation should loop when it finishes.
* @default false
*/
var looped:Null<Bool>;
/**
* Whether the animation's sprites should be flipped horizontally.
* @default false
*/
var flipX:Null<Bool>;
/**
* Whether the animation's sprites should be flipped vertically.
* @default false
*/
var flipY:Null<Bool>;
/**
* The frame rate of the animation.
* @default 24
*/
var frameRate:Null<Int>;
/**
* 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<Array<Int>>;
}

View file

@ -1,14 +1,13 @@
package funkin.play; package funkin.play;
import funkin.Note.NoteData;
import funkin.audiovis.PolygonSpectogram;
import flixel.FlxObject; import flixel.FlxObject;
import flixel.FlxSprite; import flixel.FlxSprite;
import flixel.addons.effects.FlxTrail; import flixel.addons.effects.FlxTrail;
import flixel.group.FlxGroup.FlxTypedGroup; import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.math.FlxMath; import flixel.math.FlxMath;
import flixel.util.FlxColor; import flixel.util.FlxColor;
import flixel.util.FlxTimer; import funkin.Note.NoteData;
import funkin.audiovis.PolygonSpectogram;
class PicoFight extends MusicBeatState class PicoFight extends MusicBeatState
{ {
@ -37,7 +36,7 @@ class PicoFight extends MusicBeatState
FlxG.sound.playMusic(Paths.inst("blazin")); FlxG.sound.playMusic(Paths.inst("blazin"));
SongLoad.loadFromJson('blazin', "blazin"); SongLoad.loadFromJson('blazin', "blazin");
Conductor.changeBPM(SongLoad.songData.bpm); Conductor.bpm = SongLoad.songData.bpm;
for (dumbassSection in SongLoad.songData.noteMap['hard']) for (dumbassSection in SongLoad.songData.noteMap['hard'])
{ {
@ -184,13 +183,15 @@ class PicoFight extends MusicBeatState
super.update(elapsed); 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.thickness = 10;
funnyWave.waveAmplitude = 300; funnyWave.waveAmplitude = 300;
funnyWave.realtimeVisLenght = 0.1; funnyWave.realtimeVisLenght = 0.1;
@ -198,7 +199,6 @@ class PicoFight extends MusicBeatState
picoHealth += 1; picoHealth += 1;
makeNotes(); makeNotes();
// trace(picoHealth); return true;
super.beatHit();
} }
} }

View file

@ -1,13 +1,12 @@
package funkin.play; package funkin.play;
import funkin.play.Strumline.StrumlineArrow;
import flixel.addons.effects.FlxTrail;
import flixel.addons.transition.FlxTransitionableState;
import flixel.FlxCamera; import flixel.FlxCamera;
import flixel.FlxObject; import flixel.FlxObject;
import flixel.FlxSprite; import flixel.FlxSprite;
import flixel.FlxState; import flixel.FlxState;
import flixel.FlxSubState; import flixel.FlxSubState;
import flixel.addons.effects.FlxTrail;
import flixel.addons.transition.FlxTransitionableState;
import flixel.group.FlxGroup; import flixel.group.FlxGroup;
import flixel.math.FlxMath; import flixel.math.FlxMath;
import flixel.math.FlxPoint; import flixel.math.FlxPoint;
@ -19,19 +18,17 @@ import flixel.ui.FlxBar;
import flixel.util.FlxColor; import flixel.util.FlxColor;
import flixel.util.FlxSort; import flixel.util.FlxSort;
import flixel.util.FlxTimer; 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.Note;
import funkin.play.stage.Stage;
import funkin.play.stage.StageData;
import funkin.play.Strumline.StrumlineStyle;
import funkin.Section.SwagSection; import funkin.Section.SwagSection;
import funkin.SongLoad.SwagSong; 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.PopUpStuff;
import funkin.ui.PreferencesMenu; import funkin.ui.PreferencesMenu;
import funkin.util.Constants; import funkin.util.Constants;
@ -287,7 +284,7 @@ class PlayState extends MusicBeatState implements IHook
currentSong = SongLoad.loadFromJson('tutorial'); currentSong = SongLoad.loadFromJson('tutorial');
Conductor.mapBPMChanges(currentSong); Conductor.mapBPMChanges(currentSong);
Conductor.changeBPM(currentSong.bpm); Conductor.bpm = currentSong.bpm;
switch (currentSong.song.toLowerCase()) switch (currentSong.song.toLowerCase())
{ {
@ -592,7 +589,7 @@ class PlayState extends MusicBeatState implements IHook
* *
* Call this by pressing F5 on a debug build. * 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, // Remove the current stage. If the stage gets deleted while it's still in use,
// it'll probably crash the game or something. // it'll probably crash the game or something.
@ -604,18 +601,7 @@ class PlayState extends MusicBeatState implements IHook
currentStage = null; currentStage = null;
} }
ModuleHandler.clearModuleCache(); super.debug_refreshModules();
// 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());
} }
/** /**
@ -783,7 +769,7 @@ class PlayState extends MusicBeatState implements IHook
{ {
// FlxG.log.add(ChartParser.parse()); // FlxG.log.add(ChartParser.parse());
Conductor.changeBPM(currentSong.bpm); Conductor.bpm = currentSong.bpm;
currentSong.song = currentSong.song; currentSong.song = currentSong.song;
@ -849,7 +835,9 @@ class PlayState extends MusicBeatState implements IHook
oldNote = null; oldNote = null;
var swagNote:Note = new Note(daStrumTime, daNoteData, oldNote); 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); swagNote.scrollFactor.set(0, 0);
var susLength:Float = swagNote.data.sustainLength; var susLength:Float = swagNote.data.sustainLength;
@ -938,6 +926,8 @@ class PlayState extends MusicBeatState implements IHook
if (needsReset) if (needsReset)
{ {
dispatchEvent(new ScriptEvent(ScriptEvent.SONG_RETRY));
resetCamera(); resetCamera();
persistentUpdate = true; persistentUpdate = true;
@ -948,11 +938,10 @@ class PlayState extends MusicBeatState implements IHook
FlxG.sound.music.pause(); FlxG.sound.music.pause();
vocals.pause(); vocals.pause();
var event:ScriptEvent = new ScriptEvent(ScriptEvent.SONG_RESET, false);
FlxG.sound.music.time = 0; FlxG.sound.music.time = 0;
regenNoteData(); // loads the note data from start regenNoteData(); // loads the note data from start
health = 1; health = 1;
songScore = 0;
Countdown.performCountdown(currentStageId.startsWith('school')); Countdown.performCountdown(currentStageId.startsWith('school'));
needsReset = false; needsReset = false;
@ -1004,28 +993,35 @@ class PlayState extends MusicBeatState implements IHook
if ((controls.PAUSE || androidPause) && isInCountdown && mayPauseGame) if ((controls.PAUSE || androidPause) && isInCountdown && mayPauseGame)
{ {
persistentUpdate = false; var event = new PauseScriptEvent(FlxG.random.bool(1 / 1000));
persistentDraw = true;
// There is a 1/1000 change to use a special pause menu. dispatchEvent(event);
// 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();
}
#if discord_rpc if (!event.eventCanceled)
DiscordClient.changePresence(detailsPausedText, currentSong.song + " (" + storyDifficultyText + ")", iconRPC); {
#end 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) if (FlxG.keys.justPressed.SEVEN)
@ -1040,9 +1036,6 @@ class PlayState extends MusicBeatState implements IHook
if (FlxG.keys.justPressed.EIGHT) if (FlxG.keys.justPressed.EIGHT)
FlxG.switchState(new funkin.ui.animDebugShit.DebugBoundingState()); FlxG.switchState(new funkin.ui.animDebugShit.DebugBoundingState());
if (FlxG.keys.justPressed.F5)
debug_refreshStages();
if (FlxG.keys.justPressed.NINE) if (FlxG.keys.justPressed.NINE)
iconP1.swapOldIcon(); 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; return;
if (PlayState.instance.currentStage.getBoyfriend().holdTimer > Conductor.stepCrochet * 4 * 0.001 && !holdArray.contains(true)) 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 if (Math.abs(FlxG.sound.music.time - (Conductor.songPosition - Conductor.offset)) > 20
|| (currentSong.needsVoices && Math.abs(vocals.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)); 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) if (generatedMusic)
{ {
@ -1749,7 +1749,7 @@ class PlayState extends MusicBeatState implements IHook
{ {
if (SongLoad.getSong()[Math.floor(curStep / 16)].changeBPM) 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!'); FlxG.log.add('CHANGED BPM!');
} }
} }
@ -1780,8 +1780,7 @@ class PlayState extends MusicBeatState implements IHook
// Make the characters dance on the beat // Make the characters dance on the beat
danceOnBeat(); danceOnBeat();
// Call any relevant event handlers. return true;
dispatchEvent(new SongTimeScriptEvent(ScriptEvent.SONG_BEAT_HIT, curBeat, curStep));
} }
/** /**
@ -1897,13 +1896,6 @@ class PlayState extends MusicBeatState implements IHook
Countdown.pauseCountdown(); Countdown.pauseCountdown();
} }
var event:ScriptEvent = new ScriptEvent(ScriptEvent.PAUSE, true);
dispatchEvent(event);
if (event.eventCanceled)
return;
super.openSubState(subState); super.openSubState(subState);
} }
@ -1915,6 +1907,13 @@ class PlayState extends MusicBeatState implements IHook
{ {
if (isGamePaused) if (isGamePaused)
{ {
var event:ScriptEvent = new ScriptEvent(ScriptEvent.RESUME, true);
dispatchEvent(event);
if (event.eventCanceled)
return;
if (FlxG.sound.music != null && !startingSong) if (FlxG.sound.music != null && !startingSong)
resyncVocals(); resyncVocals();
@ -1930,13 +1929,6 @@ class PlayState extends MusicBeatState implements IHook
#end #end
} }
var event:ScriptEvent = new ScriptEvent(ScriptEvent.RESUME, true);
dispatchEvent(event);
if (event.eventCanceled)
return;
super.closeSubState(); super.closeSubState();
} }
@ -1957,12 +1949,16 @@ class PlayState extends MusicBeatState implements IHook
override function dispatchEvent(event:ScriptEvent):Void 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); ScriptEventDispatcher.callEvent(currentStage, event);
// TODO: Dispatch event to song script // 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; 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. * This function is called whenever Flixel switches switching to a new FlxState.
*/ */

View file

@ -1,12 +1,8 @@
package funkin.play.stage; 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 flixel.FlxSprite;
import funkin.modding.IScriptedClass.IPlayStateScriptedClass;
import funkin.modding.events.ScriptEvent;
/** /**
* A Bopper is a stage prop which plays a dance animation. * 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. * The bopper plays the dance animation once every `danceEvery` beats.
* Set to 0 to disable idle animation.
*/ */
public var danceEvery:Int = 1; public var danceEvery:Int = 1;
@ -29,16 +26,14 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
public var shouldAlternate:Null<Bool> = null; public var shouldAlternate:Null<Bool> = 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; public var animationOffsets:Map<String, Array<Float>> = new Map<String, Array<Float>>();
override function set_x(value:Float):Float
{
this.x = value + this.xOffset;
return value;
}
/**
* Add a suffix to the `idle` animation (or `danceLeft` and `danceRight` animations)
* that this bopper will play.
*/
public var idleSuffix(default, set):String = ""; public var idleSuffix(default, set):String = "";
function set_idleSuffix(value:String):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<Float> = [0, 0];
override function set_y(value:Float):Float private var animOffsets(default, set):Array<Float> = [0, 0];
function set_animOffsets(value:Array<Float>)
{ {
this.y = value + this.yOffset; if (animOffsets == null)
return value; 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 function update_shouldAlternate():Void
{ {
if (this.animation.getByName('danceLeft') != null) if (hasAnimation('danceLeft'))
{ {
this.shouldAlternate = true; this.shouldAlternate = true;
} }
@ -84,16 +91,16 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
*/ */
public function onBeatHit(event:SongTimeScriptEvent):Void 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. * Called every `danceEvery` beats of the song.
*/ */
function dance():Void public function dance(force:Bool = false):Void
{ {
if (this.animation == null) if (this.animation == null)
{ {
@ -109,20 +116,113 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
{ {
if (hasDanced) if (hasDanced)
{ {
this.animation.play('danceRight$idleSuffix'); playAnimation('danceRight$idleSuffix', true);
} }
else else
{ {
this.animation.play('danceLeft$idleSuffix'); playAnimation('danceLeft$idleSuffix', true);
} }
hasDanced = !hasDanced; hasDanced = !hasDanced;
} }
else 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 onScriptEvent(event:ScriptEvent) {}
public function onCreate(event:ScriptEvent) {} public function onCreate(event:ScriptEvent) {}
@ -131,7 +231,7 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
public function onUpdate(event:UpdateScriptEvent) {} public function onUpdate(event:UpdateScriptEvent) {}
public function onPause(event:ScriptEvent) {} public function onPause(event:PauseScriptEvent) {}
public function onResume(event:ScriptEvent) {} public function onResume(event:ScriptEvent) {}
@ -139,16 +239,14 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
public function onSongEnd(event:ScriptEvent) {} public function onSongEnd(event:ScriptEvent) {}
public function onSongReset(event:ScriptEvent) {}
public function onGameOver(event:ScriptEvent) {} public function onGameOver(event:ScriptEvent) {}
public function onGameRetry(event:ScriptEvent) {}
public function onNoteHit(event:NoteScriptEvent) {} public function onNoteHit(event:NoteScriptEvent) {}
public function onNoteMiss(event:NoteScriptEvent) {} public function onNoteMiss(event:NoteScriptEvent) {}
public function onNoteGhostMiss(event:GhostMissNoteScriptEvent) {}
public function onStepHit(event:SongTimeScriptEvent) {} public function onStepHit(event:SongTimeScriptEvent) {}
public function onCountdownStart(event:CountdownScriptEvent) {} public function onCountdownStart(event:CountdownScriptEvent) {}
@ -158,4 +256,6 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
public function onCountdownEnd(event:CountdownScriptEvent) {} public function onCountdownEnd(event:CountdownScriptEvent) {}
public function onSongLoaded(eent:SongLoadScriptEvent) {} public function onSongLoaded(eent:SongLoadScriptEvent) {}
public function onSongRetry(event:ScriptEvent) {}
} }

View file

@ -1,18 +1,16 @@
package funkin.play.stage; 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.FlxSprite;
import flixel.group.FlxSpriteGroup; import flixel.group.FlxSpriteGroup;
import flixel.math.FlxPoint;
import flixel.util.FlxSort; import flixel.util.FlxSort;
import funkin.modding.IHook; 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.character.Character.CharacterType;
import funkin.play.stage.StageData.StageDataParser; import funkin.play.stage.StageData.StageDataParser;
import funkin.util.SortUtil; import funkin.util.SortUtil;
import funkin.util.assets.FlxAnimationUtil;
/** /**
* A Stage is a group of objects rendered in the PlayState. * 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) for (propAnim in dataProp.animations)
{ {
propSprite.animation.add(propAnim.name, propAnim.frameIndices); 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" 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, cast(propSprite, Bopper).setAnimationOffsets(propAnim.name, propAnim.offsets[0], propAnim.offsets[1]);
propAnim.flipY);
}
else
{
propSprite.animation.addByIndices(propAnim.name, propAnim.prefix, propAnim.frameIndices, "", propAnim.frameRate, propAnim.loop,
propAnim.flipX, propAnim.flipY);
} }
} }
} }
@ -377,7 +375,7 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte
public function onScriptEvent(event:ScriptEvent) {} public function onScriptEvent(event:ScriptEvent) {}
public function onPause(event:ScriptEvent) {} public function onPause(event:PauseScriptEvent) {}
public function onResume(event:ScriptEvent) {} public function onResume(event:ScriptEvent) {}
@ -385,29 +383,23 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte
public function onSongEnd(event:ScriptEvent) {} public function onSongEnd(event:ScriptEvent) {}
/**
* Resets the stage and its props.
*/
public function onSongReset(event:ScriptEvent) {}
public function onGameOver(event:ScriptEvent) {} public function onGameOver(event:ScriptEvent) {}
public function onGameRetry(event:ScriptEvent) {}
public function onCountdownStart(event:CountdownScriptEvent) {} public function onCountdownStart(event:CountdownScriptEvent) {}
public function onCountdownStep(event:CountdownScriptEvent) {} public function onCountdownStep(event:CountdownScriptEvent) {}
public function onCountdownEnd(event:CountdownScriptEvent) {} public function onCountdownEnd(event:CountdownScriptEvent) {}
/**
* A function that should get called every frame.
*/
public function onUpdate(event:UpdateScriptEvent) {} public function onUpdate(event:UpdateScriptEvent) {}
public function onNoteHit(event:NoteScriptEvent) {} public function onNoteHit(event:NoteScriptEvent) {}
public function onNoteMiss(event:NoteScriptEvent) {} public function onNoteMiss(event:NoteScriptEvent) {}
public function onNoteGhostMiss(event:GhostMissNoteScriptEvent) {}
public function onSongLoaded(eent:SongLoadScriptEvent) {} public function onSongLoaded(eent:SongLoadScriptEvent) {}
public function onSongRetry(event:ScriptEvent) {}
} }

View file

@ -1,9 +1,10 @@
package funkin.play.stage; package funkin.play.stage;
import openfl.Assets; import flixel.util.typeLimit.OneOfTwo;
import funkin.util.VersionUtil;
import funkin.util.assets.DataAssets; import funkin.util.assets.DataAssets;
import haxe.Json; import haxe.Json;
import flixel.util.typeLimit.OneOfTwo; import openfl.Assets;
using StringTools; using StringTools;
@ -17,7 +18,12 @@ class StageDataParser
* Handle breaking changes by incrementing this value * Handle breaking changes by incrementing this value
* and adding migration to the `migrateStageData()` function. * 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<String, Stage> = new Map<String, Stage>(); static final stageCache:Map<String, Stage> = new Map<String, Stage>();
@ -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<Float> = [0, 0];
static final DEFAULT_SCROLL:Array<Float> = [0, 0];
static final DEFAULT_FRAMEINDICES:Array<Int> = [];
static final DEFAULT_ANIMTYPE:String = "sparrow"; 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<Float> = [0, 0];
static final DEFAULT_POSITION:Array<Float> = [0, 0];
static final DEFAULT_SCALE:Float = 1.0;
static final DEFAULT_SCROLL:Array<Float> = [0, 0];
static final DEFAULT_ZINDEX:Int = 0;
static final DEFAULT_CHARACTER_DATA:StageDataCharacter = { static final DEFAULT_CHARACTER_DATA:StageDataCharacter = {
zIndex: DEFAULT_ZINDEX, zIndex: DEFAULT_ZINDEX,
position: DEFAULT_POSITION, position: DEFAULT_POSITION,
cameraOffsets: DEFAULT_OFFSETS,
} }
/** /**
@ -194,12 +201,18 @@ class StageDataParser
return null; return null;
} }
if (input.version != STAGE_DATA_VERSION) if (input.version == null)
{ {
trace('[STAGEDATA] ERROR: Could not load stage data for "$id": missing version'); trace('[STAGEDATA] ERROR: Could not load stage data for "$id": missing version');
return null; 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) if (input.name == null)
{ {
trace('[STAGEDATA] WARN: Stage data for "$id" missing name'); trace('[STAGEDATA] WARN: Stage data for "$id" missing name');
@ -211,10 +224,9 @@ class StageDataParser
input.cameraZoom = DEFAULT_CAMERAZOOM; 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'); input.props = [];
return null;
} }
for (inputProp in input.props) for (inputProp in input.props)
@ -296,14 +308,14 @@ class StageDataParser
inputAnimation.frameRate = 24; 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) if (inputAnimation.flipX == null)
@ -347,6 +359,10 @@ class StageDataParser
{ {
inputCharacter.position = [0, 0]; inputCharacter.position = [0, 0];
} }
if (inputCharacter.cameraOffsets == null || inputCharacter.cameraOffsets.length != 2)
{
inputCharacter.cameraOffsets = [0, 0];
}
} }
// All good! // All good!
@ -356,8 +372,12 @@ class StageDataParser
typedef StageData = 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 version:String;
var name:String; var name:String;
var cameraZoom:Null<Float>; var cameraZoom:Null<Float>;
var props:Array<StageDataProp>; var props:Array<StageDataProp>;
@ -432,7 +452,7 @@ typedef StageDataProp =
* An optional array of animations which the prop can play. * An optional array of animations which the prop can play.
* @default Prop has no animations. * @default Prop has no animations.
*/ */
var animations:Array<StageDataPropAnimation>; var animations:Array<AnimationData>;
/** /**
* If animations are used, this is the name of the animation to play first. * If animations are used, this is the name of the animation to play first.
@ -448,52 +468,6 @@ typedef StageDataProp =
var animType:String; 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<Int>;
/**
* The speed of the animation in frames per second.
* @default 24
*/
var frameRate:Null<Int>;
/**
* Whether the animation should loop.
* @default false
*/
var loop:Null<Bool>;
/**
* Whether to flip the sprite horizontally while animating.
* @default false
*/
var flipX:Null<Bool>;
/**
* Whether to flip the sprite vertically while animating.
* @default false
*/
var flipY:Null<Bool>;
};
typedef StageDataCharacter = typedef StageDataCharacter =
{ {
/** /**
@ -505,5 +479,12 @@ typedef StageDataCharacter =
/** /**
* The position to render the character at. * The position to render the character at.
*/ position:Array<Float> */
position:Array<Float>,
/**
* The camera offsets to apply when focusing on the character on this stage.
* @default [0, 0]
*/
cameraOffsets:Array<Float>,
}; };

View file

@ -1,21 +1,13 @@
package funkin.ui; package funkin.ui;
import flixel.FlxSprite; import flixel.FlxSprite;
import flixel.group.FlxSpriteGroup;
import flixel.graphics.frames.FlxAtlasFrames; import flixel.graphics.frames.FlxAtlasFrames;
import flixel.group.FlxSpriteGroup;
import flixel.util.FlxStringUtil; 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<AtlasChar> class AtlasText extends FlxTypedSpriteGroup<AtlasChar>
{ {
@ -41,7 +33,7 @@ class AtlasText extends FlxTypedSpriteGroup<AtlasChar>
inline function get_maxHeight() inline function get_maxHeight()
return font.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)) if (!fonts.exists(fontName))
fonts[fontName] = new AtlasFontData(fontName); fonts[fontName] = new AtlasFontData(fontName);
@ -246,7 +238,14 @@ private class AtlasFontData
public function new(name:AtlasFont) 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.destroyOnNoUse = false;
atlas.parent.persist = true; atlas.parent.persist = true;
@ -276,8 +275,8 @@ enum Case
Lower; Lower;
} }
enum AtlasFont enum abstract AtlasFont(String) from String to String
{ {
Default; var DEFAULT = "default";
Bold; var BOLD = "bold";
} }

View file

@ -1,6 +1,5 @@
package funkin.ui; package funkin.ui;
import funkin.Controls;
import flixel.FlxCamera; import flixel.FlxCamera;
import flixel.FlxObject; import flixel.FlxObject;
import flixel.FlxSprite; import flixel.FlxSprite;
@ -8,6 +7,7 @@ import flixel.group.FlxGroup;
import flixel.input.actions.FlxActionInput; import flixel.input.actions.FlxActionInput;
import flixel.input.gamepad.FlxGamepadInputID; import flixel.input.gamepad.FlxGamepadInputID;
import flixel.input.keyboard.FlxKey; import flixel.input.keyboard.FlxKey;
import funkin.Controls;
import funkin.ui.AtlasText; import funkin.ui.AtlasText;
import funkin.ui.MenuList; import funkin.ui.MenuList;
import funkin.ui.TextMenuList; import funkin.ui.TextMenuList;
@ -66,11 +66,11 @@ class ControlsMenu extends funkin.ui.OptionsState.Page
var item; 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.x = FlxG.width / 2 - item.width - 30;
item.y = (devicesBg.height - item.height) / 2; 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.x = FlxG.width / 2 + 30;
item.y = (devicesBg.height - item.height) / 2; 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) if (currentHeader != "UI_" && name.indexOf("UI_") == 0)
{ {
currentHeader = "UI_"; currentHeader = "UI_";
headers.add(new BoldText(0, y, "UI")).screenCenter(X); headers.add(new AtlasText(0, y, "UI", AtlasFont.BOLD)).screenCenter(X);
y += spacer; y += spacer;
} }
else if (currentHeader != "NOTE_" && name.indexOf("NOTE_") == 0) else if (currentHeader != "NOTE_" && name.indexOf("NOTE_") == 0)
{ {
currentHeader = "NOTE_"; currentHeader = "NOTE_";
headers.add(new BoldText(0, y, "NOTES")).screenCenter(X); headers.add(new AtlasText(0, y, "NOTES", AtlasFont.BOLD)).screenCenter(X);
y += spacer; y += spacer;
} }
if (currentHeader != null && name.indexOf(currentHeader) == 0) if (currentHeader != null && name.indexOf(currentHeader) == 0)
name = name.substr(currentHeader.length); 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; label.alpha = 0.6;
for (i in 0...COLUMNS) for (i in 0...COLUMNS)
createItem(label.x + 400 + i * 300, y, control, i); createItem(label.x + 400 + i * 300, y, control, i);
@ -317,7 +317,7 @@ class InputItem extends TextMenuItem
this.index = index; this.index = index;
this.input = getInput(); this.input = getInput();
super(x, y, getLabel(input), Default, callback); super(x, y, getLabel(input), DEFAULT, callback);
} }
public function updateDevice(device:Device) public function updateDevice(device:Device)

View file

@ -5,10 +5,9 @@ import flixel.FlxSubState;
import flixel.addons.transition.FlxTransitionableState; import flixel.addons.transition.FlxTransitionableState;
import flixel.group.FlxGroup; import flixel.group.FlxGroup;
import flixel.util.FlxSignal; 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 class OptionsState extends MusicBeatState
{ {
var pages = new Map<PageName, Page>(); var pages = new Map<PageName, Page>();
@ -31,17 +30,12 @@ class OptionsState extends MusicBeatState
var options = addPage(Options, new OptionsMenu(false)); var options = addPage(Options, new OptionsMenu(false));
var preferences = addPage(Preferences, new PreferencesMenu()); var preferences = addPage(Preferences, new PreferencesMenu());
var controls = addPage(Controls, new ControlsMenu()); var controls = addPage(Controls, new ControlsMenu());
// var colors = addPage(Colors, new ColorsMenu());
var mods = addPage(Mods, new ModMenu());
if (options.hasMultipleOptions()) if (options.hasMultipleOptions())
{ {
options.onExit.add(exitToMainMenu); options.onExit.add(exitToMainMenu);
controls.onExit.add(switchPage.bind(Options)); controls.onExit.add(switchPage.bind(Options));
// colors.onExit.add(switchPage.bind(Options));
preferences.onExit.add(switchPage.bind(Options)); preferences.onExit.add(switchPage.bind(Options));
mods.onExit.add(switchPage.bind(Options));
} }
else else
{ {
@ -67,12 +61,18 @@ class OptionsState extends MusicBeatState
function setPage(name:PageName) function setPage(name:PageName)
{ {
if (pages.exists(currentName)) if (pages.exists(currentName))
{
currentPage.exists = false; currentPage.exists = false;
currentPage.visible = false;
}
currentName = name; currentName = name;
if (pages.exists(currentName)) if (pages.exists(currentName))
{
currentPage.exists = true; currentPage.exists = true;
currentPage.visible = true;
}
} }
override function finishTransIn() override function finishTransIn()
@ -91,7 +91,7 @@ class OptionsState extends MusicBeatState
function exitToMainMenu() function exitToMainMenu()
{ {
currentPage.enabled = false; currentPage.enabled = false;
// Todo animate? // TODO: Animate this transition?
FlxG.switchState(new MainMenuState()); FlxG.switchState(new MainMenuState());
} }
} }
@ -172,30 +172,29 @@ class OptionsMenu extends Page
super(); super();
add(items = new TextMenuList()); add(items = new TextMenuList());
createItem(t("PREFERENCES"), function() switchPage(Preferences)); createItem("PREFERENCES", function() switchPage(Preferences));
createItem(t("CONTROLS"), function() switchPage(Controls)); createItem("CONTROLS", function() switchPage(Controls));
// createItem(t("COLORS"), function() switchPage(Colors)); // createItem("COLORS", function() switchPage(Colors));
createItem(t("MODS"), function() switchPage(Mods));
#if CAN_OPEN_LINKS #if CAN_OPEN_LINKS
if (showDonate) if (showDonate)
{ {
var hasPopupBlocker = #if web true #else false #end; var hasPopupBlocker = #if web true #else false #end;
createItem(t("DONATE"), selectDonate, hasPopupBlocker); createItem("DONATE", selectDonate, hasPopupBlocker);
} }
#end #end
#if newgrounds #if newgrounds
if (NGio.isLoggedIn) if (NGio.isLoggedIn)
createItem(t("LOGOUT"), selectLogout); createItem("LOGOUT", selectLogout);
else else
createItem(t("LOGIN"), selectLogin); createItem("LOGIN", selectLogin);
#end #end
createItem(t("EXIT"), exit); createItem("EXIT", exit);
} }
function createItem(name:String, callback:Void->Void, fireInstantly = false) 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.fireInstantly = fireInstantly;
item.screenCenter(X); item.screenCenter(X);
return item; return item;
@ -219,11 +218,7 @@ class OptionsMenu extends Page
#if CAN_OPEN_LINKS #if CAN_OPEN_LINKS
function selectDonate() function selectDonate()
{ {
#if linux WindowUtil.openURL(Constants.URL_ITCH);
Sys.command('/usr/bin/xdg-open', ["https://ninja-muffin24.itch.io/funkin", "&"]);
#else
FlxG.openURL('https://ninja-muffin24.itch.io/funkin');
#end
} }
#end #end

View file

@ -4,8 +4,8 @@ import flixel.FlxCamera;
import flixel.FlxObject; import flixel.FlxObject;
import flixel.FlxSprite; import flixel.FlxSprite;
import funkin.ui.AtlasText.AtlasFont; import funkin.ui.AtlasText.AtlasFont;
import funkin.ui.TextMenuList.TextMenuItem;
import funkin.ui.OptionsState.Page; import funkin.ui.OptionsState.Page;
import funkin.ui.TextMenuList.TextMenuItem;
class PreferencesMenu extends Page class PreferencesMenu extends Page
{ {
@ -84,7 +84,7 @@ class PreferencesMenu extends Page
private function createPrefItem(prefName:String, prefString:String, prefValue:Dynamic):Void 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); 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) if (preferences.get(prefString) == null)
{ {
preferences.set(prefString, prefValue); // Set the value to default.
trace('set preference!'); preferences.set(prefString, defaultValue);
trace('Set preference to default: ${prefString} = ${defaultValue}');
} }
else else
{ {
trace('found preference: ' + preferences.get(prefString)); trace('Found preference: ${prefString} = ${preferences.get(prefString)}');
} }
} }
} }

View file

@ -1,12 +1,12 @@
package funkin.ui; package funkin.ui;
import flixel.FlxSprite; import flixel.FlxSprite;
import flixel.graphics.frames.FlxAtlasFrames; import funkin.ui.AtlasText.AtlasFont;
import flixel.text.FlxText;
import flixel.util.FlxColor;
import funkin.ui.AtlasText;
import funkin.ui.MenuList; import funkin.ui.MenuList;
/**
* Opens a yes/no dialog box as a substate over the current state.
*/
class Prompt extends flixel.FlxSubState class Prompt extends flixel.FlxSubState
{ {
inline static var MARGIN = 100; inline static var MARGIN = 100;
@ -26,7 +26,7 @@ class Prompt extends flixel.FlxSubState
buttons = new TextMenuList(Horizontal); buttons = new TextMenuList(Horizontal);
field = new BoldText(text); field = new AtlasText(text, AtlasFont.BOLD);
field.scrollFactor.set(0, 0); field.scrollFactor.set(0, 0);
} }

View file

@ -10,7 +10,7 @@ class TextMenuList extends MenuTypedList<TextMenuItem>
super(navControls, wrapMode); 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); var item = new TextMenuItem(x, y, name, font, callback);
item.fireInstantly = fireInstantly; item.fireInstantly = fireInstantly;
@ -20,7 +20,7 @@ class TextMenuList extends MenuTypedList<TextMenuItem>
class TextMenuItem extends TextTypedMenuItem<AtlasText> class TextMenuItem extends TextTypedMenuItem<AtlasText>
{ {
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); super(x, y, new AtlasText(0, 0, name, font), name, callback);
setEmptyBackground(); setEmptyBackground();

View file

@ -1,7 +1,7 @@
package funkin.util; package funkin.util;
import lime.app.Application;
import flixel.util.FlxColor; import flixel.util.FlxColor;
import lime.app.Application;
class Constants class Constants
{ {
@ -18,6 +18,8 @@ class Constants
public static final VERSION_SUFFIX = ' PROTOTYPE'; public static final VERSION_SUFFIX = ' PROTOTYPE';
public static var VERSION(get, null):String; public static var VERSION(get, null):String;
public static final FREAKY_MENU_BPM = 102;
#if debug #if debug
public static final GIT_HASH = funkin.util.macro.GitCommit.getGitCommitHash(); 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; return 'v${Application.current.meta.get('version')}' + VERSION_SUFFIX;
} }
#end #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";
} }

View file

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

View file

@ -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
}
}

View file

@ -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<AnimationData>)
{
for (anim in animations)
{
addAtlasAnimation(target, anim);
}
}
}