1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2024-11-09 16:24:42 +00:00

Various bug fixes for strumlines

This commit is contained in:
Eric Myllyoja 2022-03-14 20:48:45 -04:00
parent 1935373963
commit 60c6e5ee29
15 changed files with 410 additions and 225 deletions

View file

@ -2,16 +2,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;
import flixel.addons.ui.FlxUIState;
import flixel.math.FlxRect;
import flixel.util.FlxTimer;
class MusicBeatState extends FlxUIState
{

View file

@ -5,10 +5,13 @@ import polymod.hscript.HScriptable;
/**
* Functions annotated with @:hscript will call the relevant script.
* Functions annotated with @:hookable can be reassigned.
* NOTE: If you receive the following error when making a function use @:hookable:
* `Cannot access this or other member field in variable initialization`
* This is because you need to perform calls and assignments using a static variable referencing the target object.
*/
@:hscript({
// ALL of these values are added to ALL scripts in the child classes.
context: [FlxG, FlxSprite, Math, Paths, Std]
})
// @:autoBuild(funkin.util.macro.HookableMacro.build())
@:autoBuild(funkin.util.macro.HookableMacro.build())
interface IHook extends HScriptable {}

View file

@ -17,15 +17,24 @@ interface IScriptedClass
}
/**
* Defines a set of callbacks available to scripted classes that involve player input.
* Defines a set of callbacks available to scripted classes which can follow the game between states.
*/
interface IInputScriptedClass extends IScriptedClass
interface IStateChangingScriptedClass extends IScriptedClass
{
public function onKeyDown(event:KeyboardInputScriptEvent):Void;
public function onKeyUp(event:KeyboardInputScriptEvent):Void;
// TODO: OnMouseDown, OnMouseUp, OnMouseMove
public function onStateChangeBegin(event:StateChangeScriptEvent):Void;
public function onStateChangeEnd(event:StateChangeScriptEvent):Void;
}
/**
* Developer note:
*
* I previously considered adding events for onKeyDown, onKeyUp, mouse events, etc.
* However, I realized that you can simply call something like the following within a module:
* `FlxG.stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);`
* This is more efficient than adding an entire event handler for every key press.
*
* -Eric
*/
/**
* Defines a set of callbacks available to scripted classes that involve the lifecycle of the Play State.
*/

View file

@ -165,6 +165,18 @@ class ScriptEvent
*/
public static inline final SONG_LOADED:ScriptEventType = "SONG_LOADED";
/**
* Called when the game is entering the current FlxState.
*
* This event is not cancelable.
*/
public static inline final STATE_ENTER:ScriptEventType = "STATE_ENTER";
/**
* Called when the game is exiting the current FlxState.
*
* This event is not cancelable.
*/
/**
* If true, the behavior associated with this event can be prevented.
* For example, cancelling COUNTDOWN_BEGIN should prevent the countdown from starting,
@ -385,3 +397,19 @@ class SongLoadScriptEvent extends ScriptEvent
return 'SongLoadScriptEvent(notes=$noteStr, id=$id, difficulty=$difficulty)';
}
}
/**
* An event that is fired when moving out of or into an FlxState.
*/
class StateChangeScriptEvent extends ScriptEvent
{
public function new(type:ScriptEventType):Void
{
super(type, false);
}
public override function toString():String
{
return 'StateChangeScriptEvent(type=' + type + ')';
}
}

View file

@ -1,7 +1,6 @@
package funkin.modding.events;
import funkin.modding.IScriptedClass;
import funkin.modding.IScriptedClass.IInputScriptedClass;
import funkin.modding.IScriptedClass.IPlayStateScriptedClass;
/**
@ -36,16 +35,14 @@ class ScriptEventDispatcher
return;
}
if (Std.isOfType(target, IInputScriptedClass))
if (Std.isOfType(target, IStateChangingScriptedClass))
{
var t = cast(target, IInputScriptedClass);
var t = cast(target, IStateChangingScriptedClass);
var t = cast(target, IPlayStateScriptedClass);
switch (event.type)
{
case ScriptEvent.KEY_DOWN:
t.onKeyDown(cast event);
return;
case ScriptEvent.KEY_UP:
t.onKeyUp(cast event);
case ScriptEvent.NOTE_HIT:
t.onNoteHit(cast event);
return;
}
}

View file

@ -7,13 +7,13 @@ 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;
import funkin.modding.IScriptedClass.IStateChangingScriptedClass;
/**
* 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
class Module implements IPlayStateScriptedClass implements IStateChangingScriptedClass
{
/**
* Whether the module is currently active.
@ -68,16 +68,20 @@ class Module implements IInputScriptedClass implements IPlayStateScriptedClass
public function onScriptEvent(event:ScriptEvent) {}
/**
* Called when the module is first created.
* This happens before the title screen appears!
*/
public function onCreate(event:ScriptEvent) {}
/**
* Called when a module is destroyed.
* This currently only happens when reloading modules with F5.
*/
public function onDestroy(event:ScriptEvent) {}
public function onUpdate(event:UpdateScriptEvent) {}
public function onKeyDown(event:KeyboardInputScriptEvent) {}
public function onKeyUp(event:KeyboardInputScriptEvent) {}
public function onPause(event:ScriptEvent) {}
public function onResume(event:ScriptEvent) {}
@ -107,4 +111,8 @@ class Module implements IInputScriptedClass implements IPlayStateScriptedClass
public function onCountdownEnd(event:CountdownScriptEvent) {}
public function onSongLoaded(eent:SongLoadScriptEvent) {}
public function onStateChangeBegin(event:StateChangeScriptEvent) {}
public function onStateChangeEnd(event:StateChangeScriptEvent) {}
}

View file

@ -96,10 +96,22 @@ class ModuleHandler
}
}
/**
* Clear the module cache, forcing all modules to call shutdown events.
*/
public static function clearModuleCache():Void
{
if (moduleCache != null)
{
var event = new ScriptEvent(ScriptEvent.DESTROY, false);
// Note: Ignore stopPropagation()
for (key => value in moduleCache)
{
ScriptEventDispatcher.callEvent(value, event);
moduleCache.remove(key);
}
moduleCache.clear();
modulePriorityOrder = [];
}

View file

@ -4,8 +4,6 @@ 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;

View file

@ -1,6 +1,6 @@
package funkin.play;
import funkin.play.Strumline.StrumlineStyle;
import funkin.play.Strumline.StrumlineArrow;
import flixel.addons.effects.FlxTrail;
import flixel.addons.transition.FlxTransitionableState;
import flixel.FlxCamera;
@ -24,12 +24,13 @@ import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEvent.SongTimeScriptEvent;
import funkin.modding.events.ScriptEvent.UpdateScriptEvent;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.modding.IHook;
import funkin.modding.module.ModuleHandler;
import funkin.Note;
import funkin.play.stage.Stage;
import funkin.play.stage.StageData;
import funkin.play.Strumline.StrumlineStyle;
import funkin.Section.SwagSection;
import funkin.shaderslmfao.ColorSwap;
import funkin.SongLoad.SwagSong;
import funkin.ui.PopUpStuff;
import funkin.ui.PreferencesMenu;
@ -43,7 +44,7 @@ using StringTools;
import Discord.DiscordClient;
#end
class PlayState extends MusicBeatState
class PlayState extends MusicBeatState implements IHook
{
/**
* STATIC VARIABLES
@ -139,11 +140,6 @@ class PlayState extends MusicBeatState
*/
private var inactiveNotes:Array<Note>;
/**
* An object which the strumline (and its notes) are positioned relative to.
*/
private var strumlineAnchor:FlxObject;
/**
* If true, the player is allowed to pause the game.
* Disabled during the ending of a song.
@ -231,7 +227,6 @@ class PlayState extends MusicBeatState
private var vocals:VoicesGroup;
private var vocalsFinished:Bool = false;
private var playerStrums:FlxTypedGroup<FlxSprite>;
private var camZooming:Bool = false;
private var gfSpeed:Int = 1;
private var combo:Int = 0;
@ -331,8 +326,6 @@ class PlayState extends MusicBeatState
add(grpNoteSplashes);
playerStrums = new FlxTypedGroup<FlxSprite>();
generateSong();
cameraFollowPoint = new FlxObject(0, 0, 1, 1);
@ -407,6 +400,10 @@ class PlayState extends MusicBeatState
case 'guns':
VanillaCutscenes.playGunsCutscene();
default:
// VanillaCutscenes will call startCountdown later.
// TODO: Alternatively: make a song script that allows startCountdown to be called,
// then cancels the countdown, hides the strumline, plays the cutscene,
// then calls Countdown.performCountdown()
startCountdown();
}
}
@ -415,8 +412,9 @@ class PlayState extends MusicBeatState
startCountdown();
}
// this.leftWatermarkText.text = '${currentSong.song.toUpperCase()} - ${SongLoad.curDiff.toUpperCase()}';
#if debug
this.rightWatermarkText.text = Constants.VERSION;
#end
}
/**
@ -936,6 +934,7 @@ class PlayState extends MusicBeatState
super.update(elapsed);
updateHealthBar();
updateScoreText();
if (needsReset)
{
@ -1173,7 +1172,7 @@ class PlayState extends MusicBeatState
daNote.active = true;
}
var strumLineMid = playerStrumline.offset.y + Note.swagWidth / 2;
var strumLineMid = playerStrumline.y + Note.swagWidth / 2;
if (daNote.followsTime)
daNote.y = (Conductor.songPosition - daNote.data.strumTime) * (0.45 * FlxMath.roundDecimal(SongLoad.getSpeed(),
@ -1181,7 +1180,7 @@ class PlayState extends MusicBeatState
if (PreferencesMenu.getPref('downscroll'))
{
daNote.y += playerStrumline.offset.y;
daNote.y += playerStrumline.y;
if (daNote.isSustainNote)
{
if (daNote.animation.curAnim.name.endsWith("end") && daNote.prevNote != null)
@ -1199,7 +1198,7 @@ class PlayState extends MusicBeatState
else
{
if (daNote.followsTime)
daNote.y = playerStrumline.offset.y - daNote.y;
daNote.y = playerStrumline.y - daNote.y;
if (daNote.isSustainNote
&& (!daNote.mustPress || (daNote.wasGoodHit || (daNote.prevNote.wasGoodHit && !daNote.canBeHit)))
&& daNote.y + daNote.offset.y * daNote.scale.y <= strumLineMid)
@ -1284,7 +1283,7 @@ class PlayState extends MusicBeatState
}
if (!isInCutscene)
keyShit();
keyShit(true);
dispatchEvent(new UpdateScriptEvent(elapsed));
}
@ -1293,7 +1292,7 @@ class PlayState extends MusicBeatState
{
// clipRect is applied to graphic itself so use frame Heights
var swagRect:FlxRect = new FlxRect(0, 0, daNote.frameWidth, daNote.frameHeight);
var strumLineMid = playerStrumline.offset.y + Note.swagWidth / 2;
var strumLineMid = playerStrumline.y + Note.swagWidth / 2;
if (PreferencesMenu.getPref('downscroll'))
{
@ -1549,7 +1548,14 @@ class PlayState extends MusicBeatState
}
}
private function keyShit():Void
public var test:(PlayState) -> Void = function(instance:PlayState)
{
trace('test');
trace(instance.currentStageId);
};
@:hookable
public function keyShit(test:Bool):Void
{
// control arrays, order L D R U
var holdArray:Array<Bool> = [controls.NOTE_LEFT, controls.NOTE_DOWN, controls.NOTE_UP, controls.NOTE_RIGHT];
@ -1566,27 +1572,27 @@ class PlayState extends MusicBeatState
controls.NOTE_RIGHT_R
];
// HOLDS, check for sustain notes
if (holdArray.contains(true) && generatedMusic)
if (holdArray.contains(true) && PlayState.instance.generatedMusic)
{
activeNotes.forEachAlive(function(daNote:Note)
PlayState.instance.activeNotes.forEachAlive(function(daNote:Note)
{
if (daNote.isSustainNote && daNote.canBeHit && daNote.mustPress && holdArray[daNote.data.noteData])
goodNoteHit(daNote);
PlayState.instance.goodNoteHit(daNote);
});
}
// PRESSES, check for note hits
if (pressArray.contains(true) && generatedMusic)
if (pressArray.contains(true) && PlayState.instance.generatedMusic)
{
Haptic.vibrate(100, 100);
currentStage.getBoyfriend().holdTimer = 0;
PlayState.instance.currentStage.getBoyfriend().holdTimer = 0;
var possibleNotes:Array<Note> = []; // notes that can be hit
var directionList:Array<Int> = []; // directions that can be hit
var dumbNotes:Array<Note> = []; // notes to kill later
activeNotes.forEachAlive(function(daNote:Note)
PlayState.instance.activeNotes.forEachAlive(function(daNote:Note)
{
if (daNote.canBeHit && daNote.mustPress && !daNote.tooLate && !daNote.wasGoodHit)
{
@ -1621,63 +1627,60 @@ class PlayState extends MusicBeatState
{
FlxG.log.add("killing dumb ass note at " + note.data.strumTime);
note.kill();
activeNotes.remove(note, true);
PlayState.instance.activeNotes.remove(note, true);
note.destroy();
}
possibleNotes.sort((a, b) -> Std.int(a.data.strumTime - b.data.strumTime));
if (perfectMode)
goodNoteHit(possibleNotes[0]);
if (PlayState.instance.perfectMode)
PlayState.instance.goodNoteHit(possibleNotes[0]);
else if (possibleNotes.length > 0)
{
for (shit in 0...pressArray.length)
{ // if a direction is hit that shouldn't be
if (pressArray[shit] && !directionList.contains(shit))
noteMiss(shit);
PlayState.instance.noteMiss(shit);
}
for (coolNote in possibleNotes)
{
if (pressArray[coolNote.data.noteData])
goodNoteHit(coolNote);
PlayState.instance.goodNoteHit(coolNote);
}
}
else
{
for (shit in 0...pressArray.length)
if (pressArray[shit])
noteMiss(shit);
PlayState.instance.noteMiss(shit);
}
}
if (currentStage == null)
if (PlayState.instance.currentStage == null)
return;
if (currentStage.getBoyfriend().holdTimer > Conductor.stepCrochet * 4 * 0.001 && !holdArray.contains(true))
if (PlayState.instance.currentStage.getBoyfriend().holdTimer > Conductor.stepCrochet * 4 * 0.001 && !holdArray.contains(true))
{
if (currentStage.getBoyfriend().animation != null
&& currentStage.getBoyfriend().animation.curAnim.name.startsWith('sing')
&& !currentStage.getBoyfriend().animation.curAnim.name.endsWith('miss'))
if (PlayState.instance.currentStage.getBoyfriend().animation != null
&& PlayState.instance.currentStage.getBoyfriend().animation.curAnim.name.startsWith('sing')
&& !PlayState.instance.currentStage.getBoyfriend().animation.curAnim.name.endsWith('miss'))
{
currentStage.getBoyfriend().playAnim('idle');
PlayState.instance.currentStage.getBoyfriend().playAnim('idle');
}
}
playerStrums.forEach(function(spr:FlxSprite)
for (keyId => isPressed in pressArray)
{
if (pressArray[spr.ID] && spr.animation.curAnim.name != 'confirm')
spr.animation.play('pressed');
if (!holdArray[spr.ID])
spr.animation.play('static');
var arrow:StrumlineArrow = PlayState.instance.playerStrumline.getArrow(keyId);
if (spr.animation.curAnim.name == 'confirm' && !currentStageId.startsWith('school'))
if (isPressed && arrow.animation.curAnim.name != 'confirm')
{
spr.centerOffsets();
spr.offset.x -= 13;
spr.offset.y -= 13;
arrow.playAnimation('pressed');
}
else
spr.centerOffsets();
});
if (!holdArray[keyId])
{
arrow.playAnimation('static');
}
}
}
function noteMiss(direction:NoteDir = 1):Void
@ -1707,13 +1710,7 @@ class PlayState extends MusicBeatState
currentStage.getBoyfriend().playAnim('sing' + note.dirNameUpper, true);
playerStrums.forEach(function(spr:FlxSprite)
{
if (Math.abs(note.data.noteData) == spr.ID)
{
spr.animation.play('confirm', true);
}
});
playerStrumline.getArrow(note.data.noteData).playAnimation('confirm', true);
note.wasGoodHit = true;
vocals.volume = 1;
@ -1848,19 +1845,31 @@ class PlayState extends MusicBeatState
var strumlineYPos = Strumline.getYPos();
playerStrumline = new Strumline(0, strumlineStyle, 4);
playerStrumline.offset = new FlxPoint(50 + FlxG.width / 2, strumlineYPos);
playerStrumline.x = 50 + FlxG.width / 2;
playerStrumline.y = strumlineYPos;
// Set the z-index so they don't appear in front of notes.
playerStrumline.zIndex = 100;
add(playerStrumline);
playerStrumline.cameras = [camHUD];
if (!isStoryMode)
{
playerStrumline.fadeInArrows();
}
enemyStrumline = new Strumline(1, strumlineStyle, 4);
enemyStrumline.offset = new FlxPoint(50, strumlineYPos);
enemyStrumline.x = 50;
enemyStrumline.y = strumlineYPos;
// Set the z-index so they don't appear in front of notes.
enemyStrumline.zIndex = 100;
add(enemyStrumline);
enemyStrumline.cameras = [camHUD];
if (!isStoryMode)
{
enemyStrumline.fadeInArrows();
}
this.refresh();
}
@ -1997,6 +2006,7 @@ class PlayState extends MusicBeatState
{
remove(currentStage);
currentStage.kill();
dispatchEvent(new ScriptEvent(ScriptEvent.DESTROY, false));
currentStage = null;
}

View file

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

View file

@ -61,7 +61,8 @@ class VanillaCutscenes
blackScreen = null;
FlxTween.tween(FlxG.camera, {zoom: PlayState.defaultCameraZoom}, (Conductor.crochet / 1000) * 5, {ease: FlxEase.quadInOut});
Countdown.performCountdown(false);
@:privateAccess
PlayState.instance.startCountdown();
@:privateAccess
PlayState.instance.cameraMovement();
}

View file

@ -19,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 implements IPlayStateScriptedClass implements IInputScriptedClass
class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScriptedClass
{
public final stageId:String;
public final stageName:String;
@ -312,7 +312,8 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte
}
/**
* Perform cleanup for when you are leaving the level.
* onDestroy gets called when the player is leaving the PlayState,
* and is used to clean up any objects that need to be destroyed.
*/
public function onDestroy(event:ScriptEvent):Void
{
@ -322,24 +323,32 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte
for (prop in this.namedProps)
{
remove(prop);
prop.kill();
prop.destroy();
}
namedProps.clear();
for (char in this.characters)
{
remove(char);
char.kill();
char.destroy();
}
characters.clear();
for (bopper in boppers)
{
remove(bopper);
bopper.kill();
bopper.destroy();
}
boppers = [];
for (sprite in this.group)
{
remove(sprite);
sprite.kill();
sprite.destroy();
}
group.clear();
@ -391,10 +400,6 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte
public function onCountdownEnd(event:CountdownScriptEvent) {}
public function onKeyDown(event:KeyboardInputScriptEvent) {}
public function onKeyUp(event:KeyboardInputScriptEvent) {}
/**
* A function that should get called every frame.
*/

View file

@ -18,8 +18,17 @@ class Constants
public static final VERSION_SUFFIX = ' PROTOTYPE';
public static var VERSION(get, null):String;
#if debug
public static final GIT_HASH = funkin.util.macro.GitCommit.getGitCommitHash();
static function get_VERSION():String
{
return 'v${Application.current.meta.get('version')} (${GIT_HASH})' + VERSION_SUFFIX;
}
#else
static function get_VERSION():String
{
return 'v${Application.current.meta.get('version')}' + VERSION_SUFFIX;
}
#end
}

View file

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

View file

@ -1,3 +1,70 @@
package funkin.util.macro;
class HookableMacro {}
import haxe.macro.Context;
import haxe.macro.Expr;
using Lambda;
class HookableMacro
{
/**
* The @:hookable annotation replaces a given function with a variable that contains a function.
* It's still callable, like normal, but now you can also replace the value! Neat!
*
* NOTE: If you receive the following error when making a function use @:hookable:
* `Cannot access this or other member field in variable initialization`
* This is because you need to perform calls and assignments using a static variable referencing the target object.
*/
public static macro function build():Array<Field>
{
Context.info('Running HookableMacro...', Context.currentPos());
var cls:haxe.macro.Type.ClassType = Context.getLocalClass().get();
var fields:Array<Field> = Context.getBuildFields();
// Find all fields with @:hookable metadata
for (field in fields)
{
if (field.meta == null)
continue;
var scriptable_meta = field.meta.find(function(m) return m.name == ':hookable');
if (scriptable_meta != null)
{
Context.info(' @:hookable annotation found on field ${field.name}', Context.currentPos());
switch (field.kind)
{
case FFun(originalFunc):
// This is the type of the function, like (Int, Int) -> Int
var replFieldTypeRet:ComplexType = originalFunc.ret == null ? Context.toComplexType(Context.getType('Void')) : originalFunc.ret;
var replFieldType:ComplexType = TFunction([for (arg in originalFunc.args) arg.type], replFieldTypeRet);
// This is the expression of the function, i.e. the function body.
var replFieldExpr:ExprDef = EFunction(FAnonymous, {
ret: originalFunc.ret,
params: originalFunc.params,
args: originalFunc.args,
expr: originalFunc.expr
});
var replField:Field = {
name: field.name,
doc: field.doc,
access: field.access,
pos: field.pos,
meta: field.meta,
kind: FVar(replFieldType, {
expr: replFieldExpr,
pos: field.pos
}),
};
// Replace the original field with the new field
fields[fields.indexOf(field)] = replField;
default:
Context.error('@:hookable can only be used on functions', field.pos);
}
}
}
return fields;
}
}