Merge branch 'rewrite/master' of https://github.com/FunkinCrew/Funkin-secret into bugfix/quick-camera-fixes

This commit is contained in:
Jenny Crowe 2024-03-28 11:05:51 -07:00
commit d4c9fffe48
19 changed files with 1307 additions and 160 deletions

View file

@ -4,12 +4,19 @@
<app title="Friday Night Funkin'" file="Funkin" packageName="com.funkin.fnf" package="com.funkin.fnf" main="Main" version="0.3.0" company="ninjamuffin99" />
<!--Switch Export with Unique ApplicationID and Icon-->
<set name="APP_ID" value="0x0100f6c013bbc000" />
<app preloader="funkin.Preloader" />
<!--
Define the OpenFL sprite which displays the preloader.
You can't replace the preloader's logic here, sadly, but you can extend it.
Basic preloading logic is done by `openfl.display.Preloader`.
-->
<app preloader="funkin.ui.transition.preload.FunkinPreloader" />
<!--Minimum without FLX_NO_GAMEPAD: 11.8, without FLX_NO_NATIVE_CURSOR: 11.2-->
<set name="SWF_VERSION" value="11.8" />
<!-- ____________________________ Window Settings ___________________________ -->
<!--These window settings apply to all targets-->
<window width="1280" height="720" fps="" background="#000000" hardware="true" vsync="false" />
<window width="1280" height="720" fps="60" background="#000000" hardware="true" vsync="false" />
<!--HTML5-specific-->
<window if="html5" resizable="true" />
<!--Desktop-specific-->
@ -95,6 +102,7 @@
<!-- If compiled via github actions, show debug version number. -->
<define name="FORCE_DEBUG_VERSION" if="GITHUB_BUILD" />
<define name="NO_REDIRECT_ASSETS_FOLDER" if="GITHUB_BUILD" />
<define name="TOUCH_HERE_TO_PLAY" if="web" />
<!-- _______________________________ Libraries ______________________________ -->
<haxelib name="lime" /> <!-- Game engine backend -->

2
art

@ -1 +1 @@
Subproject commit 03e7c2a2353b184e45955c96d763b7cdf1acbc34
Subproject commit 00463685fa570f0c853d08e250b46ef80f30bc48

2
assets

@ -1 +1 @@
Subproject commit 5f1726f1b0c11fc747b7473708cf4e5f28be05f1
Subproject commit 485243fdd44acbc4db6a97ec7bf10a8b18350be9

View file

@ -1,65 +0,0 @@
package funkin;
import flash.Lib;
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.display.Sprite;
import flixel.system.FlxBasePreloader;
import openfl.display.Sprite;
import funkin.util.CLIUtil;
import openfl.text.TextField;
import openfl.text.TextFormat;
import flixel.system.FlxAssets;
@:bitmap('art/preloaderArt.png')
class LogoImage extends BitmapData {}
class Preloader extends FlxBasePreloader
{
public function new(MinDisplayTime:Float = 0, ?AllowedURLs:Array<String>)
{
super(MinDisplayTime, AllowedURLs);
CLIUtil.resetWorkingDir(); // Bug fix for drag-and-drop.
}
var logo:Sprite;
var _text:TextField;
override function create():Void
{
this._width = Lib.current.stage.stageWidth;
this._height = Lib.current.stage.stageHeight;
_text = new TextField();
_text.width = 500;
_text.text = "Loading FNF";
_text.defaultTextFormat = new TextFormat(FlxAssets.FONT_DEFAULT, 16, 0xFFFFFFFF);
_text.embedFonts = true;
_text.selectable = false;
_text.multiline = false;
_text.wordWrap = false;
_text.autoSize = LEFT;
_text.x = 2;
_text.y = 2;
addChild(_text);
var ratio:Float = this._width / 2560; // This allows us to scale assets depending on the size of the screen.
logo = new Sprite();
logo.addChild(new Bitmap(new LogoImage(0, 0))); // Sets the graphic of the sprite to a Bitmap object, which uses our embedded BitmapData class.
logo.scaleX = logo.scaleY = ratio;
logo.x = ((this._width) / 2) - ((logo.width) / 2);
logo.y = (this._height / 2) - ((logo.height) / 2);
// addChild(logo); // Adds the graphic to the NMEPreloader's buffer.
super.create();
}
override function update(Percent:Float):Void
{
_text.text = "FNF: " + Math.round(Percent * 100) + "%";
super.update(Percent);
}
}

View file

@ -1,17 +1,18 @@
package funkin.audio;
import flixel.sound.FlxSound;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.util.FlxSignal.FlxTypedSignal;
import flixel.math.FlxMath;
import flixel.sound.FlxSound;
import flixel.system.FlxAssets.FlxSoundAsset;
import funkin.util.tools.ICloneable;
import flixel.tweens.FlxTween;
import flixel.util.FlxSignal.FlxTypedSignal;
import funkin.audio.waveform.WaveformData;
import funkin.audio.waveform.WaveformDataParser;
import funkin.data.song.SongData.SongMusicData;
import funkin.data.song.SongRegistry;
import funkin.audio.waveform.WaveformData;
import openfl.media.SoundMixer;
import funkin.audio.waveform.WaveformDataParser;
import flixel.math.FlxMath;
import funkin.util.tools.ICloneable;
import openfl.Assets;
import openfl.media.SoundMixer;
#if (openfl >= "8.0.0")
import openfl.utils.AssetType;
#end
@ -50,6 +51,11 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
*/
static var pool(default, null):FlxTypedGroup<FunkinSound> = new FlxTypedGroup<FunkinSound>();
/**
* Calculate the current time of the sound.
* NOTE: You need to `add()` the sound to the scene for `update()` to increment the time.
*/
//
public var muted(default, set):Bool = false;
function set_muted(value:Bool):Bool
@ -325,6 +331,7 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
if (FlxG.sound.music != null)
{
FlxG.sound.music.fadeTween?.cancel();
FlxG.sound.music.stop();
FlxG.sound.music.kill();
}
@ -392,8 +399,6 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
// Call onLoad() because the sound already loaded
if (onLoad != null && sound._sound != null) onLoad();
FlxG.sound.list.remove(FlxG.sound.music);
return sound;
}
@ -401,6 +406,8 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
{
// trace('[FunkinSound] Destroying sound "${this._label}"');
super.destroy();
FlxTween.cancelTweensOf(this);
this._label = 'unknown';
}
/**

View file

@ -151,11 +151,14 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
* Stop all the sounds in the group.
*/
public function stop()
{
if (members != null)
{
forEachAlive(function(sound:FunkinSound) {
sound.stop();
});
}
}
public override function destroy()
{

View file

@ -160,7 +160,9 @@ class VoicesGroup extends SoundGroup
public override function destroy():Void
{
playerVoices.destroy();
playerVoices = null;
opponentVoices.destroy();
opponentVoices = null;
super.destroy();
}
}

View file

@ -431,7 +431,11 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
{
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var entryFilePath:String = Paths.json('$dataFilePath/$id/$id-metadata${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}');
if (!openfl.Assets.exists(entryFilePath)) return null;
if (!openfl.Assets.exists(entryFilePath))
{
trace(' [WARN] Could not locate file $entryFilePath');
return null;
}
var rawJson:Null<String> = openfl.Assets.getText(entryFilePath);
if (rawJson == null) return null;
rawJson = rawJson.trim();

View file

@ -79,6 +79,23 @@ 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;
}
/**
* Plays an animation.
* @param id A string ID of the animation to play.

View file

@ -1933,14 +1933,14 @@ class PlayState extends MusicBeatSubState
FlxG.sound.music.play(true, startTimestamp - Conductor.instance.instrumentalOffset);
FlxG.sound.music.pitch = playbackRate;
// I am going insane.
// Prevent the volume from being wrong.
FlxG.sound.music.volume = 1.0;
FlxG.sound.music.fadeTween?.cancel();
trace('Playing vocals...');
add(vocals);
vocals.play();
vocals.volume = 1.0;
vocals.pitch = playbackRate;
resyncVocals();
@ -2935,6 +2935,9 @@ class PlayState extends MusicBeatSubState
// If the camera is being tweened, stop it.
cancelAllCameraTweens();
// Dispatch the destroy event.
dispatchEvent(new ScriptEvent(DESTROY, false));
if (currentConversation != null)
{
remove(currentConversation);
@ -2949,10 +2952,7 @@ class PlayState extends MusicBeatSubState
if (overrideMusic)
{
// Stop the music. Do NOT destroy it, something still references it!
if (FlxG.sound.music != null)
{
FlxG.sound.music.pause();
}
if (FlxG.sound.music != null) FlxG.sound.music.pause();
if (vocals != null)
{
vocals.pause();
@ -2962,10 +2962,7 @@ class PlayState extends MusicBeatSubState
else
{
// Stop and destroy the music.
if (FlxG.sound.music != null)
{
FlxG.sound.music.pause();
}
if (FlxG.sound.music != null) FlxG.sound.music.pause();
if (vocals != null)
{
vocals.destroy();
@ -2978,7 +2975,6 @@ class PlayState extends MusicBeatSubState
{
remove(currentStage);
currentStage.kill();
dispatchEvent(new ScriptEvent(DESTROY, false));
currentStage = null;
}

View file

@ -43,7 +43,7 @@ class Scoring
case WEEK7: scoreNoteWEEK7(msTiming);
case PBOT1: scoreNotePBOT1(msTiming);
default:
trace('ERROR: Unknown scoring system: ' + scoringSystem);
FlxG.log.error('Unknown scoring system: ${scoringSystem}');
0;
}
}
@ -62,7 +62,7 @@ class Scoring
case WEEK7: judgeNoteWEEK7(msTiming);
case PBOT1: judgeNotePBOT1(msTiming);
default:
trace('ERROR: Unknown scoring system: ' + scoringSystem);
FlxG.log.error('Unknown scoring system: ${scoringSystem}');
'miss';
}
}
@ -145,7 +145,9 @@ class Scoring
case(_ < PBOT1_PERFECT_THRESHOLD) => true:
PBOT1_MAX_SCORE;
default:
// Fancy equation.
var factor:Float = 1.0 - (1.0 / (1.0 + Math.exp(-PBOT1_SCORING_SLOPE * (absTiming - PBOT1_SCORING_OFFSET))));
var score:Int = Std.int(PBOT1_MAX_SCORE * factor + PBOT1_MIN_SCORE);
score;
@ -169,6 +171,7 @@ class Scoring
case(_ < PBOT1_SHIT_THRESHOLD) => true:
'shit';
default:
FlxG.log.warn('Missed note: Bad timing ($absTiming < $PBOT1_SHIT_THRESHOLD)');
'miss';
}
}
@ -257,6 +260,7 @@ class Scoring
case(_ < LEGACY_HIT_WINDOW * LEGACY_SHIT_THRESHOLD) => true:
'shit';
default:
FlxG.log.warn('Missed note: Bad timing ($absTiming < $LEGACY_SHIT_THRESHOLD)');
'miss';
}
}
@ -336,6 +340,7 @@ class Scoring
}
else
{
FlxG.log.warn('Missed note: Bad timing ($absTiming < $WEEK7_HIT_WINDOW)');
return 'miss';
}
}

View file

@ -4,6 +4,7 @@ import flixel.FlxSprite;
import flixel.util.FlxSignal;
import funkin.util.assets.FlxAnimationUtil;
import funkin.graphics.adobeanimate.FlxAtlasSprite;
import funkin.audio.FunkinSound;
import flixel.util.FlxTimer;
import funkin.audio.FunkinSound;
import funkin.audio.FlxStreamSound;
@ -26,8 +27,8 @@ class DJBoyfriend extends FlxAtlasSprite
var gotSpooked:Bool = false;
static final SPOOK_PERIOD:Float = 120.0;
static final TV_PERIOD:Float = 180.0;
static final SPOOK_PERIOD:Float = 10.0;
static final TV_PERIOD:Float = 10.0;
// Time since dad last SPOOKED you.
var timeSinceSpook:Float = 0;
@ -48,7 +49,6 @@ class DJBoyfriend extends FlxAtlasSprite
};
setupAnimations();
trace(listAnimations());
FlxG.debugger.track(this);
FlxG.console.registerObject("dj", this);
@ -87,20 +87,21 @@ class DJBoyfriend extends FlxAtlasSprite
timeSinceSpook = 0;
case Idle:
// We are in this state the majority of the time.
if (getCurrentAnimation() != 'Boyfriend DJ' || anim.finished)
if (getCurrentAnimation() != 'Boyfriend DJ')
{
if (timeSinceSpook > SPOOK_PERIOD && !gotSpooked)
playFlashAnimation('Boyfriend DJ', true);
}
if (getCurrentAnimation() == 'Boyfriend DJ' && this.isLoopFinished())
{
if (timeSinceSpook >= SPOOK_PERIOD && !gotSpooked)
{
currentState = Spook;
}
else if (timeSinceSpook > TV_PERIOD)
else if (timeSinceSpook >= TV_PERIOD)
{
currentState = TV;
}
else
{
playFlashAnimation('Boyfriend DJ', false);
}
}
timeSinceSpook += elapsed;
case Confirm:
@ -111,6 +112,7 @@ class DJBoyfriend extends FlxAtlasSprite
{
onSpook.dispatch();
playFlashAnimation('bf dj afk', false);
gotSpooked = true;
}
timeSinceSpook = 0;
case TV:
@ -119,6 +121,34 @@ class DJBoyfriend extends FlxAtlasSprite
default:
// I shit myself.
}
if (FlxG.keys.pressed.CONTROL)
{
if (FlxG.keys.justPressed.LEFT)
{
this.offsetX -= FlxG.keys.pressed.ALT ? 0.1 : (FlxG.keys.pressed.SHIFT ? 10.0 : 1.0);
}
if (FlxG.keys.justPressed.RIGHT)
{
this.offsetX += FlxG.keys.pressed.ALT ? 0.1 : (FlxG.keys.pressed.SHIFT ? 10.0 : 1.0);
}
if (FlxG.keys.justPressed.UP)
{
this.offsetY -= FlxG.keys.pressed.ALT ? 0.1 : (FlxG.keys.pressed.SHIFT ? 10.0 : 1.0);
}
if (FlxG.keys.justPressed.DOWN)
{
this.offsetY += FlxG.keys.pressed.ALT ? 0.1 : (FlxG.keys.pressed.SHIFT ? 10.0 : 1.0);
}
if (FlxG.keys.justPressed.SPACE)
{
currentState = (currentState == Idle ? TV : Idle);
}
}
}
function onFinishAnim():Void
@ -139,11 +169,15 @@ class DJBoyfriend extends FlxAtlasSprite
case "Boyfriend DJ watchin tv OG":
var frame:Int = FlxG.random.bool(33) ? 112 : 166;
if (FlxG.random.bool(10))
// BF switches channels when the video ends, or at a 10% chance each time his idle loops.
if (FlxG.random.bool(5))
{
frame = 60;
// boyfriend switches channel code?
// runTvLogic();
}
trace('Replay idle: ${frame}');
anim.play("Boyfriend DJ watchin tv OG", true, false, frame);
// trace('Finished confirm');
}
@ -152,24 +186,31 @@ class DJBoyfriend extends FlxAtlasSprite
public function resetAFKTimer():Void
{
timeSinceSpook = 0;
gotSpooked = false;
}
var offsetX:Float = 0.0;
var offsetY:Float = 0.0;
function setupAnimations():Void
{
// animation.addByPrefix('intro', "boyfriend dj intro", 24, false);
addOffset('boyfriend dj intro', 8, 3);
// Intro
addOffset('boyfriend dj intro', 8.0 - 1.3, 3.0 - 0.4);
// animation.addByPrefix('idle', "Boyfriend DJ0", 24, false);
// Idle
addOffset('Boyfriend DJ', 0, 0);
// animation.addByPrefix('confirm', "Boyfriend DJ confirm", 24, false);
// Confirm
addOffset('Boyfriend DJ confirm', 0, 0);
// animation.addByPrefix('spook', "bf dj afk0", 24, false);
addOffset('bf dj afk', 0, 0);
// AFK: Spook
addOffset('bf dj afk', 649.5, 58.5);
// AFK: TV
addOffset('Boyfriend DJ watchin tv OG', 0, 0);
}
var cartoonSnd:FlxStreamSound;
var cartoonSnd:Null<FunkinSound> = null;
public var playingCartoon:Bool = false;
@ -178,39 +219,47 @@ class DJBoyfriend extends FlxAtlasSprite
if (cartoonSnd == null)
{
// tv is OFF, but getting turned on
FunkinSound.playOnce(Paths.sound('tv_on'));
cartoonSnd = new FlxStreamSound();
FlxG.sound.defaultSoundGroup.add(cartoonSnd);
// Eric got FUCKING TROLLED there is no `tv_on` or `channel_switch` sound!
// FunkinSound.playOnce(Paths.sound('tv_on'), 1.0, function() {
// });
loadCartoon();
}
else
{
// plays it smidge after the click
new FlxTimer().start(0.1, function(_) {
FunkinSound.playOnce(Paths.sound('channel_switch'));
});
}
// cartoonSnd.loadEmbedded(Paths.sound("cartoons/peck"));
// cartoonSnd.play();
// new FlxTimer().start(0.1, function(_) {
// // FunkinSound.playOnce(Paths.sound('channel_switch'));
// });
cartoonSnd.destroy();
loadCartoon();
}
// loadCartoon();
}
function loadCartoon()
{
cartoonSnd.loadEmbedded(Paths.sound(getRandomFlashToon()), false, false, function() {
cartoonSnd = FunkinSound.load(Paths.sound(getRandomFlashToon()), 1.0, false, true, true, function() {
anim.play("Boyfriend DJ watchin tv OG", true, false, 60);
});
cartoonSnd.play(true, FlxG.random.float(0, cartoonSnd.length));
// Fade out music to 40% volume over 1 second.
// This helps make the TV a bit more audible.
FlxG.sound.music.fadeOut(1.0, 0.4);
// Play the cartoon at a random time between the start and 5 seconds from the end.
cartoonSnd.time = FlxG.random.float(0, Math.max(cartoonSnd.length - (5 * Constants.MS_PER_SEC), 0.0));
}
var cartoonList:Array<String> = openfl.utils.Assets.list().filter(function(path) return path.startsWith("assets/sounds/cartoons/"));
final cartoonList:Array<String> = openfl.utils.Assets.list().filter(function(path) return path.startsWith("assets/sounds/cartoons/"));
function getRandomFlashToon():String
{
var randomFile = FlxG.random.getObject(cartoonList);
// Strip folder prefix
randomFile = randomFile.replace("assets/sounds/", "");
// Strip file extension
randomFile = randomFile.substring(0, randomFile.length - 4);
return randomFile;
@ -244,11 +293,32 @@ class DJBoyfriend extends FlxAtlasSprite
var daOffset = animOffsets.get(AnimName);
if (animOffsets.exists(AnimName))
{
offset.set(daOffset[0], daOffset[1]);
var xValue = daOffset[0];
var yValue = daOffset[1];
if (AnimName == "Boyfriend DJ watchin tv OG")
{
xValue += offsetX;
yValue += offsetY;
}
offset.set(xValue, yValue);
}
else
{
offset.set(0, 0);
}
}
public override function destroy():Void
{
super.destroy();
if (cartoonSnd != null)
{
cartoonSnd.destroy();
cartoonSnd = null;
}
}
}
enum DJBoyfriendState

View file

@ -688,14 +688,6 @@ class FreeplayState extends MusicBeatSubState
if (FlxG.keys.justPressed.T) typing.hasFocus = true;
if (FlxG.sound.music != null)
{
if (FlxG.sound.music.volume < 0.7)
{
FlxG.sound.music.volume += 0.5 * elapsed;
}
}
lerpScore = MathUtil.coolLerp(lerpScore, intendedScore, 0.2);
lerpCompletion = MathUtil.coolLerp(lerpCompletion, intendedCompletion, 0.9);
@ -733,9 +725,9 @@ class FreeplayState extends MusicBeatSubState
{
if (busy) return;
var upP:Bool = controls.UI_UP_P;
var downP:Bool = controls.UI_DOWN_P;
var accepted:Bool = controls.ACCEPT;
var upP:Bool = controls.UI_UP_P && !FlxG.keys.pressed.CONTROL;
var downP:Bool = controls.UI_DOWN_P && !FlxG.keys.pressed.CONTROL;
var accepted:Bool = controls.ACCEPT && !FlxG.keys.pressed.CONTROL;
if (FlxG.onMobile)
{
@ -809,10 +801,8 @@ class FreeplayState extends MusicBeatSubState
}
#end
if (controls.UI_UP || controls.UI_DOWN)
if (!FlxG.keys.pressed.CONTROL && (controls.UI_UP || controls.UI_DOWN))
{
spamTimer += elapsed;
if (spamming)
{
if (spamTimer >= 0.07)
@ -829,7 +819,24 @@ class FreeplayState extends MusicBeatSubState
}
}
}
else if (spamTimer >= 0.9) spamming = true;
else if (spamTimer >= 0.9)
{
spamming = true;
}
else if (spamTimer <= 0)
{
if (controls.UI_UP)
{
changeSelection(-1);
}
else
{
changeSelection(1);
}
}
spamTimer += elapsed;
dj.resetAFKTimer();
}
else
{
@ -837,29 +844,18 @@ class FreeplayState extends MusicBeatSubState
spamTimer = 0;
}
if (upP)
{
dj.resetAFKTimer();
changeSelection(-1);
}
if (downP)
{
dj.resetAFKTimer();
changeSelection(1);
}
if (FlxG.mouse.wheel != 0)
{
dj.resetAFKTimer();
changeSelection(-Math.round(FlxG.mouse.wheel / 4));
}
if (controls.UI_LEFT_P)
if (controls.UI_LEFT_P && !FlxG.keys.pressed.CONTROL)
{
dj.resetAFKTimer();
changeDiff(-1);
}
if (controls.UI_RIGHT_P)
if (controls.UI_RIGHT_P && !FlxG.keys.pressed.CONTROL)
{
dj.resetAFKTimer();
changeDiff(1);
@ -1234,8 +1230,8 @@ class DifficultySelector extends FlxSprite
override function update(elapsed:Float):Void
{
if (flipX && controls.UI_RIGHT_P) moveShitDown();
if (!flipX && controls.UI_LEFT_P) moveShitDown();
if (flipX && controls.UI_RIGHT_P && !FlxG.keys.pressed.CONTROL) moveShitDown();
if (!flipX && controls.UI_LEFT_P && !FlxG.keys.pressed.CONTROL) moveShitDown();
super.update(elapsed);
}

View file

@ -182,8 +182,6 @@ class SongMenuItem extends FlxSpriteGroup
{
var charPath:String = "freeplay/icons/";
trace(char);
// TODO: Put this in the character metadata where it belongs.
// TODO: Also, can use CharacterDataParser.getCharPixelIconAsset()
switch (char)

View file

@ -438,6 +438,8 @@ class StoryMenuState extends MusicBeatState
}
}
FunkinSound.playOnce(Paths.sound('scrollMenu'), 0.4);
updateText();
updateBackground(previousLevelId);
updateProps();
@ -481,6 +483,7 @@ class StoryMenuState extends MusicBeatState
if (hasChanged)
{
buildDifficultySprite();
FunkinSound.playOnce(Paths.sound('scrollMenu'), 0.4);
// Disable the funny music thing for now.
// funnyMusicThing();
}

View file

@ -0,0 +1,994 @@
package funkin.ui.transition.preload;
import openfl.events.MouseEvent;
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.display.BlendMode;
import flash.display.Sprite;
import flash.Lib;
import flixel.system.FlxBasePreloader;
import funkin.modding.PolymodHandler;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.util.MathUtil;
import lime.app.Future;
import lime.math.Rectangle;
import openfl.display.Sprite;
import openfl.text.TextField;
import openfl.text.TextFormat;
import openfl.text.TextFormatAlign;
using StringTools;
// Annotation embeds the asset in the executable for faster loading.
// Polymod can't override this, so we can't use this technique elsewhere.
@:bitmap("art/preloaderArt.png")
class LogoImage extends BitmapData {}
#if TOUCH_HERE_TO_PLAY
@:bitmap('art/touchHereToPlay.png')
class TouchHereToPlayImage extends BitmapData {}
#end
/**
* This preloader displays a logo while the game downloads assets.
*/
class FunkinPreloader extends FlxBasePreloader
{
/**
* The logo image width at the base resolution.
* Scaled up/down appropriately as needed.
*/
static final BASE_WIDTH:Float = 1280;
/**
* Margin at the sides and bottom, around the loading bar.
*/
static final BAR_PADDING:Float = 20;
static final BAR_HEIGHT:Int = 20;
/**
* Logo takes this long (in seconds) to fade in.
*/
static final LOGO_FADE_TIME:Float = 2.5;
// Ratio between window size and BASE_WIDTH
var ratio:Float = 0;
var currentState:FunkinPreloaderState = FunkinPreloaderState.NotStarted;
// private var downloadingAssetsStartTime:Float = -1;
private var downloadingAssetsPercent:Float = -1;
private var downloadingAssetsComplete:Bool = false;
private var preloadingPlayAssetsPercent:Float = -1;
private var preloadingPlayAssetsStartTime:Float = -1;
private var preloadingPlayAssetsComplete:Bool = false;
private var cachingGraphicsPercent:Float = -1;
private var cachingGraphicsStartTime:Float = -1;
private var cachingGraphicsComplete:Bool = false;
private var cachingAudioPercent:Float = -1;
private var cachingAudioStartTime:Float = -1;
private var cachingAudioComplete:Bool = false;
private var cachingDataPercent:Float = -1;
private var cachingDataStartTime:Float = -1;
private var cachingDataComplete:Bool = false;
private var parsingSpritesheetsPercent:Float = -1;
private var parsingSpritesheetsStartTime:Float = -1;
private var parsingSpritesheetsComplete:Bool = false;
private var parsingStagesPercent:Float = -1;
private var parsingStagesStartTime:Float = -1;
private var parsingStagesComplete:Bool = false;
private var parsingCharactersPercent:Float = -1;
private var parsingCharactersStartTime:Float = -1;
private var parsingCharactersComplete:Bool = false;
private var parsingSongsPercent:Float = -1;
private var parsingSongsStartTime:Float = -1;
private var parsingSongsComplete:Bool = false;
private var initializingScriptsPercent:Float = -1;
private var cachingCoreAssetsPercent:Float = -1;
/**
* The timestamp when the other steps completed and the `Finishing up` step started.
*/
private var completeTime:Float = -1;
// Graphics
var logo:Bitmap;
#if TOUCH_HERE_TO_PLAY
var touchHereToPlay:Bitmap;
#end
var progressBar:Bitmap;
var progressLeftText:TextField;
var progressRightText:TextField;
public function new()
{
super(Constants.PRELOADER_MIN_STAGE_TIME, Constants.SITE_LOCK);
// We can't even call trace() yet, until Flixel loads.
trace('Initializing custom preloader...');
this.siteLockTitleText = Constants.SITE_LOCK_TITLE;
this.siteLockBodyText = Constants.SITE_LOCK_DESC;
}
override function create():Void
{
// Nothing happens in the base preloader.
super.create();
// Background color.
Lib.current.stage.color = Constants.COLOR_PRELOADER_BG;
// Width and height of the preloader.
this._width = Lib.current.stage.stageWidth;
this._height = Lib.current.stage.stageHeight;
// Scale assets to the screen size.
ratio = this._width / BASE_WIDTH / 2.0;
// Create the logo.
logo = createBitmap(LogoImage, function(bmp:Bitmap) {
// Scale and center the logo.
// We have to do this inside the async call, after the image size is known.
bmp.scaleX = bmp.scaleY = ratio;
bmp.x = (this._width - bmp.width) / 2;
bmp.y = (this._height - bmp.height) / 2;
});
addChild(logo);
#if TOUCH_HERE_TO_PLAY
touchHereToPlay = createBitmap(TouchHereToPlayImage, function(bmp:Bitmap) {
// Scale and center the touch to start image.
// We have to do this inside the async call, after the image size is known.
bmp.scaleX = bmp.scaleY = ratio;
bmp.x = (this._width - bmp.width) / 2;
bmp.y = (this._height - bmp.height) / 2;
});
touchHereToPlay.alpha = 0.0;
addChild(touchHereToPlay);
#end
// Create the progress bar.
progressBar = new Bitmap(new BitmapData(1, BAR_HEIGHT, true, Constants.COLOR_PRELOADER_BAR));
progressBar.x = BAR_PADDING;
progressBar.y = this._height - BAR_PADDING - BAR_HEIGHT;
addChild(progressBar);
// Create the progress message.
progressLeftText = new TextField();
var progressLeftTextFormat = new TextFormat("VCR OSD Mono", 16, Constants.COLOR_PRELOADER_BAR, true);
progressLeftTextFormat.align = TextFormatAlign.LEFT;
progressLeftText.defaultTextFormat = progressLeftTextFormat;
progressLeftText.selectable = false;
progressLeftText.width = this._width - BAR_PADDING * 2;
progressLeftText.text = 'Downloading assets...';
progressLeftText.x = BAR_PADDING;
progressLeftText.y = this._height - BAR_PADDING - BAR_HEIGHT - 16 - 4;
addChild(progressLeftText);
// Create the progress %.
progressRightText = new TextField();
var progressRightTextFormat = new TextFormat("VCR OSD Mono", 16, Constants.COLOR_PRELOADER_BAR, true);
progressRightTextFormat.align = TextFormatAlign.RIGHT;
progressRightText.defaultTextFormat = progressRightTextFormat;
progressRightText.selectable = false;
progressRightText.width = this._width - BAR_PADDING * 2;
progressRightText.text = '0%';
progressRightText.x = BAR_PADDING;
progressRightText.y = this._height - BAR_PADDING - BAR_HEIGHT - 16 - 4;
addChild(progressRightText);
}
var lastElapsed:Float = 0.0;
override function update(percent:Float):Void
{
var elapsed:Float = (Date.now().getTime() - this._startTime) / 1000.0;
// trace('Time since last frame: ' + (lastElapsed - elapsed));
downloadingAssetsPercent = percent;
var loadPercent:Float = updateState(percent, elapsed);
updateGraphics(loadPercent, elapsed);
lastElapsed = elapsed;
}
function updateState(percent:Float, elapsed:Float):Float
{
switch (currentState)
{
case FunkinPreloaderState.NotStarted:
if (downloadingAssetsPercent > 0.0) currentState = FunkinPreloaderState.DownloadingAssets;
return percent;
case FunkinPreloaderState.DownloadingAssets:
// Sometimes percent doesn't go to 100%, it's a floating point error.
if (downloadingAssetsPercent >= 1.0
|| (elapsed > Constants.PRELOADER_MIN_STAGE_TIME
&& downloadingAssetsComplete)) currentState = FunkinPreloaderState.PreloadingPlayAssets;
return percent;
case FunkinPreloaderState.PreloadingPlayAssets:
if (preloadingPlayAssetsPercent < 0.0)
{
preloadingPlayAssetsStartTime = elapsed;
preloadingPlayAssetsPercent = 0.0;
// This is quick enough to do synchronously.
// Assets.initialize();
/*
// Make a future to retrieve the manifest
var future:Future<lime.utils.AssetLibrary> = Assets.preloadLibrary('gameplay');
future.onProgress((loaded:Int, total:Int) -> {
preloadingPlayAssetsPercent = loaded / total;
});
future.onComplete((library:lime.utils.AssetLibrary) -> {
});
*/
// TODO: Reimplement this.
preloadingPlayAssetsPercent = 1.0;
preloadingPlayAssetsComplete = true;
return 0.0;
}
else if (Constants.PRELOADER_MIN_STAGE_TIME > 0)
{
var elapsedPreloadingPlayAssets:Float = elapsed - preloadingPlayAssetsStartTime;
if (preloadingPlayAssetsComplete && elapsedPreloadingPlayAssets >= Constants.PRELOADER_MIN_STAGE_TIME)
{
currentState = FunkinPreloaderState.InitializingScripts;
return 0.0;
}
else
{
// We need to return SIMULATED progress here.
if (preloadingPlayAssetsPercent < (elapsedPreloadingPlayAssets / Constants.PRELOADER_MIN_STAGE_TIME)) return preloadingPlayAssetsPercent;
else
return elapsedPreloadingPlayAssets / Constants.PRELOADER_MIN_STAGE_TIME;
}
}
else
{
if (preloadingPlayAssetsComplete) currentState = FunkinPreloaderState.InitializingScripts;
}
return preloadingPlayAssetsPercent;
case FunkinPreloaderState.InitializingScripts:
if (initializingScriptsPercent < 0.0)
{
initializingScriptsPercent = 0.0;
/*
var future:Future<Array<String>> = []; // PolymodHandler.loadNoModsAsync();
future.onProgress((loaded:Int, total:Int) -> {
trace('PolymodHandler.loadNoModsAsync() progress: ' + loaded + '/' + total);
initializingScriptsPercent = loaded / total;
});
future.onComplete((result:Array<String>) -> {
trace('Completed initializing scripts: ' + result);
});
*/
initializingScriptsPercent = 1.0;
currentState = FunkinPreloaderState.CachingGraphics;
return 0.0;
}
return initializingScriptsPercent;
case CachingGraphics:
if (cachingGraphicsPercent < 0)
{
cachingGraphicsPercent = 0.0;
cachingGraphicsStartTime = elapsed;
/*
var assetsToCache:Array<String> = []; // Assets.listGraphics('core');
var future:Future<Array<String>> = []; // Assets.cacheAssets(assetsToCache);
future.onProgress((loaded:Int, total:Int) -> {
cachingGraphicsPercent = loaded / total;
});
future.onComplete((_result) -> {
trace('Completed caching graphics.');
});
*/
// TODO: Reimplement this.
cachingGraphicsPercent = 1.0;
cachingGraphicsComplete = true;
return 0.0;
}
else if (Constants.PRELOADER_MIN_STAGE_TIME > 0)
{
var elapsedCachingGraphics:Float = elapsed - cachingGraphicsStartTime;
if (cachingGraphicsComplete && elapsedCachingGraphics >= Constants.PRELOADER_MIN_STAGE_TIME)
{
currentState = FunkinPreloaderState.CachingAudio;
return 0.0;
}
else
{
if (cachingGraphicsPercent < (elapsedCachingGraphics / Constants.PRELOADER_MIN_STAGE_TIME))
{
// Return real progress if it's lower.
return cachingGraphicsPercent;
}
else
{
// Return simulated progress if it's higher.
return elapsedCachingGraphics / Constants.PRELOADER_MIN_STAGE_TIME;
}
}
}
else
{
if (cachingGraphicsComplete)
{
currentState = FunkinPreloaderState.CachingAudio;
return 0.0;
}
else
{
return cachingGraphicsPercent;
}
}
case CachingAudio:
if (cachingAudioPercent < 0)
{
cachingAudioPercent = 0.0;
cachingAudioStartTime = elapsed;
var assetsToCache:Array<String> = []; // Assets.listSound('core');
/*
var future:Future<Array<String>> = []; // Assets.cacheAssets(assetsToCache);
future.onProgress((loaded:Int, total:Int) -> {
cachingAudioPercent = loaded / total;
});
future.onComplete((_result) -> {
trace('Completed caching audio.');
});
*/
// TODO: Reimplement this.
cachingAudioPercent = 1.0;
cachingAudioComplete = true;
return 0.0;
}
else if (Constants.PRELOADER_MIN_STAGE_TIME > 0)
{
var elapsedCachingAudio:Float = elapsed - cachingAudioStartTime;
if (cachingAudioComplete && elapsedCachingAudio >= Constants.PRELOADER_MIN_STAGE_TIME)
{
currentState = FunkinPreloaderState.CachingData;
return 0.0;
}
else
{
// We need to return SIMULATED progress here.
if (cachingAudioPercent < (elapsedCachingAudio / Constants.PRELOADER_MIN_STAGE_TIME))
{
return cachingAudioPercent;
}
else
{
return elapsedCachingAudio / Constants.PRELOADER_MIN_STAGE_TIME;
}
}
}
else
{
if (cachingAudioComplete)
{
currentState = FunkinPreloaderState.CachingData;
return 0.0;
}
else
{
return cachingAudioPercent;
}
}
case CachingData:
if (cachingDataPercent < 0)
{
cachingDataPercent = 0.0;
cachingDataStartTime = elapsed;
var assetsToCache:Array<String> = [];
var sparrowFramesToCache:Array<String> = [];
// Core files
// assetsToCache = assetsToCache.concat(Assets.listText('core'));
// assetsToCache = assetsToCache.concat(Assets.listJSON('core'));
// Core spritesheets
// assetsToCache = assetsToCache.concat(Assets.listXML('core'));
// Gameplay files
// assetsToCache = assetsToCache.concat(Assets.listText('gameplay'));
// assetsToCache = assetsToCache.concat(Assets.listJSON('gameplay'));
// We're not caching gameplay spritesheets here because they're fetched on demand.
/*
var future:Future<Array<String>> = [];
// Assets.cacheAssets(assetsToCache, true);
future.onProgress((loaded:Int, total:Int) -> {
cachingDataPercent = loaded / total;
});
future.onComplete((_result) -> {
trace('Completed caching data.');
});
*/
cachingDataPercent = 1.0;
cachingDataComplete = true;
return 0.0;
}
else if (Constants.PRELOADER_MIN_STAGE_TIME > 0)
{
var elapsedCachingData:Float = elapsed - cachingDataStartTime;
if (cachingDataComplete && elapsedCachingData >= Constants.PRELOADER_MIN_STAGE_TIME)
{
currentState = FunkinPreloaderState.ParsingSpritesheets;
return 0.0;
}
else
{
// We need to return SIMULATED progress here.
if (cachingDataPercent < (elapsedCachingData / Constants.PRELOADER_MIN_STAGE_TIME)) return cachingDataPercent;
else
return elapsedCachingData / Constants.PRELOADER_MIN_STAGE_TIME;
}
}
else
{
if (cachingDataComplete)
{
currentState = FunkinPreloaderState.ParsingSpritesheets;
return 0.0;
}
}
return cachingDataPercent;
case ParsingSpritesheets:
if (parsingSpritesheetsPercent < 0)
{
parsingSpritesheetsPercent = 0.0;
parsingSpritesheetsStartTime = elapsed;
// Core spritesheets
var sparrowFramesToCache = []; // Assets.listXML('core').map((xml:String) -> xml.replace('.xml', '').replace('core:assets/core/', ''));
// We're not caching gameplay spritesheets here because they're fetched on demand.
/*
var future:Future<Array<String>> = []; // Assets.cacheSparrowFrames(sparrowFramesToCache, true);
future.onProgress((loaded:Int, total:Int) -> {
parsingSpritesheetsPercent = loaded / total;
});
future.onComplete((_result) -> {
trace('Completed parsing spritesheets.');
});
*/
parsingSpritesheetsPercent = 1.0;
parsingSpritesheetsComplete = true;
return 0.0;
}
else if (Constants.PRELOADER_MIN_STAGE_TIME > 0)
{
var elapsedParsingSpritesheets:Float = elapsed - parsingSpritesheetsStartTime;
if (parsingSpritesheetsComplete && elapsedParsingSpritesheets >= Constants.PRELOADER_MIN_STAGE_TIME)
{
currentState = FunkinPreloaderState.ParsingStages;
return 0.0;
}
else
{
// We need to return SIMULATED progress here.
if (parsingSpritesheetsPercent < (elapsedParsingSpritesheets / Constants.PRELOADER_MIN_STAGE_TIME)) return parsingSpritesheetsPercent;
else
return elapsedParsingSpritesheets / Constants.PRELOADER_MIN_STAGE_TIME;
}
}
else
{
if (parsingSpritesheetsComplete)
{
currentState = FunkinPreloaderState.ParsingStages;
return 0.0;
}
}
return parsingSpritesheetsPercent;
case ParsingStages:
if (parsingStagesPercent < 0)
{
parsingStagesPercent = 0.0;
parsingStagesStartTime = elapsed;
/*
// TODO: Reimplement this.
var future:Future<Array<String>> = []; // StageDataParser.loadStageCacheAsync();
future.onProgress((loaded:Int, total:Int) -> {
parsingStagesPercent = loaded / total;
});
future.onComplete((_result) -> {
trace('Completed parsing stages.');
});
*/
parsingStagesPercent = 1.0;
parsingStagesComplete = true;
return 0.0;
}
else if (Constants.PRELOADER_MIN_STAGE_TIME > 0)
{
var elapsedParsingStages:Float = elapsed - parsingStagesStartTime;
if (parsingStagesComplete && elapsedParsingStages >= Constants.PRELOADER_MIN_STAGE_TIME)
{
currentState = FunkinPreloaderState.ParsingCharacters;
return 0.0;
}
else
{
// We need to return SIMULATED progress here.
if (parsingStagesPercent < (elapsedParsingStages / Constants.PRELOADER_MIN_STAGE_TIME)) return parsingStagesPercent;
else
return elapsedParsingStages / Constants.PRELOADER_MIN_STAGE_TIME;
}
}
else
{
if (parsingStagesComplete)
{
currentState = FunkinPreloaderState.ParsingCharacters;
return 0.0;
}
}
return parsingStagesPercent;
case ParsingCharacters:
if (parsingCharactersPercent < 0)
{
parsingCharactersPercent = 0.0;
parsingCharactersStartTime = elapsed;
/*
// TODO: Reimplement this.
var future:Future<Array<String>> = []; // CharacterDataParser.loadCharacterCacheAsync();
future.onProgress((loaded:Int, total:Int) -> {
parsingCharactersPercent = loaded / total;
});
future.onComplete((_result) -> {
trace('Completed parsing characters.');
});
*/
parsingCharactersPercent = 1.0;
parsingCharactersComplete = true;
return 0.0;
}
else if (Constants.PRELOADER_MIN_STAGE_TIME > 0)
{
var elapsedParsingCharacters:Float = elapsed - parsingCharactersStartTime;
if (parsingCharactersComplete && elapsedParsingCharacters >= Constants.PRELOADER_MIN_STAGE_TIME)
{
currentState = FunkinPreloaderState.ParsingSongs;
return 0.0;
}
else
{
// We need to return SIMULATED progress here.
if (parsingCharactersPercent < (elapsedParsingCharacters / Constants.PRELOADER_MIN_STAGE_TIME)) return parsingCharactersPercent;
else
return elapsedParsingCharacters / Constants.PRELOADER_MIN_STAGE_TIME;
}
}
else
{
if (parsingStagesComplete)
{
currentState = FunkinPreloaderState.ParsingSongs;
return 0.0;
}
}
return parsingCharactersPercent;
case ParsingSongs:
if (parsingSongsPercent < 0)
{
parsingSongsPercent = 0.0;
parsingSongsStartTime = elapsed;
/*
// TODO: Reimplement this.
var future:Future<Array<String>> = ;
// SongDataParser.loadSongCacheAsync();
future.onProgress((loaded:Int, total:Int) -> {
parsingSongsPercent = loaded / total;
});
future.onComplete((_result) -> {
trace('Completed parsing songs.');
});
*/
parsingSongsPercent = 1.0;
parsingSongsComplete = true;
return 0.0;
}
else if (Constants.PRELOADER_MIN_STAGE_TIME > 0)
{
var elapsedParsingSongs:Float = elapsed - parsingSongsStartTime;
if (parsingSongsComplete && elapsedParsingSongs >= Constants.PRELOADER_MIN_STAGE_TIME)
{
currentState = FunkinPreloaderState.Complete;
return 0.0;
}
else
{
// We need to return SIMULATED progress here.
if (parsingSongsPercent < (elapsedParsingSongs / Constants.PRELOADER_MIN_STAGE_TIME))
{
return parsingSongsPercent;
}
else
{
return elapsedParsingSongs / Constants.PRELOADER_MIN_STAGE_TIME;
}
}
}
else
{
if (parsingSongsComplete)
{
currentState = FunkinPreloaderState.Complete;
return 0.0;
}
else
{
return parsingSongsPercent;
}
}
case FunkinPreloaderState.Complete:
if (completeTime < 0)
{
completeTime = elapsed;
}
return 1.0;
#if TOUCH_HERE_TO_PLAY
case FunkinPreloaderState.TouchHereToPlay:
if (completeTime < 0)
{
completeTime = elapsed;
}
if (touchHereToPlay.alpha < 1.0)
{
touchHereToPlay.alpha = 1.0;
addEventListener(MouseEvent.CLICK, onTouchHereToPlay);
}
return 1.0;
#end
default:
// Do nothing.
}
return 0.0;
}
#if TOUCH_HERE_TO_PLAY
function onTouchHereToPlay(e:MouseEvent):Void
{
removeEventListener(MouseEvent.CLICK, onTouchHereToPlay);
// This is the actual thing that makes the game load.
immediatelyStartGame();
}
#end
static final TOTAL_STEPS:Int = 11;
static final ELLIPSIS_TIME:Float = 0.5;
function updateGraphics(percent:Float, elapsed:Float):Void
{
// Render logo (including transitions)
if (completeTime > 0.0)
{
var elapsedFinished:Float = renderLogoFadeOut(elapsed);
// trace('Fading out logo... (' + elapsedFinished + 's)');
if (elapsedFinished > LOGO_FADE_TIME)
{
#if TOUCH_HERE_TO_PLAY
// The logo has faded out, but we're not quite done yet.
// In order to prevent autoplay issues, we need the user to click after the loading finishes.
currentState = FunkinPreloaderState.TouchHereToPlay;
#else
immediatelyStartGame();
#end
}
}
else
{
renderLogoFadeIn(elapsed);
}
// Render progress bar
var maxWidth = this._width - BAR_PADDING * 2;
var barWidth = maxWidth * percent;
progressBar.width = barWidth;
// Cycle ellipsis count to show loading
var ellipsisCount:Int = Std.int(elapsed / ELLIPSIS_TIME) % 3 + 1;
var ellipsis:String = '';
for (i in 0...ellipsisCount)
ellipsis += '.';
// Render status text
switch (currentState)
{
// case FunkinPreloaderState.NotStarted:
default:
updateProgressLeftText('Loading (0/$TOTAL_STEPS)$ellipsis');
case FunkinPreloaderState.DownloadingAssets:
updateProgressLeftText('Downloading assets (1/$TOTAL_STEPS)$ellipsis');
case FunkinPreloaderState.PreloadingPlayAssets:
updateProgressLeftText('Preloading assets (2/$TOTAL_STEPS)$ellipsis');
case FunkinPreloaderState.InitializingScripts:
updateProgressLeftText('Initializing scripts (3/$TOTAL_STEPS)$ellipsis');
case FunkinPreloaderState.CachingGraphics:
updateProgressLeftText('Caching graphics (4/$TOTAL_STEPS)$ellipsis');
case FunkinPreloaderState.CachingAudio:
updateProgressLeftText('Caching audio (5/$TOTAL_STEPS)$ellipsis');
case FunkinPreloaderState.CachingData:
updateProgressLeftText('Caching data (6/$TOTAL_STEPS)$ellipsis');
case FunkinPreloaderState.ParsingSpritesheets:
updateProgressLeftText('Parsing spritesheets (7/$TOTAL_STEPS)$ellipsis');
case FunkinPreloaderState.ParsingStages:
updateProgressLeftText('Parsing stages (8/$TOTAL_STEPS)$ellipsis');
case FunkinPreloaderState.ParsingCharacters:
updateProgressLeftText('Parsing characters (9/$TOTAL_STEPS)$ellipsis');
case FunkinPreloaderState.ParsingSongs:
updateProgressLeftText('Parsing songs (10/$TOTAL_STEPS)$ellipsis');
case FunkinPreloaderState.Complete:
updateProgressLeftText('Finishing up ($TOTAL_STEPS/$TOTAL_STEPS)$ellipsis');
#if TOUCH_HERE_TO_PLAY
case FunkinPreloaderState.TouchHereToPlay:
updateProgressLeftText(null);
#end
}
var percentage:Int = Math.floor(percent * 100);
trace('Preloader state: ' + currentState + ' (' + percentage + '%, ' + elapsed + 's)');
// Render percent text
progressRightText.text = '$percentage%';
super.update(percent);
}
function updateProgressLeftText(text:Null<String>):Void
{
if (progressLeftText != null)
{
if (text == null)
{
progressLeftText.alpha = 0.0;
}
else if (progressLeftText.text != text)
{
// We have to keep updating the text format, because the font can take a frame or two to load.
var progressLeftTextFormat = new TextFormat("VCR OSD Mono", 16, Constants.COLOR_PRELOADER_BAR, true);
progressLeftTextFormat.align = TextFormatAlign.LEFT;
progressLeftText.defaultTextFormat = progressLeftTextFormat;
progressLeftText.text = text;
}
}
}
function immediatelyStartGame():Void
{
_loaded = true;
}
/**
* Fade out the logo.
* @param elapsed Elapsed time since the preloader started.
* @return Elapsed time since the logo started fading out.
*/
function renderLogoFadeOut(elapsed:Float):Float
{
// Fade-out takes LOGO_FADE_TIME seconds.
var elapsedFinished = elapsed - completeTime;
logo.alpha = 1.0 - MathUtil.easeInOutCirc(elapsedFinished / LOGO_FADE_TIME);
logo.scaleX = (1.0 - MathUtil.easeInBack(elapsedFinished / LOGO_FADE_TIME)) * ratio;
logo.scaleY = (1.0 - MathUtil.easeInBack(elapsedFinished / LOGO_FADE_TIME)) * ratio;
logo.x = (this._width - logo.width) / 2;
logo.y = (this._height - logo.height) / 2;
// Fade out progress bar too.
progressBar.alpha = logo.alpha;
progressLeftText.alpha = logo.alpha;
progressRightText.alpha = logo.alpha;
return elapsedFinished;
}
function renderLogoFadeIn(elapsed:Float):Void
{
// Fade-in takes LOGO_FADE_TIME seconds.
logo.alpha = MathUtil.easeInOutCirc(elapsed / LOGO_FADE_TIME);
logo.scaleX = MathUtil.easeOutBack(elapsed / LOGO_FADE_TIME) * ratio;
logo.scaleY = MathUtil.easeOutBack(elapsed / LOGO_FADE_TIME) * ratio;
logo.x = (this._width - logo.width) / 2;
logo.y = (this._height - logo.height) / 2;
}
#if html5
// These fields only exist on Web builds.
/**
* Format the layout of the site lock screen.
*/
override function createSiteLockFailureScreen():Void
{
addChild(createSiteLockFailureBackground(Constants.COLOR_PRELOADER_LOCK_BG, Constants.COLOR_PRELOADER_LOCK_BG));
addChild(createSiteLockFailureIcon(Constants.COLOR_PRELOADER_LOCK_FG, 0.9));
addChild(createSiteLockFailureText(30));
}
/**
* Format the text of the site lock screen.
*/
override function adjustSiteLockTextFields(titleText:TextField, bodyText:TextField, hyperlinkText:TextField):Void
{
var titleFormat = titleText.defaultTextFormat;
titleFormat.align = TextFormatAlign.CENTER;
titleFormat.color = Constants.COLOR_PRELOADER_LOCK_FONT;
titleText.setTextFormat(titleFormat);
var bodyFormat = bodyText.defaultTextFormat;
bodyFormat.align = TextFormatAlign.CENTER;
bodyFormat.color = Constants.COLOR_PRELOADER_LOCK_FONT;
bodyText.setTextFormat(bodyFormat);
var hyperlinkFormat = hyperlinkText.defaultTextFormat;
hyperlinkFormat.align = TextFormatAlign.CENTER;
hyperlinkFormat.color = Constants.COLOR_PRELOADER_LOCK_LINK;
hyperlinkText.setTextFormat(hyperlinkFormat);
}
#end
override function destroy():Void
{
// Ensure the graphics are properly destroyed and GC'd.
removeChild(logo);
removeChild(progressBar);
logo = progressBar = null;
super.destroy();
}
override function onLoaded():Void
{
super.onLoaded();
// We're not ACTUALLY finished.
// This function gets called when the DownloadingAssets step is done.
// We need to wait for the other steps, then the logo to fade out.
_loaded = false;
downloadingAssetsComplete = true;
}
}
enum FunkinPreloaderState
{
/**
* The state before downloading has begun.
* Moves to either `DownloadingAssets` or `CachingGraphics` based on platform.
*/
NotStarted;
/**
* Downloading assets.
* On HTML5, Lime will do this for us, before calling `onLoaded`.
* On Desktop, this step will be completed immediately, and we'll go straight to `CachingGraphics`.
*/
DownloadingAssets;
/**
* Preloading play assets.
* Loads the `manifest.json` for the `gameplay` library.
* If we make the base preloader do this, it will download all the assets as well,
* so we have to do it ourselves.
*/
PreloadingPlayAssets;
/**
* Loading FireTongue, loading Polymod, parsing and instantiating module scripts.
*/
InitializingScripts;
/**
* Loading all graphics from the `core` library to the cache.
*/
CachingGraphics;
/**
* Loading all audio from the `core` library to the cache.
*/
CachingAudio;
/**
* Loading all data files from the `core` library to the cache.
*/
CachingData;
/**
* Parsing all XML files from the `core` library into FlxFramesCollections and caching them.
*/
ParsingSpritesheets;
/**
* Parsing stage data and scripts.
*/
ParsingStages;
/**
* Parsing character data and scripts.
*/
ParsingCharacters;
/**
* Parsing song data and scripts.
*/
ParsingSongs;
/**
* Finishing up.
*/
Complete;
#if TOUCH_HERE_TO_PLAY
/**
* Touch Here to Play is displayed.
*/
TouchHereToPlay;
#end
}

View file

@ -0,0 +1,17 @@
# funkin.ui.loading.preload
This package contains code powering the HTML5 preloader screen.
The preloader performs the following tasks:
- **Downloading assets**: Downloads the `core` asset library and loads its manifest
- **Preloading play assets**: Downloads the `gameplay` asset library (manifest only)
- **Initializing scripts**: Downloads and registers stage scripts, character scripts, song scripts, and module scripts.
- **Caching graphics**: Downloads all graphics from the `core` asset library, uploads them to the GPU, then dumps them from RAM. This prepares them to be used very quickly in-game.
- **Caching audio**: Downloads all audio files from the `core` asset library, and caches them. This prepares them to be used very quickly in-game.
- **Caching data**: Downloads and caches all TXT files, all JSON files (it also parses them), and XML files (from the `core` library only). This prepares them to be used in the next steps.
- **Parsing stages**: Parses all stage data and instantiates associated stage scripts. This prepares them to be used in-game.
- **Parsing characters**: Parses all character data and instantiates associated character scripts. This prepares them to be used in-game.
- **Parsing songs**: Parses all song data and instantiates associated song scripts. This prepares them to be used in-game.
- **Finishing up**: Waits for the screen to fade out. Then, it loads the first state of the app.
Due to the first few steps not being relevant on desktop, and due to this preloader being built in Lime rather than HaxeFlixel because of how Lime handles asset loading, this preloader is not used on desktop. The splash loader is used instead.

View file

@ -1,8 +1,9 @@
package funkin.util;
import flixel.system.FlxBasePreloader;
import flixel.util.FlxColor;
import lime.app.Application;
import funkin.data.song.SongData.SongTimeFormat;
import lime.app.Application;
/**
* A store of unchanging, globally relevant values.
@ -59,6 +60,16 @@ class Constants
*/
// ==============================
/**
* Preloader sitelock.
* Matching is done by `FlxStringUtil.getDomain`, so any URL on the domain will work.
* The first link in this list is the one users will be redirected to if they try to access the game from a different URL.
*/
public static final SITE_LOCK:Array<String> = [
"https://www.newgrounds.com/portal/view/770371", // Newgrounds, baybee!
FlxBasePreloader.LOCAL // localhost for dev stuff
];
/**
* Link to download the game on Itch.io.
*/
@ -116,6 +127,44 @@ class Constants
0xFFCC1111 // right (3)
];
/**
* Color for the preloader background
*/
public static final COLOR_PRELOADER_BG:FlxColor = 0xFF000000;
/**
* Color for the preloader progress bar
*/
public static final COLOR_PRELOADER_BAR:FlxColor = 0xFF00FF00;
/**
* Color for the preloader site lock background
*/
public static final COLOR_PRELOADER_LOCK_BG:FlxColor = 0xFF1B1717;
/**
* Color for the preloader site lock foreground
*/
public static final COLOR_PRELOADER_LOCK_FG:FlxColor = 0xB96F10;
/**
* Color for the preloader site lock text
*/
public static final COLOR_PRELOADER_LOCK_FONT:FlxColor = 0xCCCCCC;
/**
* Color for the preloader site lock link
*/
public static final COLOR_PRELOADER_LOCK_LINK:FlxColor = 0xEEB211;
/**
* LANGUAGE
*/
// ==============================
public static final SITE_LOCK_TITLE:String = "You Loser!";
public static final SITE_LOCK_DESC:String = "This isn't Newgrounds!\nGo play Friday Night Funkin' on Newgrounds:";
/**
* GAME DEFAULTS
*/
@ -290,6 +339,19 @@ class Constants
*/
public static final MP3_DELAY_MS:Float = 528 / 44100 * Constants.MS_PER_SEC;
/**
* Each step of the preloader has to be on screen at least this long.
*
* 0 = The preloader immediately moves to the next step when it's ready.
* 1 = The preloader waits for 1 second before moving to the next step.
* The progress bare is automatically rescaled to match.
*/
#if debug
public static final PRELOADER_MIN_STAGE_TIME:Float = 1.0;
#else
public static final PRELOADER_MIN_STAGE_TIME:Float = 0.1;
#end
/**
* HEALTH VALUES
*/

View file

@ -48,6 +48,36 @@ class MathUtil
return Math.log(value) / Math.log(base);
}
public static function easeInOutCirc(x:Float):Float
{
if (x <= 0.0) return 0.0;
if (x >= 1.0) return 1.0;
var result:Float = (x < 0.5) ? (1 - Math.sqrt(1 - 4 * x * x)) / 2 : (Math.sqrt(1 - 4 * (1 - x) * (1 - x)) + 1) / 2;
return (result == Math.NaN) ? 1.0 : result;
}
public static function easeInOutBack(x:Float, ?c:Float = 1.70158):Float
{
if (x <= 0.0) return 0.0;
if (x >= 1.0) return 1.0;
var result:Float = (x < 0.5) ? (2 * x * x * ((c + 1) * 2 * x - c)) / 2 : (1 - 2 * (1 - x) * (1 - x) * ((c + 1) * 2 * (1 - x) - c)) / 2;
return (result == Math.NaN) ? 1.0 : result;
}
public static function easeInBack(x:Float, ?c:Float = 1.70158):Float
{
if (x <= 0.0) return 0.0;
if (x >= 1.0) return 1.0;
return (1 + c) * x * x * x - c * x * x;
}
public static function easeOutBack(x:Float, ?c:Float = 1.70158):Float
{
if (x <= 0.0) return 0.0;
if (x >= 1.0) return 1.0;
return 1 + (c + 1) * Math.pow(x - 1, 3) + c * Math.pow(x - 1, 2);
}
/**
* Get the base-2 logarithm of a value.
* @param x value