1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2024-07-15 06:35:33 +00:00

Fix a dozen tiny issues with 2hot's audio and visuals (and some script crashes!).

This commit is contained in:
EliteMasterEric 2024-03-16 00:55:57 -04:00
parent 5733386519
commit d56c33cd17
16 changed files with 332 additions and 150 deletions

2
assets

@ -1 +1 @@
Subproject commit 0e2c5bf2134c7e517b70cf74afd58abe5c7b5e50 Subproject commit 82ee26e1f733d1b2f30015ae69925e6a39d6526b

View file

@ -9,7 +9,7 @@ import openfl.utils.Assets as OpenFlAssets;
*/ */
class Paths class Paths
{ {
static var currentLevel:String; static var currentLevel:Null<String> = null;
static public function setCurrentLevel(name:String) static public function setCurrentLevel(name:String)
{ {

View file

@ -276,16 +276,27 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
* Creates a new `FunkinSound` object and loads it as the current music track. * 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 key The key of the music you want to play. Music should be at `music/<key>/<key>.ogg`.
* @param startingVolume The volume you want the music to start at. * @param params A set of additional optional parameters.
* @param overrideExisting Whether to override music if it is already playing.
* @param mapTimeChanges Whether to check for `SongMusicData` to update the Conductor with.
* Data should be at `music/<key>/<key>-metadata.json`. * Data should be at `music/<key>/<key>-metadata.json`.
*/ */
public static function playMusic(key:String, startingVolume:Float = 1.0, overrideExisting:Bool = false, mapTimeChanges:Bool = true):Void public static function playMusic(key:String, params:FunkinSoundPlayMusicParams):Void
{ {
if (!overrideExisting && FlxG.sound.music?.playing) return; if (!(params.overrideExisting ?? false) && FlxG.sound.music?.playing) return;
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;
}
}
}
if (params?.mapTimeChanges ?? true)
{ {
var songMusicData:Null<SongMusicData> = SongRegistry.instance.parseMusicData(key); 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. // Will fall back and return null if the metadata doesn't exist or can't be parsed.
@ -299,7 +310,13 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
} }
} }
FlxG.sound.music = FunkinSound.load(Paths.music('$key/$key'), startingVolume); if (FlxG.sound.music != null)
{
FlxG.sound.music.stop();
FlxG.sound.music.kill();
}
FlxG.sound.music = FunkinSound.load(Paths.music('$key/$key'), params?.startingVolume ?? 1.0, true, false, true);
// Prevent repeat update() and onFocus() calls. // Prevent repeat update() and onFocus() calls.
FlxG.sound.list.remove(FlxG.sound.music); FlxG.sound.list.remove(FlxG.sound.music);
@ -333,10 +350,10 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
sound._label = embeddedSound; sound._label = embeddedSound;
} }
if (autoPlay) sound.play();
sound.volume = volume; sound.volume = volume;
sound.group = FlxG.sound.defaultSoundGroup; sound.group = FlxG.sound.defaultSoundGroup;
sound.persist = true; sound.persist = true;
if (autoPlay) sound.play();
// Call onLoad() because the sound already loaded // Call onLoad() because the sound already loaded
if (onLoad != null && sound._sound != null) onLoad(); if (onLoad != null && sound._sound != null) onLoad();
@ -356,3 +373,33 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
return sound; 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 to check for `SongMusicData` to update the Conductor with.
* @default `true`
*/
var ?mapTimeChanges:Bool;
}

View file

@ -151,14 +151,14 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
/** /**
* Stop all the sounds in the group. * Stop all the sounds in the group.
*/ */
public function stop() public function stop():Void
{ {
forEachAlive(function(sound:FunkinSound) { forEachAlive(function(sound:FunkinSound) {
sound.stop(); sound.stop();
}); });
} }
public override function destroy() public override function destroy():Void
{ {
stop(); stop();
super.destroy(); super.destroy();
@ -176,10 +176,15 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
function get_time():Float function get_time():Float
{ {
if (getFirstAlive() != null) return getFirstAlive().time; if (getFirstAlive() != null)
{
return getFirstAlive().time;
}
else else
{
return 0; return 0;
} }
}
function set_time(time:Float):Float function set_time(time:Float):Float
{ {
@ -193,17 +198,27 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
function get_playing():Bool function get_playing():Bool
{ {
if (getFirstAlive() != null) return getFirstAlive().playing; if (getFirstAlive() != null)
{
return getFirstAlive().playing;
}
else else
{
return false; return false;
} }
}
function get_volume():Float function get_volume():Float
{ {
if (getFirstAlive() != null) return getFirstAlive().volume; if (getFirstAlive() != null)
{
return getFirstAlive().volume;
}
else else
{
return 1; return 1;
} }
}
// in PlayState, adjust the code so that it only mutes the player1 vocal tracks? // in PlayState, adjust the code so that it only mutes the player1 vocal tracks?
function set_volume(volume:Float):Float function set_volume(volume:Float):Float

View file

@ -8,7 +8,12 @@ import funkin.modding.IScriptedClass;
*/ */
class ScriptEventDispatcher class ScriptEventDispatcher
{ {
public static function callEvent(target:IScriptedClass, event:ScriptEvent):Void /**
* Invoke the given event hook on the given scripted class.
* @param target The target class to call script hooks on.
* @param event The event, which determines the script hook to call and provides parameters for it.
*/
public static function callEvent(target:Null<IScriptedClass>, event:ScriptEvent):Void
{ {
if (target == null || event == null) return; if (target == null || event == null) return;

View file

@ -2,20 +2,18 @@ package funkin.play;
import flixel.FlxG; import flixel.FlxG;
import flixel.FlxObject; import flixel.FlxObject;
import flixel.FlxSprite; import flixel.input.touch.FlxTouch;
import flixel.sound.FlxSound;
import funkin.audio.FunkinSound;
import flixel.util.FlxColor; import flixel.util.FlxColor;
import flixel.util.FlxTimer; import flixel.util.FlxTimer;
import funkin.audio.FunkinSound;
import funkin.graphics.FunkinSprite; import funkin.graphics.FunkinSprite;
import funkin.modding.events.ScriptEvent; import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEventDispatcher; import funkin.modding.events.ScriptEventDispatcher;
import funkin.play.character.BaseCharacter; import funkin.play.character.BaseCharacter;
import funkin.play.PlayState;
import funkin.util.MathUtil;
import funkin.ui.freeplay.FreeplayState; import funkin.ui.freeplay.FreeplayState;
import funkin.ui.MusicBeatSubState; import funkin.ui.MusicBeatSubState;
import funkin.ui.story.StoryMenuState; import funkin.ui.story.StoryMenuState;
import funkin.util.MathUtil;
import openfl.utils.Assets; import openfl.utils.Assets;
/** /**
@ -24,13 +22,14 @@ import openfl.utils.Assets;
* *
* The newest implementation uses a substate, which prevents having to reload the song and stage each reset. * The newest implementation uses a substate, which prevents having to reload the song and stage each reset.
*/ */
@:nullSafety
class GameOverSubState extends MusicBeatSubState class GameOverSubState extends MusicBeatSubState
{ {
/** /**
* The currently active GameOverSubState. * The currently active GameOverSubState.
* There should be only one GameOverSubState in existance at a time, we can use a singleton. * There should be only one GameOverSubState in existance at a time, we can use a singleton.
*/ */
public static var instance:GameOverSubState = null; public static var instance:Null<GameOverSubState> = null;
/** /**
* Which alternate animation on the character to use. * Which alternate animation on the character to use.
@ -38,7 +37,7 @@ class GameOverSubState extends MusicBeatSubState
* For example, playing a different animation when BF dies in Week 4 * For example, playing a different animation when BF dies in Week 4
* or Pico dies in Weekend 1. * or Pico dies in Weekend 1.
*/ */
public static var animationSuffix:String = ""; public static var animationSuffix:String = '';
/** /**
* Which alternate game over music to use. * Which alternate game over music to use.
@ -46,17 +45,19 @@ class GameOverSubState extends MusicBeatSubState
* For example, the bf-pixel script sets this to `-pixel` * For example, the bf-pixel script sets this to `-pixel`
* and the pico-playable script sets this to `Pico`. * and the pico-playable script sets this to `Pico`.
*/ */
public static var musicSuffix:String = ""; public static var musicSuffix:String = '';
/** /**
* Which alternate "blue ball" sound effect to use. * Which alternate "blue ball" sound effect to use.
*/ */
public static var blueBallSuffix:String = ""; public static var blueBallSuffix:String = '';
static var blueballed:Bool = false;
/** /**
* The boyfriend character. * The boyfriend character.
*/ */
var boyfriend:BaseCharacter; var boyfriend:Null<BaseCharacter> = null;
/** /**
* The invisible object in the scene which the camera focuses on. * The invisible object in the scene which the camera focuses on.
@ -83,7 +84,8 @@ class GameOverSubState extends MusicBeatSubState
var transparent:Bool; var transparent:Bool;
final CAMERA_ZOOM_DURATION:Float = 0.5; static final CAMERA_ZOOM_DURATION:Float = 0.5;
var targetCameraZoom:Float = 1.0; var targetCameraZoom:Float = 1.0;
public function new(params:GameOverParams) public function new(params:GameOverParams)
@ -92,24 +94,27 @@ class GameOverSubState extends MusicBeatSubState
this.isChartingMode = params?.isChartingMode ?? false; this.isChartingMode = params?.isChartingMode ?? false;
transparent = params.transparent; transparent = params.transparent;
cameraFollowPoint = new FlxObject(PlayState.instance.cameraFollowPoint.x, PlayState.instance.cameraFollowPoint.y, 1, 1);
} }
/** /**
* Reset the game over configuration to the default. * Reset the game over configuration to the default.
*/ */
public static function reset() public static function reset():Void
{ {
animationSuffix = ""; animationSuffix = '';
musicSuffix = ""; musicSuffix = '';
blueBallSuffix = ""; blueBallSuffix = '';
blueballed = false;
} }
override public function create() public override function create():Void
{ {
if (instance != null) if (instance != null)
{ {
// TODO: Do something in this case? IDK. // TODO: Do something in this case? IDK.
trace('WARNING: GameOverSubState instance already exists. This should not happen.'); FlxG.log.warn('WARNING: GameOverSubState instance already exists. This should not happen.');
} }
instance = this; instance = this;
@ -120,7 +125,7 @@ class GameOverSubState extends MusicBeatSubState
// //
// Add a black background to the screen. // Add a black background to the screen.
var bg = new FunkinSprite().makeSolidColor(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK); var bg:FunkinSprite = 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, // We make this transparent so that we can see the stage underneath during debugging,
// but it's normally opaque. // but it's normally opaque.
bg.alpha = transparent ? 0.25 : 1.0; bg.alpha = transparent ? 0.25 : 1.0;
@ -135,18 +140,7 @@ class GameOverSubState extends MusicBeatSubState
add(boyfriend); add(boyfriend);
boyfriend.resetCharacter(); boyfriend.resetCharacter();
// Assign a camera follow point to the boyfriend's position. setCameraTarget();
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();
// //
// Set up the audio // Set up the audio
@ -156,6 +150,26 @@ class GameOverSubState extends MusicBeatSubState
Conductor.instance.update(0); Conductor.instance.update(0);
} }
@:nullSafety(Off)
function setCameraTarget():Void
{
// Assign a camera follow point to the boyfriend's position.
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, Constants.DEFAULT_CAMERA_FOLLOW_RATE / 2);
targetCameraZoom = (PlayState?.instance?.currentStage?.camZoom ?? 1.0) * boyfriend.getDeathCameraZoom();
}
/**
* Forcibly reset the camera zoom level to that of the current stage.
* This prevents camera zoom events from adversely affecting the game over state.
*/
public function resetCameraZoom():Void public function resetCameraZoom():Void
{ {
// Apply camera zoom level from stage data. // Apply camera zoom level from stage data.
@ -164,12 +178,14 @@ class GameOverSubState extends MusicBeatSubState
var hasStartedAnimation:Bool = false; var hasStartedAnimation:Bool = false;
override function update(elapsed:Float) override function update(elapsed:Float):Void
{ {
if (!hasStartedAnimation) if (!hasStartedAnimation)
{ {
hasStartedAnimation = true; hasStartedAnimation = true;
if (boyfriend != null)
{
if (boyfriend.hasAnimation('fakeoutDeath') && FlxG.random.bool((1 / 4096) * 100)) if (boyfriend.hasAnimation('fakeoutDeath') && FlxG.random.bool((1 / 4096) * 100))
{ {
boyfriend.playAnimation('fakeoutDeath', true, false); boyfriend.playAnimation('fakeoutDeath', true, false);
@ -181,6 +197,7 @@ class GameOverSubState extends MusicBeatSubState
playBlueBalledSFX(); playBlueBalledSFX();
} }
} }
}
// Smoothly lerp the camera // Smoothly lerp the camera
FlxG.camera.zoom = MathUtil.smoothLerp(FlxG.camera.zoom, targetCameraZoom, elapsed, CAMERA_ZOOM_DURATION); FlxG.camera.zoom = MathUtil.smoothLerp(FlxG.camera.zoom, targetCameraZoom, elapsed, CAMERA_ZOOM_DURATION);
@ -192,10 +209,10 @@ class GameOverSubState extends MusicBeatSubState
// MOBILE ONLY: Restart the level when tapping Boyfriend. // MOBILE ONLY: Restart the level when tapping Boyfriend.
if (FlxG.onMobile) if (FlxG.onMobile)
{ {
var touch = FlxG.touches.getFirst(); var touch:FlxTouch = FlxG.touches.getFirst();
if (touch != null) if (touch != null)
{ {
if (touch.overlaps(boyfriend)) if (boyfriend == null || touch.overlaps(boyfriend))
{ {
confirmDeath(); confirmDeath();
} }
@ -215,7 +232,7 @@ class GameOverSubState extends MusicBeatSubState
blueballed = false; blueballed = false;
PlayState.instance.deathCounter = 0; PlayState.instance.deathCounter = 0;
// PlayState.seenCutscene = false; // old thing... // PlayState.seenCutscene = false; // old thing...
gameOverMusic.stop(); if (gameOverMusic != null) gameOverMusic.stop();
if (isChartingMode) if (isChartingMode)
{ {
@ -239,14 +256,14 @@ class GameOverSubState extends MusicBeatSubState
// This enables the stepHit and beatHit events. // This enables the stepHit and beatHit events.
Conductor.instance.update(gameOverMusic.time); Conductor.instance.update(gameOverMusic.time);
} }
else else if (boyfriend != null)
{ {
// Music hasn't started yet. // Music hasn't started yet.
switch (PlayStatePlaylist.campaignId) switch (PlayStatePlaylist.campaignId)
{ {
// TODO: Make the behavior for playing Jeff's voicelines generic or un-hardcoded. // 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. // This will simplify the class and make it easier for mods or future weeks to add death quotes.
case "week7": case 'week7':
if (boyfriend.getCurrentAnimation().startsWith('firstDeath') && boyfriend.isAnimationFinished() && !playingJeffQuote) if (boyfriend.getCurrentAnimation().startsWith('firstDeath') && boyfriend.isAnimationFinished() && !playingJeffQuote)
{ {
playingJeffQuote = true; playingJeffQuote = true;
@ -279,7 +296,7 @@ class GameOverSubState extends MusicBeatSubState
isEnding = true; isEnding = true;
startDeathMusic(1.0, true); // isEnding changes this function's behavior. startDeathMusic(1.0, true); // isEnding changes this function's behavior.
boyfriend.playAnimation('deathConfirm' + animationSuffix, true); if (boyfriend != null) boyfriend.playAnimation('deathConfirm' + animationSuffix, true);
// After the animation finishes... // After the animation finishes...
new FlxTimer().start(0.7, function(tmr:FlxTimer) { new FlxTimer().start(0.7, function(tmr:FlxTimer) {
@ -290,9 +307,12 @@ class GameOverSubState extends MusicBeatSubState
PlayState.instance.needsReset = true; PlayState.instance.needsReset = true;
// Readd Boyfriend to the stage. // Readd Boyfriend to the stage.
if (boyfriend != null)
{
boyfriend.isDead = false; boyfriend.isDead = false;
remove(boyfriend); remove(boyfriend);
PlayState.instance.currentStage.addCharacter(boyfriend, BF); PlayState.instance.currentStage.addCharacter(boyfriend, BF);
}
// Snap reset the camera which may have changed because of the player character data. // Snap reset the camera which may have changed because of the player character data.
resetCameraZoom(); resetCameraZoom();
@ -304,7 +324,7 @@ class GameOverSubState extends MusicBeatSubState
} }
} }
public override function dispatchEvent(event:ScriptEvent) public override function dispatchEvent(event:ScriptEvent):Void
{ {
super.dispatchEvent(event); super.dispatchEvent(event);
@ -317,11 +337,11 @@ class GameOverSubState extends MusicBeatSubState
*/ */
function resolveMusicPath(suffix:String, starting:Bool = false, ending:Bool = false):Null<String> function resolveMusicPath(suffix:String, starting:Bool = false, ending:Bool = false):Null<String>
{ {
var basePath = 'gameplay/gameover/gameOver'; var basePath:String = 'gameplay/gameover/gameOver';
if (starting) basePath += 'Start'; if (ending) basePath += 'End';
else if (ending) basePath += 'End'; else if (starting) basePath += 'Start';
var musicPath = Paths.music(basePath + suffix); var musicPath:String = Paths.music(basePath + suffix);
while (!Assets.exists(musicPath) && suffix.length > 0) while (!Assets.exists(musicPath) && suffix.length > 0)
{ {
suffix = suffix.split('-').slice(0, -1).join('-'); suffix = suffix.split('-').slice(0, -1).join('-');
@ -334,23 +354,26 @@ class GameOverSubState extends MusicBeatSubState
/** /**
* Starts the death music at the appropriate volume. * Starts the death music at the appropriate volume.
* @param startingVolume * @param startingVolume The initial volume for the music.
* @param force Whether or not to force the music to restart.
*/ */
public function startDeathMusic(startingVolume:Float = 1, force:Bool = false):Void public function startDeathMusic(startingVolume:Float = 1, force:Bool = false):Void
{ {
var musicPath = resolveMusicPath(musicSuffix, isStarting, isEnding); var musicPath:Null<String> = resolveMusicPath(musicSuffix, isStarting, isEnding);
var onComplete = null; var onComplete:() -> Void = () -> {};
if (isStarting) if (isStarting)
{ {
if (musicPath == null) if (musicPath == null)
{ {
// Looked for starting music and didn't find it. Use middle music instead.
isStarting = false; isStarting = false;
musicPath = resolveMusicPath(musicSuffix, isStarting, isEnding); musicPath = resolveMusicPath(musicSuffix, isStarting, isEnding);
} }
else else
{ {
onComplete = function() { onComplete = function() {
isStarting = false; isStarting = true;
// We need to force to ensure that the non-starting music plays. // We need to force to ensure that the non-starting music plays.
startDeathMusic(1.0, true); startDeathMusic(1.0, true);
}; };
@ -359,13 +382,16 @@ class GameOverSubState extends MusicBeatSubState
if (musicPath == null) if (musicPath == null)
{ {
trace('Could not find game over music!'); FlxG.log.warn('[GAMEOVER] Could not find game over music at path ($musicPath)!');
return; return;
} }
else if (gameOverMusic == null || !gameOverMusic.playing || force) else if (gameOverMusic == null || !gameOverMusic.playing || force)
{ {
if (gameOverMusic != null) gameOverMusic.stop(); if (gameOverMusic != null) gameOverMusic.stop();
gameOverMusic = FunkinSound.load(musicPath); gameOverMusic = FunkinSound.load(musicPath);
if (gameOverMusic == null) return;
gameOverMusic.volume = startingVolume; gameOverMusic.volume = startingVolume;
gameOverMusic.looped = !(isEnding || isStarting); gameOverMusic.looped = !(isEnding || isStarting);
gameOverMusic.onComplete = onComplete; gameOverMusic.onComplete = onComplete;
@ -378,13 +404,11 @@ class GameOverSubState extends MusicBeatSubState
} }
} }
static var blueballed:Bool = false;
/** /**
* Play the sound effect that occurs when * Play the sound effect that occurs when
* boyfriend's testicles get utterly annihilated. * boyfriend's testicles get utterly annihilated.
*/ */
public static function playBlueBalledSFX() public static function playBlueBalledSFX():Void
{ {
blueballed = true; blueballed = true;
if (Assets.exists(Paths.sound('gameplay/gameover/fnf_loss_sfx' + blueBallSuffix))) if (Assets.exists(Paths.sound('gameplay/gameover/fnf_loss_sfx' + blueBallSuffix)))
@ -403,7 +427,7 @@ class GameOverSubState extends MusicBeatSubState
* Week 7-specific hardcoded behavior, to play a custom death quote. * Week 7-specific hardcoded behavior, to play a custom death quote.
* TODO: Make this a module somehow. * TODO: Make this a module somehow.
*/ */
function playJeffQuote() function playJeffQuote():Void
{ {
var randomCensor:Array<Int> = []; var randomCensor:Array<Int> = [];
@ -418,20 +442,27 @@ class GameOverSubState extends MusicBeatSubState
}); });
} }
public override function destroy() public override function destroy():Void
{ {
super.destroy(); super.destroy();
if (gameOverMusic != null) gameOverMusic.stop(); if (gameOverMusic != null)
{
gameOverMusic.stop();
gameOverMusic = null; gameOverMusic = null;
}
blueballed = false;
instance = null; instance = null;
} }
public override function toString():String public override function toString():String
{ {
return "GameOverSubState"; return 'GameOverSubState';
} }
} }
/**
* Parameters used to instantiate a GameOverSubState.
*/
typedef GameOverParams = typedef GameOverParams =
{ {
var isChartingMode:Bool; var isChartingMode:Bool;

View file

@ -1499,7 +1499,7 @@ class PlayState extends MusicBeatSubState
public function resetCameraZoom():Void public function resetCameraZoom():Void
{ {
// Apply camera zoom level from stage data. // Apply camera zoom level from stage data.
defaultCameraZoom = currentStage.camZoom; defaultCameraZoom = currentStage?.camZoom ?? 1.0;
} }
/** /**
@ -2712,7 +2712,12 @@ class PlayState extends MusicBeatSubState
if (targetSongId == null) if (targetSongId == null)
{ {
FunkinSound.playMusic('freakyMenu'); FunkinSound.playMusic('freakyMenu',
{
startingVolume: 0.0,
overrideExisting: true,
restartTrack: false
});
// transIn = FlxTransitionableState.defaultTransIn; // transIn = FlxTransitionableState.defaultTransIn;
// transOut = FlxTransitionableState.defaultTransOut; // transOut = FlxTransitionableState.defaultTransOut;
@ -2993,7 +2998,10 @@ class PlayState extends MusicBeatSubState
*/ */
public function resetCamera():Void public function resetCamera():Void
{ {
FlxG.camera.follow(cameraFollowPoint, LOCKON, 0.04); // Apply camera zoom level from stage data.
defaultCameraZoom = currentStage?.camZoom ?? 1.0;
FlxG.camera.follow(cameraFollowPoint, LOCKON, Constants.DEFAULT_CAMERA_FOLLOW_RATE);
FlxG.camera.targetOffset.set(); FlxG.camera.targetOffset.set();
FlxG.camera.zoom = defaultCameraZoom; FlxG.camera.zoom = defaultCameraZoom;
// Snap the camera to the follow point immediately. // Snap the camera to the follow point immediately.

View file

@ -5,12 +5,13 @@ package funkin.play;
* *
* TODO: Add getters/setters for all these properties to validate them. * TODO: Add getters/setters for all these properties to validate them.
*/ */
@:nullSafety
class PlayStatePlaylist class PlayStatePlaylist
{ {
/** /**
* Whether the game is currently in Story Mode. If false, we are in Free Play Mode. * Whether the game is currently in Story Mode. If false, we are in Free Play Mode.
*/ */
public static var isStoryMode(default, default):Bool = false; public static var isStoryMode:Bool = false;
/** /**
* The loist of upcoming songs to be played. * The loist of upcoming songs to be played.
@ -31,8 +32,9 @@ class PlayStatePlaylist
/** /**
* The internal ID of the current playlist, for example `week4` or `weekend-1`. * The internal ID of the current playlist, for example `week4` or `weekend-1`.
* @default `null`, used when no playlist is loaded
*/ */
public static var campaignId:String = 'unknown'; public static var campaignId:Null<String> = null;
public static var campaignDifficulty:String = Constants.DEFAULT_DIFFICULTY; public static var campaignDifficulty:String = Constants.DEFAULT_DIFFICULTY;
@ -45,7 +47,7 @@ class PlayStatePlaylist
playlistSongIds = []; playlistSongIds = [];
campaignScore = 0; campaignScore = 0;
campaignTitle = 'UNKNOWN'; campaignTitle = 'UNKNOWN';
campaignId = 'unknown'; campaignId = null;
campaignDifficulty = Constants.DEFAULT_DIFFICULTY; campaignDifficulty = Constants.DEFAULT_DIFFICULTY;
} }
} }

View file

@ -220,7 +220,8 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
if (propSprite.frames == null || propSprite.frames.numFrames == 0) if (propSprite.frames == null || propSprite.frames.numFrames == 0)
{ {
trace(' ERROR: Could not build texture for prop.'); @:privateAccess
trace(' ERROR: Could not build texture for prop. Check the asset path (${Paths.currentLevel ?? 'default'}, ${dataProp.assetPath}).');
continue; continue;
} }

View file

@ -6,18 +6,15 @@ import flixel.addons.transition.FlxTransitionableState;
import flixel.FlxCamera; import flixel.FlxCamera;
import flixel.FlxSprite; import flixel.FlxSprite;
import flixel.FlxSubState; import flixel.FlxSubState;
import flixel.graphics.FlxGraphic;
import flixel.group.FlxGroup.FlxTypedGroup; import flixel.group.FlxGroup.FlxTypedGroup;
import funkin.graphics.FunkinCamera;
import flixel.group.FlxSpriteGroup; import flixel.group.FlxSpriteGroup;
import flixel.input.keyboard.FlxKey; import flixel.input.keyboard.FlxKey;
import funkin.play.PlayStatePlaylist;
import flixel.input.mouse.FlxMouseEvent; import flixel.input.mouse.FlxMouseEvent;
import flixel.math.FlxMath; import flixel.math.FlxMath;
import flixel.math.FlxPoint; import flixel.math.FlxPoint;
import flixel.math.FlxRect; import flixel.math.FlxRect;
import flixel.sound.FlxSound; import flixel.sound.FlxSound;
import flixel.system.debug.log.LogStyle;
import flixel.system.FlxAssets.FlxSoundAsset;
import flixel.text.FlxText; import flixel.text.FlxText;
import flixel.tweens.FlxEase; import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween; import flixel.tweens.FlxTween;
@ -27,26 +24,19 @@ import flixel.util.FlxSort;
import flixel.util.FlxTimer; import flixel.util.FlxTimer;
import funkin.audio.FunkinSound; import funkin.audio.FunkinSound;
import funkin.audio.visualize.PolygonSpectogram; import funkin.audio.visualize.PolygonSpectogram;
import funkin.audio.visualize.PolygonSpectogram;
import funkin.audio.VoicesGroup; import funkin.audio.VoicesGroup;
import funkin.audio.waveform.WaveformSprite; import funkin.audio.waveform.WaveformSprite;
import funkin.data.notestyle.NoteStyleRegistry; import funkin.data.notestyle.NoteStyleRegistry;
import funkin.data.song.SongData.SongCharacterData; import funkin.data.song.SongData.SongCharacterData;
import funkin.data.song.SongData.SongCharacterData;
import funkin.data.song.SongData.SongChartData;
import funkin.data.song.SongData.SongChartData; import funkin.data.song.SongData.SongChartData;
import funkin.data.song.SongData.SongEventData; import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongData.SongMetadata; import funkin.data.song.SongData.SongMetadata;
import funkin.data.song.SongData.SongMetadata;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongData.SongNoteData; import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongData.SongOffsets; import funkin.data.song.SongData.SongOffsets;
import funkin.data.song.SongDataUtils; import funkin.data.song.SongDataUtils;
import funkin.data.song.SongDataUtils;
import funkin.data.song.SongRegistry;
import funkin.data.song.SongRegistry; import funkin.data.song.SongRegistry;
import funkin.data.stage.StageData; import funkin.data.stage.StageData;
import funkin.graphics.FunkinCamera;
import funkin.graphics.FunkinSprite; import funkin.graphics.FunkinSprite;
import funkin.input.Cursor; import funkin.input.Cursor;
import funkin.input.TurboKeyHandler; import funkin.input.TurboKeyHandler;
@ -62,8 +52,6 @@ import funkin.save.Save;
import funkin.ui.debug.charting.commands.AddEventsCommand; import funkin.ui.debug.charting.commands.AddEventsCommand;
import funkin.ui.debug.charting.commands.AddNotesCommand; import funkin.ui.debug.charting.commands.AddNotesCommand;
import funkin.ui.debug.charting.commands.ChartEditorCommand; import funkin.ui.debug.charting.commands.ChartEditorCommand;
import funkin.ui.debug.charting.commands.ChartEditorCommand;
import funkin.ui.debug.charting.commands.ChartEditorCommand;
import funkin.ui.debug.charting.commands.CopyItemsCommand; import funkin.ui.debug.charting.commands.CopyItemsCommand;
import funkin.ui.debug.charting.commands.CutItemsCommand; import funkin.ui.debug.charting.commands.CutItemsCommand;
import funkin.ui.debug.charting.commands.DeselectAllItemsCommand; import funkin.ui.debug.charting.commands.DeselectAllItemsCommand;
@ -96,6 +84,7 @@ import funkin.ui.debug.charting.toolboxes.ChartEditorOffsetsToolbox;
import funkin.ui.haxeui.components.CharacterPlayer; import funkin.ui.haxeui.components.CharacterPlayer;
import funkin.ui.haxeui.HaxeUIState; import funkin.ui.haxeui.HaxeUIState;
import funkin.ui.mainmenu.MainMenuState; import funkin.ui.mainmenu.MainMenuState;
import funkin.ui.transition.LoadingState;
import funkin.util.Constants; import funkin.util.Constants;
import funkin.util.FileUtil; import funkin.util.FileUtil;
import funkin.util.logging.CrashHandler; import funkin.util.logging.CrashHandler;
@ -120,7 +109,6 @@ import haxe.ui.containers.Grid;
import haxe.ui.containers.HBox; import haxe.ui.containers.HBox;
import haxe.ui.containers.menus.Menu; import haxe.ui.containers.menus.Menu;
import haxe.ui.containers.menus.MenuBar; import haxe.ui.containers.menus.MenuBar;
import haxe.ui.containers.menus.MenuBar;
import haxe.ui.containers.menus.MenuCheckBox; import haxe.ui.containers.menus.MenuCheckBox;
import haxe.ui.containers.menus.MenuItem; import haxe.ui.containers.menus.MenuItem;
import haxe.ui.containers.ScrollView; import haxe.ui.containers.ScrollView;
@ -131,7 +119,6 @@ import haxe.ui.core.Screen;
import haxe.ui.events.DragEvent; import haxe.ui.events.DragEvent;
import haxe.ui.events.MouseEvent; import haxe.ui.events.MouseEvent;
import haxe.ui.events.UIEvent; import haxe.ui.events.UIEvent;
import haxe.ui.events.UIEvent;
import haxe.ui.focus.FocusManager; import haxe.ui.focus.FocusManager;
import haxe.ui.Toolkit; import haxe.ui.Toolkit;
import openfl.display.BitmapData; import openfl.display.BitmapData;
@ -5330,30 +5317,31 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
} }
catch (e) catch (e)
{ {
this.error("Could Not Playtest", 'Got an error trying to playtest the song.\n${e}'); this.error('Could Not Playtest', 'Got an error trying to playtest the song.\n${e}');
return; return;
} }
// TODO: Rework asset system so we can remove this. // TODO: Rework asset system so we can remove this jank.
switch (currentSongStage) switch (currentSongStage)
{ {
case 'mainStage': case 'mainStage':
Paths.setCurrentLevel('week1'); PlayStatePlaylist.campaignId = 'week1';
case 'spookyMansion': case 'spookyMansion':
Paths.setCurrentLevel('week2'); PlayStatePlaylist.campaignId = 'week2';
case 'phillyTrain': case 'phillyTrain':
Paths.setCurrentLevel('week3'); PlayStatePlaylist.campaignId = 'week3';
case 'limoRide': case 'limoRide':
Paths.setCurrentLevel('week4'); PlayStatePlaylist.campaignId = 'week4';
case 'mallXmas' | 'mallEvil': case 'mallXmas' | 'mallEvil':
Paths.setCurrentLevel('week5'); PlayStatePlaylist.campaignId = 'week5';
case 'school' | 'schoolEvil': case 'school' | 'schoolEvil':
Paths.setCurrentLevel('week6'); PlayStatePlaylist.campaignId = 'week6';
case 'tankmanBattlefield': case 'tankmanBattlefield':
Paths.setCurrentLevel('week7'); PlayStatePlaylist.campaignId = 'week7';
case 'phillyStreets' | 'phillyBlazin' | 'phillyBlazin2': case 'phillyStreets' | 'phillyBlazin' | 'phillyBlazin2':
Paths.setCurrentLevel('weekend1'); PlayStatePlaylist.campaignId = 'weekend1';
} }
Paths.setCurrentLevel(PlayStatePlaylist.campaignId);
subStateClosed.add(reviveUICamera); subStateClosed.add(reviveUICamera);
subStateClosed.add(resetConductorAfterTest); subStateClosed.add(resetConductorAfterTest);
@ -5361,7 +5349,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
FlxTransitionableState.skipNextTransIn = false; FlxTransitionableState.skipNextTransIn = false;
FlxTransitionableState.skipNextTransOut = false; FlxTransitionableState.skipNextTransOut = false;
var targetState = new PlayState( var targetStateParams =
{ {
targetSong: targetSong, targetSong: targetSong,
targetDifficulty: selectedDifficulty, targetDifficulty: selectedDifficulty,
@ -5372,14 +5360,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
startTimestamp: startTimestamp, startTimestamp: startTimestamp,
playbackRate: playbackRate, playbackRate: playbackRate,
overrideMusic: true, overrideMusic: true,
}); };
// Override music. // Override music.
if (audioInstTrack != null) if (audioInstTrack != null)
{ {
FlxG.sound.music = audioInstTrack; FlxG.sound.music = audioInstTrack;
} }
targetState.vocals = audioVocalTrackGroup;
// Kill and replace the UI camera so it doesn't get destroyed during the state transition. // Kill and replace the UI camera so it doesn't get destroyed during the state transition.
uiCamera.kill(); uiCamera.kill();
@ -5389,7 +5376,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
this.persistentUpdate = false; this.persistentUpdate = false;
this.persistentDraw = false; this.persistentDraw = false;
stopWelcomeMusic(); stopWelcomeMusic();
openSubState(targetState);
LoadingState.loadPlayState(targetStateParams, false, true, function(targetState) {
targetState.vocals = audioVocalTrackGroup;
});
} }
/** /**

View file

@ -179,7 +179,7 @@ class FreeplayState extends MusicBeatSubState
#if discord_rpc #if discord_rpc
// Updating Discord Rich Presence // Updating Discord Rich Presence
DiscordClient.changePresence("In the Menus", null); DiscordClient.changePresence('In the Menus', null);
#end #end
var isDebug:Bool = false; var isDebug:Bool = false;
@ -188,14 +188,19 @@ class FreeplayState extends MusicBeatSubState
isDebug = true; isDebug = true;
#end #end
FunkinSound.playMusic('freakyMenu'); FunkinSound.playMusic('freakyMenu',
{
startingVolume: 0.0,
overrideExisting: true,
restartTrack: false
});
// Add a null entry that represents the RANDOM option // Add a null entry that represents the RANDOM option
songs.push(null); songs.push(null);
// TODO: This makes custom variations disappear from Freeplay. Figure out a better solution later. // 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. // 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 // programmatically adds the songs via LevelRegistry and SongRegistry
for (levelId in LevelRegistry.instance.listBaseGameLevelIds()) for (levelId in LevelRegistry.instance.listBaseGameLevelIds())
@ -205,7 +210,7 @@ class FreeplayState extends MusicBeatSubState
var song:Song = SongRegistry.instance.fetchEntry(songId); var song:Song = SongRegistry.instance.fetchEntry(songId);
// Only display songs which actually have available charts for the current character. // 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; if (availableDifficultiesForSong.length == 0) continue;
songs.push(new FreeplaySongData(levelId, songId, song, displayedVariations)); songs.push(new FreeplaySongData(levelId, songId, song, displayedVariations));
@ -1226,7 +1231,12 @@ class FreeplayState extends MusicBeatSubState
// TODO: Stream the instrumental of the selected song? // TODO: Stream the instrumental of the selected song?
if (prevSelected == 0) if (prevSelected == 0)
{ {
FunkinSound.playMusic('freakyMenu'); FunkinSound.playMusic('freakyMenu',
{
startingVolume: 0.0,
overrideExisting: true,
restartTrack: false
});
FlxG.sound.music.fadeIn(2, 0, 0.8); FlxG.sound.music.fadeIn(2, 0, 0.8);
} }
} }

View file

@ -155,7 +155,12 @@ class MainMenuState extends MusicBeatState
function playMenuMusic():Void function playMenuMusic():Void
{ {
FunkinSound.playMusic('freakyMenu'); FunkinSound.playMusic('freakyMenu',
{
startingVolume: 0.0,
overrideExisting: true,
restartTrack: false
});
} }
function resetCamStuff() function resetCamStuff()

View file

@ -235,7 +235,12 @@ class StoryMenuState extends MusicBeatState
function playMenuMusic():Void function playMenuMusic():Void
{ {
FunkinSound.playMusic('freakyMenu'); FunkinSound.playMusic('freakyMenu',
{
startingVolume: 0.0,
overrideExisting: true,
restartTrack: false
});
} }
function updateData():Void function updateData():Void

View file

@ -220,9 +220,14 @@ class TitleState extends MusicBeatState
function playMenuMusic():Void function playMenuMusic():Void
{ {
var shouldFadeIn = (FlxG.sound.music == null); var shouldFadeIn:Bool = (FlxG.sound.music == null);
// Load music. Includes logic to handle BPM changes. // Load music. Includes logic to handle BPM changes.
FunkinSound.playMusic('freakyMenu', 0.0, false, true); FunkinSound.playMusic('freakyMenu',
{
startingVolume: 0.0,
overrideExisting: true,
restartTrack: true
});
// Fade from 0.0 to 0.7 over 4 seconds // Fade from 0.0 to 0.7 over 4 seconds
if (shouldFadeIn) FlxG.sound.music.fadeIn(4.0, 0.0, 1.0); if (shouldFadeIn) FlxG.sound.music.fadeIn(4.0, 0.0, 1.0);
} }

View file

@ -22,10 +22,12 @@ import openfl.filters.ShaderFilter;
import openfl.utils.Assets; import openfl.utils.Assets;
import flixel.util.typeLimit.NextState; import flixel.util.typeLimit.NextState;
class LoadingState extends MusicBeatState class LoadingState extends MusicBeatSubState
{ {
inline static var MIN_TIME = 1.0; inline static var MIN_TIME = 1.0;
var asSubState:Bool = false;
var target:NextState; var target:NextState;
var playParams:Null<PlayStateParams>; var playParams:Null<PlayStateParams>;
var stopMusic:Bool = false; var stopMusic:Bool = false;
@ -173,8 +175,17 @@ class LoadingState extends MusicBeatState
{ {
if (stopMusic && FlxG.sound.music != null) FlxG.sound.music.stop(); if (stopMusic && FlxG.sound.music != null) FlxG.sound.music.stop();
if (asSubState)
{
this.close();
// We will assume the target is a valid substate.
FlxG.state.openSubState(cast target);
}
else
{
FlxG.switchState(target); FlxG.switchState(target);
} }
}
static function getSongPath():String static function getSongPath():String
{ {
@ -185,17 +196,41 @@ class LoadingState extends MusicBeatState
* Starts the transition to a new `PlayState` to start a new song. * Starts the transition to a new `PlayState` to start a new song.
* First switches to the `LoadingState` if assets need to be loaded. * First switches to the `LoadingState` if assets need to be loaded.
* @param params The parameters for the next `PlayState`. * @param params The parameters for the next `PlayState`.
* @param asSubState Whether to open as a substate rather than switching to the `PlayState`.
* @param shouldStopMusic Whether to stop the current music while loading. * @param shouldStopMusic Whether to stop the current music while loading.
*/ */
public static function loadPlayState(params:PlayStateParams, shouldStopMusic = false):Void public static function loadPlayState(params:PlayStateParams, shouldStopMusic = false, asSubState = false, ?onConstruct:PlayState->Void):Void
{ {
Paths.setCurrentLevel(PlayStatePlaylist.campaignId); Paths.setCurrentLevel(PlayStatePlaylist.campaignId);
var playStateCtor:NextState = () -> new PlayState(params); var playStateCtor:() -> PlayState = function() {
return new PlayState(params);
};
if (onConstruct != null)
{
playStateCtor = function() {
var result = new PlayState(params);
onConstruct(result);
return result;
};
}
#if NO_PRELOAD_ALL #if NO_PRELOAD_ALL
// Switch to loading state while we load assets (default on HTML5 target). // Switch to loading state while we load assets (default on HTML5 target).
var loadStateCtor:NextState = () -> new LoadingState(playStateCtor, shouldStopMusic, params); var loadStateCtor:NextState = function() {
var result = new LoadingState(playStateCtor, shouldStopMusic, params);
@:privateAccess
result.asSubState = asSubState;
return result;
}
if (asSubState)
{
FlxG.state.openSubState(loadStateCtor);
}
else
{
FlxG.switchState(loadStateCtor); FlxG.switchState(loadStateCtor);
}
#else #else
// All assets preloaded, switch directly to play state (defualt on other targets). // All assets preloaded, switch directly to play state (defualt on other targets).
if (shouldStopMusic && FlxG.sound.music != null) if (shouldStopMusic && FlxG.sound.music != null)
@ -209,6 +244,34 @@ class LoadingState extends MusicBeatState
params.targetSong.cacheCharts(true); params.targetSong.cacheCharts(true);
} }
var shouldPreloadLevelAssets:Bool = !(params?.minimalMode ?? false);
if (shouldPreloadLevelAssets) preloadLevelAssets();
if (asSubState)
{
FlxG.state.openSubState(cast playStateCtor());
}
else
{
FlxG.switchState(playStateCtor);
}
#end
}
#if NO_PRELOAD_ALL
static function isSoundLoaded(path:String):Bool
{
return Assets.cache.hasSound(path);
}
static function isLibraryLoaded(library:String):Bool
{
return Assets.getLibrary(library) != null;
}
#else
static function preloadLevelAssets():Void
{
// TODO: This section is a hack! Redo this later when we have a proper asset caching system. // TODO: This section is a hack! Redo this later when we have a proper asset caching system.
FunkinSprite.preparePurgeCache(); FunkinSprite.preparePurgeCache();
FunkinSprite.cacheTexture(Paths.image('combo')); FunkinSprite.cacheTexture(Paths.image('combo'));
@ -241,7 +304,10 @@ class LoadingState extends MusicBeatState
// List all image assets in the level's library. // List all image assets in the level's library.
// This is crude and I want to remove it when we have a proper asset caching system. // This is crude and I want to remove it when we have a proper asset caching system.
// TODO: Get rid of this junk! // TODO: Get rid of this junk!
var library = openfl.utils.Assets.getLibrary(PlayStatePlaylist.campaignId); var library = PlayStatePlaylist.campaignId != null ? openfl.utils.Assets.getLibrary(PlayStatePlaylist.campaignId) : null;
if (library == null) return; // We don't need to do anymore precaching.
var assets = library.list(lime.utils.AssetType.IMAGE); var assets = library.list(lime.utils.AssetType.IMAGE);
trace('Got ${assets.length} assets: ${assets}'); trace('Got ${assets.length} assets: ${assets}');
@ -272,20 +338,6 @@ class LoadingState extends MusicBeatState
// FunkinSprite.cacheAllSongTextures(stage) // FunkinSprite.cacheAllSongTextures(stage)
FunkinSprite.purgeCache(); FunkinSprite.purgeCache();
FlxG.switchState(playStateCtor);
#end
}
#if NO_PRELOAD_ALL
static function isSoundLoaded(path:String):Bool
{
return Assets.cache.hasSound(path);
}
static function isLibraryLoaded(library:String):Bool
{
return Assets.getLibrary(library) != null;
} }
#end #end

View file

@ -442,4 +442,10 @@ class Constants
* The vertical offset of the strumline from the top edge of the screen. * The vertical offset of the strumline from the top edge of the screen.
*/ */
public static final STRUMLINE_Y_OFFSET:Float = 24; public static final STRUMLINE_Y_OFFSET:Float = 24;
/**
* The rate at which the camera lerps to its target.
* 0.04 = 4% of distance per frame.
*/
public static final DEFAULT_CAMERA_FOLLOW_RATE:Float = 0.04;
} }