1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2025-06-17 23:21:43 +00:00

Scripted modules are stable but missing some features.

This commit is contained in:
Eric Myllyoja 2022-03-13 14:36:03 -04:00
parent 49b2907a30
commit 0cc4f8a982
29 changed files with 847 additions and 803 deletions

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

@ -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);
}*/
}
}

View file

@ -1,5 +1,6 @@
package funkin;
import funkin.util.Constants;
import funkin.modding.events.ScriptEvent.UpdateScriptEvent;
import funkin.modding.module.ModuleHandler;
import funkin.NGio;
@ -140,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()
@ -299,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)
@ -310,9 +315,6 @@ class MainMenuState extends MusicBeatState
FlxG.sound.play(Paths.sound('cancelMenu'));
FlxG.switchState(new TitleState());
}
var event:UpdateScriptEvent = new UpdateScriptEvent(elapsed);
ModuleHandler.callEvent(event);
}
}

View file

@ -1,5 +1,11 @@
package funkin;
import flixel.util.FlxColor;
import flixel.text.FlxText;
import cpp.abi.Abi;
import funkin.modding.events.ScriptEvent;
import funkin.modding.module.ModuleHandler;
import funkin.modding.events.ScriptEvent.UpdateScriptEvent;
import funkin.Conductor.BPMChangeEvent;
import flixel.FlxGame;
import flixel.addons.transition.FlxTransitionableState;
@ -17,16 +23,23 @@ class MusicBeatState extends FlxUIState
inline function get_controls():Controls
return PlayerSettings.player1.controls;
public var leftWatermarkText:FlxText = null;
public var rightWatermarkText:FlxText = null;
override function create()
{
super.create();
if (transIn != null)
trace('reg ' + transIn.region);
super.create();
createWatermarkText();
}
override function update(elapsed:Float)
{
super.update(elapsed);
// everyStep();
var oldStep:Int = curStep;
@ -36,7 +49,31 @@ class MusicBeatState extends FlxUIState
if (oldStep != curStep && curStep >= 0)
stepHit();
super.update(elapsed);
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);
}
private function updateBeat():Void

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

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

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

@ -4,9 +4,11 @@ import polymod.hscript.HScriptable;
/**
* Functions annotated with @:hscript will call the relevant script.
* Functions annotated with @:hookable can be reassigned.
*/
@:hscript({
// ALL of these values are added to ALL scripts in the child classes.
context: [FlxG, FlxSprite, Math, Paths, Std]
})
// @:autoBuild(funkin.util.macro.HookableMacro.build())
interface IHook extends HScriptable {}

View file

@ -23,6 +23,7 @@ interface IInputScriptedClass extends IScriptedClass
{
public function onKeyDown(event:KeyboardInputScriptEvent):Void;
public function onKeyUp(event:KeyboardInputScriptEvent):Void;
// TODO: OnMouseDown, OnMouseUp, OnMouseMove
}
/**
@ -33,6 +34,7 @@ interface IPlayStateScriptedClass extends IScriptedClass
public function onPause(event:ScriptEvent):Void;
public function onResume(event:ScriptEvent):Void;
public function onSongLoaded(eent:SongLoadScriptEvent):Void;
public function onSongStart(event:ScriptEvent):Void;
public function onSongEnd(event:ScriptEvent):Void;
public function onSongReset(event:ScriptEvent):Void;
@ -47,4 +49,5 @@ interface IPlayStateScriptedClass extends IScriptedClass
public function onCountdownStart(event:CountdownScriptEvent):Void;
public function onCountdownStep(event:CountdownScriptEvent):Void;
public function onCountdownEnd(event:CountdownScriptEvent):Void;
}

View file

@ -108,8 +108,8 @@ class ScriptEvent
* 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.
* - 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";
@ -122,6 +122,13 @@ class ScriptEvent
*/
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 when the game over screen triggers and the death animation plays.
*
@ -150,6 +157,14 @@ class ScriptEvent
*/
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";
/**
* If true, the behavior associated with this event can be prevented.
* For example, cancelling COUNTDOWN_BEGIN should prevent the countdown from starting,
@ -167,13 +182,16 @@ class ScriptEvent
*/
public var shouldPropagate(default, null):Bool;
@:noCompletion private var __eventCanceled: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.eventCanceled = false;
this.shouldPropagate = true;
}
@ -185,10 +203,16 @@ class ScriptEvent
{
if (cancelable)
{
__eventCanceled = true;
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.
*/
@ -292,9 +316,9 @@ class CountdownScriptEvent extends ScriptEvent
*/
public var step(default, null):CountdownStep;
public function new(type:ScriptEventType, step:CountdownStep):Void
public function new(type:ScriptEventType, step:CountdownStep, cancelable = true):Void
{
super(type, false);
super(type, cancelable);
this.step = step;
}
@ -325,3 +349,39 @@ class KeyboardInputScriptEvent extends ScriptEvent
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)';
}
}

View file

@ -11,6 +11,9 @@ 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.
@ -85,6 +88,12 @@ class ScriptEventDispatcher
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;
}
}
@ -94,26 +103,20 @@ class ScriptEventDispatcher
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);

View file

@ -28,6 +28,22 @@ class Module implements IInputScriptedClass implements IPlayStateScriptedClass
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.
@ -36,10 +52,11 @@ class Module implements IInputScriptedClass implements IPlayStateScriptedClass
* 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)
public function new(moduleId:String, active:Bool = true, priority:Int = 1000):Void
{
this.moduleId = moduleId;
this.active = startActive;
this.active = active;
this.priority = priority;
}
public function toString()
@ -47,6 +64,8 @@ class Module implements IInputScriptedClass implements IPlayStateScriptedClass
return 'Module(' + this.moduleId + ')';
}
// TODO: Half of these aren't actually being called!!!!!!!
public function onScriptEvent(event:ScriptEvent) {}
public function onCreate(event:ScriptEvent) {}
@ -84,4 +103,8 @@ class Module implements IInputScriptedClass implements IPlayStateScriptedClass
public function onCountdownStart(event:CountdownScriptEvent) {}
public function onCountdownStep(event:CountdownScriptEvent) {}
public function onCountdownEnd(event:CountdownScriptEvent) {}
public function onSongLoaded(eent:SongLoadScriptEvent) {}
}

View file

@ -4,17 +4,15 @@ 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>();
/**
* Whether modules start active by default.
*/
static final DEFAULT_STARTACTIVE:Bool = true;
static var modulePriorityOrder:Array<String> = [];
/**
* Parses and preloads the game's stage data and scripts when the game starts.
@ -31,37 +29,92 @@ class ModuleHandler
trace(' Instantiating ${scriptedModuleClassNames.length} modules...');
for (moduleCls in scriptedModuleClassNames)
{
var module:Module = ScriptedModule.init(moduleCls, moduleCls, DEFAULT_STARTACTIVE);
var module:Module = ScriptedModule.init(moduleCls, moduleCls);
if (module != null)
{
trace(' Loaded module: ${moduleCls}');
// Then store it.
moduleCache.set(module.moduleId, module);
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;
}
}
public static function clearModuleCache():Void
{
if (moduleCache != null)
{
// for (module in moduleCache)
// {
// module.destroy();
// }
moduleCache.clear();
modulePriorityOrder = [];
}
}
public static function callEvent(event:ScriptEvent):Void
{
ScriptEventDispatcher.callEventOnAllTargets(moduleCache.iterator(), event);
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

@ -1,11 +1,313 @@
package funkin.play;
enum abstract CountdownStep(String) from String to String
import funkin.util.Constants;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.FlxSprite;
import flixel.input.actions.FlxAction.FlxActionAnalog;
import cpp.abi.Abi;
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
{
var BEFORE = "BEFORE";
var THREE = "THREE";
var TWO = "TWO";
var ONE = "ONE";
var GO = "GO";
var AFTER = "AFTER";
/**
* 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

@ -73,12 +73,28 @@ class PlayState extends MusicBeatState
*/
public static var isPracticeMode:Bool = false;
/**
* Whether the game is currently in a cutscene, and gameplay should be stopped.
*/
public static var isInCutscene:Bool = false;
/**
* Whether the game is currently in the countdown before the song resumes.
*/
public static var isInCountdown:Bool = false;
/**
* The current "Blueball Counter" to display in the pause menu.
* Resets when you beat a song or go back to the main menu.
*/
public static var deathCounter:Int = 0;
/**
* The default camera zoom level. The camera lerps back to this after zooming in.
* Defaults to 1.05 but may be larger or smaller depending on the current stage.
*/
public static var defaultCameraZoom:Float = 1.05;
/**
* Used to persist the position of the `cameraFollowPosition` between resets.
*/
@ -106,6 +122,12 @@ class PlayState extends MusicBeatState
*/
public var health:Float = 1;
/**
* An empty FlxObject contained in the scene.
* The current gameplay camera will be centered on this object. Tween its position to move the camera smoothly.
*/
public var cameraFollowPoint:FlxObject;
/**
* PRIVATE INSTANCE VARIABLES
* Private instance variables should be used for information that must be reset or dereferenced
@ -117,12 +139,6 @@ class PlayState extends MusicBeatState
*/
private var inactiveNotes:Array<Note>;
/**
* An empty FlxObject contained in the scene.
* The current gameplay camera will be centered on this object. Tween its position to move the camera smoothly.
*/
private var cameraFollowPoint:FlxObject;
/**
* An object which the strumline (and its notes) are positioned relative to.
*/
@ -175,6 +191,16 @@ class PlayState extends MusicBeatState
*/
public var enemyStrumline:Strumline;
/**
* The camera which contains, and controls visibility of, the user interface elements.
*/
public var camHUD:FlxCamera;
/**
* The camera which contains, and controls visibility of, the stage and characters.
*/
public var camGame:FlxCamera;
/**
* PROPERTIES
*/
@ -213,19 +239,14 @@ class PlayState extends MusicBeatState
private var startingSong:Bool = false;
private var iconP1:HealthIcon;
private var iconP2:HealthIcon;
private var camHUD:FlxCamera;
private var camGame:FlxCamera;
var dialogue:Array<String>;
var startedCountdown:Bool = false;
var talking:Bool = true;
var songScore:Int = 0;
var doof:DialogueBox;
var grpNoteSplashes:FlxTypedGroup<NoteSplash>;
var defaultCamZoom:Float = 1.05;
var inCutscene:Bool = false;
var camPos:FlxPoint;
var comboPopUps:PopUpStuff;
var startTimer:FlxTimer = new FlxTimer();
var perfectMode:Bool = false;
var previousFrameTime:Int = 0;
var songTime:Float = 0;
@ -254,15 +275,25 @@ class PlayState extends MusicBeatState
// This state receives draw calls even when a substate is active.
this.persistentDraw = true;
// Stop any pre-existing music.
if (FlxG.sound.music != null)
FlxG.sound.music.stop();
// Prepare the current song to be played.
FlxG.sound.cache(Paths.inst(currentSong.song));
FlxG.sound.cache(Paths.voices(currentSong.song));
Conductor.songPosition = -5000;
// Initialize stage stuff.
initCameras();
if (currentSong == null)
currentSong = SongLoad.loadFromJson('tutorial');
Conductor.mapBPMChanges(currentSong);
Conductor.changeBPM(currentSong.bpm);
// dialogue init shit, just for week 5 really (for now...?)
switch (currentSong.song.toLowerCase())
{
case 'senpai':
@ -273,16 +304,6 @@ class PlayState extends MusicBeatState
dialogue = CoolUtil.coolTextFile(Paths.txt('songs/thorns/thornsDialogue'));
}
// Initialize stage stuff.
initCameras();
#if discord_rpc
initDiscord();
#end
initStage();
initCharacters();
if (dialogue != null)
{
doof = new DialogueBox(false, dialogue);
@ -291,7 +312,13 @@ class PlayState extends MusicBeatState
doof.cameras = [camHUD];
}
// fake notesplash cache type deal so that it loads in the graphic?
// Once the song is loaded, we can continue and initialize the stage.
initStage();
initCharacters();
#if discord_rpc
initDiscord();
#end
comboPopUps = new PopUpStuff();
add(comboPopUps);
@ -370,42 +397,15 @@ class PlayState extends MusicBeatState
switch (currentSong.song.toLowerCase())
{
case "winter-horrorland":
var blackScreen:FlxSprite = new FlxSprite(0, 0).makeGraphic(Std.int(FlxG.width * 2), Std.int(FlxG.height * 2), FlxColor.BLACK);
add(blackScreen);
blackScreen.scrollFactor.set();
camHUD.visible = false;
new FlxTimer().start(0.1, function(tmr:FlxTimer)
{
remove(blackScreen);
FlxG.sound.play(Paths.sound('Lights_Turn_On'));
cameraFollowPoint.y = -2050;
cameraFollowPoint.x += 200;
FlxG.camera.focusOn(cameraFollowPoint.getPosition());
FlxG.camera.zoom = 1.5;
new FlxTimer().start(0.8, function(tmr:FlxTimer)
{
camHUD.visible = true;
remove(blackScreen);
FlxTween.tween(FlxG.camera, {zoom: defaultCamZoom}, 2.5, {
ease: FlxEase.quadInOut,
onComplete: function(twn:FlxTween)
{
startCountdown();
}
});
});
});
VanillaCutscenes.playHorrorStartCutscene();
case 'senpai' | 'roses' | 'thorns':
schoolIntro(doof); // doof is assumed to be non-null, lol!
case 'ugh':
ughIntro();
VanillaCutscenes.playUghCutscene();
case 'stress':
stressIntro();
VanillaCutscenes.playStressCutscene();
case 'guns':
gunsIntro();
VanillaCutscenes.playGunsCutscene();
default:
startCountdown();
}
@ -414,24 +414,19 @@ class PlayState extends MusicBeatState
{
startCountdown();
}
this.leftWatermarkText.text = '${currentSong.song.toUpperCase()} - ${SongLoad.curDiff.toUpperCase()}';
this.rightWatermarkText.text = Constants.VERSION;
}
/**
* Initializes the position of the camera.
* Initializes the game and HUD cameras.
*/
function initCameras()
{
defaultCamZoom = FlxCamera.defaultZoom;
// Configure the default camera zoom level.
defaultCameraZoom = FlxCamera.defaultZoom * 1.05;
defaultCamZoom *= 1.05;
if (FlxG.sound.music != null)
FlxG.sound.music.stop();
FlxG.sound.cache(Paths.inst(currentSong.song));
FlxG.sound.cache(Paths.voices(currentSong.song));
// var gameCam:FlxCamera = FlxG.camera;
camGame = new SwagCamera();
camHUD = new FlxCamera();
camHUD.bgColor.alpha = 0;
@ -590,99 +585,6 @@ class PlayState extends MusicBeatState
}
}
function ughIntro()
{
inCutscene = true;
var blackShit:FlxSprite = new FlxSprite(-200, -200).makeGraphic(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK);
blackShit.scrollFactor.set();
add(blackShit);
#if html5
var vid:FlxVideo = new FlxVideo('music/ughCutscene.mp4');
vid.finishCallback = function()
{
remove(blackShit);
FlxTween.tween(FlxG.camera, {zoom: defaultCamZoom}, (Conductor.crochet / 1000) * 5, {ease: FlxEase.quadInOut});
startCountdown();
cameraMovement();
};
#else
remove(blackShit);
FlxTween.tween(FlxG.camera, {zoom: defaultCamZoom}, (Conductor.crochet / 1000) * 5, {ease: FlxEase.quadInOut});
startCountdown();
cameraMovement();
#end
FlxG.camera.zoom = defaultCamZoom * 1.2;
cameraFollowPoint.x += 100;
cameraFollowPoint.y += 100;
/*
FlxG.sound.playMusic(Paths.music('DISTORTO'), 0);
FlxG.sound.music.fadeIn(5, 0, 0.5);
dad.visible = false;
var tankCutscene:TankCutscene = new TankCutscene(-20, 320);
tankCutscene.frames = Paths.getSparrowAtlas('cutsceneStuff/tankTalkSong1');
tankCutscene.animation.addByPrefix('wellWell', 'TANK TALK 1 P1', 24, false);
tankCutscene.animation.addByPrefix('killYou', 'TANK TALK 1 P2', 24, false);
tankCutscene.animation.play('wellWell');
tankCutscene.antialiasing = true;
gfCutsceneLayer.add(tankCutscene);
camHUD.visible = false;
FlxG.camera.zoom *= 1.2;
camFollow.y += 100;
tankCutscene.startSyncAudio = FlxG.sound.load(Paths.sound('wellWellWell'));
new FlxTimer().start(3, function(tmr:FlxTimer)
{
camFollow.x += 800;
camFollow.y += 100;
FlxTween.tween(FlxG.camera, {zoom: defaultCamZoom * 1.2}, 0.27, {ease: FlxEase.quadInOut});
new FlxTimer().start(1.5, function(bep:FlxTimer)
{
boyfriend.playAnim('singUP');
// play sound
FlxG.sound.play(Paths.sound('bfBeep'), function()
{
boyfriend.playAnim('idle');
});
});
new FlxTimer().start(3, function(swaggy:FlxTimer)
{
camFollow.x -= 800;
camFollow.y -= 100;
FlxTween.tween(FlxG.camera, {zoom: defaultCamZoom * 1.2}, 0.5, {ease: FlxEase.quadInOut});
tankCutscene.animation.play('killYou');
FlxG.sound.play(Paths.sound('killYou'));
new FlxTimer().start(6.1, function(swagasdga:FlxTimer)
{
FlxTween.tween(FlxG.camera, {zoom: defaultCamZoom}, (Conductor.crochet / 1000) * 5, {ease: FlxEase.quadInOut});
FlxG.sound.music.fadeOut((Conductor.crochet / 1000) * 5, 0);
new FlxTimer().start((Conductor.crochet / 1000) * 5, function(money:FlxTimer)
{
dad.visible = true;
gfCutsceneLayer.remove(tankCutscene);
});
cameraMovement();
startCountdown();
camHUD.visible = true;
});
});
});*/
}
/**
* Removes any references to the current stage, then clears the stage cache,
* then reloads all the stages.
@ -734,122 +636,13 @@ class PlayState extends MusicBeatState
ScriptEventDispatcher.callEvent(currentStage, event);
// Apply camera zoom.
defaultCamZoom *= currentStage.camZoom;
defaultCameraZoom *= currentStage.camZoom;
// Add the stage to the scene.
this.add(currentStage);
}
}
function gunsIntro()
{
inCutscene = true;
var blackShit:FlxSprite = new FlxSprite(-200, -200).makeGraphic(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK);
blackShit.scrollFactor.set();
add(blackShit);
#if html5
var vid:FlxVideo = new FlxVideo('music/gunsCutscene.mp4');
vid.finishCallback = function()
{
remove(blackShit);
FlxTween.tween(FlxG.camera, {zoom: defaultCamZoom}, (Conductor.crochet / 1000) * 5, {ease: FlxEase.quadInOut});
startCountdown();
cameraMovement();
};
#else
remove(blackShit);
FlxTween.tween(FlxG.camera, {zoom: defaultCamZoom}, (Conductor.crochet / 1000) * 5, {ease: FlxEase.quadInOut});
startCountdown();
cameraMovement();
#end
/* camFollow.setPosition(camPos.x, camPos.y);
camHUD.visible = false;
FlxG.sound.playMusic(Paths.music('DISTORTO'), 0);
FlxG.sound.music.fadeIn(5, 0, 0.5);
camFollow.y += 100;
FlxTween.tween(FlxG.camera, {zoom: defaultCamZoom * 1.3}, 4, {ease: FlxEase.quadInOut});
dad.visible = false;
var tankCutscene:TankCutscene = new TankCutscene(20, 320);
tankCutscene.frames = Paths.getSparrowAtlas('cutsceneStuff/tankTalkSong2');
tankCutscene.animation.addByPrefix('tankyguy', 'TANK TALK 2', 24, false);
tankCutscene.animation.play('tankyguy');
tankCutscene.antialiasing = true;
gfCutsceneLayer.add(tankCutscene); // add();
tankCutscene.startSyncAudio = FlxG.sound.load(Paths.sound('tankSong2'));
new FlxTimer().start(4.1, function(ugly:FlxTimer)
{
FlxTween.tween(FlxG.camera, {zoom: defaultCamZoom * 1.4}, 0.4, {ease: FlxEase.quadOut});
FlxTween.tween(FlxG.camera, {zoom: defaultCamZoom * 1.3}, 0.7, {ease: FlxEase.quadInOut, startDelay: 0.45});
gf.playAnim('sad');
});
new FlxTimer().start(11, function(tmr:FlxTimer)
{
FlxG.sound.music.fadeOut((Conductor.crochet / 1000) * 5, 0);
FlxTween.tween(FlxG.camera, {zoom: defaultCamZoom}, (Conductor.crochet * 5) / 1000, {ease: FlxEase.quartIn});
startCountdown();
new FlxTimer().start((Conductor.crochet * 25) / 1000, function(daTim:FlxTimer)
{
dad.visible = true;
gfCutsceneLayer.remove(tankCutscene);
});
camHUD.visible = true;
});*/
}
/**
* [
* [0, function(){blah;}],
* [4.6, function(){blah;}],
* [25.1, function(){blah;}],
* [30.7, function(){blah;}]
* ]
* SOMETHING LIKE THIS
*/
// var cutsceneFunctions:Array<Dynamic> = [];
function stressIntro()
{
inCutscene = true;
var blackShit:FlxSprite = new FlxSprite(-200, -200).makeGraphic(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK);
blackShit.scrollFactor.set();
add(blackShit);
#if html5
var vid:FlxVideo = new FlxVideo('music/stressCutscene.mp4');
vid.finishCallback = function()
{
remove(blackShit);
FlxTween.tween(FlxG.camera, {zoom: defaultCamZoom}, (Conductor.crochet / 1000) * 5, {ease: FlxEase.quadInOut});
startCountdown();
cameraMovement();
};
#else
remove(blackShit);
FlxTween.tween(FlxG.camera, {zoom: defaultCamZoom}, (Conductor.crochet / 1000) * 5, {ease: FlxEase.quadInOut});
startCountdown();
cameraMovement();
#end
}
function initDiscord():Void
{
#if discord_rpc
@ -920,7 +713,7 @@ class PlayState extends MusicBeatState
{
if (dialogueBox != null)
{
inCutscene = true;
isInCutscene = true;
if (currentSong.song.toLowerCase() == 'thorns')
{
@ -962,93 +755,6 @@ class PlayState extends MusicBeatState
});
}
function startCountdown():Void
{
inCutscene = false;
camHUD.visible = true;
buildStrumlines();
talking = false;
restartCountdownTimer();
}
function restartCountdownTimer():Void
{
startedCountdown = true;
Conductor.songPosition = 0;
Conductor.songPosition -= Conductor.crochet * 5;
var swagCounter:Int = 0;
startTimer.start(Conductor.crochet / 1000, function(tmr:FlxTimer)
{
// this just based on beatHit stuff but compact
if (swagCounter % gfSpeed == 0)
currentStage.getGirlfriend().dance();
if (swagCounter % 2 == 0)
{
if (currentStage.getBoyfriend().animation != null)
{
if (!currentStage.getBoyfriend().animation.curAnim.name.startsWith("sing"))
currentStage.getBoyfriend().playAnim('idle');
}
if (currentStage.getDad().animation != null)
{
if (!currentStage.getDad().animation.curAnim.name.startsWith("sing"))
currentStage.getDad().dance();
}
}
else if (currentStage.getDad().curCharacter == 'spooky' && !currentStage.getDad().animation.curAnim.name.startsWith("sing"))
currentStage.getDad().dance();
if (generatedMusic)
activeNotes.sort(SortUtil.byStrumtime, FlxSort.DESCENDING);
var introSprPaths:Array<String> = ["ready", "set", "go"];
var altSuffix:String = "";
if (currentStageId.startsWith("school"))
{
altSuffix = '-pixel';
introSprPaths = ['weeb/pixelUI/ready-pixel', 'weeb/pixelUI/set-pixel', 'weeb/pixelUI/date-pixel'];
}
var introSndPaths:Array<String> = [
"intro3" + altSuffix, "intro2" + altSuffix,
"intro1" + altSuffix, "introGo" + altSuffix
];
if (swagCounter > 0)
readySetGo(introSprPaths[swagCounter - 1]);
FlxG.sound.play(Paths.sound(introSndPaths[swagCounter]), 0.6);
swagCounter += 1;
}, 4);
}
function readySetGo(path:String):Void
{
var spr:FlxSprite = new FlxSprite().loadGraphic(Paths.image(path));
spr.scrollFactor.set();
if (currentStageId.startsWith('school'))
spr.setGraphicSize(Std.int(spr.width * Constants.PIXEL_ART_SCALE));
spr.updateHitbox();
spr.screenCenter();
add(spr);
FlxTween.tween(spr, {y: spr.y += 100, alpha: 0}, Conductor.crochet / 1000, {
ease: FlxEase.cubeInOut,
onComplete: function(twn:FlxTween)
{
spr.destroy();
}
});
}
function startSong():Void
{
startingSong = false;
@ -1244,13 +950,11 @@ class PlayState extends MusicBeatState
vocals.pause();
var event:ScriptEvent = new ScriptEvent(ScriptEvent.SONG_RESET, false);
ScriptEventDispatcher.callEvent(currentStage, event);
ModuleHandler.callEvent(event);
FlxG.sound.music.time = 0;
regenNoteData(); // loads the note data from start
health = 1;
restartCountdownTimer();
Countdown.performCountdown(currentStageId.startsWith('school'));
needsReset = false;
}
@ -1265,9 +969,9 @@ class PlayState extends MusicBeatState
// do this BEFORE super.update() so songPosition is accurate
if (startingSong)
{
if (startedCountdown)
if (isInCountdown)
{
Conductor.songPosition += FlxG.elapsed * 1000;
Conductor.songPosition += elapsed * 1000;
if (Conductor.songPosition >= 0)
startSong();
}
@ -1278,7 +982,6 @@ class PlayState extends MusicBeatState
Conductor.offset = -13; // DO NOT FORGET TO REMOVE THE HARDCODE! WHEN I MAKE BETTER OFFSET SYSTEM!
Conductor.songPosition = FlxG.sound.music.time + Conductor.offset; // 20 is THE MILLISECONDS??
// Conductor.songPosition += FlxG.elapsed * 1000;
if (!isGamePaused)
{
@ -1290,11 +993,8 @@ class PlayState extends MusicBeatState
{
songTime = (songTime + Conductor.songPosition) / 2;
Conductor.lastSongPos = Conductor.songPosition;
// Conductor.songPosition += FlxG.elapsed * 1000;
// trace('MISSED FRAME');
}
}
// Conductor.lastSongPos = FlxG.sound.music.time;
}
var androidPause:Bool = false;
@ -1303,7 +1003,7 @@ class PlayState extends MusicBeatState
androidPause = FlxG.android.justPressed.BACK;
#end
if ((controls.PAUSE || androidPause) && startedCountdown && mayPauseGame)
if ((controls.PAUSE || androidPause) && isInCountdown && mayPauseGame)
{
persistentUpdate = false;
persistentDraw = true;
@ -1390,7 +1090,7 @@ class PlayState extends MusicBeatState
if (camZooming)
{
FlxG.camera.zoom = FlxMath.lerp(defaultCamZoom, FlxG.camera.zoom, 0.95);
FlxG.camera.zoom = FlxMath.lerp(defaultCameraZoom, FlxG.camera.zoom, 0.95);
camHUD.zoom = FlxMath.lerp(1 * FlxCamera.defaultZoom, camHUD.zoom, 0.95);
}
@ -1413,7 +1113,7 @@ class PlayState extends MusicBeatState
}
}
if (!inCutscene && !_exiting)
if (!isInCutscene && !_exiting)
{
// RESET = Quick Game Over Screen
if (controls.RESET)
@ -1566,12 +1266,8 @@ class PlayState extends MusicBeatState
// TODO: Why the hell is the noteMiss logic in two different places?
if (daNote.tooLate)
{
if (currentStage != null)
{
var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_MISS, daNote, true);
ScriptEventDispatcher.callEvent(currentStage, event);
ModuleHandler.callEvent(event);
}
var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_MISS, daNote, true);
dispatchEvent(event);
health -= 0.0775;
vocals.volume = 0;
killCombo();
@ -1587,16 +1283,10 @@ class PlayState extends MusicBeatState
});
}
if (!inCutscene)
if (!isInCutscene)
keyShit();
var event:UpdateScriptEvent = new UpdateScriptEvent(elapsed);
if (currentStage != null)
{
// We're using Eric's stage handler.
ScriptEventDispatcher.callEvent(currentStage, event);
}
ModuleHandler.callEvent(event);
dispatchEvent(new UpdateScriptEvent(elapsed));
}
function applyClipRect(daNote:Note):Void
@ -1723,7 +1413,7 @@ class PlayState extends MusicBeatState
blackShit.scrollFactor.set();
add(blackShit);
camHUD.visible = false;
inCutscene = true;
isInCutscene = true;
FlxG.sound.play(Paths.sound('Lights_Shut_off'), function()
{
@ -1787,11 +1477,12 @@ class PlayState extends MusicBeatState
health += healthMulti;
// TODO: Redo note hit logic to make sure this always gets called
if (currentStage != null)
var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_HIT, daNote, true);
dispatchEvent(event);
if (event.eventCanceled)
{
var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_HIT, daNote, true);
ScriptEventDispatcher.callEvent(currentStage, event);
ModuleHandler.callEvent(event);
// TODO: Do a thing!
}
if (isSick)
@ -1959,6 +1650,8 @@ class PlayState extends MusicBeatState
}
}
if (currentStage == null)
return;
if (currentStage.getBoyfriend().holdTimer > Conductor.stepCrochet * 4 * 0.001 && !holdArray.contains(true))
{
if (currentStage.getBoyfriend().animation != null
@ -2043,13 +1736,7 @@ class PlayState extends MusicBeatState
resyncVocals();
}
if (currentStage != null)
{
// We're using Eric's stage handler. The stage should know that a step has been hit.
var event:SongTimeScriptEvent = new SongTimeScriptEvent(ScriptEvent.SONG_BEAT_HIT, curBeat, curStep);
ScriptEventDispatcher.callEvent(currentStage, event);
ModuleHandler.callEvent(event);
}
dispatchEvent(new SongTimeScriptEvent(ScriptEvent.SONG_STEP_HIT, curBeat, curStep));
}
override function beatHit()
@ -2087,30 +1774,28 @@ class PlayState extends MusicBeatState
}
}
// Make the health icons bump (the update function causes them to lerp back down).
iconP1.setGraphicSize(Std.int(iconP1.width + 30));
iconP2.setGraphicSize(Std.int(iconP2.width + 30));
iconP1.updateHitbox();
iconP2.updateHitbox();
var song = SongLoad.getSong();
var step = Math.floor(curStep / 16);
if (curBeat % 8 == 7
&& song[step].mustHitSection
&& combo > 5
&& song.length > step + 1 // GK: this fixes an error on week 1 where song[step + 1] was null
&& !song[step + 1].mustHitSection)
{
var animShit:ComboCounter = new ComboCounter(-100, 300, combo);
animShit.scrollFactor.set(0.6, 0.6);
// Make the characters dance on the beat
danceOnBeat();
var frameShit:Float = (1 / 24) * 2; // equals 2 frames in the animation
// Call any relevant event handlers.
dispatchEvent(new SongTimeScriptEvent(ScriptEvent.SONG_BEAT_HIT, curBeat, curStep));
}
new FlxTimer().start(((Conductor.crochet / 1000) * 1.25) - frameShit, function(tmr)
{
animShit.forceFinish();
});
}
/**
* Handles characters dancing to the beat of the current song.
*
* TODO: Move some of this logic into `Bopper.hx`
*/
public function danceOnBeat()
{
if (currentStage == null)
return;
if (curBeat % gfSpeed == 0)
currentStage.getGirlfriend().dance();
@ -2142,16 +1827,11 @@ class PlayState extends MusicBeatState
currentStage.getBoyfriend().playAnim('hey', true);
currentStage.getDad().playAnim('cheer', true);
}
if (currentStage != null)
{
// We're using Eric's stage handler. The stage should know that a beat has been hit.
var event:SongTimeScriptEvent = new SongTimeScriptEvent(ScriptEvent.SONG_STEP_HIT, curBeat, curStep);
ScriptEventDispatcher.callEvent(currentStage, event);
ModuleHandler.callEvent(event);
}
}
/**
* Constructs the strumlines for each player.
*/
function buildStrumlines():Void
{
var strumlineStyle:StrumlineStyle = NORMAL;
@ -2169,12 +1849,14 @@ class PlayState extends MusicBeatState
playerStrumline = new Strumline(0, strumlineStyle, 4);
playerStrumline.offset = new FlxPoint(50 + FlxG.width / 2, strumlineYPos);
// Set the z-index so they don't appear in front of notes.
playerStrumline.zIndex = 100;
add(playerStrumline);
playerStrumline.cameras = [camHUD];
enemyStrumline = new Strumline(1, strumlineStyle, 4);
enemyStrumline.offset = new FlxPoint(50, strumlineYPos);
// Set the z-index so they don't appear in front of notes.
enemyStrumline.zIndex = 100;
add(enemyStrumline);
enemyStrumline.cameras = [camHUD];
@ -2202,11 +1884,17 @@ class PlayState extends MusicBeatState
vocals.pause();
}
// Pause the restart timer.
if (!startTimer.finished)
startTimer.active = false;
// Pause the countdown.
Countdown.pauseCountdown();
}
var event:ScriptEvent = new ScriptEvent(ScriptEvent.PAUSE, true);
dispatchEvent(event);
if (event.eventCanceled)
return;
super.openSubState(subState);
}
@ -2221,8 +1909,8 @@ class PlayState extends MusicBeatState
if (FlxG.sound.music != null && !startingSong)
resyncVocals();
if (!startTimer.finished)
startTimer.active = true;
// Resume the countdown.
Countdown.resumeCountdown();
#if discord_rpc
if (startTimer.finished)
@ -2233,9 +1921,41 @@ class PlayState extends MusicBeatState
#end
}
var event:ScriptEvent = new ScriptEvent(ScriptEvent.RESUME, true);
dispatchEvent(event);
if (event.eventCanceled)
return;
super.closeSubState();
}
/**
* Prepares to start the countdown.
* Ends any running cutscenes, creates the strumlines, and starts the countdown.
*/
function startCountdown():Void
{
isInCutscene = false;
camHUD.visible = true;
talking = false;
buildStrumlines();
Countdown.performCountdown(currentStageId.startsWith('school'));
}
override function dispatchEvent(event:ScriptEvent):Void
{
ScriptEventDispatcher.callEvent(currentStage, event);
// TODO: Dispatch event to song script
// TODO: Dispatch events to character scripts
super.dispatchEvent(event);
}
/**
* Updates the position and contents of the score display.
*/
@ -2259,7 +1979,7 @@ class PlayState extends MusicBeatState
function resetCamera():Void
{
FlxG.camera.follow(cameraFollowPoint, LOCKON, 0.04);
FlxG.camera.zoom = defaultCamZoom;
FlxG.camera.zoom = defaultCameraZoom;
FlxG.camera.focusOn(cameraFollowPoint.getPosition());
}

View file

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

View file

@ -169,7 +169,7 @@ class Strumline extends FlxTypedGroup<FlxSprite>
arrow.x += Note.swagWidth * arrow.ID;
// TODO: Seems weird that these are hardcoded...
// TODO: Seems weird that these are hardcoded like this... no XML?
switch (Math.abs(arrow.ID))
{
case 0:

View file

@ -0,0 +1,102 @@
package funkin.play;
import flixel.util.FlxTimer;
import flixel.tweens.FlxTween;
import flixel.tweens.FlxTween;
import flixel.tweens.FlxEase;
import flixel.util.FlxColor;
import flixel.FlxSprite;
/**
* 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
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});
Countdown.performCountdown(false);
@: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

@ -154,4 +154,8 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
public function onCountdownStart(event:CountdownScriptEvent) {}
public function onCountdownStep(event:CountdownScriptEvent) {}
public function onCountdownEnd(event:CountdownScriptEvent) {}
public function onSongLoaded(eent:SongLoadScriptEvent) {}
}

View file

@ -360,7 +360,10 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte
// 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);
for (bopper in boppers)
{
ScriptEventDispatcher.callEvent(bopper, event);
}
}
public function onScriptEvent(event:ScriptEvent) {}
@ -386,6 +389,8 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte
public function onCountdownStep(event:CountdownScriptEvent) {}
public function onCountdownEnd(event:CountdownScriptEvent) {}
public function onKeyDown(event:KeyboardInputScriptEvent) {}
public function onKeyUp(event:KeyboardInputScriptEvent) {}
@ -398,4 +403,6 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte
public function onNoteHit(event:NoteScriptEvent) {}
public function onNoteMiss(event:NoteScriptEvent) {}
public function onSongLoaded(eent:SongLoadScriptEvent) {}
}

View file

@ -1,5 +1,6 @@
package funkin.util;
import lime.app.Application;
import flixel.util.FlxColor;
class Constants
@ -11,4 +12,14 @@ class Constants
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;
static function get_VERSION():String
{
return 'v${Application.current.meta.get('version')}' + VERSION_SUFFIX;
}
}

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

@ -0,0 +1,3 @@
package funkin.util.macro;
class HookableMacro {}

View file

@ -1,80 +0,0 @@
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

@ -1,79 +0,0 @@
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

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

View file

@ -1,128 +0,0 @@
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

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