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

This commit is contained in:
Cameron Taylor 2024-04-14 02:57:05 -04:00
commit 658fd392fa
69 changed files with 1984 additions and 683 deletions

View file

@ -3,6 +3,10 @@ on:
workflow_dispatch:
push:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
create-nightly-html5:
runs-on: [self-hosted, linux]

View file

@ -0,0 +1,35 @@
name: cancel-merged-branches
on:
pull_request:
types:
- closed
jobs:
cancel_stuff:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- uses: actions/github-script@v7
id: cancel-runs
with:
result-encoding: string
retries: 3
script: |
let branch_workflows = await github.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: "build-shit.yml",
status: "queued",
branch: "${{ github.event.pull_request.head.ref }}"
});
let runs = branch_workflows.data.workflow_runs;
runs.forEach((run) => {
github.rest.actions.cancelWorkflowRun({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: run.id
});
});
console.log(runs);

10
.vscode/settings.json vendored
View file

@ -96,6 +96,11 @@
"target": "windows",
"args": ["-debug", "-DFORCE_DEBUG_VERSION"]
},
{
"label": "Linux / Debug",
"target": "linux",
"args": ["-debug", "-DFORCE_DEBUG_VERSION"]
},
{
"label": "HashLink / Debug",
"target": "hl",
@ -130,6 +135,11 @@
"-DFORCE_DEBUG_VERSION"
]
},
{
"label": "Windows / Debug (Straight to Play - 2hot)",
"target": "windows",
"args": ["-debug", "-DSONG=2hot", "-DFORCE_DEBUG_VERSION"]
},
{
"label": "HashLink / Debug (Straight to Play - Bopeebo Normal)",
"target": "hl",

View file

@ -45,6 +45,7 @@
<library name="week6" preload="true" />
<library name="week7" preload="true" />
<library name="weekend1" preload="true" />
<library name="videos" preload="true" />
</section>
<section if="NO_PRELOAD_ALL">
<library name="songs" preload="false" />
@ -58,10 +59,13 @@
<library name="week6" preload="false" />
<library name="week7" preload="false" />
<library name="weekend1" preload="false" />
<library name="videos" preload="false" />
</section>
<library name="art" preload="false" />
<assets path="assets/songs" library="songs" exclude="*.fla|*.ogg|*.wav" if="web" />
<assets path="assets/songs" library="songs" exclude="*.fla|*.mp3|*.wav" unless="web" />
<!-- Videos go in their own library because web never needs to preload them, they can just be streamed. -->
<assets path="assets/videos" library="videos" />
<assets path="assets/shared" library="shared" exclude="*.fla|*.ogg|*.wav" if="web" />
<assets path="assets/shared" library="shared" exclude="*.fla|*.mp3|*.wav" unless="web" />
<assets path="assets/tutorial" library="tutorial" exclude="*.fla|*.ogg|*.wav" if="web" />
@ -119,9 +123,10 @@
<haxelib name="flixel-text-input" /> <!-- Improved text field rendering for HaxeUI -->
<haxelib name="polymod" /> <!-- Modding framework -->
<haxelib name="flxanimate" /> <!-- Texture atlas rendering -->
<haxelib name="hxCodec" if="desktop" /> <!-- Video playback -->
<haxelib name="hxCodec" if="desktop" unless="hl" /> <!-- Video playback -->
<haxelib name="funkVis"/>
<haxelib name="json2object" /> <!-- JSON parsing -->
<haxelib name="thx.semver" /> <!-- Version string handling -->
@ -184,6 +189,7 @@
<haxedef name="haxeui_focus_out_on_click" />
<!-- Required to use haxe.ui.backend.flixel.UIState with build macros. -->
<haxedef name="haxeui_dont_impose_base_class" />
<haxedef name="HARDCODED_CREDITS" />
<!-- Skip the Intro -->
<section if="debug">

2
assets

@ -1 +1 @@
Subproject commit 8a48607c800e096bbe6e67395adbbd48639a72ed
Subproject commit 53b014191a9750b184433ec9be2cc2c0cb74464f

View file

@ -111,7 +111,7 @@
"name": "lime",
"type": "git",
"dir": null,
"ref": "1359fe6a",
"ref": "43ebebdd8119936b99f23407057025c7849c5f5b",
"url": "https://github.com/FunkinCrew/lime"
},
{
@ -146,7 +146,7 @@
"name": "openfl",
"type": "git",
"dir": null,
"ref": "f229d763",
"ref": "228c1b5063911e2ad75cef6e3168ef0a4b9f9134",
"url": "https://github.com/FunkinCrew/openfl"
},
{

View file

@ -59,6 +59,7 @@ abstract Tallies(RawTallies)
totalNotes: 0,
totalNotesHit: 0,
maxCombo: 0,
score: 0,
isNewHighscore: false
}
}
@ -81,6 +82,9 @@ typedef RawTallies =
var good:Int;
var sick:Int;
var maxCombo:Int;
var score:Int;
var isNewHighscore:Bool;
/**

View file

@ -261,6 +261,35 @@ class InitState extends FlxState
return;
}
// TODO: Rework loading behavior so we don't have to do this.
switch (songId)
{
case 'tutorial' | 'bopeebo' | 'fresh' | 'dadbattle':
Paths.setCurrentLevel('week1');
PlayStatePlaylist.campaignId = 'week1';
case 'spookeez' | 'south' | 'monster':
Paths.setCurrentLevel('week2');
PlayStatePlaylist.campaignId = 'week2';
case 'pico' | 'philly-nice' | 'blammed':
Paths.setCurrentLevel('week3');
PlayStatePlaylist.campaignId = 'week3';
case 'high' | 'satin-panties' | 'milf':
Paths.setCurrentLevel('week4');
PlayStatePlaylist.campaignId = 'week4';
case 'cocoa' | 'eggnog' | 'winter-horrorland':
Paths.setCurrentLevel('week5');
PlayStatePlaylist.campaignId = 'week5';
case 'senpai' | 'roses' | 'thorns':
Paths.setCurrentLevel('week6');
PlayStatePlaylist.campaignId = 'week6';
case 'ugh' | 'guns' | 'stress':
Paths.setCurrentLevel('week7');
PlayStatePlaylist.campaignId = 'week7';
case 'darnell' | 'lit-up' | '2hot' | 'blazin':
Paths.setCurrentLevel('weekend1');
PlayStatePlaylist.campaignId = 'weekend1';
}
LoadingState.loadPlayState(
{
targetSong: songData,
@ -283,6 +312,10 @@ class InitState extends FlxState
return;
}
// TODO: Rework loading behavior so we don't have to do this.
Paths.setCurrentLevel(levelId);
PlayStatePlaylist.campaignId = levelId;
PlayStatePlaylist.playlistSongIds = currentLevel.getSongs();
PlayStatePlaylist.isStoryMode = true;
PlayStatePlaylist.campaignScore = 0;

View file

@ -9,7 +9,7 @@ import openfl.utils.Assets as OpenFlAssets;
*/
class Paths
{
static var currentLevel:String;
static var currentLevel:Null<String> = null;
public static function setCurrentLevel(name:String):Void
{
@ -113,7 +113,7 @@ class Paths
public static function videos(key:String, ?library:String):String
{
return getPath('videos/$key.${Constants.EXT_VIDEO}', BINARY, library);
return getPath('videos/$key.${Constants.EXT_VIDEO}', BINARY, library ?? 'videos');
}
public static function voices(song:String, ?suffix:String = ''):String

View file

@ -265,10 +265,16 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
@:allow(flixel.sound.FlxSoundGroup)
override function updateTransform():Void
{
_transform.volume = #if FLX_SOUND_SYSTEM ((FlxG.sound.muted || this.muted) ? 0 : 1) * FlxG.sound.volume * #end
(group != null ? group.volume : 1) * _volume * _volumeAdjust;
if (_transform != null)
{
_transform.volume = #if FLX_SOUND_SYSTEM ((FlxG.sound.muted || this.muted) ? 0 : 1) * FlxG.sound.volume * #end
(group != null ? group.volume : 1) * _volume * _volumeAdjust;
}
if (_channel != null) _channel.soundTransform = _transform;
if (_channel != null)
{
_channel.soundTransform = _transform;
}
}
public function clone():FunkinSound
@ -315,6 +321,13 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
}
}
if (FlxG.sound.music != null)
{
FlxG.sound.music.fadeTween?.cancel();
FlxG.sound.music.stop();
FlxG.sound.music.kill();
}
if (params?.mapTimeChanges ?? true)
{
var songMusicData:Null<SongMusicData> = SongRegistry.instance.parseMusicData(key);
@ -329,13 +342,6 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
}
}
if (FlxG.sound.music != null)
{
FlxG.sound.music.fadeTween?.cancel();
FlxG.sound.music.stop();
FlxG.sound.music.kill();
}
var music = FunkinSound.load(Paths.music('$key/$key'), params?.startingVolume ?? 1.0, params.loop ?? true, false, true);
if (music != null)
{
@ -391,10 +397,10 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
sound._label = 'unknown';
}
if (autoPlay) sound.play();
sound.volume = volume;
sound.group = FlxG.sound.defaultSoundGroup;
sound.persist = true;
if (autoPlay) sound.play();
// Call onLoad() because the sound already loaded
if (onLoad != null && sound._sound != null) onLoad();
@ -402,10 +408,16 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
return sound;
}
@:nullSafety(Off)
public override function destroy():Void
{
// trace('[FunkinSound] Destroying sound "${this._label}"');
super.destroy();
if (fadeTween != null)
{
fadeTween.cancel();
fadeTween = null;
}
FlxTween.cancelTweensOf(this);
this._label = 'unknown';
}

View file

@ -150,7 +150,7 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
/**
* Stop all the sounds in the group.
*/
public function stop()
public function stop():Void
{
if (members != null)
{
@ -160,7 +160,7 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
}
}
public override function destroy()
public override function destroy():Void
{
stop();
super.destroy();
@ -178,9 +178,14 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
function get_time():Float
{
if (getFirstAlive() != null) return getFirstAlive().time;
if (getFirstAlive() != null)
{
return getFirstAlive().time;
}
else
{
return 0;
}
}
function set_time(time:Float):Float
@ -195,16 +200,26 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
function get_playing():Bool
{
if (getFirstAlive() != null) return getFirstAlive().playing;
if (getFirstAlive() != null)
{
return getFirstAlive().playing;
}
else
{
return false;
}
}
function get_volume():Float
{
if (getFirstAlive() != null) return getFirstAlive().volume;
if (getFirstAlive() != null)
{
return getFirstAlive().volume;
}
else
{
return 1;
}
}
// in PlayState, adjust the code so that it only mutes the player1 vocal tracks?

View file

@ -159,10 +159,18 @@ class VoicesGroup extends SoundGroup
public override function destroy():Void
{
playerVoices.destroy();
playerVoices = null;
opponentVoices.destroy();
opponentVoices = null;
if (playerVoices != null)
{
playerVoices.destroy();
playerVoices = null;
}
if (opponentVoices != null)
{
opponentVoices.destroy();
opponentVoices = null;
}
super.destroy();
}
}

View file

@ -325,12 +325,3 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
}
}
}
/**
* A pair of a file name and its contents.
*/
typedef JsonFile =
{
fileName:String,
contents:String
};

View file

@ -0,0 +1,10 @@
package funkin.data;
/**
* A pair of a file name and its contents.
*/
typedef JsonFile =
{
fileName:String,
contents:String
};

View file

@ -1,5 +1,7 @@
package funkin.data.freeplay;
import funkin.data.animation.AnimationData;
/**
* A type definition for the data for an album of songs.
* It includes things like what graphics to display in Freeplay.
@ -33,4 +35,11 @@ typedef AlbumData =
* The album title will be displayed below the album art in Freeplay.
*/
public var albumTitleAsset:String;
/**
* An optional array of animations for the album title.
*/
@:optional
@:default([])
public var albumTitleAnimations:Array<AnimationData>;
}

View file

@ -427,7 +427,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
return ScriptedSong.listScriptClasses();
}
function loadEntryMetadataFile(id:String, ?variation:String):Null<BaseRegistry.JsonFile>
function loadEntryMetadataFile(id:String, ?variation:String):Null<JsonFile>
{
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var entryFilePath:String = Paths.json('$dataFilePath/$id/$id-metadata${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}');
@ -442,7 +442,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
return {fileName: entryFilePath, contents: rawJson};
}
function loadMusicDataFile(id:String, ?variation:String):Null<BaseRegistry.JsonFile>
function loadMusicDataFile(id:String, ?variation:String):Null<JsonFile>
{
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var entryFilePath:String = Paths.file('music/$id/$id-metadata${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}.json');
@ -460,7 +460,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
return openfl.Assets.exists(entryFilePath);
}
function loadEntryChartFile(id:String, ?variation:String):Null<BaseRegistry.JsonFile>
function loadEntryChartFile(id:String, ?variation:String):Null<JsonFile>
{
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var entryFilePath:String = Paths.json('$dataFilePath/$id/$id-chart${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}');

View file

@ -137,7 +137,8 @@ class FlxAtlasSprite extends FlxAnimate
anim.callback = function(_, frame:Int) {
var offset = loop ? 0 : -1;
if (frame == (anim.getFrameLabel(id).duration + offset) + anim.getFrameLabel(id).index)
var frameLabel = anim.getFrameLabel(id);
if (frame == (frameLabel.duration + offset) + frameLabel.index)
{
if (loop)
{

View file

@ -20,6 +20,6 @@ class GaussianBlurShader extends FlxRuntimeShader
public function setAmount(value:Float):Void
{
this.amount = value;
this.setFloat("amount", amount);
this.setFloat("_amount", amount);
}
}

View file

@ -17,6 +17,6 @@ class Grayscale extends FlxRuntimeShader
public function setAmount(value:Float):Void
{
amount = value;
this.setFloat("amount", amount);
this.setFloat("_amount", amount);
}
}

View file

@ -20,7 +20,7 @@ class HSVShader extends FlxRuntimeShader
function set_hue(value:Float):Float
{
this.setFloat('hue', value);
this.setFloat('_hue', value);
this.hue = value;
return this.hue;

View file

@ -240,8 +240,8 @@ class PolymodHandler
{
return {
assetLibraryPaths: [
'default' => 'preload', 'shared' => 'shared', 'songs' => 'songs', 'tutorial' => 'tutorial', 'week1' => 'week1', 'week2' => 'week2',
'week3' => 'week3', 'week4' => 'week4', 'week5' => 'week5', 'week6' => 'week6', 'week7' => 'week7', 'weekend1' => 'weekend1',
'default' => 'preload', 'shared' => 'shared', 'songs' => 'songs', 'videos' => 'videos', 'tutorial' => 'tutorial', 'week1' => 'week1',
'week2' => 'week2', 'week3' => 'week3', 'week4' => 'week4', 'week5' => 'week5', 'week6' => 'week6', 'week7' => 'week7', 'weekend1' => 'weekend1',
],
coreAssetRedirect: CORE_FOLDER,
}

View file

@ -8,7 +8,12 @@ import funkin.modding.IScriptedClass;
*/
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;

View file

@ -3,18 +3,18 @@ package funkin.play;
import flixel.FlxG;
import flixel.FlxObject;
import flixel.FlxSprite;
import funkin.audio.FunkinSound;
import flixel.input.touch.FlxTouch;
import flixel.util.FlxColor;
import flixel.util.FlxTimer;
import funkin.audio.FunkinSound;
import funkin.graphics.FunkinSprite;
import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.play.character.BaseCharacter;
import funkin.play.PlayState;
import funkin.util.MathUtil;
import funkin.ui.freeplay.FreeplayState;
import funkin.ui.MusicBeatSubState;
import funkin.ui.story.StoryMenuState;
import funkin.util.MathUtil;
import openfl.utils.Assets;
/**
@ -23,13 +23,14 @@ import openfl.utils.Assets;
*
* The newest implementation uses a substate, which prevents having to reload the song and stage each reset.
*/
@:nullSafety
class GameOverSubState extends MusicBeatSubState
{
/**
* The currently active GameOverSubState.
* 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.
@ -37,7 +38,7 @@ class GameOverSubState extends MusicBeatSubState
* For example, playing a different animation when BF dies in Week 4
* or Pico dies in Weekend 1.
*/
public static var animationSuffix:String = "";
public static var animationSuffix:String = '';
/**
* Which alternate game over music to use.
@ -45,17 +46,19 @@ class GameOverSubState extends MusicBeatSubState
* For example, the bf-pixel script sets this to `-pixel`
* 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.
*/
public static var blueBallSuffix:String = "";
public static var blueBallSuffix:String = '';
static var blueballed:Bool = false;
/**
* The boyfriend character.
*/
var boyfriend:BaseCharacter;
var boyfriend:Null<BaseCharacter> = null;
/**
* The invisible object in the scene which the camera focuses on.
@ -82,7 +85,8 @@ class GameOverSubState extends MusicBeatSubState
var transparent:Bool;
final CAMERA_ZOOM_DURATION:Float = 0.5;
static final CAMERA_ZOOM_DURATION:Float = 0.5;
var targetCameraZoom:Float = 1.0;
public function new(params:GameOverParams)
@ -91,6 +95,8 @@ class GameOverSubState extends MusicBeatSubState
this.isChartingMode = params?.isChartingMode ?? false;
transparent = params.transparent;
cameraFollowPoint = new FlxObject(PlayState.instance.cameraFollowPoint.x, PlayState.instance.cameraFollowPoint.y, 1, 1);
}
/**
@ -101,14 +107,15 @@ class GameOverSubState extends MusicBeatSubState
animationSuffix = '';
musicSuffix = '';
blueBallSuffix = '';
blueballed = false;
}
override public function create():Void
public override function create():Void
{
if (instance != null)
{
// 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;
@ -121,7 +128,7 @@ class GameOverSubState extends MusicBeatSubState
var playState = PlayState.instance;
// 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,
// but it's normally opaque.
bg.alpha = transparent ? 0.25 : 1.0;
@ -138,21 +145,10 @@ class GameOverSubState extends MusicBeatSubState
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);
FlxG.camera.target = null;
FlxG.camera.follow(cameraFollowPoint, LOCKON, 0.01);
targetCameraZoom = PlayState?.instance?.currentStage?.camZoom * boyfriend.getDeathCameraZoom();
}
setCameraTarget();
//
// Set up the audio
//
@ -161,6 +157,27 @@ class GameOverSubState extends MusicBeatSubState
Conductor.instance.update(0);
}
@:nullSafety(Off)
function setCameraTarget():Void
{
// 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, 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
{
// Apply camera zoom level from stage data.
@ -175,7 +192,7 @@ class GameOverSubState extends MusicBeatSubState
{
hasStartedAnimation = true;
if (PlayState.instance.isMinimalMode)
if (boyfriend == null || PlayState.instance.isMinimalMode)
{
// Play the "blue balled" sound. May play a variant if one has been assigned.
playBlueBalledSFX();
@ -205,10 +222,10 @@ class GameOverSubState extends MusicBeatSubState
// MOBILE ONLY: Restart the level when tapping Boyfriend.
if (FlxG.onMobile)
{
var touch = FlxG.touches.getFirst();
var touch:FlxTouch = FlxG.touches.getFirst();
if (touch != null)
{
if (touch.overlaps(boyfriend))
if (boyfriend == null || touch.overlaps(boyfriend))
{
confirmDeath();
}
@ -228,7 +245,7 @@ class GameOverSubState extends MusicBeatSubState
blueballed = false;
PlayState.instance.deathCounter = 0;
// PlayState.seenCutscene = false; // old thing...
gameOverMusic.stop();
if (gameOverMusic != null) gameOverMusic.stop();
if (isChartingMode)
{
@ -238,11 +255,11 @@ class GameOverSubState extends MusicBeatSubState
}
else if (PlayStatePlaylist.isStoryMode)
{
FlxG.switchState(() -> new StoryMenuState());
openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new StoryMenuState(sticker)));
}
else
{
FlxG.switchState(() -> new FreeplayState());
openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> FreeplayState.build(sticker)));
}
}
@ -252,7 +269,7 @@ class GameOverSubState extends MusicBeatSubState
// This enables the stepHit and beatHit events.
Conductor.instance.update(gameOverMusic.time);
}
else
else if (boyfriend != null)
{
if (PlayState.instance.isMinimalMode)
{
@ -299,7 +316,7 @@ class GameOverSubState extends MusicBeatSubState
isEnding = true;
startDeathMusic(1.0, true); // isEnding changes this function's behavior.
if (PlayState.instance.isMinimalMode) {}
if (PlayState.instance.isMinimalMode || boyfriend == null) {}
else
{
boyfriend.playAnimation('deathConfirm' + animationSuffix, true);
@ -313,7 +330,7 @@ class GameOverSubState extends MusicBeatSubState
FlxG.camera.fade(FlxColor.BLACK, 1, true, null, true);
PlayState.instance.needsReset = true;
if (PlayState.instance.isMinimalMode) {}
if (PlayState.instance.isMinimalMode || boyfriend == null) {}
else
{
// Readd Boyfriend to the stage.
@ -332,7 +349,7 @@ class GameOverSubState extends MusicBeatSubState
}
}
public override function dispatchEvent(event:ScriptEvent)
public override function dispatchEvent(event:ScriptEvent):Void
{
super.dispatchEvent(event);
@ -345,11 +362,11 @@ class GameOverSubState extends MusicBeatSubState
*/
function resolveMusicPath(suffix:String, starting:Bool = false, ending:Bool = false):Null<String>
{
var basePath = 'gameplay/gameover/gameOver';
if (starting) basePath += 'Start';
else if (ending) basePath += 'End';
var basePath:String = 'gameplay/gameover/gameOver';
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)
{
suffix = suffix.split('-').slice(0, -1).join('-');
@ -362,23 +379,26 @@ class GameOverSubState extends MusicBeatSubState
/**
* 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
{
var musicPath = resolveMusicPath(musicSuffix, isStarting, isEnding);
var onComplete = null;
var musicPath:Null<String> = resolveMusicPath(musicSuffix, isStarting, isEnding);
var onComplete:() -> Void = () -> {};
if (isStarting)
{
if (musicPath == null)
{
// Looked for starting music and didn't find it. Use middle music instead.
isStarting = false;
musicPath = resolveMusicPath(musicSuffix, isStarting, isEnding);
}
else
{
onComplete = function() {
isStarting = false;
isStarting = true;
// We need to force to ensure that the non-starting music plays.
startDeathMusic(1.0, true);
};
@ -387,13 +407,16 @@ class GameOverSubState extends MusicBeatSubState
if (musicPath == null)
{
trace('Could not find game over music!');
FlxG.log.warn('[GAMEOVER] Could not find game over music at path ($musicPath)!');
return;
}
else if (gameOverMusic == null || !gameOverMusic.playing || force)
{
if (gameOverMusic != null) gameOverMusic.stop();
gameOverMusic = FunkinSound.load(musicPath);
if (gameOverMusic == null) return;
gameOverMusic.volume = startingVolume;
gameOverMusic.looped = !(isEnding || isStarting);
gameOverMusic.onComplete = onComplete;
@ -406,13 +429,11 @@ class GameOverSubState extends MusicBeatSubState
}
}
static var blueballed:Bool = false;
/**
* Play the sound effect that occurs when
* boyfriend's testicles get utterly annihilated.
*/
public static function playBlueBalledSFX()
public static function playBlueBalledSFX():Void
{
blueballed = true;
if (Assets.exists(Paths.sound('gameplay/gameover/fnf_loss_sfx' + blueBallSuffix)))
@ -431,7 +452,7 @@ class GameOverSubState extends MusicBeatSubState
* Week 7-specific hardcoded behavior, to play a custom death quote.
* TODO: Make this a module somehow.
*/
function playJeffQuote()
function playJeffQuote():Void
{
var randomCensor:Array<Int> = [];
@ -446,20 +467,27 @@ class GameOverSubState extends MusicBeatSubState
});
}
public override function destroy()
public override function destroy():Void
{
super.destroy();
if (gameOverMusic != null) gameOverMusic.stop();
gameOverMusic = null;
if (gameOverMusic != null)
{
gameOverMusic.stop();
gameOverMusic = null;
}
blueballed = false;
instance = null;
}
public override function toString():String
{
return "GameOverSubState";
return 'GameOverSubState';
}
}
/**
* Parameters used to instantiate a GameOverSubState.
*/
typedef GameOverParams =
{
var isChartingMode:Bool;

View file

@ -12,6 +12,7 @@ import flixel.tweens.FlxTween;
import flixel.util.FlxColor;
import funkin.audio.FunkinSound;
import funkin.data.song.SongRegistry;
import funkin.ui.freeplay.FreeplayState;
import funkin.graphics.FunkinSprite;
import funkin.play.cutscene.VideoCutscene;
import funkin.play.PlayState;
@ -72,8 +73,8 @@ class PauseSubState extends MusicBeatSubState
*/
static final PAUSE_MENU_ENTRIES_VIDEO_CUTSCENE:Array<PauseMenuEntry> = [
{text: 'Resume', callback: resume},
{text: 'Restart Cutscene', callback: restartVideoCutscene},
{text: 'Skip Cutscene', callback: skipVideoCutscene},
{text: 'Restart Cutscene', callback: restartVideoCutscene},
{text: 'Exit to Menu', callback: quitToMenu},
];
@ -230,7 +231,7 @@ class PauseSubState extends MusicBeatSubState
*/
function startPauseMusic():Void
{
var pauseMusicPath:String = Paths.music('breakfast$musicSuffix');
var pauseMusicPath:String = Paths.music('breakfast$musicSuffix/breakfast$musicSuffix');
pauseMusic = FunkinSound.load(pauseMusicPath, true, true);
if (pauseMusic == null)
@ -440,7 +441,7 @@ class PauseSubState extends MusicBeatSubState
var entries:Array<PauseMenuEntry> = [];
if (PlayState.instance.currentChart != null)
{
var difficultiesInVariation = PlayState.instance.currentSong.listDifficulties(PlayState.instance.currentChart.variation);
var difficultiesInVariation = PlayState.instance.currentSong.listDifficulties(PlayState.instance.currentChart.variation, true);
trace('DIFFICULTIES: ${difficultiesInVariation}');
for (difficulty in difficultiesInVariation)
{
@ -567,6 +568,8 @@ class PauseSubState extends MusicBeatSubState
PlayStatePlaylist.campaignDifficulty = difficulty;
PlayState.instance.currentDifficulty = PlayStatePlaylist.campaignDifficulty;
FreeplayState.rememberedDifficulty = difficulty;
PlayState.instance.needsReset = true;
state.close();
@ -658,7 +661,7 @@ class PauseSubState extends MusicBeatSubState
}
else
{
state.openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new funkin.ui.freeplay.FreeplayState(null, sticker)));
state.openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> FreeplayState.build(null, sticker)));
}
}

View file

@ -245,20 +245,26 @@ class PlayState extends MusicBeatSubState
/**
* The current camera zoom level without any modifiers applied.
*/
public var currentCameraZoom:Float = FlxCamera.defaultZoom * 1.05;
public var currentCameraZoom:Float = FlxCamera.defaultZoom;
/**
* 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.
* Multiplier for currentCameraZoom for camera bops.
* Lerped back to 1.0x every frame.
*/
public var defaultCameraZoom:Float = FlxCamera.defaultZoom * 1.05;
public var cameraBopMultiplier:Float = 1.0;
/**
* Camera zoom applied on top of currentCameraZoom.
* Tweened via the `ZoomCamera` song event in additive mode.
* Default camera zoom for the current stage.
* If we aren't in a stage, just use the default zoom (1.05x).
*/
public var additiveCameraZoom:Float = 0;
public var stageZoom(get, never):Float;
function get_stageZoom():Float
{
if (currentStage != null) return currentStage.camZoom;
else
return FlxCamera.defaultZoom * 1.05;
}
/**
* The current HUD camera zoom level.
@ -268,16 +274,18 @@ class PlayState extends MusicBeatSubState
public var defaultHUDCameraZoom:Float = FlxCamera.defaultZoom * 1.0;
/**
* Intensity of the gameplay camera zoom.
* @default `1.5%`
* Camera bop intensity multiplier.
* Applied to cameraBopMultiplier on camera bops (usually every beat).
* @default `101.5%`
*/
public var cameraZoomIntensity:Float = Constants.DEFAULT_ZOOM_INTENSITY;
public var cameraBopIntensity:Float = Constants.DEFAULT_BOP_INTENSITY;
/**
* Intensity of the HUD camera zoom.
* Need to make this a multiplier later. Just shoving in 0.015 for now so it doesn't break.
* @default `3.0%`
*/
public var hudCameraZoomIntensity:Float = Constants.DEFAULT_ZOOM_INTENSITY * 2.0;
public var hudCameraZoomIntensity:Float = 0.015 * 2.0;
/**
* How many beats (quarter notes) between camera zooms.
@ -728,6 +736,10 @@ class PlayState extends MusicBeatSubState
#end
initialized = true;
// This step ensures z-indexes are applied properly,
// and it's important to call it last so all elements get affected.
refresh();
}
public override function draw():Void
@ -857,8 +869,8 @@ class PlayState extends MusicBeatSubState
regenNoteData();
// Reset camera zooming
cameraZoomIntensity = Constants.DEFAULT_ZOOM_INTENSITY;
hudCameraZoomIntensity = Constants.DEFAULT_ZOOM_INTENSITY * 2.0;
cameraBopIntensity = Constants.DEFAULT_BOP_INTENSITY;
hudCameraZoomIntensity = (cameraBopIntensity - 1.0) * 2.0;
cameraZoomRate = Constants.DEFAULT_ZOOM_RATE;
health = Constants.HEALTH_STARTING;
@ -953,11 +965,12 @@ class PlayState extends MusicBeatSubState
if (health > Constants.HEALTH_MAX) health = Constants.HEALTH_MAX;
if (health < Constants.HEALTH_MIN) health = Constants.HEALTH_MIN;
// Lerp the camera zoom towards the target level.
if (subState == null)
// Apply camera zoom + multipliers.
if (subState == null && cameraZoomRate > 0.0 && !isInCutscene)
{
currentCameraZoom = FlxMath.lerp(defaultCameraZoom, currentCameraZoom, 0.95);
FlxG.camera.zoom = currentCameraZoom + additiveCameraZoom;
cameraBopMultiplier = FlxMath.lerp(1.0, cameraBopMultiplier, 0.95); // Lerp bop multiplier back to 1.0x
var zoomPlusBop = currentCameraZoom * cameraBopMultiplier; // Apply camera bop multiplier.
FlxG.camera.zoom = zoomPlusBop; // Actually apply the zoom to the camera.
camHUD.zoom = FlxMath.lerp(defaultHUDCameraZoom, camHUD.zoom, 0.95);
}
@ -967,6 +980,7 @@ class PlayState extends MusicBeatSubState
FlxG.watch.addQuick('bfAnim', currentStage.getBoyfriend().getCurrentAnimation());
}
FlxG.watch.addQuick('health', health);
FlxG.watch.addQuick('cameraBopIntensity', cameraBopIntensity);
// TODO: Add a song event for Handle GF dance speed.
@ -1363,15 +1377,15 @@ class PlayState extends MusicBeatSubState
// activeNotes.sort(SortUtil.byStrumtime, FlxSort.DESCENDING);
}
// Only zoom camera if we are zoomed by less than 35%.
// Only bop camera if zoom level is below 135%
if (Preferences.zoomCamera
&& FlxG.camera.zoom < (1.35 * defaultCameraZoom)
&& FlxG.camera.zoom < (1.35 * FlxCamera.defaultZoom)
&& cameraZoomRate > 0
&& Conductor.instance.currentBeat % cameraZoomRate == 0)
{
// Zoom camera in (1.5%)
currentCameraZoom += cameraZoomIntensity * defaultCameraZoom;
// Hud zooms double (3%)
// Set zoom multiplier for camera bop.
cameraBopMultiplier = cameraBopIntensity;
// HUD camera zoom still uses old system. To change. (+3%)
camHUD.zoom += hudCameraZoomIntensity * defaultHUDCameraZoom;
}
// trace('Not bopping camera: ${FlxG.camera.zoom} < ${(1.35 * defaultCameraZoom)} && ${cameraZoomRate} > 0 && ${Conductor.instance.currentBeat} % ${cameraZoomRate} == ${Conductor.instance.currentBeat % cameraZoomRate}}');
@ -1562,12 +1576,11 @@ class PlayState extends MusicBeatSubState
{
if (PlayState.instance.isMinimalMode) return;
// Apply camera zoom level from stage data.
defaultCameraZoom = currentStage.camZoom;
currentCameraZoom = defaultCameraZoom;
currentCameraZoom = stageZoom;
FlxG.camera.zoom = currentCameraZoom;
// Reset additive zoom.
additiveCameraZoom = 0;
// Reset bop multiplier.
cameraBopMultiplier = 1.0;
}
/**
@ -1720,8 +1733,6 @@ class PlayState extends MusicBeatSubState
playerStrumline.fadeInArrows();
opponentStrumline.fadeInArrows();
}
this.refresh();
}
/**
@ -2222,8 +2233,8 @@ class PlayState extends MusicBeatSubState
holdNote.handledMiss = true;
// Mute vocals and play miss animation, but don't penalize.
vocals.playerVolume = 0;
if (currentStage != null && currentStage.getBoyfriend() != null) currentStage.getBoyfriend().playSingAnimation(holdNote.noteData.getDirection(), true);
// vocals.playerVolume = 0;
// if (currentStage != null && currentStage.getBoyfriend() != null) currentStage.getBoyfriend().playSingAnimation(holdNote.noteData.getDirection(), true);
}
}
}
@ -2441,7 +2452,8 @@ class PlayState extends MusicBeatSubState
if (Highscore.tallies.combo != 0)
{
// Break the combo.
Highscore.tallies.combo = comboPopUps.displayCombo(0);
if (Highscore.tallies.combo >= 10) comboPopUps.displayCombo(0);
Highscore.tallies.combo = 0;
}
if (playSound)
@ -2568,32 +2580,38 @@ class PlayState extends MusicBeatSubState
*/
function popUpScore(daNote:NoteSprite, score:Int, daRating:String, healthChange:Float):Void
{
vocals.playerVolume = 1;
if (daRating == 'miss')
{
// If daRating is 'miss', that means we made a mistake and should not continue.
trace('[WARNING] popUpScore judged a note as a miss!');
FlxG.log.warn('popUpScore judged a note as a miss!');
// TODO: Remove this.
comboPopUps.displayRating('miss');
// comboPopUps.displayRating('miss');
return;
}
vocals.playerVolume = 1;
var isComboBreak = false;
switch (daRating)
{
case 'sick':
Highscore.tallies.sick += 1;
Highscore.tallies.totalNotesHit++;
isComboBreak = Constants.JUDGEMENT_SICK_COMBO_BREAK;
case 'good':
Highscore.tallies.good += 1;
Highscore.tallies.totalNotesHit++;
isComboBreak = Constants.JUDGEMENT_GOOD_COMBO_BREAK;
case 'bad':
Highscore.tallies.bad += 1;
Highscore.tallies.totalNotesHit++;
isComboBreak = Constants.JUDGEMENT_BAD_COMBO_BREAK;
case 'shit':
Highscore.tallies.shit += 1;
Highscore.tallies.totalNotesHit++;
isComboBreak = Constants.JUDGEMENT_SHIT_COMBO_BREAK;
default:
FlxG.log.error('Wuh? Buh? Guh? Note hit judgement was $daRating!');
}
health += healthChange;
@ -2601,18 +2619,18 @@ class PlayState extends MusicBeatSubState
if (isComboBreak)
{
// Break the combo, but don't increment tallies.misses.
Highscore.tallies.combo = comboPopUps.displayCombo(0);
if (Highscore.tallies.combo >= 10) comboPopUps.displayCombo(0);
Highscore.tallies.combo = 0;
}
else
{
Highscore.tallies.combo++;
Highscore.tallies.totalNotesHit++;
if (Highscore.tallies.combo > Highscore.tallies.maxCombo) Highscore.tallies.maxCombo = Highscore.tallies.combo;
}
playerStrumline.hitNote(daNote, !isComboBreak);
if (daRating == "sick")
if (daRating == 'sick')
{
playerStrumline.playNoteSplash(daNote.noteData.getDirection());
}
@ -2724,7 +2742,7 @@ class PlayState extends MusicBeatSubState
*/
public function endSong(rightGoddamnNow:Bool = false):Void
{
FlxG.sound.music.volume = 0;
if (FlxG.sound.music != null) FlxG.sound.music.volume = 0;
vocals.volume = 0;
mayPauseGame = false;
@ -2742,6 +2760,8 @@ class PlayState extends MusicBeatSubState
deathCounter = 0;
var isNewHighscore = false;
if (currentSong != null && currentSong.validScore)
{
// crackhead double thingie, sets whether was new highscore, AND saves the song!
@ -2766,17 +2786,20 @@ class PlayState extends MusicBeatSubState
// adds current song data into the tallies for the level (story levels)
Highscore.talliesLevel = Highscore.combineTallies(Highscore.tallies, Highscore.talliesLevel);
if (Save.instance.isSongHighScore(currentSong.id, currentDifficulty, data))
if (!isPracticeMode && !isBotPlayMode && Save.instance.isSongHighScore(currentSong.id, currentDifficulty, data))
{
Save.instance.setSongScore(currentSong.id, currentDifficulty, data);
#if newgrounds
NGio.postScore(score, currentSong.id);
#end
isNewHighscore = true;
}
}
if (PlayStatePlaylist.isStoryMode)
{
isNewHighscore = false;
PlayStatePlaylist.campaignScore += songScore;
// Pop the next song ID from the list.
@ -2785,18 +2808,6 @@ class PlayState extends MusicBeatSubState
if (targetSongId == null)
{
FunkinSound.playMusic('freakyMenu',
{
overrideExisting: true,
restartTrack: false
});
// transIn = FlxTransitionableState.defaultTransIn;
// transOut = FlxTransitionableState.defaultTransOut;
// TODO: Rework week unlock logic.
// StoryMenuState.weekUnlocked[Std.int(Math.min(storyWeek + 1, StoryMenuState.weekUnlocked.length - 1))] = true;
if (currentSong.validScore)
{
NGio.unlockMedal(60961);
@ -2826,6 +2837,7 @@ class PlayState extends MusicBeatSubState
#if newgrounds
NGio.postScore(score, 'Level ${PlayStatePlaylist.campaignId}');
#end
isNewHighscore = true;
}
}
@ -2837,11 +2849,11 @@ class PlayState extends MusicBeatSubState
{
if (rightGoddamnNow)
{
moveToResultsScreen();
moveToResultsScreen(isNewHighscore);
}
else
{
zoomIntoResultsScreen();
zoomIntoResultsScreen(isNewHighscore);
}
}
}
@ -2854,7 +2866,7 @@ class PlayState extends MusicBeatSubState
FlxTransitionableState.skipNextTransIn = true;
FlxTransitionableState.skipNextTransOut = true;
FlxG.sound.music.stop();
if (FlxG.sound.music != null) FlxG.sound.music.stop();
vocals.stop();
// TODO: Softcode this cutscene.
@ -2902,11 +2914,11 @@ class PlayState extends MusicBeatSubState
{
if (rightGoddamnNow)
{
moveToResultsScreen();
moveToResultsScreen(isNewHighscore);
}
else
{
zoomIntoResultsScreen();
zoomIntoResultsScreen(isNewHighscore);
}
}
}
@ -2980,7 +2992,7 @@ class PlayState extends MusicBeatSubState
/**
* Play the camera zoom animation and then move to the results screen once it's done.
*/
function zoomIntoResultsScreen():Void
function zoomIntoResultsScreen(isNewHighscore:Bool):Void
{
trace('WENT TO RESULTS SCREEN!');
@ -3037,7 +3049,7 @@ class PlayState extends MusicBeatSubState
{
ease: FlxEase.expoIn,
onComplete: function(_) {
moveToResultsScreen();
moveToResultsScreen(isNewHighscore);
}
});
});
@ -3046,7 +3058,7 @@ class PlayState extends MusicBeatSubState
/**
* Move to the results screen right goddamn now.
*/
function moveToResultsScreen():Void
function moveToResultsScreen(isNewHighscore:Bool):Void
{
persistentUpdate = false;
vocals.stop();
@ -3058,7 +3070,24 @@ class PlayState extends MusicBeatSubState
{
storyMode: PlayStatePlaylist.isStoryMode,
title: PlayStatePlaylist.isStoryMode ? ('${PlayStatePlaylist.campaignTitle}') : ('${currentChart.songName} by ${currentChart.songArtist}'),
tallies: talliesToUse,
scoreData:
{
score: PlayStatePlaylist.isStoryMode ? PlayStatePlaylist.campaignScore : songScore,
tallies:
{
sick: talliesToUse.sick,
good: talliesToUse.good,
bad: talliesToUse.bad,
shit: talliesToUse.shit,
missed: talliesToUse.missed,
combo: talliesToUse.combo,
maxCombo: talliesToUse.maxCombo,
totalNotesHit: talliesToUse.totalNotesHit,
totalNotes: talliesToUse.totalNotes,
},
accuracy: Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes,
},
isNewHighscore: isNewHighscore
});
res.camera = camHUD;
openSubState(res);
@ -3096,6 +3125,15 @@ class PlayState extends MusicBeatSubState
FlxG.camera.focusOn(cameraFollowPoint.getPosition());
}
/**
* Sets the camera follow point's position and tweens the camera there.
*/
public function tweenCameraToPosition(?x:Float, ?y:Float, ?duration:Float, ?ease:Null<Float->Float>):Void
{
cameraFollowPoint.setPosition(x, y);
tweenCameraToFollowPoint(duration, ease);
}
/**
* Disables camera following and tweens the camera to the follow point manually.
*/
@ -3137,38 +3175,24 @@ class PlayState extends MusicBeatSubState
/**
* Tweens the camera zoom to the desired amount.
*/
public function tweenCameraZoom(?zoom:Float, ?duration:Float, ?directMode:Bool, ?ease:Null<Float->Float>):Void
public function tweenCameraZoom(?zoom:Float, ?duration:Float, ?direct:Bool, ?ease:Null<Float->Float>):Void
{
// Cancel the current tween if it's active.
cancelCameraZoomTween();
var targetZoom = zoom * FlxCamera.defaultZoom;
// Direct mode: Set zoom directly.
// Stage mode: Set zoom as a multiplier of the current stage's default zoom.
var targetZoom = zoom * (direct ? FlxCamera.defaultZoom : stageZoom);
if (directMode) // Direct mode: Tween defaultCameraZoom for basic "smooth" zooms.
if (duration == 0)
{
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});
}
// Instant zoom. No tween needed.
currentCameraZoom = targetZoom;
}
else // Additive mode: Tween additiveCameraZoom for ease-based zooms.
else
{
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});
}
// Zoom tween! Caching it so we can cancel/pause it later if needed.
cameraZoomTween = FlxTween.tween(this, {currentCameraZoom: targetZoom}, duration, {ease: ease});
}
}
@ -3205,7 +3229,10 @@ class PlayState extends MusicBeatSubState
// Don't go back in time to before the song started.
targetTimeMs = Math.max(0, targetTimeMs);
FlxG.sound.music.time = targetTimeMs;
if (FlxG.sound.music != null)
{
FlxG.sound.music.time = targetTimeMs;
}
handleSkippedNotes();
SongEventRegistry.handleSkippedEvents(songEvents, Conductor.instance.songPosition);

View file

@ -5,12 +5,13 @@ package funkin.play;
*
* TODO: Add getters/setters for all these properties to validate them.
*/
@:nullSafety
class PlayStatePlaylist
{
/**
* 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.
@ -31,8 +32,9 @@ class PlayStatePlaylist
/**
* 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;
@ -45,7 +47,7 @@ class PlayStatePlaylist
playlistSongIds = [];
campaignScore = 0;
campaignTitle = 'UNKNOWN';
campaignId = 'unknown';
campaignId = null;
campaignDifficulty = Constants.DEFAULT_DIFFICULTY;
}
}

View file

@ -0,0 +1,140 @@
package funkin.play;
import flixel.FlxSprite;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
class ResultScore extends FlxTypedSpriteGroup<ScoreNum>
{
public var scoreShit(default, set):Int = 0;
function set_scoreShit(val):Int
{
if (group == null || group.members == null) return val;
var loopNum:Int = group.members.length - 1;
var dumbNumb = Std.parseInt(Std.string(val));
var prevNum:ScoreNum;
while (dumbNumb > 0)
{
group.members[loopNum].digit = dumbNumb % 10;
// var funnyNum = group.members[loopNum];
// prevNum = group.members[loopNum + 1];
// if (prevNum != null)
// {
// funnyNum.x = prevNum.x - (funnyNum.width * 0.7);
// }
// funnyNum.y = (funnyNum.baseY - (funnyNum.height / 2)) + 73;
// funnyNum.x = (funnyNum.baseX - (funnyNum.width / 2)) + 450; // this plus value is hand picked lol!
dumbNumb = Math.floor(dumbNumb / 10);
loopNum--;
}
while (loopNum > 0)
{
group.members[loopNum].digit = 10;
loopNum--;
}
return val;
}
public function animateNumbers():Void
{
for (i in group.members)
{
i.playAnim();
}
}
public function new(x:Float, y:Float, digitCount:Int, scoreShit:Int = 100)
{
super(x, y);
for (i in 0...digitCount)
{
add(new ScoreNum(x + (65 * i), y));
}
this.scoreShit = scoreShit;
}
public function updateScore(scoreNew:Int)
{
scoreShit = scoreNew;
}
}
class ScoreNum extends FlxSprite
{
public var digit(default, set):Int = 10;
function set_digit(val):Int
{
if (val >= 0 && animation.curAnim != null && animation.curAnim.name != numToString[val])
{
animation.play(numToString[val], true, false, 0);
updateHitbox();
switch (val)
{
case 1:
// offset.x -= 15;
case 5:
// set offsets
// offset.x += 0;
// offset.y += 10;
case 7:
// offset.y += 6;
case 4:
// offset.y += 5;
case 9:
// offset.y += 5;
default:
centerOffsets(false);
}
}
return digit = val;
}
public function playAnim():Void
{
animation.play(numToString[digit], true, false, 0);
}
public var baseY:Float = 0;
public var baseX:Float = 0;
var numToString:Array<String> = [
"ZERO", "ONE", "TWO", "THREE", "FOUR", "FIVE", "SIX", "SEVEN", "EIGHT", "NINE", "DISABLED"
];
public function new(x:Float, y:Float)
{
super(x, y);
baseY = y;
baseX = x;
frames = Paths.getSparrowAtlas('resultScreen/score-digital-numbers');
for (i in 0...10)
{
var stringNum:String = numToString[i];
animation.addByPrefix(stringNum, '$stringNum DIGITAL', 24, false);
}
animation.addByPrefix('DISABLED', 'DISABLED', 24, false);
this.digit = 10;
animation.play(numToString[digit], true);
updateHitbox();
}
}

View file

@ -1,5 +1,6 @@
package funkin.play;
import funkin.util.MathUtil;
import funkin.ui.story.StoryMenuState;
import funkin.graphics.adobeanimate.FlxAtlasSprite;
import flixel.FlxSprite;
@ -10,12 +11,15 @@ import flixel.math.FlxPoint;
import funkin.ui.MusicBeatSubState;
import flixel.math.FlxRect;
import flixel.text.FlxBitmapText;
import funkin.ui.freeplay.FreeplayScore;
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.save.Save;
import funkin.save.Save.SaveScoreData;
import funkin.graphics.shaders.LeftMaskShader;
import funkin.play.components.TallyCounter;
@ -42,12 +46,15 @@ class ResultState extends MusicBeatSubState
override function create():Void
{
if (params.tallies.sick == params.tallies.totalNotesHit
&& params.tallies.maxCombo == params.tallies.totalNotesHit) resultsVariation = PERFECT;
else if (params.tallies.missed + params.tallies.bad + params.tallies.shit >= params.tallies.totalNotes * 0.50)
resultsVariation = SHIT; // if more than half of your song was missed, bad, or shit notes, you get shit ending!
else
resultsVariation = NORMAL;
/*
if (params.scoreData.sick == params.scoreData.totalNotesHit
&& params.scoreData.maxCombo == params.scoreData.totalNotesHit) resultsVariation = PERFECT;
else if (params.scoreData.missed + params.scoreData.bad + params.scoreData.shit >= params.scoreData.totalNotes * 0.50)
resultsVariation = SHIT; // if more than half of your song was missed, bad, or shit notes, you get shit ending!
else
resultsVariation = NORMAL;
*/
resultsVariation = NORMAL;
FunkinSound.playMusic('results$resultsVariation',
{
@ -130,12 +137,16 @@ class ResultState extends MusicBeatSubState
var diffSpr:String = switch (PlayState.instance.currentDifficulty)
{
case 'EASY':
case 'easy':
'difEasy';
case 'NORMAL':
case 'normal':
'difNormal';
case 'HARD':
case 'hard':
'difHard';
case 'erect':
'difErect';
case 'nightmare':
'difNightmare';
case _:
'difNormal';
}
@ -178,7 +189,7 @@ class ResultState extends MusicBeatSubState
scorePopin.visible = false;
add(scorePopin);
var highscoreNew:FlxSprite = new FlxSprite(280, 580);
var highscoreNew:FlxSprite = new FlxSprite(310, 570);
highscoreNew.frames = Paths.getSparrowAtlas("resultScreen/highscoreNew");
highscoreNew.animation.addByPrefix("new", "NEW HIGHSCORE", 24);
highscoreNew.visible = false;
@ -195,29 +206,33 @@ class ResultState extends MusicBeatSubState
* NOTE: We display how many notes were HIT, not how many notes there were in total.
*
*/
var totalHit:TallyCounter = new TallyCounter(375, hStuf * 3, params.tallies.totalNotesHit);
var totalHit:TallyCounter = new TallyCounter(375, hStuf * 3, params.scoreData.tallies.totalNotesHit);
ratingGrp.add(totalHit);
var maxCombo:TallyCounter = new TallyCounter(375, hStuf * 4, params.tallies.maxCombo);
var maxCombo:TallyCounter = new TallyCounter(375, hStuf * 4, params.scoreData.tallies.maxCombo);
ratingGrp.add(maxCombo);
hStuf += 2;
var extraYOffset:Float = 5;
var tallySick:TallyCounter = new TallyCounter(230, (hStuf * 5) + extraYOffset, params.tallies.sick, 0xFF89E59E);
var tallySick:TallyCounter = new TallyCounter(230, (hStuf * 5) + extraYOffset, params.scoreData.tallies.sick, 0xFF89E59E);
ratingGrp.add(tallySick);
var tallyGood:TallyCounter = new TallyCounter(210, (hStuf * 6) + extraYOffset, params.tallies.good, 0xFF89C9E5);
var tallyGood:TallyCounter = new TallyCounter(210, (hStuf * 6) + extraYOffset, params.scoreData.tallies.good, 0xFF89C9E5);
ratingGrp.add(tallyGood);
var tallyBad:TallyCounter = new TallyCounter(190, (hStuf * 7) + extraYOffset, params.tallies.bad, 0xFFE6CF8A);
var tallyBad:TallyCounter = new TallyCounter(190, (hStuf * 7) + extraYOffset, params.scoreData.tallies.bad, 0xFFE6CF8A);
ratingGrp.add(tallyBad);
var tallyShit:TallyCounter = new TallyCounter(220, (hStuf * 8) + extraYOffset, params.tallies.shit, 0xFFE68C8A);
var tallyShit:TallyCounter = new TallyCounter(220, (hStuf * 8) + extraYOffset, params.scoreData.tallies.shit, 0xFFE68C8A);
ratingGrp.add(tallyShit);
var tallyMissed:TallyCounter = new TallyCounter(260, (hStuf * 9) + extraYOffset, params.tallies.missed, 0xFFC68AE6);
var tallyMissed:TallyCounter = new TallyCounter(260, (hStuf * 9) + extraYOffset, params.scoreData.tallies.missed, 0xFFC68AE6);
ratingGrp.add(tallyMissed);
var score:ResultScore = new ResultScore(35, 305, 10, params.scoreData.score);
score.visible = false;
add(score);
for (ind => rating in ratingGrp.members)
{
rating.visible = false;
@ -233,11 +248,22 @@ class ResultState extends MusicBeatSubState
ratingsPopin.animation.finishCallback = anim -> {
scorePopin.animation.play("score");
scorePopin.animation.finishCallback = anim -> {
score.visible = true;
score.animateNumbers();
};
scorePopin.visible = true;
highscoreNew.visible = true;
highscoreNew.animation.play("new");
FlxTween.tween(highscoreNew, {y: highscoreNew.y + 10}, 0.8, {ease: FlxEase.quartOut});
if (params.isNewHighscore)
{
highscoreNew.visible = true;
highscoreNew.animation.play("new");
FlxTween.tween(highscoreNew, {y: highscoreNew.y + 10}, 0.8, {ease: FlxEase.quartOut});
}
else
{
highscoreNew.visible = false;
}
};
switch (resultsVariation)
@ -276,8 +302,6 @@ class ResultState extends MusicBeatSubState
}
});
if (params.tallies.isNewHighscore) trace("ITS A NEW HIGHSCORE!!!");
super.create();
}
@ -365,7 +389,7 @@ class ResultState extends MusicBeatSubState
}
else
{
openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new FreeplayState(null, sticker)));
openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> FreeplayState.build(null, sticker)));
}
}
@ -393,8 +417,13 @@ typedef ResultsStateParams =
*/
var title:String;
/**
* Whether the displayed score is a new highscore
*/
var isNewHighscore:Bool;
/**
* The score, accuracy, and judgements.
*/
var tallies:Highscore.Tallies;
var scoreData:SaveScoreData;
};

View file

@ -85,7 +85,7 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
comboSpr.velocity.y -= 150;
comboSpr.velocity.x += FlxG.random.int(1, 10);
add(comboSpr);
// add(comboSpr);
if (PlayState.instance.currentStageId.startsWith('school'))
{

View file

@ -6,6 +6,8 @@ import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import flixel.math.FlxMath;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.text.FlxText.FlxTextAlign;
import funkin.util.MathUtil;
/**
* Numerical counters used next to each judgement in the Results screen.
@ -13,18 +15,23 @@ import flixel.tweens.FlxTween;
class TallyCounter extends FlxTypedSpriteGroup<FlxSprite>
{
public var curNumber:Float = 0;
public var neededNumber:Int = 0;
public var flavour:Int = 0xFFFFFFFF;
public function new(x:Float, y:Float, neededNumber:Int = 0, ?flavour:Int = 0xFFFFFFFF)
public var align:FlxTextAlign = FlxTextAlign.LEFT;
public function new(x:Float, y:Float, neededNumber:Int = 0, ?flavour:Int = 0xFFFFFFFF, align:FlxTextAlign = FlxTextAlign.LEFT)
{
super(x, y);
this.align = align;
this.flavour = flavour;
this.neededNumber = neededNumber;
drawNumbers();
if (curNumber == neededNumber) drawNumbers();
}
var tmr:Float = 0;
@ -41,6 +48,8 @@ class TallyCounter extends FlxTypedSpriteGroup<FlxSprite>
var seperatedScore:Array<Int> = [];
var tempCombo:Int = Math.round(curNumber);
var fullNumberDigits:Int = Std.int(Math.max(1, Math.ceil(MathUtil.logBase(10, neededNumber))));
while (tempCombo != 0)
{
seperatedScore.push(tempCombo % 10);
@ -55,7 +64,13 @@ class TallyCounter extends FlxTypedSpriteGroup<FlxSprite>
{
if (ind >= members.length)
{
var numb:TallyNumber = new TallyNumber(ind * 43, 0, num);
var xPos = ind * (43 * this.scale.x);
if (this.align == FlxTextAlign.RIGHT)
{
xPos -= (fullNumberDigits * (43 * this.scale.x));
}
var numb:TallyNumber = new TallyNumber(xPos, 0, num);
numb.scale.set(this.scale.x, this.scale.y);
add(numb);
numb.color = flavour;
}

View file

@ -67,8 +67,13 @@ class VideoCutscene
if (!openfl.Assets.exists(filePath))
{
// Display a popup.
lime.app.Application.current.window.alert('Video file does not exist: ${filePath}', 'Error playing video');
return;
// lime.app.Application.current.window.alert('Video file does not exist: ${filePath}', 'Error playing video');
// return;
// TODO: After moving videos to their own library,
// this function ALWAYS FAILS on web, but the video still plays.
// I think that's due to a weird quirk with how OpenFL libraries work.
trace('Video file does not exist: ${filePath}');
}
var rawFilePath = Paths.stripLibrary(filePath);
@ -311,7 +316,7 @@ class VideoCutscene
blackScreen = null;
}
});
FlxTween.tween(FlxG.camera, {zoom: PlayState.instance.defaultCameraZoom}, transitionTime,
FlxTween.tween(FlxG.camera, {zoom: PlayState.instance.stageZoom}, transitionTime,
{
ease: FlxEase.quadInOut,
onComplete: function(twn:FlxTween) {

View file

@ -23,6 +23,7 @@ import funkin.modding.IScriptedClass.IDialogueScriptedClass;
import funkin.modding.IScriptedClass.IEventHandler;
import funkin.play.cutscene.dialogue.DialogueBox;
import funkin.util.SortUtil;
import funkin.util.EaseUtil;
/**
* A high-level handler for dialogue.
@ -179,7 +180,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
if (backdropData.fadeTime > 0.0)
{
backdrop.alpha = 0.0;
FlxTween.tween(backdrop, {alpha: 1.0}, backdropData.fadeTime, {ease: FlxEase.linear});
FlxTween.tween(backdrop, {alpha: 1.0}, backdropData.fadeTime, {ease: EaseUtil.stepped(10)});
}
else
{
@ -403,6 +404,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
type: ONESHOT, // holy shit like the game no way
startDelay: 0,
onComplete: (_) -> endOutro(),
ease: EaseUtil.stepped(8)
});
FlxTween.tween(this.music, {volume: 0.0}, outroData.fadeTime);

View file

@ -70,80 +70,76 @@ 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';
if (ease == null) ease = 'CLASSIC';
var currentStage = PlayState.instance.currentStage;
// Get target position based on char.
var targetX:Float = posX;
var targetY:Float = posY;
switch (char)
{
case -1: // Position
case -1: // Position ("focus" on origin)
trace('Focusing camera on static position.');
var xTarget:Float = posX;
var yTarget:Float = posY;
PlayState.instance.cameraFollowPoint.setPosition(xTarget, yTarget);
case 0: // Boyfriend
// Focus the camera on the player.
if (PlayState.instance.currentStage.getBoyfriend() == null)
case 0: // Boyfriend (focus on player)
if (currentStage.getBoyfriend() == null)
{
trace('No BF to focus on.');
return;
}
trace('Focusing camera on player.');
var xTarget:Float = PlayState.instance.currentStage.getBoyfriend().cameraFocusPoint.x + posX;
var yTarget:Float = PlayState.instance.currentStage.getBoyfriend().cameraFocusPoint.y + posY;
var bfPoint = currentStage.getBoyfriend().cameraFocusPoint;
targetX += bfPoint.x;
targetY += bfPoint.y;
PlayState.instance.cameraFollowPoint.setPosition(xTarget, yTarget);
case 1: // Dad
// Focus the camera on the dad.
if (PlayState.instance.currentStage.getDad() == null)
case 1: // Dad (focus on opponent)
if (currentStage.getDad() == null)
{
trace('No dad to focus on.');
return;
}
trace('Focusing camera on dad.');
trace(PlayState.instance.currentStage.getDad());
var xTarget:Float = PlayState.instance.currentStage.getDad().cameraFocusPoint.x + posX;
var yTarget:Float = PlayState.instance.currentStage.getDad().cameraFocusPoint.y + posY;
trace('Focusing camera on opponent.');
var dadPoint = currentStage.getDad().cameraFocusPoint;
targetX += dadPoint.x;
targetY += dadPoint.y;
PlayState.instance.cameraFollowPoint.setPosition(xTarget, yTarget);
case 2: // Girlfriend
// Focus the camera on the girlfriend.
if (PlayState.instance.currentStage.getGirlfriend() == null)
case 2: // Girlfriend (focus on girlfriend)
if (currentStage.getGirlfriend() == null)
{
trace('No GF to focus on.');
return;
}
trace('Focusing camera on girlfriend.');
var xTarget:Float = PlayState.instance.currentStage.getGirlfriend().cameraFocusPoint.x + posX;
var yTarget:Float = PlayState.instance.currentStage.getGirlfriend().cameraFocusPoint.y + posY;
var gfPoint = currentStage.getGirlfriend().cameraFocusPoint;
targetX += gfPoint.x;
targetY += gfPoint.y;
PlayState.instance.cameraFollowPoint.setPosition(xTarget, yTarget);
default:
trace('Unknown camera focus: ' + data);
}
if (useTween)
// Apply tween based on ease.
switch (ease)
{
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);
}
case 'CLASSIC': // Old-school. No ease. Just set follow point.
PlayState.instance.cancelCameraFollowTween();
PlayState.instance.cameraFollowPoint.setPosition(targetX, targetY);
case 'INSTANT': // Instant ease. Duration is automatically 0.
PlayState.instance.tweenCameraToPosition(targetX, targetY, 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.tweenCameraToPosition(targetX, targetY, durSeconds, easeFunction);
}
}
@ -187,12 +183,6 @@ class FocusCameraSongEvent extends SongEvent
type: SongEventFieldType.FLOAT,
units: "px"
},
{
name: 'useTween',
title: 'Use Tween',
type: SongEventFieldType.BOOL,
defaultValue: false
},
{
name: 'duration',
title: 'Duration',
@ -208,7 +198,9 @@ class FocusCameraSongEvent extends SongEvent
type: SongEventFieldType.ENUM,
keys: [
'Linear' => 'linear',
'Instant' => 'INSTANT',
'Sine In' => 'sineIn',
'Sine Out' => 'sineOut',
'Sine In/Out' => 'sineInOut',
'Quad In' => 'quadIn',
'Quad Out' => 'quadOut',
'Quad In/Out' => 'quadInOut',
@ -221,15 +213,17 @@ class FocusCameraSongEvent extends SongEvent
'Quint In' => 'quintIn',
'Quint Out' => 'quintOut',
'Quint In/Out' => 'quintInOut',
'Expo In' => 'expoIn',
'Expo Out' => 'expoOut',
'Expo In/Out' => 'expoInOut',
'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',
'Instant (Ignores duration)' => 'INSTANT',
'Classic (Ignores duration)' => 'CLASSIC'
]
}
]);

View file

@ -50,8 +50,8 @@ class SetCameraBopSongEvent extends SongEvent
var intensity:Null<Float> = data.getFloat('intensity');
if (intensity == null) intensity = 1.0;
PlayState.instance.cameraZoomIntensity = Constants.DEFAULT_ZOOM_INTENSITY * intensity;
PlayState.instance.hudCameraZoomIntensity = Constants.DEFAULT_ZOOM_INTENSITY * intensity * 2.0;
PlayState.instance.cameraBopIntensity = (Constants.DEFAULT_BOP_INTENSITY - 1.0) * intensity + 1.0;
PlayState.instance.hudCameraZoomIntensity = (Constants.DEFAULT_BOP_INTENSITY - 1.0) * intensity * 2.0;
PlayState.instance.cameraZoomRate = rate;
trace('Set camera zoom rate to ${PlayState.instance.cameraZoomRate}');
}

View file

@ -81,7 +81,6 @@ class ZoomCameraSongEvent extends SongEvent
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)
{
@ -102,9 +101,9 @@ class ZoomCameraSongEvent extends SongEvent
* ```
* {
* 'zoom': FLOAT, // Target zoom level.
* 'duration': FLOAT, // Optional duration in steps.
* 'mode': ENUM, // Whether to set additive zoom or direct zoom.
* 'ease': ENUM, // Optional easing function.
* 'duration': FLOAT, // Duration in steps.
* 'mode': ENUM, // Whether zoom is relative to the stage or absolute zoom.
* 'ease': ENUM, // Easing function.
* }
* @return SongEventSchema
*/
@ -130,9 +129,9 @@ class ZoomCameraSongEvent extends SongEvent
{
name: 'mode',
title: 'Mode',
defaultValue: 'direct',
defaultValue: 'stage',
type: SongEventFieldType.ENUM,
keys: ['Additive' => 'additive', 'Direct' => 'direct']
keys: ['Stage zoom' => 'stage', 'Absolute zoom' => 'direct']
},
{
name: 'ease',
@ -142,6 +141,9 @@ class ZoomCameraSongEvent extends SongEvent
keys: [
'Linear' => 'linear',
'Instant' => 'INSTANT',
'Sine In' => 'sineIn',
'Sine Out' => 'sineOut',
'Sine In/Out' => 'sineInOut',
'Quad In' => 'quadIn',
'Quad Out' => 'quadOut',
'Quad In/Out' => 'quadInOut',
@ -154,15 +156,15 @@ class ZoomCameraSongEvent extends SongEvent
'Quint In' => 'quintIn',
'Quint Out' => 'quintOut',
'Quint In/Out' => 'quintInOut',
'Expo In' => 'expoIn',
'Expo Out' => 'expoOut',
'Expo In/Out' => 'expoInOut',
'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',
'Elastic In/Out' => 'elasticInOut'
]
}
]);

View file

@ -77,13 +77,6 @@ class NoteHoldCover extends FlxTypedSpriteGroup<FlxSprite>
public override function update(elapsed):Void
{
super.update(elapsed);
if ((!holdNote.alive || holdNote.missedNote) && !glow.animation.curAnim.name.startsWith('holdCoverEnd'))
{
// If alive is false, the hold note was held to completion.
// If missedNote is true, the hold note was "dropped".
playEnd();
}
}
public function playStart():Void

View file

@ -295,6 +295,11 @@ class Strumline extends FlxSpriteGroup
{
if (noteData.length == 0) return;
// Ensure note data gets reset if the song happens to loop.
// NOTE: I had to remove this line because it was causing notes visible during the countdown to be placed multiple times.
// I don't remember what bug I was trying to fix by adding this.
// if (conductorInUse.currentStep == 0) nextNoteIndex = 0;
var songStart:Float = PlayState.instance?.startTimestamp ?? 0.0;
var hitWindowStart:Float = Conductor.instance.songPosition - Constants.HIT_WINDOW_MS;
var renderWindowStart:Float = Conductor.instance.songPosition + RENDER_DISTANCE_MS;
@ -365,8 +370,6 @@ class Strumline extends FlxSpriteGroup
// Hold note is offscreen, kill it.
holdNote.visible = false;
holdNote.kill(); // Do not destroy! Recycling is faster.
// The cover will see this and clean itself up.
}
else if (holdNote.hitNote && holdNote.sustainLength <= 0)
{
@ -380,10 +383,16 @@ class Strumline extends FlxSpriteGroup
playStatic(holdNote.noteDirection);
}
if (holdNote.cover != null)
if (holdNote.cover != null && isPlayer)
{
holdNote.cover.playEnd();
}
else if (holdNote.cover != null)
{
// *lightning* *zap* *crackle*
holdNote.cover.visible = false;
holdNote.cover.kill();
}
holdNote.visible = false;
holdNote.kill();
@ -405,6 +414,13 @@ class Strumline extends FlxSpriteGroup
{
holdNote.y = this.y - INITIAL_OFFSET + calculateNoteYPos(holdNote.strumTime, vwoosh) + yOffset + STRUMLINE_SIZE / 2;
}
// Clean up the cover.
if (holdNote.cover != null)
{
holdNote.cover.visible = false;
holdNote.cover.kill();
}
}
else if (Conductor.instance.songPosition > holdNote.strumTime && holdNote.hitNote)
{
@ -822,7 +838,7 @@ class Strumline extends FlxSpriteGroup
{
// The note sprite pool is full and all note splashes are active.
// We have to create a new note.
result = new SustainTrail(0, 100, noteStyle);
result = new SustainTrail(0, 0, noteStyle);
this.holdNotes.add(result);
}

View file

@ -404,11 +404,12 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
*
* @param variationId Optionally filter by a single variation.
* @param variationIds Optionally filter by multiple variations.
* @param showLocked Include charts which are not unlocked
* @param showHidden Include charts which are not accessible to the player.
*
* @return The list of difficulties.
*/
public function listDifficulties(?variationId:String, ?variationIds:Array<String>, showHidden:Bool = false):Array<String>
public function listDifficulties(?variationId:String, ?variationIds:Array<String>, showLocked:Bool = false, showHidden:Bool = false):Array<String>
{
if (variationIds == null) variationIds = [];
if (variationId != null) variationIds.push(variationId);

View file

@ -223,7 +223,8 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
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;
}

View file

@ -9,12 +9,13 @@ import funkin.save.migrator.SaveDataMigrator;
import funkin.ui.debug.charting.ChartEditorState.ChartEditorLiveInputStyle;
import funkin.ui.debug.charting.ChartEditorState.ChartEditorTheme;
import thx.semver.Version;
import funkin.util.SerializerUtil;
@:nullSafety
class Save
{
// Version 2.0.2 adds attributes to `optionsChartEditor`, that should return default values if they are null.
public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.2";
public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.3";
public static final SAVE_DATA_VERSION_RULE:thx.semver.VersionRule = "2.0.x";
// We load this version's saves from a new save path, to maintain SOME level of backwards compatibility.
@ -391,6 +392,22 @@ class Save
*/
public function getLevelScore(levelId:String, difficultyId:String = 'normal'):Null<SaveScoreData>
{
if (data.scores?.levels == null)
{
if (data.scores == null)
{
data.scores =
{
songs: [],
levels: []
};
}
else
{
data.scores.levels = [];
}
}
var level = data.scores.levels.get(levelId);
if (level == null)
{
@ -641,6 +658,9 @@ class Save
{
trace("[SAVE] Loading save from slot " + slot + "...");
// Prevent crashes if the save data is corrupted.
SerializerUtil.initSerializer();
FlxG.save.bind('$SAVE_NAME${slot}', SAVE_PATH);
if (FlxG.save.isEmpty())
@ -650,9 +670,9 @@ class Save
if (legacySaveData != null)
{
trace('[SAVE] Found legacy save data, converting...');
var gameSave = SaveDataMigrator.migrate(legacySaveData);
var gameSave = SaveDataMigrator.migrateFromLegacy(legacySaveData);
@:privateAccess
FlxG.save.mergeData(gameSave.data);
FlxG.save.mergeData(gameSave.data, true);
}
else
{
@ -664,7 +684,7 @@ class Save
trace('[SAVE] Loaded save data.');
@:privateAccess
var gameSave = SaveDataMigrator.migrate(FlxG.save.data);
FlxG.save.mergeData(gameSave.data);
FlxG.save.mergeData(gameSave.data, true);
}
}
@ -673,7 +693,7 @@ class Save
trace("[SAVE] Checking for legacy save data...");
var legacySave:FlxSave = new FlxSave();
legacySave.bind(SAVE_NAME_LEGACY, SAVE_PATH_LEGACY);
if (legacySave?.data == null)
if (legacySave.isEmpty())
{
trace("[SAVE] No legacy save data found.");
return null;

View file

@ -3,6 +3,7 @@ package funkin.save.migrator;
import funkin.save.Save;
import funkin.save.migrator.RawSaveData_v1_0_0;
import thx.semver.Version;
import funkin.util.StructureUtil;
import funkin.util.VersionUtil;
@:nullSafety
@ -26,7 +27,7 @@ class SaveDataMigrator
if (VersionUtil.validateVersion(version, Save.SAVE_DATA_VERSION_RULE))
{
// Simply import the structured data.
var save:Save = new Save(inputData);
var save:Save = new Save(StructureUtil.deepMerge(Save.getDefault(), inputData));
return save;
}
else

View file

@ -0,0 +1,34 @@
package funkin.ui.credits;
/**
* The members of the Funkin' Crew, organized by their roles.
*/
typedef CreditsData =
{
var entries:Array<CreditsDataRole>;
}
/**
* The members of a specific role on the Funkin' Crew.
*/
typedef CreditsDataRole =
{
@:optional
var header:String;
@:optional
@:default([])
var body:Array<CreditsDataMember>;
@:optional
@:default(false)
var appendBackers:Bool;
}
/**
* A member of a specific person on the Funkin' Crew.
*/
typedef CreditsDataMember =
{
var line:String;
}

View file

@ -0,0 +1,146 @@
package funkin.ui.credits;
import funkin.data.JsonFile;
using StringTools;
@:nullSafety
class CreditsDataHandler
{
public static final BACKER_PUBLIC_URL:String = 'https://funkin.me/backers';
#if HARDCODED_CREDITS
static final CREDITS_DATA_PATH:String = "assets/exclude/data/credits.json";
#else
static final CREDITS_DATA_PATH:String = "assets/data/credits.json";
#end
public static function debugPrint(data:Null<CreditsData>):Void
{
if (data == null)
{
trace('CreditsData(NULL)');
return;
}
if (data.entries == null || data.entries.length == 0)
{
trace('CreditsData(EMPTY)');
return;
}
var entryCount = data.entries.length;
var lineCount = 0;
for (entry in data.entries)
{
lineCount += entry?.body?.length ?? 0;
}
trace('CreditsData($entryCount entries containing $lineCount lines)');
}
/**
* If for some reason the full credits won't load,
* use this hardcoded data for the original Funkin' Crew.
*
* @return `CreditsData`
*/
public static inline function getFallback():CreditsData
{
return {
entries: [
{
header: 'Founders',
body: [
{line: 'ninjamuffin99'},
{line: 'PhantomArcade'},
{line: 'KawaiSprite'},
{line: 'evilsk8r'},
]
},
{
header: 'Kickstarter Backers',
appendBackers: true
}
]
};
}
public static function fetchBackerEntries():Array<String>
{
// TODO: Replace this with a web request.
// We can't just grab the current Kickstarter data and include it in builds,
// because we don't want to deadname people who haven't logged into the portal yet.
// It can be async and paginated for performance!
return ['See the list of backers at $BACKER_PUBLIC_URL.'];
}
#if HARDCODED_CREDITS
/**
* The data for the credits.
* Hardcoded into game via a macro at compile time.
*/
public static final CREDITS_DATA:Null<CreditsData> = #if macro null #else CreditsDataMacro.loadCreditsData() #end;
#else
/**
* The data for the credits.
* Loaded dynamically from the game folder when needed.
* Nullable because data may fail to parse.
*/
public static var CREDITS_DATA(get, default):Null<CreditsData> = null;
static function get_CREDITS_DATA():Null<CreditsData>
{
if (CREDITS_DATA == null) CREDITS_DATA = parseCreditsData(fetchCreditsData());
return CREDITS_DATA;
}
static function fetchCreditsData():funkin.data.JsonFile
{
#if !macro
var rawJson:String = openfl.Assets.getText(CREDITS_DATA_PATH).trim();
return {
fileName: CREDITS_DATA_PATH,
contents: rawJson
};
#else
return {
fileName: CREDITS_DATA_PATH,
contents: null
};
#end
}
static function parseCreditsData(file:JsonFile):Null<CreditsData>
{
#if !macro
if (file.contents == null) return null;
var parser = new json2object.JsonParser<CreditsData>();
parser.ignoreUnknownVariables = false;
trace('[CREDITS] Parsing credits data from ${CREDITS_DATA_PATH}');
parser.fromJson(file.contents, file.fileName);
if (parser.errors.length > 0)
{
printErrors(parser.errors, file.fileName);
return null;
}
return parser.value;
#else
return null;
#end
}
static function printErrors(errors:Array<json2object.Error>, id:String = ''):Void
{
trace('[CREDITS] Failed to parse credits data: ${id}');
for (error in errors)
funkin.data.DataError.printError(error);
}
#end
}

View file

@ -0,0 +1,67 @@
package funkin.ui.credits;
#if macro
import haxe.macro.Context;
#end
@:access(funkin.ui.credits.CreditsDataHandler)
class CreditsDataMacro
{
public static macro function loadCreditsData():haxe.macro.Expr.ExprOf<CreditsData>
{
#if !display
trace('Hardcoding credits data...');
var json = CreditsDataMacro.fetchJSON();
if (json == null)
{
Context.info('[WARN] Could not fetch JSON data for credits.', Context.currentPos());
return macro $v{CreditsDataHandler.getFallback()};
}
var creditsData = CreditsDataMacro.parseJSON(json);
if (creditsData == null)
{
Context.info('[WARN] Could not parse JSON data for credits.', Context.currentPos());
return macro $v{CreditsDataHandler.getFallback()};
}
CreditsDataHandler.debugPrint(creditsData);
return macro $v{creditsData};
// return macro $v{null};
#else
// `#if display` is used for code completion. In this case we return
// a minimal value to keep code completion fast.
return macro $v{CreditsDataHandler.getFallback()};
#end
}
#if macro
static function fetchJSON():Null<String>
{
return sys.io.File.getContent(CreditsDataHandler.CREDITS_DATA_PATH);
}
/**
* Parse the JSON data for the credits.
*
* @param json The string data to parse.
* @return The parsed data.
*/
static function parseJSON(json:String):Null<CreditsData>
{
try
{
// TODO: Use something with better validation but that still works at macro time.
return haxe.Json.parse(json);
}
catch (e)
{
trace('[ERROR] Failed to parse JSON data for credits.');
trace(e);
return null;
}
}
#end
}

View file

@ -0,0 +1,213 @@
package funkin.ui.credits;
import flixel.text.FlxText;
import flixel.util.FlxColor;
import funkin.audio.FunkinSound;
import flixel.FlxSprite;
import flixel.group.FlxSpriteGroup;
/**
* The state used to display the credits scroll.
* AAA studios often fail to credit properly, and we're better than them!
*/
class CreditsState extends MusicBeatState
{
/**
* The height the credits should start at.
* Make this an instanced variable so it gets set by the constructor.
*/
final STARTING_HEIGHT = FlxG.height;
/**
* The padding on each side of the screen.
*/
static final SCREEN_PAD = 24;
/**
* The width of the screen the credits should maximally fill up.
* Make this an instanced variable so it gets set by the constructor.
*/
final FULL_WIDTH = FlxG.width - (SCREEN_PAD * 2);
/**
* The font to use to display the text.
* To use a font from the `assets` folder, use `Paths.font(...)`.
* Choose something that will render Unicode properly.
*/
static final CREDITS_FONT = 'Arial';
/**
* The size of the font.
*/
static final CREDITS_FONT_SIZE = 48;
static final CREDITS_HEADER_FONT_SIZE = 72;
/**
* The color of the text itself.
*/
static final CREDITS_FONT_COLOR = FlxColor.WHITE;
/**
* The color of the text's outline.
*/
static final CREDITS_FONT_STROKE_COLOR = FlxColor.BLACK;
/**
* The speed the credits scroll at, in pixels per second.
*/
static final CREDITS_SCROLL_BASE_SPEED = 25.0;
/**
* The speed the credits scroll at while the button is held, in pixels per second.
*/
static final CREDITS_SCROLL_FAST_SPEED = CREDITS_SCROLL_BASE_SPEED * 4.0;
/**
* The actual sprites and text used to display the credits.
*/
var creditsGroup:FlxSpriteGroup;
var scrollPaused:Bool = false;
public function new()
{
super();
}
public override function create():Void
{
super.create();
// Background
var bg = new FlxSprite(Paths.image('menuDesat'));
bg.scrollFactor.x = 0;
bg.scrollFactor.y = 0;
bg.setGraphicSize(Std.int(FlxG.width));
bg.updateHitbox();
bg.x = 0;
bg.y = 0;
bg.visible = true;
bg.color = 0xFFB57EDC; // Lavender
add(bg);
// TODO: Once we need to display Kickstarter backers,
// make this use a recycled pool so we don't kill peformance.
creditsGroup = new FlxSpriteGroup();
creditsGroup.x = SCREEN_PAD;
creditsGroup.y = STARTING_HEIGHT;
buildCreditsGroup();
add(creditsGroup);
// Music
FunkinSound.playMusic('freeplayRandom',
{
startingVolume: 0.0,
overrideExisting: true,
restartTrack: true,
loop: true
});
FlxG.sound.music.fadeIn(2, 0, 0.8);
}
function buildCreditsGroup():Void
{
var y = 0;
for (entry in CreditsDataHandler.CREDITS_DATA.entries)
{
if (entry.header != null)
{
creditsGroup.add(buildCreditsLine(entry.header, y, true, CreditsSide.Center));
y += CREDITS_HEADER_FONT_SIZE;
}
for (line in entry?.body ?? [])
{
creditsGroup.add(buildCreditsLine(line.line, y, false, CreditsSide.Center));
y += CREDITS_FONT_SIZE;
}
if (entry.appendBackers)
{
var backers = CreditsDataHandler.fetchBackerEntries();
for (backer in backers)
{
creditsGroup.add(buildCreditsLine(backer, y, false, CreditsSide.Center));
y += CREDITS_FONT_SIZE;
}
}
// Padding between each role.
y += CREDITS_FONT_SIZE * 2;
}
}
function buildCreditsLine(text:String, yPos:Float, header:Bool, side:CreditsSide = CreditsSide.Center):FlxText
{
// CreditsSide.Center: Full screen width
// CreditsSide.Left: Left half of screen
// CreditsSide.Right: Right half of screen
var xPos = (side == CreditsSide.Right) ? (FULL_WIDTH / 2) : 0;
var width = (side == CreditsSide.Center) ? FULL_WIDTH : (FULL_WIDTH / 2);
var size = header ? CREDITS_HEADER_FONT_SIZE : CREDITS_FONT_SIZE;
var creditsLine:FlxText = new FlxText(xPos, yPos, width, text);
creditsLine.setFormat(CREDITS_FONT, size, CREDITS_FONT_COLOR, FlxTextAlign.CENTER, FlxTextBorderStyle.OUTLINE, CREDITS_FONT_STROKE_COLOR, true);
return creditsLine;
}
public override function update(elapsed:Float):Void
{
super.update(elapsed);
if (!scrollPaused)
{
// TODO: Replace with whatever the special note button is.
if (controls.ACCEPT || FlxG.keys.pressed.SPACE)
{
// Move the whole group.
creditsGroup.y -= CREDITS_SCROLL_FAST_SPEED * elapsed;
}
else
{
// Move the whole group.
creditsGroup.y -= CREDITS_SCROLL_BASE_SPEED * elapsed;
}
}
if (controls.BACK || hasEnded())
{
exit();
}
else if (controls.PAUSE)
{
scrollPaused = !scrollPaused;
}
}
function hasEnded():Bool
{
return creditsGroup.y < -creditsGroup.height;
}
function exit():Void
{
FlxG.switchState(new funkin.ui.mainmenu.MainMenuState());
}
public override function destroy():Void
{
super.destroy();
}
}
enum CreditsSide
{
Left;
Center;
Right;
}

View file

@ -6,15 +6,14 @@ import flixel.addons.transition.FlxTransitionableState;
import flixel.FlxCamera;
import flixel.FlxSprite;
import flixel.FlxSubState;
import flixel.graphics.FlxGraphic;
import flixel.group.FlxGroup.FlxTypedGroup;
import funkin.graphics.FunkinCamera;
import flixel.group.FlxSpriteGroup;
import flixel.input.keyboard.FlxKey;
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;
@ -26,26 +25,19 @@ import flixel.util.FlxSort;
import flixel.util.FlxTimer;
import funkin.audio.FunkinSound;
import funkin.audio.visualize.PolygonSpectogram;
import funkin.audio.visualize.PolygonSpectogram;
import funkin.audio.VoicesGroup;
import funkin.audio.waveform.WaveformSprite;
import funkin.data.notestyle.NoteStyleRegistry;
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.SongEventData;
import funkin.data.song.SongData.SongEventData;
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.SongOffsets;
import funkin.data.song.SongDataUtils;
import funkin.data.song.SongDataUtils;
import funkin.data.song.SongRegistry;
import funkin.data.song.SongRegistry;
import funkin.data.stage.StageData;
import funkin.graphics.FunkinCamera;
import funkin.graphics.FunkinSprite;
import funkin.input.Cursor;
import funkin.input.TurboKeyHandler;
@ -56,13 +48,12 @@ import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.play.components.HealthIcon;
import funkin.play.notes.NoteSprite;
import funkin.play.PlayState;
import funkin.play.PlayStatePlaylist;
import funkin.play.song.Song;
import funkin.save.Save;
import funkin.ui.debug.charting.commands.AddEventsCommand;
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.CopyItemsCommand;
import funkin.ui.debug.charting.commands.CutItemsCommand;
import funkin.ui.debug.charting.commands.DeselectAllItemsCommand;
@ -95,6 +86,7 @@ import funkin.ui.debug.charting.toolboxes.ChartEditorOffsetsToolbox;
import funkin.ui.haxeui.components.CharacterPlayer;
import funkin.ui.haxeui.HaxeUIState;
import funkin.ui.mainmenu.MainMenuState;
import funkin.ui.transition.LoadingState;
import funkin.util.Constants;
import funkin.util.FileUtil;
import funkin.util.logging.CrashHandler;
@ -119,7 +111,6 @@ import haxe.ui.containers.Grid;
import haxe.ui.containers.HBox;
import haxe.ui.containers.menus.Menu;
import haxe.ui.containers.menus.MenuBar;
import haxe.ui.containers.menus.MenuBar;
import haxe.ui.containers.menus.MenuCheckBox;
import haxe.ui.containers.menus.MenuItem;
import haxe.ui.containers.ScrollView;
@ -130,7 +121,6 @@ import haxe.ui.core.Screen;
import haxe.ui.events.DragEvent;
import haxe.ui.events.MouseEvent;
import haxe.ui.events.UIEvent;
import haxe.ui.events.UIEvent;
import haxe.ui.focus.FocusManager;
import haxe.ui.Toolkit;
import openfl.display.BitmapData;
@ -5338,30 +5328,31 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
}
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;
}
// TODO: Rework asset system so we can remove this.
// TODO: Rework asset system so we can remove this jank.
switch (currentSongStage)
{
case 'mainStage':
Paths.setCurrentLevel('week1');
PlayStatePlaylist.campaignId = 'week1';
case 'spookyMansion':
Paths.setCurrentLevel('week2');
PlayStatePlaylist.campaignId = 'week2';
case 'phillyTrain':
Paths.setCurrentLevel('week3');
PlayStatePlaylist.campaignId = 'week3';
case 'limoRide':
Paths.setCurrentLevel('week4');
PlayStatePlaylist.campaignId = 'week4';
case 'mallXmas' | 'mallEvil':
Paths.setCurrentLevel('week5');
PlayStatePlaylist.campaignId = 'week5';
case 'school' | 'schoolEvil':
Paths.setCurrentLevel('week6');
PlayStatePlaylist.campaignId = 'week6';
case 'tankmanBattlefield':
Paths.setCurrentLevel('week7');
PlayStatePlaylist.campaignId = 'week7';
case 'phillyStreets' | 'phillyBlazin' | 'phillyBlazin2':
Paths.setCurrentLevel('weekend1');
PlayStatePlaylist.campaignId = 'weekend1';
}
Paths.setCurrentLevel(PlayStatePlaylist.campaignId);
subStateClosed.add(reviveUICamera);
subStateClosed.add(resetConductorAfterTest);
@ -5369,7 +5360,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
FlxTransitionableState.skipNextTransIn = false;
FlxTransitionableState.skipNextTransOut = false;
var targetState = new PlayState(
var targetStateParams =
{
targetSong: targetSong,
targetDifficulty: selectedDifficulty,
@ -5380,14 +5371,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
startTimestamp: startTimestamp,
playbackRate: playbackRate,
overrideMusic: true,
});
};
// Override music.
if (audioInstTrack != null)
{
FlxG.sound.music = audioInstTrack;
}
targetState.vocals = audioVocalTrackGroup;
// Kill and replace the UI camera so it doesn't get destroyed during the state transition.
uiCamera.kill();
@ -5397,7 +5387,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
this.persistentUpdate = false;
this.persistentDraw = false;
stopWelcomeMusic();
openSubState(targetState);
LoadingState.loadPlayState(targetStateParams, false, true, function(targetState) {
targetState.vocals = audioVocalTrackGroup;
});
}
/**

View file

@ -117,12 +117,6 @@ class ChartEditorNoteSprite extends FlxSprite
{
noteFrameCollection.pushFrame(frame);
}
var frameCollectionNormal2:FlxAtlasFrames = Paths.getSparrowAtlas('NoteHoldNormal');
for (frame in frameCollectionNormal2.frames)
{
noteFrameCollection.pushFrame(frame);
}
// Pixel notes
var graphicPixel = FlxG.bitmap.add(Paths.image('weeb/pixelUI/arrows-pixels', 'week6'), false, null);

View file

@ -73,7 +73,7 @@ class ChartEditorImportExportHandler
state.loadInstFromAsset(Paths.inst(songId, '-$variation'), variation);
}
for (difficultyId in song.listDifficulties(variation))
for (difficultyId in song.listDifficulties(variation, true, true))
{
var diff:Null<SongDifficulty> = song.getDifficulty(difficultyId, variation);
if (diff == null) continue;

View file

@ -308,16 +308,6 @@ class ChartEditorToolboxHandler
state.playtestBotPlayMode = checkboxBotPlay.selected;
};
var checkboxDebugger:Null<CheckBox> = toolbox.findComponent('playtestDebuggerCheckbox', CheckBox);
if (checkboxDebugger == null) throw 'ChartEditorToolboxHandler.buildToolboxPlaytestPropertiesLayout() - Could not find playtestDebuggerCheckbox component.';
state.enabledDebuggerPopup = checkboxDebugger.selected;
checkboxDebugger.onClick = _ -> {
state.enabledDebuggerPopup = checkboxDebugger.selected;
};
var checkboxSongScripts:Null<CheckBox> = toolbox.findComponent('playtestSongScriptsCheckbox', CheckBox);
if (checkboxSongScripts == null)

View file

@ -171,10 +171,7 @@ class LatencyState extends MusicBeatSubState
trace(FlxG.sound.music._channel.position);
*/
#if FLX_DEBUG
funnyStatsGraph.update(FlxG.sound.music.time % 500);
realStats.update(swagSong.getTimeWithDiff() % 500);
#end
// localConductor.update(swagSong.time, false);
if (FlxG.keys.justPressed.S)
{

View file

@ -1,6 +1,7 @@
package funkin.ui.freeplay;
import funkin.data.freeplay.AlbumData;
import funkin.data.animation.AnimationData;
import funkin.data.freeplay.AlbumRegistry;
import funkin.data.IRegistryEntry;
import flixel.graphics.FlxGraphic;
@ -75,6 +76,16 @@ class Album implements IRegistryEntry<AlbumData>
return _data.albumTitleAsset;
}
public function hasAlbumTitleAnimations()
{
return _data.albumTitleAnimations.length > 0;
}
public function getAlbumTitleAnimations():Array<AnimationData>
{
return _data.albumTitleAnimations;
}
public function toString():String
{
return 'Album($id)';

View file

@ -1,5 +1,6 @@
package funkin.ui.freeplay;
import funkin.graphics.adobeanimate.FlxAtlasSprite;
import flixel.FlxSprite;
import flixel.group.FlxSpriteGroup;
import flixel.util.FlxSort;
@ -7,6 +8,7 @@ import flixel.tweens.FlxTween;
import flixel.util.FlxTimer;
import flixel.tweens.FlxEase;
import funkin.data.freeplay.AlbumRegistry;
import funkin.util.assets.FlxAnimationUtil;
import funkin.graphics.FunkinSprite;
import funkin.util.SortUtil;
import openfl.utils.Assets;
@ -21,9 +23,9 @@ 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;
public var albumId(default, set):Null<String>;
function set_albumId(value:String):String
function set_albumId(value:Null<String>):Null<String>
{
if (this.albumId != value)
{
@ -34,30 +36,47 @@ class AlbumRoll extends FlxSpriteGroup
return value;
}
var albumArt:FunkinSprite;
var albumTitle:FunkinSprite;
var difficultyStars:DifficultyStars;
var newAlbumArt:FlxAtlasSprite;
// var difficultyStars:DifficultyStars;
var _exitMovers:Null<FreeplayState.ExitMoverData>;
var albumData:Album;
final animNames:Map<String, String> = [
"volume1-active" => "ENTRANCE",
"volume2-active" => "ENTRANCE VOL2",
"volume3-active" => "ENTRANCE VOL3",
"volume1-trans" => "VOL1 TRANS",
"volume2-trans" => "VOL2 TRANS",
"volume3-trans" => "VOL3 TRANS",
"volume1-idle" => "VOL1 STILL",
"volume2-idle" => "VOL2 STILL",
"volume3-idle" => "VOL3 STILL",
];
public function new()
{
super();
albumTitle = new FunkinSprite(947, 491);
albumTitle.visible = true;
albumTitle.zIndex = 200;
add(albumTitle);
newAlbumArt = new FlxAtlasSprite(0, 0, Paths.animateAtlas("freeplay/albumRoll/freeplayAlbum"));
newAlbumArt.visible = false;
newAlbumArt.onAnimationFinish.add(onAlbumFinish);
difficultyStars = new DifficultyStars(140, 39);
add(newAlbumArt);
difficultyStars.stars.visible = true;
albumTitle.visible = false;
// albumArtist.visible = false;
// difficultyStars = new DifficultyStars(140, 39);
// difficultyStars.stars.visible = false;
// add(difficultyStars);
}
// var albumArtist:FlxSprite = new FlxSprite(1010, 607).loadGraphic(Paths.image('freeplay/albumArtist-kawaisprite'));
function onAlbumFinish(animName:String):Void
{
// Play the idle animation for the current album.
newAlbumArt.playAnimation(animNames.get('$albumId-idle'), false, false, true);
// End on the last frame and don't continue until playAnimation is called again.
// newAlbumArt.anim.pause();
}
/**
@ -65,6 +84,12 @@ class AlbumRoll extends FlxSpriteGroup
*/
function updateAlbum():Void
{
if (albumId == null)
{
// difficultyStars.stars.visible = false;
return;
}
albumData = AlbumRegistry.instance.fetchEntry(albumId);
if (albumData == null)
@ -74,33 +99,8 @@ class AlbumRoll extends FlxSpriteGroup
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();
}
@ -126,67 +126,46 @@ class AlbumRoll extends FlxSpriteGroup
if (exitMovers == null) return;
exitMovers.set([albumArt],
exitMovers.set([newAlbumArt],
{
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
});
}
var titleTimer:Null<FlxTimer> = null;
/**
* 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});
newAlbumArt.visible = true;
newAlbumArt.playAnimation(animNames.get('$albumId-active'), false, false, false);
albumTitle.visible = false;
// difficultyStars.stars.visible = false;
new FlxTimer().start(0.75, function(_) {
showTitle();
// showTitle();
// showStars();
});
}
public function setDifficultyStars(?difficulty:Int):Void
public function skipIntro():Void
{
if (difficulty == null) return;
difficultyStars.difficulty = difficulty;
newAlbumArt.playAnimation(animNames.get('$albumId-trans'), false, false, false);
}
public function showTitle():Void
{
albumTitle.visible = true;
}
/**
* Make the album stars visible.
*/
public function showStars():Void
{
// albumArtist.visible = false;
difficultyStars.stars.visible = false;
}
// public function setDifficultyStars(?difficulty:Int):Void
// {
// if (difficulty == null) return;
// difficultyStars.difficulty = difficulty;
// }
// /**
// * Make the album stars visible.
// */
// public function showStars():Void
// {
// difficultyStars.stars.visible = false; // true;
// }
}

View file

@ -27,8 +27,8 @@ class DJBoyfriend extends FlxAtlasSprite
var gotSpooked:Bool = false;
static final SPOOK_PERIOD:Float = 10.0;
static final TV_PERIOD:Float = 10.0;
static final SPOOK_PERIOD:Float = 120.0;
static final TV_PERIOD:Float = 180.0;
// Time since dad last SPOOKED you.
var timeSinceSpook:Float = 0;
@ -43,7 +43,14 @@ class DJBoyfriend extends FlxAtlasSprite
switch (name)
{
case "Boyfriend DJ watchin tv OG":
if (number == 85) runTvLogic();
if (number == 80)
{
FunkinSound.playOnce(Paths.sound('remote_click'));
}
if (number == 85)
{
runTvLogic();
}
default:
}
};
@ -219,19 +226,17 @@ class DJBoyfriend extends FlxAtlasSprite
if (cartoonSnd == null)
{
// tv is OFF, but getting turned on
// Eric got FUCKING TROLLED there is no `tv_on` or `channel_switch` sound!
// FunkinSound.playOnce(Paths.sound('tv_on'), 1.0, function() {
// });
loadCartoon();
FunkinSound.playOnce(Paths.sound('tv_on'), 1.0, function() {
loadCartoon();
});
}
else
{
// plays it smidge after the click
// new FlxTimer().start(0.1, function(_) {
// // FunkinSound.playOnce(Paths.sound('channel_switch'));
// });
cartoonSnd.destroy();
loadCartoon();
FunkinSound.playOnce(Paths.sound('channel_switch'), 1.0, function() {
cartoonSnd.destroy();
loadCartoon();
});
}
// loadCartoon();

View file

@ -1,106 +0,0 @@
package funkin.ui.freeplay;
import flixel.group.FlxSpriteGroup;
import funkin.graphics.adobeanimate.FlxAtlasSprite;
import funkin.graphics.shaders.HSVShader;
class DifficultyStars extends FlxSpriteGroup
{
/**
* Internal handler var for difficulty... ranges from 0... to 15
* 0 is 1 star... 15 is 0 stars!
*/
var curDifficulty(default, set):Int = 0;
/**
* Range between 0 and 15
*/
public var difficulty(default, set):Int = 1;
public var stars:FlxAtlasSprite;
var flames:FreeplayFlames;
var hsvShader:HSVShader;
public function new(x:Float, y:Float)
{
super(x, y);
hsvShader = new HSVShader();
flames = new FreeplayFlames(0, 0);
add(flames);
stars = new FlxAtlasSprite(0, 0, Paths.animateAtlas("freeplay/freeplayStars"));
stars.anim.play("diff stars");
add(stars);
stars.shader = hsvShader;
for (memb in flames.members)
memb.shader = hsvShader;
}
override function update(elapsed:Float):Void
{
super.update(elapsed);
// "loops" the current animation
// for clarity, the animation file looks like
// frame : stars
// 0-99: 1 star
// 100-199: 2 stars
// ......
// 1300-1499: 15 stars
// 1500 : 0 stars
if (curDifficulty < 15 && stars.anim.curFrame >= (curDifficulty + 1) * 100)
{
stars.anim.play("diff stars", true, false, curDifficulty * 100);
}
}
function set_difficulty(value:Int):Int
{
difficulty = value;
if (difficulty <= 0)
{
difficulty = 0;
curDifficulty = 15;
}
else if (difficulty <= 15)
{
difficulty = value;
curDifficulty = difficulty - 1;
}
else
{
difficulty = 15;
curDifficulty = difficulty - 1;
}
if (difficulty > 10) flames.flameCount = difficulty - 10;
else
flames.flameCount = 0;
return difficulty;
}
function set_curDifficulty(value:Int):Int
{
curDifficulty = value;
if (curDifficulty == 15)
{
stars.anim.play("diff stars", true, false, 1500);
stars.anim.pause();
}
else
{
stars.anim.curFrame = Std.int(curDifficulty * 100);
stars.anim.play("diff stars", true, false, curDifficulty * 100);
}
return curDifficulty;
}
}

View file

@ -42,11 +42,11 @@ class FreeplayScore extends FlxTypedSpriteGroup<ScoreNum>
return val;
}
public function new(x:Float, y:Float, scoreShit:Int = 100)
public function new(x:Float, y:Float, digitCount:Int, scoreShit:Int = 100)
{
super(x, y);
for (i in 0...7)
for (i in 0...digitCount)
{
add(new ScoreNum(x + (45 * i), y, 0));
}

View file

@ -133,8 +133,8 @@ class FreeplayState extends MusicBeatSubState
var stickerSubState:StickerSubState;
static var rememberedDifficulty:Null<String> = Constants.DEFAULT_DIFFICULTY;
static var rememberedSongId:Null<String> = null;
public static var rememberedDifficulty:Null<String> = Constants.DEFAULT_DIFFICULTY;
public static var rememberedSongId:Null<String> = 'tutorial';
public function new(?params:FreeplayStateParams, ?stickers:StickerSubState)
{
@ -145,7 +145,7 @@ class FreeplayState extends MusicBeatSubState
stickerSubState = stickers;
}
super();
super(FlxColor.TRANSPARENT);
}
override function create():Void
@ -195,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:Array<String> = song.listDifficulties(displayedVariations);
var availableDifficultiesForSong:Array<String> = song.listDifficulties(displayedVariations, false);
if (availableDifficultiesForSong.length == 0) continue;
songs.push(new FreeplaySongData(levelId, songId, song, displayedVariations));
@ -380,7 +380,7 @@ class FreeplayState extends MusicBeatSubState
}
albumRoll = new AlbumRoll();
albumRoll.albumId = 'volume1';
albumRoll.albumId = null;
add(albumRoll);
albumRoll.applyExitMovers(exitMovers);
@ -425,7 +425,7 @@ class FreeplayState extends MusicBeatSubState
tmr.time = FlxG.random.float(20, 60);
}, 0);
fp = new FreeplayScore(460, 60, 100);
fp = new FreeplayScore(460, 60, 7, 100);
fp.visible = false;
add(fp);
@ -470,11 +470,7 @@ class FreeplayState extends MusicBeatSubState
albumRoll.playIntro();
new FlxTimer().start(0.75, function(_) {
albumRoll.showTitle();
});
new FlxTimer().start(35 / 24, function(_) {
albumRoll.showStars();
// albumRoll.showTitle();
});
FlxTween.tween(grpDifficulties, {x: 90}, 0.6, {ease: FlxEase.quartOut});
@ -536,21 +532,18 @@ class FreeplayState extends MusicBeatSubState
});
}
var currentFilter:SongFilter = null;
var currentFilteredSongs:Array<FreeplaySongData> = [];
/**
* 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
* @param onlyIfChanged Only apply the filter if the song list has changed
*/
public function generateSongList(?filterStuff:SongFilter, force:Bool = false):Void
public function generateSongList(filterStuff:Null<SongFilter>, force:Bool = false, onlyIfChanged:Bool = true):Void
{
curSelected = 1;
for (cap in grpCapsules.members)
{
cap.kill();
}
var tempSongs:Array<FreeplaySongData> = songs;
if (filterStuff != null)
@ -582,6 +575,35 @@ class FreeplayState extends MusicBeatSubState
}
}
// Filter further by current selected difficulty.
if (currentDifficulty != null)
{
tempSongs = tempSongs.filter(song -> {
if (song == null) return true; // Random
return song.songDifficulties.contains(currentDifficulty);
});
}
if (onlyIfChanged)
{
// == performs equality by reference
if (tempSongs.isEqualUnordered(currentFilteredSongs)) return;
}
// Only now do we know that the filter is actually changing.
rememberedSongId = grpCapsules.members[curSelected]?.songData?.songId ?? rememberedSongId;
for (cap in grpCapsules.members)
{
cap.kill();
}
currentFilter = filterStuff;
currentFilteredSongs = tempSongs;
curSelected = 0;
var hsvShader:HSVShader = new HSVShader();
var randomCapsule:SongMenuItem = grpCapsules.recycle(SongMenuItem);
@ -658,11 +680,12 @@ class FreeplayState extends MusicBeatSubState
if (FlxG.keys.justPressed.F)
{
if (songs[curSelected] != null)
var targetSong = grpCapsules.members[curSelected]?.songData;
if (targetSong != null)
{
var realShit:Int = curSelected;
songs[curSelected].isFav = !songs[curSelected].isFav;
if (songs[curSelected].isFav)
targetSong.isFav = !targetSong.isFav;
if (targetSong.isFav)
{
FlxTween.tween(grpCapsules.members[realShit], {angle: 360}, 0.4,
{
@ -854,11 +877,13 @@ class FreeplayState extends MusicBeatSubState
{
dj.resetAFKTimer();
changeDiff(-1);
generateSongList(currentFilter, true);
}
if (controls.UI_RIGHT_P && !FlxG.keys.pressed.CONTROL)
{
dj.resetAFKTimer();
changeDiff(1);
generateSongList(currentFilter, true);
}
if (controls.BACK && !typing.hasFocus)
@ -877,6 +902,8 @@ class FreeplayState extends MusicBeatSubState
for (spr in grpSpr)
{
if (spr == null) continue;
var funnyMoveShit:MoveData = moveData;
if (moveData.x == null) funnyMoveShit.x = spr.x;
@ -899,7 +926,7 @@ class FreeplayState extends MusicBeatSubState
if (Type.getClass(FlxG.state) == MainMenuState)
{
FlxG.state.persistentUpdate = true;
FlxG.state.persistentUpdate = false;
FlxG.state.persistentDraw = true;
}
@ -908,6 +935,11 @@ class FreeplayState extends MusicBeatSubState
FlxTransitionableState.skipNextTransOut = true;
if (Type.getClass(FlxG.state) == MainMenuState)
{
FunkinSound.playMusic('freakyMenu',
{
overrideExisting: true,
restartTrack: false
});
close();
}
else
@ -926,7 +958,7 @@ class FreeplayState extends MusicBeatSubState
public override function destroy():Void
{
super.destroy();
var daSong:Null<FreeplaySongData> = songs[curSelected];
var daSong:Null<FreeplaySongData> = currentFilteredSongs[curSelected];
if (daSong != null)
{
clearDaCache(daSong.songName);
@ -948,10 +980,10 @@ class FreeplayState extends MusicBeatSubState
currentDifficulty = diffIdsCurrent[currentDifficultyIndex];
var daSong:Null<FreeplaySongData> = songs[curSelected];
var daSong:Null<FreeplaySongData> = grpCapsules.members[curSelected].songData;
if (daSong != null)
{
var songScore:SaveScoreData = Save.instance.getSongScore(songs[curSelected].songId, currentDifficulty);
var songScore:SaveScoreData = Save.instance.getSongScore(grpCapsules.members[curSelected].songData.songId, currentDifficulty);
intendedScore = songScore?.score ?? 0;
intendedCompletion = songScore?.accuracy ?? 0.0;
rememberedDifficulty = currentDifficulty;
@ -1011,15 +1043,12 @@ class FreeplayState extends MusicBeatSubState
}
}
// Set the difficulty star count on the right.
albumRoll.setDifficultyStars(daSong?.songRating);
// Set the album graphic and play the animation if relevant.
var newAlbumId:String = daSong?.albumId ?? Constants.DEFAULT_ALBUM_ID;
var newAlbumId:String = daSong?.albumId;
if (albumRoll.albumId != newAlbumId)
{
albumRoll.albumId = newAlbumId;
albumRoll.playIntro();
albumRoll.skipIntro();
}
}
@ -1103,6 +1132,12 @@ class FreeplayState extends MusicBeatSubState
targetVariation: targetVariation,
practiceMode: false,
minimalMode: false,
#if (debug || FORCE_DEBUG_VERSION)
botPlayMode: FlxG.keys.pressed.SHIFT,
#else
botPlayMode: false,
#end
// TODO: Make these an option! It's currently only accessible via chart editor.
// startTimestamp: 0.0,
// playbackRate: 0.5,
@ -1115,20 +1150,18 @@ class FreeplayState extends MusicBeatSubState
{
if (rememberedSongId != null)
{
curSelected = songs.findIndex(function(song) {
curSelected = currentFilteredSongs.findIndex(function(song) {
if (song == null) return false;
return song.songId == rememberedSongId;
});
if (curSelected == -1) curSelected = 0;
}
if (rememberedDifficulty != null)
{
currentDifficulty = rememberedDifficulty;
}
// Set the difficulty star count on the right.
var daSong:Null<FreeplaySongData> = songs[curSelected];
albumRoll.setDifficultyStars(daSong?.songRating ?? 0);
}
function changeSelection(change:Int = 0):Void
@ -1156,8 +1189,10 @@ class FreeplayState extends MusicBeatSubState
{
intendedScore = 0;
intendedCompletion = 0.0;
diffIdsCurrent = diffIdsTotal;
rememberedSongId = null;
rememberedDifficulty = null;
albumRoll.albumId = null;
}
for (index => capsule in grpCapsules.members)
@ -1195,12 +1230,33 @@ class FreeplayState extends MusicBeatSubState
});
if (didReplace)
{
FunkinSound.playMusic('freakyMenu',
{
startingVolume: 0.0,
overrideExisting: true,
restartTrack: false
});
FlxG.sound.music.fadeIn(2, 0, 0.8);
}
}
grpCapsules.members[curSelected].selected = true;
}
}
/**
* Build an instance of `FreeplayState` that is above the `MainMenuState`.
* @return The MainMenuState with the FreeplayState as a substate.
*/
public static function build(?params:FreeplayStateParams, ?stickers:StickerSubState):MusicBeatState
{
var result = new MainMenuState();
result.persistentUpdate = false;
result.persistentDraw = true;
result.openSubState(new FreeplayState(params, stickers));
return result;
}
}
/**
@ -1307,7 +1363,7 @@ class FreeplaySongData
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 albumId(default, null):Null<String> = null;
public var currentDifficulty(default, set):String = Constants.DEFAULT_DIFFICULTY;
public var displayedVariations(default, null):Array<String> = [Constants.DEFAULT_VARIATION];
@ -1333,7 +1389,7 @@ class FreeplaySongData
function updateValues(variations:Array<String>):Void
{
this.songDifficulties = song.listDifficulties(variations);
this.songDifficulties = song.listDifficulties(variations, false, false);
if (!this.songDifficulties.contains(currentDifficulty)) currentDifficulty = Constants.DEFAULT_DIFFICULTY;
var songDifficulty:SongDifficulty = song.getDifficulty(currentDifficulty, variations);
@ -1341,7 +1397,15 @@ class FreeplaySongData
this.songName = songDifficulty.songName;
this.songCharacter = songDifficulty.characters.opponent;
this.songRating = songDifficulty.difficultyRating;
this.albumId = songDifficulty.album;
if (songDifficulty.album == null)
{
FlxG.log.warn('No album for: ${songDifficulty.songName}');
this.albumId = Constants.DEFAULT_ALBUM_ID;
}
else
{
this.albumId = songDifficulty.album;
}
}
}

View file

@ -83,10 +83,10 @@ class SongMenuItem extends FlxSpriteGroup
diffRatingSprite = new FlxSprite(145, 90).loadGraphic(Paths.image('freeplay/diffRatings/diff00'));
diffRatingSprite.shader = grayscaleShader;
diffRatingSprite.origin.set(capsule.origin.x - diffRatingSprite.x, capsule.origin.y - diffRatingSprite.y);
// 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);
// grpHide.add(diffRatingSprite);
songText = new CapsuleText(capsule.width * 0.26, 45, 'Random', Std.int(40 * realScaled));
add(songText);

View file

@ -1,5 +1,6 @@
package funkin.ui.mainmenu;
import funkin.graphics.FunkinSprite;
import flixel.addons.transition.FlxTransitionableState;
import funkin.ui.debug.DebugMenuSubState;
import flixel.FlxObject;
@ -51,14 +52,13 @@ class MainMenuState extends MusicBeatState
transIn = FlxTransitionableState.defaultTransIn;
transOut = FlxTransitionableState.defaultTransOut;
if (!(FlxG?.sound?.music?.playing ?? false))
{
playMenuMusic();
}
playMenuMusic();
persistentUpdate = persistentDraw = true;
persistentUpdate = false;
persistentDraw = true;
var bg:FlxSprite = new FlxSprite(Paths.image('menuBG'));
var bg = FunkinSprite.create('menuDesat');
bg.color = 0xFFFDE871;
bg.scrollFactor.x = 0;
bg.scrollFactor.y = 0.17;
bg.setGraphicSize(Std.int(bg.width * 1.2));
@ -109,14 +109,21 @@ class MainMenuState extends MusicBeatState
});
#if CAN_OPEN_LINKS
// In order to prevent popup blockers from triggering,
// we need to open the link as an immediate result of a keypress event,
// so we can't wait for the flicker animation to complete.
var hasPopupBlocker = #if web true #else false #end;
createMenuItem('donate', 'mainmenu/donate', selectDonate, hasPopupBlocker);
createMenuItem('merch', 'mainmenu/merch', selectMerch, hasPopupBlocker);
#end
createMenuItem('options', 'mainmenu/options', function() {
startExitState(() -> new funkin.ui.options.OptionsState());
});
createMenuItem('credits', 'mainmenu/credits', function() {
startExitState(() -> new funkin.ui.credits.CreditsState());
});
// Reset position of menu items.
var spacing = 160;
var top = (FlxG.height - (spacing * (menuItems.length - 1))) / 2;
@ -125,6 +132,9 @@ class MainMenuState extends MusicBeatState
var menuItem = menuItems.members[i];
menuItem.x = FlxG.width / 2;
menuItem.y = top + spacing * i;
menuItem.scrollFactor.x = 0.0;
// This one affects how much the menu items move when you scroll between them.
menuItem.scrollFactor.y = 0.4;
}
resetCamStuff();
@ -166,6 +176,7 @@ class MainMenuState extends MusicBeatState
{
FlxG.cameras.reset(new FunkinCamera());
FlxG.camera.follow(camFollow, null, 0.06);
FlxG.camera.snapToTarget();
}
function createMenuItem(name:String, atlas:String, callback:Void->Void, fireInstantly:Bool = false):Void
@ -212,6 +223,11 @@ class MainMenuState extends MusicBeatState
{
WindowUtil.openURL(Constants.URL_ITCH);
}
function selectMerch()
{
WindowUtil.openURL(Constants.URL_MERCH);
}
#end
#if newgrounds
@ -311,8 +327,6 @@ class MainMenuState extends MusicBeatState
// Open the debug menu, defaults to ` / ~
if (controls.DEBUG_MENU)
{
this.persistentUpdate = false;
this.persistentDraw = false;
FlxG.state.openSubState(new DebugMenuSubState());
}

View file

@ -169,7 +169,7 @@ class Level implements IRegistryEntry<LevelData>
if (firstSong != null)
{
// Don't display alternate characters in Story Mode. Only show `default` and `erect` variations.
for (difficulty in firstSong.listDifficulties([Constants.DEFAULT_VARIATION, 'erect']))
for (difficulty in firstSong.listDifficulties([Constants.DEFAULT_VARIATION, 'erect'], false, false))
{
difficulties.push(difficulty);
}

View file

@ -17,7 +17,7 @@ import funkin.ui.MusicBeatState;
*/
class AttractState extends MusicBeatState
{
static final ATTRACT_VIDEO_PATH:String = Paths.videos('kickstarterTrailer');
static final ATTRACT_VIDEO_PATH:String = Paths.stripLibrary(Paths.videos('kickstarterTrailer', 'shared'));
public override function create():Void
{
@ -29,10 +29,12 @@ class AttractState extends MusicBeatState
}
#if html5
trace('Playing web video ${ATTRACT_VIDEO_PATH}');
playVideoHTML5(ATTRACT_VIDEO_PATH);
#end
#if hxCodec
trace('Playing native video ${ATTRACT_VIDEO_PATH}');
playVideoNative(ATTRACT_VIDEO_PATH);
#end
}

View file

@ -220,7 +220,7 @@ class TitleState extends MusicBeatState
function playMenuMusic():Void
{
var shouldFadeIn = (FlxG.sound.music == null);
var shouldFadeIn:Bool = (FlxG.sound.music == null);
// Load music. Includes logic to handle BPM changes.
FunkinSound.playMusic('freakyMenu',
{
@ -229,7 +229,7 @@ class TitleState extends MusicBeatState
restartTrack: true
});
// Fade from 0.0 to 0.7 over 4 seconds
if (shouldFadeIn) FlxG.sound.music.fadeIn(4.0, 0.0, 0.7);
if (shouldFadeIn) FlxG.sound.music.fadeIn(4.0, 0.0, 1.0);
}
function getIntroTextShit():Array<Array<String>>
@ -290,18 +290,6 @@ class TitleState extends MusicBeatState
// do controls.PAUSE | controls.ACCEPT instead?
var pressedEnter:Bool = FlxG.keys.justPressed.ENTER;
if (FlxG.onMobile)
{
for (touch in FlxG.touches.list)
{
if (touch.justPressed)
{
FlxG.switchState(() -> new FreeplayState());
pressedEnter = true;
}
}
}
var gamepad:FlxGamepad = FlxG.gamepads.lastActive;
if (gamepad != null)

View file

@ -22,10 +22,12 @@ import openfl.filters.ShaderFilter;
import openfl.utils.Assets;
import flixel.util.typeLimit.NextState;
class LoadingState extends MusicBeatState
class LoadingState extends MusicBeatSubState
{
inline static var MIN_TIME = 1.0;
var asSubState:Bool = false;
var target:NextState;
var playParams:Null<PlayStateParams>;
var stopMusic:Bool = false;
@ -178,7 +180,16 @@ class LoadingState extends MusicBeatState
FlxG.sound.music = null;
}
FlxG.switchState(target);
if (asSubState)
{
this.close();
// We will assume the target is a valid substate.
FlxG.state.openSubState(cast target);
}
else
{
FlxG.switchState(target);
}
}
static function getSongPath():String
@ -190,17 +201,41 @@ class LoadingState extends MusicBeatState
* Starts the transition to a new `PlayState` to start a new song.
* First switches to the `LoadingState` if assets need to be loaded.
* @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.
*/
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);
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
// Switch to loading state while we load assets (default on HTML5 target).
var loadStateCtor:NextState = () -> new LoadingState(playStateCtor, shouldStopMusic, params);
FlxG.switchState(loadStateCtor);
var loadStateCtor = function() {
var result = new LoadingState(playStateCtor, shouldStopMusic, params);
@:privateAccess
result.asSubState = asSubState;
return result;
}
if (asSubState)
{
FlxG.state.openSubState(cast loadStateCtor());
}
else
{
FlxG.switchState(loadStateCtor);
}
#else
// All assets preloaded, switch directly to play state (defualt on other targets).
if (shouldStopMusic && FlxG.sound.music != null)
@ -210,14 +245,42 @@ class LoadingState extends MusicBeatState
}
// Load and cache the song's charts.
if (params?.targetSong != null)
// Don't do this if we already provided the music and charts.
if (params?.targetSong != null && !params.overrideMusic)
{
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.
FunkinSprite.preparePurgeCache();
FunkinSprite.cacheTexture(Paths.image('combo'));
FunkinSprite.cacheTexture(Paths.image('healthBar'));
FunkinSprite.cacheTexture(Paths.image('menuDesat'));
FunkinSprite.cacheTexture(Paths.image('combo'));
@ -247,7 +310,10 @@ class LoadingState extends MusicBeatState
// 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.
// 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);
trace('Got ${assets.length} assets: ${assets}');
@ -278,20 +344,6 @@ class LoadingState extends MusicBeatState
// FunkinSprite.cacheAllSongTextures(stage)
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

View file

@ -1,5 +1,10 @@
package funkin.ui.transition.preload;
import openfl.filters.GlowFilter;
import openfl.display.SpreadMethod;
import openfl.display.GradientType;
import openfl.geom.Matrix;
import openfl.filters.BlurFilter;
import openfl.events.MouseEvent;
import flash.display.Bitmap;
import flash.display.BitmapData;
@ -46,7 +51,7 @@ class FunkinPreloader extends FlxBasePreloader
*/
static final BAR_PADDING:Float = 20;
static final BAR_HEIGHT:Int = 20;
static final BAR_HEIGHT:Int = 12;
/**
* Logo takes this long (in seconds) to fade in.
@ -108,13 +113,22 @@ class FunkinPreloader extends FlxBasePreloader
#if TOUCH_HERE_TO_PLAY
var touchHereToPlay:Bitmap;
#end
var progressBarPieces:Array<Sprite>;
var progressBar:Bitmap;
var progressLeftText:TextField;
var progressRightText:TextField;
var dspText:TextField;
var enhancedText:TextField;
var stereoText:TextField;
var vfdShader:VFDOverlay;
var box:Sprite;
var progressLines:Sprite;
public function new()
{
super(Constants.PRELOADER_MIN_STAGE_TIME, Constants.SITE_LOCK);
super(Constants.PRELOADER_MIN_STAGE_TIME);
// We can't even call trace() yet, until Flixel loads.
trace('Initializing custom preloader...');
@ -146,7 +160,7 @@ class FunkinPreloader extends FlxBasePreloader
bmp.x = (this._width - bmp.width) / 2;
bmp.y = (this._height - bmp.height) / 2;
});
addChild(logo);
// addChild(logo);
#if TOUCH_HERE_TO_PLAY
touchHereToPlay = createBitmap(TouchHereToPlayImage, function(bmp:Bitmap) {
@ -160,16 +174,48 @@ class FunkinPreloader extends FlxBasePreloader
addChild(touchHereToPlay);
#end
var amountOfPieces:Int = 16;
progressBarPieces = [];
var maxBarWidth = this._width - BAR_PADDING * 2;
var pieceWidth = maxBarWidth / amountOfPieces;
var pieceGap:Int = 8;
progressLines = new openfl.display.Sprite();
progressLines.graphics.lineStyle(2, Constants.COLOR_PRELOADER_BAR);
progressLines.graphics.drawRect(-2, 480, this._width + 4, 30);
addChild(progressLines);
var progressBarPiece = new Sprite();
progressBarPiece.graphics.beginFill(Constants.COLOR_PRELOADER_BAR);
progressBarPiece.graphics.drawRoundRect(0, 0, pieceWidth - pieceGap, BAR_HEIGHT, 4, 4);
progressBarPiece.graphics.endFill();
for (i in 0...amountOfPieces)
{
var piece = new Sprite();
piece.graphics.beginFill(Constants.COLOR_PRELOADER_BAR);
piece.graphics.drawRoundRect(0, 0, pieceWidth - pieceGap, BAR_HEIGHT, 4, 4);
piece.graphics.endFill();
piece.x = i * (piece.width + pieceGap);
piece.y = this._height - BAR_PADDING - BAR_HEIGHT - 200;
addChild(piece);
progressBarPieces.push(piece);
}
// 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);
// 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();
dspText = new TextField();
enhancedText = new TextField();
stereoText = new TextField();
var progressLeftTextFormat = new TextFormat("VCR OSD Mono", 16, Constants.COLOR_PRELOADER_BAR, true);
var progressLeftTextFormat = new TextFormat("DS-Digital", 32, Constants.COLOR_PRELOADER_BAR, true);
progressLeftTextFormat.align = TextFormatAlign.LEFT;
progressLeftText.defaultTextFormat = progressLeftTextFormat;
@ -177,13 +223,14 @@ class FunkinPreloader extends FlxBasePreloader
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;
progressLeftText.y = this._height - BAR_PADDING - BAR_HEIGHT - 290;
// progressLeftText.shader = new VFDOverlay();
addChild(progressLeftText);
// Create the progress %.
progressRightText = new TextField();
var progressRightTextFormat = new TextFormat("VCR OSD Mono", 16, Constants.COLOR_PRELOADER_BAR, true);
var progressRightTextFormat = new TextFormat("DS-Digital", 16, Constants.COLOR_PRELOADER_BAR, true);
progressRightTextFormat.align = TextFormatAlign.RIGHT;
progressRightText.defaultTextFormat = progressRightTextFormat;
@ -193,6 +240,60 @@ class FunkinPreloader extends FlxBasePreloader
progressRightText.x = BAR_PADDING;
progressRightText.y = this._height - BAR_PADDING - BAR_HEIGHT - 16 - 4;
addChild(progressRightText);
box = new Sprite();
box.graphics.beginFill(Constants.COLOR_PRELOADER_BAR, 1);
box.graphics.drawRoundRect(0, 0, 64, 20, 5, 5);
box.graphics.drawRoundRect(70, 0, 58, 20, 5, 5);
box.graphics.endFill();
box.graphics.beginFill(Constants.COLOR_PRELOADER_BAR, 0.1);
box.graphics.drawRoundRect(0, 0, 128, 20, 5, 5);
box.graphics.endFill();
box.x = 880;
box.y = 440;
addChild(box);
dspText.selectable = false;
dspText.textColor = 0x000000;
dspText.width = this._width;
dspText.height = 20;
dspText.text = 'DSP';
dspText.x = 10;
dspText.y = -5;
box.addChild(dspText);
enhancedText.selectable = false;
enhancedText.textColor = Constants.COLOR_PRELOADER_BAR;
enhancedText.width = this._width;
enhancedText.height = 100;
enhancedText.text = 'ENHANCED';
enhancedText.x = -100;
enhancedText.y = 0;
box.addChild(enhancedText);
stereoText.selectable = false;
stereoText.textColor = Constants.COLOR_PRELOADER_BAR;
stereoText.width = this._width;
stereoText.height = 100;
stereoText.text = 'STEREO';
stereoText.x = 0;
stereoText.y = -40;
box.addChild(stereoText);
// var dummyMatrix:openfl.geom.Matrix = new Matrix();
// dummyMatrix.createGradientBox(this._width, this._height * 0.1, 90 * Math.PI / 180);
// var gradient:Sprite = new Sprite();
// gradient.graphics.beginGradientFill(GradientType.LINEAR, [0xFFFFFF, 0x000000], [1, 1], [0, 255], dummyMatrix, SpreadMethod.REFLECT);
// gradient.graphics.drawRect(0, 0, this._width, this._height);
// gradient.graphics.endFill();
// addChild(gradient);
var vfdBitmap:Bitmap = new Bitmap(new BitmapData(this._width, this._height, true, 0xFFFFFFFF));
addChild(vfdBitmap);
vfdShader = new VFDOverlay();
vfdBitmap.shader = vfdShader;
}
var lastElapsed:Float = 0.0;
@ -200,6 +301,8 @@ class FunkinPreloader extends FlxBasePreloader
override function update(percent:Float):Void
{
var elapsed:Float = (Date.now().getTime() - this._startTime) / 1000.0;
vfdShader.update(elapsed * 100);
// trace('Time since last frame: ' + (lastElapsed - elapsed));
downloadingAssetsPercent = percent;
@ -748,12 +851,19 @@ class FunkinPreloader extends FlxBasePreloader
else
{
renderLogoFadeIn(elapsed);
// Render progress bar
var maxWidth = this._width - BAR_PADDING * 2;
var barWidth = maxWidth * percent;
var piecesToRender:Int = Std.int(percent * progressBarPieces.length);
for (i => piece in progressBarPieces)
{
piece.alpha = i <= piecesToRender ? 0.9 : 0.1;
}
}
// Render progress bar
var maxWidth = this._width - BAR_PADDING * 2;
var barWidth = maxWidth * percent;
progressBar.width = barWidth;
// progressBar.width = barWidth;
// Cycle ellipsis count to show loading
var ellipsisCount:Int = Std.int(elapsed / ELLIPSIS_TIME) % 3 + 1;
@ -766,29 +876,29 @@ class FunkinPreloader extends FlxBasePreloader
{
// case FunkinPreloaderState.NotStarted:
default:
updateProgressLeftText('Loading (0/$TOTAL_STEPS)$ellipsis');
updateProgressLeftText('Loading \n0/$TOTAL_STEPS $ellipsis');
case FunkinPreloaderState.DownloadingAssets:
updateProgressLeftText('Downloading assets (1/$TOTAL_STEPS)$ellipsis');
updateProgressLeftText('Downloading assets \n1/$TOTAL_STEPS $ellipsis');
case FunkinPreloaderState.PreloadingPlayAssets:
updateProgressLeftText('Preloading assets (2/$TOTAL_STEPS)$ellipsis');
updateProgressLeftText('Preloading assets \n2/$TOTAL_STEPS $ellipsis');
case FunkinPreloaderState.InitializingScripts:
updateProgressLeftText('Initializing scripts (3/$TOTAL_STEPS)$ellipsis');
updateProgressLeftText('Initializing scripts \n3/$TOTAL_STEPS $ellipsis');
case FunkinPreloaderState.CachingGraphics:
updateProgressLeftText('Caching graphics (4/$TOTAL_STEPS)$ellipsis');
updateProgressLeftText('Caching graphics \n4/$TOTAL_STEPS $ellipsis');
case FunkinPreloaderState.CachingAudio:
updateProgressLeftText('Caching audio (5/$TOTAL_STEPS)$ellipsis');
updateProgressLeftText('Caching audio \n5/$TOTAL_STEPS $ellipsis');
case FunkinPreloaderState.CachingData:
updateProgressLeftText('Caching data (6/$TOTAL_STEPS)$ellipsis');
updateProgressLeftText('Caching data \n6/$TOTAL_STEPS $ellipsis');
case FunkinPreloaderState.ParsingSpritesheets:
updateProgressLeftText('Parsing spritesheets (7/$TOTAL_STEPS)$ellipsis');
updateProgressLeftText('Parsing spritesheets \n7/$TOTAL_STEPS $ellipsis');
case FunkinPreloaderState.ParsingStages:
updateProgressLeftText('Parsing stages (8/$TOTAL_STEPS)$ellipsis');
updateProgressLeftText('Parsing stages \n8/$TOTAL_STEPS $ellipsis');
case FunkinPreloaderState.ParsingCharacters:
updateProgressLeftText('Parsing characters (9/$TOTAL_STEPS)$ellipsis');
updateProgressLeftText('Parsing characters \n9/$TOTAL_STEPS $ellipsis');
case FunkinPreloaderState.ParsingSongs:
updateProgressLeftText('Parsing songs (10/$TOTAL_STEPS)$ellipsis');
updateProgressLeftText('Parsing songs \n10/$TOTAL_STEPS $ellipsis');
case FunkinPreloaderState.Complete:
updateProgressLeftText('Finishing up ($TOTAL_STEPS/$TOTAL_STEPS)$ellipsis');
updateProgressLeftText('Finishing up \n$TOTAL_STEPS/$TOTAL_STEPS $ellipsis');
#if TOUCH_HERE_TO_PLAY
case FunkinPreloaderState.TouchHereToPlay:
updateProgressLeftText(null);
@ -815,10 +925,21 @@ class FunkinPreloader extends FlxBasePreloader
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);
var progressLeftTextFormat = new TextFormat("DS-Digital", 32, Constants.COLOR_PRELOADER_BAR, true);
progressLeftTextFormat.align = TextFormatAlign.LEFT;
progressLeftText.defaultTextFormat = progressLeftTextFormat;
progressLeftText.text = text;
dspText.defaultTextFormat = new TextFormat("Quantico", 20, 0x000000, false);
dspText.text = 'DSP\t\t\t\t\tFNF'; // fukin dum....
dspText.textColor = 0x000000;
enhancedText.defaultTextFormat = new TextFormat("Inconsolata Black", 16, Constants.COLOR_PRELOADER_BAR, false);
enhancedText.text = 'ENHANCED';
enhancedText.textColor = Constants.COLOR_PRELOADER_BAR;
stereoText.defaultTextFormat = new TextFormat("Inconsolata Bold", 36, Constants.COLOR_PRELOADER_BAR, false);
stereoText.text = 'NATURAL STEREO';
}
}
}
@ -845,9 +966,17 @@ class FunkinPreloader extends FlxBasePreloader
logo.y = (this._height - logo.height) / 2;
// Fade out progress bar too.
progressBar.alpha = logo.alpha;
// progressBar.alpha = logo.alpha;
progressLeftText.alpha = logo.alpha;
progressRightText.alpha = logo.alpha;
box.alpha = logo.alpha;
dspText.alpha = logo.alpha;
enhancedText.alpha = logo.alpha;
stereoText.alpha = logo.alpha;
progressLines.alpha = logo.alpha;
for (piece in progressBarPieces)
piece.alpha = logo.alpha;
return elapsedFinished;
}
@ -901,8 +1030,8 @@ class FunkinPreloader extends FlxBasePreloader
{
// Ensure the graphics are properly destroyed and GC'd.
removeChild(logo);
removeChild(progressBar);
logo = progressBar = null;
// removeChild(progressBar);
logo = null;
super.destroy();
}

View file

@ -0,0 +1,70 @@
package funkin.ui.transition.preload;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import openfl.display.GraphicsShader;
class VFDOverlay extends GraphicsShader
{
public var elapsedTime(default, set):Float = 0;
function set_elapsedTime(value:Float):Float
{
u_time.value = [value];
return value;
}
@:glFragmentSource('#pragma header
const vec2 s = vec2(1, 1.7320508);
uniform float u_time;
float rand(float co) { return fract(sin(co*(91.3458)) * 47453.5453); }
float rand(vec2 co){ return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453); }
void main(void) {
vec4 col = texture2D (bitmap, openfl_TextureCoordv);
vec2 game_res = vec2(1280.0, 720.0);
const float tileAmount = 10.;
vec2 uv = (2. * openfl_TextureCoordv.xy * -1.);
uv *= 50.;
vec4 hexCenter = floor(vec4(uv, uv - vec2(0.5, 1.0)) / s.xyxy) + 0.5;
vec4 offset = vec4(uv - hexCenter.xy * s, uv - (hexCenter.zw + 0.5) * s) + 0.0;
vec4 hexInfo = dot(offset.xy, offset.xy) < dot(offset.zw, offset.zw) ? vec4(offset.xy, hexCenter.xy) : vec4(offset.zw, hexCenter.zw);
// Distance to the nearest edge of a hexagon
vec2 p = abs(hexInfo.xy) ;
float edgeDist = max(dot(p, normalize(vec2(1.0, sqrt(3.0)))), p.x);
float edgeWidth = 0.05 * tileAmount; // Adjust edge width based on tile amount
float edgeSharpness = 0.011 * tileAmount;
float outline = smoothstep(edgeWidth - edgeSharpness, edgeWidth, edgeDist);
float color_mix = mix(0.0, 0.3, outline); // Mix black outline with white fill
float flicker = (sin(u_time) * 0.05) + 1.0;
float sinshit = smoothstep(-3.0, 1.0, sin(uv.y * 3.));
col = vec4(vec3(0.0), color_mix);
col = mix(col, vec4(0., 0., 0., sinshit), 0.5 * flicker);
float specs = rand(uv.xy);
vec4 noise = vec4(0., 0., 0., specs);
col = mix(col, noise, 0.1);
gl_FragColor = col;
}
')
public function new()
{
super();
this.elapsedTime = 0;
}
public function update(elapsed:Float):Void
{
this.elapsedTime += elapsed;
}
}

View file

@ -60,6 +60,11 @@ class Constants
*/
// ==============================
/**
* Link to buy merch for the game.
*/
public static final URL_MERCH:String = 'https://needlejuicerecords.com/pages/friday-night-funkin';
/**
* Preloader sitelock.
* Matching is done by `FlxStringUtil.getDomain`, so any URL on the domain will work.
@ -135,7 +140,7 @@ class Constants
/**
* Color for the preloader progress bar
*/
public static final COLOR_PRELOADER_BAR:FlxColor = 0xFF00FF00;
public static final COLOR_PRELOADER_BAR:FlxColor = 0xFFA4FF11;
/**
* Color for the preloader site lock background
@ -181,6 +186,12 @@ class Constants
*/
public static final DEFAULT_DIFFICULTY_LIST:Array<String> = ['easy', 'normal', 'hard'];
/**
* List of all difficulties used by the base game.
* Includes Erect and Nightmare.
*/
public static final DEFAULT_DIFFICULTY_LIST_FULL:Array<String> = ['easy', 'normal', 'hard', 'erect', 'nightmare'];
/**
* Default player character for charts.
*/
@ -212,9 +223,10 @@ class Constants
public static final DEFAULT_VARIATION_LIST:Array<String> = ['default', 'erect', 'pico'];
/**
* The default intensity for camera zooms.
* The default intensity multiplier for camera bops.
* Prolly needs to be tuned bc it's a multiplier now.
*/
public static final DEFAULT_ZOOM_INTENSITY:Float = 0.015;
public static final DEFAULT_BOP_INTENSITY:Float = 1.015;
/**
* The default rate for camera zooms (in beats per zoom).
@ -346,7 +358,7 @@ class Constants
* The progress bare is automatically rescaled to match.
*/
#if debug
public static final PRELOADER_MIN_STAGE_TIME:Float = 1.0;
public static final PRELOADER_MIN_STAGE_TIME:Float = 0.0;
#else
public static final PRELOADER_MIN_STAGE_TIME:Float = 0.1;
#end
@ -514,4 +526,10 @@ class Constants
* The vertical offset of the strumline from the top edge of the screen.
*/
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;
}

View file

@ -0,0 +1,17 @@
package funkin.util;
class EaseUtil
{
/**
* Returns an ease function that eases via steps.
* Useful for "retro" style fades (week 6!)
* @param steps how many steps to ease over
* @return Float->Float
*/
public static inline function stepped(steps:Int):Float->Float
{
return function(t:Float):Float {
return Math.floor(t * steps) / steps;
}
}
}

View file

@ -63,6 +63,31 @@ class SerializerUtil
}
}
public static function initSerializer():Void
{
haxe.Unserializer.DEFAULT_RESOLVER = new FunkinTypeResolver();
}
/**
* Serialize a Haxe object using the built-in Serializer.
* @param input The object to serialize
* @return The serialized object as a string
*/
public static function fromHaxeObject(input:Dynamic):String
{
return haxe.Serializer.run(input);
}
/**
* Convert a serialized Haxe object back into a Haxe object.
* @param input The serialized object as a string
* @return The deserialized object
*/
public static function toHaxeObject(input:String):Dynamic
{
return haxe.Unserializer.run(input);
}
/**
* Customize how certain types are serialized when converting to JSON.
*/
@ -90,3 +115,26 @@ class SerializerUtil
return result;
}
}
class FunkinTypeResolver
{
public function new()
{
// Blank constructor.
}
public function resolveClass(name:String):Class<Dynamic>
{
if (name == 'Dynamic')
{
FlxG.log.warn('Found invalid class type in save data, indicates partial save corruption.');
return null;
}
return Type.resolveClass(name);
};
public function resolveEnum(name:String):Enum<Dynamic>
{
return Type.resolveEnum(name);
};
}

View file

@ -0,0 +1,136 @@
package funkin.util;
import funkin.util.tools.MapTools;
import haxe.DynamicAccess;
/**
* Utilities for working with anonymous structures.
*/
class StructureUtil
{
/**
* Merge two structures, with the second overwriting the first.
* Performs a SHALLOW clone, where child structures are not merged.
* @param a The base structure.
* @param b The new structure.
* @return The merged structure.
*/
public static function merge(a:Dynamic, b:Dynamic):Dynamic
{
var result:DynamicAccess<Dynamic> = Reflect.copy(a);
for (field in Reflect.fields(b))
{
result.set(field, Reflect.field(b, field));
}
return result;
}
public static function toMap(a:Dynamic):haxe.ds.Map<String, Dynamic>
{
var result:haxe.ds.Map<String, Dynamic> = [];
for (field in Reflect.fields(a))
{
result.set(field, Reflect.field(a, field));
}
return result;
}
public static function isMap(a:Dynamic):Bool
{
return Std.isOfType(a, haxe.Constraints.IMap);
}
public static function isObject(a:Dynamic):Bool
{
switch (Type.typeof(a))
{
case TObject:
return true;
default:
return false;
}
}
public static function isPrimitive(a:Dynamic):Bool
{
switch (Type.typeof(a))
{
case TInt | TFloat | TBool:
return true;
case TClass(c):
return false;
case TEnum(e):
return false;
case TObject:
return false;
case TFunction:
return false;
case TNull:
return true;
case TUnknown:
return false;
default:
return false;
}
}
/**
* Merge two structures, with the second overwriting the first.
* Performs a DEEP clone, where child structures are also merged recursively.
* @param a The base structure.
* @param b The new structure.
* @return The merged structure.
*/
public static function deepMerge(a:Dynamic, b:Dynamic):Dynamic
{
if (a == null) return b;
if (b == null) return null;
if (isPrimitive(a) && isPrimitive(b)) return b;
if (isMap(b))
{
if (isMap(a))
{
return MapTools.merge(a, b);
}
else
{
return StructureUtil.toMap(a).merge(b);
}
}
if (!Reflect.isObject(a) || !Reflect.isObject(b)) return b;
if (Std.isOfType(b, haxe.ds.StringMap))
{
if (Std.isOfType(a, haxe.ds.StringMap))
{
return MapTools.merge(a, b);
}
else
{
return StructureUtil.toMap(a).merge(b);
}
}
var result:DynamicAccess<Dynamic> = Reflect.copy(a);
for (field in Reflect.fields(b))
{
if (Reflect.isObject(b))
{
// Note that isObject also returns true for class instances,
// but we just assume that's not a problem here.
result.set(field, deepMerge(Reflect.field(result, field), Reflect.field(b, field)));
}
else
{
// If we're here, b[field] is a primitive.
result.set(field, Reflect.field(b, field));
}
}
return result;
}
}

View file

@ -33,6 +33,24 @@ class MapTools
return map.copy();
}
/**
* Create a new map which is a combination of the two given maps.
* @param a The base map.
* @param b The other map. The values from this take precedence.
* @return The combined map.
*/
public static function merge<K, T>(a:Map<K, T>, b:Map<K, T>):Map<K, T>
{
var result = a.copy();
for (pair in b.keyValueIterator())
{
result.set(pair.key, pair.value);
}
return result;
}
/**
* Create a new array with clones of all elements of the given array, to prevent modifying the original.
*/