1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2025-03-21 01:19:26 +00:00

Scripted module and script event systems, reworked strumlines.

This commit is contained in:
Eric Myllyoja 2022-03-11 01:30:01 -05:00
parent fe109f878f
commit 49b2907a30
32 changed files with 1992 additions and 1047 deletions

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

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

@ -538,7 +538,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

@ -20,12 +20,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 +36,7 @@ class GameOverSubstate extends MusicBeatSubstate
daBf = 'bf';
}
var daSong = PlayState.SONG.song.toLowerCase();
var daSong = PlayState.currentSong.song.toLowerCase();
switch (daSong)
{
@ -48,7 +48,9 @@ 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);
@ -57,7 +59,7 @@ class GameOverSubstate extends MusicBeatSubstate
FlxG.sound.play(Paths.sound('fnf_loss_sfx' + stageSuffix));
// Conductor.changeBPM(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

@ -1,5 +1,6 @@
package funkin;
import funkin.modding.module.ModuleHandler;
import funkin.play.stage.StageData;
import funkin.charting.ChartingState;
import flixel.addons.transition.FlxTransitionSprite.GraphicTransTileDiamond;
@ -122,6 +123,8 @@ class InitState extends FlxTransitionableState
StageDataParser.loadStageCache();
ModuleHandler.loadModuleCache();
#if song
var song = getSong();
@ -186,7 +189,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

@ -57,9 +57,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
@ -173,12 +173,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 +198,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,5 +1,7 @@
package funkin;
import funkin.modding.events.ScriptEvent.UpdateScriptEvent;
import funkin.modding.module.ModuleHandler;
import funkin.NGio;
import flixel.FlxObject;
import flixel.FlxSprite;
@ -272,6 +274,8 @@ class MainMenuState extends MusicBeatState
override function update(elapsed:Float)
{
super.update(elapsed);
if (FlxG.onMobile)
{
var touch:FlxTouch = FlxG.touches.getFirst();
@ -307,7 +311,8 @@ class MainMenuState extends MusicBeatState
FlxG.switchState(new TitleState());
}
super.update(elapsed);
var event:UpdateScriptEvent = new UpdateScriptEvent(elapsed);
ModuleHandler.callEvent(event);
}
}

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

@ -51,7 +51,7 @@ class PauseSubState extends MusicBeatSubstate
add(bg);
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();
@ -76,7 +76,7 @@ class PauseSubState extends MusicBeatSubstate
practiceText.setFormat(Paths.font('vcr.ttf'), 32);
practiceText.updateHitbox();
practiceText.x = FlxG.width - (practiceText.width + 20);
practiceText.visible = PlayState.practiceMode;
practiceText.visible = PlayState.isPracticeMode;
add(practiceText);
levelDifficulty.alpha = 0;
@ -157,7 +157,7 @@ class PauseSubState extends MusicBeatSubstate
case "Resume":
close();
case "EASY" | 'NORMAL' | "HARD":
PlayState.SONG = SongLoad.loadFromJson(PlayState.SONG.song.toLowerCase(), PlayState.SONG.song.toLowerCase());
PlayState.currentSong = SongLoad.loadFromJson(PlayState.currentSong.song.toLowerCase(), PlayState.currentSong.song.toLowerCase());
SongLoad.curDiff = daSelected.toLowerCase();
PlayState.storyDifficulty = curSelected;
@ -167,8 +167,8 @@ class PauseSubState extends MusicBeatSubstate
close();
case 'Toggle Practice Mode':
PlayState.practiceMode = !PlayState.practiceMode;
practiceText.visible = PlayState.practiceMode;
PlayState.isPracticeMode = !PlayState.isPracticeMode;
practiceText.visible = PlayState.isPracticeMode;
case 'Change Difficulty':
menuItems = difficultyChoices;

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

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

@ -3,7 +3,6 @@ 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.
*/
@:hscript({

View file

@ -0,0 +1,50 @@
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 that involve player input.
*/
interface IInputScriptedClass extends IScriptedClass
{
public function onKeyDown(event:KeyboardInputScriptEvent):Void;
public function onKeyUp(event:KeyboardInputScriptEvent):Void;
}
/**
* 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:ScriptEvent):Void;
public function onResume(event:ScriptEvent):Void;
public function onSongStart(event:ScriptEvent):Void;
public function onSongEnd(event:ScriptEvent):Void;
public function onSongReset(event:ScriptEvent):Void;
public function onGameOver(event:ScriptEvent):Void;
public function onGameRetry(event:ScriptEvent):Void;
public function onNoteHit(event:NoteScriptEvent):Void;
public function onNoteMiss(event:NoteScriptEvent):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;
}

View file

@ -0,0 +1,327 @@
package funkin.modding.events;
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 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 song is reset. This can happen from the pause menu or the game over screen.
*
* This event is not cancelable.
*/
public static inline final SONG_RESET:ScriptEventType = "SONG_RESET";
/**
* Called when the countdown begins. This occurs before the song starts.
*
* This event IS cancelable! Canceling this event will prevent the countdown from starting.
* - The song will not start until you call PlayState.beginCountdown().
* - Note that calling startCountdown() 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 game over screen triggers and the death animation plays.
*
* This event is not cancelable.
*/
public static inline final GAME_OVER:ScriptEventType = "GAME_OVER";
/**
* Called when the player presses a key to restart the game after the death animation.
*
* This event IS cancelable! Canceling this event will prevent the game from restarting.
*/
public static inline final GAME_RETRY:ScriptEventType = "GAME_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";
/**
* If true, the behavior associated with this event can be prevented.
* For example, cancelling COUNTDOWN_BEGIN 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;
@:noCompletion private var __eventCanceled: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_BEGIN will prevent the countdown from starting.
*/
public function cancelEvent():Void
{
if (cancelable)
{
__eventCanceled = true;
}
}
/**
* 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 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, false);
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):Void
{
super(type, false);
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 + ')';
}
}

View file

@ -0,0 +1,127 @@
package funkin.modding.events;
import funkin.modding.IScriptedClass;
import funkin.modding.IScriptedClass.IInputScriptedClass;
import funkin.modding.IScriptedClass.IPlayStateScriptedClass;
/**
* Utility functions to assist with handling scripted classes.
*/
class ScriptEventDispatcher
{
public static function callEvent(target:IScriptedClass, event:ScriptEvent):Void
{
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, IInputScriptedClass))
{
var t = cast(target, IInputScriptedClass);
switch (event.type)
{
case ScriptEvent.KEY_DOWN:
t.onKeyDown(cast event);
return;
case ScriptEvent.KEY_UP:
t.onKeyUp(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.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_RESET:
t.onSongReset(event);
return;
case ScriptEvent.PAUSE:
t.onPause(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;
}
}
throw "No helper 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,87 @@
package funkin.modding.module;
import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEvent.UpdateScriptEvent;
import funkin.modding.events.ScriptEvent.KeyboardInputScriptEvent;
import funkin.modding.events.ScriptEvent.NoteScriptEvent;
import funkin.modding.events.ScriptEvent.SongTimeScriptEvent;
import funkin.modding.events.ScriptEvent.CountdownScriptEvent;
import funkin.modding.IScriptedClass.IPlayStateScriptedClass;
import funkin.modding.IScriptedClass.IInputScriptedClass;
/**
* 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 IInputScriptedClass implements IPlayStateScriptedClass
{
/**
* Whether the module is currently active.
*/
public var active(default, set):Bool = false;
function set_active(value:Bool):Bool
{
this.active = value;
return value;
}
public var moduleId(default, null):String = 'UNKNOWN';
/**
* Called when the module is initialized.
* It may not be safe to reference other modules here since they may not be loaded yet.
*
* @param startActive Whether to start with the module active.
* If false, the module will be inactive and must be enabled by another script,
* such as a stage or another module.
*/
public function new(moduleId:String, startActive:Bool)
{
this.moduleId = moduleId;
this.active = startActive;
}
public function toString()
{
return 'Module(' + this.moduleId + ')';
}
public function onScriptEvent(event:ScriptEvent) {}
public function onCreate(event:ScriptEvent) {}
public function onDestroy(event:ScriptEvent) {}
public function onUpdate(event:UpdateScriptEvent) {}
public function onKeyDown(event:KeyboardInputScriptEvent) {}
public function onKeyUp(event:KeyboardInputScriptEvent) {}
public function onPause(event:ScriptEvent) {}
public function onResume(event:ScriptEvent) {}
public function onSongStart(event:ScriptEvent) {}
public function onSongEnd(event:ScriptEvent) {}
public function onSongReset(event:ScriptEvent) {}
public function onGameOver(event:ScriptEvent) {}
public function onGameRetry(event:ScriptEvent) {}
public function onNoteHit(event:NoteScriptEvent) {}
public function onNoteMiss(event:NoteScriptEvent) {}
public function onStepHit(event:SongTimeScriptEvent) {}
public function onBeatHit(event:SongTimeScriptEvent) {}
public function onCountdownStart(event:CountdownScriptEvent) {}
public function onCountdownStep(event:CountdownScriptEvent) {}
}

View file

@ -0,0 +1,67 @@
package funkin.modding.module;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEvent.UpdateScriptEvent;
/**
* Utility functions for loading and manipulating active modules.
*/
class ModuleHandler
{
static final moduleCache:Map<String, Module> = new Map<String, Module>();
/**
* Whether modules start active by default.
*/
static final DEFAULT_STARTACTIVE:Bool = true;
/**
* 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, DEFAULT_STARTACTIVE);
if (module != null)
{
trace(' Loaded module: ${moduleCls}');
// Then store it.
moduleCache.set(module.moduleId, module);
}
else
{
trace(' Failed to instantiate module: ${moduleCls}');
}
}
trace("[MODULEHANDLER] Module cache loaded.");
}
public static function clearModuleCache():Void
{
if (moduleCache != null)
{
// for (module in moduleCache)
// {
// module.destroy();
// }
moduleCache.clear();
}
}
public static function callEvent(event:ScriptEvent):Void
{
ScriptEventDispatcher.callEventOnAllTargets(moduleCache.iterator(), 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,11 @@
package funkin.play;
enum abstract CountdownStep(String) from String to String
{
var BEFORE = "BEFORE";
var THREE = "THREE";
var TWO = "TWO";
var ONE = "ONE";
var GO = "GO";
var AFTER = "AFTER";
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,245 @@
package funkin.play;
import funkin.ui.PreferencesMenu;
import funkin.Note.NoteColor;
import funkin.Note.NoteDir;
import funkin.Note.NoteType;
import flixel.tweens.FlxTween;
import flixel.tweens.FlxEase;
import funkin.util.Constants;
import flixel.FlxSprite;
import flixel.math.FlxPoint;
import flixel.group.FlxGroup.FlxTypedGroup;
/**
* A group controlling the individual notes of the strumline for a given player.
*/
class Strumline extends FlxTypedGroup<FlxSprite>
{
public var offset(default, set):FlxPoint = new FlxPoint(0, 0);
function set_offset(value:FlxPoint):FlxPoint
{
this.offset = value;
updatePositions();
return value;
}
/**
* 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:FlxSprite = new FlxSprite(0, 0);
arrow.ID = index;
// Color changing for arrows is a WIP.
/*
var colorSwapShader:ColorSwap = new ColorSwap();
colorSwapShader.update(Note.arrowColors[i]);
arrow.shader = colorSwapShader;
*/
switch (style)
{
case NORMAL:
createNormalNote(arrow);
case PIXEL:
createPixelNote(arrow);
}
arrow.updateHitbox();
arrow.scrollFactor.set();
arrow.animation.play('static');
applyFadeIn(arrow);
add(arrow);
}
/**
* Apply a small animation which moves the arrow down and fades it in.
* Only plays at the start of Free Play songs I guess?
* @param arrow The arrow to animate.
* @param index The index of the arrow in the strumline.
*/
function applyFadeIn(arrow:FlxSprite):Void
{
if (!PlayState.isStoryMode)
{
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)});
}
}
/**
* Applies the default note style to an arrow.
* @param arrow The arrow to apply the style to.
* @param index The index of the arrow in the strumline.
*/
function createNormalNote(arrow:FlxSprite):Void
{
arrow.frames = Paths.getSparrowAtlas('NOTE_assets');
arrow.animation.addByPrefix('green', 'arrowUP');
arrow.animation.addByPrefix('blue', 'arrowDOWN');
arrow.animation.addByPrefix('purple', 'arrowLEFT');
arrow.animation.addByPrefix('red', 'arrowRIGHT');
arrow.setGraphicSize(Std.int(arrow.width * 0.7));
arrow.antialiasing = true;
arrow.x += Note.swagWidth * arrow.ID;
switch (Math.abs(arrow.ID))
{
case 0:
arrow.animation.addByPrefix('static', 'arrow static instance 1');
arrow.animation.addByPrefix('pressed', 'left press', 24, false);
arrow.animation.addByPrefix('confirm', 'left confirm', 24, false);
case 1:
arrow.animation.addByPrefix('static', 'arrow static instance 2');
arrow.animation.addByPrefix('pressed', 'down press', 24, false);
arrow.animation.addByPrefix('confirm', 'down confirm', 24, false);
case 2:
arrow.animation.addByPrefix('static', 'arrow static instance 4');
arrow.animation.addByPrefix('pressed', 'up press', 24, false);
arrow.animation.addByPrefix('confirm', 'up confirm', 24, false);
case 3:
arrow.animation.addByPrefix('static', 'arrow static instance 3');
arrow.animation.addByPrefix('pressed', 'right press', 24, false);
arrow.animation.addByPrefix('confirm', 'right confirm', 24, false);
}
}
/**
* Applies the pixel note style to an arrow.
* @param arrow The arrow to apply the style to.
* @param index The index of the arrow in the strumline.
*/
function createPixelNote(arrow:FlxSprite):Void
{
arrow.loadGraphic(Paths.image('weeb/pixelUI/arrows-pixels'), true, 17, 17);
arrow.animation.add('purplel', [4]);
arrow.animation.add('blue', [5]);
arrow.animation.add('green', [6]);
arrow.animation.add('red', [7]);
arrow.setGraphicSize(Std.int(arrow.width * Constants.PIXEL_ART_SCALE));
arrow.updateHitbox();
// Forcibly disable anti-aliasing on pixel graphics to stop blur.
arrow.antialiasing = false;
arrow.x += Note.swagWidth * arrow.ID;
// TODO: Seems weird that these are hardcoded...
switch (Math.abs(arrow.ID))
{
case 0:
arrow.animation.add('static', [0]);
arrow.animation.add('pressed', [4, 8], 12, false);
arrow.animation.add('confirm', [12, 16], 24, false);
case 1:
arrow.animation.add('static', [1]);
arrow.animation.add('pressed', [5, 9], 12, false);
arrow.animation.add('confirm', [13, 17], 24, false);
case 2:
arrow.animation.add('static', [2]);
arrow.animation.add('pressed', [6, 10], 12, false);
arrow.animation.add('confirm', [14, 18], 12, false);
case 3:
arrow.animation.add('static', [3]);
arrow.animation.add('pressed', [7, 11], 12, false);
arrow.animation.add('confirm', [15, 19], 24, false);
}
}
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):FlxSprite
{
// members maintains the order that the arrows were added.
return this.members[value];
}
public inline function getArrowByNoteType(value:NoteType):FlxSprite
{
return getArrow(value.int);
}
public inline function getArrowByNoteDir(value:NoteDir):FlxSprite
{
return getArrow(value.int);
}
public inline function getArrowByNoteColor(value:NoteColor):FlxSprite
{
return getArrow(value.int);
}
public static inline function getYPos():Int
{
return PreferencesMenu.getPref('downscroll') ? (FlxG.height - 150) : 50;
}
}
/**
* TODO: Unhardcode this and make it part of the note style system.
*/
enum StrumlineStyle
{
NORMAL;
PIXEL;
}

View file

@ -1,12 +1,18 @@
package funkin.play.stage;
import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEvent.UpdateScriptEvent;
import funkin.modding.events.ScriptEvent.NoteScriptEvent;
import funkin.modding.events.ScriptEvent.SongTimeScriptEvent;
import funkin.modding.events.ScriptEvent.CountdownScriptEvent;
import funkin.modding.IScriptedClass.IPlayStateScriptedClass;
import flixel.FlxSprite;
/**
* 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.
@ -76,9 +82,9 @@ 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 (event.beat % danceEvery == 0)
{
dance();
}
@ -87,7 +93,7 @@ class Bopper extends FlxSprite
/**
* Called every `danceEvery` beats of the song.
*/
public function dance():Void
function dance():Void
{
if (this.animation == null)
{
@ -116,4 +122,36 @@ class Bopper extends FlxSprite
this.animation.play('idle$idleSuffix');
}
}
public function onScriptEvent(event:ScriptEvent) {}
public function onCreate(event:ScriptEvent) {}
public function onDestroy(event:ScriptEvent) {}
public function onUpdate(event:UpdateScriptEvent) {}
public function onPause(event:ScriptEvent) {}
public function onResume(event:ScriptEvent) {}
public function onSongStart(event:ScriptEvent) {}
public function onSongEnd(event:ScriptEvent) {}
public function onSongReset(event:ScriptEvent) {}
public function onGameOver(event:ScriptEvent) {}
public function onGameRetry(event:ScriptEvent) {}
public function onNoteHit(event:NoteScriptEvent) {}
public function onNoteMiss(event:NoteScriptEvent) {}
public function onStepHit(event:SongTimeScriptEvent) {}
public function onCountdownStart(event:CountdownScriptEvent) {}
public function onCountdownStep(event:CountdownScriptEvent) {}
}

View file

@ -1,5 +1,10 @@
package funkin.play.stage;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEvent.CountdownScriptEvent;
import funkin.modding.events.ScriptEvent.KeyboardInputScriptEvent;
import funkin.modding.IScriptedClass;
import flixel.FlxSprite;
import flixel.group.FlxSpriteGroup;
import flixel.math.FlxPoint;
@ -14,7 +19,7 @@ import funkin.util.SortUtil;
*
* 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 implements IInputScriptedClass
{
public final stageId:String;
public final stageName:String;
@ -50,16 +55,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)
{
@ -162,8 +175,6 @@ class Stage extends FlxSpriteGroup implements IHook
}
trace(' Prop placed.');
}
this.refresh();
}
/**
@ -200,39 +211,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 +231,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.
*/
@ -360,9 +314,11 @@ class Stage extends FlxSpriteGroup implements IHook
/**
* Perform cleanup for when you are leaving the level.
*/
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)
{
@ -390,13 +346,56 @@ class Stage extends FlxSpriteGroup implements IHook
}
/**
* 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.
ScriptEventDispatcher.callEventOnAllTargets(cast boppers, event);
}
public function onScriptEvent(event:ScriptEvent) {}
public function onPause(event:ScriptEvent) {}
public function onResume(event:ScriptEvent) {}
public function onSongStart(event:ScriptEvent) {}
public function onSongEnd(event:ScriptEvent) {}
/**
* Resets the stage and its props.
*/
public function onSongReset(event:ScriptEvent) {}
public function onGameOver(event:ScriptEvent) {}
public function onGameRetry(event:ScriptEvent) {}
public function onCountdownStart(event:CountdownScriptEvent) {}
public function onCountdownStep(event:CountdownScriptEvent) {}
public function onKeyDown(event:KeyboardInputScriptEvent) {}
public function onKeyUp(event:KeyboardInputScriptEvent) {}
/**
* A function that should get called every frame.
*/
public function onUpdate(event:UpdateScriptEvent) {}
public function onNoteHit(event:NoteScriptEvent) {}
public function onNoteMiss(event:NoteScriptEvent) {}
}

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

@ -0,0 +1,14 @@
package funkin.util;
import flixel.util.FlxColor;
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;
}

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,80 @@
package modding.module;
import modding.module.events.ModuleEvent;
/**
* A module is an interface which provides for scripts to perform custom behavior
* without requiring a specific context.
*
* You may have the module active at all times, or only when another script enables it.
*/
class Module
{
/**
* Whether the module is currently active.
*/
public var active(default, set):Bool = false;
function set_active(value:Bool):Bool
{
this.active = value;
return value;
}
public var moduleId(default, null):String = 'UNKNOWN';
/**
* Called when the module is initialized.
* It may not be safe to reference other modules here since they may not be loaded yet.
*
* @param startActive Whether to start with the module active.
* If false, the module will be inactive and must be enabled by another script,
* such as a stage or another module.
*/
public function new(moduleId:String, startActive:Bool)
{
this.moduleId = moduleId;
this.active = startActive;
}
/**
* Called after the module was initialized, but before anything else.
* Other modules may still be uninitialized at this stage.
*/
public function onPostCreate() {}
/**
* Called at the beginning of a song, before the countdown begins.
*/
public function onSongStart() {}
/**
* Called at the end of a song, after the song fades out.
*/
public function onSongEnd() {}
/**
* Called at the beginning of the countdown.
*/
public function onBeginCountdown(event:ModuleEvent) {}
/**
* Called four times per section of a song.
*/
public function onSongBeat() {}
/**
* Called sixteen times per section of a song.
*/
public function onSongStep() {}
/**
* Called at the end of the `update()` loop.
* Be careful! Using this can have a significant impact on performance.
*/
public function onUpdate(event:UpdateModuleEvent) {}
public function onNoteHit() {}
public function onNoteMiss() {}
}

View file

@ -0,0 +1,79 @@
package modding.module;
import modding.module.ModuleEvent;
import modding.module.ModuleEvent.UpdateModuleEvent;
class ModuleHandler
{
static final moduleCache:Map<String, Module> = new Map<String, Module>();
/**
* Whether modules start active by default.
*/
static final DEFAULT_STARTACTIVE:Bool = true;
/**
* 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, DEFAULT_STARTACTIVE);
if (module != null)
{
trace(' Loaded module: ${moduleCls}');
// Then store it.
moduleCache.set(module.moduleId, module);
}
else
{
trace(' Failed to instantiate module: ${moduleCls}');
}
}
trace("[MODULEHANDLER] Module cache loaded.");
call_onPostCreate();
}
static function clearModuleCache():Void
{
if (moduleCache != null)
{
moduleCache.clear();
}
}
/**
* Calls onPostCreate on all modules.
*/
public static function call_onPostCreate():Void
{
for (module in moduleCache)
{
module.onPostCreate();
}
}
/**
* Calls onUpdate on all modules.
*/
public static function call_onUpdate(elapsed:Float):Void
{
var event = new UpdateModuleEvent(elapsed);
for (module in moduleCache)
{
module.onUpdate(event);
}
}
}

View file

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

View file

@ -0,0 +1,128 @@
package modding.module;
import openfl.events.EventType;
typedef ModuleEventType = EventType<ModuleEvent>;
class ModuleEvent
{
public static inline var SONG_START:ModuleEventType = "SONG_START";
public static inline var SONG_END:ModuleEventType = "SONG_END";
public static inline var COUNTDOWN_BEGIN:ModuleEventType = "COUNTDOWN_BEGIN";
public static inline var COUNTDOWN_STEP:ModuleEventType = "COUNTDOWN_STEP";
public static inline var SONG_BEAT_HIT:ModuleEventType = "SONG_BEAT_HIT";
public static inline var SONG_STEP_HIT:ModuleEventType = "SONG_STEP_HIT";
public static inline var PAUSE:ModuleEventType = "PAUSE";
public static inline var RESUME:ModuleEventType = "RESUME";
public static inline var UPDATE:ModuleEventType = "UPDATE";
/**
* Note hit success, health gained, note data, player vs opponent, etc
* are all provided as event parameters.
*
* Event is cancelable, which will cause the press to be ignored and the note to be missed.
*/
public static inline var NOTE_HIT:ModuleEventType = "NOTE_HIT";
public static inline var NOTE_MISS:ModuleEventType = "NOTE_MISS";
public static inline var GAME_OVER:ModuleEventType = "GAME_OVER";
public static inline var RETRY:ModuleEventType = "RETRY";
/**
* If true, the behavior associated with this event can be prevented.
* For example, cancelling COUNTDOWN_BEGIN 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):ModuleEventType;
@:noCompletion private var __eventCanceled:Bool;
@:noCompletion private var __shouldPropagate:Bool;
public function new(type:ModuleEventType, 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_BEGIN will prevent the countdown from starting.
*/
public function cancelEvent():Void
{
if (cancelable)
{
__eventCanceled = true;
}
}
/**
* Call this function to stop any other modules from receiving the event.
*/
public function stopPropagation():Void
{
__shouldPropagate = false;
}
public function toString():String
{
return 'ModuleEvent(type=$type, cancelable=$cancelable)';
}
}
/**
* SPECIFIC EVENTS
*/
/**
* An event that is fired associated with a specific note.
*/
class NoteModuleEvent extends ModuleEvent
{
/**
* 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:ModuleEventType, note:Note, cancelable:Bool = false):Void
{
super(type, cancelable);
this.note = note;
}
public override function toString():String
{
return 'NoteModuleEvent(type=' + type + ', cancelable=' + cancelable + ', note=' + note + ')';
}
}
/**
* An event that is fired during the update loop.
*/
class UpdateModuleEvent extends ModuleEvent
{
/**
* 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(ModuleEvent.UPDATE, false);
this.elapsed = elapsed;
}
public override function toString():String
{
return 'UpdateModuleEvent(elapsed=$elapsed)';
}
}

View file

@ -0,0 +1 @@
package modding.module.events;