1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2025-03-21 09:29:41 +00:00

A bunch of Freeplay visual fixes

This commit is contained in:
EliteMasterEric 2024-09-01 03:22:34 -04:00
parent 78fb6e1188
commit 7511de1e7a
17 changed files with 436 additions and 109 deletions

2
assets

@ -1 +1 @@
Subproject commit 5a0c8da32111571648b8fd07deabe2f78ae99eaa
Subproject commit dc226333655b7ec841b213968ef5264278fbcd63

33
source/funkin/Assets.hx Normal file
View file

@ -0,0 +1,33 @@
package funkin;
/**
* A wrapper around `openfl.utils.Assets` which disallows access to the harmful functions.
* Later we'll add Funkin-specific caching to this.
*/
class Assets
{
public static function getText(path:String):String
{
return openfl.utils.Assets.getText(path);
}
public static function getMusic(path:String):openfl.media.Sound
{
return openfl.utils.Assets.getMusic(path);
}
public static function getBitmapData(path:String):openfl.display.BitmapData
{
return openfl.utils.Assets.getBitmapData(path);
}
public static function getBytes(path:String):haxe.io.Bytes
{
return openfl.utils.Assets.getBytes(path);
}
public static function list(type:openfl.utils.AssetType):Array<String>
{
return openfl.utils.Assets.list(type);
}
}

View file

@ -31,6 +31,13 @@ class PlayerData
@:default(false)
public var showUnownedChars:Bool = false;
/**
* Which freeplay style to use for this character.
*/
@:optional
@:default("bf")
public var freeplayStyle:String = Constants.DEFAULT_FREEPLAY_STYLE;
/**
* Data for displaying this character in the Freeplay menu.
* If null, display no DJ.
@ -105,6 +112,9 @@ class PlayerFreeplayDJData
@:jignored
var prefixToOffsetsMap:Map<String, Array<Float>>;
@:optional
var charSelect:Null<PlayerFreeplayDJCharSelectData>;
@:optional
var cartoon:Null<PlayerFreeplayDJCartoonData>;
@ -237,6 +247,11 @@ class PlayerFreeplayDJData
{
return fistPump?.loopBadEndFrame ?? 0;
}
public function getCharSelectTransitionDelay():Float
{
return charSelect?.transitionDelay ?? 0.25;
}
}
class PlayerCharSelectData
@ -300,6 +315,11 @@ typedef PlayerResultsAnimationData =
var loopFrameLabel:Null<String>;
};
typedef PlayerFreeplayDJCharSelectData =
{
var transitionDelay:Float;
}
typedef PlayerFreeplayDJCartoonData =
{
var soundClickFrame:Int;

View file

@ -264,11 +264,20 @@ class SongOffsets implements ICloneable<SongOffsets>
@:default([])
public var vocals:Map<String, Float>;
public function new(instrumental:Float = 0.0, ?altInstrumentals:Map<String, Float>, ?vocals:Map<String, Float>)
/**
* The offset, in milliseconds, to apply to the songs vocals, relative to each alternate instrumental.
* This is useful for the circumstance where, for example, an alt instrumental has a few seconds of lead in before the song starts.
*/
@:optional
@:default([])
public var altVocals:Map<String, Map<String, Float>>;
public function new(instrumental:Float = 0.0, ?altInstrumentals:Map<String, Float>, ?vocals:Map<String, Float>, ?altVocals:Map<String, Map<String, Float>>)
{
this.instrumental = instrumental;
this.altInstrumentals = altInstrumentals == null ? new Map<String, Float>() : altInstrumentals;
this.vocals = vocals == null ? new Map<String, Float>() : vocals;
this.altVocals = altVocals == null ? new Map<String, Map<String, Float>>() : altVocals;
}
public function getInstrumentalOffset(?instrumental:String):Float

View file

@ -105,23 +105,6 @@ class FlxAtlasSprite extends FlxAnimate
return this.currentAnimation;
}
/**
* `anim.finished` always returns false on looping animations,
* but this function will return true if we are on the last frame of the looping animation.
*/
public function isLoopFinished():Bool
{
if (this.anim == null) return false;
if (!this.anim.isPlaying) return false;
// Reverse animation finished.
if (this.anim.reversed && this.anim.curFrame == 0) return true;
// Forward animation finished.
if (!this.anim.reversed && this.anim.curFrame >= (this.anim.length - 1)) return true;
return false;
}
var _completeAnim:Bool = false;
var fr:FlxKeyFrame = null;
@ -142,6 +125,8 @@ class FlxAtlasSprite extends FlxAnimate
// Skip if not allowed to play animations.
if ((!canPlayOtherAnims && !ignoreOther)) return;
if (anim == null) return;
if (id == null || id == '') id = this.currentAnimation;
if (this.currentAnimation == id && !restart)
@ -189,10 +174,16 @@ class FlxAtlasSprite extends FlxAnimate
// Move to the first frame of the animation.
// goToFrameLabel(id);
trace('Playing animation $id');
this.anim.play(id, restart, false, startFrame);
goToFrameLabel(id);
fr = anim.getFrameLabel(id);
if (this.anim.symbolDictionary.exists(id) || (this.anim.getByName(id) != null))
{
this.anim.play(id, restart, false, startFrame);
}
// Only call goToFrameLabel if there is a frame label with that name. This prevents annoying warnings!
if (getFrameLabelNames().indexOf(id) != -1)
{
goToFrameLabel(id);
fr = anim.getFrameLabel(id);
}
anim.curFrame += startFrame;
this.currentAnimation = id;
@ -218,6 +209,8 @@ class FlxAtlasSprite extends FlxAnimate
*/
public function isLoopComplete():Bool
{
if (this.anim == null) return false;
if (!this.anim.isPlaying) return false;
return (anim.reversed && anim.curFrame == 0 || !(anim.reversed) && (anim.curFrame) >= (anim.length - 1));
}
@ -244,6 +237,18 @@ class FlxAtlasSprite extends FlxAnimate
this.anim.goToFrameLabel(label);
}
function getFrameLabelNames(?layer:haxe.extern.EitherType<Int, String> = null)
{
var labels = this.anim.getFrameLabels(layer);
var array = [];
for (label in labels)
{
array.push(label.name);
}
return array;
}
function getNextFrameLabel(label:String):String
{
return listAnimations()[(getLabelIndex(label) + 1) % listAnimations().length];
@ -272,7 +277,7 @@ class FlxAtlasSprite extends FlxAnimate
{
onAnimationFrame.dispatch(currentAnimation, frame);
if (fr != null && frame > (fr.index + fr.duration - 1) || isLoopFinished())
if (fr != null && frame > (fr.index + fr.duration - 1) || isLoopComplete())
{
anim.pause();
_onAnimationComplete();

View file

@ -73,6 +73,22 @@ interface INoteScriptedClass extends IScriptedClass
public function onNoteMiss(event:NoteScriptEvent):Void;
}
/**
* Defines a set of callbacks available to scripted classes which represent sprites synced with the BPM.
*/
interface IBPMSyncedScriptedClass extends IScriptedClass
{
/**
* Called once every step of the song.
*/
public function onStepHit(event:SongTimeScriptEvent):Void;
/**
* Called once every beat of the song.
*/
public function onBeatHit(event:SongTimeScriptEvent):Void;
}
/**
* Developer note:
*
@ -86,7 +102,7 @@ interface INoteScriptedClass extends IScriptedClass
/**
* Defines a set of callbacks available to scripted classes that involve the lifecycle of the Play State.
*/
interface IPlayStateScriptedClass extends INoteScriptedClass
interface IPlayStateScriptedClass extends INoteScriptedClass extends IBPMSyncedScriptedClass
{
/**
* Called when the game is paused.
@ -136,16 +152,6 @@ interface IPlayStateScriptedClass extends INoteScriptedClass
*/
public function onSongEvent(event:SongEventScriptEvent):Void;
/**
* Called once every step of the song.
*/
public function onStepHit(event:SongTimeScriptEvent):Void;
/**
* Called once every beat of the song.
*/
public function onBeatHit(event:SongTimeScriptEvent):Void;
/**
* Called when the countdown of the song starts.
*/

View file

@ -235,6 +235,10 @@ class PolymodHandler
Polymod.addImportAlias('funkin.data.event.SongEventSchema', funkin.data.event.SongEventSchema.SongEventSchemaRaw);
// `lime.utils.Assets` literally just has a private `resolveClass` function for some reason? so we replace it with our own.
Polymod.addImportAlias('lime.utils.Assets', funkin.Assets);
Polymod.addImportAlias('openfl.utils.Assets', funkin.Assets);
// Add blacklisting for prohibited classes and packages.
// `Sys`
@ -269,11 +273,6 @@ class PolymodHandler
// System.load() can load malicious DLLs
Polymod.blacklistImport('lime.system.System');
// `lime.utils.Assets`
// Literally just has a private `resolveClass` function for some reason?
Polymod.blacklistImport('lime.utils.Assets');
Polymod.blacklistImport('openfl.utils.Assets');
// `openfl.desktop.NativeProcess`
// Can load native processes on the host operating system.
Polymod.blacklistImport('openfl.desktop.NativeProcess');

View file

@ -94,6 +94,21 @@ class ScriptEventDispatcher
}
}
if (Std.isOfType(target, IBPMSyncedScriptedClass))
{
var t:IBPMSyncedScriptedClass = cast(target, IBPMSyncedScriptedClass);
switch (event.type)
{
case SONG_BEAT_HIT:
t.onBeatHit(cast event);
return;
case SONG_STEP_HIT:
t.onStepHit(cast event);
return;
default: // Continue;
}
}
if (Std.isOfType(target, IPlayStateScriptedClass))
{
var t:IPlayStateScriptedClass = cast(target, IPlayStateScriptedClass);
@ -102,12 +117,6 @@ class ScriptEventDispatcher
case NOTE_GHOST_MISS:
t.onNoteGhostMiss(cast event);
return;
case SONG_BEAT_HIT:
t.onBeatHit(cast event);
return;
case SONG_STEP_HIT:
t.onStepHit(cast event);
return;
case SONG_START:
t.onSongStart(event);
return;

View file

@ -7,10 +7,12 @@ import flixel.math.FlxMath;
import funkin.util.FramesJSFLParser;
import funkin.util.FramesJSFLParser.FramesJSFLInfo;
import funkin.util.FramesJSFLParser.FramesJSFLFrame;
import funkin.modding.IScriptedClass.IBPMSyncedScriptedClass;
import flixel.math.FlxMath;
import funkin.modding.events.ScriptEvent;
import funkin.vis.dsp.SpectralAnalyzer;
class CharSelectGF extends FlxAtlasSprite
class CharSelectGF extends FlxAtlasSprite implements IBPMSyncedScriptedClass
{
var fadeTimer:Float = 0;
var fadingStatus:FadeStatus = OFF;
@ -54,6 +56,7 @@ class CharSelectGF extends FlxAtlasSprite
default:
}
#if FEATURE_DEBUG_FUNCTIONS
if (FlxG.keys.justPressed.J)
{
alpha = 1;
@ -65,8 +68,27 @@ class CharSelectGF extends FlxAtlasSprite
alpha = 0;
fadingStatus = FADE_IN;
}
#end
}
public function onStepHit(event:SongTimeScriptEvent):Void {}
var danceEvery:Int = 2;
public function onBeatHit(event:SongTimeScriptEvent):Void
{
// TODO: There's a minor visual bug where there's a little stutter.
// This happens because the animation is getting restarted while it's already playing.
// I tried make this not interrupt an existing idle,
// but isAnimationFinished() and isLoopComplete() both don't work! What the hell?
// danceEvery isn't necessary if that gets fixed.
if (getCurrentAnimation() == "idle" && (event.beat % danceEvery == 0))
{
trace('GF beat hit');
playAnimation("idle", true, false, false);
}
};
override public function draw()
{
if (analyzer != null) drawFFT();
@ -160,18 +182,27 @@ class CharSelectGF extends FlxAtlasSprite
}
// We don't need to update any anims if we didn't change GF
if (prevGF == curGF) return;
if (prevGF != curGF)
{
loadAtlas(Paths.animateAtlas("charSelect/" + curGF + "Chill"));
loadAtlas(Paths.animateAtlas("charSelect/" + curGF + "Chill"));
animInInfo = FramesJSFLParser.parse(Paths.file("images/charSelect/" + curGF + "AnimInfo/" + curGF + "In.txt"));
animOutInfo = FramesJSFLParser.parse(Paths.file("images/charSelect/" + curGF + "AnimInfo/" + curGF + "Out.txt"));
}
animInInfo = FramesJSFLParser.parse(Paths.file("images/charSelect/" + curGF + "AnimInfo/" + curGF + "In.txt"));
animOutInfo = FramesJSFLParser.parse(Paths.file("images/charSelect/" + curGF + "AnimInfo/" + curGF + "Out.txt"));
playAnimation("idle", true, false, true);
playAnimation("idle", true, false, false);
// addFrameCallback(getNextFrameLabel("idle"), () -> playAnimation("idle", true, false, false));
updateHitbox();
}
public function onScriptEvent(event:ScriptEvent):Void {};
public function onCreate(event:ScriptEvent):Void {};
public function onDestroy(event:ScriptEvent):Void {};
public function onUpdate(event:UpdateScriptEvent):Void {};
}
enum FadeStatus

View file

@ -3,8 +3,10 @@ package funkin.ui.charSelect;
import flixel.FlxSprite;
import funkin.graphics.adobeanimate.FlxAtlasSprite;
import flxanimate.animate.FlxKeyFrame;
import funkin.modding.IScriptedClass.IBPMSyncedScriptedClass;
import funkin.modding.events.ScriptEvent;
class CharSelectPlayer extends FlxAtlasSprite
class CharSelectPlayer extends FlxAtlasSprite implements IBPMSyncedScriptedClass
{
var desLp:FlxKeyFrame = null;
@ -18,11 +20,20 @@ class CharSelectPlayer extends FlxAtlasSprite
switch (animLabel)
{
case "slidein":
if (hasAnimation("slidein idle point")) playAnimation("slidein idle point", true, false, false);
if (hasAnimation("slidein idle point"))
{
playAnimation("slidein idle point", true, false, false);
}
else
playAnimation("idle", true, false, true);
{
// Handled by onBeatHit now
playAnimation("idle", true, false, false);
}
case "slidein idle point":
playAnimation("idle", true, false, true);
// Handled by onBeatHit now
playAnimation("idle", true, false, false);
case "idle":
trace('Waiting for onBeatHit');
}
});
@ -31,6 +42,22 @@ class CharSelectPlayer extends FlxAtlasSprite
});
}
public function onStepHit(event:SongTimeScriptEvent):Void {}
public function onBeatHit(event:SongTimeScriptEvent):Void
{
// TODO: There's a minor visual bug where there's a little stutter.
// This happens because the animation is getting restarted while it's already playing.
// I tried make this not interrupt an existing idle,
// but isAnimationFinished() and isLoopComplete() both don't work! What the hell?
// danceEvery isn't necessary if that gets fixed.
if (getCurrentAnimation() == "idle")
{
trace('Player beat hit');
playAnimation("idle", true, false, false);
}
};
public function updatePosition(str:String)
{
switch (str)
@ -61,4 +88,12 @@ class CharSelectPlayer extends FlxAtlasSprite
updatePosition(str);
}
public function onScriptEvent(event:ScriptEvent):Void {};
public function onCreate(event:ScriptEvent):Void {};
public function onDestroy(event:ScriptEvent):Void {};
public function onUpdate(event:UpdateScriptEvent):Void {};
}

View file

@ -148,7 +148,7 @@ class CharSelectSubState extends MusicBeatSubState
var stageSpr:FlxSprite = new FlxSprite(-40, 391);
stageSpr.frames = Paths.getSparrowAtlas("charSelect/charSelectStage");
stageSpr.animation.addByPrefix("idle", "stage", 24, true);
stageSpr.animation.addByPrefix("idle", "stage full instance 1", 24, true);
stageSpr.animation.play("idle");
add(stageSpr);
@ -195,14 +195,14 @@ class CharSelectSubState extends MusicBeatSubState
var dipshitBlur:FlxSprite = new FlxSprite(419, -65);
dipshitBlur.frames = Paths.getSparrowAtlas("charSelect/dipshitBlur");
dipshitBlur.animation.addByPrefix('idle', "CHOOSE vertical", 24, true);
dipshitBlur.animation.addByPrefix('idle', "CHOOSE vertical offset instance 1", 24, true);
dipshitBlur.blend = BlendMode.ADD;
dipshitBlur.animation.play("idle");
add(dipshitBlur);
var dipshitBacking:FlxSprite = new FlxSprite(423, -17);
dipshitBacking.frames = Paths.getSparrowAtlas("charSelect/dipshitBacking");
dipshitBacking.animation.addByPrefix('idle', "CHOOSE horizontal", 24, true);
dipshitBacking.animation.addByPrefix('idle', "CHOOSE horizontal offset instance 1", 24, true);
dipshitBacking.blend = BlendMode.ADD;
dipshitBacking.animation.play("idle");
add(dipshitBacking);
@ -261,14 +261,14 @@ class CharSelectSubState extends MusicBeatSubState
cursorConfirmed = new FlxSprite(0, 0);
cursorConfirmed.scrollFactor.set();
cursorConfirmed.frames = Paths.getSparrowAtlas("charSelect/charSelectorConfirm");
cursorConfirmed.animation.addByPrefix("idle", "cursor ACCEPTED", 24, true);
cursorConfirmed.animation.addByPrefix("idle", "cursor ACCEPTED instance 1", 24, true);
cursorConfirmed.visible = false;
add(cursorConfirmed);
cursorDenied = new FlxSprite(0, 0);
cursorDenied.scrollFactor.set();
cursorDenied.frames = Paths.getSparrowAtlas("charSelect/charSelectorDenied");
cursorDenied.animation.addByPrefix("idle", "cursor DENIED", 24, false);
cursorDenied.animation.addByPrefix("idle", "cursor DENIED instance 1", 24, false);
cursorDenied.visible = false;
add(cursorDenied);
@ -289,8 +289,6 @@ class CharSelectSubState extends MusicBeatSubState
FlxG.debugger.addTrackerProfile(new TrackerProfile(CharSelectSubState, ["curChar", "grpXSpread", "grpYSpread"]));
FlxG.debugger.track(this);
FlxG.sound.playMusic(Paths.music('charSelect/charSelectMusic'));
camFollow = new FlxObject(0, 0, 1, 1);
add(camFollow);
camFollow.screenCenter();
@ -567,6 +565,16 @@ class CharSelectSubState extends MusicBeatSubState
cursorDarkBlue.y = MathUtil.coolLerp(cursorDarkBlue.y, cursorLocIntended.y, lerpAmnt * 0.2);
}
public override function dispatchEvent(event:ScriptEvent):Void
{
// super.dispatchEvent(event) dispatches event to module scripts.
super.dispatchEvent(event);
// Dispatch events (like onBeatHit) to props
ScriptEventDispatcher.callEvent(playerChill, event);
ScriptEventDispatcher.callEvent(gfChill, event);
}
function spamOnStep():Void
{
if (spamUp || spamDown || spamLeft || spamRight)

View file

@ -15,7 +15,9 @@ class FreeplayDJ extends FlxAtlasSprite
{
// Represents the sprite's current status.
// Without state machines I would have driven myself crazy years ago.
public var currentState:FreeplayDJState = Intro;
// Made this PRIVATE so we can keep track of everything that can alter the state!
// Add a function to this class if you want to edit this value from outside.
private var currentState:FreeplayDJState = Intro;
// A callback activated when the intro animation finishes.
public var onIntroDone:FlxSignal = new FlxSignal();
@ -378,7 +380,7 @@ class FreeplayDJ extends FlxAtlasSprite
public function toCharSelect():Void
{
if (hasAnimation('charSelect'))
if (hasAnimation(playableCharData.getAnimationPrefix('charSelect')))
{
currentState = CharSelect;
var animPrefix = playableCharData.getAnimationPrefix('charSelect');

View file

@ -224,12 +224,14 @@ class FreeplayState extends MusicBeatSubState
{
currentCharacterId = params?.character ?? rememberedCharacterId;
var fetchPlayableCharacter = function():PlayableCharacter {
var result = PlayerRegistry.instance.fetchEntry(params?.character ?? rememberedCharacterId);
if (result == null) throw 'No valid playable character with id ${params?.character}';
var targetCharId = params?.character ?? rememberedCharacterId;
var result = PlayerRegistry.instance.fetchEntry(targetCharId);
if (result == null) throw 'No valid playable character with id ${targetCharId}';
return result;
};
currentCharacter = fetchPlayableCharacter();
styleData = FreeplayStyleRegistry.instance.fetchEntry(currentCharacter.getFreeplayStyleID());
rememberedCharacterId = currentCharacter?.id ?? Constants.DEFAULT_CHARACTER;
fromResultsParams = params?.fromResults;
@ -317,6 +319,9 @@ class FreeplayState extends MusicBeatSubState
isDebug = true;
#end
// Block input until the intro finishes.
busy = true;
// Add a null entry that represents the RANDOM option
songs.push(null);
@ -645,13 +650,14 @@ class FreeplayState extends MusicBeatSubState
// be careful not to "add()" things in here unless it's to a group that's already added to the state
// otherwise it won't be properly attatched to funnyCamera (relavent code should be at the bottom of create())
var onDJIntroDone = function() {
busy = false;
// when boyfriend hits dat shiii
albumRoll.playIntro();
var daSong = grpCapsules.members[curSelected].songData;
albumRoll.albumId = daSong?.albumId;
FlxTween.tween(grpDifficulties, {x: 90}, 0.6, {ease: FlxEase.quartOut});
diffSelLeft.visible = true;
@ -1187,6 +1193,158 @@ class FreeplayState extends MusicBeatSubState
});
}
function tryOpenCharSelect():Void
{
// Check if we have ACCESS to character select!
trace('Is Pico unlocked? ${PlayerRegistry.instance.fetchEntry('pico')?.isUnlocked()}');
trace('Number of characters: ${PlayerRegistry.instance.countUnlockedCharacters()}');
if (PlayerRegistry.instance.countUnlockedCharacters() > 1)
{
trace('Opening character select!');
}
else
{
trace('Not enough characters unlocked to open character select!');
FunkinSound.playOnce(Paths.sound('cancelMenu'));
return;
}
busy = true;
FunkinSound.playOnce(Paths.sound('confirmMenu'));
if (dj != null)
{
dj.toCharSelect();
}
// Get this character's transition delay, with a reasonable default.
var transitionDelay:Float = currentCharacter.getFreeplayDJData()?.getCharSelectTransitionDelay() ?? 0.25;
new FlxTimer().start(transitionDelay, _ -> {
transitionToCharSelect();
});
}
function transitionToCharSelect():Void
{
var transitionGradient = new FlxSprite(0, 720).loadGraphic(Paths.image('freeplay/transitionGradient'));
transitionGradient.scale.set(1280, 1);
transitionGradient.updateHitbox();
transitionGradient.cameras = [rankCamera];
exitMoversCharSel.set([transitionGradient],
{
y: -720,
speed: 0.8,
wait: 0.1
});
add(transitionGradient);
for (index => capsule in grpCapsules.members)
{
var distFromSelected:Float = Math.abs(index - curSelected) - 1;
if (distFromSelected < 5)
{
capsule.doLerp = false;
exitMoversCharSel.set([capsule],
{
y: -250,
speed: 0.8,
wait: 0.1
});
}
}
fadeShader.fade(1.0, 0.0, 0.8, {ease: FlxEase.quadIn});
FlxG.sound.music.fadeOut(0.9, 0);
new FlxTimer().start(0.9, _ -> {
FlxG.switchState(new funkin.ui.charSelect.CharSelectSubState());
});
for (grpSpr in exitMoversCharSel.keys())
{
var moveData:Null<MoveData> = exitMoversCharSel.get(grpSpr);
if (moveData == null) continue;
for (spr in grpSpr)
{
if (spr == null) continue;
var funnyMoveShit:MoveData = moveData;
var moveDataY = funnyMoveShit.y ?? spr.y;
var moveDataSpeed = funnyMoveShit.speed ?? 0.2;
var moveDataWait = funnyMoveShit.wait ?? 0.0;
FlxTween.tween(spr, {y: moveDataY + spr.y}, moveDataSpeed, {ease: FlxEase.backIn});
}
}
backingCard?.enterCharSel();
}
function enterFromCharSel():Void
{
busy = true;
if (_parentState != null) _parentState.persistentDraw = false;
var transitionGradient = new FlxSprite(0, 720).loadGraphic(Paths.image('freeplay/transitionGradient'));
transitionGradient.scale.set(1280, 1);
transitionGradient.updateHitbox();
transitionGradient.cameras = [rankCamera];
exitMoversCharSel.set([transitionGradient],
{
y: -720,
speed: 1.5,
wait: 0.1
});
add(transitionGradient);
// FlxTween.tween(transitionGradient, {alpha: 0}, 1, {ease: FlxEase.circIn});
// for (index => capsule in grpCapsules.members)
// {
// var distFromSelected:Float = Math.abs(index - curSelected) - 1;
// if (distFromSelected < 5)
// {
// capsule.doLerp = false;
// exitMoversCharSel.set([capsule],
// {
// y: -250,
// speed: 0.8,
// wait: 0.1
// });
// }
// }
fadeShader.fade(0.0, 1.0, 0.8, {ease: FlxEase.quadIn});
for (grpSpr in exitMoversCharSel.keys())
{
var moveData:Null<MoveData> = exitMoversCharSel.get(grpSpr);
if (moveData == null) continue;
for (spr in grpSpr)
{
if (spr == null) continue;
var funnyMoveShit:MoveData = moveData;
var moveDataY = funnyMoveShit.y ?? spr.y;
var moveDataSpeed = funnyMoveShit.speed ?? 0.2;
var moveDataWait = funnyMoveShit.wait ?? 0.0;
spr.y += moveDataY;
FlxTween.tween(spr, {y: spr.y - moveDataY}, moveDataSpeed * 1.2,
{
ease: FlxEase.expoOut,
onComplete: function(_) {
for (index => capsule in grpCapsules.members)
{
capsule.doLerp = true;
fromCharSelect = false;
busy = false;
albumRoll.applyExitMovers(exitMovers, exitMoversCharSel);
}
}
});
}
}
}
var touchY:Float = 0;
var touchX:Float = 0;
var dxTouch:Float = 0;
@ -1247,32 +1405,7 @@ class FreeplayState extends MusicBeatSubState
if (controls.FREEPLAY_CHAR_SELECT && !busy)
{
// Check if we have ACCESS to character select!
trace('Is Pico unlocked? ${PlayerRegistry.instance.fetchEntry('pico')?.isUnlocked()}');
trace('Number of characters: ${PlayerRegistry.instance.countUnlockedCharacters()}');
if (PlayerRegistry.instance.countUnlockedCharacters() > 1)
{
if (dj != null)
{
busy = true;
// Transition to character select after animation
dj.onCharSelectComplete = function() {
FlxG.switchState(new funkin.ui.charSelect.CharSelectSubState());
}
dj.toCharSelect();
}
else
{
// Transition to character select immediately
FlxG.switchState(new funkin.ui.charSelect.CharSelectSubState());
}
}
else
{
trace('Not enough characters unlocked to open character select!');
FunkinSound.playOnce(Paths.sound('cancelMenu'));
}
tryOpenCharSelect();
}
if (controls.FREEPLAY_FAVORITE && !busy)

View file

@ -9,6 +9,7 @@ import funkin.play.scoring.Scoring.ScoringRank;
* An object used to retrieve data about a playable character (also known as "weeks").
* Can be scripted to override each function, for custom behavior.
*/
@:nullSafety
class PlayableCharacter implements IRegistryEntry<PlayerData>
{
/**
@ -19,7 +20,7 @@ class PlayableCharacter implements IRegistryEntry<PlayerData>
/**
* Playable character data as parsed from the JSON file.
*/
public final _data:PlayerData;
public final _data:Null<PlayerData>;
/**
* @param id The ID of the JSON file to parse.
@ -41,7 +42,7 @@ class PlayableCharacter implements IRegistryEntry<PlayerData>
public function getName():String
{
// TODO: Maybe add localization support?
return _data.name;
return _data?.name ?? "Unknown";
}
/**
@ -50,7 +51,7 @@ class PlayableCharacter implements IRegistryEntry<PlayerData>
*/
public function getOwnedCharacterIds():Array<String>
{
return _data.ownedChars;
return _data?.ownedChars ?? [];
}
/**
@ -59,17 +60,17 @@ class PlayableCharacter implements IRegistryEntry<PlayerData>
*/
public function shouldShowUnownedChars():Bool
{
return _data.showUnownedChars;
return _data?.showUnownedChars ?? false;
}
public function shouldShowCharacter(id:String):Bool
{
if (_data.ownedChars.contains(id))
if (getOwnedCharacterIds().contains(id))
{
return true;
}
if (_data.showUnownedChars)
if (shouldShowUnownedChars())
{
var result = !PlayerRegistry.instance.isCharacterOwned(id);
return result;
@ -78,19 +79,25 @@ class PlayableCharacter implements IRegistryEntry<PlayerData>
return false;
}
public function getFreeplayDJData():PlayerFreeplayDJData
public function getFreeplayStyleID():String
{
return _data.freeplayDJ;
return _data?.freeplayStyle ?? Constants.DEFAULT_FREEPLAY_STYLE;
}
public function getFreeplayDJData():Null<PlayerFreeplayDJData>
{
return _data?.freeplayDJ;
}
public function getFreeplayDJText(index:Int):String
{
return _data.freeplayDJ.getFreeplayDJText(index);
// Silly little placeholder
return _data?.freeplayDJ?.getFreeplayDJText(index) ?? 'GET FREAKY ON A FRIDAY';
}
public function getCharSelectData():PlayerCharSelectData
public function getCharSelectData():Null<PlayerCharSelectData>
{
return _data.charSelect;
return _data?.charSelect;
}
/**
@ -99,7 +106,7 @@ class PlayableCharacter implements IRegistryEntry<PlayerData>
*/
public function getResultsAnimationDatas(rank:ScoringRank):Array<PlayerResultsAnimationData>
{
if (_data.results == null)
if (_data == null || _data.results == null)
{
return [];
}
@ -124,7 +131,7 @@ class PlayableCharacter implements IRegistryEntry<PlayerData>
*/
public function isUnlocked():Bool
{
return _data.unlocked;
return _data?.unlocked ?? true;
}
/**

View file

@ -110,7 +110,17 @@ class MainMenuState extends MusicBeatState
FlxTransitionableState.skipNextTransIn = true;
FlxTransitionableState.skipNextTransOut = true;
openSubState(new FreeplayState());
#if FEATURE_DEBUG_FUNCTIONS
// Debug function: Hold SHIFT when selecting Freeplay to swap character without the char select menu
var targetCharacter:Null<String> = (FlxG.keys.pressed.SHIFT) ? (FreeplayState.rememberedCharacterId == "pico" ? "bf" : "pico") : null;
#else
var targetCharacter:Null<String> = null;
#end
openSubState(new FreeplayState(
{
character: targetCharacter
}));
});
#if CAN_OPEN_LINKS

View file

@ -258,6 +258,11 @@ class Constants
*/
public static final DEFAULT_NOTE_STYLE:String = 'funkin';
/**
* The default freeplay style for characters.
*/
public static final DEFAULT_FREEPLAY_STYLE:String = 'bf';
/**
* The default pixel note style for songs.
*/

View file

@ -33,4 +33,19 @@ class ReflectUtil
{
return Type.getClassName(Type.getClass(obj));
}
public static function getAnonymousFieldsOf(obj:Dynamic):Array<String>
{
return Reflect.fields(obj);
}
public static function getAnonymousField(obj:Dynamic, name:String):Dynamic
{
return Reflect.field(obj, name);
}
public static function hasAnonymousField(obj:Dynamic, name:String):Bool
{
return Reflect.hasField(obj, name);
}
}