Merge branch 'rewrite/master' into a-bot-bars

This commit is contained in:
EliteMasterEric 2024-03-28 20:26:13 -04:00
commit e5a10c6a25
94 changed files with 3241 additions and 1040 deletions

View File

@ -13,8 +13,9 @@ jobs:
apt update
apt install -y sudo git curl unzip
- name: Fix git config on posix runner
# this can't be {{ github.workspace }} because that's not docker-aware
run: |
git config --global --add safe.directory ${{ github.workspace }}
git config --global --add safe.directory $GITHUB_WORKSPACE
- name: Get checkout token
uses: actions/create-github-app-token@v1
id: app_token
@ -90,8 +91,9 @@ jobs:
runs-on: [self-hosted, macos]
steps:
- name: Fix git config on posix runner
# this can't be {{ github.workspace }} because that's not docker-aware
run: |
git config --global --add safe.directory ${{ github.workspace }}
git config --global --add safe.directory $GITHUB_WORKSPACE
- name: Get checkout token
uses: actions/create-github-app-token@v1
id: app_token

View File

@ -4,6 +4,7 @@ export
# Ignore all JSONS in the images folder (including FlxAnimate JSONs)
assets/preload/images
assets/shared/images
assets/weekend1/images
# Don't ignore data files
# TODO: These don't work.

17
.vscode/settings.json vendored
View File

@ -85,7 +85,7 @@
},
"projectManager.git.baseFolders": ["./"],
"haxecheckstyle.sourceFolders": ["src", "Source"],
"haxecheckstyle.sourceFolders": ["src", "source"],
"haxecheckstyle.externalSourceRoots": [],
"haxecheckstyle.configurationFile": "checkstyle.json",
"haxecheckstyle.codeSimilarityBufferSize": 100,
@ -204,6 +204,21 @@
"label": "HTML5 / Debug (Watch)",
"target": "html5",
"args": ["-debug", "-watch", "-DFORCE_DEBUG_VERSION"]
},
{
"label": "macOS / Debug",
"target": "mac",
"args": ["-debug", "-DFORCE_DEBUG_VERSION"]
},
{
"label": "macOS / Release",
"target": "mac",
"args": ["-release"]
},
{
"label": "macOS / Release (GitHub Actions)",
"target": "mac",
"args": ["-release", "-DGITHUB_BUILD"]
}
],
"cmake.configureOnOpen": false,

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 9ca1b77f8c69a391c2454a47b61c3865464d92ac
Subproject commit 69ac1b5f10ee8722ac0586a0e0282f04dccc07eb

View File

@ -79,7 +79,7 @@
{
"props": {
"ignoreExtern": true,
"format": "^[A-Z][A-Z0-9]*(_[A-Z0-9_]+)*$",
"format": "^[a-z][A-Z][A-Z0-9]*(_[A-Z0-9_]+)*$",
"tokens": ["INLINE", "NOTINLINE"]
},
"type": "ConstantName"

View File

@ -153,7 +153,7 @@
"name": "polymod",
"type": "git",
"dir": null,
"ref": "5547763a22858a1f10939e082de421d587c862bf",
"ref": "8553b800965f225bb14c7ab8f04bfa9cdec362ac",
"url": "https://github.com/larsiusprime/polymod"
},
{

View File

@ -11,9 +11,11 @@ import openfl.display.Sprite;
import openfl.events.Event;
import openfl.Lib;
import openfl.media.Video;
import funkin.util.CLIUtil;
import openfl.net.NetStream;
/**
* The main class which initializes HaxeFlixel and starts the game in its initial state.
*/
class Main extends Sprite
{
var gameWidth:Int = 1280; // Width of the game in pixels (might be less / more in actual pixels depending on your zoom).
@ -76,26 +78,27 @@ class Main extends Sprite
var netStream:NetStream;
var overlay:Sprite;
/**
* A frame counter displayed at the top left.
*/
public static var fpsCounter:FPS;
/**
* A RAM counter displayed at the top left.
*/
public static var memoryCounter:MemoryCounter;
function setupGame():Void
{
/**
* The `zoom` argument of FlxGame was removed in the dev branch of Flixel,
* since it was considered confusing and unintuitive.
* If you want to change how the game scales when you resize the window,
* you can use `FlxG.scaleMode`.
* -Eric
*/
initHaxeUI();
// addChild gets called by the user settings code.
fpsCounter = new FPS(10, 3, 0xFFFFFF);
// addChild(fpsCounter); // Handled by Preferences.init
#if !html5
// addChild gets called by the user settings code.
// TODO: disabled on HTML5 (todo: find another method that works?)
memoryCounter = new MemoryCounter(10, 13, 0xFFFFFF);
// addChild(memoryCounter);
#end
// George recommends binding the save before FlxGame is created.
@ -112,6 +115,8 @@ class Main extends Sprite
#if hxcpp_debug_server
trace('hxcpp_debug_server is enabled! You can now connect to the game with a debugger.');
#else
trace('hxcpp_debug_server is disabled! This build does not support debugging.');
#end
}

View File

@ -3,34 +3,37 @@ package source; // Yeah, I know...
import sys.FileSystem;
import sys.io.File;
/**
* A script which executes after the game is built.
*/
class Postbuild
{
static inline final buildTimeFile = '.build_time';
static inline final BUILD_TIME_FILE:String = '.build_time';
static function main()
static function main():Void
{
printBuildTime();
}
static function printBuildTime()
static function printBuildTime():Void
{
// get buildEnd before fs operations since they are blocking
var end:Float = Sys.time();
if (FileSystem.exists(buildTimeFile))
if (FileSystem.exists(BUILD_TIME_FILE))
{
var fi = File.read(buildTimeFile);
var fi:sys.io.FileInput = File.read(BUILD_TIME_FILE);
var start:Float = fi.readDouble();
fi.close();
sys.FileSystem.deleteFile(buildTimeFile);
sys.FileSystem.deleteFile(BUILD_TIME_FILE);
var buildTime = roundToTwoDecimals(end - start);
var buildTime:Float = roundToTwoDecimals(end - start);
trace('Build took: ${buildTime} seconds');
}
}
private static function roundToTwoDecimals(value:Float):Float
static function roundToTwoDecimals(value:Float):Float
{
return Math.round(value * 100) / 100;
}

View File

@ -2,20 +2,23 @@ package source; // Yeah, I know...
import sys.io.File;
/**
* A script which executes before the game is built.
*/
class Prebuild
{
static inline final buildTimeFile = '.build_time';
static inline final BUILD_TIME_FILE:String = '.build_time';
static function main()
static function main():Void
{
saveBuildTime();
trace('Building...');
}
static function saveBuildTime()
static function saveBuildTime():Void
{
var fo = File.write(buildTimeFile);
var now = Sys.time();
var fo:sys.io.FileOutput = File.write(BUILD_TIME_FILE);
var now:Float = Sys.time();
fo.writeDouble(now);
fo.close();
}

View File

@ -1,234 +0,0 @@
package flixel.addons.transition;
import flixel.FlxSubState;
import flixel.addons.transition.FlxTransitionableState;
/**
* A `FlxSubState` which can perform visual transitions
*
* Usage:
*
* First, extend `FlxTransitionableSubState` as ie, `FooState`.
*
* Method 1:
*
* ```haxe
* var in:TransitionData = new TransitionData(...); // add your data where "..." is
* var out:TransitionData = new TransitionData(...);
*
* FlxG.switchState(() -> new FooState(in,out));
* ```
*
* Method 2:
*
* ```haxe
* FlxTransitionableSubState.defaultTransIn = new TransitionData(...);
* FlxTransitionableSubState.defaultTransOut = new TransitionData(...);
*
* FlxG.switchState(() -> new FooState());
* ```
*/
class FlxTransitionableSubState extends FlxSubState
{
// global default transitions for ALL states, used if transIn/transOut are null
public static var defaultTransIn(get, set):TransitionData;
static function get_defaultTransIn():TransitionData
{
return FlxTransitionableState.defaultTransIn;
}
static function set_defaultTransIn(value:TransitionData):TransitionData
{
return FlxTransitionableState.defaultTransIn = value;
}
public static var defaultTransOut(get, set):TransitionData;
static function get_defaultTransOut():TransitionData
{
return FlxTransitionableState.defaultTransOut;
}
static function set_defaultTransOut(value:TransitionData):TransitionData
{
return FlxTransitionableState.defaultTransOut = value;
}
public static var skipNextTransIn(get, set):Bool;
static function get_skipNextTransIn():Bool
{
return FlxTransitionableState.skipNextTransIn;
}
static function set_skipNextTransIn(value:Bool):Bool
{
return FlxTransitionableState.skipNextTransIn = value;
}
public static var skipNextTransOut(get, set):Bool;
static function get_skipNextTransOut():Bool
{
return FlxTransitionableState.skipNextTransOut;
}
static function set_skipNextTransOut(value:Bool):Bool
{
return FlxTransitionableState.skipNextTransOut = value;
}
// beginning & ending transitions for THIS state:
public var transIn:TransitionData;
public var transOut:TransitionData;
public var hasTransIn(get, never):Bool;
public var hasTransOut(get, never):Bool;
/**
* Create a state with the ability to do visual transitions
* @param TransIn Plays when the state begins
* @param TransOut Plays when the state ends
*/
public function new(?TransIn:TransitionData, ?TransOut:TransitionData)
{
transIn = TransIn;
transOut = TransOut;
if (transIn == null && defaultTransIn != null)
{
transIn = defaultTransIn;
}
if (transOut == null && defaultTransOut != null)
{
transOut = defaultTransOut;
}
super();
}
override function destroy():Void
{
super.destroy();
transIn = null;
transOut = null;
_onExit = null;
}
override function create():Void
{
super.create();
transitionIn();
}
override function startOutro(onOutroComplete:() -> Void)
{
if (!hasTransOut) onOutroComplete();
else if (!_exiting)
{
// play the exit transition, and when it's done call FlxG.switchState
_exiting = true;
transitionOut(onOutroComplete);
if (skipNextTransOut)
{
skipNextTransOut = false;
finishTransOut();
}
}
}
/**
* Starts the in-transition. Can be called manually at any time.
*/
public function transitionIn():Void
{
if (transIn != null && transIn.type != NONE)
{
if (skipNextTransIn)
{
skipNextTransIn = false;
if (finishTransIn != null)
{
finishTransIn();
}
return;
}
var _trans = createTransition(transIn);
_trans.setStatus(FULL);
openSubState(_trans);
_trans.finishCallback = finishTransIn;
_trans.start(OUT);
}
}
/**
* Starts the out-transition. Can be called manually at any time.
*/
public function transitionOut(?OnExit:Void->Void):Void
{
_onExit = OnExit;
if (hasTransOut)
{
var _trans = createTransition(transOut);
_trans.setStatus(EMPTY);
openSubState(_trans);
_trans.finishCallback = finishTransOut;
_trans.start(IN);
}
else
{
_onExit();
}
}
var transOutFinished:Bool = false;
var _exiting:Bool = false;
var _onExit:Void->Void;
function get_hasTransIn():Bool
{
return transIn != null && transIn.type != NONE;
}
function get_hasTransOut():Bool
{
return transOut != null && transOut.type != NONE;
}
function createTransition(data:TransitionData):Transition
{
return switch (data.type)
{
case TILES: new Transition(data);
case FADE: new Transition(data);
default: null;
}
}
function finishTransIn()
{
closeSubState();
}
function finishTransOut()
{
transOutFinished = true;
if (!_exiting)
{
closeSubState();
}
if (_onExit != null)
{
_onExit();
}
}
}

View File

@ -3,7 +3,6 @@ package funkin;
import funkin.util.Constants;
import flixel.util.FlxSignal;
import flixel.math.FlxMath;
import funkin.play.song.Song.SongDifficulty;
import funkin.data.song.SongData.SongTimeChange;
import funkin.data.song.SongDataUtils;
@ -36,7 +35,15 @@ class Conductor
* You can also do stuff like store a reference to the Conductor and pass it around or temporarily replace it,
* or have a second Conductor running at the same time, or other weird stuff like that if you need to.
*/
public static var instance:Conductor = new Conductor();
public static var instance(get, never):Conductor;
static var _instance:Null<Conductor> = null;
static function get_instance():Conductor
{
if (_instance == null) _instance = new Conductor();
return _instance;
}
/**
* Signal fired when the current Conductor instance advances to a new measure.
@ -136,6 +143,9 @@ class Conductor
return beatLengthMs / timeSignatureNumerator;
}
/**
* The numerator for the current time signature (the `3` in `3/4`).
*/
public var timeSignatureNumerator(get, never):Int;
function get_timeSignatureNumerator():Int
@ -145,6 +155,9 @@ class Conductor
return currentTimeChange.timeSignatureNum;
}
/**
* The denominator for the current time signature (the `4` in `3/4`).
*/
public var timeSignatureDenominator(get, never):Int;
function get_timeSignatureDenominator():Int
@ -245,7 +258,7 @@ class Conductor
* WARNING: Avoid this for things like setting the BPM of the title screen music,
* you should have a metadata file for it instead.
*/
public function forceBPM(?bpm:Float = null)
public function forceBPM(?bpm:Float):Void
{
if (bpm != null)
{
@ -253,7 +266,7 @@ class Conductor
}
else
{
// trace('[CONDUCTOR] Resetting BPM to default');
trace('[CONDUCTOR] Resetting BPM to default');
}
this.bpmOverride = bpm;
@ -266,7 +279,7 @@ class Conductor
* @param songPosition The current position in the song in milliseconds.
* Leave blank to use the FlxG.sound.music position.
*/
public function update(?songPos:Float)
public function update(?songPos:Float):Void
{
if (songPos == null)
{
@ -274,9 +287,9 @@ class Conductor
songPos = (FlxG.sound.music != null) ? (FlxG.sound.music.time + instrumentalOffset + formatOffset) : 0.0;
}
var oldMeasure = this.currentMeasure;
var oldBeat = this.currentBeat;
var oldStep = this.currentStep;
var oldMeasure:Float = this.currentMeasure;
var oldBeat:Float = this.currentBeat;
var oldStep:Float = this.currentStep;
// Set the song position we are at (for purposes of calculating note positions, etc).
this.songPosition = songPos;
@ -338,39 +351,43 @@ class Conductor
}
}
public function mapTimeChanges(songTimeChanges:Array<SongTimeChange>)
/**
* Apply the `SongTimeChange` data from the song metadata to this Conductor.
* @param songTimeChanges The SongTimeChanges.
*/
public function mapTimeChanges(songTimeChanges:Array<SongTimeChange>):Void
{
timeChanges = [];
// Sort in place just in case it's out of order.
SongDataUtils.sortTimeChanges(songTimeChanges);
for (currentTimeChange in songTimeChanges)
for (songTimeChange in songTimeChanges)
{
// TODO: Maybe handle this different?
// Do we care about BPM at negative timestamps?
// Without any custom handling, `currentStepTime` becomes non-zero at `songPosition = 0`.
if (currentTimeChange.timeStamp < 0.0) currentTimeChange.timeStamp = 0.0;
if (songTimeChange.timeStamp < 0.0) songTimeChange.timeStamp = 0.0;
if (currentTimeChange.timeStamp <= 0.0)
if (songTimeChange.timeStamp <= 0.0)
{
currentTimeChange.beatTime = 0.0;
songTimeChange.beatTime = 0.0;
}
else
{
// Calculate the beat time of this timestamp.
currentTimeChange.beatTime = 0.0;
songTimeChange.beatTime = 0.0;
if (currentTimeChange.timeStamp > 0.0 && timeChanges.length > 0)
if (songTimeChange.timeStamp > 0.0 && timeChanges.length > 0)
{
var prevTimeChange:SongTimeChange = timeChanges[timeChanges.length - 1];
currentTimeChange.beatTime = FlxMath.roundDecimal(prevTimeChange.beatTime
+ ((currentTimeChange.timeStamp - prevTimeChange.timeStamp) * prevTimeChange.bpm / Constants.SECS_PER_MIN / Constants.MS_PER_SEC),
songTimeChange.beatTime = FlxMath.roundDecimal(prevTimeChange.beatTime
+ ((songTimeChange.timeStamp - prevTimeChange.timeStamp) * prevTimeChange.bpm / Constants.SECS_PER_MIN / Constants.MS_PER_SEC),
4);
}
}
timeChanges.push(currentTimeChange);
timeChanges.push(songTimeChange);
}
if (timeChanges.length > 0)
@ -384,6 +401,8 @@ class Conductor
/**
* Given a time in milliseconds, return a time in steps.
* @param ms The time in milliseconds.
* @return The time in steps.
*/
public function getTimeInSteps(ms:Float):Float
{
@ -413,7 +432,7 @@ class Conductor
var lastStepLengthMs:Float = ((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) / timeSignatureNumerator;
var resultFractionalStep:Float = (ms - lastTimeChange.timeStamp) / lastStepLengthMs;
resultStep += resultFractionalStep; // Math.floor();
resultStep += resultFractionalStep;
return resultStep;
}
@ -421,6 +440,8 @@ class Conductor
/**
* Given a time in steps and fractional steps, return a time in milliseconds.
* @param stepTime The time in steps.
* @return The time in milliseconds.
*/
public function getStepTimeInMs(stepTime:Float):Float
{
@ -457,6 +478,8 @@ class Conductor
/**
* Given a time in beats and fractional beats, return a time in milliseconds.
* @param beatTime The time in beats.
* @return The time in milliseconds.
*/
public function getBeatTimeInMs(beatTime:Float):Float
{
@ -491,13 +514,16 @@ class Conductor
}
}
/**
* Add variables of the current Conductor instance to the Flixel debugger.
*/
public static function watchQuick():Void
{
FlxG.watch.addQuick("songPosition", Conductor.instance.songPosition);
FlxG.watch.addQuick("bpm", Conductor.instance.bpm);
FlxG.watch.addQuick("currentMeasureTime", Conductor.instance.currentMeasureTime);
FlxG.watch.addQuick("currentBeatTime", Conductor.instance.currentBeatTime);
FlxG.watch.addQuick("currentStepTime", Conductor.instance.currentStepTime);
FlxG.watch.addQuick('songPosition', Conductor.instance.songPosition);
FlxG.watch.addQuick('bpm', Conductor.instance.bpm);
FlxG.watch.addQuick('currentMeasureTime', Conductor.instance.currentMeasureTime);
FlxG.watch.addQuick('currentBeatTime', Conductor.instance.currentBeatTime);
FlxG.watch.addQuick('currentStepTime', Conductor.instance.currentStepTime);
}
/**
@ -505,6 +531,6 @@ class Conductor
*/
public static function reset():Void
{
Conductor.instance = new Conductor();
_instance = new Conductor();
}
}

View File

@ -12,7 +12,6 @@ import flixel.math.FlxRect;
import flixel.FlxSprite;
import flixel.system.debug.log.LogStyle;
import flixel.util.FlxColor;
import funkin.ui.options.PreferencesMenu;
import funkin.util.macro.MacroUtil;
import funkin.util.WindowUtil;
import funkin.play.PlayStatePlaylist;
@ -24,6 +23,7 @@ import funkin.data.stage.StageRegistry;
import funkin.data.dialogue.ConversationRegistry;
import funkin.data.dialogue.DialogueBoxRegistry;
import funkin.data.dialogue.SpeakerRegistry;
import funkin.data.freeplay.AlbumRegistry;
import funkin.data.song.SongRegistry;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.modding.module.ModuleHandler;
@ -31,7 +31,6 @@ import funkin.ui.title.TitleState;
import funkin.util.CLIUtil;
import funkin.util.CLIUtil.CLIParams;
import funkin.util.TimerUtil;
import funkin.ui.transition.LoadingState;
import funkin.util.TrackerUtil;
#if discord_rpc
import Discord.DiscordClient;
@ -167,10 +166,12 @@ class InitState extends FlxState
ConversationRegistry.instance.loadEntries();
DialogueBoxRegistry.instance.loadEntries();
SpeakerRegistry.instance.loadEntries();
AlbumRegistry.instance.loadEntries();
StageRegistry.instance.loadEntries();
// TODO: CharacterDataParser doesn't use json2object, so it's way slower than the other parsers.
CharacterDataParser.loadCharacterCache(); // TODO: Migrate characters to BaseRegistry.
// TODO: CharacterDataParser doesn't use json2object, so it's way slower than the other parsers and more prone to syntax errors.
// Move it to use a BaseRegistry.
CharacterDataParser.loadCharacterCache();
ModuleHandler.buildModuleCallbacks();
ModuleHandler.loadModuleCache();
@ -188,25 +189,35 @@ class InitState extends FlxState
*/
function startGame():Void
{
#if SONG // -DSONG=bopeebo
#if SONG
// -DSONG=bopeebo
startSong(defineSong(), defineDifficulty());
#elseif LEVEL // -DLEVEL=week1 -DDIFFICULTY=hard
#elseif LEVEL
// -DLEVEL=week1 -DDIFFICULTY=hard
startLevel(defineLevel(), defineDifficulty());
#elseif FREEPLAY // -DFREEPLAY
#elseif FREEPLAY
// -DFREEPLAY
FlxG.switchState(() -> new funkin.ui.freeplay.FreeplayState());
#elseif DIALOGUE // -DDIALOGUE
#elseif DIALOGUE
// -DDIALOGUE
FlxG.switchState(() -> new funkin.ui.debug.dialogue.ConversationDebugState());
#elseif ANIMATE // -DANIMATE
#elseif ANIMATE
// -DANIMATE
FlxG.switchState(() -> new funkin.ui.debug.anim.FlxAnimateTest());
#elseif WAVEFORM // -DWAVEFORM
#elseif WAVEFORM
// -DWAVEFORM
FlxG.switchState(() -> new funkin.ui.debug.WaveformTestState());
#elseif CHARTING // -DCHARTING
#elseif CHARTING
// -DCHARTING
FlxG.switchState(() -> new funkin.ui.debug.charting.ChartEditorState());
#elseif STAGEBUILD // -DSTAGEBUILD
#elseif STAGEBUILD
// -DSTAGEBUILD
FlxG.switchState(() -> new funkin.ui.debug.stage.StageBuilderState());
#elseif ANIMDEBUG // -DANIMDEBUG
#elseif ANIMDEBUG
// -DANIMDEBUG
FlxG.switchState(() -> new funkin.ui.debug.anim.DebugBoundingState());
#elseif LATENCY // -DLATENCY
#elseif LATENCY
// -DLATENCY
FlxG.switchState(() -> new funkin.LatencyState());
#else
startGameNormally();

View File

@ -11,144 +11,144 @@ class Paths
{
static var currentLevel:String;
static public function setCurrentLevel(name:String)
public static function setCurrentLevel(name:String):Void
{
currentLevel = name.toLowerCase();
}
public static function stripLibrary(path:String):String
{
var parts = path.split(':');
var parts:Array<String> = path.split(':');
if (parts.length < 2) return path;
return parts[1];
}
public static function getLibrary(path:String):String
{
var parts = path.split(':');
if (parts.length < 2) return "preload";
var parts:Array<String> = path.split(':');
if (parts.length < 2) return 'preload';
return parts[0];
}
static function getPath(file:String, type:AssetType, library:Null<String>)
static function getPath(file:String, type:AssetType, library:Null<String>):String
{
if (library != null) return getLibraryPath(file, library);
if (currentLevel != null)
{
var levelPath = getLibraryPathForce(file, currentLevel);
var levelPath:String = getLibraryPathForce(file, currentLevel);
if (OpenFlAssets.exists(levelPath, type)) return levelPath;
}
var levelPath = getLibraryPathForce(file, "shared");
var levelPath:String = getLibraryPathForce(file, 'shared');
if (OpenFlAssets.exists(levelPath, type)) return levelPath;
return getPreloadPath(file);
}
static public function getLibraryPath(file:String, library = "preload")
public static function getLibraryPath(file:String, library = 'preload'):String
{
return if (library == "preload" || library == "default") getPreloadPath(file); else getLibraryPathForce(file, library);
return if (library == 'preload' || library == 'default') getPreloadPath(file); else getLibraryPathForce(file, library);
}
inline static function getLibraryPathForce(file:String, library:String)
static inline function getLibraryPathForce(file:String, library:String):String
{
return '$library:assets/$library/$file';
}
inline static function getPreloadPath(file:String)
static inline function getPreloadPath(file:String):String
{
return 'assets/$file';
}
inline static public function file(file:String, type:AssetType = TEXT, ?library:String)
public static function file(file:String, type:AssetType = TEXT, ?library:String):String
{
return getPath(file, type, library);
}
public static inline function animateAtlas(path:String, ?library:String)
public static function animateAtlas(path:String, ?library:String):String
{
return getLibraryPath('images/$path', library);
}
inline static public function txt(key:String, ?library:String)
public static function txt(key:String, ?library:String):String
{
return getPath('data/$key.txt', TEXT, library);
}
inline static public function frag(key:String, ?library:String)
public static function frag(key:String, ?library:String):String
{
return getPath('shaders/$key.frag', TEXT, library);
}
inline static public function vert(key:String, ?library:String)
public static function vert(key:String, ?library:String):String
{
return getPath('shaders/$key.vert', TEXT, library);
}
inline static public function xml(key:String, ?library:String)
public static function xml(key:String, ?library:String):String
{
return getPath('data/$key.xml', TEXT, library);
}
inline static public function json(key:String, ?library:String)
public static function json(key:String, ?library:String):String
{
return getPath('data/$key.json', TEXT, library);
}
static public function sound(key:String, ?library:String)
public static function sound(key:String, ?library:String):String
{
return getPath('sounds/$key.${Constants.EXT_SOUND}', SOUND, library);
}
inline static public function soundRandom(key:String, min:Int, max:Int, ?library:String)
public static function soundRandom(key:String, min:Int, max:Int, ?library:String):String
{
return sound(key + FlxG.random.int(min, max), library);
}
inline static public function music(key:String, ?library:String)
public static function music(key:String, ?library:String):String
{
return getPath('music/$key.${Constants.EXT_SOUND}', MUSIC, library);
}
inline static public function videos(key:String, ?library:String)
public static function videos(key:String, ?library:String):String
{
return getPath('videos/$key.${Constants.EXT_VIDEO}', BINARY, library);
}
inline static public function voices(song:String, ?suffix:String = '')
public static function voices(song:String, ?suffix:String = ''):String
{
if (suffix == null) suffix = ""; // no suffix, for a sorta backwards compatibility with older-ish voice files
if (suffix == null) suffix = ''; // no suffix, for a sorta backwards compatibility with older-ish voice files
return 'songs:assets/songs/${song.toLowerCase()}/Voices$suffix.${Constants.EXT_SOUND}';
}
inline static public function inst(song:String, ?suffix:String = '')
public static function inst(song:String, ?suffix:String = ''):String
{
return 'songs:assets/songs/${song.toLowerCase()}/Inst$suffix.${Constants.EXT_SOUND}';
}
inline static public function image(key:String, ?library:String)
public static function image(key:String, ?library:String):String
{
return getPath('images/$key.png', IMAGE, library);
}
inline static public function font(key:String)
public static function font(key:String):String
{
return 'assets/fonts/$key';
}
inline static public function ui(key:String, ?library:String)
public static function ui(key:String, ?library:String):String
{
return xml('ui/$key', library);
}
static public function getSparrowAtlas(key:String, ?library:String)
public static function getSparrowAtlas(key:String, ?library:String):FlxAtlasFrames
{
return FlxAtlasFrames.fromSparrow(image(key, library), file('images/$key.xml', library));
}
inline static public function getPackerAtlas(key:String, ?library:String)
public static function getPackerAtlas(key:String, ?library:String):FlxAtlasFrames
{
return FlxAtlasFrames.fromSpriteSheetPacker(image(key, library), file('images/$key.txt', library));
}

View File

@ -13,6 +13,7 @@ import flixel.util.FlxSignal;
*/
class PlayerSettings
{
// TODO: Finish implementation of second player.
public static var numPlayers(default, null) = 0;
public static var numAvatars(default, null) = 0;
public static var player1(default, null):PlayerSettings;
@ -21,12 +22,21 @@ class PlayerSettings
public static var onAvatarAdd(default, null) = new FlxTypedSignal<PlayerSettings->Void>();
public static var onAvatarRemove(default, null) = new FlxTypedSignal<PlayerSettings->Void>();
/**
* The player number associated with this settings object.
*/
public var id(default, null):Int;
/**
* The controls handler for this player.
*/
public var controls(default, null):Controls;
/**
* Return the PlayerSettings for the given player number, or `null` if that player isn't active.
*
* @param id The player number this represents.
* @return The PlayerSettings for the given player number, or `null` if that player isn't active.
*/
public static function get(id:Int):Null<PlayerSettings>
{
@ -38,6 +48,9 @@ class PlayerSettings
};
}
/**
* Initialize the PlayerSettings singletons for each player.
*/
public static function init():Void
{
if (player1 == null)
@ -56,22 +69,30 @@ class PlayerSettings
}
}
public static function reset()
/**
* Forcibly destroy the PlayerSettings singletons for each player.
*/
public static function reset():Void
{
player1 = null;
player2 = null;
numPlayers = 0;
}
static function onGamepadAdded(gamepad:FlxGamepad)
/**
* Callback invoked when a gamepad is added.
* @param gamepad The gamepad that was added.
*/
static function onGamepadAdded(gamepad:FlxGamepad):Void
{
// TODO: Make this detect and handle multiple players
player1.addGamepad(gamepad);
}
/**
* @param id The player number this represents. This was refactored to START AT `1`.
*/
private function new(id:Int)
function new(id:Int)
{
trace('loading player settings for id: $id');
@ -83,11 +104,11 @@ class PlayerSettings
function addKeyboard():Void
{
var useDefault = true;
var useDefault:Bool = true;
if (Save.instance.hasControls(id, Keys))
{
var keyControlData = Save.instance.getControls(id, Keys);
trace("keyControlData: " + haxe.Json.stringify(keyControlData));
trace('Loading keyboard control scheme from user save');
useDefault = false;
controls.fromSaveData(keyControlData, Keys);
}
@ -98,7 +119,7 @@ class PlayerSettings
if (useDefault)
{
trace("Loading default keyboard control scheme");
trace('Loading default keyboard control scheme');
controls.setKeyboardScheme(Solo);
}
@ -109,13 +130,13 @@ class PlayerSettings
* Called after an FlxGamepad has been detected.
* @param gamepad The gamepad that was detected.
*/
function addGamepad(gamepad:FlxGamepad)
function addGamepad(gamepad:FlxGamepad):Void
{
var useDefault = true;
if (Save.instance.hasControls(id, Gamepad(gamepad.id)))
{
var padControlData = Save.instance.getControls(id, Gamepad(gamepad.id));
trace("padControlData: " + haxe.Json.stringify(padControlData));
trace('Loading gamepad control scheme from user save');
useDefault = false;
controls.addGamepadWithSaveData(gamepad.id, padControlData);
}
@ -126,7 +147,7 @@ class PlayerSettings
if (useDefault)
{
trace("Loading gamepad control scheme");
trace('Loading default gamepad control scheme');
controls.addDefaultGamepad(gamepad.id);
}
PreciseInputManager.instance.initializeButtons(controls, gamepad);
@ -135,12 +156,12 @@ class PlayerSettings
/**
* Save this player's controls to the game's persistent save.
*/
public function saveControls()
public function saveControls():Void
{
var keyData = controls.createSaveData(Keys);
if (keyData != null)
{
trace("saving key data: " + haxe.Json.stringify(keyData));
trace('Saving keyboard control scheme to user save');
Save.instance.setControls(id, Keys, keyData);
}
@ -149,7 +170,7 @@ class PlayerSettings
var padData = controls.createSaveData(Gamepad(controls.gamepadsAdded[0]));
if (padData != null)
{
trace("saving pad data: " + haxe.Json.stringify(padData));
trace('Saving gamepad control scheme to user save');
Save.instance.setControls(id, Gamepad(controls.gamepadsAdded[0]), padData);
}
}

View File

@ -20,7 +20,7 @@ class Preferences
static function set_naughtyness(value:Bool):Bool
{
var save = Save.instance;
var save:Save = Save.instance;
save.options.naughtyness = value;
save.flush();
return value;
@ -39,7 +39,7 @@ class Preferences
static function set_downscroll(value:Bool):Bool
{
var save = Save.instance;
var save:Save = Save.instance;
save.options.downscroll = value;
save.flush();
return value;
@ -58,7 +58,7 @@ class Preferences
static function set_flashingLights(value:Bool):Bool
{
var save = Save.instance;
var save:Save = Save.instance;
save.options.flashingLights = value;
save.flush();
return value;
@ -77,7 +77,7 @@ class Preferences
static function set_zoomCamera(value:Bool):Bool
{
var save = Save.instance;
var save:Save = Save.instance;
save.options.zoomCamera = value;
save.flush();
return value;
@ -122,15 +122,20 @@ class Preferences
{
if (value != Save.instance.options.autoPause) FlxG.autoPause = value;
var save = Save.instance;
var save:Save = Save.instance;
save.options.autoPause = value;
save.flush();
return value;
}
/**
* Loads the user's preferences from the save data and apply them.
*/
public static function init():Void
{
// Apply the autoPause setting (enables automatic pausing on focus lost).
FlxG.autoPause = Preferences.autoPause;
// Apply the debugDisplay setting (enables the FPS and RAM display).
toggleDebugDisplay(Preferences.debugDisplay);
}

View File

@ -1,65 +0,0 @@
package funkin;
import flash.Lib;
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.display.BlendMode;
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,16 +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 funkin.data.song.SongData.SongMusicData;
import funkin.data.song.SongRegistry;
import flixel.tweens.FlxTween;
import flixel.util.FlxSignal.FlxTypedSignal;
import funkin.audio.waveform.WaveformData;
import funkin.audio.waveform.WaveformDataParser;
import flixel.math.FlxMath;
import funkin.data.song.SongData.SongMusicData;
import funkin.data.song.SongRegistry;
import funkin.util.tools.ICloneable;
import openfl.Assets;
import openfl.media.SoundMixer;
#if (openfl >= "8.0.0")
import openfl.utils.AssetType;
#end
@ -18,6 +20,7 @@ import openfl.utils.AssetType;
/**
* A FlxSound which adds additional functionality:
* - Delayed playback via negative song position.
* - Easy functions for immediate playback and recycling.
*/
@:nullSafety
class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
@ -48,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
@ -286,15 +294,28 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
* Creates a new `FunkinSound` object and loads it as the current music track.
*
* @param key The key of the music you want to play. Music should be at `music/<key>/<key>.ogg`.
* @param overrideExisting Whether to override music if it is already playing.
* @param mapTimeChanges Whether to check for `SongMusicData` to update the Conductor with.
* @param params A set of additional optional parameters.
* Data should be at `music/<key>/<key>-metadata.json`.
* @return Whether the music was started. `false` if music was already playing or could not be started
*/
public static function playMusic(key:String, overrideExisting:Bool = false, mapTimeChanges:Bool = true):Void
public static function playMusic(key:String, params:FunkinSoundPlayMusicParams):Bool
{
if (!overrideExisting && FlxG.sound.music?.playing) return;
if (!(params.overrideExisting ?? false) && (FlxG.sound.music?.exists ?? false) && FlxG.sound.music.playing) return false;
if (mapTimeChanges)
if (!(params.restartTrack ?? false) && FlxG.sound.music?.playing)
{
if (FlxG.sound.music != null && Std.isOfType(FlxG.sound.music, FunkinSound))
{
var existingSound:FunkinSound = cast FlxG.sound.music;
// Stop here if we would play a matching music track.
if (existingSound._label == Paths.music('$key/$key'))
{
return false;
}
}
}
if (params?.mapTimeChanges ?? true)
{
var songMusicData:Null<SongMusicData> = SongRegistry.instance.parseMusicData(key);
// Will fall back and return null if the metadata doesn't exist or can't be parsed.
@ -308,10 +329,27 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
}
}
FlxG.sound.music = FunkinSound.load(Paths.music('$key/$key'));
if (FlxG.sound.music != null)
{
FlxG.sound.music.fadeTween?.cancel();
FlxG.sound.music.stop();
FlxG.sound.music.kill();
}
// Prevent repeat update() and onFocus() calls.
FlxG.sound.list.remove(FlxG.sound.music);
var music = FunkinSound.load(Paths.music('$key/$key'), params?.startingVolume ?? 1.0, params.loop ?? true, false, true);
if (music != null)
{
FlxG.sound.music = music;
// Prevent repeat update() and onFocus() calls.
FlxG.sound.list.remove(FlxG.sound.music);
return true;
}
else
{
return false;
}
}
/**
@ -326,11 +364,18 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
* @param autoPlay Whether to play the sound immediately or wait for a `play()` call.
* @param onComplete Called when the sound finished playing.
* @param onLoad Called when the sound finished loading. Called immediately for succesfully loaded embedded sounds.
* @return A `FunkinSound` object.
* @return A `FunkinSound` object, or `null` if the sound could not be loaded.
*/
public static function load(embeddedSound:FlxSoundAsset, volume:Float = 1.0, looped:Bool = false, autoDestroy:Bool = false, autoPlay:Bool = false,
?onComplete:Void->Void, ?onLoad:Void->Void):FunkinSound
?onComplete:Void->Void, ?onLoad:Void->Void):Null<FunkinSound>
{
@:privateAccess
if (SoundMixer.__soundChannels.length >= SoundMixer.MAX_ACTIVE_CHANNELS)
{
FlxG.log.error('FunkinSound could not play sound, channels exhausted! Found ${SoundMixer.__soundChannels.length} active sound channels.');
return null;
}
var sound:FunkinSound = pool.recycle(construct);
// Load the sound.
@ -341,6 +386,10 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
{
sound._label = embeddedSound;
}
else
{
sound._label = 'unknown';
}
sound.volume = volume;
sound.group = FlxG.sound.defaultSoundGroup;
@ -350,11 +399,41 @@ 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;
}
public override function destroy():Void
{
// trace('[FunkinSound] Destroying sound "${this._label}"');
super.destroy();
FlxTween.cancelTweensOf(this);
this._label = 'unknown';
}
/**
* Play a sound effect once, then destroy it.
* @param key
* @param volume
* @return static function construct():FunkinSound
*/
public static function playOnce(key:String, volume:Float = 1.0, ?onComplete:Void->Void, ?onLoad:Void->Void):Void
{
var result = FunkinSound.load(key, volume, false, true, true, onComplete, onLoad);
}
/**
* Stop all sounds in the pool and allow them to be recycled.
*/
public static function stopAllAudio(musicToo:Bool = false):Void
{
for (sound in pool)
{
if (sound == null) continue;
if (!musicToo && sound == FlxG.sound.music) continue;
sound.destroy();
}
}
static function construct():FunkinSound
{
var sound:FunkinSound = new FunkinSound();
@ -365,3 +444,39 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
return sound;
}
}
/**
* Additional parameters for `FunkinSound.playMusic()`
*/
typedef FunkinSoundPlayMusicParams =
{
/**
* The volume you want the music to start at.
* @default `1.0`
*/
var ?startingVolume:Float;
/**
* Whether to override music if a different track is already playing.
* @default `false`
*/
var ?overrideExisting:Bool;
/**
* Whether to override music if the same track is already playing.
* @default `false`
*/
var ?restartTrack:Bool;
/**
* Whether the music should loop or play once.
* @default `true`
*/
var ?loop:Bool;
/**
* Whether to check for `SongMusicData` to update the Conductor with.
* @default `true`
*/
var ?mapTimeChanges:Bool;
}

View File

@ -1,7 +1,6 @@
package funkin.audio;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.sound.FlxSound;
import funkin.audio.FunkinSound;
import flixel.tweens.FlxTween;
@ -153,9 +152,12 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
*/
public function stop()
{
forEachAlive(function(sound:FunkinSound) {
sound.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

@ -55,6 +55,13 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
this.entries = new Map<String, T>();
this.scriptedEntryIds = [];
// Lazy initialization of singletons should let this get called,
// but we have this check just in case.
if (FlxG.game != null)
{
FlxG.console.registerObject('registry$registryId', this);
}
}
/**

View File

@ -15,7 +15,14 @@ class ConversationRegistry extends BaseRegistry<Conversation, ConversationData>
public static final CONVERSATION_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x";
public static final instance:ConversationRegistry = new ConversationRegistry();
public static var instance(get, never):ConversationRegistry;
static var _instance:Null<ConversationRegistry> = null;
static function get_instance():ConversationRegistry
{
if (_instance == null) _instance = new ConversationRegistry();
return _instance;
}
public function new()
{

View File

@ -15,7 +15,14 @@ class DialogueBoxRegistry extends BaseRegistry<DialogueBox, DialogueBoxData>
public static final DIALOGUEBOX_DATA_VERSION_RULE:thx.semver.VersionRule = "1.1.x";
public static final instance:DialogueBoxRegistry = new DialogueBoxRegistry();
public static var instance(get, never):DialogueBoxRegistry;
static var _instance:Null<DialogueBoxRegistry> = null;
static function get_instance():DialogueBoxRegistry
{
if (_instance == null) _instance = new DialogueBoxRegistry();
return _instance;
}
public function new()
{

View File

@ -15,7 +15,14 @@ class SpeakerRegistry extends BaseRegistry<Speaker, SpeakerData>
public static final SPEAKER_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x";
public static final instance:SpeakerRegistry = new SpeakerRegistry();
public static var instance(get, never):SpeakerRegistry;
static var _instance:Null<SpeakerRegistry> = null;
static function get_instance():SpeakerRegistry
{
if (_instance == null) _instance = new SpeakerRegistry();
return _instance;
}
public function new()
{

View File

@ -0,0 +1,36 @@
package funkin.data.freeplay;
/**
* A type definition for the data for an album of songs.
* It includes things like what graphics to display in Freeplay.
* @see https://lib.haxe.org/p/json2object/
*/
typedef AlbumData =
{
/**
* Semantic version for album data.
*/
public var version:String;
/**
* Readable name of the album.
*/
public var name:String;
/**
* Readable name of the artist(s) of the album.
*/
public var artists:Array<String>;
/**
* Asset key for the album art.
* The album art will be displayed in Freeplay.
*/
public var albumArtAsset:String;
/**
* Asset key for the album title.
* The album title will be displayed below the album art in Freeplay.
*/
public var albumTitleAsset:String;
}

View File

@ -0,0 +1,84 @@
package funkin.data.freeplay;
import funkin.ui.freeplay.Album;
import funkin.data.freeplay.AlbumData;
import funkin.ui.freeplay.ScriptedAlbum;
class AlbumRegistry extends BaseRegistry<Album, AlbumData>
{
/**
* The current version string for the album data format.
* Handle breaking changes by incrementing this value
* and adding migration to the `migrateAlbumData()` function.
*/
public static final ALBUM_DATA_VERSION:thx.semver.Version = '1.0.0';
public static final ALBUM_DATA_VERSION_RULE:thx.semver.VersionRule = '1.0.x';
public static final instance:AlbumRegistry = new AlbumRegistry();
public function new()
{
super('ALBUM', 'ui/freeplay/albums', ALBUM_DATA_VERSION_RULE);
}
/**
* Read, parse, and validate the JSON data and produce the corresponding data object.
* @param id The ID of the entry to load.
* @return The parsed data object.
*/
public function parseEntryData(id:String):Null<AlbumData>
{
// JsonParser does not take type parameters,
// otherwise this function would be in BaseRegistry.
var parser:json2object.JsonParser<AlbumData> = new json2object.JsonParser<AlbumData>();
parser.ignoreUnknownVariables = false;
switch (loadEntryFile(id))
{
case {fileName: fileName, contents: contents}:
parser.fromJson(contents, fileName);
default:
return null;
}
if (parser.errors.length > 0)
{
printErrors(parser.errors, id);
return null;
}
return parser.value;
}
/**
* Parse and validate the JSON data and produce the corresponding data object.
*
* NOTE: Must be implemented on the implementation class.
* @param contents The JSON as a string.
* @param fileName An optional file name for error reporting.
* @return The parsed data object.
*/
public function parseEntryDataRaw(contents:String, ?fileName:String):Null<AlbumData>
{
var parser:json2object.JsonParser<AlbumData> = new json2object.JsonParser<AlbumData>();
parser.ignoreUnknownVariables = false;
parser.fromJson(contents, fileName);
if (parser.errors.length > 0)
{
printErrors(parser.errors, fileName);
return null;
}
return parser.value;
}
function createScriptedEntry(clsName:String):Album
{
return ScriptedAlbum.init(clsName, 'unknown');
}
function getScriptedClassNames():Array<String>
{
return ScriptedAlbum.listScriptClasses();
}
}

View File

@ -15,7 +15,14 @@ class LevelRegistry extends BaseRegistry<Level, LevelData>
public static final LEVEL_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x";
public static final instance:LevelRegistry = new LevelRegistry();
public static var instance(get, never):LevelRegistry;
static var _instance:Null<LevelRegistry> = null;
static function get_instance():LevelRegistry
{
if (_instance == null) _instance = new LevelRegistry();
return _instance;
}
public function new()
{

View File

@ -15,7 +15,14 @@ class NoteStyleRegistry extends BaseRegistry<NoteStyle, NoteStyleData>
public static final NOTE_STYLE_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x";
public static final instance:NoteStyleRegistry = new NoteStyleRegistry();
public static var instance(get, never):NoteStyleRegistry;
static var _instance:Null<NoteStyleRegistry> = null;
static function get_instance():NoteStyleRegistry
{
if (_instance == null) _instance = new NoteStyleRegistry();
return _instance;
}
public function new()
{

View File

@ -706,7 +706,7 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
this = new SongEventDataRaw(time, eventKind, value);
}
public inline function valueAsStruct(?defaultKey:String = "key"):Dynamic
public function valueAsStruct(?defaultKey:String = "key"):Dynamic
{
if (this.value == null) return {};
if (Std.isOfType(this.value, Array))

View File

@ -40,10 +40,17 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
}
/**
* TODO: What if there was a Singleton macro which created static functions
* that redirected to the instance?
* TODO: What if there was a Singleton macro which automatically created the property for us?
*/
public static final instance:SongRegistry = new SongRegistry();
public static var instance(get, never):SongRegistry;
static var _instance:Null<SongRegistry> = null;
static function get_instance():SongRegistry
{
if (_instance == null) _instance = new SongRegistry();
return _instance;
}
public function new()
{
@ -424,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

@ -15,7 +15,14 @@ class StageRegistry extends BaseRegistry<Stage, StageData>
public static final STAGE_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x";
public static final instance:StageRegistry = new StageRegistry();
public static var instance(get, never):StageRegistry;
static var _instance:Null<StageRegistry> = null;
static function get_instance():StageRegistry
{
if (_instance == null) _instance = new StageRegistry();
return _instance;
}
public function new()
{

View File

@ -3,6 +3,7 @@ package funkin.graphics;
import flixel.FlxSprite;
import flixel.util.FlxColor;
import flixel.graphics.FlxGraphic;
import flixel.tweens.FlxTween;
import openfl.display3D.textures.TextureBase;
import funkin.graphics.framebuffer.FixedBitmapData;
import openfl.display.BitmapData;
@ -253,7 +254,7 @@ class FunkinSprite extends FlxSprite
}
/**
* Ensure scale is applied when cloning a sprite.
* Ensure scale is applied when cloning a sprite.R
* The default `clone()` method acts kinda weird TBH.
* @return A clone of this sprite.
*/
@ -266,4 +267,13 @@ class FunkinSprite extends FlxSprite
return result;
}
public override function destroy():Void
{
frames = null;
// Cancel all tweens so they don't continue to run on a destroyed sprite.
// This prevents crashes.
FlxTween.cancelTweensOf(this);
super.destroy();
}
}

View File

@ -3,7 +3,9 @@ package funkin.graphics.adobeanimate;
import flixel.util.FlxSignal.FlxTypedSignal;
import flxanimate.FlxAnimate;
import flxanimate.FlxAnimate.Settings;
import flixel.math.FlxPoint;
import flxanimate.frames.FlxAnimateFrames;
import openfl.display.BitmapData;
import openfl.utils.Assets;
/**
* A sprite which provides convenience functions for rendering a texture atlas with animations.
@ -31,7 +33,7 @@ class FlxAtlasSprite extends FlxAnimate
var canPlayOtherAnims:Bool = true;
public function new(x:Float, y:Float, path:String, ?settings:Settings)
public function new(x:Float, y:Float, ?path:String, ?settings:Settings)
{
if (settings == null) settings = SETTINGS;
@ -77,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

@ -8,6 +8,7 @@ import funkin.data.level.LevelRegistry;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.data.song.SongRegistry;
import funkin.data.stage.StageRegistry;
import funkin.data.freeplay.AlbumRegistry;
import funkin.modding.module.ModuleHandler;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.save.Save;
@ -208,7 +209,6 @@ class PolymodHandler
// Add import aliases for certain classes.
// NOTE: Scripted classes are automatically aliased to their parent class.
Polymod.addImportAlias('flixel.math.FlxPoint', flixel.math.FlxPoint.FlxBasePoint);
Polymod.addImportAlias('flixel.system.FlxSound', flixel.sound.FlxSound);
// Add blacklisting for prohibited classes and packages.
// `polymod.*`
@ -324,6 +324,7 @@ class PolymodHandler
ConversationRegistry.instance.loadEntries();
DialogueBoxRegistry.instance.loadEntries();
SpeakerRegistry.instance.loadEntries();
AlbumRegistry.instance.loadEntries();
StageRegistry.instance.loadEntries();
CharacterDataParser.loadCharacterCache(); // TODO: Migrate characters to BaseRegistry.
ModuleHandler.loadModuleCache();

View File

@ -9,6 +9,7 @@ import funkin.modding.module.ModuleHandler;
import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEvent.CountdownScriptEvent;
import flixel.util.FlxTimer;
import funkin.audio.FunkinSound;
class Countdown
{
@ -282,7 +283,7 @@ class Countdown
if (soundPath == null) return;
FlxG.sound.play(Paths.sound(soundPath), Constants.COUNTDOWN_VOLUME);
FunkinSound.playOnce(Paths.sound(soundPath), Constants.COUNTDOWN_VOLUME);
}
public static function decrement(step:CountdownStep):CountdownStep

View File

@ -3,7 +3,6 @@ package funkin.play;
import flixel.FlxG;
import flixel.FlxObject;
import flixel.FlxSprite;
import flixel.sound.FlxSound;
import funkin.audio.FunkinSound;
import flixel.util.FlxColor;
import flixel.util.FlxTimer;
@ -97,14 +96,14 @@ class GameOverSubState extends MusicBeatSubState
/**
* Reset the game over configuration to the default.
*/
public static function reset()
public static function reset():Void
{
animationSuffix = "";
musicSuffix = "";
blueBallSuffix = "";
animationSuffix = '';
musicSuffix = '';
blueBallSuffix = '';
}
override public function create()
override public function create():Void
{
if (instance != null)
{
@ -119,6 +118,8 @@ class GameOverSubState extends MusicBeatSubState
// Set up the visuals
//
var playState = PlayState.instance;
// Add a black background to the screen.
var bg = new FunkinSprite().makeSolidColor(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK);
// We make this transparent so that we can see the stage underneath during debugging,
@ -130,23 +131,27 @@ class GameOverSubState extends MusicBeatSubState
// Pluck Boyfriend from the PlayState and place him (in the same position) in the GameOverSubState.
// We can then play the character's `firstDeath` animation.
boyfriend = PlayState.instance.currentStage.getBoyfriend(true);
boyfriend.isDead = true;
add(boyfriend);
boyfriend.resetCharacter();
if (PlayState.instance.isMinimalMode) {}
else
{
boyfriend = PlayState.instance.currentStage.getBoyfriend(true);
boyfriend.isDead = true;
add(boyfriend);
boyfriend.resetCharacter();
// Assign a camera follow point to the boyfriend's position.
cameraFollowPoint = new FlxObject(PlayState.instance.cameraFollowPoint.x, PlayState.instance.cameraFollowPoint.y, 1, 1);
cameraFollowPoint.x = boyfriend.getGraphicMidpoint().x;
cameraFollowPoint.y = boyfriend.getGraphicMidpoint().y;
var offsets:Array<Float> = boyfriend.getDeathCameraOffsets();
cameraFollowPoint.x += offsets[0];
cameraFollowPoint.y += offsets[1];
add(cameraFollowPoint);
// Assign a camera follow point to the boyfriend's position.
cameraFollowPoint = new FlxObject(PlayState.instance.cameraFollowPoint.x, PlayState.instance.cameraFollowPoint.y, 1, 1);
cameraFollowPoint.x = boyfriend.getGraphicMidpoint().x;
cameraFollowPoint.y = boyfriend.getGraphicMidpoint().y;
var offsets:Array<Float> = boyfriend.getDeathCameraOffsets();
cameraFollowPoint.x += offsets[0];
cameraFollowPoint.y += offsets[1];
add(cameraFollowPoint);
FlxG.camera.target = null;
FlxG.camera.follow(cameraFollowPoint, LOCKON, 0.01);
targetCameraZoom = PlayState?.instance?.currentStage?.camZoom * boyfriend.getDeathCameraZoom();
FlxG.camera.target = null;
FlxG.camera.follow(cameraFollowPoint, LOCKON, 0.01);
targetCameraZoom = PlayState?.instance?.currentStage?.camZoom * boyfriend.getDeathCameraZoom();
}
//
// Set up the audio
@ -164,21 +169,29 @@ class GameOverSubState extends MusicBeatSubState
var hasStartedAnimation:Bool = false;
override function update(elapsed:Float)
override function update(elapsed:Float):Void
{
if (!hasStartedAnimation)
{
hasStartedAnimation = true;
if (boyfriend.hasAnimation('fakeoutDeath') && FlxG.random.bool((1 / 4096) * 100))
if (PlayState.instance.isMinimalMode)
{
boyfriend.playAnimation('fakeoutDeath', true, false);
// Play the "blue balled" sound. May play a variant if one has been assigned.
playBlueBalledSFX();
}
else
{
boyfriend.playAnimation('firstDeath', true, false); // ignoreOther is set to FALSE since you WANT to be able to mash and confirm game over!
// Play the "blue balled" sound. May play a variant if one has been assigned.
playBlueBalledSFX();
if (boyfriend.hasAnimation('fakeoutDeath') && FlxG.random.bool((1 / 4096) * 100))
{
boyfriend.playAnimation('fakeoutDeath', true, false);
}
else
{
boyfriend.playAnimation('firstDeath', true, false); // ignoreOther is set to FALSE since you WANT to be able to mash and confirm game over!
// Play the "blue balled" sound. May play a variant if one has been assigned.
playBlueBalledSFX();
}
}
}
@ -241,27 +254,34 @@ class GameOverSubState extends MusicBeatSubState
}
else
{
// Music hasn't started yet.
switch (PlayStatePlaylist.campaignId)
if (PlayState.instance.isMinimalMode)
{
// TODO: Make the behavior for playing Jeff's voicelines generic or un-hardcoded.
// This will simplify the class and make it easier for mods to add death quotes.
case "week7":
if (boyfriend.getCurrentAnimation().startsWith('firstDeath') && boyfriend.isAnimationFinished() && !playingJeffQuote)
{
playingJeffQuote = true;
playJeffQuote();
// Start music at lower volume
startDeathMusic(0.2, false);
boyfriend.playAnimation('deathLoop' + animationSuffix);
}
default:
// Start music at normal volume once the initial death animation finishes.
if (boyfriend.getCurrentAnimation().startsWith('firstDeath') && boyfriend.isAnimationFinished())
{
startDeathMusic(1.0, false);
boyfriend.playAnimation('deathLoop' + animationSuffix);
}
// startDeathMusic(1.0, false);
}
else
{
// Music hasn't started yet.
switch (PlayStatePlaylist.campaignId)
{
// TODO: Make the behavior for playing Jeff's voicelines generic or un-hardcoded.
// This will simplify the class and make it easier for mods to add death quotes.
case 'week7':
if (boyfriend.getCurrentAnimation().startsWith('firstDeath') && boyfriend.isAnimationFinished() && !playingJeffQuote)
{
playingJeffQuote = true;
playJeffQuote();
// Start music at lower volume
startDeathMusic(0.2, false);
boyfriend.playAnimation('deathLoop' + animationSuffix);
}
default:
// Start music at normal volume once the initial death animation finishes.
if (boyfriend.getCurrentAnimation().startsWith('firstDeath') && boyfriend.isAnimationFinished())
{
startDeathMusic(1.0, false);
boyfriend.playAnimation('deathLoop' + animationSuffix);
}
}
}
}
@ -279,7 +299,11 @@ class GameOverSubState extends MusicBeatSubState
isEnding = true;
startDeathMusic(1.0, true); // isEnding changes this function's behavior.
boyfriend.playAnimation('deathConfirm' + animationSuffix, true);
if (PlayState.instance.isMinimalMode) {}
else
{
boyfriend.playAnimation('deathConfirm' + animationSuffix, true);
}
// After the animation finishes...
new FlxTimer().start(0.7, function(tmr:FlxTimer) {
@ -289,10 +313,14 @@ class GameOverSubState extends MusicBeatSubState
FlxG.camera.fade(FlxColor.BLACK, 1, true, null, true);
PlayState.instance.needsReset = true;
// Readd Boyfriend to the stage.
boyfriend.isDead = false;
remove(boyfriend);
PlayState.instance.currentStage.addCharacter(boyfriend, BF);
if (PlayState.instance.isMinimalMode) {}
else
{
// Readd Boyfriend to the stage.
boyfriend.isDead = false;
remove(boyfriend);
PlayState.instance.currentStage.addCharacter(boyfriend, BF);
}
// Snap reset the camera which may have changed because of the player character data.
resetCameraZoom();
@ -389,7 +417,7 @@ class GameOverSubState extends MusicBeatSubState
blueballed = true;
if (Assets.exists(Paths.sound('gameplay/gameover/fnf_loss_sfx' + blueBallSuffix)))
{
FlxG.sound.play(Paths.sound('gameplay/gameover/fnf_loss_sfx' + blueBallSuffix));
FunkinSound.playOnce(Paths.sound('gameplay/gameover/fnf_loss_sfx' + blueBallSuffix));
}
else
{
@ -409,7 +437,7 @@ class GameOverSubState extends MusicBeatSubState
if (!Preferences.naughtyness) 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() {
FunkinSound.playOnce(Paths.sound('jeffGameover/jeffGameover-' + FlxG.random.int(1, 25, randomCensor)), function() {
// Once the quote ends, fade in the game over music.
if (!isEnding && gameOverMusic != null)
{

View File

@ -26,7 +26,11 @@ class GitarooPause extends MusicBeatState
override function create():Void
{
if (FlxG.sound.music != null) FlxG.sound.music.stop();
if (FlxG.sound.music != null)
{
FlxG.sound.music.destroy();
FlxG.sound.music = null;
}
var bg:FunkinSprite = FunkinSprite.create('pauseAlt/pauseBG');
add(bg);

View File

@ -366,7 +366,7 @@ class PauseSubState extends MusicBeatSubState
*/
function changeSelection(change:Int = 0):Void
{
FlxG.sound.play(Paths.sound('scrollMenu'), 0.4);
FunkinSound.playOnce(Paths.sound('scrollMenu'), 0.4);
currentEntry += change;

View File

@ -1,24 +1,15 @@
package funkin.play;
import funkin.audio.FunkinSound;
import flixel.addons.display.FlxPieDial;
import flixel.addons.display.FlxPieDial;
import flixel.addons.transition.FlxTransitionableState;
import flixel.addons.transition.FlxTransitionableState;
import flixel.addons.transition.FlxTransitionableSubState;
import flixel.addons.transition.FlxTransitionableSubState;
import flixel.addons.transition.Transition;
import flixel.addons.transition.Transition;
import flixel.FlxCamera;
import flixel.FlxObject;
import flixel.FlxState;
import funkin.graphics.FunkinSprite;
import flixel.FlxSubState;
import funkin.graphics.FunkinSprite;
import flixel.math.FlxMath;
import flixel.math.FlxPoint;
import flixel.math.FlxRect;
import funkin.graphics.FunkinSprite;
import flixel.text.FlxText;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
@ -26,18 +17,19 @@ import flixel.ui.FlxBar;
import flixel.util.FlxColor;
import flixel.util.FlxTimer;
import funkin.api.newgrounds.NGio;
import funkin.audio.VoicesGroup;
import funkin.audio.FunkinSound;
import funkin.audio.VoicesGroup;
import funkin.data.dialogue.ConversationRegistry;
import funkin.data.event.SongEventRegistry;
import funkin.data.notestyle.NoteStyleData;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.data.song.SongData.SongCharacterData;
import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongRegistry;
import funkin.data.stage.StageRegistry;
import funkin.graphics.FunkinCamera;
import funkin.graphics.FunkinSprite;
import funkin.Highscore.Tallies;
import funkin.input.PreciseInputManager;
import funkin.modding.events.ScriptEvent;
@ -48,14 +40,11 @@ import funkin.play.components.ComboMilestone;
import funkin.play.components.HealthIcon;
import funkin.play.components.PopUpStuff;
import funkin.play.cutscene.dialogue.Conversation;
import funkin.play.cutscene.dialogue.Conversation;
import funkin.play.cutscene.VanillaCutscenes;
import funkin.play.cutscene.VideoCutscene;
import funkin.play.notes.NoteDirection;
import funkin.play.notes.NoteSplash;
import funkin.play.notes.NoteSprite;
import funkin.play.notes.NoteSprite;
import funkin.play.notes.notestyle.NoteStyle;
import funkin.play.notes.notestyle.NoteStyle;
import funkin.play.notes.Strumline;
import funkin.play.notes.SustainTrail;
@ -69,7 +58,6 @@ import funkin.ui.mainmenu.MainMenuState;
import funkin.ui.MusicBeatSubState;
import funkin.ui.options.PreferencesMenu;
import funkin.ui.story.StoryMenuState;
import funkin.graphics.FunkinCamera;
import funkin.ui.transition.LoadingState;
import funkin.util.SerializerUtil;
import haxe.Int64;
@ -237,6 +225,17 @@ class PlayState extends MusicBeatSubState
*/
public var cameraFollowPoint:FlxObject;
/**
* An FlxTween that tweens the camera to the follow point.
* Only used when tweening the camera manually, rather than tweening via follow.
*/
public var cameraFollowTween:FlxTween;
/**
* An FlxTween that zooms the camera to the desired amount.
*/
public var cameraZoomTween:FlxTween;
/**
* The camera follow point from the last stage.
* Used to persist the position of the `cameraFollowPosition` between levels.
@ -244,14 +243,23 @@ class PlayState extends MusicBeatSubState
public var previousCameraFollowPoint:FlxPoint = null;
/**
* The current camera zoom level.
*
* The camera zoom is increased every beat, and lerped back to this value every frame, creating a smooth 'zoom-in' effect.
* Defaults to 1.05 but may be larger or smaller depending on the current stage,
* and may be changed by the `ZoomCamera` song event.
* The current camera zoom level without any modifiers applied.
*/
public var currentCameraZoom:Float = FlxCamera.defaultZoom * 1.05;
/**
* currentCameraZoom is increased every beat, and lerped back to this value every frame, creating a smooth 'zoom-in' effect.
* Defaults to 1.05, but may be larger or smaller depending on the current stage.
* Tweened via the `ZoomCamera` song event in direct mode.
*/
public var defaultCameraZoom:Float = FlxCamera.defaultZoom * 1.05;
/**
* Camera zoom applied on top of currentCameraZoom.
* Tweened via the `ZoomCamera` song event in additive mode.
*/
public var additiveCameraZoom:Float = 0;
/**
* The current HUD camera zoom level.
*
@ -397,10 +405,15 @@ class PlayState extends MusicBeatSubState
var startingSong:Bool = false;
/**
* False if `FlxG.sound.music`
* Track if we currently have the music paused for a Pause substate, so we can unpause it when we return.
*/
var musicPausedBySubState:Bool = false;
/**
* Track any camera tweens we've paused for a Pause substate, so we can unpause them when we return.
*/
var cameraTweensPausedBySubState:List<FlxTween> = new List<FlxTween>();
/**
* False until `create()` has completed.
*/
@ -923,8 +936,8 @@ class PlayState extends MusicBeatSubState
var pauseSubState:FlxSubState = new PauseSubState({mode: isChartingMode ? Charting : Standard});
FlxTransitionableSubState.skipNextTransIn = true;
FlxTransitionableSubState.skipNextTransOut = true;
FlxTransitionableState.skipNextTransIn = true;
FlxTransitionableState.skipNextTransOut = true;
pauseSubState.camera = camHUD;
openSubState(pauseSubState);
// boyfriendPos.put(); // TODO: Why is this here?
@ -943,7 +956,9 @@ class PlayState extends MusicBeatSubState
// Lerp the camera zoom towards the target level.
if (subState == null)
{
FlxG.camera.zoom = FlxMath.lerp(defaultCameraZoom, FlxG.camera.zoom, 0.95);
currentCameraZoom = FlxMath.lerp(defaultCameraZoom, currentCameraZoom, 0.95);
FlxG.camera.zoom = currentCameraZoom + additiveCameraZoom;
camHUD.zoom = FlxMath.lerp(defaultHUDCameraZoom, camHUD.zoom, 0.95);
}
@ -1045,8 +1060,8 @@ class PlayState extends MusicBeatSubState
isChartingMode: isChartingMode,
transparent: persistentDraw
});
FlxTransitionableSubState.skipNextTransIn = true;
FlxTransitionableSubState.skipNextTransOut = true;
FlxTransitionableState.skipNextTransIn = true;
FlxTransitionableState.skipNextTransOut = true;
openSubState(gameOverSubState);
}
@ -1121,14 +1136,30 @@ class PlayState extends MusicBeatSubState
// Pause the music.
if (FlxG.sound.music != null)
{
musicPausedBySubState = FlxG.sound.music.playing;
if (musicPausedBySubState)
if (FlxG.sound.music.playing)
{
FlxG.sound.music.pause();
musicPausedBySubState = true;
}
// Pause vocals.
// Not tracking that we've done this via a bool because vocal re-syncing involves pausing the vocals anyway.
if (vocals != null) vocals.pause();
}
// Pause camera tweening, and keep track of which tweens we pause.
if (cameraFollowTween != null && cameraFollowTween.active)
{
cameraFollowTween.active = false;
cameraTweensPausedBySubState.add(cameraFollowTween);
}
if (cameraZoomTween != null && cameraZoomTween.active)
{
cameraZoomTween.active = false;
cameraTweensPausedBySubState.add(cameraZoomTween);
}
// Pause the countdown.
Countdown.pauseCountdown();
}
@ -1150,17 +1181,26 @@ class PlayState extends MusicBeatSubState
if (event.eventCanceled) return;
// Resume
// Resume music if we paused it.
if (musicPausedBySubState)
{
FlxG.sound.music.play();
musicPausedBySubState = false;
}
// Resume camera tweens if we paused any.
for (camTween in cameraTweensPausedBySubState)
{
camTween.active = true;
}
cameraTweensPausedBySubState.clear();
if (currentConversation != null)
{
currentConversation.resumeMusic();
}
// Re-sync vocals.
if (FlxG.sound.music != null && !startingSong && !isInCutscene) resyncVocals();
// Resume the countdown.
@ -1245,16 +1285,35 @@ class PlayState extends MusicBeatSubState
currentStage = null;
}
// Stop the instrumental.
if (FlxG.sound.music != null)
if (!overrideMusic)
{
FlxG.sound.music.stop();
}
// Stop the instrumental.
if (FlxG.sound.music != null)
{
FlxG.sound.music.destroy();
FlxG.sound.music = null;
}
// Stop the vocals.
if (vocals != null && vocals.exists)
// Stop the vocals.
if (vocals != null && vocals.exists)
{
vocals.destroy();
vocals = null;
}
}
else
{
vocals.stop();
// Stop the instrumental.
if (FlxG.sound.music != null)
{
FlxG.sound.music.stop();
}
// Stop the vocals.
if (vocals != null && vocals.exists)
{
vocals.stop();
}
}
super.debug_refreshModules();
@ -1305,10 +1364,13 @@ class PlayState extends MusicBeatSubState
}
// Only zoom camera if we are zoomed by less than 35%.
if (FlxG.camera.zoom < (1.35 * defaultCameraZoom) && cameraZoomRate > 0 && Conductor.instance.currentBeat % cameraZoomRate == 0)
if (Preferences.zoomCamera
&& FlxG.camera.zoom < (1.35 * defaultCameraZoom)
&& cameraZoomRate > 0
&& Conductor.instance.currentBeat % cameraZoomRate == 0)
{
// Zoom camera in (1.5%)
FlxG.camera.zoom += cameraZoomIntensity * defaultCameraZoom;
currentCameraZoom += cameraZoomIntensity * defaultCameraZoom;
// Hud zooms double (3%)
camHUD.zoom += hudCameraZoomIntensity * defaultHUDCameraZoom;
}
@ -1498,8 +1560,14 @@ class PlayState extends MusicBeatSubState
public function resetCameraZoom():Void
{
if (PlayState.instance.isMinimalMode) return;
// Apply camera zoom level from stage data.
defaultCameraZoom = currentStage.camZoom;
currentCameraZoom = defaultCameraZoom;
FlxG.camera.zoom = currentCameraZoom;
// Reset additive zoom.
additiveCameraZoom = 0;
}
/**
@ -1843,19 +1911,26 @@ class PlayState extends MusicBeatSubState
currentChart.playInst(1.0, false);
}
if (FlxG.sound.music == null)
{
FlxG.log.error('PlayState failed to initialize instrumental!');
return;
}
FlxG.sound.music.onComplete = endSong.bind(false);
// A negative instrumental offset means the song skips the first few milliseconds of the track.
// This just gets added into the startTimestamp behavior so we don't need to do anything extra.
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;
if (FlxG.sound.music.fadeTween != null) FlxG.sound.music.fadeTween.cancel();
trace('Playing vocals...');
add(vocals);
vocals.play();
vocals.volume = 1.0;
vocals.pitch = playbackRate;
resyncVocals();
@ -2034,8 +2109,7 @@ class PlayState extends MusicBeatSubState
holdNote.handledMiss = true;
// We dropped a hold note.
// Mute vocals and play miss animation, but don't penalize.
vocals.opponentVolume = 0;
// Play miss animation, but don't penalize.
currentStage.getOpponent().playSingAnimation(holdNote.noteData.getDirection(), true);
}
}
@ -2373,7 +2447,7 @@ class PlayState extends MusicBeatSubState
if (playSound)
{
vocals.playerVolume = 0;
FlxG.sound.play(Paths.soundRandom('missnote', 1, 3), FlxG.random.float(0.1, 0.2));
FunkinSound.playOnce(Paths.soundRandom('missnote', 1, 3), FlxG.random.float(0.5, 0.6));
}
}
@ -2428,7 +2502,7 @@ class PlayState extends MusicBeatSubState
if (event.playSound)
{
vocals.playerVolume = 0;
FlxG.sound.play(Paths.soundRandom('missnote', 1, 3), FlxG.random.float(0.1, 0.2));
FunkinSound.playOnce(Paths.soundRandom('missnote', 1, 3), FlxG.random.float(0.1, 0.2));
}
}
@ -2609,8 +2683,8 @@ class PlayState extends MusicBeatSubState
var pauseSubState:FlxSubState = new PauseSubState({mode: Conversation});
persistentUpdate = false;
FlxTransitionableSubState.skipNextTransIn = true;
FlxTransitionableSubState.skipNextTransOut = true;
FlxTransitionableState.skipNextTransIn = true;
FlxTransitionableState.skipNextTransOut = true;
pauseSubState.camera = camCutscene;
openSubState(pauseSubState);
}
@ -2625,8 +2699,8 @@ class PlayState extends MusicBeatSubState
var pauseSubState:FlxSubState = new PauseSubState({mode: Cutscene});
persistentUpdate = false;
FlxTransitionableSubState.skipNextTransIn = true;
FlxTransitionableSubState.skipNextTransOut = true;
FlxTransitionableState.skipNextTransIn = true;
FlxTransitionableState.skipNextTransOut = true;
pauseSubState.camera = camCutscene;
openSubState(pauseSubState);
}
@ -2711,7 +2785,11 @@ class PlayState extends MusicBeatSubState
if (targetSongId == null)
{
FunkinSound.playMusic('freakyMenu');
FunkinSound.playMusic('freakyMenu',
{
overrideExisting: true,
restartTrack: false
});
// transIn = FlxTransitionableState.defaultTransIn;
// transOut = FlxTransitionableState.defaultTransOut;
@ -2789,7 +2867,7 @@ class PlayState extends MusicBeatSubState
camHUD.visible = false;
isInCutscene = true;
FlxG.sound.play(Paths.sound('Lights_Shut_off'), function() {
FunkinSound.playOnce(Paths.sound('Lights_Shut_off'), function() {
// no camFollow so it centers on horror tree
var targetSong:Song = SongRegistry.instance.fetchEntry(targetSongId);
LoadingState.loadPlayState(
@ -2846,6 +2924,12 @@ class PlayState extends MusicBeatSubState
*/
function performCleanup():Void
{
// If the camera is being tweened, stop it.
cancelAllCameraTweens();
// Dispatch the destroy event.
dispatchEvent(new ScriptEvent(DESTROY, false));
if (currentConversation != null)
{
remove(currentConversation);
@ -2860,7 +2944,7 @@ class PlayState extends MusicBeatSubState
if (overrideMusic)
{
// Stop the music. Do NOT destroy it, something still references it!
FlxG.sound.music.pause();
if (FlxG.sound.music != null) FlxG.sound.music.pause();
if (vocals != null)
{
vocals.pause();
@ -2870,7 +2954,7 @@ class PlayState extends MusicBeatSubState
else
{
// Stop and destroy the music.
FlxG.sound.music.pause();
if (FlxG.sound.music != null) FlxG.sound.music.pause();
if (vocals != null)
{
vocals.destroy();
@ -2883,7 +2967,6 @@ class PlayState extends MusicBeatSubState
{
remove(currentStage);
currentStage.kill();
dispatchEvent(new ScriptEvent(DESTROY, false));
currentStage = null;
}
@ -2904,6 +2987,9 @@ class PlayState extends MusicBeatSubState
// Stop camera zooming on beat.
cameraZoomRate = 0;
// Cancel camera tweening if it's active.
cancelAllCameraTweens();
// If the opponent is GF, zoom in on the opponent.
// Else, if there is no GF, zoom in on BF.
// Else, zoom in on GF.
@ -2990,15 +3076,119 @@ class PlayState extends MusicBeatSubState
/**
* Resets the camera's zoom level and focus point.
*/
public function resetCamera():Void
public function resetCamera(?resetZoom:Bool = true, ?cancelTweens:Bool = true):Void
{
// Cancel camera tweens if any are active.
if (cancelTweens)
{
cancelAllCameraTweens();
}
FlxG.camera.follow(cameraFollowPoint, LOCKON, 0.04);
FlxG.camera.targetOffset.set();
FlxG.camera.zoom = defaultCameraZoom;
if (resetZoom)
{
resetCameraZoom();
}
// Snap the camera to the follow point immediately.
FlxG.camera.focusOn(cameraFollowPoint.getPosition());
}
/**
* Disables camera following and tweens the camera to the follow point manually.
*/
public function tweenCameraToFollowPoint(?duration:Float, ?ease:Null<Float->Float>):Void
{
// Cancel the current tween if it's active.
cancelCameraFollowTween();
if (duration == 0)
{
// Instant movement. Just reset the camera to force it to the follow point.
resetCamera(false, false);
}
else
{
// Disable camera following for the duration of the tween.
FlxG.camera.target = null;
// Follow tween! Caching it so we can cancel/pause it later if needed.
var followPos:FlxPoint = cameraFollowPoint.getPosition() - FlxPoint.weak(FlxG.camera.width * 0.5, FlxG.camera.height * 0.5);
cameraFollowTween = FlxTween.tween(FlxG.camera.scroll, {x: followPos.x, y: followPos.y}, duration,
{
ease: ease,
onComplete: function(_) {
resetCamera(false, false); // Re-enable camera following when the tween is complete.
}
});
}
}
public function cancelCameraFollowTween()
{
if (cameraFollowTween != null)
{
cameraFollowTween.cancel();
}
}
/**
* Tweens the camera zoom to the desired amount.
*/
public function tweenCameraZoom(?zoom:Float, ?duration:Float, ?directMode:Bool, ?ease:Null<Float->Float>):Void
{
// Cancel the current tween if it's active.
cancelCameraZoomTween();
var targetZoom = zoom * FlxCamera.defaultZoom;
if (directMode) // Direct mode: Tween defaultCameraZoom for basic "smooth" zooms.
{
if (duration == 0)
{
// Instant zoom. No tween needed.
defaultCameraZoom = targetZoom;
}
else
{
// Zoom tween! Caching it so we can cancel/pause it later if needed.
cameraZoomTween = FlxTween.tween(this, {defaultCameraZoom: targetZoom}, duration, {ease: ease});
}
}
else // Additive mode: Tween additiveCameraZoom for ease-based zooms.
{
if (duration == 0)
{
// Instant zoom. No tween needed.
additiveCameraZoom = targetZoom;
}
else
{
// Zoom tween! Caching it so we can cancel/pause it later if needed.
cameraZoomTween = FlxTween.tween(this, {additiveCameraZoom: targetZoom}, duration, {ease: ease});
}
}
}
public function cancelCameraZoomTween()
{
if (cameraZoomTween != null)
{
cameraZoomTween.cancel();
}
}
/**
* Cancel all active camera tweens simultaneously.
*/
public function cancelAllCameraTweens()
{
cancelCameraFollowTween();
cancelCameraZoomTween();
}
#if (debug || FORCE_DEBUG_VERSION)
/**
* Jumps forward or backward a number of sections in the song.

View File

@ -13,6 +13,7 @@ import flixel.text.FlxBitmapText;
import flixel.tweens.FlxEase;
import funkin.ui.freeplay.FreeplayState;
import flixel.tweens.FlxTween;
import funkin.audio.FunkinSound;
import flixel.util.FlxGradient;
import flixel.util.FlxTimer;
import funkin.graphics.shaders.LeftMaskShader;
@ -48,9 +49,13 @@ class ResultState extends MusicBeatSubState
else
resultsVariation = NORMAL;
var loops:Bool = resultsVariation != SHIT;
FlxG.sound.playMusic(Paths.music("results" + resultsVariation), 1, loops);
FunkinSound.playMusic('results$resultsVariation',
{
startingVolume: 1.0,
overrideExisting: true,
restartTrack: true,
loop: resultsVariation != SHIT
});
// TEMP-ish, just used to sorta "cache" the 3000x3000 image!
var cacheBullShit:FlxSprite = new FlxSprite().loadGraphic(Paths.image("resultScreen/soundSystem"));
@ -104,7 +109,7 @@ class ResultState extends MusicBeatSubState
add(gf);
var boyfriend:FlxSprite = FunkinSprite.createSparrow(640, -200, 'resultScreen/resultBoyfriendGOOD');
boyfriend.animation.addByPrefix("fall", "Boyfriend Good", 24, false);
boyfriend.animation.addByPrefix("fall", "Boyfriend Good Anim0", 24, false);
boyfriend.visible = false;
boyfriend.animation.finishCallback = function(_) {
boyfriend.animation.play('fall', true, false, 14);
@ -159,7 +164,7 @@ class ResultState extends MusicBeatSubState
add(blackTopBar);
var resultsAnim:FunkinSprite = FunkinSprite.createSparrow(-200, -10, "resultScreen/results");
resultsAnim.animation.addByPrefix("result", "results", 24, false);
resultsAnim.animation.addByPrefix("result", "results instance 1", 24, false);
resultsAnim.animation.play("result");
add(resultsAnim);
@ -348,9 +353,12 @@ class ResultState extends MusicBeatSubState
if (controls.PAUSE)
{
FlxTween.tween(FlxG.sound.music, {volume: 0}, 0.8);
FlxTween.tween(FlxG.sound.music, {pitch: 3}, 0.1, {onComplete: _ -> {
FlxTween.tween(FlxG.sound.music, {pitch: 0.5}, 0.4);
}});
FlxTween.tween(FlxG.sound.music, {pitch: 3}, 0.1,
{
onComplete: _ -> {
FlxTween.tween(FlxG.sound.music, {pitch: 0.5}, 0.4);
}
});
if (params.storyMode)
{
openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new StoryMenuState(sticker)));

View File

@ -76,10 +76,17 @@ class AnimateAtlasCharacter extends BaseCharacter
{
trace('Creating Animate Atlas character: ' + this.characterId);
var atlasSprite:FlxAtlasSprite = loadAtlasSprite();
setSprite(atlasSprite);
try
{
var atlasSprite:FlxAtlasSprite = loadAtlasSprite();
setSprite(atlasSprite);
loadAnimations();
loadAnimations();
}
catch (e)
{
throw "Exception thrown while building FlxAtlasSprite: " + e;
}
super.onCreate(event);
}

View File

@ -4,6 +4,7 @@ import flixel.FlxSprite;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import flixel.util.FlxTimer;
import funkin.audio.FunkinSound;
class ComboMilestone extends FlxTypedSpriteGroup<FlxSprite>
{
@ -78,7 +79,7 @@ class ComboMilestone extends FlxTypedSpriteGroup<FlxSprite>
function setupCombo(daCombo:Int)
{
FlxG.sound.play(Paths.sound('comboSound'));
FunkinSound.playOnce(Paths.sound('comboSound'));
wasComboSetup = true;
var loopNum:Int = 0;

View File

@ -4,6 +4,7 @@ import flixel.FlxSprite;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.util.FlxColor;
import funkin.audio.FunkinSound;
import flixel.util.FlxTimer;
/**
@ -40,7 +41,7 @@ class VanillaCutscenes
FlxG.camera.zoom = 2.5;
// Play the Sound effect.
FlxG.sound.play(Paths.sound('Lights_Turn_On'), function() {
FunkinSound.playOnce(Paths.sound('Lights_Turn_On'), function() {
// Fade in the HUD.
trace('SFX done...');
PlayState.instance.camHUD.visible = true;

View File

@ -5,6 +5,7 @@ import flixel.FlxSprite;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.util.FlxColor;
import flixel.util.FlxSignal;
import flixel.util.FlxTimer;
#if html5
import funkin.graphics.video.FlxVideo;
@ -28,6 +29,31 @@ class VideoCutscene
static var vid:FlxVideoSprite;
#end
/**
* Called when the video is started.
*/
public static final onVideoStarted:FlxSignal = new FlxSignal();
/**
* Called if the video is paused.
*/
public static final onVideoPaused:FlxSignal = new FlxSignal();
/**
* Called if the video is resumed.
*/
public static final onVideoResumed:FlxSignal = new FlxSignal();
/**
* Called if the video is restarted. onVideoStarted is not called.
*/
public static final onVideoRestarted:FlxSignal = new FlxSignal();
/**
* Called when the video is ended or skipped.
*/
public static final onVideoEnded:FlxSignal = new FlxSignal();
/**
* Play a video cutscene.
* TODO: Currently this is hardcoded to start the countdown after the video is done.
@ -94,6 +120,8 @@ class VideoCutscene
PlayState.instance.add(vid);
PlayState.instance.refresh();
onVideoStarted.dispatch();
}
else
{
@ -129,6 +157,8 @@ class VideoCutscene
vid.y = 0;
// vid.scale.set(0.5, 0.5);
});
onVideoStarted.dispatch();
}
else
{
@ -143,6 +173,7 @@ class VideoCutscene
if (vid != null)
{
vid.restartVideo();
onVideoRestarted.dispatch();
}
#end
@ -156,6 +187,8 @@ class VideoCutscene
// Resume the video if it was paused.
vid.resume();
}
onVideoRestarted.dispatch();
}
#end
}
@ -166,6 +199,7 @@ class VideoCutscene
if (vid != null)
{
vid.pauseVideo();
onVideoPaused.dispatch();
}
#end
@ -173,6 +207,45 @@ class VideoCutscene
if (vid != null)
{
vid.pause();
onVideoPaused.dispatch();
}
#end
}
public static function hideVideo():Void
{
#if html5
if (vid != null)
{
vid.visible = false;
blackScreen.visible = false;
}
#end
#if hxCodec
if (vid != null)
{
vid.visible = false;
blackScreen.visible = false;
}
#end
}
public static function showVideo():Void
{
#if html5
if (vid != null)
{
vid.visible = true;
blackScreen.visible = false;
}
#end
#if hxCodec
if (vid != null)
{
vid.visible = true;
blackScreen.visible = false;
}
#end
}
@ -183,6 +256,7 @@ class VideoCutscene
if (vid != null)
{
vid.resumeVideo();
onVideoResumed.dispatch();
}
#end
@ -190,6 +264,7 @@ class VideoCutscene
if (vid != null)
{
vid.resume();
onVideoResumed.dispatch();
}
#end
}
@ -240,6 +315,7 @@ class VideoCutscene
{
ease: FlxEase.quadInOut,
onComplete: function(twn:FlxTween) {
onVideoEnded.dispatch();
onCutsceneFinish(cutsceneType);
}
});

View File

@ -1,28 +1,28 @@
package funkin.play.cutscene.dialogue;
import funkin.data.IRegistryEntry;
import flixel.addons.display.FlxPieDial;
import flixel.FlxSprite;
import flixel.group.FlxSpriteGroup;
import flixel.util.FlxColor;
import funkin.graphics.FunkinSprite;
import flixel.tweens.FlxTween;
import flixel.tweens.FlxEase;
import flixel.sound.FlxSound;
import funkin.util.SortUtil;
import flixel.tweens.FlxTween;
import flixel.util.FlxColor;
import flixel.util.FlxSort;
import funkin.modding.events.ScriptEvent;
import funkin.modding.IScriptedClass.IEventHandler;
import funkin.play.cutscene.dialogue.DialogueBox;
import funkin.modding.IScriptedClass.IDialogueScriptedClass;
import funkin.modding.events.ScriptEventDispatcher;
import flixel.addons.display.FlxPieDial;
import funkin.audio.FunkinSound;
import funkin.data.dialogue.ConversationData;
import funkin.data.dialogue.ConversationData.DialogueEntryData;
import funkin.data.dialogue.ConversationRegistry;
import funkin.data.dialogue.SpeakerData;
import funkin.data.dialogue.SpeakerRegistry;
import funkin.data.dialogue.DialogueBoxData;
import funkin.data.dialogue.DialogueBoxRegistry;
import funkin.data.dialogue.SpeakerData;
import funkin.data.dialogue.SpeakerRegistry;
import funkin.data.IRegistryEntry;
import funkin.graphics.FunkinSprite;
import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.modding.IScriptedClass.IDialogueScriptedClass;
import funkin.modding.IScriptedClass.IEventHandler;
import funkin.play.cutscene.dialogue.DialogueBox;
import funkin.util.SortUtil;
/**
* A high-level handler for dialogue.
@ -90,7 +90,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
/**
* AUDIO
*/
var music:FlxSound;
var music:FunkinSound;
/**
* GRAPHICS
@ -129,8 +129,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
{
if (_data.music == null) return;
music = new FlxSound().loadEmbedded(Paths.music(_data.music.asset), true, true);
music.volume = 0;
music = FunkinSound.load(Paths.music(_data.music.asset), 0.0, true, true, true);
if (_data.music.fadeTime > 0.0)
{
@ -140,9 +139,6 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
{
music.volume = 1.0;
}
FlxG.sound.list.add(music);
music.play();
}
public function pauseMusic():Void

View File

@ -1,5 +1,6 @@
package funkin.play.event;
import flixel.tweens.FlxEase;
// Data from the chart
import funkin.data.song.SongData;
import funkin.data.song.SongData.SongEventData;
@ -69,6 +70,13 @@ class FocusCameraSongEvent extends SongEvent
if (char == null) char = cast data.value;
var useTween:Null<Bool> = data.getBool('useTween');
if (useTween == null) useTween = false;
var duration:Null<Float> = data.getFloat('duration');
if (duration == null) duration = 4.0;
var ease:Null<String> = data.getString('ease');
if (ease == null) ease = 'linear';
switch (char)
{
case -1: // Position
@ -117,6 +125,26 @@ class FocusCameraSongEvent extends SongEvent
default:
trace('Unknown camera focus: ' + data);
}
if (useTween)
{
switch (ease)
{
case 'INSTANT':
PlayState.instance.tweenCameraToFollowPoint(0);
default:
var durSeconds = Conductor.instance.stepLengthMs * duration / 1000;
var easeFunction:Null<Float->Float> = Reflect.field(FlxEase, ease);
if (easeFunction == null)
{
trace('Invalid ease function: $ease');
return;
}
PlayState.instance.tweenCameraToFollowPoint(durSeconds, easeFunction);
}
}
}
public override function getTitle():String
@ -158,6 +186,51 @@ class FocusCameraSongEvent extends SongEvent
step: 10.0,
type: SongEventFieldType.FLOAT,
units: "px"
},
{
name: 'useTween',
title: 'Use Tween',
type: SongEventFieldType.BOOL,
defaultValue: false
},
{
name: 'duration',
title: 'Duration',
defaultValue: 4.0,
step: 0.5,
type: SongEventFieldType.FLOAT,
units: 'steps'
},
{
name: 'ease',
title: 'Easing Type',
defaultValue: 'linear',
type: SongEventFieldType.ENUM,
keys: [
'Linear' => 'linear',
'Instant' => 'INSTANT',
'Quad In' => 'quadIn',
'Quad Out' => 'quadOut',
'Quad In/Out' => 'quadInOut',
'Cube In' => 'cubeIn',
'Cube Out' => 'cubeOut',
'Cube In/Out' => 'cubeInOut',
'Quart In' => 'quartIn',
'Quart Out' => 'quartOut',
'Quart In/Out' => 'quartInOut',
'Quint In' => 'quintIn',
'Quint Out' => 'quintOut',
'Quint In/Out' => 'quintInOut',
'Smooth Step In' => 'smoothStepIn',
'Smooth Step Out' => 'smoothStepOut',
'Smooth Step In/Out' => 'smoothStepInOut',
'Sine In' => 'sineIn',
'Sine Out' => 'sineOut',
'Sine In/Out' => 'sineInOut',
'Elastic In' => 'elasticIn',
'Elastic Out' => 'elasticOut',
'Elastic In/Out' => 'elasticInOut',
]
}
]);
}

View File

@ -52,6 +52,11 @@ class ZoomCameraSongEvent extends SongEvent
super('ZoomCamera');
}
static final DEFAULT_ZOOM:Float = 1.0;
static final DEFAULT_DURATION:Float = 4.0;
static final DEFAULT_MODE:String = 'direct';
static final DEFAULT_EASE:String = 'linear';
public override function handleEvent(data:SongEventData):Void
{
// Does nothing if there is no PlayState camera or stage.
@ -60,21 +65,23 @@ class ZoomCameraSongEvent extends SongEvent
// Does nothing if we are minimal mode.
if (PlayState.instance.isMinimalMode) return;
var zoom:Null<Float> = data.getFloat('zoom');
if (zoom == null) zoom = 1.0;
var duration:Null<Float> = data.getFloat('duration');
if (duration == null) duration = 4.0;
var zoom:Float = data.getFloat('zoom') ?? DEFAULT_ZOOM;
var ease:Null<String> = data.getString('ease');
if (ease == null) ease = 'linear';
var duration:Float = data.getFloat('duration') ?? DEFAULT_DURATION;
var mode:String = data.getString('mode') ?? DEFAULT_MODE;
var isDirectMode:Bool = mode == 'direct';
var ease:String = data.getString('ease') ?? DEFAULT_EASE;
// If it's a string, check the value.
switch (ease)
{
case 'INSTANT':
// Set the zoom. Use defaultCameraZoom to prevent breaking camera bops.
PlayState.instance.defaultCameraZoom = zoom * FlxCamera.defaultZoom;
PlayState.instance.tweenCameraZoom(zoom, 0, isDirectMode);
default:
var durSeconds = Conductor.instance.stepLengthMs * duration / 1000;
var easeFunction:Null<Float->Float> = Reflect.field(FlxEase, ease);
if (easeFunction == null)
{
@ -82,8 +89,7 @@ class ZoomCameraSongEvent extends SongEvent
return;
}
FlxTween.tween(PlayState.instance, {defaultCameraZoom: zoom * FlxCamera.defaultZoom}, (Conductor.instance.stepLengthMs * duration / 1000),
{ease: easeFunction});
PlayState.instance.tweenCameraZoom(zoom, durSeconds, isDirectMode, easeFunction);
}
}
@ -96,8 +102,9 @@ class ZoomCameraSongEvent extends SongEvent
* ```
* {
* 'zoom': FLOAT, // Target zoom level.
* 'duration': FLOAT, // Optional duration in steps
* 'ease': ENUM, // Optional easing function
* 'duration': FLOAT, // Optional duration in steps.
* 'mode': ENUM, // Whether to set additive zoom or direct zoom.
* 'ease': ENUM, // Optional easing function.
* }
* @return SongEventSchema
*/
@ -120,6 +127,13 @@ class ZoomCameraSongEvent extends SongEvent
type: SongEventFieldType.FLOAT,
units: 'steps'
},
{
name: 'mode',
title: 'Mode',
defaultValue: 'direct',
type: SongEventFieldType.ENUM,
keys: ['Additive' => 'additive', 'Direct' => 'direct']
},
{
name: 'ease',
title: 'Easing Type',

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

@ -139,7 +139,16 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
for (vari in _data.playData.songVariations)
{
var variMeta:Null<SongMetadata> = fetchVariationMetadata(id, vari);
if (variMeta != null) _metadata.set(variMeta.variation, variMeta);
if (variMeta != null)
{
_metadata.set(variMeta.variation, variMeta);
trace(' Loaded variation: $vari');
}
else
{
FlxG.log.warn('[SONG] Failed to load variation metadata (${id}:${vari}), is the path correct?');
trace(' FAILED to load variation: $vari');
}
}
}
@ -213,6 +222,26 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
return _metadata.values();
}
/**
* List the album IDs for each variation of the song.
* @return A map of variation IDs to album IDs.
*/
public function listAlbums():Map<String, String>
{
var result:Map<String, String> = new Map<String, String>();
for (difficultyId in difficulties.keys())
{
var meta:Null<SongDifficulty> = difficulties.get(difficultyId);
if (meta != null && meta.album != null)
{
result.set(difficultyId, meta.album);
}
}
return result;
}
/**
* Populate the difficulty data from the provided metadata.
* Does not load chart data (that is triggered later when we want to play the song).
@ -354,12 +383,17 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
public function getFirstValidVariation(?diffId:String, ?possibleVariations:Array<String>):Null<String>
{
if (variations == null) possibleVariations = variations;
if (possibleVariations == null)
{
possibleVariations = variations;
possibleVariations.sort(SortUtil.defaultsThenAlphabetically.bind(Constants.DEFAULT_VARIATION_LIST));
}
if (diffId == null) diffId = listDifficulties(null, possibleVariations)[0];
for (variation in variations)
for (variationId in possibleVariations)
{
if (difficulties.exists('$diffId-$variation')) return variation;
var variationSuffix = (variationId != Constants.DEFAULT_VARIATION) ? '-$variationId' : '';
if (difficulties.exists('$diffId$variationSuffix')) return variationId;
}
return null;

View File

@ -5,6 +5,7 @@ import flixel.group.FlxSpriteGroup;
import flixel.math.FlxMath;
import flixel.util.FlxTimer;
import funkin.util.MathUtil;
import funkin.audio.FunkinSound;
/**
* Loosley based on FlxTypeText lolol
@ -200,7 +201,7 @@ class Alphabet extends FlxSpriteGroup
if (FlxG.random.bool(40))
{
var daSound:String = "GF_";
FlxG.sound.play(Paths.soundRandom(daSound, 1, 4));
FunkinSound.playOnce(Paths.soundRandom(daSound, 1, 4));
}
add(letter);

View File

@ -5,6 +5,7 @@ import flixel.effects.FlxFlicker;
import flixel.group.FlxGroup;
import flixel.math.FlxPoint;
import flixel.util.FlxSignal;
import funkin.audio.FunkinSound;
class MenuTypedList<T:MenuListItem> extends FlxTypedGroup<T>
{
@ -93,7 +94,7 @@ class MenuTypedList<T:MenuListItem> extends FlxTypedGroup<T>
if (newIndex != selectedIndex)
{
FlxG.sound.play(Paths.sound('scrollMenu'));
FunkinSound.playOnce(Paths.sound('scrollMenu'));
selectItem(newIndex);
}
@ -163,7 +164,7 @@ class MenuTypedList<T:MenuListItem> extends FlxTypedGroup<T>
else
{
busy = true;
FlxG.sound.play(Paths.sound('confirmMenu'));
FunkinSound.playOnce(Paths.sound('confirmMenu'));
FlxFlicker.flicker(selected, 1, 0.06, true, false, function(_) {
busy = false;
selected.callback();

View File

@ -7,6 +7,7 @@ import flixel.FlxSubState;
import flixel.addons.transition.FlxTransitionableState;
import flixel.text.FlxText;
import flixel.util.FlxColor;
import funkin.audio.FunkinSound;
import flixel.util.FlxSort;
import funkin.modding.PolymodHandler;
import funkin.modding.events.ScriptEvent;
@ -151,6 +152,8 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
}
else
{
FunkinSound.stopAllAudio();
onComplete();
}
}

View File

@ -1,6 +1,6 @@
package funkin.ui;
import flixel.addons.transition.FlxTransitionableSubState;
import flixel.addons.transition.FlxTransitionableState;
import flixel.FlxSubState;
import flixel.text.FlxText;
import funkin.ui.mainmenu.MainMenuState;

View File

@ -4,6 +4,7 @@ import flixel.math.FlxPoint;
import flixel.FlxObject;
import flixel.FlxSprite;
import funkin.ui.MusicBeatSubState;
import funkin.audio.FunkinSound;
import funkin.ui.TextMenuList;
import funkin.ui.debug.charting.ChartEditorState;
import funkin.ui.MusicBeatSubState;
@ -71,7 +72,7 @@ class DebugMenuSubState extends MusicBeatSubState
if (controls.BACK)
{
FlxG.sound.play(Paths.sound('cancelMenu'));
FunkinSound.playOnce(Paths.sound('cancelMenu'));
exitDebugMenu();
}
}

View File

@ -1,32 +1,35 @@
package funkin.ui.debug.anim;
import funkin.util.SerializerUtil;
import funkin.play.character.CharacterData;
import flixel.FlxCamera;
import flixel.FlxSprite;
import flixel.FlxState;
import flixel.addons.display.FlxGridOverlay;
import flixel.addons.ui.FlxInputText;
import flixel.addons.ui.FlxUIDropDownMenu;
import flixel.FlxCamera;
import flixel.FlxSprite;
import flixel.FlxState;
import flixel.graphics.frames.FlxAtlasFrames;
import flixel.graphics.frames.FlxFrame;
import flixel.group.FlxGroup;
import flixel.math.FlxPoint;
import flixel.sound.FlxSound;
import flixel.text.FlxText;
import flixel.util.FlxColor;
import funkin.util.MouseUtil;
import flixel.util.FlxSpriteUtil;
import flixel.util.FlxTimer;
import funkin.audio.FunkinSound;
import funkin.input.Cursor;
import funkin.play.character.BaseCharacter;
import funkin.play.character.CharacterData;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.play.character.SparrowCharacter;
import haxe.ui.RuntimeComponentBuilder;
import funkin.ui.mainmenu.MainMenuState;
import funkin.util.MouseUtil;
import funkin.util.SerializerUtil;
import funkin.util.SortUtil;
import haxe.ui.components.DropDown;
import haxe.ui.core.Component;
import haxe.ui.core.Screen;
import haxe.ui.events.ItemEvent;
import haxe.ui.events.UIEvent;
import funkin.ui.mainmenu.MainMenuState;
import haxe.ui.RuntimeComponentBuilder;
import lime.utils.Assets as LimeAssets;
import openfl.Assets;
import openfl.events.Event;
@ -34,13 +37,8 @@ import openfl.events.IOErrorEvent;
import openfl.geom.Rectangle;
import openfl.net.FileReference;
import openfl.net.URLLoader;
import funkin.ui.mainmenu.MainMenuState;
import openfl.net.URLRequest;
import openfl.utils.ByteArray;
import funkin.input.Cursor;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.util.SortUtil;
import haxe.ui.core.Screen;
using flixel.util.FlxSpriteUtil;
@ -179,7 +177,7 @@ class DebugBoundingState extends FlxState
var objShit = js.html.URL.createObjectURL(swagList.item(0));
trace(objShit);
var funnysound = new FlxSound().loadStream('https://cdn.discordapp.com/attachments/767500676166451231/817821618251759666/Flutter.mp3', false, false,
var funnysound = new FunkinSound().loadStream('https://cdn.discordapp.com/attachments/767500676166451231/817821618251759666/Flutter.mp3', false, false,
null, function() {
trace('LOADED SHIT??');
});

View File

@ -15,7 +15,6 @@ import flixel.input.mouse.FlxMouseEvent;
import flixel.math.FlxMath;
import flixel.math.FlxPoint;
import flixel.math.FlxRect;
import flixel.sound.FlxSound;
import flixel.system.debug.log.LogStyle;
import flixel.system.FlxAssets.FlxSoundAsset;
import flixel.text.FlxText;
@ -878,6 +877,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/
var noteDisplayDirty:Bool = true;
var noteTooltipsDirty:Bool = true;
/**
* Whether the selected charactesr have been modified and the health icons need to be updated.
*/
@ -1089,7 +1090,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
* The chill audio track that plays in the chart editor.
* Plays when the main music is NOT being played.
*/
var welcomeMusic:FlxSound = new FlxSound();
var welcomeMusic:FunkinSound = new FunkinSound();
/**
* The audio track for the instrumental.
@ -1541,6 +1542,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// Make sure view is updated when the variation changes.
noteDisplayDirty = true;
notePreviewDirty = true;
noteTooltipsDirty = true;
notePreviewViewportBoundsDirty = true;
switchToCurrentInstrumental();
@ -1562,6 +1564,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// Make sure view is updated when the difficulty changes.
noteDisplayDirty = true;
notePreviewDirty = true;
noteTooltipsDirty = true;
notePreviewViewportBoundsDirty = true;
// Make sure the difficulty we selected is in the list of difficulties.
@ -3663,8 +3666,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
selectionSquare.width = eventSprite.width;
selectionSquare.height = eventSprite.height;
}
// Additional cleanup on notes.
if (noteTooltipsDirty) eventSprite.updateTooltipText();
}
noteTooltipsDirty = false;
// Sort the notes DESCENDING. This keeps the sustain behind the associated note.
renderedNotes.sort(FlxSort.byY, FlxSort.DESCENDING); // TODO: .group.insertionSort()
@ -3879,8 +3887,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
function handleCursor():Void
{
// Mouse sounds
if (FlxG.mouse.justPressed) FlxG.sound.play(Paths.sound("chartingSounds/ClickDown"));
if (FlxG.mouse.justReleased) FlxG.sound.play(Paths.sound("chartingSounds/ClickUp"));
if (FlxG.mouse.justPressed) FunkinSound.playOnce(Paths.sound("chartingSounds/ClickDown"));
if (FlxG.mouse.justReleased) FunkinSound.playOnce(Paths.sound("chartingSounds/ClickUp"));
// Note: If a menu is open in HaxeUI, don't handle cursor behavior.
var shouldHandleCursor:Bool = !(isHaxeUIFocused || playbarHeadDragging || isHaxeUIDialogOpen)
@ -4940,7 +4948,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
playbarNoteSnap.text = '1/${noteSnapQuant}';
playbarDifficulty.text = '${selectedDifficulty.toTitleCase()}';
// playbarBPM.text = 'BPM: ${(Conductor.currentTimeChange?.bpm ?? 0.0)}';
playbarBPM.text = 'BPM: ${(Conductor.instance.bpm ?? 0.0)}';
}
function handlePlayhead():Void

View File

@ -51,7 +51,12 @@ class SetItemSelectionCommand implements ChartEditorCommand
}
var eventData = eventSelected.valueAsStruct(defaultKey);
state.eventDataToPlace = eventData;
var eventDataClone = Reflect.copy(eventData);
if (eventDataClone != null)
{
state.eventDataToPlace = eventDataClone;
}
state.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT);
}

View File

@ -164,8 +164,7 @@ class ChartEditorEventSprite extends FlxSprite
this.eventData = value;
// Update the position to match the note data.
updateEventPosition();
// Update the tooltip text.
this.tooltip.tipData = {text: this.eventData.buildTooltip()};
updateTooltipText();
return this.eventData;
}
}
@ -188,6 +187,13 @@ class ChartEditorEventSprite extends FlxSprite
this.updateTooltipPosition();
}
public function updateTooltipText():Void
{
if (this.eventData == null) return;
if (this.isGhost) return;
this.tooltip.tipData = {text: this.eventData.buildTooltip()};
}
public function updateTooltipPosition():Void
{
// No tooltip for ghost sprites.

View File

@ -1,7 +1,6 @@
package funkin.ui.debug.charting.handlers;
import flixel.system.FlxAssets.FlxSoundAsset;
import flixel.sound.FlxSound;
import funkin.audio.VoicesGroup;
import funkin.audio.FunkinSound;
import funkin.play.character.BaseCharacter.CharacterType;
@ -302,7 +301,8 @@ class ChartEditorAudioHandler
trace('WARN: Failed to play sound $path, asset not found.');
return;
}
var snd:FunkinSound = FunkinSound.load(asset);
var snd:Null<FunkinSound> = FunkinSound.load(asset);
if (snd == null) return;
snd.autoDestroy = true;
snd.play(true);
snd.volume = volume;

View File

@ -237,6 +237,11 @@ class ChartEditorEventDataToolbox extends ChartEditorBaseToolbox
{
value = event.target.value.value;
}
else if (field.type == BOOL)
{
var chk:CheckBox = cast event.target;
value = cast(chk.selected, Null<Bool>); // Need to cast to nullable bool or the compiler will get mad.
}
trace('ChartEditorToolboxHandler.buildEventDataFormFromSchema() - ${event.target.id} = ${value}');
@ -253,14 +258,15 @@ class ChartEditorEventDataToolbox extends ChartEditorBaseToolbox
// Edit the event data of any existing events.
if (!_initializing && chartEditorState.currentEventSelection.length > 0)
{
for (event in chartEditorState.currentEventSelection)
for (songEvent in chartEditorState.currentEventSelection)
{
event.eventKind = chartEditorState.eventKindToPlace;
event.value = chartEditorState.eventDataToPlace;
songEvent.eventKind = chartEditorState.eventKindToPlace;
songEvent.value = Reflect.copy(chartEditorState.eventDataToPlace);
}
chartEditorState.saveDataDirty = true;
chartEditorState.noteDisplayDirty = true;
chartEditorState.notePreviewDirty = true;
chartEditorState.noteTooltipsDirty = true;
}
}
}

View File

@ -11,6 +11,7 @@ import funkin.data.dialogue.DialogueBoxData;
import funkin.data.dialogue.DialogueBoxRegistry;
import funkin.data.dialogue.SpeakerData;
import funkin.data.dialogue.SpeakerRegistry;
import funkin.data.freeplay.AlbumRegistry;
import funkin.play.cutscene.dialogue.Conversation;
import funkin.play.cutscene.dialogue.DialogueBox;
import funkin.play.cutscene.dialogue.Speaker;

View File

@ -71,8 +71,6 @@ class LatencyState extends MusicBeatSubState
// trace("EVENT LISTENER: " + key);
});
// FlxG.sound.playMusic(Paths.sound('soundTest'));
// funnyStatsGraph.hi
Conductor.instance.forceBPM(60);
@ -242,13 +240,6 @@ class LatencyState extends MusicBeatSubState
}
}
/* if (FlxG.keys.justPressed.SPACE)
{
FlxG.sound.music.stop();
FlxG.resetState();
}*/
noteGrp.forEach(function(daNote:NoteSprite) {
daNote.y = (strumLine.y - ((Conductor.instance.songPosition - Conductor.instance.instrumentalOffset) - daNote.noteData.time) * 0.45);
daNote.x = strumLine.x + 30;

View File

@ -37,8 +37,6 @@ class StageBuilderState extends MusicBeatState
FlxG.mouse.visible = true;
// var alsoSnd:FlxSound = new FlxSound();
// snd = new Sound();
// var swagBytes:ByteArray = new ByteArray(8192);

View File

@ -49,8 +49,11 @@ class StageOffsetSubState extends HaxeUISubState
{
super.create();
var playState = PlayState.instance;
FlxG.mouse.visible = true;
PlayState.instance.pauseMusic();
playState.pauseMusic();
playState.cancelAllCameraTweens();
FlxG.camera.target = null;
setupUIListeners();
@ -63,8 +66,8 @@ class StageOffsetSubState extends HaxeUISubState
// add(uiStuff);
PlayState.instance.persistentUpdate = true;
component.cameras = [PlayState.instance.camHUD];
playState.persistentUpdate = true;
component.cameras = [playState.camHUD];
// uiStuff.cameras = [PlayState.instance.camHUD];
// btn.cameras = [PlayState.instance.camHUD];
@ -72,7 +75,7 @@ class StageOffsetSubState extends HaxeUISubState
var layerList:ListView = findComponent("prop-layers");
for (thing in PlayState.instance.currentStage)
for (thing in playState.currentStage)
{
var prop:StageProp = cast thing;
if (prop != null && prop.name != null)

View File

@ -0,0 +1,89 @@
package funkin.ui.freeplay;
import funkin.data.freeplay.AlbumData;
import funkin.data.freeplay.AlbumRegistry;
import funkin.data.IRegistryEntry;
import flixel.graphics.FlxGraphic;
/**
* A class representing the data for an album as displayed in Freeplay.
*/
class Album implements IRegistryEntry<AlbumData>
{
/**
* The internal ID for this album.
*/
public final id:String;
/**
* The full data for an album.
*/
public final _data:AlbumData;
public function new(id:String)
{
this.id = id;
this._data = _fetchData(id);
if (_data == null)
{
throw 'Could not parse album data for id: $id';
}
}
/**
* Return the name of the album.
* @
*/
public function getAlbumName():String
{
return _data.name;
}
/**
* Return the artists of the album.
* @return The list of artists
*/
public function getAlbumArtists():Array<String>
{
return _data.artists;
}
/**
* Get the asset key for the album art.
* @return The asset key
*/
public function getAlbumArtAssetKey():String
{
return _data.albumArtAsset;
}
/**
* Get the album art as a graphic, ready to apply to a sprite.
* @return The built graphic
*/
public function getAlbumArtGraphic():FlxGraphic
{
return FlxG.bitmap.add(Paths.image(getAlbumArtAssetKey()));
}
/**
* Get the asset key for the album title.
*/
public function getAlbumTitleAssetKey():String
{
return _data.albumTitleAsset;
}
public function toString():String
{
return 'Album($id)';
}
public function destroy():Void {}
static function _fetchData(id:String):Null<AlbumData>
{
return AlbumRegistry.instance.parseEntryDataWithMigration(id, AlbumRegistry.instance.fetchEntryVersion(id));
}
}

View File

@ -0,0 +1,192 @@
package funkin.ui.freeplay;
import flixel.FlxSprite;
import flixel.group.FlxSpriteGroup;
import flixel.util.FlxSort;
import flixel.tweens.FlxTween;
import flixel.util.FlxTimer;
import flixel.tweens.FlxEase;
import funkin.data.freeplay.AlbumRegistry;
import funkin.graphics.FunkinSprite;
import funkin.util.SortUtil;
import openfl.utils.Assets;
/**
* The graphic for the album roll in the FreeplayState.
* Simply set `albumID` to fetch the required data and update the textures.
*/
class AlbumRoll extends FlxSpriteGroup
{
/**
* The ID of the album to display.
* Modify this value to automatically update the album art and title.
*/
public var albumId(default, set):String;
function set_albumId(value:String):String
{
if (this.albumId != value)
{
this.albumId = value;
updateAlbum();
}
return value;
}
var albumArt:FunkinSprite;
var albumTitle:FunkinSprite;
var difficultyStars:DifficultyStars;
var _exitMovers:Null<FreeplayState.ExitMoverData>;
var albumData:Album;
public function new()
{
super();
albumTitle = new FunkinSprite(947, 491);
albumTitle.visible = true;
albumTitle.zIndex = 200;
add(albumTitle);
difficultyStars = new DifficultyStars(140, 39);
difficultyStars.stars.visible = true;
albumTitle.visible = false;
// albumArtist.visible = false;
// var albumArtist:FlxSprite = new FlxSprite(1010, 607).loadGraphic(Paths.image('freeplay/albumArtist-kawaisprite'));
}
/**
* Load the album data by ID and update the textures.
*/
function updateAlbum():Void
{
albumData = AlbumRegistry.instance.fetchEntry(albumId);
if (albumData == null)
{
FlxG.log.warn('Could not find album data for album ID: ${albumId}');
return;
};
if (albumArt != null)
{
FlxTween.cancelTweensOf(albumArt);
albumArt.visible = false;
albumArt.destroy();
remove(albumArt);
}
// Paths.animateAtlas('freeplay/albumRoll'),
albumArt = FunkinSprite.create(1500, 360, albumData.getAlbumArtAssetKey());
albumArt.setGraphicSize(262, 262); // Magic number for size IG
albumArt.zIndex = 100;
// playIntro();
add(albumArt);
applyExitMovers();
if (Assets.exists(Paths.image(albumData.getAlbumTitleAssetKey())))
{
albumTitle.loadGraphic(Paths.image(albumData.getAlbumTitleAssetKey()));
}
else
{
albumTitle.visible = false;
}
refresh();
}
public function refresh():Void
{
sort(SortUtil.byZIndex, FlxSort.ASCENDING);
}
/**
* Apply exit movers for the album roll.
* @param exitMovers The exit movers to apply.
*/
public function applyExitMovers(?exitMovers:FreeplayState.ExitMoverData):Void
{
if (exitMovers == null)
{
exitMovers = _exitMovers;
}
else
{
_exitMovers = exitMovers;
}
if (exitMovers == null) return;
exitMovers.set([albumArt],
{
x: FlxG.width,
speed: 0.4,
wait: 0
});
exitMovers.set([albumTitle],
{
x: FlxG.width,
speed: 0.2,
wait: 0.1
});
/*
exitMovers.set([albumArtist],
{
x: FlxG.width * 1.1,
speed: 0.2,
wait: 0.2
});
*/
exitMovers.set([difficultyStars],
{
x: FlxG.width * 1.2,
speed: 0.2,
wait: 0.3
});
}
/**
* Play the intro animation on the album art.
*/
public function playIntro():Void
{
albumArt.visible = true;
FlxTween.tween(albumArt, {x: 950, y: 320, angle: -340}, 0.5, {ease: FlxEase.elasticOut});
albumTitle.visible = false;
new FlxTimer().start(0.75, function(_) {
showTitle();
});
}
public function setDifficultyStars(?difficulty:Int):Void
{
if (difficulty == null) return;
difficultyStars.difficulty = difficulty;
}
public function showTitle():Void
{
albumTitle.visible = true;
}
/**
* Make the album stars visible.
*/
public function showStars():Void
{
// albumArtist.visible = false;
difficultyStars.stars.visible = false;
}
}

View File

@ -4,8 +4,9 @@ import flixel.FlxSprite;
import flixel.util.FlxSignal;
import funkin.util.assets.FlxAnimationUtil;
import funkin.graphics.adobeanimate.FlxAtlasSprite;
import flixel.sound.FlxSound;
import funkin.audio.FunkinSound;
import flixel.util.FlxTimer;
import funkin.audio.FunkinSound;
import funkin.audio.FlxStreamSound;
class DJBoyfriend extends FlxAtlasSprite
@ -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
FlxG.sound.play(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(_) {
FlxG.sound.play(Paths.sound('channel_switch'));
});
// new FlxTimer().start(0.1, function(_) {
// // FunkinSound.playOnce(Paths.sound('channel_switch'));
// });
cartoonSnd.destroy();
loadCartoon();
}
// cartoonSnd.loadEmbedded(Paths.sound("cartoons/peck"));
// cartoonSnd.play();
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,10 +293,31 @@ 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;
}
}
}

View File

@ -1,19 +1,14 @@
package funkin.ui.freeplay;
import openfl.text.TextField;
import flixel.addons.display.FlxGridOverlay;
import flixel.addons.transition.FlxTransitionableState;
import flixel.addons.ui.FlxInputText;
import flixel.FlxCamera;
import flixel.FlxGame;
import flixel.FlxSprite;
import flixel.FlxState;
import flixel.group.FlxGroup;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import flixel.input.touch.FlxTouch;
import flixel.math.FlxAngle;
import flixel.math.FlxMath;
import flixel.math.FlxPoint;
import flixel.system.debug.watch.Tracker.TrackerProfile;
import flixel.text.FlxText;
@ -25,7 +20,6 @@ import flixel.util.FlxTimer;
import funkin.audio.FunkinSound;
import funkin.data.level.LevelRegistry;
import funkin.data.song.SongRegistry;
import funkin.graphics.adobeanimate.FlxAtlasSprite;
import funkin.graphics.FunkinCamera;
import funkin.graphics.FunkinSprite;
import funkin.graphics.shaders.AngleMask;
@ -33,28 +27,16 @@ import funkin.graphics.shaders.HSVShader;
import funkin.graphics.shaders.PureColor;
import funkin.graphics.shaders.StrokeShader;
import funkin.input.Controls;
import funkin.input.Controls.Control;
import funkin.play.components.HealthIcon;
import funkin.play.PlayState;
import funkin.play.PlayStatePlaylist;
import funkin.play.song.Song;
import funkin.save.Save;
import funkin.save.Save.SaveScoreData;
import funkin.ui.AtlasText;
import funkin.ui.freeplay.BGScrollingText;
import funkin.ui.freeplay.DifficultyStars;
import funkin.ui.freeplay.DJBoyfriend;
import funkin.ui.freeplay.FreeplayScore;
import funkin.ui.freeplay.LetterSort;
import funkin.ui.freeplay.SongMenuItem;
import funkin.ui.mainmenu.MainMenuState;
import funkin.ui.MusicBeatState;
import funkin.ui.MusicBeatSubState;
import funkin.ui.transition.LoadingState;
import funkin.ui.transition.StickerSubState;
import funkin.util.MathUtil;
import funkin.util.MathUtil;
import lime.app.Future;
import lime.utils.Assets;
/**
@ -65,6 +47,9 @@ typedef FreeplayStateParams =
?character:String,
};
/**
* The state for the freeplay menu, allowing the player to select any song to play.
*/
class FreeplayState extends MusicBeatSubState
{
//
@ -120,30 +105,31 @@ class FreeplayState extends MusicBeatSubState
var grpDifficulties:FlxTypedSpriteGroup<DifficultySprite>;
var coolColors:Array<Int> = [
0xff9271fd,
0xff9271fd,
0xff223344,
0xFF9271FD,
0xFF9271FD,
0xFF223344,
0xFF941653,
0xFFfc96d7,
0xFFa0d1ff,
0xffff78bf,
0xfff6b604
0xFFFC96D7,
0xFFA0D1FF,
0xFFFF78BF,
0xFFF6B604
];
var grpSongs:FlxTypedGroup<Alphabet>;
var grpCapsules:FlxTypedGroup<SongMenuItem>;
var curCapsule:SongMenuItem;
var curPlaying:Bool = false;
var ostName:FlxText;
var difficultyStars:DifficultyStars;
var displayedVariations:Array<String>;
var dj:DJBoyfriend;
var ostName:FlxText;
var albumRoll:AlbumRoll;
var letterSort:LetterSort;
var typing:FlxInputText;
var exitMovers:Map<Array<FlxSprite>, MoveData> = new Map();
var exitMovers:ExitMoverData = new Map();
var stickerSubState:StickerSubState;
@ -179,7 +165,7 @@ class FreeplayState extends MusicBeatSubState
#if discord_rpc
// Updating Discord Rich Presence
DiscordClient.changePresence("In the Menus", null);
DiscordClient.changePresence('In the Menus', null);
#end
var isDebug:Bool = false;
@ -188,14 +174,18 @@ class FreeplayState extends MusicBeatSubState
isDebug = true;
#end
FunkinSound.playMusic('freakyMenu');
FunkinSound.playMusic('freakyMenu',
{
overrideExisting: true,
restartTrack: false
});
// Add a null entry that represents the RANDOM option
songs.push(null);
// TODO: This makes custom variations disappear from Freeplay. Figure out a better solution later.
// Default character (BF) shows default and Erect variations. Pico shows only Pico variations.
displayedVariations = (currentCharacter == "bf") ? [Constants.DEFAULT_VARIATION, "erect"] : [currentCharacter];
displayedVariations = (currentCharacter == 'bf') ? [Constants.DEFAULT_VARIATION, 'erect'] : [currentCharacter];
// programmatically adds the songs via LevelRegistry and SongRegistry
for (levelId in LevelRegistry.instance.listBaseGameLevelIds())
@ -205,7 +195,7 @@ class FreeplayState extends MusicBeatSubState
var song:Song = SongRegistry.instance.fetchEntry(songId);
// Only display songs which actually have available charts for the current character.
var availableDifficultiesForSong = song.listDifficulties(displayedVariations);
var availableDifficultiesForSong:Array<String> = song.listDifficulties(displayedVariations);
if (availableDifficultiesForSong.length == 0) continue;
songs.push(new FreeplaySongData(levelId, songId, song, displayedVariations));
@ -226,16 +216,16 @@ class FreeplayState extends MusicBeatSubState
trace(FlxCamera.defaultZoom);
var pinkBack:FunkinSprite = FunkinSprite.create('freeplay/pinkBack');
pinkBack.color = 0xFFffd4e9; // sets it to pink!
pinkBack.color = 0xFFFFD4E9; // sets it to pink!
pinkBack.x -= pinkBack.width;
FlxTween.tween(pinkBack, {x: 0}, 0.6, {ease: FlxEase.quartOut});
add(pinkBack);
var orangeBackShit:FunkinSprite = new FunkinSprite(84, 440).makeSolidColor(Std.int(pinkBack.width), 75, 0xFFfeda00);
var orangeBackShit:FunkinSprite = new FunkinSprite(84, 440).makeSolidColor(Std.int(pinkBack.width), 75, 0xFFFEDA00);
add(orangeBackShit);
var alsoOrangeLOL:FunkinSprite = new FunkinSprite(0, orangeBackShit.y).makeSolidColor(100, Std.int(orangeBackShit.height), 0xFFffd400);
var alsoOrangeLOL:FunkinSprite = new FunkinSprite(0, orangeBackShit.y).makeSolidColor(100, Std.int(orangeBackShit.height), 0xFFFFD400);
add(alsoOrangeLOL);
exitMovers.set([pinkBack, orangeBackShit, alsoOrangeLOL],
@ -254,10 +244,10 @@ class FreeplayState extends MusicBeatSubState
add(grpTxtScrolls);
grpTxtScrolls.visible = false;
FlxG.debugger.addTrackerProfile(new TrackerProfile(BGScrollingText, ["x", "y", "speed", "size"]));
FlxG.debugger.addTrackerProfile(new TrackerProfile(BGScrollingText, ['x', 'y', 'speed', 'size']));
var moreWays:BGScrollingText = new BGScrollingText(0, 160, "HOT BLOODED IN MORE WAYS THAN ONE", FlxG.width, true, 43);
moreWays.funnyColor = 0xFFfff383;
var moreWays:BGScrollingText = new BGScrollingText(0, 160, 'HOT BLOODED IN MORE WAYS THAN ONE', FlxG.width, true, 43);
moreWays.funnyColor = 0xFFFFF383;
moreWays.speed = 6.8;
grpTxtScrolls.add(moreWays);
@ -267,8 +257,8 @@ class FreeplayState extends MusicBeatSubState
speed: 0.4,
});
var funnyScroll:BGScrollingText = new BGScrollingText(0, 220, "BOYFRIEND", FlxG.width / 2, false, 60);
funnyScroll.funnyColor = 0xFFff9963;
var funnyScroll:BGScrollingText = new BGScrollingText(0, 220, 'BOYFRIEND', FlxG.width / 2, false, 60);
funnyScroll.funnyColor = 0xFFFF9963;
funnyScroll.speed = -3.8;
grpTxtScrolls.add(funnyScroll);
@ -280,7 +270,7 @@ class FreeplayState extends MusicBeatSubState
wait: 0
});
var txtNuts:BGScrollingText = new BGScrollingText(0, 285, "PROTECT YO NUTS", FlxG.width / 2, true, 43);
var txtNuts:BGScrollingText = new BGScrollingText(0, 285, 'PROTECT YO NUTS', FlxG.width / 2, true, 43);
txtNuts.speed = 3.5;
grpTxtScrolls.add(txtNuts);
exitMovers.set([txtNuts],
@ -289,8 +279,8 @@ class FreeplayState extends MusicBeatSubState
speed: 0.4,
});
var funnyScroll2:BGScrollingText = new BGScrollingText(0, 335, "BOYFRIEND", FlxG.width / 2, false, 60);
funnyScroll2.funnyColor = 0xFFff9963;
var funnyScroll2:BGScrollingText = new BGScrollingText(0, 335, 'BOYFRIEND', FlxG.width / 2, false, 60);
funnyScroll2.funnyColor = 0xFFFF9963;
funnyScroll2.speed = -3.8;
grpTxtScrolls.add(funnyScroll2);
@ -300,8 +290,8 @@ class FreeplayState extends MusicBeatSubState
speed: 0.5,
});
var moreWays2:BGScrollingText = new BGScrollingText(0, 397, "HOT BLOODED IN MORE WAYS THAN ONE", FlxG.width, true, 43);
moreWays2.funnyColor = 0xFFfff383;
var moreWays2:BGScrollingText = new BGScrollingText(0, 397, 'HOT BLOODED IN MORE WAYS THAN ONE', FlxG.width, true, 43);
moreWays2.funnyColor = 0xFFFFF383;
moreWays2.speed = 6.8;
grpTxtScrolls.add(moreWays2);
@ -311,8 +301,8 @@ class FreeplayState extends MusicBeatSubState
speed: 0.4
});
var funnyScroll3:BGScrollingText = new BGScrollingText(0, orangeBackShit.y + 10, "BOYFRIEND", FlxG.width / 2, 60);
funnyScroll3.funnyColor = 0xFFfea400;
var funnyScroll3:BGScrollingText = new BGScrollingText(0, orangeBackShit.y + 10, 'BOYFRIEND', FlxG.width / 2, 60);
funnyScroll3.funnyColor = 0xFFFEA400;
funnyScroll3.speed = -3.8;
grpTxtScrolls.add(funnyScroll3);
@ -328,8 +318,10 @@ class FreeplayState extends MusicBeatSubState
x: -dj.width * 1.6,
speed: 0.5
});
// TODO: Replace this.
if (currentCharacter == "pico") dj.visible = false;
if (currentCharacter == 'pico') dj.visible = false;
add(dj);
var bgDad:FlxSprite = new FlxSprite(pinkBack.width * 0.75, 0).loadGraphic(Paths.image('freeplay/freeplayBGdad'));
@ -387,62 +379,23 @@ class FreeplayState extends MusicBeatSubState
if (diffSprite.difficultyId == currentDifficulty) diffSprite.visible = true;
}
// NOTE: This is an AtlasSprite because we use an animation to bring it into view.
// TODO: Add the ability to select the album graphic.
var albumArt:FlxAtlasSprite = new FlxAtlasSprite(640, 360, Paths.animateAtlas("freeplay/albumRoll"));
albumArt.visible = false;
add(albumArt);
albumRoll = new AlbumRoll();
albumRoll.albumId = 'volume1';
add(albumRoll);
exitMovers.set([albumArt],
{
x: FlxG.width,
speed: 0.4,
wait: 0
});
var albumTitle:FlxSprite = new FlxSprite(947, 491).loadGraphic(Paths.image('freeplay/albumTitle-fnfvol1'));
var albumArtist:FlxSprite = new FlxSprite(1010, 607).loadGraphic(Paths.image('freeplay/albumArtist-kawaisprite'));
difficultyStars = new DifficultyStars(140, 39);
difficultyStars.stars.visible = false;
albumTitle.visible = false;
albumArtist.visible = false;
exitMovers.set([albumTitle],
{
x: FlxG.width,
speed: 0.2,
wait: 0.1
});
exitMovers.set([albumArtist],
{
x: FlxG.width * 1.1,
speed: 0.2,
wait: 0.2
});
exitMovers.set([difficultyStars],
{
x: FlxG.width * 1.2,
speed: 0.2,
wait: 0.3
});
add(albumTitle);
add(albumArtist);
add(difficultyStars);
albumRoll.applyExitMovers(exitMovers);
var overhangStuff:FlxSprite = new FlxSprite().makeGraphic(FlxG.width, 64, FlxColor.BLACK);
overhangStuff.y -= overhangStuff.height;
add(overhangStuff);
FlxTween.tween(overhangStuff, {y: 0}, 0.3, {ease: FlxEase.quartOut});
var fnfFreeplay:FlxText = new FlxText(8, 8, 0, "FREEPLAY", 48);
fnfFreeplay.font = "VCR OSD Mono";
var fnfFreeplay:FlxText = new FlxText(8, 8, 0, 'FREEPLAY', 48);
fnfFreeplay.font = 'VCR OSD Mono';
fnfFreeplay.visible = false;
ostName = new FlxText(8, 8, FlxG.width - 8 - 8, "OFFICIAL OST", 48);
ostName.font = "VCR OSD Mono";
ostName = new FlxText(8, 8, FlxG.width - 8 - 8, 'OFFICIAL OST', 48);
ostName.font = 'VCR OSD Mono';
ostName.alignment = RIGHT;
ostName.visible = false;
@ -454,21 +407,21 @@ class FreeplayState extends MusicBeatSubState
wait: 0
});
var sillyStroke = new StrokeShader(0xFFFFFFFF, 2, 2);
var sillyStroke:StrokeShader = new StrokeShader(0xFFFFFFFF, 2, 2);
fnfFreeplay.shader = sillyStroke;
add(fnfFreeplay);
add(ostName);
var fnfHighscoreSpr:FlxSprite = new FlxSprite(860, 70);
fnfHighscoreSpr.frames = Paths.getSparrowAtlas('freeplay/highscore');
fnfHighscoreSpr.animation.addByPrefix("highscore", "highscore small instance 1", 24, false);
fnfHighscoreSpr.animation.addByPrefix('highscore', 'highscore small instance 1', 24, false);
fnfHighscoreSpr.visible = false;
fnfHighscoreSpr.setGraphicSize(0, Std.int(fnfHighscoreSpr.height * 1));
fnfHighscoreSpr.updateHitbox();
add(fnfHighscoreSpr);
new FlxTimer().start(FlxG.random.float(12, 50), function(tmr) {
fnfHighscoreSpr.animation.play("highscore");
fnfHighscoreSpr.animation.play('highscore');
tmr.time = FlxG.random.float(20, 60);
}, 0);
@ -479,7 +432,7 @@ class FreeplayState extends MusicBeatSubState
var clearBoxSprite:FlxSprite = new FlxSprite(1165, 65).loadGraphic(Paths.image('freeplay/clearBox'));
add(clearBoxSprite);
txtCompletion = new AtlasText(1185, 87, "69", AtlasFont.FREEPLAY_CLEAR);
txtCompletion = new AtlasText(1185, 87, '69', AtlasFont.FREEPLAY_CLEAR);
txtCompletion.visible = false;
add(txtCompletion);
@ -496,9 +449,9 @@ class FreeplayState extends MusicBeatSubState
letterSort.changeSelectionCallback = (str) -> {
switch (str)
{
case "fav":
case 'fav':
generateSongList({filterType: FAVORITE}, true);
case "ALL":
case 'ALL':
generateSongList(null, true);
default:
generateSongList({filterType: REGEXP, filterData: str}, true);
@ -514,25 +467,20 @@ class FreeplayState extends MusicBeatSubState
dj.onIntroDone.add(function() {
// when boyfriend hits dat shiii
albumArt.visible = true;
albumArt.anim.play("");
albumArt.anim.onComplete = function() {
albumArt.anim.pause();
};
albumRoll.playIntro();
new FlxTimer().start(1, function(_) {
albumTitle.visible = true;
new FlxTimer().start(0.75, function(_) {
albumRoll.showTitle();
});
new FlxTimer().start(35 / 24, function(_) {
albumArtist.visible = true;
difficultyStars.stars.visible = true;
albumRoll.showStars();
});
FlxTween.tween(grpDifficulties, {x: 90}, 0.6, {ease: FlxEase.quartOut});
var diffSelLeft = new DifficultySelector(20, grpDifficulties.y - 10, false, controls);
var diffSelRight = new DifficultySelector(325, grpDifficulties.y - 10, true, controls);
var diffSelLeft:DifficultySelector = new DifficultySelector(20, grpDifficulties.y - 10, false, controls);
var diffSelRight:DifficultySelector = new DifficultySelector(325, grpDifficulties.y - 10, true, controls);
add(diffSelLeft);
add(diffSelRight);
@ -562,7 +510,7 @@ class FreeplayState extends MusicBeatSubState
});
});
pinkBack.color = 0xFFffd863;
pinkBack.color = 0xFFFFD863;
bgDad.visible = true;
orangeBackShit.visible = true;
alsoOrangeLOL.visible = true;
@ -571,9 +519,9 @@ class FreeplayState extends MusicBeatSubState
generateSongList(null, false);
var swag:Alphabet = new Alphabet(1, 0, "swag");
// var swag:Alphabet = new Alphabet(1, 0, 'swag');
var funnyCam = new FunkinCamera(0, 0, FlxG.width, FlxG.height);
var funnyCam:FunkinCamera = new FunkinCamera(0, 0, FlxG.width, FlxG.height);
funnyCam.bgColor = FlxColor.TRANSPARENT;
FlxG.cameras.add(funnyCam);
@ -588,12 +536,20 @@ class FreeplayState extends MusicBeatSubState
});
}
/**
* Given the current filter, rebuild the current song list.
*
* @param filterStuff A filter to apply to the song list (regex, startswith, all, favorite)
* @param force
*/
public function generateSongList(?filterStuff:SongFilter, force:Bool = false):Void
{
curSelected = 1;
for (cap in grpCapsules.members)
{
cap.kill();
}
var tempSongs:Array<FreeplaySongData> = songs;
@ -604,7 +560,7 @@ class FreeplayState extends MusicBeatSubState
case REGEXP:
// filterStuff.filterData has a string with the first letter of the sorting range, and the second one
// this creates a filter to return all the songs that start with a letter between those two
var filterRegexp = new EReg("^[" + filterStuff.filterData + "].*", "i");
var filterRegexp:EReg = new EReg('^[' + filterStuff.filterData + '].*', 'i');
tempSongs = tempSongs.filter(str -> {
if (str == null) return true; // Random
return filterRegexp.match(str.songName);
@ -660,14 +616,19 @@ class FreeplayState extends MusicBeatSubState
funnyMenu.favIcon.visible = tempSongs[i].isFav;
funnyMenu.hsvShader = hsvShader;
if (i < 8) funnyMenu.initJumpIn(Math.min(i, 4), force);
if (i < 8)
{
funnyMenu.initJumpIn(Math.min(i, 4), force);
}
else
{
funnyMenu.forcePosition();
}
grpCapsules.add(funnyMenu);
}
FlxG.console.registerFunction("changeSelection", changeSelection);
FlxG.console.registerFunction('changeSelection', changeSelection);
rememberSelection();
@ -699,7 +660,7 @@ class FreeplayState extends MusicBeatSubState
{
if (songs[curSelected] != null)
{
var realShit = curSelected;
var realShit:Int = curSelected;
songs[curSelected].isFav = !songs[curSelected].isFav;
if (songs[curSelected].isFav)
{
@ -708,7 +669,7 @@ class FreeplayState extends MusicBeatSubState
ease: FlxEase.elasticOut,
onComplete: _ -> {
grpCapsules.members[realShit].favIcon.visible = true;
grpCapsules.members[realShit].favIcon.animation.play("fav");
grpCapsules.members[realShit].favIcon.animation.play('fav');
}
});
}
@ -727,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);
@ -772,9 +725,9 @@ class FreeplayState extends MusicBeatSubState
{
if (busy) return;
var upP = controls.UI_UP_P;
var downP = controls.UI_DOWN_P;
var accepted = 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)
{
@ -786,14 +739,14 @@ class FreeplayState extends MusicBeatSubState
}
if (touch.pressed)
{
var dx = initTouchPos.x - touch.screenX;
var dy = initTouchPos.y - touch.screenY;
var dx:Float = initTouchPos.x - touch.screenX;
var dy:Float = initTouchPos.y - touch.screenY;
var angle = Math.atan2(dy, dx);
var length = Math.sqrt(dx * dx + dy * dy);
var angle:Float = Math.atan2(dy, dx);
var length:Float = Math.sqrt(dx * dx + dy * dy);
FlxG.watch.addQuick("LENGTH", length);
FlxG.watch.addQuick("ANGLE", Math.round(FlxAngle.asDegrees(angle)));
FlxG.watch.addQuick('LENGTH', length);
FlxG.watch.addQuick('ANGLE', Math.round(FlxAngle.asDegrees(angle)));
}
}
@ -848,22 +801,42 @@ 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)
{
spamTimer = 0;
if (controls.UI_UP) changeSelection(-1);
if (controls.UI_UP)
{
changeSelection(-1);
}
else
{
changeSelection(1);
}
}
}
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
{
@ -871,51 +844,30 @@ 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);
}
// TODO: DEBUG REMOVE THIS
if (FlxG.keys.justPressed.P)
{
var newParams:FreeplayStateParams =
{
character: currentCharacter == "bf" ? "pico" : "bf",
};
openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new funkin.ui.freeplay.FreeplayState(newParams, sticker)));
}
if (controls.BACK && !typing.hasFocus)
{
FlxTween.globalManager.clear();
FlxTimer.globalManager.clear();
dj.onIntroDone.removeAll();
FlxG.sound.play(Paths.sound('cancelMenu'));
FunkinSound.playOnce(Paths.sound('cancelMenu'));
var longestTimer:Float = 0;
@ -974,7 +926,7 @@ class FreeplayState extends MusicBeatSubState
public override function destroy():Void
{
super.destroy();
var daSong = songs[curSelected];
var daSong:Null<FreeplaySongData> = songs[curSelected];
if (daSong != null)
{
clearDaCache(daSong.songName);
@ -985,7 +937,7 @@ class FreeplayState extends MusicBeatSubState
{
touchTimer = 0;
var currentDifficultyIndex = diffIdsCurrent.indexOf(currentDifficulty);
var currentDifficultyIndex:Int = diffIdsCurrent.indexOf(currentDifficulty);
if (currentDifficultyIndex == -1) currentDifficultyIndex = diffIdsCurrent.indexOf(Constants.DEFAULT_DIFFICULTY);
@ -996,7 +948,7 @@ class FreeplayState extends MusicBeatSubState
currentDifficulty = diffIdsCurrent[currentDifficultyIndex];
var daSong = songs[curSelected];
var daSong:Null<FreeplaySongData> = songs[curSelected];
if (daSong != null)
{
var songScore:SaveScoreData = Save.instance.getSongScore(songs[curSelected].songId, currentDifficulty);
@ -1060,11 +1012,19 @@ class FreeplayState extends MusicBeatSubState
}
// Set the difficulty star count on the right.
difficultyStars.difficulty = daSong?.songRating ?? difficultyStars.difficulty; // yay haxe 4.3
albumRoll.setDifficultyStars(daSong?.songRating);
// Set the album graphic and play the animation if relevant.
var newAlbumId:String = daSong?.albumId ?? Constants.DEFAULT_ALBUM_ID;
if (albumRoll.albumId != newAlbumId)
{
albumRoll.albumId = newAlbumId;
albumRoll.playIntro();
}
}
// Clears the cache of songs, frees up memory, they' ll have to be loaded in later tho function clearDaCache(actualSongTho:String)
function clearDaCache(actualSongTho:String)
function clearDaCache(actualSongTho:String):Void
{
for (song in songs)
{
@ -1079,7 +1039,7 @@ class FreeplayState extends MusicBeatSubState
function capsuleOnConfirmRandom(randomCapsule:SongMenuItem):Void
{
trace("RANDOM SELECTED");
trace('RANDOM SELECTED');
busy = true;
letterSort.inputEnabled = false;
@ -1095,10 +1055,10 @@ class FreeplayState extends MusicBeatSubState
if (availableSongCapsules.length == 0)
{
trace("No songs available!");
trace('No songs available!');
busy = false;
letterSort.inputEnabled = true;
FlxG.sound.play(Paths.sound('cancelMenu'));
FunkinSound.playOnce(Paths.sound('cancelMenu'));
return;
}
@ -1131,7 +1091,7 @@ class FreeplayState extends MusicBeatSubState
PlayStatePlaylist.campaignId = cap.songData.levelId;
// Visual and audio effects.
FlxG.sound.play(Paths.sound('confirmMenu'));
FunkinSound.playOnce(Paths.sound('confirmMenu'));
dj.confirm();
new FlxTimer().start(1, function(tmr:FlxTimer) {
@ -1167,24 +1127,22 @@ class FreeplayState extends MusicBeatSubState
}
// Set the difficulty star count on the right.
var daSong = songs[curSelected];
difficultyStars.difficulty = daSong?.songRating ?? 0;
var daSong:Null<FreeplaySongData> = songs[curSelected];
albumRoll.setDifficultyStars(daSong?.songRating ?? 0);
}
function changeSelection(change:Int = 0):Void
{
// NGio.logEvent('Fresh');
FlxG.sound.play(Paths.sound('scrollMenu'), 0.4);
// FlxG.sound.playMusic(Paths.inst(songs[curSelected].songName));
FunkinSound.playOnce(Paths.sound('scrollMenu'), 0.4);
var prevSelected = curSelected;
var prevSelected:Int = curSelected;
curSelected += change;
if (curSelected < 0) curSelected = grpCapsules.countLiving() - 1;
if (curSelected >= grpCapsules.countLiving()) curSelected = 0;
var daSongCapsule = grpCapsules.members[curSelected];
var daSongCapsule:SongMenuItem = grpCapsules.members[curSelected];
if (daSongCapsule.songData != null)
{
var songScore:SaveScoreData = Save.instance.getSongScore(daSongCapsule.songData.songId, currentDifficulty);
@ -1218,15 +1176,25 @@ class FreeplayState extends MusicBeatSubState
{
if (curSelected == 0)
{
FlxG.sound.playMusic(Paths.music('freeplay/freeplayRandom'), 0);
FunkinSound.playMusic('freeplayRandom',
{
startingVolume: 0.0,
overrideExisting: true,
restartTrack: true
});
FlxG.sound.music.fadeIn(2, 0, 0.8);
}
else
{
// TODO: Stream the instrumental of the selected song?
if (prevSelected == 0)
var didReplace:Bool = FunkinSound.playMusic('freakyMenu',
{
startingVolume: 0.0,
overrideExisting: true,
restartTrack: false
});
if (didReplace)
{
FunkinSound.playMusic('freakyMenu');
FlxG.sound.music.fadeIn(2, 0, 0.8);
}
}
@ -1235,6 +1203,9 @@ class FreeplayState extends MusicBeatSubState
}
}
/**
* The difficulty selector arrows to the left and right of the difficulty.
*/
class DifficultySelector extends FlxSprite
{
var controls:Controls;
@ -1247,7 +1218,7 @@ class DifficultySelector extends FlxSprite
this.controls = controls;
frames = Paths.getSparrowAtlas('freeplay/freeplaySelector');
animation.addByPrefix('shine', "arrow pointer loop", 24);
animation.addByPrefix('shine', 'arrow pointer loop', 24);
animation.play('shine');
whiteShader = new PureColor(FlxColor.WHITE);
@ -1259,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);
}
@ -1281,34 +1252,62 @@ class DifficultySelector extends FlxSprite
}
}
/**
* Structure for the current song filter.
*/
typedef SongFilter =
{
var filterType:FilterType;
var ?filterData:Dynamic;
}
/**
* Possible types to use for the song filter.
*/
enum abstract FilterType(String)
{
var STARTSWITH;
var REGEXP;
var FAVORITE;
var ALL;
/**
* Filter to songs which start with a string
*/
public var STARTSWITH;
/**
* Filter to songs which match a regular expression
*/
public var REGEXP;
/**
* Filter to songs which are favorited
*/
public var FAVORITE;
/**
* Filter to all songs
*/
public var ALL;
}
/**
* Data about a specific song in the freeplay menu.
*/
class FreeplaySongData
{
/**
* Whether or not the song has been favorited.
*/
public var isFav:Bool = false;
var song:Song;
public var levelId(default, null):String = "";
public var songId(default, null):String = "";
public var levelId(default, null):String = '';
public var songId(default, null):String = '';
public var songDifficulties(default, null):Array<String> = [];
public var songName(default, null):String = "";
public var songCharacter(default, null):String = "";
public var songName(default, null):String = '';
public var songCharacter(default, null):String = '';
public var songRating(default, null):Int = 0;
public var albumId(default, null):String = '';
public var currentDifficulty(default, set):String = Constants.DEFAULT_DIFFICULTY;
public var displayedVariations(default, null):Array<String> = [Constants.DEFAULT_VARIATION];
@ -1332,19 +1331,28 @@ class FreeplaySongData
updateValues(displayedVariations);
}
function updateValues(displayedVariations:Array<String>):Void
function updateValues(variations:Array<String>):Void
{
this.songDifficulties = song.listDifficulties(displayedVariations);
this.songDifficulties = song.listDifficulties(variations);
if (!this.songDifficulties.contains(currentDifficulty)) currentDifficulty = Constants.DEFAULT_DIFFICULTY;
var songDifficulty:SongDifficulty = song.getDifficulty(currentDifficulty, displayedVariations);
var songDifficulty:SongDifficulty = song.getDifficulty(currentDifficulty, variations);
if (songDifficulty == null) return;
this.songName = songDifficulty.songName;
this.songCharacter = songDifficulty.characters.opponent;
this.songRating = songDifficulty.difficultyRating;
this.albumId = songDifficulty.album;
}
}
/**
* The map storing information about the exit movers.
*/
typedef ExitMoverData = Map<Array<FlxSprite>, MoveData>;
/**
* The data for an exit mover.
*/
typedef MoveData =
{
var ?x:Float;
@ -1353,8 +1361,14 @@ typedef MoveData =
var ?wait:Float;
}
/**
* The sprite for the difficulty
*/
class DifficultySprite extends FlxSprite
{
/**
* The difficulty id which this sprite represents.
*/
public var difficultyId:String;
public function new(diffId:String)

View File

@ -0,0 +1,9 @@
package funkin.ui.freeplay;
/**
* A script that can be tied to an Album.
* Create a scripted class that extends Album to use this.
* This allows you to customize how a specific album appears.
*/
@:hscriptClass
class ScriptedAlbum extends funkin.ui.freeplay.Album implements polymod.hscript.HScriptedClass {}

View File

@ -65,25 +65,26 @@ class SongMenuItem extends FlxSpriteGroup
var rank:String = FlxG.random.getObject(ranks);
ranking = new FlxSprite(capsule.width * 0.84, 30);
ranking.loadGraphic(Paths.image("freeplay/ranks/" + rank));
ranking.loadGraphic(Paths.image('freeplay/ranks/' + rank));
ranking.scale.x = ranking.scale.y = realScaled;
ranking.alpha = 0.75;
// ranking.alpha = 0.75;
ranking.visible = false;
ranking.origin.set(capsule.origin.x - ranking.x, capsule.origin.y - ranking.y);
add(ranking);
grpHide.add(ranking);
switch (rank)
{
case "perfect":
case 'perfect':
ranking.x -= 10;
}
grayscaleShader = new Grayscale(1);
diffRatingSprite = new FlxSprite(145, 90).loadGraphic(Paths.image("freeplay/diffRatings/diff00"));
diffRatingSprite = new FlxSprite(145, 90).loadGraphic(Paths.image('freeplay/diffRatings/diff00'));
diffRatingSprite.shader = grayscaleShader;
diffRatingSprite.visible = false;
add(diffRatingSprite);
// TODO: Readd once ratings are fully implemented
// add(diffRatingSprite);
diffRatingSprite.origin.set(capsule.origin.x - diffRatingSprite.x, capsule.origin.y - diffRatingSprite.y);
grpHide.add(diffRatingSprite);
@ -104,7 +105,7 @@ class SongMenuItem extends FlxSpriteGroup
favIcon = new FlxSprite(400, 40);
favIcon.frames = Paths.getSparrowAtlas('freeplay/favHeart');
favIcon.animation.addByPrefix('fav', "favorite heart", 24, false);
favIcon.animation.addByPrefix('fav', 'favorite heart', 24, false);
favIcon.animation.play('fav');
favIcon.setGraphicSize(50, 50);
favIcon.visible = false;
@ -114,10 +115,11 @@ class SongMenuItem extends FlxSpriteGroup
setVisibleGrp(false);
}
function updateDifficultyRating(newRating:Int)
function updateDifficultyRating(newRating:Int):Void
{
var ratingPadded:String = newRating < 10 ? '0$newRating' : '$newRating';
diffRatingSprite.loadGraphic(Paths.image('freeplay/diffRatings/diff${ratingPadded}'));
diffRatingSprite.visible = false;
}
function set_hsvShader(value:HSVShader):HSVShader
@ -129,7 +131,7 @@ class SongMenuItem extends FlxSpriteGroup
return value;
}
function textAppear()
function textAppear():Void
{
songText.scale.x = 1.7;
songText.scale.y = 0.2;
@ -144,7 +146,7 @@ class SongMenuItem extends FlxSpriteGroup
});
}
function setVisibleGrp(value:Bool)
function setVisibleGrp(value:Bool):Void
{
for (spr in grpHide.members)
{
@ -156,7 +158,7 @@ class SongMenuItem extends FlxSpriteGroup
updateSelected();
}
public function init(?x:Float, ?y:Float, songData:Null<FreeplaySongData>)
public function init(?x:Float, ?y:Float, songData:Null<FreeplaySongData>):Void
{
if (x != null) this.x = x;
if (y != null) this.y = y;
@ -176,28 +178,26 @@ class SongMenuItem extends FlxSpriteGroup
* @param char The character ID used by this song.
* If the character has no freeplay icon, a warning will be thrown and nothing will display.
*/
public function setCharacter(char:String)
public function setCharacter(char:String):Void
{
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)
{
case "monster-christmas":
charPath += "monsterpixel";
case "mom-car":
charPath += "mommypixel";
case "dad":
charPath += "daddypixel";
case "darnell-blazin":
charPath += "darnellpixel";
case "senpai-angry":
charPath += "senpaipixel";
case 'monster-christmas':
charPath += 'monsterpixel';
case 'mom-car':
charPath += 'mommypixel';
case 'dad':
charPath += 'daddypixel';
case 'darnell-blazin':
charPath += 'darnellpixel';
case 'senpai-angry':
charPath += 'senpaipixel';
default:
charPath += char + "pixel";
charPath += '${char}pixel';
}
if (!openfl.utils.Assets.exists(Paths.image(charPath)))
@ -211,7 +211,7 @@ class SongMenuItem extends FlxSpriteGroup
switch (char)
{
case "parents-christmas":
case 'parents-christmas':
pixelIcon.origin.x = 140;
default:
pixelIcon.origin.x = 100;
@ -262,7 +262,7 @@ class SongMenuItem extends FlxSpriteGroup
var grpHide:FlxGroup;
public function forcePosition()
public function forcePosition():Void
{
visible = true;
capsule.alpha = 1;
@ -287,7 +287,7 @@ class SongMenuItem extends FlxSpriteGroup
setVisibleGrp(true);
}
override function update(elapsed:Float)
override function update(elapsed:Float):Void
{
if (doJumpIn)
{

View File

@ -1,6 +1,6 @@
package funkin.ui.mainmenu;
import flixel.addons.transition.FlxTransitionableSubState;
import flixel.addons.transition.FlxTransitionableState;
import funkin.ui.debug.DebugMenuSubState;
import flixel.FlxObject;
import flixel.FlxSprite;
@ -103,8 +103,8 @@ class MainMenuState extends MusicBeatState
persistentDraw = true;
persistentUpdate = false;
// Freeplay has its own custom transition
FlxTransitionableSubState.skipNextTransIn = true;
FlxTransitionableSubState.skipNextTransOut = true;
FlxTransitionableState.skipNextTransIn = true;
FlxTransitionableState.skipNextTransOut = true;
openSubState(new FreeplayState());
});
@ -155,7 +155,11 @@ class MainMenuState extends MusicBeatState
function playMenuMusic():Void
{
FunkinSound.playMusic('freakyMenu');
FunkinSound.playMusic('freakyMenu',
{
overrideExisting: true,
restartTrack: false
});
}
function resetCamStuff()
@ -321,7 +325,7 @@ class MainMenuState extends MusicBeatState
if (controls.BACK && menuItems.enabled && !menuItems.busy)
{
FlxG.sound.play(Paths.sound('cancelMenu'));
FunkinSound.playOnce(Paths.sound('cancelMenu'));
FlxG.switchState(() -> new TitleState());
}
}

View File

@ -5,9 +5,11 @@ import flixel.FlxSubState;
import flixel.addons.transition.FlxTransitionableState;
import flixel.group.FlxGroup;
import flixel.util.FlxSignal;
import funkin.audio.FunkinSound;
import funkin.ui.mainmenu.MainMenuState;
import funkin.ui.MusicBeatState;
import funkin.util.WindowUtil;
import funkin.audio.FunkinSound;
import funkin.input.Controls;
class OptionsState extends MusicBeatState
@ -143,7 +145,7 @@ class Page extends FlxGroup
{
if (canExit && controls.BACK)
{
FlxG.sound.play(Paths.sound('cancelMenu'));
FunkinSound.playOnce(Paths.sound('cancelMenu'));
exit();
}
}

View File

@ -1,37 +1,33 @@
package funkin.ui.story;
import funkin.ui.mainmenu.MainMenuState;
import funkin.save.Save;
import funkin.save.Save.SaveScoreData;
import openfl.utils.Assets;
import flixel.addons.transition.FlxTransitionableState;
import flixel.FlxSprite;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.text.FlxText;
import flixel.addons.transition.FlxTransitionableState;
import flixel.tweens.FlxEase;
import funkin.graphics.FunkinSprite;
import funkin.ui.MusicBeatState;
import flixel.tweens.FlxTween;
import flixel.util.FlxColor;
import flixel.util.FlxTimer;
import funkin.data.level.LevelRegistry;
import funkin.audio.FunkinSound;
import funkin.data.level.LevelRegistry;
import funkin.data.song.SongRegistry;
import funkin.graphics.FunkinSprite;
import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.play.PlayState;
import funkin.play.PlayStatePlaylist;
import funkin.ui.mainmenu.MainMenuState;
import funkin.play.song.Song;
import funkin.data.song.SongData.SongMusicData;
import funkin.data.song.SongRegistry;
import funkin.util.MathUtil;
import funkin.save.Save;
import funkin.save.Save.SaveScoreData;
import funkin.ui.mainmenu.MainMenuState;
import funkin.ui.MusicBeatState;
import funkin.ui.transition.LoadingState;
import funkin.ui.transition.StickerSubState;
import funkin.util.MathUtil;
import openfl.utils.Assets;
class StoryMenuState extends MusicBeatState
{
static final DEFAULT_BACKGROUND_COLOR:FlxColor = FlxColor.fromString("#F9CF51");
static final DEFAULT_BACKGROUND_COLOR:FlxColor = FlxColor.fromString('#F9CF51');
static final BACKGROUND_HEIGHT:Int = 400;
var currentDifficultyId:String = 'normal';
@ -166,25 +162,25 @@ class StoryMenuState extends MusicBeatState
updateProps();
tracklistText = new FlxText(FlxG.width * 0.05, levelBackground.x + levelBackground.height + 100, 0, "Tracks", 32);
tracklistText.setFormat("VCR OSD Mono", 32);
tracklistText.setFormat('VCR OSD Mono', 32);
tracklistText.alignment = CENTER;
tracklistText.color = 0xFFe55777;
tracklistText.color = 0xFFE55777;
add(tracklistText);
scoreText = new FlxText(10, 10, 0, 'HIGH SCORE: 42069420');
scoreText.setFormat("VCR OSD Mono", 32);
scoreText.setFormat('VCR OSD Mono', 32);
scoreText.zIndex = 1000;
add(scoreText);
modeText = new FlxText(10, 10, 0, 'Base Game Levels [TAB to switch]');
modeText.setFormat("VCR OSD Mono", 32);
modeText.setFormat('VCR OSD Mono', 32);
modeText.screenCenter(X);
modeText.visible = hasModdedLevels();
modeText.zIndex = 1000;
add(modeText);
levelTitleText = new FlxText(FlxG.width * 0.7, 10, 0, 'LEVEL 1');
levelTitleText.setFormat("VCR OSD Mono", 32, FlxColor.WHITE, RIGHT);
levelTitleText.setFormat('VCR OSD Mono', 32, FlxColor.WHITE, RIGHT);
levelTitleText.alpha = 0.7;
levelTitleText.zIndex = 1000;
add(levelTitleText);
@ -217,7 +213,7 @@ class StoryMenuState extends MusicBeatState
#if discord_rpc
// Updating Discord Rich Presence
DiscordClient.changePresence("In the Menus", null);
DiscordClient.changePresence('In the Menus', null);
#end
}
@ -235,7 +231,11 @@ class StoryMenuState extends MusicBeatState
function playMenuMusic():Void
{
FunkinSound.playMusic('freakyMenu');
FunkinSound.playMusic('freakyMenu',
{
overrideExisting: true,
restartTrack: false
});
}
function updateData():Void
@ -307,11 +307,11 @@ class StoryMenuState extends MusicBeatState
changeDifficulty(0);
}
override function update(elapsed:Float)
override function update(elapsed:Float):Void
{
Conductor.instance.update();
highScoreLerp = Std.int(MathUtil.coolLerp(highScoreLerp, highScore, 0.5));
highScoreLerp = Std.int(MathUtil.smoothLerp(highScoreLerp, highScore, elapsed, 0.5));
scoreText.text = 'LEVEL SCORE: ${Math.round(highScoreLerp)}';
@ -386,7 +386,7 @@ class StoryMenuState extends MusicBeatState
if (controls.BACK && !exitingMenu && !selectedLevel)
{
FlxG.sound.play(Paths.sound('cancelMenu'));
FunkinSound.playOnce(Paths.sound('cancelMenu'));
exitingMenu = true;
FlxG.switchState(() -> new MainMenuState());
}
@ -438,6 +438,8 @@ class StoryMenuState extends MusicBeatState
}
}
FunkinSound.playOnce(Paths.sound('scrollMenu'), 0.4);
updateText();
updateBackground(previousLevelId);
updateProps();
@ -450,7 +452,11 @@ class StoryMenuState extends MusicBeatState
*/
function changeDifficulty(change:Int = 0):Void
{
var difficultyList:Array<String> = currentLevel.getDifficulties();
// "For now, NO erect in story mode" -Dave
var difficultyList:Array<String> = Constants.DEFAULT_DIFFICULTY_LIST;
// Use this line to displays all difficulties
// var difficultyList:Array<String> = currentLevel.getDifficulties();
var currentIndex:Int = difficultyList.indexOf(currentDifficultyId);
currentIndex += change;
@ -477,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();
}
@ -515,7 +522,7 @@ class StoryMenuState extends MusicBeatState
{
if (!currentLevel.isUnlocked())
{
FlxG.sound.play(Paths.sound('cancelMenu'));
FunkinSound.playOnce(Paths.sound('cancelMenu'));
return;
}
@ -523,7 +530,7 @@ class StoryMenuState extends MusicBeatState
selectedLevel = true;
FlxG.sound.play(Paths.sound('confirmMenu'));
FunkinSound.playOnce(Paths.sound('confirmMenu'));
currentLevelTitle.isFlashing = true;
@ -552,10 +559,13 @@ class StoryMenuState extends MusicBeatState
FlxTransitionableState.skipNextTransIn = false;
FlxTransitionableState.skipNextTransOut = false;
var targetVariation:String = targetSong.getFirstValidVariation(PlayStatePlaylist.campaignDifficulty);
LoadingState.loadPlayState(
{
targetSong: targetSong,
targetDifficulty: PlayStatePlaylist.campaignDifficulty,
targetVariation: targetVariation
}, true);
});
}

View File

@ -22,7 +22,11 @@ class AttractState extends MusicBeatState
public override function create():Void
{
// Pause existing music.
FlxG.sound.music.stop();
if (FlxG.sound.music != null)
{
FlxG.sound.music.destroy();
FlxG.sound.music = null;
}
#if html5
playVideoHTML5(ATTRACT_VIDEO_PATH);

View File

@ -222,9 +222,14 @@ class TitleState extends MusicBeatState
{
var shouldFadeIn = (FlxG.sound.music == null);
// Load music. Includes logic to handle BPM changes.
FunkinSound.playMusic('freakyMenu', false, true);
FunkinSound.playMusic('freakyMenu',
{
startingVolume: 0.0,
overrideExisting: true,
restartTrack: true
});
// Fade from 0.0 to 0.7 over 4 seconds
if (shouldFadeIn) FlxG.sound.music.fadeIn(4, 0, 0.7);
if (shouldFadeIn) FlxG.sound.music.fadeIn(4.0, 0.0, 0.7);
}
function getIntroTextShit():Array<Array<String>>
@ -322,7 +327,7 @@ class TitleState extends MusicBeatState
if (Date.now().getDay() == 5) NGio.unlockMedal(61034);
titleText.animation.play('press');
FlxG.camera.flash(FlxColor.WHITE, 1);
FlxG.sound.play(Paths.sound('confirmMenu'), 0.7);
FunkinSound.playOnce(Paths.sound('confirmMenu'), 0.7);
transitioning = true;
var targetState:NextState = () -> new MainMenuState();
@ -337,7 +342,7 @@ class TitleState extends MusicBeatState
// ngSpr??
FlxG.switchState(targetState);
});
// FlxG.sound.play(Paths.music('titleShoot'), 0.7);
// FunkinSound.playOnce(Paths.music('titleShoot'), 0.7);
}
if (pressedEnter && !skippedIntro && initialized) skipIntro();
@ -384,14 +389,12 @@ class TitleState extends MusicBeatState
{
cheatActive = true;
FlxG.sound.playMusic(Paths.music('tutorialTitle'), 1);
var spec:SpectogramSprite = new SpectogramSprite(FlxG.sound.music);
add(spec);
Conductor.instance.forceBPM(190);
FlxG.camera.flash(FlxColor.WHITE, 1);
FlxG.sound.play(Paths.sound('confirmMenu'), 0.7);
FunkinSound.playOnce(Paths.sound('confirmMenu'), 0.7);
}
function createCoolText(textArray:Array<String>)
@ -452,9 +455,9 @@ class TitleState extends MusicBeatState
switch (i + 1)
{
case 1:
createCoolText(['ninjamuffin99', 'phantomArcade', 'kawaisprite', 'evilsk8r']);
createCoolText(['The', 'Funkin Crew Inc']);
case 3:
addMoreText('present');
addMoreText('presents');
case 4:
deleteCoolText();
case 5:

View File

@ -171,7 +171,12 @@ class LoadingState extends MusicBeatState
function onLoad():Void
{
if (stopMusic && FlxG.sound.music != null) FlxG.sound.music.stop();
// Stop the instrumental.
if (stopMusic && FlxG.sound.music != null)
{
FlxG.sound.music.destroy();
FlxG.sound.music = null;
}
FlxG.switchState(target);
}
@ -200,7 +205,8 @@ class LoadingState extends MusicBeatState
// All assets preloaded, switch directly to play state (defualt on other targets).
if (shouldStopMusic && FlxG.sound.music != null)
{
FlxG.sound.music.stop();
FlxG.sound.music.destroy();
FlxG.sound.music = null;
}
// Load and cache the song's charts.

View File

@ -18,6 +18,7 @@ import flixel.addons.transition.FlxTransitionableState;
import openfl.display.BitmapData;
import funkin.ui.freeplay.FreeplayState;
import openfl.geom.Matrix;
import funkin.audio.FunkinSound;
import openfl.display.Sprite;
import openfl.display.Bitmap;
import flixel.FlxState;
@ -137,7 +138,7 @@ class StickerSubState extends MusicBeatSubState
new FlxTimer().start(sticker.timing, _ -> {
sticker.visible = false;
var daSound:String = FlxG.random.getObject(sounds);
FlxG.sound.play(Paths.sound(daSound));
FunkinSound.playOnce(Paths.sound(daSound));
if (grpStickers == null || ind == grpStickers.members.length - 1)
{
@ -227,7 +228,7 @@ class StickerSubState extends MusicBeatSubState
sticker.visible = true;
var daSound:String = FlxG.random.getObject(sounds);
FlxG.sound.play(Paths.sound(daSound));
FunkinSound.playOnce(Paths.sound(daSound));
var frameTimer:Int = FlxG.random.int(0, 2);

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
*/
@ -157,6 +206,11 @@ class Constants
*/
public static final DEFAULT_VARIATION:String = 'default';
/**
* Standard variations used by the game.
*/
public static final DEFAULT_VARIATION_LIST:Array<String> = ['default', 'erect', 'pico'];
/**
* The default intensity for camera zooms.
*/
@ -175,17 +229,22 @@ class Constants
/**
* The default name for songs.
*/
public static final DEFAULT_SONGNAME:String = "Unknown";
public static final DEFAULT_SONGNAME:String = 'Unknown';
/**
* The default artist for songs.
*/
public static final DEFAULT_ARTIST:String = "Unknown";
public static final DEFAULT_ARTIST:String = 'Unknown';
/**
* The default note style for songs.
*/
public static final DEFAULT_NOTE_STYLE:String = "funkin";
public static final DEFAULT_NOTE_STYLE:String = 'funkin';
/**
* The default album for songs in Freeplay.
*/
public static final DEFAULT_ALBUM_ID:String = 'volume1';
/**
* The default timing format for songs.
@ -279,6 +338,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

@ -13,12 +13,25 @@ class MathUtil
/**
* Perform linear interpolation between the base and the target, based on the current framerate.
* @param base The starting value, when `progress <= 0`.
* @param target The ending value, when `progress >= 1`.
* @param ratio Value used to interpolate between `base` and `target`.
*
* @return The interpolated value.
*/
@:deprecated('Use smoothLerp instead')
public static function coolLerp(base:Float, target:Float, ratio:Float):Float
{
return base + cameraLerp(ratio) * (target - base);
}
/**
* Perform linear interpolation based on the current framerate.
* @param lerp Value used to interpolate between `base` and `target`.
*
* @return The interpolated value.
*/
@:deprecated('Use smoothLerp instead')
public static function cameraLerp(lerp:Float):Float
{
return lerp * (FlxG.elapsed / (1 / 60));
@ -30,26 +43,60 @@ class MathUtil
* @param value The value to get the logarithm of.
* @return `log_base(value)`
*/
public static function logBase(base:Float, value:Float)
public static function logBase(base:Float, value:Float):Float
{
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);
}
/**
* @returns `2^x`
* Get the base-2 logarithm of a value.
* @param x value
* @return `2^x`
*/
public static function exp2(x:Float)
public static function exp2(x:Float):Float
{
return Math.pow(2, x);
}
/**
* Linearly interpolate between two values.
*
* @param base The starting value, when `progress <= 0`.
* @param target The ending value, when `progress >= 1`.
* @param progress Value used to interpolate between `base` and `target`.
* @return The interpolated value.
*/
public static function lerp(base:Float, target:Float, progress:Float)
public static function lerp(base:Float, target:Float, progress:Float):Float
{
return base + progress * (target - base);
}
@ -62,12 +109,23 @@ class MathUtil
* @param duration The total duration of the interpolation. Nominal duration until remaining distance is less than `precision`.
* @param precision The target precision of the interpolation. Defaults to 1% of distance remaining.
* @see https://twitter.com/FreyaHolmer/status/1757918211679650262
*
* @return A value between the current value and the target value.
*/
public static function smoothLerp(current:Float, target:Float, elapsed:Float, duration:Float, precision:Float = 1 / 100):Float
{
// An alternative algorithm which uses a separate half-life value:
// var halfLife:Float = -duration / logBase(2, precision);
// lerp(current, target, 1 - exp2(-elapsed / halfLife));
return lerp(current, target, 1 - Math.pow(precision, elapsed / duration));
if (current == target) return target;
var result:Float = lerp(current, target, 1 - Math.pow(precision, elapsed / duration));
// TODO: Is there a better way to ensure a lerp which actually reaches the target?
// Research a framerate-independent PID lerp.
if (Math.abs(result - target) < (precision * target)) result = target;
return result;
}
}

View File

@ -16,7 +16,7 @@ class MemoryUtil
public static function buildGCInfo():String
{
#if cpp
var result = "HXCPP-Immix:";
var result:String = 'HXCPP-Immix:';
result += '\n- Memory Used: ${cpp.vm.Gc.memInfo(cpp.vm.Gc.MEM_INFO_USAGE)} bytes';
result += '\n- Memory Reserved: ${cpp.vm.Gc.memInfo(cpp.vm.Gc.MEM_INFO_RESERVED)} bytes';
result += '\n- Memory Current Pool: ${cpp.vm.Gc.memInfo(cpp.vm.Gc.MEM_INFO_CURRENT)} bytes';
@ -35,10 +35,10 @@ class MemoryUtil
result += '\n- HXCPP C++11: ${#if HXCPP_CPP11 'Enabled' #else 'Disabled' #end}';
result += '\n- Source Annotation: ${#if annotate_source 'Enabled' #else 'Disabled' #end}';
#elseif js
var result = "JS-MNS:";
var result:String = 'JS-MNS:';
result += '\n- Memory Used: ${getMemoryUsed()} bytes';
#else
var result = "Unknown GC";
var result:String = 'Unknown GC';
#end
return result;
@ -66,7 +66,7 @@ class MemoryUtil
#if cpp
cpp.vm.Gc.enable(true);
#else
throw "Not implemented!";
throw 'Not implemented!';
#end
}
@ -78,7 +78,7 @@ class MemoryUtil
#if cpp
cpp.vm.Gc.enable(false);
#else
throw "Not implemented!";
throw 'Not implemented!';
#end
}
@ -92,7 +92,7 @@ class MemoryUtil
#if cpp
cpp.vm.Gc.run(major);
#else
throw "Not implemented!";
throw 'Not implemented!';
#end
}
@ -107,7 +107,7 @@ class MemoryUtil
#if cpp
cpp.vm.Gc.compact();
#else
throw "Not implemented!";
throw 'Not implemented!';
#end
}
}

View File

@ -38,6 +38,9 @@ class MouseUtil
}
}
/**
* Increment the zoom level of the current camera by the mouse wheel scroll value.
*/
public static function mouseWheelZoom():Void
{
if (FlxG.mouse.wheel != 0) FlxG.camera.zoom += FlxG.mouse.wheel * (0.1 * FlxG.camera.zoom);

View File

@ -16,9 +16,9 @@ class PlatformUtil
#if mac
return true;
#elseif html5
return js.Browser.window.navigator.platform.startsWith("Mac")
|| js.Browser.window.navigator.platform.startsWith("iPad")
|| js.Browser.window.navigator.platform.startsWith("iPhone");
return js.Browser.window.navigator.platform.startsWith('Mac')
|| js.Browser.window.navigator.platform.startsWith('iPad')
|| js.Browser.window.navigator.platform.startsWith('iPhone');
#else
return false;
#end
@ -27,7 +27,7 @@ class PlatformUtil
/**
* Detects and returns the current host platform.
* Always returns `HTML5` on web, regardless of the computer running that browser.
* Returns `null` if the platform could not be detected.
* @return The host platform, or `null` if the platform could not be detected.
*/
public static function detectHostPlatform():Null<HostPlatform>
{

View File

@ -27,29 +27,53 @@ class SortUtil
/**
* You can use this function in FlxTypedGroup.sort() to sort FlxObjects by their z-index values.
* The value defaults to 0, but by assigning it you can easily rearrange objects as desired.
*
* @param order Either `FlxSort.ASCENDING` or `FlxSort.DESCENDING`
* @param a The first FlxObject to compare.
* @param b The second FlxObject to compare.
* @return 1 if `a` has a higher z-index, -1 if `b` has a higher z-index.
*/
public static inline function byZIndex(Order:Int, Obj1:FlxBasic, Obj2:FlxBasic):Int
public static inline function byZIndex(order:Int, a:FlxBasic, b:FlxBasic):Int
{
if (Obj1 == null || Obj2 == null) return 0;
return FlxSort.byValues(Order, Obj1.zIndex, Obj2.zIndex);
if (a == null || b == null) return 0;
return FlxSort.byValues(order, a.zIndex, b.zIndex);
}
/**
* Given two Notes, returns 1 or -1 based on whether `a` or `b` has an earlier strumtime.
*
* @param order Either `FlxSort.ASCENDING` or `FlxSort.DESCENDING`
* @param a The first Note to compare.
* @param b The second Note to compare.
* @return 1 if `a` has an earlier strumtime, -1 if `b` has an earlier strumtime.
*/
public static inline function byStrumtime(order:Int, a:NoteSprite, b:NoteSprite)
public static inline function byStrumtime(order:Int, a:NoteSprite, b:NoteSprite):Int
{
return noteDataByTime(order, a.noteData, b.noteData);
}
public static inline function noteDataByTime(order:Int, a:SongNoteData, b:SongNoteData)
/**
* Given two Note Data objects, returns 1 or -1 based on whether `a` or `b` has an earlier time.
*
* @param order Either `FlxSort.ASCENDING` or `FlxSort.DESCENDING`
* @param a The first Event to compare.
* @param b The second Event to compare.
* @return 1 if `a` has an earlier time, -1 if `b` has an earlier time.
*/
public static inline function noteDataByTime(order:Int, a:SongNoteData, b:SongNoteData):Int
{
return FlxSort.byValues(order, a.time, b.time);
}
public static inline function eventDataByTime(order:Int, a:SongEventData, b:SongEventData)
/**
* Given two Event Data objects, returns 1 or -1 based on whether `a` or `b` has an earlier time.
*
* @param order Either `FlxSort.ASCENDING` or `FlxSort.DESCENDING`
* @param a The first Event to compare.
* @param b The second Event to compare.
* @return 1 if `a` has an earlier time, -1 if `b` has an earlier time.
*/
public static inline function eventDataByTime(order:Int, a:SongEventData, b:SongEventData):Int
{
return FlxSort.byValues(order, a.time, b.time);
}
@ -58,8 +82,11 @@ class SortUtil
* Given two FlxFrames, sort their names alphabetically.
*
* @param order Either `FlxSort.ASCENDING` or `FlxSort.DESCENDING`
* @param a The first Frame to compare.
* @param b The second Frame to compare.
* @return 1 if `a` has an earlier time, -1 if `b` has an earlier time.
*/
public static inline function byFrameName(a:FlxFrame, b:FlxFrame)
public static inline function byFrameName(a:FlxFrame, b:FlxFrame):Int
{
return alphabetically(a.name, b.name);
}
@ -68,6 +95,7 @@ class SortUtil
* Sort predicate for sorting strings alphabetically.
* @param a The first string to compare.
* @param b The second string to compare.
* @return 1 if `a` comes before `b`, -1 if `b` comes before `a`, 0 if they are equal
*/
public static function alphabetically(a:String, b:String):Int
{
@ -81,9 +109,11 @@ class SortUtil
/**
* Sort predicate which sorts two strings alphabetically, but prioritizes a specific string first.
* Example usage: `array.sort(defaultThenAlphabetical.bind('test'))` will sort the array so that the string 'test' is first.
*
* @param defaultValue The value to prioritize.
* @param a The first string to compare.
* @param b The second string to compare.
* @param defaultValue The value to prioritize.
* @return 1 if `a` comes before `b`, -1 if `b` comes before `a`, 0 if they are equal
*/
public static function defaultThenAlphabetically(defaultValue:String, a:String, b:String):Int
{
@ -96,9 +126,11 @@ class SortUtil
/**
* Sort predicate which sorts two strings alphabetically, but prioritizes a specific string first.
* Example usage: `array.sort(defaultsThenAlphabetical.bind(['test']))` will sort the array so that the string 'test' is first.
*
* @param defaultValues The values to prioritize.
* @param a The first string to compare.
* @param b The second string to compare.
* @param defaultValues The values to prioritize.
* @return 1 if `a` comes before `b`, -1 if `b` comes before `a`, 0 if they are equal
*/
public static function defaultsThenAlphabetically(defaultValues:Array<String>, a:String, b:String):Int
{

View File

@ -5,23 +5,42 @@ import haxe.Timer;
class TimerUtil
{
/**
* Store the current time.
*/
public static function start():Float
{
return Timer.stamp();
}
private static function took(start:Float, ?end:Float):Float
/**
* Return the elapsed time.
*/
static function took(start:Float, ?end:Float):Float
{
var endOrNow:Float = end != null ? end : Timer.stamp();
return endOrNow - start;
}
/**
* Return the elapsed time in seconds as a string.
* @param start The start time.
* @param end The end time.
* @param precision The number of decimal places to round to.
* @return The elapsed time in seconds as a string.
*/
public static function seconds(start:Float, ?end:Float, ?precision = 2):String
{
var seconds:Float = FloatTools.round(took(start, end), precision);
return '${seconds} seconds';
}
/**
* Return the elapsed time in milliseconds as a string.
* @param start The start time.
* @param end The end time.
* @return The elapsed time in milliseconds as a string.
*/
public static function ms(start:Float, ?end:Float):String
{
var seconds:Float = took(start, end);

View File

@ -18,7 +18,7 @@ class TrackerUtil
public static function initTrackers():Void
{
#if FLX_DEBUG
Tracker.addProfile(new TrackerProfile(Highscore, ["tallies"]));
Tracker.addProfile(new TrackerProfile(Highscore, ['tallies']));
FlxG.console.registerClass(Highscore);
#end
}

View File

@ -15,6 +15,9 @@ 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.
* @param version The semantic version to validate.
* @param versionRule The version rule to validate against.
* @return `true` if the version satisfies the rule, `false` otherwise.
*/
public static function validateVersion(version:thx.semver.Version, versionRule:thx.semver.VersionRule):Bool
{
@ -32,6 +35,9 @@ 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.
* @param version The semantic version to validate.
* @param versionRule The version rule to validate against.
* @return `true` if the version satisfies the rule, `false` otherwise.
*/
public static function validateVersionStr(version:String, versionRule:String):Bool
{
@ -56,7 +62,7 @@ class VersionUtil
public static function getVersionFromJSON(input:Null<String>):Null<thx.semver.Version>
{
if (input == null) return null;
var parsed = SerializerUtil.fromJSON(input);
var parsed:Dynamic = SerializerUtil.fromJSON(input);
if (parsed == null) return null;
if (parsed.version == null) return null;
var versionStr:String = parsed.version; // Dynamic -> String cast
@ -64,6 +70,11 @@ class VersionUtil
return version;
}
/**
* Get and parse the semantic version from a JSON string.
* @param input The JSON string to parse.
* @return The semantic version, or null if it could not be parsed.
*/
public static function parseVersion(input:Dynamic):Null<thx.semver.Version>
{
if (input == null) return null;

View File

@ -24,7 +24,7 @@ class WindowUtil
{
#if CAN_OPEN_LINKS
#if linux
Sys.command('/usr/bin/xdg-open', [targetUrl, "&"]);
Sys.command('/usr/bin/xdg-open', [targetUrl, '&']);
#else
// This should work on Windows and HTML5.
FlxG.openURL(targetUrl);
@ -42,7 +42,7 @@ class WindowUtil
{
#if CAN_OPEN_LINKS
#if windows
Sys.command('explorer', [targetPath.replace("/", "\\")]);
Sys.command('explorer', [targetPath.replace('/', '\\')]);
#elseif mac
Sys.command('open', [targetPath]);
#elseif linux
@ -61,9 +61,9 @@ class WindowUtil
{
#if CAN_OPEN_LINKS
#if windows
Sys.command('explorer', ["/select," + targetPath.replace("/", "\\")]);
Sys.command('explorer', ['/select,' + targetPath.replace('/', '\\')]);
#elseif mac
Sys.command('open', ["-R", targetPath]);
Sys.command('open', ['-R', targetPath]);
#elseif linux
// TODO: unsure of the linux equivalent to opening a folder and then "selecting" a file.
Sys.command('open', [targetPath]);
@ -82,7 +82,7 @@ class WindowUtil
* Wires up FlxSignals that happen based on window activity.
* For example, we can run a callback when the window is closed.
*/
public static function initWindowEvents()
public static function initWindowEvents():Void
{
// onUpdate is called every frame just before rendering.
@ -95,7 +95,7 @@ class WindowUtil
/**
* Turns off that annoying "Report to Microsoft" dialog that pops up when the game crashes.
*/
public static function disableCrashHandler()
public static function disableCrashHandler():Void
{
#if (cpp && windows)
untyped __cpp__('SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX);');

View File

@ -174,7 +174,7 @@ class ScreenshotPlugin extends FlxBasic
FlxTween.tween(flashSpr, {alpha: 0}, 0.15, {ease: FlxEase.quadOut, onComplete: _ -> FlxG.stage.removeChild(flashSpr)});
// Play a sound (auto-play is true).
FunkinSound.load(Paths.sound('screenshot'), 1.0, false, true, true);
FunkinSound.playOnce(Paths.sound('screenshot'), 1.0);
}
static final PREVIEW_INITIAL_DELAY = 0.25; // How long before the preview starts fading in.

View File

@ -1,3 +1,6 @@
package haxe.ui.backend.flixel;
/**
* Override HaxeUI to use `MusicBeatState` instead of `FlxState`.
*/
typedef UIStateBase = funkin.ui.MusicBeatState;

View File

@ -1,3 +1,6 @@
package haxe.ui.backend.flixel;
/**
* Override HaxeUI to use `MusicBeatSubState` instead of `FlxSubState`.
*/
typedef UISubStateBase = funkin.ui.MusicBeatSubState;

View File

@ -1,32 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<TextureAtlas imagePath="arrows.png">
<SubTexture name="staticLeft0001" x="0" y="0" width="17" height="17"/>
<SubTexture name="staticDown0001" x="17" y="0" width="17" height="17"/>
<SubTexture name="staticUp0001" x="34" y="0" width="17" height="17"/>
<SubTexture name="staticRight0001" x="51" y="0" width="17" height="17"/>
<SubTexture name="noteLeft0001" x="0" y="17" width="17" height="17"/>
<SubTexture name="noteDown0001" x="17" y="17" width="17" height="17"/>
<SubTexture name="noteUp0001" x="34" y="17" width="17" height="17"/>
<SubTexture name="noteRight0001" x="51" y="17" width="17" height="17"/>
<SubTexture name="pressedLeft0001" x="0" y="17" width="17" height="17"/>
<SubTexture name="pressedDown0001" x="17" y="17" width="17" height="17"/>
<SubTexture name="pressedUp0001" x="34" y="17" width="17" height="17"/>
<SubTexture name="pressedRight0001" x="51" y="17" width="17" height="17"/>
<SubTexture name="pressedLeft0002" x="0" y="34" width="17" height="17"/>
<SubTexture name="pressedDown0002" x="17" y="34" width="17" height="17"/>
<SubTexture name="pressedUp0002" x="34" y="34" width="17" height="17"/>
<SubTexture name="pressedRight0002" x="51" y="34" width="17" height="17"/>
<SubTexture name="confirmLeft0001" x="0" y="51" width="17" height="17"/>
<SubTexture name="confirmDown0001" x="17" y="51" width="17" height="17"/>
<SubTexture name="confirmUp0001" x="34" y="51" width="17" height="17"/>
<SubTexture name="confirmRight0001" x="51" y="51" width="17" height="17"/>
<SubTexture name="confirmLeft0002" x="0" y="68" width="17" height="17"/>
<SubTexture name="confirmDown0002" x="17" y="68" width="17" height="17"/>
<SubTexture name="confirmUp0002" x="34" y="68" width="17" height="17"/>
<SubTexture name="confirmRight0002" x="51" y="68" width="17" height="17"/>
<SubTexture name="staticLeft0001" x="0" y="0" width="17" height="17" />
<SubTexture name="staticDown0001" x="17" y="0" width="17" height="17" />
<SubTexture name="staticUp0001" x="34" y="0" width="17" height="17" />
<SubTexture name="staticRight0001" x="51" y="0" width="17" height="17" />
<SubTexture name="noteLeft0001" x="0" y="17" width="17" height="17" />
<SubTexture name="noteDown0001" x="17" y="17" width="17" height="17" />
<SubTexture name="noteUp0001" x="34" y="17" width="17" height="17" />
<SubTexture name="noteRight0001" x="51" y="17" width="17" height="17" />
<SubTexture name="pressedLeft0001" x="0" y="17" width="17" height="17" />
<SubTexture name="pressedDown0001" x="17" y="17" width="17" height="17" />
<SubTexture name="pressedUp0001" x="34" y="17" width="17" height="17" />
<SubTexture name="pressedRight0001" x="51" y="17" width="17" height="17" />
<SubTexture name="pressedLeft0002" x="0" y="34" width="17" height="17" />
<SubTexture name="pressedDown0002" x="17" y="34" width="17" height="17" />
<SubTexture name="pressedUp0002" x="34" y="34" width="17" height="17" />
<SubTexture name="pressedRight0002" x="51" y="34" width="17" height="17" />
<SubTexture name="confirmLeft0001" x="0" y="51" width="17" height="17" />
<SubTexture name="confirmDown0001" x="17" y="51" width="17" height="17" />
<SubTexture name="confirmUp0001" x="34" y="51" width="17" height="17" />
<SubTexture name="confirmRight0001" x="51" y="51" width="17" height="17" />
<SubTexture name="confirmLeft0002" x="0" y="68" width="17" height="17" />
<SubTexture name="confirmDown0002" x="17" y="68" width="17" height="17" />
<SubTexture name="confirmUp0002" x="34" y="68" width="17" height="17" />
<SubTexture name="confirmRight0002" x="51" y="68" width="17" height="17" />
</TextureAtlas>