mirror of
https://github.com/FunkinCrew/Funkin.git
synced 2025-04-12 06:54:41 -04:00
First iteration of stable custom characters, plus some other neat changes.
This commit is contained in:
parent
1a85b4a1ce
commit
a7d338becb
36 changed files with 1744 additions and 812 deletions
Project.xml
example_mods/introMod/images
source
Main.hx
funkin
Character.hxGameOverSubstate.hxMusicBeatState.hxMusicBeatSubstate.hxNote.hxStoryMenuState.hx
i18n
modding
play
AnimationData.hxCountdown.hxPlayState.hx
character
BaseCharacter.hxCharacterBase.hxCharacterData.hxMultiSparrowCharacter.hxPackerCharacter.hxScriptedCharacter.hxSparrowCharacter.hx
render
stage
ui
util
|
@ -122,7 +122,8 @@
|
|||
<!--haxelib name="newgrounds" unless="switch"/> -->
|
||||
<haxelib name="faxe" if='switch' />
|
||||
<haxelib name="polymod" />
|
||||
<haxelib name="firetongue" />
|
||||
|
||||
<haxelib name="thx.semver" />
|
||||
|
||||
<!-- <haxelib name="colyseus"/> -->
|
||||
<!-- <haxelib name="colyseus-websocket" /> -->
|
||||
|
|
Binary file not shown.
Before ![]() (image error) Size: 2.3 MiB |
|
@ -46,8 +46,6 @@ class Main extends Sprite
|
|||
// 4. Replace the call to PolymodHandler.loadAllMods() with a call to PolymodHandler.loadModsById(ids:Array<String>).
|
||||
funkin.modding.PolymodHandler.loadAllMods();
|
||||
|
||||
funkin.i18n.FireTongueHandler.init();
|
||||
|
||||
if (stage != null)
|
||||
{
|
||||
init();
|
||||
|
|
|
@ -57,7 +57,6 @@ class Character extends FlxSprite
|
|||
loadOffsetFile(curCharacter);
|
||||
|
||||
playAnim('danceRight');
|
||||
|
||||
case 'gf-christmas':
|
||||
tex = Paths.getSparrowAtlas('characters/gfChristmas');
|
||||
frames = tex;
|
||||
|
@ -133,19 +132,6 @@ class Character extends FlxSprite
|
|||
updateHitbox();
|
||||
antialiasing = false;
|
||||
|
||||
case 'dad':
|
||||
// DAD ANIMATION LOADING CODE
|
||||
tex = Paths.getSparrowAtlas('characters/DADDY_DEAREST');
|
||||
frames = tex;
|
||||
quickAnimAdd('idle', 'Dad idle dance');
|
||||
quickAnimAdd('singUP', 'Dad Sing Note UP');
|
||||
quickAnimAdd('singRIGHT', 'Dad Sing Note RIGHT');
|
||||
quickAnimAdd('singDOWN', 'Dad Sing Note DOWN');
|
||||
quickAnimAdd('singLEFT', 'Dad Sing Note LEFT');
|
||||
|
||||
loadOffsetFile(curCharacter);
|
||||
|
||||
playAnim('idle');
|
||||
case 'spooky':
|
||||
tex = Paths.getSparrowAtlas('characters/spooky_kids_assets');
|
||||
frames = tex;
|
||||
|
@ -259,36 +245,6 @@ class Character extends FlxSprite
|
|||
|
||||
loadMappedAnims();
|
||||
|
||||
case 'bf':
|
||||
var tex = Paths.getSparrowAtlas('characters/BOYFRIEND');
|
||||
frames = tex;
|
||||
quickAnimAdd('idle', 'BF idle dance');
|
||||
quickAnimAdd('singUP', 'BF NOTE UP0');
|
||||
quickAnimAdd('singLEFT', 'BF NOTE LEFT0');
|
||||
quickAnimAdd('singRIGHT', 'BF NOTE RIGHT0');
|
||||
quickAnimAdd('singDOWN', 'BF NOTE DOWN0');
|
||||
quickAnimAdd('singUPmiss', 'BF NOTE UP MISS');
|
||||
quickAnimAdd('singLEFTmiss', 'BF NOTE LEFT MISS');
|
||||
quickAnimAdd('singRIGHTmiss', 'BF NOTE RIGHT MISS');
|
||||
quickAnimAdd('singDOWNmiss', 'BF NOTE DOWN MISS');
|
||||
quickAnimAdd('preAttack', 'bf pre attack');
|
||||
quickAnimAdd('attack', 'boyfriend attack');
|
||||
quickAnimAdd('hey', 'BF HEY');
|
||||
|
||||
quickAnimAdd('firstDeath', "BF dies");
|
||||
animation.addByPrefix('deathLoop', "BF Dead Loop", 24, true);
|
||||
quickAnimAdd('deathConfirm', "BF Dead confirm");
|
||||
|
||||
animation.addByPrefix('scared', 'BF idle shaking', 24, true);
|
||||
|
||||
loadOffsetFile(curCharacter);
|
||||
|
||||
playAnim('idle');
|
||||
|
||||
flipX = true;
|
||||
|
||||
loadOffsetFile(curCharacter);
|
||||
|
||||
case 'bf-christmas':
|
||||
var tex = Paths.getSparrowAtlas('characters/bfChristmas');
|
||||
frames = tex;
|
||||
|
@ -693,7 +649,7 @@ class Character extends FlxSprite
|
|||
*/
|
||||
public function dance()
|
||||
{
|
||||
if (animation == null)
|
||||
if (animation == null || animation.curAnim == null)
|
||||
return;
|
||||
if (!debugMode)
|
||||
{
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
package funkin;
|
||||
|
||||
import funkin.modding.events.ScriptEventDispatcher;
|
||||
import funkin.modding.events.ScriptEvent;
|
||||
import funkin.play.character.BaseCharacter;
|
||||
import flixel.FlxObject;
|
||||
import flixel.FlxSubState;
|
||||
import flixel.math.FlxPoint;
|
||||
|
@ -10,80 +13,83 @@ import haxe.display.Display;
|
|||
import funkin.ui.PreferencesMenu;
|
||||
import funkin.play.PlayState;
|
||||
|
||||
using StringTools;
|
||||
|
||||
/**
|
||||
* A substate which renders over the PlayState when the player dies.
|
||||
* Displays the player death animation, plays the music, and handles restarting the song.
|
||||
*
|
||||
* The newest implementation uses a substate, which prevents having to reload the song and stage each reset.
|
||||
*/
|
||||
class GameOverSubstate extends MusicBeatSubstate
|
||||
{
|
||||
var bf:Boyfriend;
|
||||
var camFollow:FlxObject;
|
||||
/**
|
||||
* The boyfriend character.
|
||||
*/
|
||||
var boyfriend:BaseCharacter;
|
||||
|
||||
var stageSuffix:String = "";
|
||||
var randomGameover:Int = 1;
|
||||
/**
|
||||
* The invisible object in the scene which the camera focuses on.
|
||||
*/
|
||||
var cameraFollowPoint:FlxObject;
|
||||
|
||||
var gameOverMusic:FlxSound;
|
||||
/**
|
||||
* The music playing in the background of the state.
|
||||
*/
|
||||
var gameOverMusic:FlxSound = new FlxSound();
|
||||
|
||||
/**
|
||||
* Whether the player has confirmed and prepared to restart the level.
|
||||
* This means the animation and transition have already started.
|
||||
*/
|
||||
var isEnding:Bool = false;
|
||||
|
||||
/**
|
||||
* Music variant to use.
|
||||
* TODO: De-hardcode this somehow.
|
||||
*/
|
||||
var musicVariant:String = "";
|
||||
|
||||
public function new()
|
||||
{
|
||||
gameOverMusic = new FlxSound();
|
||||
FlxG.sound.list.add(gameOverMusic);
|
||||
|
||||
var daStage = PlayState.instance.currentStageId;
|
||||
var daBf:String = '';
|
||||
switch (daStage)
|
||||
{
|
||||
case 'school' | 'schoolEvil':
|
||||
stageSuffix = '-pixel';
|
||||
daBf = 'bf-pixel-dead';
|
||||
default:
|
||||
daBf = 'bf';
|
||||
}
|
||||
|
||||
var daSong = PlayState.currentSong.song.toLowerCase();
|
||||
|
||||
switch (daSong)
|
||||
{
|
||||
case 'stress':
|
||||
daBf = 'bf-holding-gf-dead';
|
||||
}
|
||||
|
||||
super();
|
||||
|
||||
FlxG.sound.list.add(gameOverMusic);
|
||||
gameOverMusic.stop();
|
||||
|
||||
Conductor.songPosition = 0;
|
||||
|
||||
var bfXPos = PlayState.instance.currentStage.getBoyfriend().getScreenPosition().x;
|
||||
var bfYPos = PlayState.instance.currentStage.getBoyfriend().getScreenPosition().y;
|
||||
bf = new Boyfriend(bfXPos, bfYPos, daBf);
|
||||
add(bf);
|
||||
playBlueBalledSFX();
|
||||
|
||||
camFollow = new FlxObject(bf.getGraphicMidpoint().x, bf.getGraphicMidpoint().y, 1, 1);
|
||||
add(camFollow);
|
||||
|
||||
FlxG.sound.play(Paths.sound('fnf_loss_sfx' + stageSuffix));
|
||||
// Conductor.changeBPM(100);
|
||||
|
||||
switch (PlayState.currentSong.player1)
|
||||
switch (PlayState.instance.currentStageId)
|
||||
{
|
||||
case 'pico':
|
||||
stageSuffix = 'Pico';
|
||||
case 'school' | 'schoolEvil':
|
||||
musicVariant = "-pixel";
|
||||
default:
|
||||
if (PlayState.instance.currentStage.getBoyfriend().characterId == 'pico')
|
||||
{
|
||||
musicVariant = "Pico";
|
||||
}
|
||||
else
|
||||
{
|
||||
musicVariant = "";
|
||||
}
|
||||
}
|
||||
|
||||
// FlxG.camera.followLerp = 1;
|
||||
// FlxG.camera.focusOn(FlxPoint.get(FlxG.width / 2, FlxG.height / 2));
|
||||
// We have to remove boyfriend from the stage. Then we can add him back at the end.
|
||||
boyfriend = PlayState.instance.currentStage.getBoyfriend(true);
|
||||
boyfriend.isDead = true;
|
||||
boyfriend.playAnimation('firstDeath');
|
||||
add(boyfriend);
|
||||
|
||||
// commented out for now
|
||||
FlxG.camera.scroll.set();
|
||||
cameraFollowPoint = new FlxObject(PlayState.instance.cameraFollowPoint.x, PlayState.instance.cameraFollowPoint.y, 1, 1);
|
||||
add(cameraFollowPoint);
|
||||
|
||||
// FlxG.camera.scroll.set();
|
||||
FlxG.camera.target = null;
|
||||
|
||||
bf.playAnim('firstDeath');
|
||||
|
||||
var randomCensor:Array<Int> = [];
|
||||
|
||||
if (PreferencesMenu.getPref('censor-naughty'))
|
||||
randomCensor = [1, 3, 8, 13, 17, 21];
|
||||
|
||||
randomGameover = FlxG.random.int(1, 25, randomCensor);
|
||||
FlxG.camera.follow(cameraFollowPoint, LOCKON, 0.01);
|
||||
}
|
||||
|
||||
var playingDeathSound:Bool = false;
|
||||
|
||||
override function update(elapsed:Float)
|
||||
{
|
||||
// makes the lerp non-dependant on the framerate
|
||||
|
@ -96,14 +102,14 @@ class GameOverSubstate extends MusicBeatSubstate
|
|||
var touch = FlxG.touches.getFirst();
|
||||
if (touch != null)
|
||||
{
|
||||
if (touch.overlaps(bf))
|
||||
endBullshit();
|
||||
if (touch.overlaps(boyfriend))
|
||||
confirmDeath();
|
||||
}
|
||||
}
|
||||
|
||||
if (controls.ACCEPT)
|
||||
{
|
||||
endBullshit();
|
||||
confirmDeath();
|
||||
}
|
||||
|
||||
if (controls.BACK)
|
||||
|
@ -119,74 +125,129 @@ class GameOverSubstate extends MusicBeatSubstate
|
|||
FlxG.switchState(new FreeplayState());
|
||||
}
|
||||
|
||||
if (bf.animation.curAnim.name == 'firstDeath' && bf.animation.curAnim.curFrame == 12)
|
||||
// Start panning the camera to BF after 12 frames.
|
||||
// TODO: Should this be de-hardcoded?
|
||||
if (boyfriend.getCurrentAnimation().startsWith('firstDeath') && boyfriend.animation.curAnim.curFrame == 12)
|
||||
{
|
||||
FlxG.camera.follow(camFollow, LOCKON, 0.01);
|
||||
}
|
||||
|
||||
switch (PlayState.storyWeek)
|
||||
{
|
||||
case 7:
|
||||
if (bf.animation.curAnim.name == 'firstDeath' && bf.animation.curAnim.finished && !playingDeathSound)
|
||||
{
|
||||
playingDeathSound = true;
|
||||
|
||||
bf.startedDeath = true;
|
||||
coolStartDeath(0.2);
|
||||
|
||||
FlxG.sound.play(Paths.sound('jeffGameover/jeffGameover-' + randomGameover), 1, false, null, true, function()
|
||||
{
|
||||
if (!isEnding)
|
||||
{
|
||||
gameOverMusic.fadeIn(4, 0.2, 1);
|
||||
}
|
||||
// FlxG.sound.music.fadeIn(4, 0.2, 1);
|
||||
});
|
||||
}
|
||||
default:
|
||||
if (bf.animation.curAnim.name == 'firstDeath' && bf.animation.curAnim.finished)
|
||||
{
|
||||
bf.startedDeath = true;
|
||||
coolStartDeath();
|
||||
}
|
||||
cameraFollowPoint.x = boyfriend.getGraphicMidpoint().x;
|
||||
cameraFollowPoint.y = boyfriend.getGraphicMidpoint().y;
|
||||
}
|
||||
|
||||
if (gameOverMusic.playing)
|
||||
{
|
||||
Conductor.songPosition = gameOverMusic.time;
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (PlayState.storyWeek)
|
||||
{
|
||||
case 7:
|
||||
if (boyfriend.getCurrentAnimation().startsWith('firstDeath') && boyfriend.isAnimationFinished() && !playingJeffQuote)
|
||||
{
|
||||
playingJeffQuote = true;
|
||||
playJeffQuote();
|
||||
|
||||
startDeathMusic(0.2);
|
||||
}
|
||||
default:
|
||||
if (boyfriend.getCurrentAnimation().startsWith('firstDeath') && boyfriend.isAnimationFinished())
|
||||
{
|
||||
startDeathMusic();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispatchEvent(new UpdateScriptEvent(elapsed));
|
||||
}
|
||||
|
||||
private function coolStartDeath(?vol:Float = 1):Void
|
||||
override function dispatchEvent(event:ScriptEvent)
|
||||
{
|
||||
super.dispatchEvent(event);
|
||||
|
||||
ScriptEventDispatcher.callEvent(boyfriend, event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the death music at the appropriate volume.
|
||||
* @param startingVolume
|
||||
*/
|
||||
function startDeathMusic(?startingVolume:Float = 1):Void
|
||||
{
|
||||
if (!isEnding)
|
||||
{
|
||||
gameOverMusic.loadEmbedded(Paths.music('gameOver' + stageSuffix));
|
||||
gameOverMusic.volume = vol;
|
||||
gameOverMusic.loadEmbedded(Paths.music('gameOver' + musicVariant));
|
||||
gameOverMusic.volume = startingVolume;
|
||||
gameOverMusic.play();
|
||||
}
|
||||
else
|
||||
{
|
||||
gameOverMusic.loadEmbedded(Paths.music('gameOverEnd' + musicVariant));
|
||||
gameOverMusic.volume = startingVolume;
|
||||
gameOverMusic.play();
|
||||
}
|
||||
// FlxG.sound.playMusic();
|
||||
}
|
||||
|
||||
var isEnding:Bool = false;
|
||||
/**
|
||||
* Play the sound effect that occurs when
|
||||
* boyfriend's testicles get utterly annihilated.
|
||||
*/
|
||||
function playBlueBalledSFX()
|
||||
{
|
||||
FlxG.sound.play(Paths.sound('fnf_loss_sfx' + musicVariant));
|
||||
}
|
||||
|
||||
function endBullshit():Void
|
||||
var playingJeffQuote:Bool = false;
|
||||
|
||||
/**
|
||||
* Week 7-specific hardcoded behavior, to play a custom death quote.
|
||||
* TODO: Make this a module somehow.
|
||||
*/
|
||||
function playJeffQuote()
|
||||
{
|
||||
var randomCensor:Array<Int> = [];
|
||||
|
||||
if (PreferencesMenu.getPref('censor-naughty'))
|
||||
randomCensor = [1, 3, 8, 13, 17, 21];
|
||||
|
||||
FlxG.sound.play(Paths.sound('jeffGameover/jeffGameover-' + FlxG.random.int(1, 25, randomCensor)), 1, false, null, true, function()
|
||||
{
|
||||
// Once the quote ends, fade in the game over music.
|
||||
if (!isEnding && gameOverMusic != null)
|
||||
{
|
||||
gameOverMusic.fadeIn(4, 0.2, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Do behavior which occurs when you confirm and move to restart the level.
|
||||
*/
|
||||
function confirmDeath():Void
|
||||
{
|
||||
if (!isEnding)
|
||||
{
|
||||
isEnding = true;
|
||||
bf.playAnim('deathConfirm', true);
|
||||
gameOverMusic.stop();
|
||||
// FlxG.sound.music.stop();
|
||||
FlxG.sound.play(Paths.music('gameOverEnd' + stageSuffix));
|
||||
startDeathMusic(); // isEnding changes this function's behavior.
|
||||
|
||||
boyfriend.playAnimation('deathConfirm', true);
|
||||
|
||||
// After the animation finishes...
|
||||
new FlxTimer().start(0.7, function(tmr:FlxTimer)
|
||||
{
|
||||
// ...fade out the graphics. Then after that happens...
|
||||
FlxG.camera.fade(FlxColor.BLACK, 2, false, function()
|
||||
{
|
||||
// ...close the GameOverSubstate.
|
||||
FlxG.camera.fade(FlxColor.BLACK, 1, true, null, true);
|
||||
PlayState.needsReset = true;
|
||||
|
||||
// Readd Boyfriend to the stage.
|
||||
boyfriend.isDead = false;
|
||||
remove(boyfriend);
|
||||
PlayState.instance.currentStage.addCharacter(boyfriend, BF);
|
||||
|
||||
// Close the substate.
|
||||
close();
|
||||
// LoadingState.loadAndSwitchState(new PlayState());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
package funkin;
|
||||
|
||||
import funkin.play.stage.StageData.StageDataParser;
|
||||
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||
import flixel.FlxState;
|
||||
import flixel.FlxSubState;
|
||||
import flixel.util.FlxColor;
|
||||
import flixel.text.FlxText;
|
||||
import funkin.modding.events.ScriptEvent;
|
||||
|
@ -8,6 +12,10 @@ import funkin.modding.events.ScriptEvent.UpdateScriptEvent;
|
|||
import funkin.Conductor.BPMChangeEvent;
|
||||
import flixel.addons.ui.FlxUIState;
|
||||
|
||||
/**
|
||||
* MusicBeatState actually represents the core utility FlxState of the game.
|
||||
* It includes functionality for event handling, as well as maintaining BPM-based update events.
|
||||
*/
|
||||
class MusicBeatState extends FlxUIState
|
||||
{
|
||||
private var curStep:Int = 0;
|
||||
|
@ -21,6 +29,19 @@ class MusicBeatState extends FlxUIState
|
|||
public var leftWatermarkText:FlxText = null;
|
||||
public var rightWatermarkText:FlxText = null;
|
||||
|
||||
public function new()
|
||||
{
|
||||
super();
|
||||
|
||||
initCallbacks();
|
||||
}
|
||||
|
||||
function initCallbacks()
|
||||
{
|
||||
subStateOpened.add(onOpenSubstateComplete);
|
||||
subStateClosed.add(onCloseSubstateComplete);
|
||||
}
|
||||
|
||||
override function create()
|
||||
{
|
||||
super.create();
|
||||
|
@ -35,6 +56,10 @@ class MusicBeatState extends FlxUIState
|
|||
{
|
||||
super.update(elapsed);
|
||||
|
||||
// This can now be used in EVERY STATE YAY!
|
||||
if (FlxG.keys.justPressed.F5)
|
||||
debug_refreshModules();
|
||||
|
||||
// everyStep();
|
||||
var oldStep:Int = curStep;
|
||||
|
||||
|
@ -71,6 +96,23 @@ class MusicBeatState extends FlxUIState
|
|||
ModuleHandler.callEvent(event);
|
||||
}
|
||||
|
||||
function debug_refreshModules()
|
||||
{
|
||||
ModuleHandler.clearModuleCache();
|
||||
|
||||
// Forcibly reload scripts so that scripted stages can be edited.
|
||||
polymod.hscript.PolymodScriptClass.clearScriptClasses();
|
||||
polymod.hscript.PolymodScriptClass.registerAllScriptClasses();
|
||||
|
||||
// Reload the stages in cache. This might cause a lag spike but who cares this is a debug utility.
|
||||
StageDataParser.loadStageCache();
|
||||
CharacterDataParser.loadCharacterCache();
|
||||
ModuleHandler.loadModuleCache();
|
||||
|
||||
// Create a new instance of the current state class.
|
||||
FlxG.resetState();
|
||||
}
|
||||
|
||||
private function updateBeat():Void
|
||||
{
|
||||
curBeat = Math.floor(curStep / 4);
|
||||
|
@ -103,4 +145,56 @@ class MusicBeatState extends FlxUIState
|
|||
lastBeatHitTime = Conductor.songPosition;
|
||||
// do literally nothing dumbass
|
||||
}
|
||||
|
||||
override function switchTo(nextState:FlxState):Bool
|
||||
{
|
||||
var event = new StateChangeScriptEvent(ScriptEvent.STATE_CHANGE_BEGIN, nextState, true);
|
||||
|
||||
dispatchEvent(event);
|
||||
|
||||
if (event.eventCanceled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return super.switchTo(nextState);
|
||||
}
|
||||
|
||||
public override function openSubState(targetSubstate:FlxSubState):Void
|
||||
{
|
||||
var event = new SubStateScriptEvent(ScriptEvent.SUBSTATE_OPEN_BEGIN, targetSubstate, true);
|
||||
|
||||
dispatchEvent(event);
|
||||
|
||||
if (event.eventCanceled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
super.openSubState(targetSubstate);
|
||||
}
|
||||
|
||||
function onOpenSubstateComplete(targetState:FlxSubState):Void
|
||||
{
|
||||
dispatchEvent(new SubStateScriptEvent(ScriptEvent.SUBSTATE_OPEN_END, targetState, true));
|
||||
}
|
||||
|
||||
public override function closeSubState():Void
|
||||
{
|
||||
var event = new SubStateScriptEvent(ScriptEvent.SUBSTATE_CLOSE_BEGIN, this.subState, true);
|
||||
|
||||
dispatchEvent(event);
|
||||
|
||||
if (event.eventCanceled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
super.closeSubState();
|
||||
}
|
||||
|
||||
function onCloseSubstateComplete(targetState:FlxSubState):Void
|
||||
{
|
||||
dispatchEvent(new SubStateScriptEvent(ScriptEvent.SUBSTATE_CLOSE_END, targetState, true));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
package funkin;
|
||||
|
||||
import funkin.modding.module.ModuleHandler;
|
||||
import funkin.modding.events.ScriptEvent;
|
||||
import funkin.Conductor.BPMChangeEvent;
|
||||
import flixel.FlxSubState;
|
||||
|
||||
/**
|
||||
* MusicBeatSubstate reincorporates the functionality of MusicBeatState into an FlxSubState.
|
||||
*/
|
||||
class MusicBeatSubstate extends FlxSubState
|
||||
{
|
||||
public function new()
|
||||
|
@ -53,6 +58,11 @@ class MusicBeatSubstate extends FlxSubState
|
|||
beatHit();
|
||||
}
|
||||
|
||||
function dispatchEvent(event:ScriptEvent)
|
||||
{
|
||||
ModuleHandler.callEvent(event);
|
||||
}
|
||||
|
||||
public function beatHit():Void
|
||||
{
|
||||
// do literally nothing dumbass
|
||||
|
|
|
@ -227,6 +227,7 @@ class Note extends FlxSprite
|
|||
{
|
||||
super.update(elapsed);
|
||||
|
||||
// mustPress indicates the player is the one pressing the key
|
||||
if (mustPress)
|
||||
{
|
||||
// miss on the NEXT frame so lag doesnt make u miss notes
|
||||
|
@ -244,7 +245,8 @@ class Note extends FlxSprite
|
|||
}
|
||||
|
||||
if (data.strumTime > Conductor.songPosition - HIT_WINDOW)
|
||||
{ // * 0.5 if sustain note, so u have to keep holding it closer to all the way thru!
|
||||
{
|
||||
// * 0.5 if sustain note, so u have to keep holding it closer to all the way thru!
|
||||
if (data.strumTime < Conductor.songPosition + (HIT_WINDOW * (isSustainNote ? 0.5 : 1)))
|
||||
canBeHit = true;
|
||||
}
|
||||
|
@ -455,7 +457,12 @@ enum abstract NoteColor(NoteType) from Int to Int from NoteType
|
|||
|
||||
enum abstract NoteKind(String) from String to String
|
||||
{
|
||||
/**
|
||||
* The default note type.
|
||||
*/
|
||||
var NORMAL = "normal";
|
||||
|
||||
// Testing shiz
|
||||
var PYRO_LIGHT = "pyro_light";
|
||||
var PYRO_KICK = "pyro_kick";
|
||||
var PYRO_TOSS = "pyro_toss";
|
||||
|
|
|
@ -34,6 +34,8 @@ class StoryMenuState extends MusicBeatState
|
|||
];
|
||||
var curDifficulty:Int = 1;
|
||||
|
||||
// TODO: This info is just hardcoded right now.
|
||||
// We should probably make it so that weeks must be completed in order to unlock the next week.
|
||||
public static var weekUnlocked:Array<Bool> = [true, true, true, true, true, true, true, true];
|
||||
|
||||
var weekCharacters:Array<Dynamic> = [
|
||||
|
|
|
@ -1,114 +0,0 @@
|
|||
package funkin.i18n;
|
||||
|
||||
import firetongue.FireTongue;
|
||||
|
||||
class FireTongueHandler
|
||||
{
|
||||
static final DEFAULT_LOCALE = 'en-US';
|
||||
// static final DEFAULT_LOCALE = 'pt-BR';
|
||||
static final LOCALE_DIR = 'assets/locales/';
|
||||
|
||||
static var tongue:FireTongue;
|
||||
|
||||
/**
|
||||
* Initialize the FireTongue instance.
|
||||
* This will automatically start with the default locale for you.
|
||||
*/
|
||||
public static function init():Void
|
||||
{
|
||||
tongue = new FireTongue(OPENFL, // Haxe framework being used.
|
||||
// This should really have been a parameterized object...
|
||||
null, // Function to check if a file exists. Specify null to use the one from the framework.
|
||||
null, // Function to retrieve the text of a file. Specify null to use the one from the framework.
|
||||
null, // Function to get a list of files in a directory. Specify null to use the one from the framework.
|
||||
firetongue.FireTongue.Case.Upper);
|
||||
|
||||
// TODO: Make this use the language from the user's preferences.
|
||||
setLanguage(DEFAULT_LOCALE);
|
||||
|
||||
trace('[FIRETONGUE] Initialized. Available locales: ${tongue.locales.join(', ')}');
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch the language used by FireTongue.
|
||||
* @param locale The name of the locale to use, such as `en-US`.
|
||||
*/
|
||||
public static function setLanguage(locale:String):Void
|
||||
{
|
||||
tongue.initialize({
|
||||
locale: locale, // The locale to load.
|
||||
|
||||
finishedCallback: onFinishLoad, // Function run when the locale is loaded.
|
||||
directory: LOCALE_DIR, // Folder (relative to assets/) to load data from.
|
||||
replaceMissing: false, // If true, missing flags fallback to the default locale.
|
||||
checkMissing: true, // If true, check for and store the list of missing flags for this locale.
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Function called when FireTongue finishes loading a language.
|
||||
*/
|
||||
static function onFinishLoad()
|
||||
{
|
||||
if (tongue == null)
|
||||
return;
|
||||
|
||||
trace('[FIRETONGUE] Finished loading locale: ${tongue.locale}');
|
||||
if (tongue.missingFlags != null)
|
||||
{
|
||||
if (tongue.missingFlags.get(tongue.locale) != null && tongue.missingFlags.get(tongue.locale).length != 0)
|
||||
{
|
||||
trace('[FIRETONGUE] Missing flags: ${tongue.missingFlags.get(tongue.locale).join(', ')}');
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('[FIRETONGUE] No missing flags for this locale. (Note: Another locale has missing flags.)');
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('[FIRETONGUE] No missing flags.');
|
||||
}
|
||||
|
||||
trace('[FIRETONGUE] HELLO_WORLD = ${t("HELLO_WORLD")}');
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a localized string based on the given key.
|
||||
*
|
||||
* Example:
|
||||
* import i18n.FiretongueHandler.t;
|
||||
* trace(t('HELLO')); // Prints "Hello!"
|
||||
*
|
||||
* @param key The key to use to retrieve the localized string.
|
||||
* @param context The data file to load the key from.
|
||||
* @return The localized string.
|
||||
*/
|
||||
public static function t(key:String, context:String = 'data'):String
|
||||
{
|
||||
// The localization strings can be stored all in one file,
|
||||
// or split into several contexts.
|
||||
return tongue.get(key, context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a localized string while replacing specific values.
|
||||
* In this way, you can use the same invocation call to properly localize
|
||||
* a variety of different languages with distinct grammar.
|
||||
*
|
||||
* Example:
|
||||
* import i18n.FiretongueHandler.f;
|
||||
* trace(f('COLLECT_X_APPLES', 'data', ['<X>'], ['10']); // Prints "Collect 10 apples!"
|
||||
*
|
||||
* @param key The key to use to retrieve the localized string.
|
||||
* @param context The data file to load the key from.
|
||||
* @param flags The flags to replace in the string.
|
||||
* @param values The values to replace those flags with.
|
||||
* @return The localized string.
|
||||
*/
|
||||
public static function f(key:String, context:String = 'data', flags:Array<String> = null, values:Array<String> = null):String
|
||||
{
|
||||
var str = t(key, context);
|
||||
return firetongue.Replace.flags(str, flags, values);
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
# i18n
|
||||
|
||||
This package contains functions used for internationalization (i18n).
|
|
@ -23,6 +23,20 @@ interface IStateChangingScriptedClass extends IScriptedClass
|
|||
{
|
||||
public function onStateChangeBegin(event:StateChangeScriptEvent):Void;
|
||||
public function onStateChangeEnd(event:StateChangeScriptEvent):Void;
|
||||
|
||||
public function onSubstateOpenBegin(event:SubStateScriptEvent):Void;
|
||||
public function onSubstateOpenEnd(event:SubStateScriptEvent):Void;
|
||||
public function onSubstateCloseBegin(event:SubStateScriptEvent):Void;
|
||||
public function onSubstateCloseEnd(event:SubStateScriptEvent):Void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines a set of callbacks available to scripted classes which represent notes.
|
||||
*/
|
||||
interface INoteScriptedClass extends IScriptedClass
|
||||
{
|
||||
public function onNoteHit(event:NoteScriptEvent):Void;
|
||||
public function onNoteMiss(event:NoteScriptEvent):Void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -46,12 +60,12 @@ interface IPlayStateScriptedClass extends IScriptedClass
|
|||
public function onSongLoaded(eent:SongLoadScriptEvent):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 onSongRetry(event:ScriptEvent):Void;
|
||||
|
||||
public function onNoteHit(event:NoteScriptEvent):Void;
|
||||
public function onNoteMiss(event:NoteScriptEvent):Void;
|
||||
public function onNoteGhostMiss(event:GhostMissNoteScriptEvent):Void;
|
||||
|
||||
public function onStepHit(event:SongTimeScriptEvent):Void;
|
||||
public function onBeatHit(event:SongTimeScriptEvent):Void;
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
package funkin.modding.events;
|
||||
|
||||
import flixel.FlxState;
|
||||
import flixel.FlxSubState;
|
||||
import funkin.Note.NoteDir;
|
||||
import funkin.play.Countdown.CountdownStep;
|
||||
import openfl.events.EventType;
|
||||
import openfl.events.KeyboardEvent;
|
||||
|
@ -83,6 +86,15 @@ class ScriptEvent
|
|||
*/
|
||||
public static inline final NOTE_MISS:ScriptEventType = "NOTE_MISS";
|
||||
|
||||
/**
|
||||
* Called when a character presses a note when there was none there, causing them to lose health.
|
||||
* Important information such as direction pressed, etc. are all provided.
|
||||
*
|
||||
* This event IS cancelable! Canceling this event prevents the note from being considered missed,
|
||||
* avoiding lost health/score and preventing the miss animation.
|
||||
*/
|
||||
public static inline final NOTE_GHOST_MISS:ScriptEventType = "NOTE_GHOST_MISS";
|
||||
|
||||
/**
|
||||
* Called when the song starts. This occurs as the countdown ends and the instrumental and vocals begin.
|
||||
*
|
||||
|
@ -97,13 +109,6 @@ class ScriptEvent
|
|||
*/
|
||||
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.
|
||||
*
|
||||
|
@ -130,18 +135,19 @@ class ScriptEvent
|
|||
public static inline final COUNTDOWN_END:ScriptEventType = "COUNTDOWN_END";
|
||||
|
||||
/**
|
||||
* Called when the game over screen triggers and the death animation plays.
|
||||
* Called before the game over screen triggers and the death animation plays.
|
||||
*
|
||||
* This event is not cancelable.
|
||||
*/
|
||||
public static inline final GAME_OVER:ScriptEventType = "GAME_OVER";
|
||||
|
||||
/**
|
||||
* Called when the player presses a key to restart the game after the death animation.
|
||||
* Called when the player presses a key to restart the game.
|
||||
* This can happen from the pause menu or the game over screen.
|
||||
*
|
||||
* This event IS cancelable! Canceling this event will prevent the game from restarting.
|
||||
*/
|
||||
public static inline final GAME_RETRY:ScriptEventType = "GAME_RETRY";
|
||||
public static inline final SONG_RETRY:ScriptEventType = "SONG_RETRY";
|
||||
|
||||
/**
|
||||
* Called when the player pushes down any key on the keyboard.
|
||||
|
@ -166,11 +172,46 @@ class ScriptEvent
|
|||
public static inline final SONG_LOADED:ScriptEventType = "SONG_LOADED";
|
||||
|
||||
/**
|
||||
* Called when the game is entering the current FlxState.
|
||||
* Called when the game is about to switch the current FlxState.
|
||||
*
|
||||
* This event is not cancelable.
|
||||
*/
|
||||
public static inline final STATE_ENTER:ScriptEventType = "STATE_ENTER";
|
||||
public static inline final STATE_CHANGE_BEGIN:ScriptEventType = "STATE_CHANGE_BEGIN";
|
||||
|
||||
/**
|
||||
* Called when the game has finished switching the current FlxState.
|
||||
*
|
||||
* This event is not cancelable.
|
||||
*/
|
||||
public static inline final STATE_CHANGE_END:ScriptEventType = "STATE_CHANGE_END";
|
||||
|
||||
/**
|
||||
* Called when the game is about to open a new FlxSubState.
|
||||
*
|
||||
* This event is not cancelable.
|
||||
*/
|
||||
public static inline final SUBSTATE_OPEN_BEGIN:ScriptEventType = "SUBSTATE_OPEN_BEGIN";
|
||||
|
||||
/**
|
||||
* Called when the game has finished opening a new FlxSubState.
|
||||
*
|
||||
* This event is not cancelable.
|
||||
*/
|
||||
public static inline final SUBSTATE_OPEN_END:ScriptEventType = "SUBSTATE_OPEN_END";
|
||||
|
||||
/**
|
||||
* Called when the game is about to close the current FlxSubState.
|
||||
*
|
||||
* This event is not cancelable.
|
||||
*/
|
||||
public static inline final SUBSTATE_CLOSE_BEGIN:ScriptEventType = "SUBSTATE_CLOSE_BEGIN";
|
||||
|
||||
/**
|
||||
* Called when the game has finished closing the current FlxSubState.
|
||||
*
|
||||
* This event is not cancelable.
|
||||
*/
|
||||
public static inline final SUBSTATE_CLOSE_END:ScriptEventType = "SUBSTATE_CLOSE_END";
|
||||
|
||||
/**
|
||||
* Called when the game is exiting the current FlxState.
|
||||
|
@ -265,6 +306,59 @@ class NoteScriptEvent extends ScriptEvent
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An event that is fired when you press a key with no note present.
|
||||
*/
|
||||
class GhostMissNoteScriptEvent extends ScriptEvent
|
||||
{
|
||||
/**
|
||||
* The direction that was mistakenly pressed.
|
||||
*/
|
||||
public var dir(default, null):NoteDir;
|
||||
|
||||
/**
|
||||
* Whether there was a note within judgement range when this ghost note was pressed.
|
||||
*/
|
||||
public var hasPossibleNotes(default, null):Bool;
|
||||
|
||||
/**
|
||||
* How much health should be lost when this ghost note is pressed.
|
||||
* Remember that max health is 2.00.
|
||||
*/
|
||||
public var healthChange(default, default):Float;
|
||||
|
||||
/**
|
||||
* How much score should be lost when this ghost note is pressed.
|
||||
*/
|
||||
public var scoreChange(default, default):Int;
|
||||
|
||||
/**
|
||||
* Whether to play the record scratch sound.
|
||||
*/
|
||||
public var playSound(default, default):Bool;
|
||||
|
||||
/**
|
||||
* Whether to play the miss animation on the player.
|
||||
*/
|
||||
public var playAnim(default, default):Bool;
|
||||
|
||||
public function new(dir:NoteDir, hasPossibleNotes:Bool, healthChange:Float, scoreChange:Int):Void
|
||||
{
|
||||
super(ScriptEvent.NOTE_GHOST_MISS, true);
|
||||
this.dir = dir;
|
||||
this.hasPossibleNotes = hasPossibleNotes;
|
||||
this.healthChange = healthChange;
|
||||
this.scoreChange = scoreChange;
|
||||
this.playSound = true;
|
||||
this.playAnim = true;
|
||||
}
|
||||
|
||||
public override function toString():String
|
||||
{
|
||||
return 'GhostMissNoteScriptEvent(dir=' + dir + ', hasPossibleNotes=' + hasPossibleNotes + ')';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An event that is fired during the update loop.
|
||||
*/
|
||||
|
@ -403,13 +497,41 @@ class SongLoadScriptEvent extends ScriptEvent
|
|||
*/
|
||||
class StateChangeScriptEvent extends ScriptEvent
|
||||
{
|
||||
public function new(type:ScriptEventType):Void
|
||||
/**
|
||||
* The state the game is moving into.
|
||||
*/
|
||||
public var targetState(default, null):FlxState;
|
||||
|
||||
public function new(type:ScriptEventType, targetState:FlxState, cancelable:Bool = false):Void
|
||||
{
|
||||
super(type, false);
|
||||
super(type, cancelable);
|
||||
this.targetState = targetState;
|
||||
}
|
||||
|
||||
public override function toString():String
|
||||
{
|
||||
return 'StateChangeScriptEvent(type=' + type + ')';
|
||||
return 'StateChangeScriptEvent(type=' + type + ', targetState=' + targetState + ')';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An event that is fired when moving out of or into an FlxSubState.
|
||||
*/
|
||||
class SubStateScriptEvent extends ScriptEvent
|
||||
{
|
||||
/**
|
||||
* The state the game is moving into.
|
||||
*/
|
||||
public var targetState(default, null):FlxSubState;
|
||||
|
||||
public function new(type:ScriptEventType, targetState:FlxSubState, cancelable:Bool = false):Void
|
||||
{
|
||||
super(type, cancelable);
|
||||
this.targetState = targetState;
|
||||
}
|
||||
|
||||
public override function toString():String
|
||||
{
|
||||
return 'SubStateScriptEvent(type=' + type + ', targetState=' + targetState + ')';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,18 +35,6 @@ class ScriptEventDispatcher
|
|||
return;
|
||||
}
|
||||
|
||||
if (Std.isOfType(target, IStateChangingScriptedClass))
|
||||
{
|
||||
var t = cast(target, IStateChangingScriptedClass);
|
||||
var t = cast(target, IPlayStateScriptedClass);
|
||||
switch (event.type)
|
||||
{
|
||||
case ScriptEvent.NOTE_HIT:
|
||||
t.onNoteHit(cast event);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (Std.isOfType(target, IPlayStateScriptedClass))
|
||||
{
|
||||
var t = cast(target, IPlayStateScriptedClass);
|
||||
|
@ -58,6 +46,9 @@ class ScriptEventDispatcher
|
|||
case ScriptEvent.NOTE_MISS:
|
||||
t.onNoteMiss(cast event);
|
||||
return;
|
||||
case ScriptEvent.NOTE_GHOST_MISS:
|
||||
t.onNoteGhostMiss(cast event);
|
||||
return;
|
||||
case ScriptEvent.SONG_BEAT_HIT:
|
||||
t.onBeatHit(cast event);
|
||||
return;
|
||||
|
@ -70,8 +61,11 @@ class ScriptEventDispatcher
|
|||
case ScriptEvent.SONG_END:
|
||||
t.onSongEnd(event);
|
||||
return;
|
||||
case ScriptEvent.SONG_RESET:
|
||||
t.onSongReset(event);
|
||||
case ScriptEvent.SONG_RETRY:
|
||||
t.onSongRetry(event);
|
||||
return;
|
||||
case ScriptEvent.GAME_OVER:
|
||||
t.onGameOver(event);
|
||||
return;
|
||||
case ScriptEvent.PAUSE:
|
||||
t.onPause(event);
|
||||
|
@ -94,7 +88,38 @@ class ScriptEventDispatcher
|
|||
}
|
||||
}
|
||||
|
||||
throw "No helper for event type: " + event.type;
|
||||
if (Std.isOfType(target, IStateChangingScriptedClass))
|
||||
{
|
||||
var t = cast(target, IStateChangingScriptedClass);
|
||||
switch (event.type)
|
||||
{
|
||||
case ScriptEvent.STATE_CHANGE_BEGIN:
|
||||
t.onStateChangeBegin(cast event);
|
||||
return;
|
||||
case ScriptEvent.STATE_CHANGE_END:
|
||||
t.onStateChangeEnd(cast event);
|
||||
return;
|
||||
case ScriptEvent.SUBSTATE_OPEN_BEGIN:
|
||||
t.onSubstateOpenBegin(cast event);
|
||||
return;
|
||||
case ScriptEvent.SUBSTATE_OPEN_END:
|
||||
t.onSubstateOpenEnd(cast event);
|
||||
return;
|
||||
case ScriptEvent.SUBSTATE_CLOSE_BEGIN:
|
||||
t.onSubstateCloseBegin(cast event);
|
||||
return;
|
||||
case ScriptEvent.SUBSTATE_CLOSE_END:
|
||||
t.onSubstateCloseEnd(cast event);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Prevent "NO HELPER error."
|
||||
return;
|
||||
}
|
||||
|
||||
throw "No function called for event type: " + event.type;
|
||||
}
|
||||
|
||||
public static function callEventOnAllTargets(targets:Iterator<IScriptedClass>, event:ScriptEvent):Void
|
||||
|
|
|
@ -18,7 +18,7 @@ class Module implements IPlayStateScriptedClass implements IStateChangingScripte
|
|||
/**
|
||||
* Whether the module is currently active.
|
||||
*/
|
||||
public var active(default, set):Bool = false;
|
||||
public var active(default, set):Bool = true;
|
||||
|
||||
function set_active(value:Bool):Bool
|
||||
{
|
||||
|
@ -48,14 +48,11 @@ class Module implements IPlayStateScriptedClass implements IStateChangingScripte
|
|||
* 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.
|
||||
* NOTE: To make the module start inactive, call `this.active = false` in the constructor.
|
||||
*/
|
||||
public function new(moduleId:String, active:Bool = true, priority:Int = 1000):Void
|
||||
public function new(moduleId:String, priority:Int = 1000):Void
|
||||
{
|
||||
this.moduleId = moduleId;
|
||||
this.active = active;
|
||||
this.priority = priority;
|
||||
}
|
||||
|
||||
|
@ -90,16 +87,14 @@ class Module implements IPlayStateScriptedClass implements IStateChangingScripte
|
|||
|
||||
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 onNoteGhostMiss(event:GhostMissNoteScriptEvent) {}
|
||||
|
||||
public function onStepHit(event:SongTimeScriptEvent) {}
|
||||
|
||||
public function onBeatHit(event:SongTimeScriptEvent) {}
|
||||
|
@ -110,9 +105,19 @@ class Module implements IPlayStateScriptedClass implements IStateChangingScripte
|
|||
|
||||
public function onCountdownEnd(event:CountdownScriptEvent) {}
|
||||
|
||||
public function onSongLoaded(eent:SongLoadScriptEvent) {}
|
||||
public function onSongLoaded(event:SongLoadScriptEvent) {}
|
||||
|
||||
public function onStateChangeBegin(event:StateChangeScriptEvent) {}
|
||||
|
||||
public function onStateChangeEnd(event:StateChangeScriptEvent) {}
|
||||
|
||||
public function onSubstateOpenBegin(event:SubStateScriptEvent) {}
|
||||
|
||||
public function onSubstateOpenEnd(event:SubStateScriptEvent) {}
|
||||
|
||||
public function onSubstateCloseBegin(event:SubStateScriptEvent) {}
|
||||
|
||||
public function onSubstateCloseEnd(event:SubStateScriptEvent) {}
|
||||
|
||||
public function onSongRetry(event:ScriptEvent) {}
|
||||
}
|
||||
|
|
|
@ -47,6 +47,16 @@ class ModuleHandler
|
|||
trace("[MODULEHANDLER] Module cache loaded.");
|
||||
}
|
||||
|
||||
public static function buildModuleCallbacks():Void
|
||||
{
|
||||
FlxG.signals.postStateSwitch.add(onStateSwitchComplete);
|
||||
}
|
||||
|
||||
static function onStateSwitchComplete():Void
|
||||
{
|
||||
callEvent(new StateChangeScriptEvent(ScriptEvent.STATE_CHANGE_END, FlxG.state, true));
|
||||
}
|
||||
|
||||
static function addToModuleCache(module:Module):Void
|
||||
{
|
||||
moduleCache.set(module.moduleId, module);
|
||||
|
|
|
@ -16,11 +16,18 @@ typedef AnimationData =
|
|||
*/
|
||||
var prefix:String;
|
||||
|
||||
/**
|
||||
* Optionally specify an asset path to use for this specific animation.
|
||||
* ONLY for use by MultiSparrow characters.
|
||||
* @default The assetPath of the parent sprite
|
||||
*/
|
||||
var assetPath:Null<String>;
|
||||
|
||||
/**
|
||||
* Offset the character's position by this amount when playing this animation.
|
||||
* @default [0, 0]
|
||||
*/
|
||||
var offsets:Null<Array<Float>>;
|
||||
var offsets:Null<Array<Int>>;
|
||||
|
||||
/**
|
||||
* Whether the animation should loop when it finishes.
|
||||
|
|
|
@ -37,6 +37,9 @@ class Countdown
|
|||
PlayState.isInCountdown = true;
|
||||
Conductor.songPosition = Conductor.crochet * -5;
|
||||
countdownStep = BEFORE;
|
||||
// Handle onBeatHit events manually
|
||||
@:privateAccess
|
||||
PlayState.instance.dispatchEvent(new SongTimeScriptEvent(ScriptEvent.SONG_BEAT_HIT, 0, 0));
|
||||
|
||||
var cancelled:Bool = propagateCountdownEvent(countdownStep);
|
||||
if (cancelled)
|
||||
|
@ -49,9 +52,9 @@ class Countdown
|
|||
{
|
||||
countdownStep = decrement(countdownStep);
|
||||
|
||||
// Play the dance animations manually.
|
||||
// Handle onBeatHit events manually
|
||||
@:privateAccess
|
||||
PlayState.instance.danceOnBeat();
|
||||
PlayState.instance.dispatchEvent(new SongTimeScriptEvent(ScriptEvent.SONG_BEAT_HIT, 0, 0));
|
||||
|
||||
// Countdown graphic.
|
||||
showCountdownGraphic(countdownStep, isPixelStyle);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package funkin.play;
|
||||
|
||||
import funkin.play.character.CharacterBase;
|
||||
import funkin.play.character.BaseCharacter;
|
||||
import flixel.addons.effects.FlxTrail;
|
||||
import flixel.addons.transition.FlxTransitionableState;
|
||||
import flixel.FlxCamera;
|
||||
|
@ -86,6 +86,12 @@ class PlayState extends MusicBeatState implements IHook
|
|||
*/
|
||||
public static var isInCountdown:Bool = false;
|
||||
|
||||
/**
|
||||
* Gets set to true when the PlayState needs to reset (player opted to restart or died).
|
||||
* Gets disabled once resetting happens.
|
||||
*/
|
||||
public static var needsReset: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.
|
||||
|
@ -125,6 +131,11 @@ class PlayState extends MusicBeatState implements IHook
|
|||
*/
|
||||
public var health:Float = 1;
|
||||
|
||||
/**
|
||||
* The player's current score.
|
||||
*/
|
||||
public var songScore:Int = 0;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
@ -225,7 +236,6 @@ class PlayState extends MusicBeatState implements IHook
|
|||
public static var storyWeek:Int = 0;
|
||||
public static var storyPlaylist:Array<String> = [];
|
||||
public static var storyDifficulty:Int = 1;
|
||||
public static var needsReset:Bool = false;
|
||||
public static var seenCutscene:Bool = false;
|
||||
public static var campaignScore:Int = 0;
|
||||
|
||||
|
@ -242,7 +252,6 @@ class PlayState extends MusicBeatState implements IHook
|
|||
|
||||
var dialogue:Array<String>;
|
||||
var talking:Bool = true;
|
||||
var songScore:Int = 0;
|
||||
var doof:DialogueBox;
|
||||
var grpNoteSplashes:FlxTypedGroup<NoteSplash>;
|
||||
var comboPopUps:PopUpStuff;
|
||||
|
@ -377,6 +386,8 @@ class PlayState extends MusicBeatState implements IHook
|
|||
iconP1.cameras = [camHUD];
|
||||
iconP2.cameras = [camHUD];
|
||||
scoreText.cameras = [camHUD];
|
||||
leftWatermarkText.cameras = [camHUD];
|
||||
rightWatermarkText.cameras = [camHUD];
|
||||
|
||||
// if (SONG.song == 'South')
|
||||
// FlxG.camera.alpha = 0.7;
|
||||
|
@ -494,26 +505,48 @@ class PlayState extends MusicBeatState implements IHook
|
|||
if (currentSong.song.toLowerCase() == 'stress')
|
||||
gfVersion = 'pico-speaker';
|
||||
|
||||
var girlfriend:Character = new Character(350, -70, gfVersion);
|
||||
girlfriend.scrollFactor.set(0.95, 0.95);
|
||||
if (gfVersion == 'pico-speaker')
|
||||
if (currentSong.song.toLowerCase() == 'tutorial')
|
||||
gfVersion = '';
|
||||
|
||||
//
|
||||
// GIRLFRIEND
|
||||
//
|
||||
var girlfriend:BaseCharacter = CharacterDataParser.fetchCharacter(gfVersion);
|
||||
|
||||
if (girlfriend != null)
|
||||
{
|
||||
girlfriend.x -= 50;
|
||||
girlfriend.y -= 200;
|
||||
girlfriend.characterType = CharacterType.GF;
|
||||
girlfriend.scrollFactor.set(0.95, 0.95);
|
||||
if (gfVersion == 'pico-speaker')
|
||||
{
|
||||
girlfriend.x -= 50;
|
||||
girlfriend.y -= 200;
|
||||
}
|
||||
}
|
||||
else if (gfVersion != '')
|
||||
{
|
||||
trace('WARNING: Could not load girlfriend character with ID ${gfVersion}, skipping...');
|
||||
}
|
||||
|
||||
//
|
||||
// DAD
|
||||
//
|
||||
var dad = new Character(100, 100, currentSong.player2);
|
||||
var dad:BaseCharacter = CharacterDataParser.fetchCharacter(currentSong.player2);
|
||||
|
||||
cameraFollowPoint.setPosition(dad.getGraphicMidpoint().x, dad.getGraphicMidpoint().y);
|
||||
if (dad != null)
|
||||
{
|
||||
dad.characterType = CharacterType.DAD;
|
||||
cameraFollowPoint.setPosition(dad.getGraphicMidpoint().x, dad.getGraphicMidpoint().y);
|
||||
}
|
||||
|
||||
switch (currentSong.player2)
|
||||
{
|
||||
case 'gf':
|
||||
dad.setPosition(girlfriend.x, girlfriend.y);
|
||||
girlfriend.visible = false;
|
||||
var gfPoint:FlxPoint = currentStage.getGirlfriendPosition();
|
||||
dad.setPosition(gfPoint.x, gfPoint.y);
|
||||
|
||||
// girlfriend.visible = false;
|
||||
|
||||
if (isStoryMode)
|
||||
{
|
||||
cameraFollowPoint.x += 600;
|
||||
|
@ -553,12 +586,11 @@ class PlayState extends MusicBeatState implements IHook
|
|||
//
|
||||
// BOYFRIEND
|
||||
//
|
||||
var boyfriend:CharacterBase;
|
||||
switch (currentSong.player1)
|
||||
var boyfriend:BaseCharacter = CharacterDataParser.fetchCharacter(currentSong.player1);
|
||||
|
||||
if (boyfriend != null)
|
||||
{
|
||||
default:
|
||||
boyfriend = CharacterDataParser.fetchCharacter(currentSong.player1);
|
||||
boyfriend.characterType = CharacterType.BF;
|
||||
boyfriend.characterType = CharacterType.BF;
|
||||
}
|
||||
|
||||
// REPOSITIONING PER STAGE
|
||||
|
@ -589,8 +621,8 @@ class PlayState extends MusicBeatState implements IHook
|
|||
// We're using Eric's stage handler.
|
||||
// Characters get added to the stage, not the main scene.
|
||||
currentStage.addCharacter(boyfriend, BF);
|
||||
currentStage.addCharacterOld(girlfriend, GF);
|
||||
currentStage.addCharacterOld(dad, DAD);
|
||||
currentStage.addCharacter(girlfriend, GF);
|
||||
currentStage.addCharacter(dad, DAD);
|
||||
|
||||
// Redo z-indexes.
|
||||
currentStage.refresh();
|
||||
|
@ -612,7 +644,7 @@ class PlayState extends MusicBeatState implements IHook
|
|||
*
|
||||
* Call this by pressing F5 on a debug build.
|
||||
*/
|
||||
function debug_refreshStages()
|
||||
override function debug_refreshModules()
|
||||
{
|
||||
// Remove the current stage. If the stage gets deleted while it's still in use,
|
||||
// it'll probably crash the game or something.
|
||||
|
@ -624,19 +656,7 @@ class PlayState extends MusicBeatState implements IHook
|
|||
currentStage = null;
|
||||
}
|
||||
|
||||
ModuleHandler.clearModuleCache();
|
||||
|
||||
// Forcibly reload scripts so that scripted stages can be edited.
|
||||
polymod.hscript.PolymodScriptClass.clearScriptClasses();
|
||||
polymod.hscript.PolymodScriptClass.registerAllScriptClasses();
|
||||
|
||||
// Reload the stages in cache. This might cause a lag spike but who cares this is a debug utility.
|
||||
StageDataParser.loadStageCache();
|
||||
CharacterDataParser.loadCharacterCache();
|
||||
ModuleHandler.loadModuleCache();
|
||||
|
||||
// Reload the level. This should use new data from the assets folder.
|
||||
LoadingState.loadAndSwitchState(new PlayState());
|
||||
super.debug_refreshModules();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -957,6 +977,8 @@ class PlayState extends MusicBeatState implements IHook
|
|||
|
||||
if (needsReset)
|
||||
{
|
||||
dispatchEvent(new ScriptEvent(ScriptEvent.SONG_RETRY));
|
||||
|
||||
resetCamera();
|
||||
|
||||
persistentUpdate = true;
|
||||
|
@ -967,11 +989,10 @@ class PlayState extends MusicBeatState implements IHook
|
|||
FlxG.sound.music.pause();
|
||||
vocals.pause();
|
||||
|
||||
var event:ScriptEvent = new ScriptEvent(ScriptEvent.SONG_RESET, false);
|
||||
|
||||
FlxG.sound.music.time = 0;
|
||||
regenNoteData(); // loads the note data from start
|
||||
health = 1;
|
||||
songScore = 0;
|
||||
Countdown.performCountdown(currentStageId.startsWith('school'));
|
||||
|
||||
needsReset = false;
|
||||
|
@ -1059,9 +1080,6 @@ class PlayState extends MusicBeatState implements IHook
|
|||
if (FlxG.keys.justPressed.EIGHT)
|
||||
FlxG.switchState(new funkin.ui.animDebugShit.DebugBoundingState());
|
||||
|
||||
if (FlxG.keys.justPressed.F5)
|
||||
debug_refreshStages();
|
||||
|
||||
if (FlxG.keys.justPressed.NINE)
|
||||
iconP1.swapOldIcon();
|
||||
|
||||
|
@ -1158,6 +1176,8 @@ class PlayState extends MusicBeatState implements IHook
|
|||
|
||||
deathCounter += 1;
|
||||
|
||||
dispatchEvent(new ScriptEvent(ScriptEvent.GAME_OVER));
|
||||
|
||||
openSubState(new GameOverSubstate());
|
||||
|
||||
#if discord_rpc
|
||||
|
@ -1226,35 +1246,28 @@ class PlayState extends MusicBeatState implements IHook
|
|||
}
|
||||
}
|
||||
|
||||
if (!daNote.mustPress && daNote.wasGoodHit)
|
||||
if (!daNote.mustPress && daNote.wasGoodHit && !daNote.tooLate)
|
||||
{
|
||||
if (currentSong.song != 'Tutorial')
|
||||
camZooming = true;
|
||||
|
||||
var altAnim:String = "";
|
||||
var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_HIT, daNote, true);
|
||||
dispatchEvent(event);
|
||||
|
||||
if (SongLoad.getSong()[Math.floor(curStep / 16)] != null)
|
||||
// Calling event.cancelEvent() in a module should force the CPU to miss the note.
|
||||
// This is useful for cool shit, including but not limited to:
|
||||
// - Making the AI ignore notes which are hazardous.
|
||||
// - Making the AI miss notes on purpose for aesthetic reasons.
|
||||
if (event.eventCanceled)
|
||||
{
|
||||
if (SongLoad.getSong()[Math.floor(curStep / 16)].altAnim)
|
||||
altAnim = '-alt';
|
||||
daNote.tooLate = true;
|
||||
}
|
||||
|
||||
if (daNote.data.altNote)
|
||||
altAnim = '-alt';
|
||||
|
||||
if (!daNote.isSustainNote)
|
||||
else
|
||||
{
|
||||
currentStage.getDad().playAnim('sing' + daNote.dirNameUpper + altAnim, true);
|
||||
// Volume of DAD.
|
||||
if (currentSong.needsVoices)
|
||||
vocals.volume = 1;
|
||||
}
|
||||
|
||||
currentStage.getDad().holdTimer = 0;
|
||||
|
||||
if (currentSong.needsVoices)
|
||||
vocals.volume = 1;
|
||||
|
||||
daNote.kill();
|
||||
activeNotes.remove(daNote, true);
|
||||
daNote.destroy();
|
||||
}
|
||||
|
||||
// WIP interpolation shit? Need to fix the pause issue
|
||||
|
@ -1279,18 +1292,8 @@ class PlayState extends MusicBeatState implements IHook
|
|||
daNote.destroy();
|
||||
}
|
||||
}
|
||||
else if (daNote.tooLate || daNote.wasGoodHit)
|
||||
if (daNote.wasGoodHit)
|
||||
{
|
||||
// TODO: Why the hell is the noteMiss logic in two different places?
|
||||
if (daNote.tooLate)
|
||||
{
|
||||
var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_MISS, daNote, true);
|
||||
dispatchEvent(event);
|
||||
health -= 0.0775;
|
||||
vocals.volume = 0;
|
||||
killCombo();
|
||||
}
|
||||
|
||||
daNote.active = false;
|
||||
daNote.visible = false;
|
||||
|
||||
|
@ -1298,6 +1301,11 @@ class PlayState extends MusicBeatState implements IHook
|
|||
activeNotes.remove(daNote, true);
|
||||
daNote.destroy();
|
||||
}
|
||||
|
||||
if (daNote.tooLate)
|
||||
{
|
||||
noteMiss(daNote);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1329,8 +1337,10 @@ class PlayState extends MusicBeatState implements IHook
|
|||
|
||||
function killCombo():Void
|
||||
{
|
||||
if (combo > 5 && currentStage.getGirlfriend().animOffsets.exists('sad'))
|
||||
currentStage.getGirlfriend().playAnim('sad');
|
||||
// Girlfriend gets sad if you combo break after hitting 5 notes.
|
||||
if (currentStage.getGirlfriend() != null)
|
||||
if (combo > 5 && currentStage.getGirlfriend().hasAnimation('sad'))
|
||||
currentStage.getGirlfriend().playAnimation('sad');
|
||||
|
||||
if (combo != 0)
|
||||
{
|
||||
|
@ -1494,15 +1504,6 @@ class PlayState extends MusicBeatState implements IHook
|
|||
|
||||
health += healthMulti;
|
||||
|
||||
// TODO: Redo note hit logic to make sure this always gets called
|
||||
var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_HIT, daNote, true);
|
||||
dispatchEvent(event);
|
||||
|
||||
if (event.eventCanceled)
|
||||
{
|
||||
// TODO: Do a thing!
|
||||
}
|
||||
|
||||
if (isSick)
|
||||
{
|
||||
var noteSplash:NoteSplash = grpNoteSplashes.recycle(NoteSplash);
|
||||
|
@ -1531,7 +1532,7 @@ class PlayState extends MusicBeatState implements IHook
|
|||
cameraFollowPoint.setPosition(currentStage.getDad().getMidpoint().x + 150, currentStage.getDad().getMidpoint().y - 100);
|
||||
// camFollow.setPosition(lucky.getMidpoint().x - 120, lucky.getMidpoint().y + 210);
|
||||
|
||||
switch (currentStage.getDad().curCharacter)
|
||||
switch (currentStage.getDad().characterId)
|
||||
{
|
||||
case 'mom':
|
||||
cameraFollowPoint.y = currentStage.getDad().getMidpoint().y;
|
||||
|
@ -1540,7 +1541,7 @@ class PlayState extends MusicBeatState implements IHook
|
|||
cameraFollowPoint.x = currentStage.getDad().getMidpoint().x - 100;
|
||||
}
|
||||
|
||||
if (currentStage.getDad().curCharacter == 'mom')
|
||||
if (currentStage.getDad().characterId == 'mom')
|
||||
vocals.volume = 1;
|
||||
|
||||
if (currentSong.song.toLowerCase() == 'tutorial')
|
||||
|
@ -1573,9 +1574,11 @@ class PlayState extends MusicBeatState implements IHook
|
|||
trace(instance.currentStageId);
|
||||
};
|
||||
|
||||
@:hookable
|
||||
public function keyShit(test:Bool):Void
|
||||
{
|
||||
if (PlayState.instance == null)
|
||||
return;
|
||||
|
||||
// control arrays, order L D R U
|
||||
var holdArray:Array<Bool> = [controls.NOTE_LEFT, controls.NOTE_DOWN, controls.NOTE_UP, controls.NOTE_RIGHT];
|
||||
var pressArray:Array<Bool> = [
|
||||
|
@ -1659,7 +1662,7 @@ class PlayState extends MusicBeatState implements IHook
|
|||
for (shit in 0...pressArray.length)
|
||||
{ // if a direction is hit that shouldn't be
|
||||
if (pressArray[shit] && !directionList.contains(shit))
|
||||
PlayState.instance.noteMiss(shit);
|
||||
PlayState.instance.ghostNoteMiss(shit);
|
||||
}
|
||||
for (coolNote in possibleNotes)
|
||||
{
|
||||
|
@ -1669,23 +1672,15 @@ class PlayState extends MusicBeatState implements IHook
|
|||
}
|
||||
else
|
||||
{
|
||||
// HNGGG I really want to add an option for ghost tapping
|
||||
for (shit in 0...pressArray.length)
|
||||
if (pressArray[shit])
|
||||
PlayState.instance.noteMiss(shit);
|
||||
PlayState.instance.ghostNoteMiss(shit, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (PlayState.instance.currentStage == null)
|
||||
return;
|
||||
if (PlayState.instance.currentStage.getBoyfriend().holdTimer > Conductor.stepCrochet * 4 * 0.001 && !holdArray.contains(true))
|
||||
{
|
||||
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'))
|
||||
{
|
||||
PlayState.instance.currentStage.getBoyfriend().playAnimation('idle');
|
||||
}
|
||||
}
|
||||
|
||||
for (keyId => isPressed in pressArray)
|
||||
{
|
||||
|
@ -1702,33 +1697,78 @@ class PlayState extends MusicBeatState implements IHook
|
|||
}
|
||||
}
|
||||
|
||||
function noteMiss(direction:NoteDir = 1):Void
|
||||
/**
|
||||
* Called when a player presses a key with no note present.
|
||||
* Scripts can modify the amount of health/score lost, whether player animations or sounds are used,
|
||||
* or even cancel the event entirely.
|
||||
*
|
||||
* @param direction
|
||||
* @param hasPossibleNotes
|
||||
*/
|
||||
function ghostNoteMiss(direction:NoteType = 1, hasPossibleNotes:Bool = true):Void
|
||||
{
|
||||
// whole function used to be encased in if (!boyfriend.stunned)
|
||||
health -= 0.07;
|
||||
killCombo();
|
||||
var event:GhostMissNoteScriptEvent = new GhostMissNoteScriptEvent(direction, // Direction missed in.
|
||||
hasPossibleNotes, // Whether there was a note you could have hit.
|
||||
- 0.035 * 2, // How much health to add (negative).
|
||||
- 10 // Amount of score to add (negative).
|
||||
);
|
||||
dispatchEvent(event);
|
||||
|
||||
// Calling event.cancelEvent() skips animations and penalties. Neat!
|
||||
if (event.eventCanceled)
|
||||
return;
|
||||
|
||||
health += event.healthChange;
|
||||
|
||||
if (!isPracticeMode)
|
||||
songScore += event.scoreChange;
|
||||
|
||||
if (event.playSound)
|
||||
{
|
||||
vocals.volume = 0;
|
||||
FlxG.sound.play(Paths.soundRandom('missnote', 1, 3), FlxG.random.float(0.1, 0.2));
|
||||
}
|
||||
}
|
||||
|
||||
function noteMiss(note:Note):Void
|
||||
{
|
||||
var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_MISS, note, true);
|
||||
dispatchEvent(event);
|
||||
// Calling event.cancelEvent() skips all the other logic! Neat!
|
||||
if (event.eventCanceled)
|
||||
return;
|
||||
|
||||
health -= 0.0775;
|
||||
if (!isPracticeMode)
|
||||
songScore -= 10;
|
||||
|
||||
vocals.volume = 0;
|
||||
FlxG.sound.play(Paths.soundRandom('missnote', 1, 3), FlxG.random.float(0.1, 0.2));
|
||||
killCombo();
|
||||
|
||||
currentStage.getBoyfriend().playAnimation('sing' + direction.nameUpper + 'miss', true);
|
||||
note.active = false;
|
||||
note.visible = false;
|
||||
|
||||
note.kill();
|
||||
activeNotes.remove(note, true);
|
||||
note.destroy();
|
||||
}
|
||||
|
||||
function goodNoteHit(note:Note):Void
|
||||
{
|
||||
if (!note.wasGoodHit)
|
||||
{
|
||||
var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_HIT, note, true);
|
||||
dispatchEvent(event);
|
||||
|
||||
// Calling event.cancelEvent() skips all the other logic! Neat!
|
||||
if (event.eventCanceled)
|
||||
return;
|
||||
|
||||
if (!note.isSustainNote)
|
||||
{
|
||||
combo += 1;
|
||||
popUpScore(note.data.strumTime, note);
|
||||
}
|
||||
|
||||
currentStage.getBoyfriend().playAnimation('sing' + note.dirNameUpper, true);
|
||||
|
||||
playerStrumline.getArrow(note.data.noteData).playAnimation('confirm', true);
|
||||
|
||||
note.wasGoodHit = true;
|
||||
|
@ -1813,22 +1853,6 @@ class PlayState extends MusicBeatState implements IHook
|
|||
if (currentStage == null)
|
||||
return;
|
||||
|
||||
if (curBeat % gfSpeed == 0)
|
||||
currentStage.getGirlfriend().dance();
|
||||
|
||||
if (curBeat % 2 == 0)
|
||||
{
|
||||
if (currentStage.getBoyfriend().animation != null && !currentStage.getBoyfriend().animation.curAnim.name.startsWith("sing"))
|
||||
currentStage.getBoyfriend().playAnimation('idle');
|
||||
if (currentStage.getDad().animation != null && !currentStage.getDad().animation.curAnim.name.startsWith("sing"))
|
||||
currentStage.getDad().dance();
|
||||
}
|
||||
else if (currentStage.getDad().curCharacter == 'spooky')
|
||||
{
|
||||
if (!currentStage.getDad().animation.curAnim.name.startsWith("sing"))
|
||||
currentStage.getDad().dance();
|
||||
}
|
||||
|
||||
if (curBeat % 8 == 7 && currentSong.song == 'Bopeebo')
|
||||
{
|
||||
currentStage.getBoyfriend().playAnimation('hey', true);
|
||||
|
@ -1836,12 +1860,12 @@ class PlayState extends MusicBeatState implements IHook
|
|||
|
||||
if (curBeat % 16 == 15
|
||||
&& currentSong.song == 'Tutorial'
|
||||
&& currentStage.getDad().curCharacter == 'gf'
|
||||
&& currentStage.getDad().characterId == 'gf'
|
||||
&& curBeat > 16
|
||||
&& curBeat < 48)
|
||||
{
|
||||
currentStage.getBoyfriend().playAnimation('hey', true);
|
||||
currentStage.getDad().playAnim('cheer', true);
|
||||
currentStage.getDad().playAnimation('cheer', true);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1976,12 +2000,20 @@ class PlayState extends MusicBeatState implements IHook
|
|||
|
||||
override function dispatchEvent(event:ScriptEvent):Void
|
||||
{
|
||||
// ORDER: Module, Stage, Character, Song, Note
|
||||
// Modules should get the first chance to cancel the event.
|
||||
|
||||
// super.dispatchEvent(event) dispatches event to module scripts.
|
||||
super.dispatchEvent(event);
|
||||
|
||||
// Dispatch event to stage script.
|
||||
ScriptEventDispatcher.callEvent(currentStage, event);
|
||||
|
||||
// TODO: Dispatch event to song script
|
||||
// TODO: Dispatch events to character scripts
|
||||
// Dispatch event to character script(s).
|
||||
if (currentStage != null)
|
||||
currentStage.dispatchToCharacters(event);
|
||||
|
||||
super.dispatchEvent(event);
|
||||
// TODO: Dispatch event to song script
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2045,11 +2077,17 @@ class PlayState extends MusicBeatState implements IHook
|
|||
|
||||
/**
|
||||
* This function is called whenever Flixel switches switching to a new FlxState.
|
||||
* @return Whether to actually switch to the new state.
|
||||
*/
|
||||
override function switchTo(nextState:FlxState):Bool
|
||||
{
|
||||
performCleanup();
|
||||
var result = super.switchTo(nextState);
|
||||
|
||||
return super.switchTo(nextState);
|
||||
if (result)
|
||||
{
|
||||
performCleanup();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
281
source/funkin/play/character/BaseCharacter.hx
Normal file
281
source/funkin/play/character/BaseCharacter.hx
Normal file
|
@ -0,0 +1,281 @@
|
|||
package funkin.play.character;
|
||||
|
||||
import funkin.modding.events.ScriptEvent;
|
||||
import funkin.modding.events.ScriptEvent.UpdateScriptEvent;
|
||||
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||
import funkin.Note.NoteDir;
|
||||
import funkin.modding.events.ScriptEvent.NoteScriptEvent;
|
||||
import funkin.play.stage.Bopper;
|
||||
|
||||
using StringTools;
|
||||
|
||||
/**
|
||||
* A Character is a stage prop which bops to the music as well as controlled by the strumlines.
|
||||
*
|
||||
* Remember: The character's origin is at its FEET. (horizontal center, vertical bottom)
|
||||
*/
|
||||
class BaseCharacter extends Bopper
|
||||
{
|
||||
// Metadata about a character.
|
||||
public var characterId(default, null):String;
|
||||
public var characterName(default, null):String;
|
||||
|
||||
/**
|
||||
* Whether the player is an active character (Boyfriend) or not.
|
||||
*/
|
||||
public var characterType:CharacterType = OTHER;
|
||||
|
||||
final _data:CharacterData;
|
||||
|
||||
/**
|
||||
* Tracks how long, in seconds, the character has been playing the current `sing` animation.
|
||||
* This is used to ensure that characters play the `sing` animations for at least one beat,
|
||||
* preventing them from reverting to the `idle` animation between notes.
|
||||
*/
|
||||
public var holdTimer:Float = 0;
|
||||
|
||||
final singTimeCrochet:Float;
|
||||
|
||||
public var isDead:Bool = false;
|
||||
|
||||
public function new(id:String)
|
||||
{
|
||||
super();
|
||||
this.characterId = id;
|
||||
|
||||
_data = CharacterDataParser.fetchCharacterData(this.characterId);
|
||||
if (_data == null)
|
||||
{
|
||||
throw 'Could not find character data for characterId: $characterId';
|
||||
}
|
||||
else
|
||||
{
|
||||
this.characterName = _data.name;
|
||||
this.singTimeCrochet = _data.singTime;
|
||||
}
|
||||
}
|
||||
|
||||
public override function onUpdate(event:UpdateScriptEvent):Void
|
||||
{
|
||||
super.onUpdate(event);
|
||||
|
||||
// Reset hold timer for each note pressed.
|
||||
if (justPressedNote())
|
||||
{
|
||||
holdTimer = 0;
|
||||
}
|
||||
|
||||
if (isDead)
|
||||
{
|
||||
playDeathAnimation();
|
||||
}
|
||||
|
||||
// Handle character note hold time.
|
||||
if (getCurrentAnimation().startsWith("sing"))
|
||||
{
|
||||
holdTimer += event.elapsed;
|
||||
var singTimeMs:Float = singTimeCrochet * (Conductor.crochet * 0.001); // x beats, to ms.
|
||||
// Without this check here, the player character would only play the `sing` animation
|
||||
// for one beat, as opposed to holding it as long as the player is holding the button.
|
||||
var shouldStopSinging:Bool = (this.characterType == BF) ? !isHoldingNote() : true;
|
||||
|
||||
FlxG.watch.addQuick('singTimeMs-${characterId}', singTimeMs);
|
||||
if (holdTimer > singTimeMs && shouldStopSinging)
|
||||
{
|
||||
trace('holdTimer reached ${holdTimer}sec (> ${singTimeMs}), stopping sing animation');
|
||||
holdTimer = 0;
|
||||
dance(true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
holdTimer = 0;
|
||||
// super.onBeatHit handles the regular `dance()` calls.
|
||||
}
|
||||
FlxG.watch.addQuick('holdTimer-${characterId}', holdTimer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Since no `onBeatHit` or `dance` calls happen in GameOverSubstate,
|
||||
* this regularly gets called instead.
|
||||
*/
|
||||
public function playDeathAnimation(force:Bool = false):Void
|
||||
{
|
||||
if (force || (getCurrentAnimation().startsWith("firstDeath") && isAnimationFinished()))
|
||||
{
|
||||
playAnimation("deathLoop");
|
||||
}
|
||||
}
|
||||
|
||||
override function dance(force:Bool = false)
|
||||
{
|
||||
if (!force)
|
||||
{
|
||||
if (getCurrentAnimation().startsWith("sing"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (["hey", "cheer"].contains(getCurrentAnimation()) && !isAnimationFinished())
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent dancing while another animation is playing.
|
||||
if (!force && getCurrentAnimation().startsWith("sing"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, fallback to the super dance() method, which handles playing the idle animation.
|
||||
super.dance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the player just pressed a note.
|
||||
* Used when determing whether a the player character should revert to the `idle` animation.
|
||||
* On non-player characters, this should be ignored.
|
||||
*/
|
||||
function justPressedNote(player:Int = 1):Bool
|
||||
{
|
||||
// Returns true if at least one of LEFT, DOWN, UP, or RIGHT is being held.
|
||||
switch (player)
|
||||
{
|
||||
case 1:
|
||||
return [
|
||||
PlayerSettings.player1.controls.NOTE_LEFT_P,
|
||||
PlayerSettings.player1.controls.NOTE_DOWN_P,
|
||||
PlayerSettings.player1.controls.NOTE_UP_P,
|
||||
PlayerSettings.player1.controls.NOTE_RIGHT_P,
|
||||
].contains(true);
|
||||
case 2:
|
||||
return [
|
||||
PlayerSettings.player2.controls.NOTE_LEFT_P,
|
||||
PlayerSettings.player2.controls.NOTE_DOWN_P,
|
||||
PlayerSettings.player2.controls.NOTE_UP_P,
|
||||
PlayerSettings.player2.controls.NOTE_RIGHT_P,
|
||||
].contains(true);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the player is holding a note.
|
||||
* Used when determing whether a the player character should revert to the `idle` animation.
|
||||
* On non-player characters, this should be ignored.
|
||||
*/
|
||||
function isHoldingNote(player:Int = 1):Bool
|
||||
{
|
||||
// Returns true if at least one of LEFT, DOWN, UP, or RIGHT is being held.
|
||||
switch (player)
|
||||
{
|
||||
case 1:
|
||||
return [
|
||||
PlayerSettings.player1.controls.NOTE_LEFT,
|
||||
PlayerSettings.player1.controls.NOTE_DOWN,
|
||||
PlayerSettings.player1.controls.NOTE_UP,
|
||||
PlayerSettings.player1.controls.NOTE_RIGHT,
|
||||
].contains(true);
|
||||
case 2:
|
||||
return [
|
||||
PlayerSettings.player2.controls.NOTE_LEFT,
|
||||
PlayerSettings.player2.controls.NOTE_DOWN,
|
||||
PlayerSettings.player2.controls.NOTE_UP,
|
||||
PlayerSettings.player2.controls.NOTE_RIGHT,
|
||||
].contains(true);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Every time a note is hit, check if the note is from the same strumline.
|
||||
* If it is, then play the sing animation.
|
||||
*/
|
||||
public override function onNoteHit(event:NoteScriptEvent)
|
||||
{
|
||||
super.onNoteHit(event);
|
||||
|
||||
trace('HIT NOTE: ${event.note.data.dir} : ${event.note.isSustainNote}');
|
||||
|
||||
holdTimer = 0;
|
||||
|
||||
if (event.note.mustPress && characterType == BF)
|
||||
{
|
||||
// If the note is from the same strumline, play the sing animation.
|
||||
this.playSingAnimation(event.note.data.dir, false, event.note.data.altNote ? "alt" : null);
|
||||
}
|
||||
else if (!event.note.mustPress && characterType == DAD)
|
||||
{
|
||||
// If the note is from the same strumline, play the sing animation.
|
||||
this.playSingAnimation(event.note.data.dir, false, event.note.data.altNote ? "alt" : null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Every time a note is missed, check if the note is from the same strumline.
|
||||
* If it is, then play the sing animation.
|
||||
*/
|
||||
public override function onNoteMiss(event:NoteScriptEvent)
|
||||
{
|
||||
super.onNoteMiss(event);
|
||||
|
||||
if (event.note.mustPress && characterType == BF)
|
||||
{
|
||||
// If the note is from the same strumline, play the sing animation.
|
||||
this.playSingAnimation(event.note.data.dir, true, event.note.data.altNote ? "alt" : null);
|
||||
}
|
||||
else if (!event.note.mustPress && characterType == DAD)
|
||||
{
|
||||
// If the note is from the same strumline, play the sing animation.
|
||||
this.playSingAnimation(event.note.data.dir, true, event.note.data.altNote ? "alt" : null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Every time a wrong key is pressed, play the miss animation if we are Boyfriend.
|
||||
*/
|
||||
public override function onNoteGhostMiss(event:GhostMissNoteScriptEvent)
|
||||
{
|
||||
super.onNoteGhostMiss(event);
|
||||
|
||||
if (event.eventCanceled || !event.playAnim)
|
||||
{
|
||||
// Skipping...
|
||||
return;
|
||||
}
|
||||
|
||||
if (characterType == BF)
|
||||
{
|
||||
trace('Playing ghost miss animation...');
|
||||
// If the note is from the same strumline, play the sing animation.
|
||||
this.playSingAnimation(event.dir, true, null);
|
||||
}
|
||||
}
|
||||
|
||||
public override function onDestroy(event:ScriptEvent):Void
|
||||
{
|
||||
this.characterType = OTHER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Play the appropriate singing animation, for the given note direction.
|
||||
* @param dir The direction of the note.
|
||||
* @param miss If true, play the miss animation instead of the sing animation.
|
||||
* @param suffix A suffix to append to the animation name, like `alt`.
|
||||
*/
|
||||
public function playSingAnimation(dir:NoteDir, miss:Bool = false, suffix:String = ""):Void
|
||||
{
|
||||
var anim:String = 'sing${dir.nameUpper}${miss ? 'miss' : ''}${suffix != "" ? '-${suffix}' : ''}';
|
||||
|
||||
// restart even if already playing, because the character might sing the same note twice.
|
||||
playAnimation(anim, true);
|
||||
}
|
||||
}
|
||||
|
||||
enum CharacterType
|
||||
{
|
||||
BF;
|
||||
DAD;
|
||||
GF;
|
||||
OTHER;
|
||||
}
|
|
@ -1,140 +0,0 @@
|
|||
package funkin.play.character;
|
||||
|
||||
import funkin.modding.events.ScriptEvent;
|
||||
import funkin.modding.events.ScriptEvent.UpdateScriptEvent;
|
||||
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||
import funkin.Note.NoteDir;
|
||||
import funkin.modding.events.ScriptEvent.NoteScriptEvent;
|
||||
import funkin.play.stage.Bopper;
|
||||
|
||||
/**
|
||||
* A Character is a stage prop which bops to the music as well as controlled by the strumlines.
|
||||
*
|
||||
* Remember: The character's origin is at its FEET. (horizontal center, vertical bottom)
|
||||
*/
|
||||
class CharacterBase extends Bopper
|
||||
{
|
||||
public var characterId(default, null):String;
|
||||
public var characterName(default, null):String;
|
||||
|
||||
/**
|
||||
* Whether the player is an active character (Boyfriend) or not.
|
||||
*/
|
||||
public var characterType:CharacterType = OTHER;
|
||||
|
||||
public var attachedStrumlines(default, null):Array<Int>;
|
||||
|
||||
final _data:CharacterData;
|
||||
|
||||
/**
|
||||
* Tracks how long, in seconds, the character has been playing the current `sing` animation.
|
||||
* This is used to ensure that characters play the `sing` animations for at least one beat,
|
||||
* preventing them from reverting to the `idle` animation between notes.
|
||||
*/
|
||||
public var holdTimer:Float = 0;
|
||||
|
||||
final singTimeCrochet:Float;
|
||||
|
||||
public function new(id:String)
|
||||
{
|
||||
super();
|
||||
this.characterId = id;
|
||||
this.attachedStrumlines = [];
|
||||
|
||||
_data = CharacterDataParser.parseCharacterData(this.characterId);
|
||||
if (_data == null)
|
||||
{
|
||||
throw 'Could not find character data for characterId: $characterId';
|
||||
}
|
||||
else
|
||||
{
|
||||
this.characterName = _data.name;
|
||||
this.singTimeCrochet = _data.singTime;
|
||||
}
|
||||
}
|
||||
|
||||
public override function onUpdate(event:UpdateScriptEvent):Void
|
||||
{
|
||||
super.onUpdate(event);
|
||||
|
||||
// Handle character note hold time.
|
||||
holdTimer += event.elapsed;
|
||||
var singTimeMs:Float = singTimeCrochet * Conductor.crochet;
|
||||
// Without this check here, the player character would only play the `sing` animation
|
||||
// for one beat, as opposed to holding it as long as the player is holding the button.
|
||||
var shouldStopSinging:Bool = (this.characterType == BF) ? !isHoldingNote() : true;
|
||||
|
||||
if (holdTimer > singTimeMs && shouldStopSinging)
|
||||
{
|
||||
holdTimer = 0;
|
||||
dance();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the player is holding a note.
|
||||
* Used when determing whether a the player character should revert to the `idle` animation.
|
||||
* On non-player characters, this should be ignored.
|
||||
*/
|
||||
function isHoldingNote(player:Int = 1):Bool
|
||||
{
|
||||
// Returns true if at least one of LEFT, DOWN, UP, or RIGHT is being held.
|
||||
switch (player)
|
||||
{
|
||||
case 1:
|
||||
return [
|
||||
PlayerSettings.player1.controls.NOTE_LEFT,
|
||||
PlayerSettings.player1.controls.NOTE_DOWN,
|
||||
PlayerSettings.player1.controls.NOTE_UP,
|
||||
PlayerSettings.player1.controls.NOTE_RIGHT,
|
||||
].contains(true);
|
||||
case 2:
|
||||
return [
|
||||
PlayerSettings.player2.controls.NOTE_LEFT,
|
||||
PlayerSettings.player2.controls.NOTE_DOWN,
|
||||
PlayerSettings.player2.controls.NOTE_UP,
|
||||
PlayerSettings.player2.controls.NOTE_RIGHT,
|
||||
].contains(true);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Every time a note is hit, check if the note is from the same strumline.
|
||||
* If it is, then play the sing animation.
|
||||
*/
|
||||
public override function onNoteHit(event:NoteScriptEvent)
|
||||
{
|
||||
super.onNoteHit(event);
|
||||
// If event.note is from the same strumline as this character, then sing.
|
||||
// if (this.attachedStrumlines.indexOf(event.note.strumline) != -1)
|
||||
// {
|
||||
// this.playSingAnimation(event.note.dir, false, note.alt);
|
||||
// }
|
||||
}
|
||||
|
||||
public override function onDestroy(event:ScriptEvent):Void
|
||||
{
|
||||
this.characterType = OTHER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Play the appropriate singing animation, for the given note direction.
|
||||
* @param dir The direction of the note.
|
||||
* @param miss If true, play the miss animation instead of the sing animation.
|
||||
* @param suffix A suffix to append to the animation name, like `alt`.
|
||||
*/
|
||||
function playSingAnimation(dir:NoteDir, miss:Bool = false, suffix:String = ""):Void
|
||||
{
|
||||
var anim:String = 'sing${dir.nameUpper}${miss ? 'miss' : ''}${suffix != "" ? '-${suffix}' : ''}';
|
||||
playAnimation(anim, true);
|
||||
}
|
||||
}
|
||||
|
||||
enum CharacterType
|
||||
{
|
||||
BF;
|
||||
DAD;
|
||||
GF;
|
||||
OTHER;
|
||||
}
|
|
@ -1,14 +1,20 @@
|
|||
package funkin.play.character;
|
||||
|
||||
import openfl.Assets;
|
||||
import haxe.Json;
|
||||
import funkin.play.character.render.PackerCharacter;
|
||||
import funkin.play.character.render.SparrowCharacter;
|
||||
import funkin.util.assets.DataAssets;
|
||||
import funkin.play.character.CharacterBase;
|
||||
import funkin.play.character.ScriptedCharacter.ScriptedSparrowCharacter;
|
||||
import funkin.play.character.ScriptedCharacter.ScriptedPackerCharacter;
|
||||
import flixel.util.typeLimit.OneOfTwo;
|
||||
import funkin.modding.events.ScriptEvent;
|
||||
import funkin.modding.events.ScriptEventDispatcher;
|
||||
import funkin.play.character.BaseCharacter;
|
||||
import funkin.play.character.MultiSparrowCharacter;
|
||||
import funkin.play.character.PackerCharacter;
|
||||
import funkin.play.character.SparrowCharacter;
|
||||
import funkin.play.character.ScriptedCharacter.ScriptedBaseCharacter;
|
||||
import funkin.play.character.ScriptedCharacter.ScriptedMultiSparrowCharacter;
|
||||
import funkin.play.character.ScriptedCharacter.ScriptedPackerCharacter;
|
||||
import funkin.play.character.ScriptedCharacter.ScriptedSparrowCharacter;
|
||||
import funkin.util.assets.DataAssets;
|
||||
import funkin.util.VersionUtil;
|
||||
import haxe.Json;
|
||||
import openfl.utils.Assets;
|
||||
|
||||
using StringTools;
|
||||
|
||||
|
@ -19,9 +25,15 @@ class CharacterDataParser
|
|||
* Handle breaking changes by incrementing this value
|
||||
* and adding migration to the `migrateStageData()` function.
|
||||
*/
|
||||
public static final CHARACTER_DATA_VERSION:String = "1.0";
|
||||
public static final CHARACTER_DATA_VERSION:String = "1.0.0";
|
||||
|
||||
static final characterCache:Map<String, CharacterBase> = new Map<String, CharacterBase>();
|
||||
/**
|
||||
* The current version rule check for the stage data format.
|
||||
*/
|
||||
public static final CHARACTER_DATA_VERSION_RULE:String = "1.0.x";
|
||||
|
||||
static final characterCache:Map<String, CharacterData> = new Map<String, CharacterData>();
|
||||
static final characterScriptedClass:Map<String, String> = new Map<String, String>();
|
||||
|
||||
static final DEFAULT_CHAR_ID:String = 'UNKNOWN';
|
||||
|
||||
|
@ -37,73 +49,23 @@ class CharacterDataParser
|
|||
trace("[CHARDATA] Loading character cache...");
|
||||
|
||||
//
|
||||
// SCRIPTED CHARACTERS
|
||||
//
|
||||
|
||||
// Generic (Sparrow) characters
|
||||
var scriptedCharClassNames:Array<String> = ScriptedCharacter.listScriptClasses();
|
||||
trace(' Instantiating ${scriptedCharClassNames.length} scripted characters...');
|
||||
for (charCls in scriptedCharClassNames)
|
||||
{
|
||||
_storeChar(ScriptedCharacter.init(charCls, DEFAULT_CHAR_ID), charCls);
|
||||
}
|
||||
|
||||
// Sparrow characters
|
||||
scriptedCharClassNames = ScriptedSparrowCharacter.listScriptClasses();
|
||||
if (scriptedCharClassNames.length > 0)
|
||||
{
|
||||
trace(' Instantiating ${scriptedCharClassNames.length} scripted characters (SPARROW)...');
|
||||
for (charCls in scriptedCharClassNames)
|
||||
{
|
||||
_storeChar(ScriptedSparrowCharacter.init(charCls, DEFAULT_CHAR_ID), charCls);
|
||||
}
|
||||
}
|
||||
|
||||
// // Packer characters
|
||||
// scriptedCharClassNames = ScriptedPackerCharacter.listScriptClasses();
|
||||
// if (scriptedCharClassNames.length > 0)
|
||||
// {
|
||||
// trace(' Instantiating ${scriptedCharClassNames.length} scripted characters (PACKER)...');
|
||||
// for (charCls in scriptedCharClassNames)
|
||||
// {
|
||||
// _storeChar(ScriptedPackerCharacter.init(charCls, DEFAULT_CHAR_ID), charCls);
|
||||
// }
|
||||
// }
|
||||
|
||||
// TODO: Add more character types.
|
||||
|
||||
//
|
||||
// UNSCRIPTED STAGES
|
||||
// UNSCRIPTED CHARACTERS
|
||||
//
|
||||
var charIdList:Array<String> = DataAssets.listDataFilesInPath('characters/');
|
||||
var unscriptedCharIds:Array<String> = charIdList.filter(function(charId:String):Bool
|
||||
{
|
||||
return !characterCache.exists(charId);
|
||||
});
|
||||
trace(' Instantiating ${unscriptedCharIds.length} non-scripted characters...');
|
||||
trace(' Fetching data for ${unscriptedCharIds.length} characters...');
|
||||
for (charId in unscriptedCharIds)
|
||||
{
|
||||
var char:CharacterBase = null;
|
||||
try
|
||||
{
|
||||
var charData:CharacterData = parseCharacterData(charId);
|
||||
if (charData != null)
|
||||
{
|
||||
switch (charData.renderType)
|
||||
{
|
||||
case CharacterRenderType.PACKER:
|
||||
char = new PackerCharacter(charId);
|
||||
case CharacterRenderType.SPARROW:
|
||||
// default
|
||||
char = new SparrowCharacter(charId);
|
||||
default:
|
||||
trace(' Failed to instantiate character: ${charId} (Bad render type ${charData.renderType})');
|
||||
}
|
||||
}
|
||||
if (char != null)
|
||||
{
|
||||
trace(' Loaded character data: ${char.characterName}');
|
||||
characterCache.set(charId, char);
|
||||
trace(' Loaded character data: ${charId}');
|
||||
characterCache.set(charId, charData);
|
||||
}
|
||||
}
|
||||
catch (e)
|
||||
|
@ -113,39 +75,140 @@ class CharacterDataParser
|
|||
}
|
||||
}
|
||||
|
||||
//
|
||||
// SCRIPTED CHARACTERS
|
||||
//
|
||||
|
||||
// Fuck I wish scripted classes supported static functions.
|
||||
|
||||
var scriptedCharClassNames1:Array<String> = ScriptedSparrowCharacter.listScriptClasses();
|
||||
if (scriptedCharClassNames1.length > 0)
|
||||
{
|
||||
trace(' Instantiating ${scriptedCharClassNames1.length} (Sparrow) scripted characters...');
|
||||
for (charCls in scriptedCharClassNames1)
|
||||
{
|
||||
var character = ScriptedSparrowCharacter.init(charCls, DEFAULT_CHAR_ID);
|
||||
characterScriptedClass.set(character.characterId, charCls);
|
||||
}
|
||||
}
|
||||
|
||||
var scriptedCharClassNames2:Array<String> = ScriptedPackerCharacter.listScriptClasses();
|
||||
if (scriptedCharClassNames2.length > 0)
|
||||
{
|
||||
trace(' Instantiating ${scriptedCharClassNames2.length} (Packer) scripted characters...');
|
||||
for (charCls in scriptedCharClassNames2)
|
||||
{
|
||||
var character = ScriptedPackerCharacter.init(charCls, DEFAULT_CHAR_ID);
|
||||
characterScriptedClass.set(character.characterId, charCls);
|
||||
}
|
||||
}
|
||||
|
||||
var scriptedCharClassNames3:Array<String> = ScriptedMultiSparrowCharacter.listScriptClasses();
|
||||
trace(' Instantiating ${scriptedCharClassNames3.length} (Multi-Sparrow) scripted characters...');
|
||||
for (charCls in scriptedCharClassNames3)
|
||||
{
|
||||
var character = ScriptedBaseCharacter.init(charCls, DEFAULT_CHAR_ID);
|
||||
if (character == null)
|
||||
{
|
||||
trace(' Failed to instantiate scripted character: ${charCls}');
|
||||
continue;
|
||||
}
|
||||
characterScriptedClass.set(character.characterId, charCls);
|
||||
}
|
||||
|
||||
// NOTE: Only instantiate the ones not populated above.
|
||||
// ScriptedBaseCharacter.listScriptClasses() will pick up scripts extending the other classes.
|
||||
var scriptedCharClassNames:Array<String> = ScriptedBaseCharacter.listScriptClasses();
|
||||
scriptedCharClassNames.filter(function(charCls:String):Bool
|
||||
{
|
||||
return !scriptedCharClassNames1.contains(charCls)
|
||||
&& !scriptedCharClassNames2.contains(charCls)
|
||||
&& !scriptedCharClassNames3.contains(charCls);
|
||||
});
|
||||
|
||||
trace(' Instantiating ${scriptedCharClassNames.length} (Base) scripted characters...');
|
||||
for (charCls in scriptedCharClassNames)
|
||||
{
|
||||
var character = ScriptedBaseCharacter.init(charCls, DEFAULT_CHAR_ID);
|
||||
if (character == null)
|
||||
{
|
||||
trace(' Failed to instantiate scripted character: ${charCls}');
|
||||
continue;
|
||||
}
|
||||
characterScriptedClass.set(character.characterId, charCls);
|
||||
}
|
||||
|
||||
trace(' Successfully loaded ${Lambda.count(characterCache)} stages.');
|
||||
}
|
||||
|
||||
static function _storeChar(char:CharacterBase, charCls:String):Void
|
||||
public static function fetchCharacter(charId:String):Null<BaseCharacter>
|
||||
{
|
||||
if (char != null)
|
||||
if (charId == null || charId == '')
|
||||
{
|
||||
trace(' Loaded scripted character: ${char.characterName}');
|
||||
// Disable the rendering logic for stage until it's loaded.
|
||||
// Note that kill() =/= destroy()
|
||||
char.kill();
|
||||
// Gracefully handle songs that don't use this character.
|
||||
return null;
|
||||
}
|
||||
|
||||
// Then store it.
|
||||
characterCache.set(char.characterId, char);
|
||||
if (characterCache.exists(charId))
|
||||
{
|
||||
var charData:CharacterData = characterCache.get(charId);
|
||||
var charScriptClass:String = characterScriptedClass.get(charId);
|
||||
|
||||
var char:BaseCharacter;
|
||||
|
||||
if (charScriptClass != null)
|
||||
{
|
||||
switch (charData.renderType)
|
||||
{
|
||||
case CharacterRenderType.MULTISPARROW:
|
||||
char = ScriptedMultiSparrowCharacter.init(charScriptClass, charId);
|
||||
case CharacterRenderType.SPARROW:
|
||||
char = ScriptedSparrowCharacter.init(charScriptClass, charId);
|
||||
case CharacterRenderType.PACKER:
|
||||
char = ScriptedPackerCharacter.init(charScriptClass, charId);
|
||||
default:
|
||||
// We're going to assume that the script class does the rendering.
|
||||
char = ScriptedBaseCharacter.init(charScriptClass, charId);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (charData.renderType)
|
||||
{
|
||||
case CharacterRenderType.MULTISPARROW:
|
||||
char = new MultiSparrowCharacter(charId);
|
||||
case CharacterRenderType.SPARROW:
|
||||
char = new SparrowCharacter(charId);
|
||||
case CharacterRenderType.PACKER:
|
||||
char = new PackerCharacter(charId);
|
||||
default:
|
||||
trace('[WARN] Creating character with undefined renderType ${charData.renderType}');
|
||||
char = new BaseCharacter(charId);
|
||||
}
|
||||
}
|
||||
|
||||
trace('[CHARDATA] Successfully instantiated character: ${charId}');
|
||||
|
||||
// Call onCreate only in the fetchCharacter() function, not at application initialization.
|
||||
ScriptEventDispatcher.callEvent(char, new ScriptEvent(ScriptEvent.CREATE));
|
||||
|
||||
return char;
|
||||
}
|
||||
else
|
||||
{
|
||||
trace(' Failed to instantiate scripted character class: ${charCls}');
|
||||
trace('[CHARDATA] Failed to build character, not found in cache: ${charId}');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static function fetchCharacter(charId:String):Null<CharacterBase>
|
||||
public static function fetchCharacterData(charId:String):Null<CharacterData>
|
||||
{
|
||||
if (characterCache.exists(charId))
|
||||
{
|
||||
trace('[CHARDATA] Successfully fetch stage: ${charId}');
|
||||
var character:CharacterBase = characterCache.get(charId);
|
||||
character.revive();
|
||||
return character;
|
||||
return characterCache.get(charId);
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('[CHARDATA] Failed to fetch character, not found in cache: ${charId}');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -154,12 +217,12 @@ class CharacterDataParser
|
|||
{
|
||||
if (characterCache != null)
|
||||
{
|
||||
for (char in characterCache)
|
||||
{
|
||||
char.destroy();
|
||||
}
|
||||
characterCache.clear();
|
||||
}
|
||||
if (characterScriptedClass != null)
|
||||
{
|
||||
characterScriptedClass.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -180,9 +243,9 @@ class CharacterDataParser
|
|||
static function loadCharacterFile(charPath:String):String
|
||||
{
|
||||
var charFilePath:String = Paths.json('characters/${charPath}');
|
||||
var rawJson = Assets.getText(charFilePath).trim();
|
||||
var rawJson = StringTools.trim(Assets.getText(charFilePath));
|
||||
|
||||
while (!rawJson.endsWith("}"))
|
||||
while (!StringTools.endsWith(rawJson, "}"))
|
||||
{
|
||||
rawJson = rawJson.substr(0, rawJson.length - 1);
|
||||
}
|
||||
|
@ -208,18 +271,26 @@ class CharacterDataParser
|
|||
}
|
||||
}
|
||||
|
||||
static final DEFAULT_NAME:String = "Untitled Character";
|
||||
static final DEFAULT_RENDERTYPE:CharacterRenderType = CharacterRenderType.SPARROW;
|
||||
static final DEFAULT_STARTINGANIM:String = "idle";
|
||||
static final DEFAULT_SCROLL:Array<Float> = [0, 0];
|
||||
static final DEFAULT_ISPIXEL:Bool = false;
|
||||
/**
|
||||
* The default time the character should sing for, in beats.
|
||||
* Values that are too low will cause the character to stop singing between notes.
|
||||
* Originally, this value was set to 1, but it was changed to 2 because that became
|
||||
* too low after some other code changes.
|
||||
*/
|
||||
static final DEFAULT_SINGTIME:Float = 2.0;
|
||||
|
||||
static final DEFAULT_DANCEEVERY:Int = 1;
|
||||
static final DEFAULT_FRAMERATE:Int = 24;
|
||||
static final DEFAULT_FLIPX:Bool = false;
|
||||
static final DEFAULT_SCALE:Float = 1;
|
||||
static final DEFAULT_FLIPY:Bool = false;
|
||||
static final DEFAULT_FRAMERATE:Int = 24;
|
||||
static final DEFAULT_ISPIXEL:Bool = false;
|
||||
static final DEFAULT_LOOP:Bool = false;
|
||||
static final DEFAULT_FRAMEINDICES:Array<Int> = [];
|
||||
static final DEFAULT_NAME:String = "Untitled Character";
|
||||
static final DEFAULT_OFFSETS:Array<Int> = [0, 0];
|
||||
static final DEFAULT_RENDERTYPE:CharacterRenderType = CharacterRenderType.SPARROW;
|
||||
static final DEFAULT_SCALE:Float = 1;
|
||||
static final DEFAULT_SCROLL:Array<Float> = [0, 0];
|
||||
static final DEFAULT_STARTINGANIM:String = "idle";
|
||||
|
||||
/**
|
||||
* Set unspecified parameters to their defaults.
|
||||
|
@ -238,13 +309,13 @@ class CharacterDataParser
|
|||
|
||||
if (input.version == null)
|
||||
{
|
||||
trace('[CHARDATA] ERROR: Could not load character data for "$id": missing version');
|
||||
return null;
|
||||
trace('[CHARDATA] WARN: No semantic version specified for character data file "$id", assuming ${CHARACTER_DATA_VERSION}');
|
||||
input.version = CHARACTER_DATA_VERSION;
|
||||
}
|
||||
|
||||
if (input.version == CHARACTER_DATA_VERSION)
|
||||
if (!VersionUtil.validateVersion(input.version, CHARACTER_DATA_VERSION_RULE))
|
||||
{
|
||||
trace('[CHARDATA] ERROR: Could not load character data for "$id": bad/outdated version (got ${input.version}, expected ${CHARACTER_DATA_VERSION})');
|
||||
trace('[CHARDATA] ERROR: Could not load character data for "$id": bad version (got ${input.version}, expected ${CHARACTER_DATA_VERSION_RULE})');
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -285,6 +356,11 @@ class CharacterDataParser
|
|||
input.danceEvery = DEFAULT_DANCEEVERY;
|
||||
}
|
||||
|
||||
if (input.singTime == null)
|
||||
{
|
||||
input.singTime = DEFAULT_SINGTIME;
|
||||
}
|
||||
|
||||
if (input.animations == null || input.animations.length == 0)
|
||||
{
|
||||
trace('[CHARDATA] ERROR: Could not load character data for "$id": missing animations');
|
||||
|
@ -309,9 +385,9 @@ class CharacterDataParser
|
|||
inputAnimation.frameRate = DEFAULT_FRAMERATE;
|
||||
}
|
||||
|
||||
if (inputAnimation.frameIndices == null)
|
||||
if (inputAnimation.offsets == null)
|
||||
{
|
||||
inputAnimation.frameIndices = DEFAULT_FRAMEINDICES;
|
||||
inputAnimation.offsets = DEFAULT_OFFSETS;
|
||||
}
|
||||
|
||||
if (inputAnimation.looped == null)
|
||||
|
@ -339,15 +415,20 @@ enum abstract CharacterRenderType(String) from String to String
|
|||
{
|
||||
var SPARROW = 'sparrow';
|
||||
var PACKER = 'packer';
|
||||
// TODO: Aesprite?
|
||||
var MULTISPARROW = 'multisparrow';
|
||||
// TODO: FlxSpine?
|
||||
// https://api.haxeflixel.com/flixel/addons/editors/spine/FlxSpine.html
|
||||
// TODO: Aseprite?
|
||||
// https://lib.haxe.org/p/openfl-aseprite/
|
||||
// TODO: Animate?
|
||||
// TODO: Experimental...
|
||||
// https://lib.haxe.org/p/flxanimate
|
||||
// TODO: REDACTED
|
||||
}
|
||||
|
||||
typedef CharacterData =
|
||||
{
|
||||
/**
|
||||
* The sematic version of the chart data format.
|
||||
* The sematic version number of the character data JSON format.
|
||||
*/
|
||||
var version:String;
|
||||
|
||||
|
|
217
source/funkin/play/character/MultiSparrowCharacter.hx
Normal file
217
source/funkin/play/character/MultiSparrowCharacter.hx
Normal file
|
@ -0,0 +1,217 @@
|
|||
package funkin.play.character;
|
||||
|
||||
import funkin.modding.events.ScriptEvent;
|
||||
import funkin.util.assets.FlxAnimationUtil;
|
||||
import flixel.graphics.frames.FlxFramesCollection;
|
||||
|
||||
/**
|
||||
* For some characters which use Sparrow atlases, the spritesheets need to be split
|
||||
* into multiple files. This character renderer handles by showing the appropriate sprite.
|
||||
*
|
||||
* Examples in base game include BF Holding GF (most of the sprites are in one file
|
||||
* but the death animation is in a separate file).
|
||||
* Only example I can think of in mods is Tricky (which has a separate file for each animation).
|
||||
*
|
||||
* BaseCharacter has game logic, SparrowCharacter has only rendering logic.
|
||||
* KEEP THEM SEPARATE!
|
||||
*/
|
||||
class MultiSparrowCharacter extends BaseCharacter
|
||||
{
|
||||
/**
|
||||
* The actual group which holds all spritesheets this character uses.
|
||||
*/
|
||||
private var members:Map<String, FlxFramesCollection> = new Map<String, FlxFramesCollection>();
|
||||
|
||||
/**
|
||||
* A map between animation names and what frame collection the animation should use.
|
||||
*/
|
||||
private var animAssetPath:Map<String, String> = new Map<String, String>();
|
||||
|
||||
/**
|
||||
* The current frame collection being used.
|
||||
*/
|
||||
private var activeMember:String;
|
||||
|
||||
public function new(id:String)
|
||||
{
|
||||
super(id);
|
||||
}
|
||||
|
||||
override function onCreate(event:ScriptEvent):Void
|
||||
{
|
||||
trace('Creating MULTI SPARROW CHARACTER: ' + this.characterId);
|
||||
|
||||
buildSprite();
|
||||
|
||||
playAnimation(_data.startingAnimation);
|
||||
}
|
||||
|
||||
function buildSprite()
|
||||
{
|
||||
buildSpritesheets();
|
||||
buildAnimations();
|
||||
|
||||
if (_data.isPixel)
|
||||
{
|
||||
this.antialiasing = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.antialiasing = true;
|
||||
}
|
||||
|
||||
if (_data.scale != null)
|
||||
{
|
||||
this.setGraphicSize(Std.int(this.width * this.scale.x));
|
||||
this.updateHitbox();
|
||||
}
|
||||
}
|
||||
|
||||
function buildSpritesheets()
|
||||
{
|
||||
// Build the list of asset paths to use.
|
||||
// Ignore nulls and duplicates.
|
||||
var assetList = [_data.assetPath];
|
||||
for (anim in _data.animations)
|
||||
{
|
||||
if (anim.assetPath != null && !assetList.contains(anim.assetPath))
|
||||
{
|
||||
assetList.push(anim.assetPath);
|
||||
}
|
||||
animAssetPath.set(anim.name, anim.assetPath);
|
||||
}
|
||||
|
||||
// Load the Sparrow atlas for each path and store them in the members map.
|
||||
for (asset in assetList)
|
||||
{
|
||||
var texture:FlxFramesCollection = Paths.getSparrowAtlas(asset, 'shared');
|
||||
// If we don't do this, the unused textures will be removed as soon as they're loaded.
|
||||
texture.parent.destroyOnNoUse = false;
|
||||
|
||||
if (texture == null)
|
||||
{
|
||||
trace('Multi-Sparrow atlas could not load texture: ${asset}');
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('Adding multi-sparrow atlas: ${asset}');
|
||||
members.set(asset, texture);
|
||||
}
|
||||
}
|
||||
|
||||
// Use the default frame collection to start.
|
||||
loadFramesByAssetPath(_data.assetPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace this sprite's animation frames with the ones at this asset path.
|
||||
*/
|
||||
function loadFramesByAssetPath(assetPath:String):Void
|
||||
{
|
||||
if (_data.assetPath == null)
|
||||
{
|
||||
trace('[ERROR] Multi-Sparrow character has no default asset path!');
|
||||
return;
|
||||
}
|
||||
if (assetPath == null)
|
||||
{
|
||||
trace('Asset path is null, falling back to default. This is normal!');
|
||||
loadFramesByAssetPath(_data.assetPath);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.activeMember == assetPath)
|
||||
{
|
||||
trace('Already using this asset path: ${assetPath}');
|
||||
return;
|
||||
}
|
||||
|
||||
if (members.exists(assetPath))
|
||||
{
|
||||
trace('Loading frames from asset path: ${assetPath}');
|
||||
this.frames = members.get(assetPath);
|
||||
this.activeMember = assetPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('Multi-Sparrow could not find asset path: ${assetPath}');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace this sprite's animation frames with the ones needed to play this animation.
|
||||
*/
|
||||
function loadFramesByAnimName(animName)
|
||||
{
|
||||
if (animAssetPath.exists(animName))
|
||||
{
|
||||
loadFramesByAssetPath(animAssetPath.get(animName));
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('Multi-Sparrow could not find animation: ${animName}');
|
||||
}
|
||||
}
|
||||
|
||||
function buildAnimations()
|
||||
{
|
||||
trace('[SPARROWCHAR] Loading ${_data.animations.length} animations for ${characterId}');
|
||||
|
||||
// We need to swap to the proper frame collection before adding the animations, I think?
|
||||
for (anim in _data.animations)
|
||||
{
|
||||
trace('Using frames: ${anim.name}');
|
||||
loadFramesByAnimName(anim.name);
|
||||
trace('Adding animation');
|
||||
FlxAnimationUtil.addSparrowAnimation(this, anim);
|
||||
|
||||
if (anim.offsets == null)
|
||||
{
|
||||
setAnimationOffsets(anim.name, 0, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
setAnimationOffsets(anim.name, anim.offsets[0], anim.offsets[1]);
|
||||
}
|
||||
}
|
||||
|
||||
var animNames = this.animation.getNameList();
|
||||
trace('[SPARROWCHAR] Successfully loaded ${animNames.length} animations for ${characterId}');
|
||||
}
|
||||
|
||||
public override function playAnimation(name:String, restart:Bool = false):Void
|
||||
{
|
||||
loadFramesByAnimName(name);
|
||||
super.playAnimation(name, restart);
|
||||
}
|
||||
|
||||
override function set_frames(value:FlxFramesCollection):FlxFramesCollection
|
||||
{
|
||||
// DISABLE THIS SO WE DON'T DESTROY OUR HARD WORK
|
||||
// WE WILL MAKE SURE TO LOAD THE PROPER SPRITESHEET BEFORE PLAYING AN ANIM
|
||||
// if (animation != null)
|
||||
// {
|
||||
// animation.destroyAnimations();
|
||||
// }
|
||||
|
||||
if (value != null)
|
||||
{
|
||||
graphic = value.parent;
|
||||
this.frames = value;
|
||||
this.frame = value.getByIndex(0);
|
||||
this.numFrames = value.numFrames;
|
||||
resetHelpers();
|
||||
this.bakedRotationAngle = 0;
|
||||
this.animation.frameIndex = 0;
|
||||
graphicLoaded();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.frames = null;
|
||||
this.frame = null;
|
||||
this.graphic = null;
|
||||
}
|
||||
|
||||
return this.frames;
|
||||
}
|
||||
}
|
|
@ -1,15 +1,16 @@
|
|||
package funkin.play.character.render;
|
||||
package funkin.play.character;
|
||||
|
||||
import funkin.play.character.CharacterBase.CharacterType;
|
||||
import funkin.play.character.BaseCharacter.CharacterType;
|
||||
|
||||
/**
|
||||
* A PackerCharacter is a Character which is rendered by
|
||||
* displaying an animation derived from a Packer spritesheet file.
|
||||
*/
|
||||
class PackerCharacter extends CharacterBase
|
||||
class PackerCharacter extends BaseCharacter
|
||||
{
|
||||
public function new(id:String)
|
||||
{
|
||||
super(id);
|
||||
}
|
||||
// TODO: Put code here, dumbass!
|
||||
}
|
|
@ -1,14 +1,23 @@
|
|||
package funkin.play.character;
|
||||
|
||||
import funkin.play.character.render.PackerCharacter;
|
||||
import funkin.play.character.render.SparrowCharacter;
|
||||
import funkin.play.character.PackerCharacter;
|
||||
import funkin.play.character.SparrowCharacter;
|
||||
import funkin.play.character.MultiSparrowCharacter;
|
||||
import funkin.modding.IHook;
|
||||
|
||||
/**
|
||||
* Note: Making a scripted class extending BaseCharacter is not recommended.
|
||||
* Do so ONLY if are handling all the character rendering yourself,
|
||||
* and can't use one of the built-in render modes.
|
||||
*/
|
||||
@:hscriptClass
|
||||
class ScriptedCharacter extends SparrowCharacter implements IHook {}
|
||||
class ScriptedBaseCharacter extends BaseCharacter implements IHook {}
|
||||
|
||||
@:hscriptClass
|
||||
class ScriptedSparrowCharacter extends SparrowCharacter implements IHook {}
|
||||
|
||||
@:hscriptClass
|
||||
class ScriptedMultiSparrowCharacter extends MultiSparrowCharacter implements IHook {}
|
||||
|
||||
@:hscriptClass
|
||||
class ScriptedPackerCharacter extends PackerCharacter implements IHook {}
|
||||
|
|
81
source/funkin/play/character/SparrowCharacter.hx
Normal file
81
source/funkin/play/character/SparrowCharacter.hx
Normal file
|
@ -0,0 +1,81 @@
|
|||
package funkin.play.character;
|
||||
|
||||
import funkin.modding.events.ScriptEvent;
|
||||
import funkin.util.assets.FlxAnimationUtil;
|
||||
import flixel.graphics.frames.FlxFramesCollection;
|
||||
|
||||
/**
|
||||
* A SparrowCharacter is a Character which is rendered by
|
||||
* displaying an animation derived from a SparrowV2 atlas spritesheet file.
|
||||
*
|
||||
* BaseCharacter has game logic, SparrowCharacter has only rendering logic.
|
||||
* KEEP THEM SEPARATE!
|
||||
*/
|
||||
class SparrowCharacter extends BaseCharacter
|
||||
{
|
||||
public function new(id:String)
|
||||
{
|
||||
super(id);
|
||||
}
|
||||
|
||||
override function onCreate(event:ScriptEvent):Void
|
||||
{
|
||||
trace('Creating SPARROW CHARACTER: ' + this.characterId);
|
||||
|
||||
loadSpritesheet();
|
||||
loadAnimations();
|
||||
|
||||
playAnimation(_data.startingAnimation);
|
||||
}
|
||||
|
||||
function loadSpritesheet()
|
||||
{
|
||||
trace('[SPARROWCHAR] Loading spritesheet ${_data.assetPath} for ${characterId}');
|
||||
|
||||
var tex:FlxFramesCollection = Paths.getSparrowAtlas(_data.assetPath, 'shared');
|
||||
if (tex == null)
|
||||
{
|
||||
trace('Could not load Sparrow sprite: ${_data.assetPath}');
|
||||
return;
|
||||
}
|
||||
|
||||
this.frames = tex;
|
||||
|
||||
if (_data.isPixel)
|
||||
{
|
||||
this.antialiasing = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.antialiasing = true;
|
||||
}
|
||||
|
||||
if (_data.scale != null)
|
||||
{
|
||||
this.setGraphicSize(Std.int(this.width * this.scale.x));
|
||||
this.updateHitbox();
|
||||
}
|
||||
}
|
||||
|
||||
function loadAnimations()
|
||||
{
|
||||
trace('[SPARROWCHAR] Loading ${_data.animations.length} animations for ${characterId}');
|
||||
|
||||
FlxAnimationUtil.addSparrowAnimations(this, _data.animations);
|
||||
|
||||
for (anim in _data.animations)
|
||||
{
|
||||
if (anim.offsets == null)
|
||||
{
|
||||
setAnimationOffsets(anim.name, 0, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
setAnimationOffsets(anim.name, anim.offsets[0], anim.offsets[1]);
|
||||
}
|
||||
}
|
||||
|
||||
var animNames = this.animation.getNameList();
|
||||
trace('[SPARROWCHAR] Successfully loaded ${animNames.length} animations for ${characterId}');
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
package funkin.play.character.render;
|
||||
|
||||
import funkin.play.character.CharacterBase.CharacterType;
|
||||
|
||||
/**
|
||||
* A SparrowCharacter is a Character which is rendered by
|
||||
* displaying an animation derived from a SparrowV2 atlas spritesheet file.
|
||||
*/
|
||||
class SparrowCharacter extends CharacterBase
|
||||
{
|
||||
public function new(id:String)
|
||||
{
|
||||
super(id);
|
||||
}
|
||||
}
|
|
@ -30,24 +30,14 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
|
|||
public var shouldAlternate:Null<Bool> = null;
|
||||
|
||||
/**
|
||||
* Set this value to define an additional horizontal offset to this sprite's position.
|
||||
* Offset the character's sprite by this much when playing each animation.
|
||||
*/
|
||||
public var xOffset(default, set):Float = 0;
|
||||
|
||||
override function set_x(value:Float):Float
|
||||
{
|
||||
this.x = this.xOffset + value;
|
||||
return this.x;
|
||||
}
|
||||
|
||||
function set_xOffset(value:Float):Float
|
||||
{
|
||||
var diff = value - this.xOffset;
|
||||
this.xOffset = value;
|
||||
this.x += diff;
|
||||
return value;
|
||||
}
|
||||
var animationOffsets:Map<String, Array<Int>> = new Map<String, Array<Int>>();
|
||||
|
||||
/**
|
||||
* Add a suffix to the `idle` animation (or `danceLeft` and `danceRight` animations)
|
||||
* that this bopper will play.
|
||||
*/
|
||||
public var idleSuffix(default, set):String = "";
|
||||
|
||||
function set_idleSuffix(value:String):String
|
||||
|
@ -110,7 +100,7 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
|
|||
/**
|
||||
* Called every `danceEvery` beats of the song.
|
||||
*/
|
||||
function dance():Void
|
||||
function dance(force:Bool = false):Void
|
||||
{
|
||||
if (this.animation == null)
|
||||
{
|
||||
|
@ -142,23 +132,95 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
|
|||
|
||||
public function hasAnimation(id:String):Bool
|
||||
{
|
||||
if (this.animation == null)
|
||||
return false;
|
||||
|
||||
return this.animation.getByName(id) != null;
|
||||
}
|
||||
|
||||
/*
|
||||
* @param AnimName The string name of the animation you want to play.
|
||||
* @param Force Whether to force the animation to restart.
|
||||
/**
|
||||
* Ensure that a given animation exists before playing it.
|
||||
* Will gracefully check for name, then name with stripped suffixes, then 'idle', then fail to play.
|
||||
* @param name
|
||||
*/
|
||||
public function playAnimation(name:String, force:Bool = false):Void
|
||||
function correctAnimationName(name:String)
|
||||
{
|
||||
this.animation.play(name, force, false, 0);
|
||||
// If the animation exists, we're good.
|
||||
if (hasAnimation(name))
|
||||
return name;
|
||||
|
||||
trace('[BOPPER] Animation "$name" does not exist!');
|
||||
|
||||
// Attempt to strip a `-alt` suffix, if it exists.
|
||||
if (name.lastIndexOf('-') != -1)
|
||||
{
|
||||
var correctName = name.substring(0, name.lastIndexOf('-'));
|
||||
trace('[BOPPER] Attempting to fallback to "$correctName"');
|
||||
return correctAnimationName(correctName);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (name != 'idle')
|
||||
{
|
||||
trace('[BOPPER] Attempting to fallback to "idle"');
|
||||
return correctAnimationName('idle');
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('[BOPPER] Failing animation playback.');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name The name of the animation to play.
|
||||
* @param restart Whether to restart the animation if it is already playing.
|
||||
*/
|
||||
public function playAnimation(name:String, restart:Bool = false):Void
|
||||
{
|
||||
var correctName = correctAnimationName(name);
|
||||
if (correctName == null)
|
||||
return;
|
||||
|
||||
this.animation.play(correctName, restart, false, 0);
|
||||
|
||||
applyAnimationOffsets(correctName);
|
||||
}
|
||||
|
||||
function applyAnimationOffsets(name:String)
|
||||
{
|
||||
var offsets = animationOffsets.get(name);
|
||||
if (offsets != null)
|
||||
{
|
||||
this.offset.set(offsets[0], offsets[1]);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.offset.set(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public function isAnimationFinished():Bool
|
||||
{
|
||||
return this.animation.finished;
|
||||
}
|
||||
|
||||
public function setAnimationOffsets(name:String, xOffset:Int, yOffset:Int):Void
|
||||
{
|
||||
animationOffsets.set(name, [xOffset, yOffset]);
|
||||
applyAnimationOffsets(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the animation that is currently playing.
|
||||
* If no animation is playing (usually this means the character is BROKEN!),
|
||||
* returns an empty string to prevent NPEs.
|
||||
*/
|
||||
public function getCurrentAnimation():String
|
||||
{
|
||||
if (this.animation == null || this.animation.curAnim == null)
|
||||
return "";
|
||||
return this.animation.curAnim.name;
|
||||
}
|
||||
|
||||
|
@ -178,16 +240,14 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
|
|||
|
||||
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 onNoteGhostMiss(event:GhostMissNoteScriptEvent) {}
|
||||
|
||||
public function onStepHit(event:SongTimeScriptEvent) {}
|
||||
|
||||
public function onCountdownStart(event:CountdownScriptEvent) {}
|
||||
|
@ -197,4 +257,6 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
|
|||
public function onCountdownEnd(event:CountdownScriptEvent) {}
|
||||
|
||||
public function onSongLoaded(eent:SongLoadScriptEvent) {}
|
||||
|
||||
public function onSongRetry(event:ScriptEvent) {}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package funkin.play.stage;
|
|||
|
||||
import funkin.modding.IHook;
|
||||
|
||||
@:hscriptClass
|
||||
@:keep
|
||||
class ScriptedBopper extends Bopper implements IHook {}
|
||||
//
|
||||
// @:hscriptClass
|
||||
// @:keep
|
||||
// class ScriptedBopper extends Bopper implements IHook {}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package funkin.play.stage;
|
||||
|
||||
import funkin.play.character.CharacterBase;
|
||||
import funkin.util.assets.FlxAnimationUtil;
|
||||
import funkin.play.character.BaseCharacter;
|
||||
import funkin.modding.events.ScriptEventDispatcher;
|
||||
import funkin.modding.events.ScriptEvent;
|
||||
import funkin.modding.events.ScriptEvent.CountdownScriptEvent;
|
||||
|
@ -11,7 +12,7 @@ import flixel.group.FlxSpriteGroup;
|
|||
import flixel.math.FlxPoint;
|
||||
import flixel.util.FlxSort;
|
||||
import funkin.modding.IHook;
|
||||
import funkin.play.character.CharacterBase.CharacterType;
|
||||
import funkin.play.character.BaseCharacter.CharacterType;
|
||||
import funkin.play.stage.StageData.StageDataParser;
|
||||
import funkin.util.SortUtil;
|
||||
|
||||
|
@ -30,7 +31,7 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte
|
|||
public var camZoom:Float = 1.0;
|
||||
|
||||
var namedProps:Map<String, FlxSprite> = new Map<String, FlxSprite>();
|
||||
var characters:Map<String, CharacterBase> = new Map<String, CharacterBase>();
|
||||
var characters:Map<String, BaseCharacter> = new Map<String, BaseCharacter>();
|
||||
var charactersOld:Map<String, Character> = new Map<String, Character>();
|
||||
var boppers:Array<Bopper> = new Array<Bopper>();
|
||||
|
||||
|
@ -145,21 +146,22 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte
|
|||
for (propAnim in dataProp.animations)
|
||||
{
|
||||
propSprite.animation.add(propAnim.name, propAnim.frameIndices);
|
||||
|
||||
if (Std.isOfType(propSprite, Bopper))
|
||||
{
|
||||
cast(propSprite, Bopper).setAnimationOffsets(propAnim.name, propAnim.offsets[0], propAnim.offsets[1]);
|
||||
}
|
||||
}
|
||||
default: // "sparrow"
|
||||
for (propAnim in dataProp.animations)
|
||||
{
|
||||
if (propAnim.frameIndices.length == 0)
|
||||
{
|
||||
propSprite.animation.addByPrefix(propAnim.name, propAnim.prefix, propAnim.frameRate, propAnim.looped, propAnim.flipX,
|
||||
propAnim.flipY);
|
||||
}
|
||||
else
|
||||
{
|
||||
propSprite.animation.addByIndices(propAnim.name, propAnim.prefix, propAnim.frameIndices, "", propAnim.frameRate, propAnim.looped,
|
||||
propAnim.flipX, propAnim.flipY);
|
||||
}
|
||||
}
|
||||
FlxAnimationUtil.addSparrowAnimations(propSprite, dataProp.animations);
|
||||
}
|
||||
|
||||
if (Std.isOfType(propSprite, Bopper))
|
||||
{
|
||||
for (propAnim in dataProp.animations)
|
||||
{
|
||||
cast(propSprite, Bopper).setAnimationOffsets(propAnim.name, propAnim.offsets[0], propAnim.offsets[1]);
|
||||
}
|
||||
}
|
||||
|
||||
if (dataProp.startingAnimation != null)
|
||||
|
@ -236,8 +238,11 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte
|
|||
/**
|
||||
* Used by the PlayState to add a character to the stage.
|
||||
*/
|
||||
public function addCharacter(character:CharacterBase, charType:CharacterType)
|
||||
public function addCharacter(character:BaseCharacter, charType:CharacterType)
|
||||
{
|
||||
if (character == null)
|
||||
return;
|
||||
|
||||
// Apply position and z-index.
|
||||
switch (charType)
|
||||
{
|
||||
|
@ -264,59 +269,63 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte
|
|||
this.add(character);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by the PlayState to add a character to the stage.
|
||||
*/
|
||||
public function addCharacterOld(character:Character, charType:CharacterType)
|
||||
public inline function getGirlfriendPosition():FlxPoint
|
||||
{
|
||||
// Apply position and z-index.
|
||||
switch (charType)
|
||||
{
|
||||
case BF:
|
||||
this.charactersOld.set("bf", character);
|
||||
character.zIndex = _data.characters.bf.zIndex;
|
||||
character.x = _data.characters.bf.position[0];
|
||||
character.y = _data.characters.bf.position[1];
|
||||
case GF:
|
||||
this.charactersOld.set("gf", character);
|
||||
character.zIndex = _data.characters.gf.zIndex;
|
||||
character.x = _data.characters.gf.position[0];
|
||||
character.y = _data.characters.gf.position[1];
|
||||
case DAD:
|
||||
this.charactersOld.set("dad", character);
|
||||
character.zIndex = _data.characters.dad.zIndex;
|
||||
character.x = _data.characters.dad.position[0];
|
||||
character.y = _data.characters.dad.position[1];
|
||||
default:
|
||||
this.charactersOld.set(character.curCharacter, character);
|
||||
}
|
||||
return new FlxPoint(_data.characters.gf.position[0], _data.characters.gf.position[1]);
|
||||
}
|
||||
|
||||
// Add the character to the scene.
|
||||
this.add(character);
|
||||
public inline function getBoyfriendPosition():FlxPoint
|
||||
{
|
||||
return new FlxPoint(_data.characters.bf.position[0], _data.characters.bf.position[1]);
|
||||
}
|
||||
|
||||
public inline function getDadPosition():FlxPoint
|
||||
{
|
||||
return new FlxPoint(_data.characters.dad.position[0], _data.characters.dad.position[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a given character from the stage.
|
||||
*/
|
||||
public function getCharacter(id:String):CharacterBase
|
||||
public function getCharacter(id:String):BaseCharacter
|
||||
{
|
||||
return this.characters.get(id);
|
||||
}
|
||||
|
||||
public function getBoyfriend():CharacterBase
|
||||
/**
|
||||
* Retrieve the Boyfriend character.
|
||||
* @param pop If true, the character will be removed from the stage as well.
|
||||
*/
|
||||
public function getBoyfriend(?pop:Bool = false):BaseCharacter
|
||||
{
|
||||
return getCharacter('bf');
|
||||
if (pop)
|
||||
{
|
||||
var boyfriend:BaseCharacter = getCharacter("bf");
|
||||
|
||||
// Remove the character from the stage.
|
||||
this.remove(boyfriend);
|
||||
this.characters.remove("bf");
|
||||
|
||||
return boyfriend;
|
||||
}
|
||||
else
|
||||
{
|
||||
return getCharacter('bf');
|
||||
}
|
||||
|
||||
// return this.charactersOld.get('bf');
|
||||
}
|
||||
|
||||
public function getGirlfriend():Character
|
||||
public function getGirlfriend():BaseCharacter
|
||||
{
|
||||
return this.charactersOld.get('gf');
|
||||
return getCharacter('gf');
|
||||
// return this.charactersOld.get('gf');
|
||||
}
|
||||
|
||||
public function getDad():Character
|
||||
public function getDad():BaseCharacter
|
||||
{
|
||||
return this.charactersOld.get('dad');
|
||||
return getCharacter('dad');
|
||||
// return this.charactersOld.get('dad');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -345,6 +354,32 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte
|
|||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch an event to all the characters in the stage.
|
||||
* @param event The script event to dispatch.
|
||||
*/
|
||||
public function dispatchToCharacters(event:ScriptEvent):Void
|
||||
{
|
||||
for (characterId in characters.keys())
|
||||
{
|
||||
dispatchToCharacter(characterId, event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch an event to a specific character.
|
||||
* @param characterId The ID of the character to dispatch to.
|
||||
* @param event The script event to dispatch.
|
||||
*/
|
||||
public function dispatchToCharacter(characterId:String, event:ScriptEvent):Void
|
||||
{
|
||||
var character:BaseCharacter = getCharacter(characterId);
|
||||
if (character != null)
|
||||
{
|
||||
ScriptEventDispatcher.callEvent(character, event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* onDestroy gets called when the player is leaving the PlayState,
|
||||
* and is used to clean up any objects that need to be destroyed.
|
||||
|
@ -419,15 +454,8 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte
|
|||
|
||||
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) {}
|
||||
|
@ -443,5 +471,9 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte
|
|||
|
||||
public function onNoteMiss(event:NoteScriptEvent) {}
|
||||
|
||||
public function onNoteGhostMiss(event:GhostMissNoteScriptEvent) {}
|
||||
|
||||
public function onSongLoaded(eent:SongLoadScriptEvent) {}
|
||||
|
||||
public function onSongRetry(event:ScriptEvent) {}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package funkin.play.stage;
|
||||
|
||||
import funkin.util.VersionUtil;
|
||||
import flixel.util.typeLimit.OneOfTwo;
|
||||
import funkin.util.assets.DataAssets;
|
||||
import haxe.Json;
|
||||
|
@ -17,7 +18,12 @@ class StageDataParser
|
|||
* Handle breaking changes by incrementing this value
|
||||
* and adding migration to the `migrateStageData()` function.
|
||||
*/
|
||||
public static final STAGE_DATA_VERSION:String = "1.0";
|
||||
public static final STAGE_DATA_VERSION:String = "1.0.0";
|
||||
|
||||
/**
|
||||
* The current version rule check for the stage data format.
|
||||
*/
|
||||
public static final STAGE_DATA_VERSION_RULE:String = "1.0.x";
|
||||
|
||||
static final stageCache:Map<String, Stage> = new Map<String, Stage>();
|
||||
|
||||
|
@ -163,16 +169,16 @@ class StageDataParser
|
|||
}
|
||||
}
|
||||
|
||||
static final DEFAULT_NAME:String = "Untitled Stage";
|
||||
static final DEFAULT_CAMERAZOOM:Float = 1.0;
|
||||
static final DEFAULT_ZINDEX:Int = 0;
|
||||
static final DEFAULT_DANCEEVERY:Int = 0;
|
||||
static final DEFAULT_SCALE:Float = 1.0;
|
||||
static final DEFAULT_ISPIXEL:Bool = false;
|
||||
static final DEFAULT_POSITION:Array<Float> = [0, 0];
|
||||
static final DEFAULT_SCROLL:Array<Float> = [0, 0];
|
||||
static final DEFAULT_FRAMEINDICES:Array<Int> = [];
|
||||
static final DEFAULT_ANIMTYPE:String = "sparrow";
|
||||
static final DEFAULT_CAMERAZOOM:Float = 1.0;
|
||||
static final DEFAULT_DANCEEVERY:Int = 0;
|
||||
static final DEFAULT_ISPIXEL:Bool = false;
|
||||
static final DEFAULT_NAME:String = "Untitled Stage";
|
||||
static final DEFAULT_OFFSETS:Array<Int> = [0, 0];
|
||||
static final DEFAULT_POSITION:Array<Float> = [0, 0];
|
||||
static final DEFAULT_SCALE:Float = 1.0;
|
||||
static final DEFAULT_SCROLL:Array<Float> = [0, 0];
|
||||
static final DEFAULT_ZINDEX:Int = 0;
|
||||
|
||||
static final DEFAULT_CHARACTER_DATA:StageDataCharacter = {
|
||||
zIndex: DEFAULT_ZINDEX,
|
||||
|
@ -200,9 +206,9 @@ class StageDataParser
|
|||
return null;
|
||||
}
|
||||
|
||||
if (input.version != STAGE_DATA_VERSION)
|
||||
if (!VersionUtil.validateVersion(input.version, STAGE_DATA_VERSION_RULE))
|
||||
{
|
||||
trace('[STAGEDATA] ERROR: Could not load stage data for "$id": bad/outdated version (got ${input.version}, expected ${STAGE_DATA_VERSION})');
|
||||
trace('[STAGEDATA] ERROR: Could not load stage data for "$id": bad version (got ${input.version}, expected ${STAGE_DATA_VERSION_RULE})');
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -302,9 +308,9 @@ class StageDataParser
|
|||
inputAnimation.frameRate = 24;
|
||||
}
|
||||
|
||||
if (inputAnimation.frameIndices == null)
|
||||
if (inputAnimation.offsets == null)
|
||||
{
|
||||
inputAnimation.frameIndices = DEFAULT_FRAMEINDICES;
|
||||
inputAnimation.offsets = DEFAULT_OFFSETS;
|
||||
}
|
||||
|
||||
if (inputAnimation.looped == null)
|
||||
|
@ -362,8 +368,12 @@ class StageDataParser
|
|||
|
||||
typedef StageData =
|
||||
{
|
||||
// Uses semantic versioning.
|
||||
/**
|
||||
* The sematic version number of the stage data JSON format.
|
||||
* Supports fancy comparisons like NPM does it's neat.
|
||||
*/
|
||||
var version:String;
|
||||
|
||||
var name:String;
|
||||
var cameraZoom:Null<Float>;
|
||||
var props:Array<StageDataProp>;
|
||||
|
|
|
@ -15,7 +15,8 @@ abstract BoldText(AtlasText) from AtlasText to AtlasText
|
|||
}
|
||||
|
||||
/**
|
||||
* Alphabet.hx has a ton of bugs and does a bunch of stuff I don't need, fuck that class
|
||||
* AtlasText is an improved version of Alphabet and FlxBitmapText.
|
||||
* It supports animations on the letters, and is less buggy than Alphabet.
|
||||
*/
|
||||
class AtlasText extends FlxTypedSpriteGroup<AtlasChar>
|
||||
{
|
||||
|
|
|
@ -5,7 +5,6 @@ import flixel.FlxSubState;
|
|||
import flixel.addons.transition.FlxTransitionableState;
|
||||
import flixel.group.FlxGroup;
|
||||
import flixel.util.FlxSignal;
|
||||
import funkin.i18n.FireTongueHandler.t;
|
||||
|
||||
// typedef OptionsState = OptionsMenu_old;
|
||||
// class OptionsState_new extends MusicBeatState
|
||||
|
@ -172,25 +171,25 @@ class OptionsMenu extends Page
|
|||
super();
|
||||
|
||||
add(items = new TextMenuList());
|
||||
createItem(t("PREFERENCES"), function() switchPage(Preferences));
|
||||
createItem(t("CONTROLS"), function() switchPage(Controls));
|
||||
// createItem(t("COLORS"), function() switchPage(Colors));
|
||||
createItem(t("MODS"), function() switchPage(Mods));
|
||||
createItem("PREFERENCES", function() switchPage(Preferences));
|
||||
createItem("CONTROLS", function() switchPage(Controls));
|
||||
// createItem("COLORS", function() switchPage(Colors));
|
||||
createItem("MODS", function() switchPage(Mods));
|
||||
|
||||
#if CAN_OPEN_LINKS
|
||||
if (showDonate)
|
||||
{
|
||||
var hasPopupBlocker = #if web true #else false #end;
|
||||
createItem(t("DONATE"), selectDonate, hasPopupBlocker);
|
||||
createItem("DONATE", selectDonate, hasPopupBlocker);
|
||||
}
|
||||
#end
|
||||
#if newgrounds
|
||||
if (NGio.isLoggedIn)
|
||||
createItem(t("LOGOUT"), selectLogout);
|
||||
createItem("LOGOUT", selectLogout);
|
||||
else
|
||||
createItem(t("LOGIN"), selectLogin);
|
||||
createItem("LOGIN", selectLogin);
|
||||
#end
|
||||
createItem(t("EXIT"), exit);
|
||||
createItem("EXIT", exit);
|
||||
}
|
||||
|
||||
function createItem(name:String, callback:Void->Void, fireInstantly = false)
|
||||
|
|
31
source/funkin/util/VersionUtil.hx
Normal file
31
source/funkin/util/VersionUtil.hx
Normal file
|
@ -0,0 +1,31 @@
|
|||
package funkin.util;
|
||||
|
||||
import thx.semver.VersionRule;
|
||||
import thx.semver.Version;
|
||||
|
||||
/**
|
||||
* Remember, increment the patch version (1.0.x) if you make a bugfix,
|
||||
* increment the minor version (1.x.0) if you make a new feature (but previous content is still compatible),
|
||||
* and increment the major version (x.0.0) if you make a breaking change (e.g. new API or reorganized file format).
|
||||
*/
|
||||
class VersionUtil
|
||||
{
|
||||
/**
|
||||
* Checks that a given verison number satisisfies a given version rule.
|
||||
* Version rule can be complex, e.g. "1.0.x" or ">=1.0.0,<1.1.0", or anything NPM supports.
|
||||
*/
|
||||
public static function validateVersion(version:String, versionRule:String):Bool
|
||||
{
|
||||
try
|
||||
{
|
||||
var v:Version = version; // Perform a cast.
|
||||
var vr:VersionRule = versionRule; // Perform a cast.
|
||||
return v.satisfies(vr);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
trace('[VERSIONUTIL] Invalid semantic version: ${version}');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -21,7 +21,10 @@ class DataAssets
|
|||
{
|
||||
var pathNoSuffix = textPath.substring(0, textPath.length - ext.length);
|
||||
var pathNoPrefix = pathNoSuffix.substring(queryPath.length);
|
||||
results.push(pathNoPrefix);
|
||||
|
||||
// No duplicates! Why does this happen?
|
||||
if (!results.contains(pathNoPrefix))
|
||||
results.push(pathNoPrefix);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
42
source/funkin/util/assets/FlxAnimationUtil.hx
Normal file
42
source/funkin/util/assets/FlxAnimationUtil.hx
Normal file
|
@ -0,0 +1,42 @@
|
|||
package funkin.util.assets;
|
||||
|
||||
import funkin.play.AnimationData;
|
||||
import flixel.FlxSprite;
|
||||
|
||||
class FlxAnimationUtil
|
||||
{
|
||||
/**
|
||||
* Properly adds an animation to a sprite based on JSON data.
|
||||
*/
|
||||
public static function addSparrowAnimation(target:FlxSprite, anim:AnimationData)
|
||||
{
|
||||
var frameRate = anim.frameRate == null ? 24 : anim.frameRate;
|
||||
var looped = anim.looped == null ? false : anim.looped;
|
||||
var flipX = anim.flipX == null ? false : anim.flipX;
|
||||
var flipY = anim.flipY == null ? false : anim.flipY;
|
||||
|
||||
if (anim.frameIndices != null && anim.frameIndices.length > 0)
|
||||
{
|
||||
// trace('addByIndices(${anim.name}, ${anim.prefix}, ${anim.frameIndices}, ${frameRate}, ${looped}, ${flipX}, ${flipY})');
|
||||
target.animation.addByIndices(anim.name, anim.prefix, anim.frameIndices, "", frameRate, looped, flipX, flipY);
|
||||
// trace('RESULT:${target.animation.getAnimationList()}');
|
||||
}
|
||||
else
|
||||
{
|
||||
// trace('addByPrefix(${anim.name}, ${anim.prefix}, ${frameRate}, ${looped}, ${flipX}, ${flipY})');
|
||||
target.animation.addByPrefix(anim.name, anim.prefix, frameRate, looped, flipX, flipY);
|
||||
// trace('RESULT:${target.animation.getAnimationList()}');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Properly adds multiple animations to a sprite based on JSON data.
|
||||
*/
|
||||
public static function addSparrowAnimations(target:FlxSprite, animations:Array<AnimationData>)
|
||||
{
|
||||
for (anim in animations)
|
||||
{
|
||||
addSparrowAnimation(target, anim);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue