Merge pull request from ninjamuffin99/feature/scripted-modules

Polymod - Scripted Modules
This commit is contained in:
Cameron Taylor 2022-04-18 20:30:34 -04:00 committed by GitHub
commit d84207c1b4
62 changed files with 3693 additions and 2052 deletions

5
.gitignore vendored
View file

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

17
.vscode/launch.json vendored
View file

@ -12,6 +12,21 @@
"name": "Haxe Eval",
"type": "haxe-eval",
"request": "launch"
}
},
{
"name": "HTML5 Debug",
"type": "chrome",
"request": "launch",
"url": "http://127.0.0.1:3001",
"sourceMaps": true,
"webRoot": "${workspaceFolder}",
"preLaunchTask": "debug: html"
},
{
"name": "Mac (Debug)",
"type": "hxcpp",
"request": "launch",
"program": "${workspaceRoot}/export/debug/macos/bin/Funkin.app/Contents/MacOS/Funkin",
"preLaunchTask": "debug: mac"
]
}

View file

@ -96,7 +96,13 @@
<!-- <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="CHANGELOG.md" rename='changelog.txt' />
@ -122,7 +128,7 @@
<!--haxelib name="newgrounds" unless="switch"/> -->
<haxelib name="faxe" if='switch' />
<haxelib name="polymod" />
<haxelib name="firetongue" />
<haxelib name="thx.semver" />
<!-- <haxelib name="colyseus"/> -->
<!-- <haxelib name="colyseus-websocket" /> -->
@ -191,7 +197,7 @@
<haxeflag name="--macro" value="include('funkin')" />
<!-- Necessary to provide stack traces for HScript. -->
<haxedef name="hscriptPos" />
<haxedef name="hscriptPos" />
<haxedef name="HXCPP_CHECK_POINTER" />
<haxedef name="HXCPP_STACK_LINE" />

View file

@ -1,2 +1,4 @@
./tricky
./enaSkin
# Exclude any experimental mods that my have been added.
/*
!introMod
!testing123

View file

@ -0,0 +1,6 @@
# introMod
This intro mod demonstrates two simple things:
1. You can replace any game asset simply by placing a modded asset in the right spot.
2. You can append to text files simply by placing a text file in the right spot, but under the `_append` directory.

View file

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

View file

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

View file

@ -1,9 +1,9 @@
package;
import funkin.InitState;
import funkin.MemoryCounter;
import flixel.FlxGame;
import flixel.FlxState;
import funkin.InitState;
import funkin.MemoryCounter;
import openfl.Lib;
import openfl.display.FPS;
import openfl.display.Sprite;
@ -37,17 +37,9 @@ class Main extends Sprite
{
super();
// TODO: Ideally this should change to utilize a user interface.
// 1. Call PolymodHandler.getAllMods(). This gives you an array of ModMetadata items,
// each of which contains information about the mod including an icon.
// 2. Provide an interface to enable, disable, and reorder enabled mods.
// A design similar to that of Minecraft resource packs would be intuitive.
// 3. The interface should save (to the save file) and output an ordered array of mod IDs.
// 4. Replace the call to PolymodHandler.loadAllMods() with a call to PolymodHandler.loadModsById(ids:Array<String>).
// TODO: Replace this with loadEnabledMods().
funkin.modding.PolymodHandler.loadAllMods();
funkin.i18n.FireTongueHandler.init();
if (stage != null)
{
init();

View file

@ -1,5 +1,6 @@
package funkin;
import funkin.util.Constants;
import funkin.Note.NoteData;
import funkin.SongLoad.SwagSong;
import funkin.Section.SwagSection;
@ -128,7 +129,7 @@ class Character extends FlxSprite
playAnim('danceRight');
setGraphicSize(Std.int(width * PlayState.daPixelZoom));
setGraphicSize(Std.int(width * Constants.PIXEL_ART_SCALE));
updateHitbox();
antialiasing = false;

View file

@ -15,9 +15,31 @@ typedef BPMChangeEvent =
class Conductor
{
/**
* Beats per minute of the song.
*/
public static var bpm:Float = 100;
public static var crochet:Float = ((60 / bpm) * 1000); // beats in milliseconds
public static var stepCrochet:Float = crochet / 4; // steps in milliseconds
/**
* Duration of a beat in millisecond.
*/
public static var crochet(get, null):Float;
static function get_crochet():Float
{
return ((60 / bpm) * 1000);
}
/**
* Duration of a step in milliseconds.
*/
public static var stepCrochet(get, null):Float;
static function get_stepCrochet():Float
{
return crochet / 4;
}
public static var songPosition:Float;
public static var lastSongPos:Float;
public static var offset:Float = 0;
@ -52,12 +74,4 @@ class Conductor
}
trace("new BPM map BUDDY " + bpmChangeMap);
}
public static function changeBPM(newBpm:Float)
{
bpm = newBpm;
crochet = ((60 / bpm) * 1000);
stepCrochet = crochet / 4;
}
}

View file

@ -1,5 +1,6 @@
package funkin;
import funkin.util.Constants;
import flixel.FlxSprite;
import flixel.addons.text.FlxTypeText;
import flixel.graphics.frames.FlxAtlasFrames;
@ -38,7 +39,7 @@ class DialogueBox extends FlxSpriteGroup
{
super();
switch (PlayState.SONG.song.toLowerCase())
switch (PlayState.currentSong.song.toLowerCase())
{
case 'senpai':
FlxG.sound.playMusic(Paths.music('Lunchbox'), 0);
@ -63,7 +64,7 @@ class DialogueBox extends FlxSpriteGroup
portraitLeft = new FlxSprite(-20, 40);
portraitLeft.frames = Paths.getSparrowAtlas('weeb/senpaiPortrait');
portraitLeft.animation.addByPrefix('enter', 'Senpai Portrait Enter', 24, false);
portraitLeft.setGraphicSize(Std.int(portraitLeft.width * PlayState.daPixelZoom * 0.9));
portraitLeft.setGraphicSize(Std.int(portraitLeft.width * Constants.PIXEL_ART_SCALE * 0.9));
portraitLeft.updateHitbox();
portraitLeft.scrollFactor.set();
add(portraitLeft);
@ -72,7 +73,7 @@ class DialogueBox extends FlxSpriteGroup
portraitRight = new FlxSprite(0, 40);
portraitRight.frames = Paths.getSparrowAtlas('weeb/bfPortrait');
portraitRight.animation.addByPrefix('enter', 'Boyfriend portrait enter', 24, false);
portraitRight.setGraphicSize(Std.int(portraitRight.width * PlayState.daPixelZoom * 0.9));
portraitRight.setGraphicSize(Std.int(portraitRight.width * Constants.PIXEL_ART_SCALE * 0.9));
portraitRight.updateHitbox();
portraitRight.scrollFactor.set();
add(portraitRight);
@ -81,7 +82,7 @@ class DialogueBox extends FlxSpriteGroup
box = new FlxSprite(-20, 45);
var hasDialog = false;
switch (PlayState.SONG.song.toLowerCase())
switch (PlayState.currentSong.song.toLowerCase())
{
case 'senpai':
hasDialog = true;
@ -113,7 +114,7 @@ class DialogueBox extends FlxSpriteGroup
return;
box.animation.play('normalOpen');
box.setGraphicSize(Std.int(box.width * PlayState.daPixelZoom * 0.9));
box.setGraphicSize(Std.int(box.width * Constants.PIXEL_ART_SCALE * 0.9));
box.updateHitbox();
add(box);
@ -121,7 +122,7 @@ class DialogueBox extends FlxSpriteGroup
portraitLeft.screenCenter(X);
handSelect = new FlxSprite(1042, 590).loadGraphic(Paths.image('weeb/pixelUI/hand_textbox'));
handSelect.setGraphicSize(Std.int(handSelect.width * PlayState.daPixelZoom * 0.9));
handSelect.setGraphicSize(Std.int(handSelect.width * Constants.PIXEL_ART_SCALE * 0.9));
handSelect.updateHitbox();
handSelect.visible = false;
add(handSelect);
@ -154,9 +155,9 @@ class DialogueBox extends FlxSpriteGroup
override function update(elapsed:Float)
{
// HARD CODING CUZ IM STUPDI
if (PlayState.SONG.song.toLowerCase() == 'roses')
if (PlayState.currentSong.song.toLowerCase() == 'roses')
portraitLeft.visible = false;
if (PlayState.SONG.song.toLowerCase() == 'thorns')
if (PlayState.currentSong.song.toLowerCase() == 'thorns')
{
portraitLeft.color = FlxColor.BLACK;
swagDialogue.color = FlxColor.WHITE;
@ -192,7 +193,7 @@ class DialogueBox extends FlxSpriteGroup
{
isEnding = true;
if (PlayState.SONG.song.toLowerCase() == 'senpai' || PlayState.SONG.song.toLowerCase() == 'thorns')
if (PlayState.currentSong.song.toLowerCase() == 'senpai' || PlayState.currentSong.song.toLowerCase() == 'thorns')
FlxG.sound.music.fadeOut(2.2, 0);
new FlxTimer().start(0.2, function(tmr:FlxTimer)

View file

@ -1,6 +1,5 @@
package funkin;
import funkin.Controls.Control;
import flash.text.TextField;
import flixel.FlxCamera;
import flixel.FlxGame;
@ -21,16 +20,17 @@ import flixel.tweens.FlxTween;
import flixel.util.FlxColor;
import flixel.util.FlxSpriteUtil;
import flixel.util.FlxTimer;
import funkin.Controls.Control;
import funkin.freeplayStuff.BGScrollingText;
import funkin.freeplayStuff.DJBoyfriend;
import funkin.freeplayStuff.FreeplayScore;
import funkin.freeplayStuff.SongMenuItem;
import lime.app.Future;
import lime.utils.Assets;
import funkin.play.PlayState;
import funkin.shaderslmfao.AngleMask;
import funkin.shaderslmfao.PureColor;
import funkin.shaderslmfao.StrokeShader;
import funkin.play.PlayState;
import lime.app.Future;
import lime.utils.Assets;
using StringTools;
@ -238,9 +238,6 @@ class FreeplayState extends MusicBeatSubstate
add(new DifficultySelector(20, grpDifficulties.y - 10, false, controls));
add(new DifficultySelector(325, grpDifficulties.y - 10, true, controls));
var animShit:ComboCounter = new ComboCounter(100, 300, 1000000);
// add(animShit);
new FlxTimer().start(1 / 24, function(handShit)
{
fnfFreeplay.visible = true;
@ -388,7 +385,7 @@ class FreeplayState extends MusicBeatSubstate
{
if (FlxG.sound.music.volume < 0.7)
{
FlxG.sound.music.volume += 0.5 * FlxG.elapsed;
FlxG.sound.music.volume += 0.5 * elapsed;
}
}
@ -435,7 +432,7 @@ class FreeplayState extends MusicBeatSubstate
if (touchTimer >= 1.5)
accepted = true;
touchTimer += FlxG.elapsed;
touchTimer += elapsed;
var touch:FlxTouch = FlxG.touches.getFirst();
velTouch = Math.abs((touch.screenY - dyTouch)) / 50;
@ -472,24 +469,6 @@ class FreeplayState extends MusicBeatSubstate
else
{
touchTimer = 0;
/* if (velTouch >= 0)
{
trace(velTouch);
velTouch -= FlxG.elapsed;
veloctiyLoopShit += velTouch;
trace("VEL LOOP: " + veloctiyLoopShit);
if (veloctiyLoopShit >= 30)
{
veloctiyLoopShit = 0;
changeSelection(1);
}
// trace(velTouch);
}*/
}
}
@ -519,6 +498,9 @@ class FreeplayState extends MusicBeatSubstate
if (controls.BACK)
{
FlxG.sound.play(Paths.sound('cancelMenu'));
FlxTransitionableState.skipNextTransIn = true;
FlxTransitionableState.skipNextTransOut = true;
FlxG.switchState(new MainMenuState());
}
@ -538,7 +520,7 @@ class FreeplayState extends MusicBeatSubstate
curDifficulty = 1;
}*/
PlayState.SONG = SongLoad.loadFromJson(poop, songs[curSelected].songName.toLowerCase());
PlayState.currentSong = SongLoad.loadFromJson(poop, songs[curSelected].songName.toLowerCase());
PlayState.isStoryMode = false;
PlayState.storyDifficulty = curDifficulty;
// SongLoad.curDiff = Highscore.formatSong()

View file

@ -1,14 +1,11 @@
package funkin;
import flixel.FlxObject;
import flixel.FlxSubState;
import flixel.math.FlxPoint;
import flixel.system.FlxSound;
import flixel.util.FlxColor;
import flixel.util.FlxTimer;
import haxe.display.Display;
import funkin.ui.PreferencesMenu;
import funkin.play.PlayState;
import funkin.ui.PreferencesMenu;
class GameOverSubstate extends MusicBeatSubstate
{
@ -20,12 +17,12 @@ class GameOverSubstate extends MusicBeatSubstate
var gameOverMusic:FlxSound;
public function new(x:Float, y:Float)
public function new()
{
gameOverMusic = new FlxSound();
FlxG.sound.list.add(gameOverMusic);
var daStage = PlayState.curStageId;
var daStage = PlayState.instance.currentStageId;
var daBf:String = '';
switch (daStage)
{
@ -36,7 +33,7 @@ class GameOverSubstate extends MusicBeatSubstate
daBf = 'bf';
}
var daSong = PlayState.SONG.song.toLowerCase();
var daSong = PlayState.currentSong.song.toLowerCase();
switch (daSong)
{
@ -48,16 +45,18 @@ class GameOverSubstate extends MusicBeatSubstate
Conductor.songPosition = 0;
bf = new Boyfriend(x, y, daBf);
var bfXPos = PlayState.instance.currentStage.getBoyfriend().getScreenPosition().x;
var bfYPos = PlayState.instance.currentStage.getBoyfriend().getScreenPosition().y;
bf = new Boyfriend(bfXPos, bfYPos, daBf);
add(bf);
camFollow = new FlxObject(bf.getGraphicMidpoint().x, bf.getGraphicMidpoint().y, 1, 1);
add(camFollow);
FlxG.sound.play(Paths.sound('fnf_loss_sfx' + stageSuffix));
// Conductor.changeBPM(100);
// Conductor.bpm = 100;
switch (PlayState.SONG.player1)
switch (PlayState.currentSong.player1)
{
case 'pico':
stageSuffix = 'Pico';

View file

@ -37,7 +37,7 @@ class HealthIcon extends FlxSprite
if (isOldIcon)
changeIcon('bf-old');
else
changeIcon(PlayState.SONG.player1);
changeIcon(PlayState.currentSong.player1);
}
var pixelArrayFunny:Array<String> = CoolUtil.coolTextFile(Paths.file('images/icons/pixelIcons.txt'));

View file

@ -8,9 +8,12 @@ import flixel.math.FlxPoint;
import flixel.math.FlxRect;
import flixel.util.FlxColor;
import funkin.charting.ChartingState;
import funkin.charting.ChartingState;
import funkin.modding.module.ModuleHandler;
import funkin.play.PicoFight;
import funkin.play.PlayState;
import funkin.play.stage.StageData;
import funkin.play.stage.StageData;
import funkin.ui.PreferencesMenu;
import funkin.ui.animDebugShit.DebugBoundingState;
import funkin.ui.stageBuildShit.StageBuilderState;
@ -123,6 +126,8 @@ class InitState extends FlxTransitionableState
StageDataParser.loadStageCache();
ModuleHandler.loadModuleCache();
#if song
var song = getSong();
@ -187,7 +192,7 @@ class InitState extends FlxTransitionableState
{
var dif = getDif();
PlayState.SONG = SongLoad.loadFromJson(song, song);
PlayState.currentSong = SongLoad.loadFromJson(song, song);
PlayState.isStoryMode = isStoryMode;
PlayState.storyDifficulty = dif;
SongLoad.curDiff = switch (dif)

View file

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

View file

@ -2,9 +2,9 @@ package funkin;
import flixel.FlxSprite;
import flixel.FlxState;
import flixel.graphics.frames.FlxAtlasFrames;
import flixel.math.FlxMath;
import flixel.util.FlxTimer;
import funkin.play.PlayState;
import haxe.io.Path;
import lime.app.Future;
import lime.app.Promise;
@ -12,7 +12,6 @@ import lime.utils.AssetLibrary;
import lime.utils.AssetManifest;
import lime.utils.Assets as LimeAssets;
import openfl.utils.Assets;
import funkin.play.PlayState;
class LoadingState extends MusicBeatState
{
@ -21,7 +20,6 @@ class LoadingState extends MusicBeatState
var target:FlxState;
var stopMusic = false;
var callbacks:MultiCallback;
var danceLeft = false;
var loadBar:FlxSprite;
@ -57,9 +55,9 @@ class LoadingState extends MusicBeatState
callbacks = new MultiCallback(onLoad);
var introComplete = callbacks.add("introComplete");
checkLoadSong(getSongPath());
if (PlayState.SONG.needsVoices)
if (PlayState.currentSong.needsVoices)
{
var files = PlayState.SONG.voiceList;
var files = PlayState.currentSong.voiceList;
if (files == null)
files = [""]; // loads with no file name assumption, to load "Voices.ogg" or whatev normally
@ -117,17 +115,15 @@ class LoadingState extends MusicBeatState
}
}
override function beatHit()
override function beatHit():Bool
{
super.beatHit();
// super.beatHit() returns false if a module cancelled the event.
if (!super.beatHit())
return false;
// logo.animation.play('bump');
danceLeft = !danceLeft;
/*
if (danceLeft)
gfDance.animation.play('danceRight');
else
gfDance.animation.play('danceLeft'); */
return true;
}
var targetShit:Float = 0;
@ -173,12 +169,12 @@ class LoadingState extends MusicBeatState
static function getSongPath()
{
return Paths.inst(PlayState.SONG.song);
return Paths.inst(PlayState.currentSong.song);
}
static function getVocalPath(?suffix:String)
{
return Paths.voices(PlayState.SONG.song, suffix);
return Paths.voices(PlayState.currentSong.song, suffix);
}
inline static public function loadAndSwitchState(target:FlxState, stopMusic = false)
@ -198,7 +194,7 @@ class LoadingState extends MusicBeatState
}
#if NO_PRELOAD_ALL
var loaded = isSoundLoaded(getSongPath())
&& (!PlayState.SONG.needsVoices || isSoundLoaded(getVocalPath()))
&& (!PlayState.currentSong.needsVoices || isSoundLoaded(getVocalPath()))
&& isLibraryLoaded("shared");
if (!loaded)

View file

@ -1,6 +1,5 @@
package funkin;
import funkin.NGio;
import flixel.FlxObject;
import flixel.FlxSprite;
import flixel.FlxState;
@ -15,14 +14,18 @@ import flixel.tweens.FlxTween;
import flixel.ui.FlxButton;
import flixel.util.FlxColor;
import flixel.util.FlxTimer;
import lime.app.Application;
import openfl.filters.ShaderFilter;
import funkin.NGio;
import funkin.modding.events.ScriptEvent.UpdateScriptEvent;
import funkin.modding.module.ModuleHandler;
import funkin.shaderslmfao.ScreenWipeShader;
import funkin.ui.AtlasMenuList;
import funkin.ui.MenuList;
import funkin.ui.OptionsState;
import funkin.ui.PreferencesMenu;
import funkin.ui.Prompt;
import funkin.util.Constants;
import lime.app.Application;
import openfl.filters.ShaderFilter;
using StringTools;
@ -30,8 +33,8 @@ using StringTools;
import Discord.DiscordClient;
#end
#if newgrounds
import io.newgrounds.NG;
import funkin.ui.NgPrompt;
import io.newgrounds.NG;
#end
class MainMenuState extends MusicBeatState
@ -99,7 +102,7 @@ class MainMenuState extends MusicBeatState
}
});
menuItems.enabled = false; // disable for intro
menuItems.enabled = true; // can move on intro
menuItems.createItem('story mode', function() startExitState(new StoryMenuState()));
menuItems.createItem('freeplay', function()
{
@ -138,17 +141,21 @@ class MainMenuState extends MusicBeatState
FlxG.camera.follow(camFollow, null, 0.06);
// FlxG.camera.setScrollBounds(bg.x, bg.x + bg.width, bg.y, bg.y + bg.height * 1.2);
var versionStr = 'v${Application.current.meta.get('version')}';
versionStr += ' (secret week 8 build do not leak)';
super.create();
var versionShit:FlxText = new FlxText(5, FlxG.height - 18, 0, versionStr, 12);
versionShit.scrollFactor.set();
versionShit.setFormat("VCR OSD Mono", 16, FlxColor.WHITE, LEFT, FlxTextBorderStyle.OUTLINE, FlxColor.BLACK);
add(versionShit);
// This has to come AFTER!
this.leftWatermarkText.text = Constants.VERSION;
this.rightWatermarkText.text = "blablabla test";
// var versionStr = 'v${Application.current.meta.get('version')}';
// versionStr += ' (secret week 8 build do not leak)';
//
// var versionShit:FlxText = new FlxText(5, FlxG.height - 18, 0, versionStr, 12);
// versionShit.scrollFactor.set();
// versionShit.setFormat("VCR OSD Mono", 16, FlxColor.WHITE, LEFT, FlxTextBorderStyle.OUTLINE, FlxColor.BLACK);
// add(versionShit);
// NG.core.calls.event.logEvent('swag').send();
super.create();
}
override function closeSubState()
@ -162,7 +169,7 @@ class MainMenuState extends MusicBeatState
{
super.finishTransIn();
menuItems.enabled = true;
// menuItems.enabled = true;
// #if newgrounds
// if (NGio.savedSessionFailed)
@ -272,6 +279,8 @@ class MainMenuState extends MusicBeatState
override function update(elapsed:Float)
{
super.update(elapsed);
if (FlxG.onMobile)
{
var touch:FlxTouch = FlxG.touches.getFirst();
@ -295,7 +304,7 @@ class MainMenuState extends MusicBeatState
if (FlxG.sound.music.volume < 0.8)
{
FlxG.sound.music.volume += 0.5 * FlxG.elapsed;
FlxG.sound.music.volume += 0.5 * elapsed;
}
if (_exiting)
@ -306,8 +315,6 @@ class MainMenuState extends MusicBeatState
FlxG.sound.play(Paths.sound('cancelMenu'));
FlxG.switchState(new TitleState());
}
super.update(elapsed);
}
}

View file

@ -1,12 +1,21 @@
package funkin;
import funkin.Conductor.BPMChangeEvent;
import flixel.FlxGame;
import flixel.addons.transition.FlxTransitionableState;
import flixel.FlxState;
import flixel.FlxSubState;
import flixel.addons.ui.FlxUIState;
import flixel.math.FlxRect;
import flixel.util.FlxTimer;
import flixel.text.FlxText;
import flixel.util.FlxColor;
import flixel.util.FlxSort;
import funkin.Conductor.BPMChangeEvent;
import funkin.modding.PolymodHandler;
import funkin.modding.events.ScriptEvent;
import funkin.modding.module.ModuleHandler;
import funkin.util.SortUtil;
/**
* MusicBeatState actually represents the core utility FlxState of the game.
* It includes functionality for event handling, as well as maintaining BPM-based update events.
*/
class MusicBeatState extends FlxUIState
{
private var curStep:Int = 0;
@ -17,16 +26,37 @@ class MusicBeatState extends FlxUIState
inline function get_controls():Controls
return PlayerSettings.player1.controls;
public var leftWatermarkText:FlxText = null;
public var rightWatermarkText:FlxText = null;
public function new()
{
super();
initCallbacks();
}
function initCallbacks()
{
subStateOpened.add(onOpenSubstateComplete);
subStateClosed.add(onCloseSubstateComplete);
}
override function create()
{
if (transIn != null)
trace('reg ' + transIn.region);
super.create();
createWatermarkText();
}
override function update(elapsed:Float)
{
super.update(elapsed);
// This can now be used in EVERY STATE YAY!
if (FlxG.keys.justPressed.F5)
debug_refreshModules();
// everyStep();
var oldStep:Int = curStep;
@ -36,7 +66,41 @@ class MusicBeatState extends FlxUIState
if (oldStep != curStep && curStep >= 0)
stepHit();
super.update(elapsed);
FlxG.watch.addQuick("songPos", Conductor.songPosition);
dispatchEvent(new UpdateScriptEvent(elapsed));
}
function createWatermarkText()
{
// Both have an xPos of 0, but a width equal to the full screen.
// The rightWatermarkText is right aligned, which puts the text in the correct spot.
leftWatermarkText = new FlxText(0, FlxG.height - 18, FlxG.width, '', 12);
rightWatermarkText = new FlxText(0, FlxG.height - 18, FlxG.width, '', 12);
// 100,000 should be good enough.
leftWatermarkText.zIndex = 100000;
rightWatermarkText.zIndex = 100000;
leftWatermarkText.scrollFactor.set(0, 0);
rightWatermarkText.scrollFactor.set(0, 0);
leftWatermarkText.setFormat("VCR OSD Mono", 16, FlxColor.WHITE, LEFT, FlxTextBorderStyle.OUTLINE, FlxColor.BLACK);
rightWatermarkText.setFormat("VCR OSD Mono", 16, FlxColor.WHITE, RIGHT, FlxTextBorderStyle.OUTLINE, FlxColor.BLACK);
add(leftWatermarkText);
add(rightWatermarkText);
}
function dispatchEvent(event:ScriptEvent)
{
ModuleHandler.callEvent(event);
}
function debug_refreshModules()
{
PolymodHandler.forceReloadAssets();
// Restart the current state, so old data is cleared.
FlxG.resetState();
}
private function updateBeat():Void
@ -60,15 +124,97 @@ class MusicBeatState extends FlxUIState
curStep = lastChange.stepTime + Math.floor((Conductor.songPosition - lastChange.songTime) / Conductor.stepCrochet);
}
public function stepHit():Void
public function stepHit():Bool
{
var event = new SongTimeScriptEvent(ScriptEvent.SONG_STEP_HIT, curBeat, curStep);
dispatchEvent(event);
if (event.eventCanceled)
{
return false;
}
if (curStep % 4 == 0)
beatHit();
return true;
}
public function beatHit():Void
public function beatHit():Bool
{
var event = new SongTimeScriptEvent(ScriptEvent.SONG_BEAT_HIT, curBeat, curStep);
dispatchEvent(event);
if (event.eventCanceled)
{
return false;
}
lastBeatHitTime = Conductor.songPosition;
// do literally nothing dumbass
return true;
}
/**
* Refreshes the state, by redoing the render order of all sprites.
* It does this based on the `zIndex` of each prop.
*/
public function refresh()
{
sort(SortUtil.byZIndex, FlxSort.ASCENDING);
}
override function switchTo(nextState:FlxState):Bool
{
var event = new StateChangeScriptEvent(ScriptEvent.STATE_CHANGE_BEGIN, nextState, true);
dispatchEvent(event);
if (event.eventCanceled)
{
return false;
}
return super.switchTo(nextState);
}
public override function openSubState(targetSubstate:FlxSubState):Void
{
var event = new SubStateScriptEvent(ScriptEvent.SUBSTATE_OPEN_BEGIN, targetSubstate, true);
dispatchEvent(event);
if (event.eventCanceled)
{
return;
}
super.openSubState(targetSubstate);
}
function onOpenSubstateComplete(targetState:FlxSubState):Void
{
dispatchEvent(new SubStateScriptEvent(ScriptEvent.SUBSTATE_OPEN_END, targetState, true));
}
public override function closeSubState():Void
{
var event = new SubStateScriptEvent(ScriptEvent.SUBSTATE_CLOSE_BEGIN, this.subState, true);
dispatchEvent(event);
if (event.eventCanceled)
{
return;
}
super.closeSubState();
}
function onCloseSubstateComplete(targetState:FlxSubState):Void
{
dispatchEvent(new SubStateScriptEvent(ScriptEvent.SUBSTATE_CLOSE_END, targetState, true));
}
}

View file

@ -1,8 +1,13 @@
package funkin;
import funkin.Conductor.BPMChangeEvent;
import flixel.FlxSubState;
import funkin.Conductor.BPMChangeEvent;
import funkin.modding.events.ScriptEvent;
import funkin.modding.module.ModuleHandler;
/**
* MusicBeatSubstate reincorporates the functionality of MusicBeatState into an FlxSubState.
*/
class MusicBeatSubstate extends FlxSubState
{
public function new()
@ -53,6 +58,11 @@ class MusicBeatSubstate extends FlxSubState
beatHit();
}
function dispatchEvent(event:ScriptEvent)
{
ModuleHandler.callEvent(event);
}
public function beatHit():Void
{
// do literally nothing dumbass

View file

@ -1,5 +1,6 @@
package funkin;
import funkin.util.Constants;
import flixel.FlxSprite;
import flixel.math.FlxMath;
import funkin.shaderslmfao.ColorSwap;
@ -109,10 +110,8 @@ class Note extends FlxSprite
data.noteData = noteData;
var daStage:String = PlayState.curStageId;
// TODO: Make this logic more generic
switch (daStage)
switch (PlayState.instance.currentStageId)
{
case 'school' | 'schoolEvil':
loadGraphic(Paths.image('weeb/pixelUI/arrows-pixels'), true, 17, 17);
@ -137,7 +136,7 @@ class Note extends FlxSprite
animation.add('bluehold', [1]);
}
setGraphicSize(Std.int(width * PlayState.daPixelZoom));
setGraphicSize(Std.int(width * Constants.PIXEL_ART_SCALE));
updateHitbox();
default:
@ -194,7 +193,7 @@ class Note extends FlxSprite
x -= width / 2;
if (PlayState.curStageId.startsWith('school'))
if (PlayState.instance.currentStageId.startsWith('school'))
x += 30;
if (prevNote.isSustainNote)

View file

@ -1,6 +1,5 @@
package funkin;
import funkin.Controls.Control;
import flixel.FlxSprite;
import flixel.FlxSubState;
import flixel.addons.transition.FlxTransitionableState;
@ -11,6 +10,7 @@ import flixel.text.FlxText;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.util.FlxColor;
import funkin.Controls.Control;
import funkin.play.PlayState;
class PauseSubState extends MusicBeatSubstate
@ -33,6 +33,10 @@ class PauseSubState extends MusicBeatSubstate
var practiceText:FlxText;
var exitingToMenu:Bool = false;
var bg:FlxSprite;
var metaDataGrp:FlxTypedGroup<FlxSprite>;
public function new(x:Float, y:Float)
{
super();
@ -48,39 +52,42 @@ class PauseSubState extends MusicBeatSubstate
FlxG.sound.list.add(pauseMusic);
var bg:FlxSprite = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, FlxColor.BLACK);
bg = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, FlxColor.BLACK);
bg.alpha = 0;
bg.scrollFactor.set();
add(bg);
metaDataGrp = new FlxTypedGroup<FlxSprite>();
add(metaDataGrp);
var levelInfo:FlxText = new FlxText(20, 15, 0, "", 32);
levelInfo.text += PlayState.SONG.song;
levelInfo.text += PlayState.currentSong.song;
levelInfo.scrollFactor.set();
levelInfo.setFormat(Paths.font("vcr.ttf"), 32);
levelInfo.updateHitbox();
add(levelInfo);
metaDataGrp.add(levelInfo);
var levelDifficulty:FlxText = new FlxText(20, 15 + 32, 0, "", 32);
levelDifficulty.text += CoolUtil.difficultyString();
levelDifficulty.scrollFactor.set();
levelDifficulty.setFormat(Paths.font('vcr.ttf'), 32);
levelDifficulty.updateHitbox();
add(levelDifficulty);
metaDataGrp.add(levelDifficulty);
var deathCounter:FlxText = new FlxText(20, 15 + 64, 0, "", 32);
deathCounter.text = "Blue balled: " + PlayState.deathCounter;
deathCounter.scrollFactor.set();
deathCounter.setFormat(Paths.font('vcr.ttf'), 32);
deathCounter.updateHitbox();
add(deathCounter);
metaDataGrp.add(deathCounter);
practiceText = new FlxText(20, 15 + 64 + 32, 0, "PRACTICE MODE", 32);
practiceText.scrollFactor.set();
practiceText.setFormat(Paths.font('vcr.ttf'), 32);
practiceText.updateHitbox();
practiceText.x = FlxG.width - (practiceText.width + 20);
practiceText.visible = PlayState.practiceMode;
add(practiceText);
practiceText.visible = PlayState.isPracticeMode;
metaDataGrp.add(practiceText);
levelDifficulty.alpha = 0;
levelInfo.alpha = 0;
@ -133,71 +140,103 @@ class PauseSubState extends MusicBeatSubstate
var downP = controls.UI_DOWN_P;
var accepted = controls.ACCEPT;
if (upP)
#if debug
// to pause the game and get screenshots easy, press H on pause menu!
if (FlxG.keys.justPressed.H)
{
changeSelection(-1);
bg.visible = !bg.visible;
grpMenuShit.visible = !grpMenuShit.visible;
metaDataGrp.visible = !metaDataGrp.visible;
}
if (downP)
{
changeSelection(1);
}
var androidPause:Bool = false;
#if android
androidPause = FlxG.android.justPressed.BACK;
#end
if (androidPause)
close();
if (accepted)
if (!exitingToMenu)
{
var daSelected:String = menuItems[curSelected];
switch (daSelected)
if (upP)
{
case "Resume":
close();
case "EASY" | 'NORMAL' | "HARD":
PlayState.SONG = SongLoad.loadFromJson(PlayState.SONG.song.toLowerCase(), PlayState.SONG.song.toLowerCase());
SongLoad.curDiff = daSelected.toLowerCase();
PlayState.storyDifficulty = curSelected;
PlayState.needsReset = true;
close();
case 'Toggle Practice Mode':
PlayState.practiceMode = !PlayState.practiceMode;
practiceText.visible = PlayState.practiceMode;
case 'Change Difficulty':
menuItems = difficultyChoices;
regenMenu();
case 'BACK':
menuItems = pauseOG;
regenMenu();
case "Restart Song":
PlayState.needsReset = true;
close();
// FlxG.resetState();
case "Exit to menu":
PlayState.seenCutscene = false;
PlayState.deathCounter = 0;
if (PlayState.isStoryMode)
FlxG.switchState(new StoryMenuState());
else
FlxG.switchState(new FreeplayState());
changeSelection(-1);
}
if (downP)
{
changeSelection(1);
}
}
if (FlxG.keys.justPressed.J)
{
// for reference later!
// PlayerSettings.player1.controls.replaceBinding(Control.LEFT, Keys, FlxKey.J, null);
var androidPause:Bool = false;
#if android
androidPause = FlxG.android.justPressed.BACK;
#end
if (androidPause)
close();
if (accepted)
{
var daSelected:String = menuItems[curSelected];
switch (daSelected)
{
case "Resume":
close();
case "EASY" | 'NORMAL' | "HARD":
PlayState.currentSong = SongLoad.loadFromJson(PlayState.currentSong.song.toLowerCase(), PlayState.currentSong.song.toLowerCase());
SongLoad.curDiff = daSelected.toLowerCase();
PlayState.storyDifficulty = curSelected;
PlayState.needsReset = true;
close();
case 'Toggle Practice Mode':
PlayState.isPracticeMode = !PlayState.isPracticeMode;
practiceText.visible = PlayState.isPracticeMode;
case 'Change Difficulty':
menuItems = difficultyChoices;
regenMenu();
case 'BACK':
menuItems = pauseOG;
regenMenu();
case "Restart Song":
PlayState.needsReset = true;
close();
// FlxG.resetState();
case "Exit to menu":
exitingToMenu = true;
PlayState.seenCutscene = false;
PlayState.deathCounter = 0;
for (item in grpMenuShit.members)
{
item.targetY = -3;
item.alpha = 0.6;
}
FlxTween.tween(bg, {alpha: 1}, 0.4, {
ease: FlxEase.quartInOut,
onComplete: function(_)
{
FlxTransitionableState.skipNextTransIn = true;
FlxTransitionableState.skipNextTransOut = true;
FlxG.cameras.list[1].alpha = 0; // bullshit for the UI camera???
if (PlayState.isStoryMode)
FlxG.switchState(new StoryMenuState());
else
FlxG.switchState(new FreeplayState());
}
});
}
}
if (FlxG.keys.justPressed.J)
{
// for reference later!
// PlayerSettings.player1.controls.replaceBinding(Control.LEFT, Keys, FlxKey.J, null);
}
}
}
@ -219,12 +258,9 @@ class PauseSubState extends MusicBeatSubstate
if (curSelected >= menuItems.length)
curSelected = 0;
var bullShit:Int = 0;
for (item in grpMenuShit.members)
for (index => item in grpMenuShit.members)
{
item.targetY = bullShit - curSelected;
bullShit++;
item.targetY = index - curSelected;
item.alpha = 0.6;
// item.setGraphicSize(Std.int(item.width * 0.8));

View file

@ -311,7 +311,7 @@ class StoryMenuState extends MusicBeatState
PlayState.isStoryMode = true;
selectedWeek = true;
PlayState.SONG = SongLoad.loadFromJson(PlayState.storyPlaylist[0].toLowerCase(), PlayState.storyPlaylist[0].toLowerCase());
PlayState.currentSong = SongLoad.loadFromJson(PlayState.storyPlaylist[0].toLowerCase(), PlayState.storyPlaylist[0].toLowerCase());
PlayState.storyWeek = curWeek;
PlayState.campaignScore = 0;

View file

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

View file

@ -4,6 +4,9 @@ import haxe.format.JsonParser;
import openfl.Assets;
import openfl.geom.Matrix3D;
import openfl.geom.Matrix;
#if sys
import sys.io.File;
#end
/**
* Generally designed / written in a way that can be easily taken out of FNF and used elsewhere

View file

@ -48,12 +48,12 @@ class ABotVis extends FlxTypedSpriteGroup<FlxSprite>
{
// updateViz();
updateFFT();
updateFFT(elapsed);
super.update(elapsed);
}
function updateFFT()
function updateFFT(elapsed:Float)
{
if (vis.snd != null)
{
@ -112,7 +112,7 @@ class ABotVis extends FlxTypedSpriteGroup<FlxSprite>
avgVel *= 10000000;
volumes[i] += avgVel - (FlxG.elapsed * (volumes[i] * 50));
volumes[i] += avgVel - (elapsed * (volumes[i] * 50));
var animFrame:Int = Std.int(volumes[i]);

View file

@ -1,12 +1,5 @@
package funkin.charting;
import funkin.Conductor.BPMChangeEvent;
import funkin.Note.NoteData;
import funkin.Section.SwagSection;
import funkin.SongLoad.SwagSong;
import funkin.audiovis.ABotVis;
import funkin.audiovis.PolygonSpectogram;
import funkin.audiovis.SpectogramSprite;
import flixel.FlxSprite;
import flixel.addons.display.FlxGridOverlay;
import flixel.addons.transition.FlxTransitionableState;
@ -23,14 +16,21 @@ import flixel.system.FlxSound;
import flixel.text.FlxText;
import flixel.ui.FlxButton;
import flixel.util.FlxColor;
import funkin.Conductor.BPMChangeEvent;
import funkin.Note.NoteData;
import funkin.Section.SwagSection;
import funkin.SongLoad.SwagSong;
import funkin.audiovis.ABotVis;
import funkin.audiovis.PolygonSpectogram;
import funkin.audiovis.SpectogramSprite;
import funkin.play.PlayState;
import funkin.rendering.MeshRender;
import haxe.Json;
import lime.media.AudioBuffer;
import lime.utils.Assets;
import openfl.events.Event;
import openfl.events.IOErrorEvent;
import openfl.net.FileReference;
import funkin.rendering.MeshRender;
import funkin.play.PlayState;
using Lambda;
using StringTools;
@ -122,9 +122,9 @@ class ChartingState extends MusicBeatState
curRenderedNotes = new FlxTypedGroup<Note>();
curRenderedSustains = new FlxTypedGroup<FlxSprite>();
if (PlayState.SONG != null)
if (PlayState.currentSong != null)
{
_song = SongLoad.songData = PlayState.SONG;
_song = SongLoad.songData = PlayState.currentSong;
trace("LOADED A PLAYSTATE SONGFILE");
}
else
@ -144,7 +144,7 @@ class ChartingState extends MusicBeatState
updateGrid();
loadSong(_song.song);
Conductor.changeBPM(_song.bpm);
Conductor.bpm = _song.bpm;
Conductor.mapBPMChanges(_song);
bpmTxt = new FlxText(1000, 50, 0, "", 16);
@ -545,7 +545,7 @@ class ChartingState extends MusicBeatState
{
tempBpm = nums.value;
Conductor.mapBPMChanges(_song);
Conductor.changeBPM(nums.value);
Conductor.bpm = nums.value;
}
else if (wname == 'note_susLength')
{
@ -814,7 +814,7 @@ class ChartingState extends MusicBeatState
lastSection = curSection;
PlayState.SONG = _song;
PlayState.currentSong = _song;
// JUST FOR DEBUG DARNELL STUFF, GENERALIZE THIS FOR BETTER LOADING ELSEWHERE TOO!
PlayState.storyWeek = 8;
@ -903,7 +903,7 @@ class ChartingState extends MusicBeatState
{
if (FlxG.keys.pressed.W || FlxG.keys.pressed.S)
{
var daTime:Float = 700 * FlxG.elapsed;
var daTime:Float = 700 * elapsed;
if (FlxG.keys.pressed.CONTROL)
daTime *= 0.2;
@ -975,9 +975,9 @@ class ChartingState extends MusicBeatState
_song.bpm = tempBpm;
/* if (FlxG.keys.justPressed.UP)
Conductor.changeBPM(Conductor.bpm + 1);
Conductor.bpm = Conductor.bpm + 1;
if (FlxG.keys.justPressed.DOWN)
Conductor.changeBPM(Conductor.bpm - 1); */
Conductor.bpm = Conductor.bpm - 1; */
var shiftThing:Int = 1;
if (FlxG.keys.pressed.SHIFT)
@ -1213,7 +1213,7 @@ class ChartingState extends MusicBeatState
if (SongLoad.getSong()[curSection].changeBPM && SongLoad.getSong()[curSection].bpm > 0)
{
Conductor.changeBPM(SongLoad.getSong()[curSection].bpm);
Conductor.bpm = SongLoad.getSong()[curSection].bpm;
FlxG.log.add('CHANGED BPM!');
}
else
@ -1223,7 +1223,7 @@ class ChartingState extends MusicBeatState
for (i in 0...curSection)
if (SongLoad.getSong()[i].changeBPM)
daBPM = SongLoad.getSong()[i].bpm;
Conductor.changeBPM(daBPM);
Conductor.bpm = daBPM;
}
/* // PORT BULLSHIT, INCASE THERE'S NO SUSTAIN DATA FOR A NOTE
@ -1462,13 +1462,13 @@ class ChartingState extends MusicBeatState
function loadJson(song:String):Void
{
PlayState.SONG = SongLoad.loadFromJson(song.toLowerCase(), song.toLowerCase());
PlayState.currentSong = SongLoad.loadFromJson(song.toLowerCase(), song.toLowerCase());
LoadingState.loadAndSwitchState(new ChartingState());
}
function loadAutosave():Void
{
PlayState.SONG = FlxG.save.data.autosave;
PlayState.currentSong = FlxG.save.data.autosave;
FlxG.resetState();
}

View file

@ -60,7 +60,7 @@ class BGScrollingText extends FlxSpriteGroup
{
for (txt in grpTexts.group)
{
txt.x -= 1 * (speed * (FlxG.elapsed / (1 / 60)));
txt.x -= 1 * (speed * (elapsed / (1 / 60)));
if (speed > 0)
{

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

@ -3,8 +3,11 @@ package funkin.modding;
import polymod.hscript.HScriptable;
/**
* Add this interface to a class to make it a scriptable object.
* Functions annotated with @:hscript will call the relevant script.
* Functions annotated with @:hookable can be reassigned.
* NOTE: If you receive the following error when making a function use @:hookable:
* `Cannot access this or other member field in variable initialization`
* This is because you need to perform calls and assignments using a static variable referencing the target object.
*/
@:hscript({
// ALL of these values are added to ALL scripts in the child classes.

View file

@ -0,0 +1,76 @@
package funkin.modding;
import funkin.modding.events.ScriptEvent;
/**
* Defines a set of callbacks available to all scripted classes.
*
* Includes events handling basic life cycle relevant to all scripted classes.
*/
interface IScriptedClass
{
public function onScriptEvent(event:ScriptEvent):Void;
public function onCreate(event:ScriptEvent):Void;
public function onDestroy(event:ScriptEvent):Void;
public function onUpdate(event:UpdateScriptEvent):Void;
}
/**
* Defines a set of callbacks available to scripted classes which can follow the game between states.
*/
interface IStateChangingScriptedClass extends IScriptedClass
{
public function onStateChangeBegin(event:StateChangeScriptEvent):Void;
public function onStateChangeEnd(event:StateChangeScriptEvent):Void;
public function onSubstateOpenBegin(event:SubStateScriptEvent):Void;
public function onSubstateOpenEnd(event:SubStateScriptEvent):Void;
public function onSubstateCloseBegin(event:SubStateScriptEvent):Void;
public function onSubstateCloseEnd(event:SubStateScriptEvent):Void;
}
/**
* Defines a set of callbacks available to scripted classes which represent notes.
*/
interface INoteScriptedClass extends IScriptedClass
{
public function onNoteHit(event:NoteScriptEvent):Void;
public function onNoteMiss(event:NoteScriptEvent):Void;
}
/**
* Developer note:
*
* I previously considered adding events for onKeyDown, onKeyUp, mouse events, etc.
* However, I realized that you can simply call something like the following within a module:
* `FlxG.stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);`
* This is more efficient than adding an entire event handler for every key press.
*
* -Eric
*/
/**
* Defines a set of callbacks available to scripted classes that involve the lifecycle of the Play State.
*/
interface IPlayStateScriptedClass extends IScriptedClass
{
public function onPause(event:PauseScriptEvent):Void;
public function onResume(event:ScriptEvent):Void;
public function onSongLoaded(eent:SongLoadScriptEvent):Void;
public function onSongStart(event:ScriptEvent):Void;
public function onSongEnd(event:ScriptEvent):Void;
public function onGameOver(event:ScriptEvent):Void;
public function onSongRetry(event:ScriptEvent):Void;
public function onNoteHit(event:NoteScriptEvent):Void;
public function onNoteMiss(event:NoteScriptEvent):Void;
public function onNoteGhostMiss(event:GhostMissNoteScriptEvent):Void;
public function onStepHit(event:SongTimeScriptEvent):Void;
public function onBeatHit(event:SongTimeScriptEvent):Void;
public function onCountdownStart(event:CountdownScriptEvent):Void;
public function onCountdownStep(event:CountdownScriptEvent):Void;
public function onCountdownEnd(event:CountdownScriptEvent):Void;
}

View file

@ -1,10 +1,9 @@
package funkin.modding;
import polymod.Polymod.ModMetadata;
import funkin.modding.module.ModuleHandler;
import funkin.play.stage.StageData;
import polymod.Polymod;
import polymod.backends.OpenFLBackend;
import polymod.backends.PolymodAssets.PolymodAssetType;
import polymod.format.ParseRules.LinesParseFormat;
import polymod.format.ParseRules.TextFileFormat;
class PolymodHandler
@ -31,6 +30,15 @@ class PolymodHandler
loadModsById(getAllModIds());
}
/**
* Loads the game with configured mods enabled with Polymod.
*/
public static function loadEnabledMods()
{
trace("Initializing Polymod (using configured mods)...");
loadModsById(getEnabledModIds());
}
/**
* Loads the game without any mods enabled with Polymod.
*/
@ -146,8 +154,8 @@ class PolymodHandler
{
return {
assetLibraryPaths: [
"songs" => "songs", "shared" => "", "tutorial" => "tutorial", "scripts" => "scripts", "week1" => "week1", "week2" => "week2",
"week3" => "week3", "week4" => "week4", "week5" => "week5", "week6" => "week6", "week7" => "week7", "week8" => "week8",
"songs" => "songs", "shared" => "", "tutorial" => "tutorial", "scripts" => "scripts", "week1" => "week1", "week2" => "week2",
"week3" => "week3", "week4" => "week4", "week5" => "week5", "week6" => "week6", "week7" => "week7", "week8" => "week8",
]
}
}
@ -165,4 +173,61 @@ class PolymodHandler
var modIds = [for (i in getAllMods()) i.id];
return modIds;
}
public static function setEnabledMods(newModList:Array<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

@ -0,0 +1,554 @@
package funkin.modding.events;
import flixel.FlxState;
import flixel.FlxSubState;
import funkin.Note.NoteDir;
import funkin.play.Countdown.CountdownStep;
import openfl.events.EventType;
import openfl.events.KeyboardEvent;
typedef ScriptEventType = EventType<ScriptEvent>;
/**
* This is a base class for all events that are issued to scripted classes.
* It can be used to identify the type of event called, store data, and cancel event propagation.
*/
class ScriptEvent
{
/**
* Called when the relevant object is created.
* Keep in mind that the constructor may be called before the object is needed,
* for the purposes of caching data or otherwise.
*
* This event is not cancelable.
*/
public static inline final CREATE:ScriptEventType = "CREATE";
/**
* Called when the relevant object is destroyed.
* This should perform relevant cleanup to ensure good performance.
*
* This event is not cancelable.
*/
public static inline final DESTROY:ScriptEventType = "DESTROY";
/**
* Called during the update function.
* This is called every frame, so be careful!
*
* This event is not cancelable.
*/
public static inline final UPDATE:ScriptEventType = "UPDATE";
/**
* Called when the player moves to pause the game.
*
* This event IS cancelable! Canceling the event will prevent the game from pausing.
*/
public static inline final PAUSE:ScriptEventType = "PAUSE";
/**
* Called when the player moves to unpause the game while paused.
*
* This event IS cancelable! Canceling the event will prevent the game from resuming.
*/
public static inline final RESUME:ScriptEventType = "RESUME";
/**
* Called once per step in the song. This happens 4 times per measure.
*
* This event is not cancelable.
*/
public static inline final SONG_BEAT_HIT:ScriptEventType = "BEAT_HIT";
/**
* Called once per step in the song. This happens 16 times per measure.
*
* This event is not cancelable.
*/
public static inline final SONG_STEP_HIT:ScriptEventType = "STEP_HIT";
/**
* Called when a character hits a note.
* Important information such as judgement/timing, note data, player/opponent, etc. are all provided.
*
* This event IS cancelable! Canceling this event prevents the note from being hit,
* and will likely result in a miss later.
*/
public static inline final NOTE_HIT:ScriptEventType = "NOTE_HIT";
/**
* Called when a character misses a note.
* Important information such as note data, player/opponent, etc. are all provided.
*
* This event IS cancelable! Canceling this event prevents the note from being considered missed,
* avoiding a combo break and lost health.
*/
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.
*
* This event is not cancelable.
*/
public static inline final SONG_START:ScriptEventType = "SONG_START";
/**
* Called when the song ends. This happens as the instrumental and vocals end.
*
* This event is not cancelable.
*/
public static inline final SONG_END:ScriptEventType = "SONG_END";
/**
* Called when the countdown begins. This occurs before the song starts.
*
* This event IS cancelable! Canceling this event will prevent the countdown from starting.
* - The song will not start until you call Countdown.performCountdown() later.
* - Note that calling performCountdown() will trigger this event again, so be sure to add logic to ignore it.
*/
public static inline final COUNTDOWN_START:ScriptEventType = "COUNTDOWN_START";
/**
* Called when a step of the countdown happens.
* Includes information about what step of the countdown was hit.
*
* This event IS cancelable! Canceling this event will pause the countdown.
* - The countdown will not resume until you call PlayState.resumeCountdown().
*/
public static inline final COUNTDOWN_STEP:ScriptEventType = "COUNTDOWN_STEP";
/**
* Called when the countdown is done but just before the song starts.
*
* This event is not cancelable.
*/
public static inline final COUNTDOWN_END:ScriptEventType = "COUNTDOWN_END";
/**
* Called before the game over screen triggers and the death animation plays.
*
* This event is not cancelable.
*/
public static inline final GAME_OVER:ScriptEventType = "GAME_OVER";
/**
* Called when the player presses a key to restart the game.
* This can happen from the pause menu or the game over screen.
*
* This event IS cancelable! Canceling this event will prevent the game from restarting.
*/
public static inline final SONG_RETRY:ScriptEventType = "SONG_RETRY";
/**
* Called when the player pushes down any key on the keyboard.
*
* This event is not cancelable.
*/
public static inline final KEY_DOWN:ScriptEventType = "KEY_DOWN";
/**
* Called when the player releases a key on the keyboard.
*
* This event is not cancelable.
*/
public static inline final KEY_UP:ScriptEventType = "KEY_UP";
/**
* Called when the game has finished loading the notes from JSON.
* This allows modders to mutate the notes before they are used in the song.
*
* This event is not cancelable.
*/
public static inline final SONG_LOADED:ScriptEventType = "SONG_LOADED";
/**
* Called when the game is about to switch the current FlxState.
*
* This event is not cancelable.
*/
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.
*
* This event is not cancelable.
*/
/**
* If true, the behavior associated with this event can be prevented.
* For example, cancelling COUNTDOWN_START should prevent the countdown from starting,
* until another script restarts it, or cancelling NOTE_HIT should cause the note to be missed.
*/
public var cancelable(default, null):Bool;
/**
* The type associated with the event.
*/
public var type(default, null):ScriptEventType;
/**
* Whether the event should continue to be triggered on additional targets.
*/
public var shouldPropagate(default, null):Bool;
/**
* Whether the event has been canceled by one of the scripts that received it.
*/
public var eventCanceled(default, null):Bool;
public function new(type:ScriptEventType, cancelable:Bool = false):Void
{
this.type = type;
this.cancelable = cancelable;
this.eventCanceled = false;
this.shouldPropagate = true;
}
/**
* Call this function on a cancelable event to cancel the associated behavior.
* For example, cancelling COUNTDOWN_START will prevent the countdown from starting.
*/
public function cancelEvent():Void
{
if (cancelable)
{
eventCanceled = true;
}
}
public function cancel():Void
{
// This typo happens enough that I just added this.
cancelEvent();
}
/**
* Call this function to stop any other Scripteds from receiving the event.
*/
public function stopPropagation():Void
{
shouldPropagate = false;
}
public function toString():String
{
return 'ScriptEvent(type=$type, cancelable=$cancelable)';
}
}
/**
* SPECIFIC EVENTS
*/
/**
* An event that is fired associated with a specific note.
*/
class NoteScriptEvent extends ScriptEvent
{
/**
* The note associated with this event.
* You cannot replace it, but you can edit it.
*/
public var note(default, null):Note;
public function new(type:ScriptEventType, note:Note, cancelable:Bool = false):Void
{
super(type, cancelable);
this.note = note;
}
public override function toString():String
{
return 'NoteScriptEvent(type=' + type + ', cancelable=' + cancelable + ', note=' + note + ')';
}
}
/**
* 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.
*/
class UpdateScriptEvent extends ScriptEvent
{
/**
* The note associated with this event.
* You cannot replace it, but you can edit it.
*/
public var elapsed(default, null):Float;
public function new(elapsed:Float):Void
{
super(ScriptEvent.UPDATE, false);
this.elapsed = elapsed;
}
public override function toString():String
{
return 'UpdateScriptEvent(elapsed=$elapsed)';
}
}
/**
* An event that is fired regularly during the song.
* May be on beat or on step.
*/
class SongTimeScriptEvent extends ScriptEvent
{
/**
* The current beat of the song.
*/
public var beat(default, null):Int;
/**
* The current step of the song.
*/
public var step(default, null):Int;
public function new(type:ScriptEventType, beat:Int, step:Int):Void
{
super(type, true);
this.beat = beat;
this.step = step;
}
public override function toString():String
{
return 'SongTimeScriptEvent(type=' + type + ', beat=' + beat + ', step=' + step + ')';
}
}
/**
* An event that is fired regularly during the song.
* May be on beat or on step.
*/
class CountdownScriptEvent extends ScriptEvent
{
/**
* The current step of the countdown.
*/
public var step(default, null):CountdownStep;
public function new(type:ScriptEventType, step:CountdownStep, cancelable = true):Void
{
super(type, cancelable);
this.step = step;
}
public override function toString():String
{
return 'CountdownScriptEvent(type=' + type + ', step=' + step + ')';
}
}
/**
* An event that is fired when the player presses a key.
*/
class KeyboardInputScriptEvent extends ScriptEvent
{
/**
* The associated keyboard event.
*/
public var event(default, null):KeyboardEvent;
public function new(type:ScriptEventType, event:KeyboardEvent):Void
{
super(type, false);
this.event = event;
}
public override function toString():String
{
return 'KeyboardInputScriptEvent(type=' + type + ', event=' + event + ')';
}
}
/**
* An event that is fired once the song's chart has been parsed.
*/
class SongLoadScriptEvent extends ScriptEvent
{
/**
* The note associated with this event.
* You cannot replace it, but you can edit it.
*/
public var notes(default, set):Array<Note>;
public var id(default, null):String;
public var difficulty(default, null):String;
function set_notes(notes:Array<Note>):Array<Note>
{
this.notes = notes;
return this.notes;
}
public function new(id:String, difficulty:String, notes:Array<Note>):Void
{
super(ScriptEvent.SONG_LOADED, false);
this.id = id;
this.difficulty = difficulty;
this.notes = notes;
}
public override function toString():String
{
var noteStr = notes == null ? 'null' : 'Array(' + notes.length + ')';
return 'SongLoadScriptEvent(notes=$noteStr, id=$id, difficulty=$difficulty)';
}
}
/**
* An event that is fired when moving out of or into an FlxState.
*/
class StateChangeScriptEvent extends ScriptEvent
{
/**
* 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, cancelable);
this.targetState = targetState;
}
public override function toString():String
{
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

@ -0,0 +1,152 @@
package funkin.modding.events;
import funkin.modding.IScriptedClass.IPlayStateScriptedClass;
import funkin.modding.IScriptedClass;
/**
* Utility functions to assist with handling scripted classes.
*/
class ScriptEventDispatcher
{
public static function callEvent(target:IScriptedClass, event:ScriptEvent):Void
{
if (target == null || event == null)
return;
target.onScriptEvent(event);
// If one target says to stop propagation, stop.
if (!event.shouldPropagate)
{
return;
}
// IScriptedClass
switch (event.type)
{
case ScriptEvent.CREATE:
target.onCreate(event);
return;
case ScriptEvent.DESTROY:
target.onDestroy(event);
return;
case ScriptEvent.UPDATE:
target.onUpdate(cast event);
return;
}
if (Std.isOfType(target, IPlayStateScriptedClass))
{
var t = cast(target, IPlayStateScriptedClass);
switch (event.type)
{
case ScriptEvent.NOTE_HIT:
t.onNoteHit(cast event);
return;
case ScriptEvent.NOTE_MISS:
t.onNoteMiss(cast event);
return;
case ScriptEvent.NOTE_GHOST_MISS:
t.onNoteGhostMiss(cast event);
return;
case ScriptEvent.SONG_BEAT_HIT:
t.onBeatHit(cast event);
return;
case ScriptEvent.SONG_STEP_HIT:
t.onStepHit(cast event);
return;
case ScriptEvent.SONG_START:
t.onSongStart(event);
return;
case ScriptEvent.SONG_END:
t.onSongEnd(event);
return;
case ScriptEvent.SONG_RETRY:
t.onSongRetry(event);
return;
case ScriptEvent.GAME_OVER:
t.onGameOver(event);
return;
case ScriptEvent.PAUSE:
t.onPause(cast event);
return;
case ScriptEvent.RESUME:
t.onResume(event);
return;
case ScriptEvent.COUNTDOWN_START:
t.onCountdownStart(cast event);
return;
case ScriptEvent.COUNTDOWN_STEP:
t.onCountdownStep(cast event);
return;
case ScriptEvent.COUNTDOWN_END:
t.onCountdownEnd(cast event);
return;
case ScriptEvent.SONG_LOADED:
t.onSongLoaded(cast event);
return;
}
}
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
{
if (targets == null || event == null)
return;
if (Std.isOfType(targets, Array))
{
var t = cast(targets, Array<Dynamic>);
if (t.length == 0)
return;
}
for (target in targets)
{
var t:IScriptedClass = cast target;
if (t == null)
continue;
callEvent(t, event);
// If one target says to stop propagation, stop.
if (!event.shouldPropagate)
{
return;
}
}
}
}

View file

@ -0,0 +1,118 @@
package funkin.modding.module;
import funkin.modding.IScriptedClass.IPlayStateScriptedClass;
import funkin.modding.IScriptedClass.IStateChangingScriptedClass;
import funkin.modding.events.ScriptEvent;
/**
* A module is a scripted class which receives all events without requiring a specific context.
* You may have the module active at all times, or only when another script enables it.
*/
class Module implements IPlayStateScriptedClass implements IStateChangingScriptedClass
{
/**
* Whether the module is currently active.
*/
public var active(default, set):Bool = true;
function set_active(value:Bool):Bool
{
this.active = value;
return value;
}
public var moduleId(default, null):String = 'UNKNOWN';
/**
* Determines the order in which modules receive events.
* You can modify this to change the order in which a given module receives events.
*
* Priority 1 is processed before Priority 1000, etc.
*/
public var priority(default, set):Int;
function set_priority(value:Int):Int
{
this.priority = value;
@:privateAccess
ModuleHandler.reorderModuleCache();
return value;
}
/**
* Called when the module is initialized.
* It may not be safe to reference other modules here since they may not be loaded yet.
*
* NOTE: To make the module start inactive, call `this.active = false` in the constructor.
*/
public function new(moduleId:String, priority:Int = 1000):Void
{
this.moduleId = moduleId;
this.priority = priority;
}
public function toString()
{
return 'Module(' + this.moduleId + ')';
}
// TODO: Half of these aren't actually being called!!!!!!!
public function onScriptEvent(event:ScriptEvent) {}
/**
* Called when the module is first created.
* This happens before the title screen appears!
*/
public function onCreate(event:ScriptEvent) {}
/**
* Called when a module is destroyed.
* This currently only happens when reloading modules with F5.
*/
public function onDestroy(event:ScriptEvent) {}
public function onUpdate(event:UpdateScriptEvent) {}
public function onPause(event:PauseScriptEvent) {}
public function onResume(event:ScriptEvent) {}
public function onSongStart(event:ScriptEvent) {}
public function onSongEnd(event:ScriptEvent) {}
public function onGameOver(event:ScriptEvent) {}
public function onNoteHit(event:NoteScriptEvent) {}
public function onNoteMiss(event:NoteScriptEvent) {}
public function onNoteGhostMiss(event:GhostMissNoteScriptEvent) {}
public function onStepHit(event:SongTimeScriptEvent) {}
public function onBeatHit(event:SongTimeScriptEvent) {}
public function onCountdownStart(event:CountdownScriptEvent) {}
public function onCountdownStep(event:CountdownScriptEvent) {}
public function onCountdownEnd(event:CountdownScriptEvent) {}
public function onSongLoaded(event:SongLoadScriptEvent) {}
public function onStateChangeBegin(event:StateChangeScriptEvent) {}
public function onStateChangeEnd(event:StateChangeScriptEvent) {}
public function onSubstateOpenBegin(event:SubStateScriptEvent) {}
public function onSubstateOpenEnd(event:SubStateScriptEvent) {}
public function onSubstateCloseBegin(event:SubStateScriptEvent) {}
public function onSubstateCloseEnd(event:SubStateScriptEvent) {}
public function onSongRetry(event:ScriptEvent) {}
}

View file

@ -0,0 +1,132 @@
package funkin.modding.module;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEvent.UpdateScriptEvent;
using funkin.util.IteratorTools;
/**
* Utility functions for loading and manipulating active modules.
*/
class ModuleHandler
{
static final moduleCache:Map<String, Module> = new Map<String, Module>();
static var modulePriorityOrder:Array<String> = [];
/**
* Parses and preloads the game's stage data and scripts when the game starts.
*
* If you want to force stages to be reloaded, you can just call this function again.
*/
public static function loadModuleCache():Void
{
// Clear any stages that are cached if there were any.
clearModuleCache();
trace("[MODULEHANDLER] Loading module cache...");
var scriptedModuleClassNames:Array<String> = ScriptedModule.listScriptClasses();
trace(' Instantiating ${scriptedModuleClassNames.length} modules...');
for (moduleCls in scriptedModuleClassNames)
{
var module:Module = ScriptedModule.init(moduleCls, moduleCls);
if (module != null)
{
trace(' Loaded module: ${moduleCls}');
// Then store it.
addToModuleCache(module);
}
else
{
trace(' Failed to instantiate module: ${moduleCls}');
}
}
reorderModuleCache();
trace("[MODULEHANDLER] Module cache loaded.");
}
static function addToModuleCache(module:Module):Void
{
moduleCache.set(module.moduleId, module);
}
static function reorderModuleCache():Void
{
modulePriorityOrder = moduleCache.keys().array();
modulePriorityOrder.sort(function(a:String, b:String):Int
{
var aModule:Module = moduleCache.get(a);
var bModule:Module = moduleCache.get(b);
if (aModule.priority != bModule.priority)
{
return aModule.priority - bModule.priority;
}
else
{
// Sort alphabetically. Yes that's how this works.
return a > b ? 1 : -1;
}
});
}
public static function getModule(moduleId:String):Module
{
return moduleCache.get(moduleId);
}
public static function activateModule(moduleId:String):Void
{
var module:Module = getModule(moduleId);
if (module != null)
{
module.active = true;
}
}
public static function deactivateModule(moduleId:String):Void
{
var module:Module = getModule(moduleId);
if (module != null)
{
module.active = false;
}
}
/**
* Clear the module cache, forcing all modules to call shutdown events.
*/
public static function clearModuleCache():Void
{
if (moduleCache != null)
{
var event = new ScriptEvent(ScriptEvent.DESTROY, false);
// Note: Ignore stopPropagation()
for (key => value in moduleCache)
{
ScriptEventDispatcher.callEvent(value, event);
moduleCache.remove(key);
}
moduleCache.clear();
modulePriorityOrder = [];
}
}
public static function callEvent(event:ScriptEvent):Void
{
for (moduleId in modulePriorityOrder)
{
var module:Module = moduleCache.get(moduleId);
// The module needs to be active to receive events.
if (module != null && module.active)
{
ScriptEventDispatcher.callEvent(module, event);
}
}
}
}

View file

@ -0,0 +1,9 @@
package funkin.modding.module;
import funkin.modding.IHook;
@:hscriptClass
class ScriptedModule extends Module implements IHook
{
// No body needed for this class, it's magic ;)
}

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

@ -0,0 +1,311 @@
package funkin.play;
import funkin.util.Constants;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.FlxSprite;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.modding.module.ModuleHandler;
import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEvent.CountdownScriptEvent;
import flixel.util.FlxTimer;
using StringTools;
class Countdown
{
/**
* The current step of the countdown.
*/
public static var countdownStep(default, null):CountdownStep = BEFORE;
/**
* The currently running countdown. This will be null if there is no countdown running.
*/
static var countdownTimer:FlxTimer = null;
/**
* Performs the countdown.
* Pauses the song, plays the countdown graphics/sound, and then starts the song.
* This will automatically stop and restart the countdown if it is already running.
*/
public static function performCountdown(isPixelStyle:Bool):Void
{
// Stop any existing countdown.
stopCountdown();
PlayState.isInCountdown = true;
Conductor.songPosition = Conductor.crochet * -5;
countdownStep = BEFORE;
var cancelled:Bool = propagateCountdownEvent(countdownStep);
if (cancelled)
return;
// The timer function gets called based on the beat of the song.
countdownTimer = new FlxTimer();
countdownTimer.start(Conductor.crochet / 1000, function(tmr:FlxTimer)
{
countdownStep = decrement(countdownStep);
// Play the dance animations manually.
@:privateAccess
PlayState.instance.danceOnBeat();
// Countdown graphic.
showCountdownGraphic(countdownStep, isPixelStyle);
// Countdown sound.
playCountdownSound(countdownStep, isPixelStyle);
// Event handling bullshit.
var cancelled:Bool = propagateCountdownEvent(countdownStep);
if (cancelled)
pauseCountdown();
if (countdownStep == AFTER)
{
stopCountdown();
}
}, 6); // Before, 3, 2, 1, GO!, After
}
/**
* @return TRUE if the event was cancelled.
*/
static function propagateCountdownEvent(index:CountdownStep):Bool
{
var event:ScriptEvent;
switch (index)
{
case BEFORE:
event = new CountdownScriptEvent(ScriptEvent.COUNTDOWN_START, index);
case THREE | TWO | ONE | GO: // I didn't know you could use `|` in a switch/case block!
event = new CountdownScriptEvent(ScriptEvent.COUNTDOWN_STEP, index);
case AFTER:
event = new CountdownScriptEvent(ScriptEvent.COUNTDOWN_END, index, false);
default:
return true;
}
// Stage
ScriptEventDispatcher.callEvent(PlayState.instance.currentStage, event);
// Modules
ModuleHandler.callEvent(event);
return event.eventCanceled;
}
/**
* Pauses the countdown at the current step. You can start it up again later by calling resumeCountdown().
*
* If you want to call this from a module, it's better to use the event system and cancel the onCountdownStep event.
*/
public static function pauseCountdown()
{
if (countdownTimer != null && !countdownTimer.finished)
{
countdownTimer.active = false;
}
}
/**
* Resumes the countdown at the current step. Only makes sense if you called pauseCountdown() first.
*
* If you want to call this from a module, it's better to use the event system and cancel the onCountdownStep event.
*/
public static function resumeCountdown()
{
if (countdownTimer != null && !countdownTimer.finished)
{
countdownTimer.active = true;
}
}
/**
* Stops the countdown at the current step. You will have to restart it again later.
*
* If you want to call this from a module, it's better to use the event system and cancel the onCountdownStart event.
*/
public static function stopCountdown()
{
if (countdownTimer != null)
{
countdownTimer.cancel();
countdownTimer.destroy();
countdownTimer = null;
}
}
/**
* Stops the current countdown, then starts the song for you.
*/
public static function skipCountdown()
{
stopCountdown();
// This will trigger PlayState.startSong()
Conductor.songPosition = 0;
// PlayState.isInCountdown = false;
}
/**
* Resets the countdown. Only works if it's already running.
*/
public static function resetCountdown()
{
if (countdownTimer != null)
{
countdownTimer.reset();
}
}
/**
* Retrieves the graphic to use for this step of the countdown.
* TODO: Make this less dumb. Unhardcode it? Use modules? Use notestyles?
*
* This is public so modules can do lol funny shit.
*/
public static function showCountdownGraphic(index:CountdownStep, isPixelStyle:Bool):Void
{
var spritePath:String = null;
if (isPixelStyle)
{
switch (index)
{
case TWO:
spritePath = 'weeb/pixelUI/ready-pixel';
case ONE:
spritePath = 'weeb/pixelUI/set-pixel';
case GO:
spritePath = 'weeb/pixelUI/date-pixel';
default:
// null
}
}
else
{
switch (index)
{
case TWO:
spritePath = 'ready';
case ONE:
spritePath = 'set';
case GO:
spritePath = 'go';
default:
// null
}
}
if (spritePath == null)
return;
var countdownSprite:FlxSprite = new FlxSprite(0, 0).loadGraphic(Paths.image(spritePath));
countdownSprite.scrollFactor.set(0, 0);
if (isPixelStyle)
countdownSprite.setGraphicSize(Std.int(countdownSprite.width * Constants.PIXEL_ART_SCALE));
countdownSprite.updateHitbox();
countdownSprite.screenCenter();
// Fade sprite in, then out, then destroy it.
FlxTween.tween(countdownSprite, {y: countdownSprite.y += 100, alpha: 0}, Conductor.crochet / 1000, {
ease: FlxEase.cubeInOut,
onComplete: function(twn:FlxTween)
{
countdownSprite.destroy();
}
});
PlayState.instance.add(countdownSprite);
}
/**
* Retrieves the sound file to use for this step of the countdown.
* TODO: Make this less dumb. Unhardcode it? Use modules? Use notestyles?
*
* This is public so modules can do lol funny shit.
*/
public static function playCountdownSound(index:CountdownStep, isPixelStyle:Bool):Void
{
var soundPath:String = null;
if (isPixelStyle)
{
switch (index)
{
case THREE:
soundPath = 'intro3-pixel';
case TWO:
soundPath = 'intro2-pixel';
case ONE:
soundPath = 'intro1-pixel';
case GO:
soundPath = 'introGo-pixel';
default:
// null
}
}
else
{
switch (index)
{
case THREE:
soundPath = 'intro3';
case TWO:
soundPath = 'intro2';
case ONE:
soundPath = 'intro1';
case GO:
soundPath = 'introGo';
default:
// null
}
}
if (soundPath == null)
return;
FlxG.sound.play(Paths.sound(soundPath), Constants.COUNTDOWN_VOLUME);
}
public static function decrement(step:CountdownStep):CountdownStep
{
switch (step)
{
case BEFORE:
return THREE;
case THREE:
return TWO;
case TWO:
return ONE;
case ONE:
return GO;
case GO:
return AFTER;
default:
return AFTER;
}
}
}
/**
* The countdown step.
* This can't be an enum abstract because scripts may need it.
*/
enum CountdownStep
{
BEFORE;
THREE;
TWO;
ONE;
GO;
AFTER;
}

View file

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

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,6 @@
package funkin.play;
/**
* A static class which holds any functions related to scoring.
*/
class Scoring {}

View file

@ -0,0 +1,253 @@
package funkin.play;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import flixel.FlxSprite;
import flixel.math.FlxPoint;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import funkin.Note.NoteColor;
import funkin.Note.NoteDir;
import funkin.Note.NoteType;
import funkin.ui.PreferencesMenu;
import funkin.util.Constants;
/**
* A group controlling the individual notes of the strumline for a given player.
*
* FUN FACT: Setting the X and Y of a FlxSpriteGroup will move all the sprites in the group.
*/
class Strumline extends FlxTypedSpriteGroup<StrumlineArrow>
{
/**
* The style of the strumline.
* Options are normal and pixel.
*/
var style:StrumlineStyle;
/**
* The player this strumline belongs to.
* 0 is Player 1, etc.
*/
var playerId:Int;
/**
* The number of notes in the strumline.
*/
var size:Int;
public function new(playerId:Int = 0, style:StrumlineStyle = NORMAL, size:Int = 4)
{
super(0);
this.playerId = playerId;
this.style = style;
this.size = size;
generateStrumline();
}
function generateStrumline():Void
{
for (index in 0...size)
{
createStrumlineArrow(index);
}
}
function createStrumlineArrow(index:Int):Void
{
var arrow:StrumlineArrow = new StrumlineArrow(index, style);
add(arrow);
}
/**
* Apply a small animation which moves the arrow down and fades it in.
* Only plays at the start of Free Play songs.
*
* Note that modifying the offset of the whole strumline won't have the
* @param arrow The arrow to animate.
* @param index The index of the arrow in the strumline.
*/
function fadeInArrow(arrow:FlxSprite):Void
{
arrow.y -= 10;
arrow.alpha = 0;
FlxTween.tween(arrow, {y: arrow.y + 10, alpha: 1}, 1, {ease: FlxEase.circOut, startDelay: 0.5 + (0.2 * arrow.ID)});
}
public function fadeInArrows():Void
{
for (arrow in this.members)
{
fadeInArrow(arrow);
}
}
function updatePositions()
{
for (arrow in members)
{
arrow.x = Note.swagWidth * arrow.ID;
arrow.x += offset.x;
arrow.y = 0;
arrow.y += offset.y;
}
}
/**
* Retrieves the arrow at the given position in the strumline.
* @param index The index to retrieve.
* @return The corresponding FlxSprite.
*/
public inline function getArrow(value:Int):StrumlineArrow
{
// members maintains the order that the arrows were added.
return this.members[value];
}
public inline function getArrowByNoteType(value:NoteType):StrumlineArrow
{
return getArrow(value.int);
}
public inline function getArrowByNoteDir(value:NoteDir):StrumlineArrow
{
return getArrow(value.int);
}
public inline function getArrowByNoteColor(value:NoteColor):StrumlineArrow
{
return getArrow(value.int);
}
/**
* Get the default Y offset of the strumline.
* @return Int
*/
public static inline function getYPos():Int
{
return PreferencesMenu.getPref('downscroll') ? (FlxG.height - 150) : 50;
}
}
class StrumlineArrow extends FlxSprite
{
var style:StrumlineStyle;
public function new(id:Int, style:StrumlineStyle)
{
super(0, 0);
this.ID = id;
this.style = style;
// TODO: Unhardcode this. Maybe use a note style system>
switch (style)
{
case PIXEL:
buildPixelGraphic();
case NORMAL:
buildNormalGraphic();
}
this.updateHitbox();
scrollFactor.set(0, 0);
animation.play('static');
}
public function playAnimation(anim:String, force:Bool = false)
{
animation.play(anim, force);
centerOffsets();
centerOrigin();
}
/**
* Applies the default note style to an arrow.
*/
function buildNormalGraphic():Void
{
this.frames = Paths.getSparrowAtlas('NOTE_assets');
this.animation.addByPrefix('green', 'arrowUP');
this.animation.addByPrefix('blue', 'arrowDOWN');
this.animation.addByPrefix('purple', 'arrowLEFT');
this.animation.addByPrefix('red', 'arrowRIGHT');
this.setGraphicSize(Std.int(this.width * 0.7));
this.antialiasing = true;
this.x += Note.swagWidth * this.ID;
switch (Math.abs(this.ID))
{
case 0:
this.animation.addByPrefix('static', 'arrow static instance 1');
this.animation.addByPrefix('pressed', 'left press', 24, false);
this.animation.addByPrefix('confirm', 'left confirm', 24, false);
case 1:
this.animation.addByPrefix('static', 'arrow static instance 2');
this.animation.addByPrefix('pressed', 'down press', 24, false);
this.animation.addByPrefix('confirm', 'down confirm', 24, false);
case 2:
this.animation.addByPrefix('static', 'arrow static instance 4');
this.animation.addByPrefix('pressed', 'up press', 24, false);
this.animation.addByPrefix('confirm', 'up confirm', 24, false);
case 3:
this.animation.addByPrefix('static', 'arrow static instance 3');
this.animation.addByPrefix('pressed', 'right press', 24, false);
this.animation.addByPrefix('confirm', 'right confirm', 24, false);
}
}
/**
* Applies the pixel note style to an arrow.
*/
function buildPixelGraphic():Void
{
this.loadGraphic(Paths.image('weeb/pixelUI/arrows-pixels'), true, 17, 17);
this.animation.add('purplel', [4]);
this.animation.add('blue', [5]);
this.animation.add('green', [6]);
this.animation.add('red', [7]);
this.setGraphicSize(Std.int(this.width * Constants.PIXEL_ART_SCALE));
this.updateHitbox();
// Forcibly disable anti-aliasing on pixel graphics to stop blur.
this.antialiasing = false;
this.x += Note.swagWidth * this.ID;
// TODO: Seems weird that these are hardcoded like this... no XML?
switch (Math.abs(this.ID))
{
case 0:
this.animation.add('static', [0]);
this.animation.add('pressed', [4, 8], 12, false);
this.animation.add('confirm', [12, 16], 24, false);
case 1:
this.animation.add('static', [1]);
this.animation.add('pressed', [5, 9], 12, false);
this.animation.add('confirm', [13, 17], 24, false);
case 2:
this.animation.add('static', [2]);
this.animation.add('pressed', [6, 10], 12, false);
this.animation.add('confirm', [14, 18], 12, false);
case 3:
this.animation.add('static', [3]);
this.animation.add('pressed', [7, 11], 12, false);
this.animation.add('confirm', [15, 19], 24, false);
}
}
}
/**
* TODO: Unhardcode this and make it part of the note style system.
*/
enum StrumlineStyle
{
NORMAL;
PIXEL;
}

View file

@ -0,0 +1,103 @@
package funkin.play;
import flixel.FlxSprite;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.tweens.FlxTween;
import flixel.util.FlxColor;
import flixel.util.FlxTimer;
/**
* Static methods for playing cutscenes in the PlayState.
* TODO: Softcode this shit!!!!!1!
*/
class VanillaCutscenes
{
public static function playUghCutscene():Void
{
playVideoCutscene('music/ughCutscene.mp4');
}
public static function playGunsCutscene():Void
{
playVideoCutscene('music/gunsCutscene.mp4');
}
public static function playStressCutscene():Void
{
playVideoCutscene('music/stressCutscene.mp4');
}
static var blackScreen:FlxSprite;
/**
* Plays a cutscene from a video file, then starts the countdown once the video is done.
* TODO: Cutscene is currently skipped on native platforms.
*/
static function playVideoCutscene(path:String):Void
{
PlayState.isInCutscene = true;
PlayState.instance.camHUD.visible = false;
blackScreen = new FlxSprite(-200, -200).makeGraphic(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK);
blackScreen.scrollFactor.set(0, 0);
PlayState.instance.add(blackScreen);
#if html5
var vid:FlxVideo = new FlxVideo(path);
vid.finishCallback = finishVideoCutscene;
#else
finishVideoCutscene();
#end
}
/**
* Does the cleanup to start the countdown after the video is done.
* Gets called immediately if the video can't be played.
*/
static function finishVideoCutscene():Void
{
PlayState.instance.remove(blackScreen);
blackScreen = null;
FlxTween.tween(FlxG.camera, {zoom: PlayState.defaultCameraZoom}, (Conductor.crochet / 1000) * 5, {ease: FlxEase.quadInOut});
@:privateAccess
PlayState.instance.startCountdown();
@:privateAccess
PlayState.instance.cameraMovement();
}
public static function playHorrorStartCutscene()
{
PlayState.isInCutscene = true;
PlayState.instance.camHUD.visible = false;
blackScreen = new FlxSprite(-200, -200).makeGraphic(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK);
blackScreen.scrollFactor.set(0, 0);
PlayState.instance.add(blackScreen);
new FlxTimer().start(0.1, function(tmr:FlxTimer)
{
PlayState.instance.remove(blackScreen);
FlxG.sound.play(Paths.sound('Lights_Turn_On'));
PlayState.instance.cameraFollowPoint.y = -2050;
PlayState.instance.cameraFollowPoint.x += 200;
FlxG.camera.focusOn(PlayState.instance.cameraFollowPoint.getPosition());
FlxG.camera.zoom = 1.5;
new FlxTimer().start(0.8, function(tmr:FlxTimer)
{
PlayState.instance.camHUD.visible = true;
PlayState.instance.remove(blackScreen);
blackScreen = null;
FlxTween.tween(FlxG.camera, {zoom: PlayState.defaultCameraZoom}, 2.5, {
ease: FlxEase.quadInOut,
onComplete: function(twn:FlxTween)
{
Countdown.performCountdown(false);
}
});
});
});
}
}

View file

@ -1,15 +1,18 @@
package funkin.play.stage;
import flixel.FlxSprite;
import funkin.modding.IScriptedClass.IPlayStateScriptedClass;
import funkin.modding.events.ScriptEvent;
/**
* A Bopper is a stage prop which plays a dance animation.
* Y'know, a thingie that bops. A bopper.
*/
class Bopper extends FlxSprite
class Bopper extends FlxSprite implements IPlayStateScriptedClass
{
/**
* The bopper plays the dance animation once every `danceEvery` beats.
* Set to 0 to disable idle animation.
*/
public var danceEvery:Int = 1;
@ -23,16 +26,14 @@ class Bopper extends FlxSprite
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;
override function set_x(value:Float):Float
{
this.x = value + this.xOffset;
return value;
}
public var animationOffsets:Map<String, Array<Float>> = new Map<String, Array<Float>>();
/**
* Add a suffix to the `idle` animation (or `danceLeft` and `danceRight` animations)
* that this bopper will play.
*/
public var idleSuffix(default, set):String = "";
function set_idleSuffix(value:String):String
@ -43,14 +44,26 @@ class Bopper extends FlxSprite
}
/**
* 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;
return value;
if (animOffsets == null)
animOffsets = [0, 0];
if (animOffsets == value)
return value;
var xDiff = animOffsets[0] - value[0];
var yDiff = animOffsets[1] - value[1];
this.x += xDiff;
this.y += yDiff;
return animOffsets = value;
}
/**
@ -67,7 +80,7 @@ class Bopper extends FlxSprite
function update_shouldAlternate():Void
{
if (this.animation.getByName('danceLeft') != null)
if (hasAnimation('danceLeft'))
{
this.shouldAlternate = true;
}
@ -76,18 +89,18 @@ class Bopper extends FlxSprite
/**
* Called once every beat of the song.
*/
public function onBeatHit(curBeat:Int):Void
public function onBeatHit(event:SongTimeScriptEvent):Void
{
if (curBeat % danceEvery == 0)
if (danceEvery > 0 && event.beat % danceEvery == 0)
{
dance();
dance(true);
}
}
/**
* Called every `danceEvery` beats of the song.
*/
public function dance():Void
public function dance(force:Bool = false):Void
{
if (this.animation == null)
{
@ -103,17 +116,146 @@ class Bopper extends FlxSprite
{
if (hasDanced)
{
this.animation.play('danceRight$idleSuffix');
playAnimation('danceRight$idleSuffix', true);
}
else
{
this.animation.play('danceLeft$idleSuffix');
playAnimation('danceLeft$idleSuffix', true);
}
hasDanced = !hasDanced;
}
else
{
this.animation.play('idle$idleSuffix');
playAnimation('idle$idleSuffix', true);
}
}
public function hasAnimation(id:String):Bool
{
if (this.animation == null)
return false;
return this.animation.getByName(id) != null;
}
/**
* Ensure that a given animation exists before playing it.
* Will gracefully check for name, then name with stripped suffixes, then 'idle', then fail to play.
* @param name
*/
function correctAnimationName(name:String)
{
// If the animation exists, we're good.
if (hasAnimation(name))
return name;
trace('[BOPPER] Animation "$name" does not exist!');
// Attempt to strip a `-alt` suffix, if it exists.
if (name.lastIndexOf('-') != -1)
{
var correctName = name.substring(0, name.lastIndexOf('-'));
trace('[BOPPER] Attempting to fallback to "$correctName"');
return correctAnimationName(correctName);
}
else
{
if (name != 'idle')
{
trace('[BOPPER] Attempting to fallback to "idle"');
return correctAnimationName('idle');
}
else
{
trace('[BOPPER] Failing animation playback.');
return null;
}
}
}
/**
* @param name The name of the animation to play.
* @param restart Whether to restart the animation if it is already playing.
*/
public function playAnimation(name:String, restart:Bool = false):Void
{
var correctName = correctAnimationName(name);
if (correctName == null)
return;
this.animation.play(correctName, restart, false, 0);
applyAnimationOffsets(correctName);
}
function applyAnimationOffsets(name:String)
{
var offsets = animationOffsets.get(name);
if (offsets != null)
{
this.animOffsets = [offsets[0] + globalOffsets[0], offsets[1] + globalOffsets[1]];
}
else
{
this.animOffsets = globalOffsets;
}
}
public function isAnimationFinished():Bool
{
return this.animation.finished;
}
public function setAnimationOffsets(name:String, xOffset:Float, yOffset:Float):Void
{
animationOffsets.set(name, [xOffset, yOffset]);
}
/**
* Returns the name of the animation that is currently playing.
* If no animation is playing (usually this means the character is BROKEN!),
* returns an empty string to prevent NPEs.
*/
public function getCurrentAnimation():String
{
if (this.animation == null || this.animation.curAnim == null)
return "";
return this.animation.curAnim.name;
}
public function onScriptEvent(event:ScriptEvent) {}
public function onCreate(event:ScriptEvent) {}
public function onDestroy(event:ScriptEvent) {}
public function onUpdate(event:UpdateScriptEvent) {}
public function onPause(event:PauseScriptEvent) {}
public function onResume(event:ScriptEvent) {}
public function onSongStart(event:ScriptEvent) {}
public function onSongEnd(event:ScriptEvent) {}
public function onGameOver(event:ScriptEvent) {}
public function onNoteHit(event:NoteScriptEvent) {}
public function onNoteMiss(event:NoteScriptEvent) {}
public function onNoteGhostMiss(event:GhostMissNoteScriptEvent) {}
public function onStepHit(event:SongTimeScriptEvent) {}
public function onCountdownStart(event:CountdownScriptEvent) {}
public function onCountdownStep(event:CountdownScriptEvent) {}
public function onCountdownEnd(event:CountdownScriptEvent) {}
public function onSongLoaded(eent:SongLoadScriptEvent) {}
public function onSongRetry(event:ScriptEvent) {}
}

View file

@ -2,19 +2,22 @@ package funkin.play.stage;
import flixel.FlxSprite;
import flixel.group.FlxSpriteGroup;
import flixel.math.FlxPoint;
import flixel.util.FlxSort;
import funkin.modding.IHook;
import funkin.modding.IScriptedClass;
import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.play.character.Character.CharacterType;
import funkin.play.stage.StageData.StageDataParser;
import funkin.util.SortUtil;
import funkin.util.assets.FlxAnimationUtil;
/**
* A Stage is a group of objects rendered in the PlayState.
*
* A Stage is comprised of one or more props, each of which is a FlxSprite.
*/
class Stage extends FlxSpriteGroup implements IHook
class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScriptedClass
{
public final stageId:String;
public final stageName:String;
@ -50,16 +53,24 @@ class Stage extends FlxSpriteGroup implements IHook
}
}
/**
* Called when the player is moving into the PlayState where the song will be played.
*/
public function onCreate(event:ScriptEvent):Void
{
buildStage();
this.refresh();
}
/**
* The default stage construction routine. Called when the stage is going to be played in.
* Instantiates each prop and adds it to the stage, while setting its parameters.
*/
public function buildStage()
function buildStage()
{
trace('Building stage for display: ${this.stageId}');
this.camZoom = _data.cameraZoom;
// this.scrollFactor = new FlxPoint(1, 1);
for (dataProp in _data.props)
{
@ -130,19 +141,19 @@ class Stage extends FlxSpriteGroup implements IHook
for (propAnim in dataProp.animations)
{
propSprite.animation.add(propAnim.name, propAnim.frameIndices);
if (Std.isOfType(propSprite, Bopper))
{
cast(propSprite, Bopper).setAnimationOffsets(propAnim.name, propAnim.offsets[0], propAnim.offsets[1]);
}
}
default: // "sparrow"
for (propAnim in dataProp.animations)
FlxAnimationUtil.addAtlasAnimations(propSprite, dataProp.animations);
if (Std.isOfType(propSprite, Bopper))
{
if (propAnim.frameIndices.length == 0)
for (propAnim in dataProp.animations)
{
propSprite.animation.addByPrefix(propAnim.name, propAnim.prefix, propAnim.frameRate, propAnim.loop, propAnim.flipX,
propAnim.flipY);
}
else
{
propSprite.animation.addByIndices(propAnim.name, propAnim.prefix, propAnim.frameIndices, "", propAnim.frameRate, propAnim.loop,
propAnim.flipX, propAnim.flipY);
cast(propSprite, Bopper).setAnimationOffsets(propAnim.name, propAnim.offsets[0], propAnim.offsets[1]);
}
}
}
@ -162,8 +173,6 @@ class Stage extends FlxSpriteGroup implements IHook
}
trace(' Prop placed.');
}
this.refresh();
}
/**
@ -200,39 +209,6 @@ class Stage extends FlxSpriteGroup implements IHook
trace('Stage sorted by z-index');
}
/**
* Resets the stage and it's props (needs to be overridden with your own logic!)
*/
public function resetStage()
{
// Override me in your script to reset stage shit however you please!
// also note: maybe add some default behaviour to reset stage stuff?
}
/**
* A function that should get called every frame.
*/
public function onUpdate(elapsed:Float):Void
{
// Override me in your scripted stage to perform custom behavior!
}
/**
* A function that gets called when the player hits a note.
*/
public function onNoteHit(note:Note):Void
{
// Override me in your scripted stage to perform custom behavior!
}
/**
* A function that gets called when the player hits a note.
*/
public function onNoteMiss(note:Note):Void
{
// Override me in your scripted stage to perform custom behavior!
}
/**
* Adjusts the position and other properties of the soon-to-be child of this sprite group.
* Private helper to avoid duplicate code in `add()` and `insert()`.
@ -253,30 +229,6 @@ class Stage extends FlxSpriteGroup implements IHook
clipRectTransform(sprite, clipRect);
}
/**
* A function that gets called once per step in the song.
* @param curStep The current step number.
*/
public function onStepHit(curStep:Int):Void
{
// Override me in your scripted stage to perform custom behavior!
}
/**
* A function that gets called once per beat in the song (once every four steps).
* @param curStep The current beat number.
*/
public function onBeatHit(curBeat:Int):Void
{
// Override me in your scripted stage to perform custom behavior!
// Make sure to call super.onBeatHit(curBeat) if you want to keep the boppers dancing.
for (bopper in boppers)
{
bopper.onBeatHit(curBeat);
}
}
/**
* Used by the PlayState to add a character to the stage.
*/
@ -358,45 +310,108 @@ class Stage extends FlxSpriteGroup implements IHook
}
/**
* Perform cleanup for when you are leaving the level.
* onDestroy gets called when the player is leaving the PlayState,
* and is used to clean up any objects that need to be destroyed.
*/
public override function kill()
public function onDestroy(event:ScriptEvent):Void
{
super.kill();
// Make sure to call kill() when returning a stage to cache,
// and destroy() only when performing a hard cache refresh.
kill();
for (prop in this.namedProps)
{
prop.destroy();
if (prop != null)
{
remove(prop);
prop.kill();
prop.destroy();
}
}
namedProps.clear();
for (char in this.characters)
{
char.destroy();
if (char != null)
{
remove(char);
char.kill();
char.destroy();
}
}
characters.clear();
for (bopper in boppers)
{
bopper.destroy();
if (bopper != null)
{
remove(bopper);
bopper.kill();
bopper.destroy();
}
}
boppers = [];
for (sprite in this.group)
{
sprite.destroy();
if (sprite != null)
{
sprite.kill();
sprite.destroy();
remove(sprite);
}
}
group.clear();
}
/**
* Perform cleanup for when you are destroying the stage
* and removing all its data from cache.
*
* Call this ONLY when you are performing a hard cache clear.
* A function that gets called once per step in the song.
* @param curStep The current step number.
*/
public override function destroy()
public function onStepHit(event:SongTimeScriptEvent):Void {}
/**
* A function that gets called once per beat in the song (once every four steps).
* @param curStep The current beat number.
*/
public function onBeatHit(event:SongTimeScriptEvent):Void
{
super.destroy();
// Override me in your scripted stage to perform custom behavior!
// Make sure to call super.onBeatHit(curBeat) if you want to keep the boppers dancing.
for (bopper in boppers)
{
ScriptEventDispatcher.callEvent(bopper, event);
}
}
public function onScriptEvent(event:ScriptEvent) {}
public function onPause(event:PauseScriptEvent) {}
public function onResume(event:ScriptEvent) {}
public function onSongStart(event:ScriptEvent) {}
public function onSongEnd(event:ScriptEvent) {}
public function onGameOver(event:ScriptEvent) {}
public function onCountdownStart(event:CountdownScriptEvent) {}
public function onCountdownStep(event:CountdownScriptEvent) {}
public function onCountdownEnd(event:CountdownScriptEvent) {}
public function onUpdate(event:UpdateScriptEvent) {}
public function onNoteHit(event:NoteScriptEvent) {}
public function onNoteMiss(event:NoteScriptEvent) {}
public function onNoteGhostMiss(event:GhostMissNoteScriptEvent) {}
public function onSongLoaded(eent:SongLoadScriptEvent) {}
public function onSongRetry(event:ScriptEvent) {}
}

View file

@ -1,9 +1,10 @@
package funkin.play.stage;
import openfl.Assets;
import flixel.util.typeLimit.OneOfTwo;
import funkin.util.VersionUtil;
import funkin.util.assets.DataAssets;
import haxe.Json;
import flixel.util.typeLimit.OneOfTwo;
import openfl.Assets;
using StringTools;
@ -17,7 +18,12 @@ class StageDataParser
* Handle breaking changes by incrementing this value
* and adding migration to the `migrateStageData()` function.
*/
public static final STAGE_DATA_VERSION:String = "1.0";
public static final STAGE_DATA_VERSION:String = "1.0.0";
/**
* The current version rule check for the stage data format.
*/
public static final STAGE_DATA_VERSION_RULE:String = "1.0.x";
static final stageCache:Map<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_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 = {
zIndex: DEFAULT_ZINDEX,
position: DEFAULT_POSITION,
cameraOffsets: DEFAULT_OFFSETS,
}
/**
@ -194,12 +201,18 @@ class StageDataParser
return null;
}
if (input.version != STAGE_DATA_VERSION)
if (input.version == null)
{
trace('[STAGEDATA] ERROR: Could not load stage data for "$id": missing version');
return null;
}
if (!VersionUtil.validateVersion(input.version, STAGE_DATA_VERSION_RULE))
{
trace('[STAGEDATA] ERROR: Could not load stage data for "$id": bad version (got ${input.version}, expected ${STAGE_DATA_VERSION_RULE})');
return null;
}
if (input.name == null)
{
trace('[STAGEDATA] WARN: Stage data for "$id" missing name');
@ -211,10 +224,9 @@ class StageDataParser
input.cameraZoom = DEFAULT_CAMERAZOOM;
}
if (input.props == null || input.props.length == 0)
if (input.props == null)
{
trace('[STAGEDATA] ERROR: Could not load stage data for "$id": missing props');
return null;
input.props = [];
}
for (inputProp in input.props)
@ -296,14 +308,14 @@ class StageDataParser
inputAnimation.frameRate = 24;
}
if (inputAnimation.frameIndices == null)
if (inputAnimation.offsets == null)
{
inputAnimation.frameIndices = DEFAULT_FRAMEINDICES;
inputAnimation.offsets = DEFAULT_OFFSETS;
}
if (inputAnimation.loop == null)
if (inputAnimation.looped == null)
{
inputAnimation.loop = true;
inputAnimation.looped = true;
}
if (inputAnimation.flipX == null)
@ -347,6 +359,10 @@ class StageDataParser
{
inputCharacter.position = [0, 0];
}
if (inputCharacter.cameraOffsets == null || inputCharacter.cameraOffsets.length != 2)
{
inputCharacter.cameraOffsets = [0, 0];
}
}
// All good!
@ -356,8 +372,12 @@ class StageDataParser
typedef StageData =
{
// Uses semantic versioning.
/**
* The sematic version number of the stage data JSON format.
* Supports fancy comparisons like NPM does it's neat.
*/
var version:String;
var name:String;
var cameraZoom:Null<Float>;
var props:Array<StageDataProp>;
@ -432,7 +452,7 @@ typedef StageDataProp =
* An optional array of animations which the prop can play.
* @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.
@ -448,52 +468,6 @@ typedef StageDataProp =
var animType:String;
};
typedef StageDataPropAnimation =
{
/**
* The name of the animation.
*/
var name:String;
/**
* The common beginning of image names in atlas for this animation's frames.
* For example, if the frames are named "test0001.png", "test0002.png", etc., use "test".
*/
var prefix:String;
/**
* If you want this animation to use only certain frames of an animation with a given prefix,
* select them here.
* @example [0, 1, 2, 3] (use only the first four frames)
* @default [] (all frames)
*/
var frameIndices:Array<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 =
{
/**
@ -505,5 +479,12 @@ typedef StageDataCharacter =
/**
* 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;
import flixel.FlxSprite;
import flixel.group.FlxSpriteGroup;
import flixel.graphics.frames.FlxAtlasFrames;
import flixel.group.FlxSpriteGroup;
import flixel.util.FlxStringUtil;
@:forward
abstract BoldText(AtlasText) from AtlasText to AtlasText
{
inline public function new(x = 0.0, y = 0.0, text:String)
{
this = new AtlasText(x, y, text, Bold);
}
}
/**
* Alphabet.hx has a ton of bugs and does a bunch of stuff I don't need, fuck that class
* AtlasText is an improved version of Alphabet and FlxBitmapText.
* It supports animations on the letters, and is less buggy than Alphabet.
*/
class AtlasText extends FlxTypedSpriteGroup<AtlasChar>
{
@ -41,7 +33,7 @@ class AtlasText extends FlxTypedSpriteGroup<AtlasChar>
inline function get_maxHeight()
return font.maxHeight;
public function new(x = 0.0, y = 0.0, text:String, fontName:AtlasFont = Default)
public function new(x = 0.0, y = 0.0, text:String, fontName:AtlasFont = AtlasFont.DEFAULT)
{
if (!fonts.exists(fontName))
fonts[fontName] = new AtlasFontData(fontName);
@ -246,7 +238,14 @@ private class AtlasFontData
public function new(name:AtlasFont)
{
atlas = Paths.getSparrowAtlas("fonts/" + name.getName().toLowerCase());
var fontName:String = name;
atlas = Paths.getSparrowAtlas('fonts/${fontName.toLowerCase()}');
if (atlas == null)
{
FlxG.log.warn('Could not find font atlas for font "${fontName}".');
return;
}
atlas.parent.destroyOnNoUse = false;
atlas.parent.persist = true;
@ -276,8 +275,8 @@ enum Case
Lower;
}
enum AtlasFont
enum abstract AtlasFont(String) from String to String
{
Default;
Bold;
var DEFAULT = "default";
var BOLD = "bold";
}

View file

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

View file

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

View file

@ -1,5 +1,6 @@
package funkin.ui;
import funkin.util.Constants;
import flixel.FlxSprite;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.tweens.FlxTween;
@ -22,7 +23,7 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
var rating:FlxSprite = new FlxSprite();
var ratingPath:String = daRating;
if (PlayState.curStageId.startsWith('school'))
if (PlayState.instance.currentStageId.startsWith('school'))
ratingPath = "weeb/pixelUI/" + ratingPath + "-pixel";
rating.loadGraphic(Paths.image(ratingPath));
@ -40,9 +41,9 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
add(rating);
if (PlayState.curStageId.startsWith('school'))
if (PlayState.instance.currentStageId.startsWith('school'))
{
rating.setGraphicSize(Std.int(rating.width * PlayState.daPixelZoom * 0.7));
rating.setGraphicSize(Std.int(rating.width * Constants.PIXEL_ART_SCALE * 0.7));
}
else
{
@ -69,7 +70,7 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
var pixelShitPart1:String = "";
var pixelShitPart2:String = '';
if (PlayState.curStageId.startsWith('school'))
if (PlayState.instance.currentStageId.startsWith('school'))
{
pixelShitPart1 = 'weeb/pixelUI/';
pixelShitPart2 = '-pixel';
@ -90,9 +91,9 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
add(comboSpr);
if (PlayState.curStageId.startsWith('school'))
if (PlayState.instance.currentStageId.startsWith('school'))
{
comboSpr.setGraphicSize(Std.int(comboSpr.width * PlayState.daPixelZoom * 0.7));
comboSpr.setGraphicSize(Std.int(comboSpr.width * Constants.PIXEL_ART_SCALE * 0.7));
}
else
{
@ -129,9 +130,9 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
var numScore:FlxSprite = new FlxSprite().loadGraphic(Paths.image(pixelShitPart1 + 'num' + Std.int(i) + pixelShitPart2));
numScore.y = comboSpr.y;
if (PlayState.curStageId.startsWith('school'))
if (PlayState.instance.currentStageId.startsWith('school'))
{
numScore.setGraphicSize(Std.int(numScore.width * PlayState.daPixelZoom));
numScore.setGraphicSize(Std.int(numScore.width * Constants.PIXEL_ART_SCALE));
}
else
{

View file

@ -4,8 +4,8 @@ import flixel.FlxCamera;
import flixel.FlxObject;
import flixel.FlxSprite;
import funkin.ui.AtlasText.AtlasFont;
import funkin.ui.TextMenuList.TextMenuItem;
import funkin.ui.OptionsState.Page;
import funkin.ui.TextMenuList.TextMenuItem;
class PreferencesMenu extends Page
{
@ -84,7 +84,7 @@ class PreferencesMenu extends Page
private function createPrefItem(prefName:String, prefString:String, prefValue:Dynamic):Void
{
items.createItem(120, (120 * items.length) + 30, prefName, AtlasFont.Bold, function()
items.createItem(120, (120 * items.length) + 30, prefName, AtlasFont.BOLD, function()
{
preferenceCheck(prefString, prefValue);
@ -157,16 +157,17 @@ class PreferencesMenu extends Page
});
}
private static function preferenceCheck(prefString:String, prefValue:Dynamic):Void
private static function preferenceCheck(prefString:String, defaultValue:Dynamic):Void
{
if (preferences.get(prefString) == null)
{
preferences.set(prefString, prefValue);
trace('set preference!');
// Set the value to default.
preferences.set(prefString, defaultValue);
trace('Set preference to default: ${prefString} = ${defaultValue}');
}
else
{
trace('found preference: ' + preferences.get(prefString));
trace('Found preference: ${prefString} = ${preferences.get(prefString)}');
}
}
}

View file

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

View file

@ -10,7 +10,7 @@ class TextMenuList extends MenuTypedList<TextMenuItem>
super(navControls, wrapMode);
}
public function createItem(x = 0.0, y = 0.0, name:String, font:AtlasFont = Bold, callback, fireInstantly = false)
public function createItem(x = 0.0, y = 0.0, name:String, font:AtlasFont = BOLD, callback, fireInstantly = false)
{
var item = new TextMenuItem(x, y, name, font, callback);
item.fireInstantly = fireInstantly;
@ -20,7 +20,7 @@ class TextMenuList extends MenuTypedList<TextMenuItem>
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);
setEmptyBackground();

View file

@ -0,0 +1,39 @@
package funkin.util;
import flixel.util.FlxColor;
import lime.app.Application;
class Constants
{
/**
* The scale factor to use when increasing the size of pixel art graphics.
*/
public static final PIXEL_ART_SCALE = 6;
public static final HEALTH_BAR_RED:FlxColor = 0xFFFF0000;
public static final HEALTH_BAR_GREEN:FlxColor = 0xFF66FF33;
public static final COUNTDOWN_VOLUME = 0.6;
public static final VERSION_SUFFIX = ' PROTOTYPE';
public static var VERSION(get, null):String;
public static final FREAKY_MENU_BPM = 102;
#if debug
public static final GIT_HASH = funkin.util.macro.GitCommit.getGitCommitHash();
static function get_VERSION():String
{
return 'v${Application.current.meta.get('version')} (${GIT_HASH})' + VERSION_SUFFIX;
}
#else
static function get_VERSION():String
{
return 'v${Application.current.meta.get('version')}' + VERSION_SUFFIX;
}
#end
public static final URL_KICKSTARTER:String = "https://www.kickstarter.com/projects/funkin/friday-night-funkin-the-full-ass-game/";
public static final URL_ITCH:String = "https://ninja-muffin24.itch.io/funkin";
}

View file

@ -0,0 +1,16 @@
package funkin.util;
/**
* A static extension which provides utility functions for Iterators.
*
* For example, add `using IteratorTools` then call `iterator.array()`.
*
* @see https://haxe.org/manual/lf-static-extension.html
*/
class IteratorTools
{
public static function array<T>(iterator:Iterator<T>):Array<T>
{
return [for (i in iterator) i];
}
}

View file

@ -1,9 +1,8 @@
package funkin.util;
#if !macro
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.FlxBasic;
import flixel.util.FlxSort;
import flixel.FlxObject;
#end
class SortUtil
@ -12,8 +11,18 @@ class SortUtil
* You can use this function in FlxTypedGroup.sort() to sort FlxObjects by their z-index values.
* The value defaults to 0, but by assigning it you can easily rearrange objects as desired.
*/
public static inline function byZIndex(Order:Int, Obj1:FlxObject, Obj2:FlxObject):Int
public static inline function byZIndex(Order:Int, Obj1:FlxBasic, Obj2:FlxBasic):Int
{
return FlxSort.byValues(Order, Obj1.zIndex, Obj2.zIndex);
}
/**
* Given two Notes, returns 1 or -1 based on whether `a` or `b` has an earlier strumtime.
*
* @param order Either `FlxSort.ASCENDING` or `FlxSort.DESCENDING`
*/
public static inline function byStrumtime(order:Int, a:Note, b:Note)
{
return FlxSort.byValues(order, a.data.strumTime, b.data.strumTime);
}
}

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

View file

@ -0,0 +1,35 @@
package funkin.util.macro;
#if debug
class GitCommit
{
public static macro function getGitCommitHash():haxe.macro.Expr.ExprOf<String>
{
#if !display
// Get the current line number.
var pos = haxe.macro.Context.currentPos();
var process = new sys.io.Process('git', ['rev-parse', 'HEAD']);
if (process.exitCode() != 0)
{
var message = process.stderr.readAll().toString();
haxe.macro.Context.info('[WARN] Could not determine current git commit; is this a proper Git repository?', pos);
}
// read the output of the process
var commitHash:String = process.stdout.readLine();
var commitHashSplice:String = commitHash.substr(0, 7);
trace('Git Commit ID ${commitHashSplice}');
// Generates a string expression
return macro $v{commitHashSplice};
#else
// `#if display` is used for code completion. In this case returning an
// empty string is good enough; We don't want to call git on every hint.
var commitHash:String = "";
return macro $v{commitHashSplice};
#end
}
}
#end