diff --git a/CHANGELOG.md b/CHANGELOG.md index 53e981284..22a36fa8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,14 +4,28 @@ All notable changes will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.5.0] - 2024-08-?? +## [0.5.0] - 2024-09-12 ### Added - Added a new Character Select screen to switch between playable characters in Freeplay - Modding isn't 100% there but we're working on it! - Added Pico as a playable character! Unlock him by completing Weekend 1 (if you haven't already done that) - The songs from Weekend 1 have moved; you must now switch to Pico in Freeplay to access them -- Added ## new Pico remixes! Access them by selecting Pico from in the Character Select screen +- Added 10 new Pico remixes! Access them by selecting Pico from in the Character Select screen + - Bopeebo (Pico Mix) + - Fresh (Pico Mix) + - DadBattle (Pico Mix) + - Spookeez (Pico Mix) + - South (Pico Mix) + - Philly Nice (Pico Mix) + - Blammed (Pico Mix) + - Eggnog (Pico Mix) + - Ugh (Pico Mix) + - Guns (Pico Mix) +- Added 1 new Boyfriend remix! Access it by selecting Pico from in the Character Select screen + - Darnell (BF Mix) - Added 2 new Erect remixes! Access them by switching difficulty on the song + - Cocoa Erect + - Ugh Erect - Implemented support for a new Instrumental Selector in Freeplay - Beating a Pico remix lets you use that instrumental when playing as Boyfriend - Added the first batch of Erect Stages! These graphical overhauls of the original stages will be used when playing Erect remixes and Pico remixes diff --git a/assets b/assets index e80d92ac4..5e5e01fbe 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit e80d92ac484fe432ab34d45b22158f5d565afb0f +Subproject commit 5e5e01fbed1e2d5979f983e195b9c0dfbf852b91 diff --git a/source/funkin/Assets.hx b/source/funkin/Assets.hx new file mode 100644 index 000000000..5351676d4 --- /dev/null +++ b/source/funkin/Assets.hx @@ -0,0 +1,38 @@ +package funkin; + +/** + * A wrapper around `openfl.utils.Assets` which disallows access to the harmful functions. + * Later we'll add Funkin-specific caching to this. + */ +class Assets +{ + public static function getText(path:String):String + { + return openfl.utils.Assets.getText(path); + } + + public static function getMusic(path:String):openfl.media.Sound + { + return openfl.utils.Assets.getMusic(path); + } + + public static function getBitmapData(path:String):openfl.display.BitmapData + { + return openfl.utils.Assets.getBitmapData(path); + } + + public static function getBytes(path:String):haxe.io.Bytes + { + return openfl.utils.Assets.getBytes(path); + } + + public static function exists(path:String, ?type:openfl.utils.AssetType):Bool + { + return openfl.utils.Assets.exists(path, type); + } + + public static function list(type:openfl.utils.AssetType):Array + { + return openfl.utils.Assets.list(type); + } +} diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index 21f83022e..a203a8d77 100644 --- a/source/funkin/InitState.hx +++ b/source/funkin/InitState.hx @@ -19,6 +19,7 @@ import funkin.play.PlayStatePlaylist; import openfl.display.BitmapData; import funkin.data.story.level.LevelRegistry; import funkin.data.notestyle.NoteStyleRegistry; +import funkin.data.freeplay.style.FreeplayStyleRegistry; import funkin.data.event.SongEventRegistry; import funkin.data.stage.StageRegistry; import funkin.data.dialogue.conversation.ConversationRegistry; @@ -170,6 +171,7 @@ class InitState extends FlxState ConversationRegistry.instance.loadEntries(); DialogueBoxRegistry.instance.loadEntries(); SpeakerRegistry.instance.loadEntries(); + FreeplayStyleRegistry.instance.loadEntries(); AlbumRegistry.instance.loadEntries(); StageRegistry.instance.loadEntries(); diff --git a/source/funkin/data/freeplay/player/PlayerData.hx b/source/funkin/data/freeplay/player/PlayerData.hx index 55657ba46..de293c24e 100644 --- a/source/funkin/data/freeplay/player/PlayerData.hx +++ b/source/funkin/data/freeplay/player/PlayerData.hx @@ -31,6 +31,13 @@ class PlayerData @:default(false) public var showUnownedChars:Bool = false; + /** + * Which freeplay style to use for this character. + */ + @:optional + @:default("bf") + public var freeplayStyle:String = Constants.DEFAULT_FREEPLAY_STYLE; + /** * Data for displaying this character in the Freeplay menu. * If null, display no DJ. @@ -105,6 +112,9 @@ class PlayerFreeplayDJData @:jignored var prefixToOffsetsMap:Map>; + @:optional + var charSelect:Null; + @:optional var cartoon:Null; @@ -237,6 +247,11 @@ class PlayerFreeplayDJData { return fistPump?.loopBadEndFrame ?? 0; } + + public function getCharSelectTransitionDelay():Float + { + return charSelect?.transitionDelay ?? 0.25; + } } class PlayerCharSelectData @@ -253,6 +268,8 @@ class PlayerCharSelectData typedef PlayerResultsData = { + var music:PlayerResultsMusicData; + var perfect:Array; var excellent:Array; var great:Array; @@ -260,6 +277,27 @@ typedef PlayerResultsData = var loss:Array; }; +typedef PlayerResultsMusicData = +{ + @:optional + var PERFECT_GOLD:String; + + @:optional + var PERFECT:String; + + @:optional + var EXCELLENT:String; + + @:optional + var GREAT:String; + + @:optional + var GOOD:String; + + @:optional + var SHIT:String; +} + typedef PlayerResultsAnimationData = { /** @@ -300,6 +338,11 @@ typedef PlayerResultsAnimationData = var loopFrameLabel:Null; }; +typedef PlayerFreeplayDJCharSelectData = +{ + var transitionDelay:Float; +} + typedef PlayerFreeplayDJCartoonData = { var soundClickFrame:Int; diff --git a/source/funkin/data/freeplay/style/CHANGELOG.md b/source/funkin/data/freeplay/style/CHANGELOG.md new file mode 100644 index 000000000..8fe9f7eb8 --- /dev/null +++ b/source/funkin/data/freeplay/style/CHANGELOG.md @@ -0,0 +1,9 @@ +# Freeplay Style Data Schema Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] +Initial release. diff --git a/source/funkin/data/freeplay/style/FreeplayStyleData.hx b/source/funkin/data/freeplay/style/FreeplayStyleData.hx new file mode 100644 index 000000000..1af198217 --- /dev/null +++ b/source/funkin/data/freeplay/style/FreeplayStyleData.hx @@ -0,0 +1,48 @@ +package funkin.data.freeplay.style; + +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. + * @see https://lib.haxe.org/p/json2object/ + */ +typedef FreeplayStyleData = +{ + /** + * Semantic version for style data. + */ + public var version:String; + + /** + * Asset key for the background image. + */ + public var bgAsset:String; + + /** + * Asset key for the difficulty selector image. + */ + public var selectorAsset:String; + + /** + * Asset key for the numbers shown at the top right of the screen. + */ + public var numbersAsset:String; + + /** + * Asset key for the freeplay capsules. + */ + public var capsuleAsset:String; + + /** + * Color data for the capsule text outline. + * the order of this array goes as follows: [DESELECTED, SELECTED] + */ + public var capsuleTextColors:Array; + + /** + * Delay time after confirming a song selection, before entering PlayState. + * Useful for letting longer animations play out. + */ + public var startDelay:Float; +} diff --git a/source/funkin/data/freeplay/style/FreeplayStyleRegistry.hx b/source/funkin/data/freeplay/style/FreeplayStyleRegistry.hx new file mode 100644 index 000000000..626e2ac39 --- /dev/null +++ b/source/funkin/data/freeplay/style/FreeplayStyleRegistry.hx @@ -0,0 +1,84 @@ +package funkin.data.freeplay.style; + +import funkin.ui.freeplay.FreeplayStyle; +import funkin.data.freeplay.style.FreeplayStyleData; +import funkin.ui.freeplay.ScriptedFreeplayStyle; + +class FreeplayStyleRegistry extends BaseRegistry +{ + /** + * The current version string for the style data format. + * Handle breaking changes by incrementing this value + * and adding migration to the `migrateStyleData()` function. + */ + public static final FREEPLAYSTYLE_DATA_VERSION:thx.semver.Version = '1.0.0'; + + public static final FREEPLAYSTYLE_DATA_VERSION_RULE:thx.semver.VersionRule = '1.0.x'; + + public static final instance:FreeplayStyleRegistry = new FreeplayStyleRegistry(); + + public function new() + { + super('FREEPLAYSTYLE', 'ui/freeplay/styles', FREEPLAYSTYLE_DATA_VERSION_RULE); + } + + /** + * Read, parse, and validate the JSON data and produce the corresponding data object. + * @param id The ID of the entry to load. + * @return The parsed data object. + */ + public function parseEntryData(id:String):Null + { + // JsonParser does not take type parameters, + // otherwise this function would be in BaseRegistry. + var parser:json2object.JsonParser = new json2object.JsonParser(); + parser.ignoreUnknownVariables = false; + + switch (loadEntryFile(id)) + { + case {fileName: fileName, contents: contents}: + parser.fromJson(contents, fileName); + default: + return null; + } + + if (parser.errors.length > 0) + { + printErrors(parser.errors, id); + return null; + } + return parser.value; + } + + /** + * Parse and validate the JSON data and produce the corresponding data object. + * + * NOTE: Must be implemented on the implementation class. + * @param contents The JSON as a string. + * @param fileName An optional file name for error reporting. + * @return The parsed data object. + */ + public function parseEntryDataRaw(contents:String, ?fileName:String):Null + { + var parser:json2object.JsonParser = new json2object.JsonParser(); + parser.ignoreUnknownVariables = false; + parser.fromJson(contents, fileName); + + if (parser.errors.length > 0) + { + printErrors(parser.errors, fileName); + return null; + } + return parser.value; + } + + function createScriptedEntry(clsName:String):FreeplayStyle + { + return ScriptedFreeplayStyle.init(clsName, 'unknown'); + } + + function getScriptedClassNames():Array + { + return ScriptedFreeplayStyle.listScriptClasses(); + } +} diff --git a/source/funkin/data/song/CHANGELOG.md b/source/funkin/data/song/CHANGELOG.md index ca36a1d6d..86c9f6912 100644 --- a/source/funkin/data/song/CHANGELOG.md +++ b/source/funkin/data/song/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - If the value isn't present, it will use the `playData.characters.opponent`, but if it is present, it will be used (even if it's empty, in which case no vocals will be used for the opponent) - Added `playData.characters.playerVocals` to specify which vocal track(s) to play for the player. - If the value isn't present, it will use the `playData.characters.player`, but if it is present, it will be used (even if it's empty, in which case no vocals will be used for the player) +- Added `offsets.altVocals` field to apply vocal offsets when alternate instrumentals are used. + ## [2.2.3] ### Added diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx index f487eb54d..074ed0b44 100644 --- a/source/funkin/data/song/SongData.hx +++ b/source/funkin/data/song/SongData.hx @@ -257,18 +257,27 @@ class SongOffsets implements ICloneable public var altInstrumentals:Map; /** - * The offset, in milliseconds, to apply to the song's vocals, relative to the chart. + * The offset, in milliseconds, to apply to the song's vocals, relative to the song's base instrumental. * These are applied ON TOP OF the instrumental offset. */ @:optional @:default([]) public var vocals:Map; - public function new(instrumental:Float = 0.0, ?altInstrumentals:Map, ?vocals:Map) + /** + * The offset, in milliseconds, to apply to the songs vocals, relative to each alternate instrumental. + * This is useful for the circumstance where, for example, an alt instrumental has a few seconds of lead in before the song starts. + */ + @:optional + @:default([]) + public var altVocals:Map>; + + public function new(instrumental:Float = 0.0, ?altInstrumentals:Map, ?vocals:Map, ?altVocals:Map>) { this.instrumental = instrumental; this.altInstrumentals = altInstrumentals == null ? new Map() : altInstrumentals; this.vocals = vocals == null ? new Map() : vocals; + this.altVocals = altVocals == null ? new Map>() : altVocals; } public function getInstrumentalOffset(?instrumental:String):Float @@ -293,11 +302,19 @@ class SongOffsets implements ICloneable return value; } - public function getVocalOffset(charId:String):Float + public function getVocalOffset(charId:String, ?instrumental:String):Float { - if (!this.vocals.exists(charId)) return 0.0; - - return this.vocals.get(charId); + if (instrumental == null) + { + if (!this.vocals.exists(charId)) return 0.0; + return this.vocals.get(charId); + } + else + { + if (!this.altVocals.exists(instrumental)) return 0.0; + if (!this.altVocals.get(instrumental).exists(charId)) return 0.0; + return this.altVocals.get(instrumental).get(charId); + } } public function setVocalOffset(charId:String, value:Float):Float @@ -320,7 +337,7 @@ class SongOffsets implements ICloneable */ public function toString():String { - return 'SongOffsets(${this.instrumental}ms, ${this.altInstrumentals}, ${this.vocals})'; + return 'SongOffsets(${this.instrumental}ms, ${this.altInstrumentals}, ${this.vocals}, ${this.altVocals})'; } } diff --git a/source/funkin/data/song/SongRegistry.hx b/source/funkin/data/song/SongRegistry.hx index a3305c4ec..e7cab246c 100644 --- a/source/funkin/data/song/SongRegistry.hx +++ b/source/funkin/data/song/SongRegistry.hx @@ -20,7 +20,7 @@ class SongRegistry extends BaseRegistry * Handle breaking changes by incrementing this value * and adding migration to the `migrateStageData()` function. */ - public static final SONG_METADATA_VERSION:thx.semver.Version = "2.2.3"; + public static final SONG_METADATA_VERSION:thx.semver.Version = "2.2.4"; public static final SONG_METADATA_VERSION_RULE:thx.semver.VersionRule = "2.2.x"; diff --git a/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx b/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx index b32914ca3..c4eaaff50 100644 --- a/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx +++ b/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx @@ -105,23 +105,6 @@ class FlxAtlasSprite extends FlxAnimate return this.currentAnimation; } - /** - * `anim.finished` always returns false on looping animations, - * but this function will return true if we are on the last frame of the looping animation. - */ - public function isLoopFinished():Bool - { - if (this.anim == null) return false; - if (!this.anim.isPlaying) return false; - - // Reverse animation finished. - if (this.anim.reversed && this.anim.curFrame == 0) return true; - // Forward animation finished. - if (!this.anim.reversed && this.anim.curFrame >= (this.anim.length - 1)) return true; - - return false; - } - var _completeAnim:Bool = false; var fr:FlxKeyFrame = null; @@ -142,6 +125,8 @@ class FlxAtlasSprite extends FlxAnimate // Skip if not allowed to play animations. if ((!canPlayOtherAnims && !ignoreOther)) return; + if (anim == null) return; + if (id == null || id == '') id = this.currentAnimation; if (this.currentAnimation == id && !restart) @@ -189,10 +174,16 @@ class FlxAtlasSprite extends FlxAnimate // Move to the first frame of the animation. // goToFrameLabel(id); trace('Playing animation $id'); - this.anim.play(id, restart, false, startFrame); - goToFrameLabel(id); - - fr = anim.getFrameLabel(id); + if (this.anim.symbolDictionary.exists(id) || (this.anim.getByName(id) != null)) + { + this.anim.play(id, restart, false, startFrame); + } + // Only call goToFrameLabel if there is a frame label with that name. This prevents annoying warnings! + if (getFrameLabelNames().indexOf(id) != -1) + { + goToFrameLabel(id); + fr = anim.getFrameLabel(id); + } anim.curFrame += startFrame; this.currentAnimation = id; @@ -218,6 +209,8 @@ class FlxAtlasSprite extends FlxAnimate */ public function isLoopComplete():Bool { + if (this.anim == null) return false; + if (!this.anim.isPlaying) return false; return (anim.reversed && anim.curFrame == 0 || !(anim.reversed) && (anim.curFrame) >= (anim.length - 1)); } @@ -244,6 +237,18 @@ class FlxAtlasSprite extends FlxAnimate this.anim.goToFrameLabel(label); } + function getFrameLabelNames(?layer:haxe.extern.EitherType = null) + { + var labels = this.anim.getFrameLabels(layer); + var array = []; + for (label in labels) + { + array.push(label.name); + } + + return array; + } + function getNextFrameLabel(label:String):String { return listAnimations()[(getLabelIndex(label) + 1) % listAnimations().length]; @@ -272,7 +277,7 @@ class FlxAtlasSprite extends FlxAnimate { onAnimationFrame.dispatch(currentAnimation, frame); - if (fr != null && frame > (fr.index + fr.duration - 1) || isLoopFinished()) + if (fr != null && frame > (fr.index + fr.duration - 1) || isLoopComplete()) { anim.pause(); _onAnimationComplete(); diff --git a/source/funkin/graphics/shaders/AngleMask.hx b/source/funkin/graphics/shaders/AngleMask.hx index c5ef87b72..ce27311cd 100644 --- a/source/funkin/graphics/shaders/AngleMask.hx +++ b/source/funkin/graphics/shaders/AngleMask.hx @@ -1,12 +1,25 @@ package funkin.graphics.shaders; import flixel.system.FlxAssets.FlxShader; +import flixel.util.FlxColor; class AngleMask extends FlxShader { + public var extraColor(default, set):FlxColor = 0xFFFFFFFF; + + function set_extraColor(value:FlxColor):FlxColor + { + extraTint.value = [value.redFloat, value.greenFloat, value.blueFloat]; + this.extraColor = value; + + return this.extraColor; + } + @:glFragmentSource(' #pragma header + uniform vec3 extraTint; + uniform vec2 endPosition; vec2 hash22(vec2 p) { vec3 p3 = fract(vec3(p.xyx) * vec3(.1031, .1030, .0973)); @@ -69,6 +82,7 @@ class AngleMask extends FlxShader void main() { vec4 col = antialias(openfl_TextureCoordv); + col.xyz = col.xyz * extraTint.xyz; // col.xyz = gamma(col.xyz); gl_FragColor = col; }') @@ -77,5 +91,6 @@ class AngleMask extends FlxShader super(); endPosition.value = [90, 100]; // 100 AS DEFAULT WORKS NICELY FOR FREEPLAY? + extraTint.value = [1, 1, 1]; } } diff --git a/source/funkin/graphics/shaders/BlueFade.hx b/source/funkin/graphics/shaders/BlueFade.hx new file mode 100644 index 000000000..f57bcfbf1 --- /dev/null +++ b/source/funkin/graphics/shaders/BlueFade.hx @@ -0,0 +1,51 @@ +package funkin.graphics.shaders; + +import flixel.system.FlxAssets.FlxShader; +import flixel.tweens.FlxEase; +import flixel.tweens.FlxTween; + +class BlueFade extends FlxShader +{ + public var fadeVal(default, set):Float; + + function set_fadeVal(val:Float):Float + { + fadeAmt.value = [val]; + fadeVal = val; + // trace(fadeVal); + + return val; + } + + public function fade(startAmt:Float = 0, targetAmt:Float = 1, duration:Float, _options:TweenOptions):Void + { + fadeVal = startAmt; + FlxTween.tween(this, {fadeVal: targetAmt}, duration, _options); + } + + @:glFragmentSource(' + #pragma header + + // Value from (0, 1) + uniform float fadeAmt; + + // fade the image to blue as it fades to black + + void main() + { + vec4 tex = flixel_texture2D(bitmap, openfl_TextureCoordv); + + vec4 finalColor = mix(vec4(vec4(0.0, 0.0, tex.b, tex.a) * fadeAmt), vec4(tex * fadeAmt), fadeAmt); + + // Output to screen + gl_FragColor = finalColor; + } + + ') + public function new() + { + super(); + + this.fadeVal = 1; + } +} diff --git a/source/funkin/modding/IScriptedClass.hx b/source/funkin/modding/IScriptedClass.hx index 5f2ff2b9e..14aa6b494 100644 --- a/source/funkin/modding/IScriptedClass.hx +++ b/source/funkin/modding/IScriptedClass.hx @@ -73,6 +73,22 @@ interface INoteScriptedClass extends IScriptedClass public function onNoteMiss(event:NoteScriptEvent):Void; } +/** + * Defines a set of callbacks available to scripted classes which represent sprites synced with the BPM. + */ +interface IBPMSyncedScriptedClass extends IScriptedClass +{ + /** + * Called once every step of the song. + */ + public function onStepHit(event:SongTimeScriptEvent):Void; + + /** + * Called once every beat of the song. + */ + public function onBeatHit(event:SongTimeScriptEvent):Void; +} + /** * Developer note: * @@ -86,7 +102,7 @@ interface INoteScriptedClass extends IScriptedClass /** * Defines a set of callbacks available to scripted classes that involve the lifecycle of the Play State. */ -interface IPlayStateScriptedClass extends INoteScriptedClass +interface IPlayStateScriptedClass extends INoteScriptedClass extends IBPMSyncedScriptedClass { /** * Called when the game is paused. @@ -136,16 +152,6 @@ interface IPlayStateScriptedClass extends INoteScriptedClass */ public function onSongEvent(event:SongEventScriptEvent):Void; - /** - * Called once every step of the song. - */ - public function onStepHit(event:SongTimeScriptEvent):Void; - - /** - * Called once every beat of the song. - */ - public function onBeatHit(event:SongTimeScriptEvent):Void; - /** * Called when the countdown of the song starts. */ diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx index 52d4624cb..75c69e506 100644 --- a/source/funkin/modding/PolymodHandler.hx +++ b/source/funkin/modding/PolymodHandler.hx @@ -235,6 +235,10 @@ class PolymodHandler Polymod.addImportAlias('funkin.data.event.SongEventSchema', funkin.data.event.SongEventSchema.SongEventSchemaRaw); + // `lime.utils.Assets` literally just has a private `resolveClass` function for some reason? so we replace it with our own. + Polymod.addImportAlias('lime.utils.Assets', funkin.Assets); + Polymod.addImportAlias('openfl.utils.Assets', funkin.Assets); + // Add blacklisting for prohibited classes and packages. // `Sys` @@ -269,11 +273,6 @@ class PolymodHandler // System.load() can load malicious DLLs Polymod.blacklistImport('lime.system.System'); - // `lime.utils.Assets` - // Literally just has a private `resolveClass` function for some reason? - Polymod.blacklistImport('lime.utils.Assets'); - Polymod.blacklistImport('openfl.utils.Assets'); - // `openfl.desktop.NativeProcess` // Can load native processes on the host operating system. Polymod.blacklistImport('openfl.desktop.NativeProcess'); diff --git a/source/funkin/modding/events/ScriptEventDispatcher.hx b/source/funkin/modding/events/ScriptEventDispatcher.hx index c262c311d..7e19173c4 100644 --- a/source/funkin/modding/events/ScriptEventDispatcher.hx +++ b/source/funkin/modding/events/ScriptEventDispatcher.hx @@ -94,6 +94,21 @@ class ScriptEventDispatcher } } + if (Std.isOfType(target, IBPMSyncedScriptedClass)) + { + var t:IBPMSyncedScriptedClass = cast(target, IBPMSyncedScriptedClass); + switch (event.type) + { + case SONG_BEAT_HIT: + t.onBeatHit(cast event); + return; + case SONG_STEP_HIT: + t.onStepHit(cast event); + return; + default: // Continue; + } + } + if (Std.isOfType(target, IPlayStateScriptedClass)) { var t:IPlayStateScriptedClass = cast(target, IPlayStateScriptedClass); @@ -102,12 +117,6 @@ class ScriptEventDispatcher case NOTE_GHOST_MISS: t.onNoteGhostMiss(cast event); return; - case SONG_BEAT_HIT: - t.onBeatHit(cast event); - return; - case SONG_STEP_HIT: - t.onStepHit(cast event); - return; case SONG_START: t.onSongStart(event); return; diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index f4b177763..414c833ff 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -668,7 +668,7 @@ class PlayState extends MusicBeatSubState // Prepare the current song's instrumental and vocals to be played. if (!overrideMusic && currentChart != null) { - currentChart.cacheInst(); + currentChart.cacheInst(currentInstrumental); currentChart.cacheVocals(); } @@ -677,7 +677,7 @@ class PlayState extends MusicBeatSubState if (currentChart.offsets != null) { - Conductor.instance.instrumentalOffset = currentChart.offsets.getInstrumentalOffset(); + Conductor.instance.instrumentalOffset = currentChart.offsets.getInstrumentalOffset(currentInstrumental); } Conductor.instance.mapTimeChanges(currentChart.timeChanges); @@ -860,7 +860,7 @@ class PlayState extends MusicBeatSubState { // Stop the vocals if they already exist. if (vocals != null) vocals.stop(); - vocals = currentChart.buildVocals(); + vocals = currentChart.buildVocals(currentInstrumental); if (vocals.members.length == 0) { @@ -1791,7 +1791,7 @@ class PlayState extends MusicBeatSubState { // Stop the vocals if they already exist. if (vocals != null) vocals.stop(); - vocals = currentChart.buildVocals(); + vocals = currentChart.buildVocals(currentInstrumental); if (vocals.members.length == 0) { diff --git a/source/funkin/play/ResultState.hx b/source/funkin/play/ResultState.hx index b1ff69a3a..ec6069ad4 100644 --- a/source/funkin/play/ResultState.hx +++ b/source/funkin/play/ResultState.hx @@ -404,12 +404,12 @@ class ResultState extends MusicBeatSubState // } new FlxTimer().start(rank.getMusicDelay(), _ -> { - if (rank.hasMusicIntro()) + var introMusic:String = Paths.music(getMusicPath(playerCharacter, rank) + '/' + getMusicPath(playerCharacter, rank) + '-intro'); + if (Assets.exists(introMusic)) { // Play the intro music. - var introMusic:String = Paths.music(rank.getMusicPath() + '/' + rank.getMusicPath() + '-intro'); FunkinSound.load(introMusic, 1.0, false, true, true, () -> { - FunkinSound.playMusic(rank.getMusicPath(), + FunkinSound.playMusic(getMusicPath(playerCharacter, rank), { startingVolume: 1.0, overrideExisting: true, @@ -420,7 +420,7 @@ class ResultState extends MusicBeatSubState } else { - FunkinSound.playMusic(rank.getMusicPath(), + FunkinSound.playMusic(getMusicPath(playerCharacter, rank), { startingVolume: 1.0, overrideExisting: true, @@ -441,6 +441,11 @@ class ResultState extends MusicBeatSubState super.create(); } + function getMusicPath(playerCharacter:Null, rank:ScoringRank):String + { + return playerCharacter?.getResultsMusicPath(rank) ?? 'resultsNORMAL'; + } + var rankTallyTimer:Null = null; var clearPercentTarget:Int = 100; var clearPercentLerp:Int = 0; diff --git a/source/funkin/play/scoring/Scoring.hx b/source/funkin/play/scoring/Scoring.hx index 02e5750bc..6c9f9bd97 100644 --- a/source/funkin/play/scoring/Scoring.hx +++ b/source/funkin/play/scoring/Scoring.hx @@ -556,40 +556,6 @@ enum abstract ScoringRank(String) } } - public function getMusicPath():String - { - switch (abstract) - { - case PERFECT_GOLD: - return 'resultsPERFECT'; - case PERFECT: - return 'resultsPERFECT'; - case EXCELLENT: - return 'resultsEXCELLENT'; - case GREAT: - return 'resultsNORMAL'; - case GOOD: - return 'resultsNORMAL'; - case SHIT: - return 'resultsSHIT'; - default: - return 'resultsNORMAL'; - } - } - - public function hasMusicIntro():Bool - { - switch (abstract) - { - case EXCELLENT: - return true; - case SHIT: - return true; - default: - return false; - } - } - public function getFreeplayRankIconAsset():String { switch (abstract) diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index 841600848..9d35902b0 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -533,6 +533,28 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry + { + var targetDifficulty:Null = getDifficulty(difficultyId, variationId); + if (targetDifficulty == null) return []; + + return targetDifficulty?.characters?.altInstrumentals ?? []; + } + + public function getBaseInstrumentalId(difficultyId:String, variationId:String):String + { + var targetDifficulty:Null = getDifficulty(difficultyId, variationId); + if (targetDifficulty == null) return ''; + + return targetDifficulty?.characters?.instrumental ?? ''; + } + /** * Purge the cached chart data for each difficulty of this song. */ @@ -851,7 +873,7 @@ class SongDifficulty * @param charId The player ID. * @return The generated vocal group. */ - public function buildVocals():VoicesGroup + public function buildVocals(?instId:String = ''):VoicesGroup { var result:VoicesGroup = new VoicesGroup(); @@ -870,8 +892,8 @@ class SongDifficulty result.addOpponentVoice(FunkinSound.load(opponentVoice)); } - result.playerVoicesOffset = offsets.getVocalOffset(characters.player); - result.opponentVoicesOffset = offsets.getVocalOffset(characters.opponent); + result.playerVoicesOffset = offsets.getVocalOffset(characters.player, instId); + result.opponentVoicesOffset = offsets.getVocalOffset(characters.opponent, instId); return result; } diff --git a/source/funkin/ui/charSelect/CharSelectGF.hx b/source/funkin/ui/charSelect/CharSelectGF.hx index 2e0e3980c..ea7d41448 100644 --- a/source/funkin/ui/charSelect/CharSelectGF.hx +++ b/source/funkin/ui/charSelect/CharSelectGF.hx @@ -7,10 +7,12 @@ import flixel.math.FlxMath; import funkin.util.FramesJSFLParser; import funkin.util.FramesJSFLParser.FramesJSFLInfo; import funkin.util.FramesJSFLParser.FramesJSFLFrame; +import funkin.modding.IScriptedClass.IBPMSyncedScriptedClass; import flixel.math.FlxMath; +import funkin.modding.events.ScriptEvent; import funkin.vis.dsp.SpectralAnalyzer; -class CharSelectGF extends FlxAtlasSprite +class CharSelectGF extends FlxAtlasSprite implements IBPMSyncedScriptedClass { var fadeTimer:Float = 0; var fadingStatus:FadeStatus = OFF; @@ -54,6 +56,7 @@ class CharSelectGF extends FlxAtlasSprite default: } + #if FEATURE_DEBUG_FUNCTIONS if (FlxG.keys.justPressed.J) { alpha = 1; @@ -65,8 +68,27 @@ class CharSelectGF extends FlxAtlasSprite alpha = 0; fadingStatus = FADE_IN; } + #end } + public function onStepHit(event:SongTimeScriptEvent):Void {} + + var danceEvery:Int = 2; + + public function onBeatHit(event:SongTimeScriptEvent):Void + { + // TODO: There's a minor visual bug where there's a little stutter. + // This happens because the animation is getting restarted while it's already playing. + // I tried make this not interrupt an existing idle, + // but isAnimationFinished() and isLoopComplete() both don't work! What the hell? + // danceEvery isn't necessary if that gets fixed. + if (getCurrentAnimation() == "idle" && (event.beat % danceEvery == 0)) + { + trace('GF beat hit'); + playAnimation("idle", true, false, false); + } + }; + override public function draw() { if (analyzer != null) drawFFT(); @@ -160,18 +182,27 @@ class CharSelectGF extends FlxAtlasSprite } // We don't need to update any anims if we didn't change GF - if (prevGF == curGF) return; + if (prevGF != curGF) + { + loadAtlas(Paths.animateAtlas("charSelect/" + curGF + "Chill")); - loadAtlas(Paths.animateAtlas("charSelect/" + curGF + "Chill")); + animInInfo = FramesJSFLParser.parse(Paths.file("images/charSelect/" + curGF + "AnimInfo/" + curGF + "In.txt")); + animOutInfo = FramesJSFLParser.parse(Paths.file("images/charSelect/" + curGF + "AnimInfo/" + curGF + "Out.txt")); + } - animInInfo = FramesJSFLParser.parse(Paths.file("images/charSelect/" + curGF + "AnimInfo/" + curGF + "In.txt")); - animOutInfo = FramesJSFLParser.parse(Paths.file("images/charSelect/" + curGF + "AnimInfo/" + curGF + "Out.txt")); - - playAnimation("idle", true, false, true); + playAnimation("idle", true, false, false); // addFrameCallback(getNextFrameLabel("idle"), () -> playAnimation("idle", true, false, false)); updateHitbox(); } + + public function onScriptEvent(event:ScriptEvent):Void {}; + + public function onCreate(event:ScriptEvent):Void {}; + + public function onDestroy(event:ScriptEvent):Void {}; + + public function onUpdate(event:UpdateScriptEvent):Void {}; } enum FadeStatus diff --git a/source/funkin/ui/charSelect/CharSelectPlayer.hx b/source/funkin/ui/charSelect/CharSelectPlayer.hx index 07c82d038..8e94e25fe 100644 --- a/source/funkin/ui/charSelect/CharSelectPlayer.hx +++ b/source/funkin/ui/charSelect/CharSelectPlayer.hx @@ -3,8 +3,10 @@ package funkin.ui.charSelect; import flixel.FlxSprite; import funkin.graphics.adobeanimate.FlxAtlasSprite; import flxanimate.animate.FlxKeyFrame; +import funkin.modding.IScriptedClass.IBPMSyncedScriptedClass; +import funkin.modding.events.ScriptEvent; -class CharSelectPlayer extends FlxAtlasSprite +class CharSelectPlayer extends FlxAtlasSprite implements IBPMSyncedScriptedClass { var desLp:FlxKeyFrame = null; @@ -18,11 +20,20 @@ class CharSelectPlayer extends FlxAtlasSprite switch (animLabel) { case "slidein": - if (hasAnimation("slidein idle point")) playAnimation("slidein idle point", true, false, false); + if (hasAnimation("slidein idle point")) + { + playAnimation("slidein idle point", true, false, false); + } else - playAnimation("idle", true, false, true); + { + // Handled by onBeatHit now + playAnimation("idle", true, false, false); + } case "slidein idle point": - playAnimation("idle", true, false, true); + // Handled by onBeatHit now + playAnimation("idle", true, false, false); + case "idle": + trace('Waiting for onBeatHit'); } }); @@ -31,6 +42,22 @@ class CharSelectPlayer extends FlxAtlasSprite }); } + public function onStepHit(event:SongTimeScriptEvent):Void {} + + public function onBeatHit(event:SongTimeScriptEvent):Void + { + // TODO: There's a minor visual bug where there's a little stutter. + // This happens because the animation is getting restarted while it's already playing. + // I tried make this not interrupt an existing idle, + // but isAnimationFinished() and isLoopComplete() both don't work! What the hell? + // danceEvery isn't necessary if that gets fixed. + if (getCurrentAnimation() == "idle") + { + trace('Player beat hit'); + playAnimation("idle", true, false, false); + } + }; + public function updatePosition(str:String) { switch (str) @@ -61,4 +88,12 @@ class CharSelectPlayer extends FlxAtlasSprite updatePosition(str); } + + public function onScriptEvent(event:ScriptEvent):Void {}; + + public function onCreate(event:ScriptEvent):Void {}; + + public function onDestroy(event:ScriptEvent):Void {}; + + public function onUpdate(event:UpdateScriptEvent):Void {}; } diff --git a/source/funkin/ui/charSelect/CharSelectSubState.hx b/source/funkin/ui/charSelect/CharSelectSubState.hx index c2850916a..60b931410 100644 --- a/source/funkin/ui/charSelect/CharSelectSubState.hx +++ b/source/funkin/ui/charSelect/CharSelectSubState.hx @@ -26,6 +26,12 @@ import funkin.ui.PixelatedIcon; import funkin.util.MathUtil; import funkin.vis.dsp.SpectralAnalyzer; import openfl.display.BlendMode; +import flixel.util.FlxTimer; +import flixel.tweens.FlxEase; +import flixel.sound.FlxSound; +import funkin.audio.FunkinSound; +import funkin.graphics.shaders.BlueFade; +import openfl.filters.ShaderFilter; class CharSelectSubState extends MusicBeatSubState { @@ -57,9 +63,16 @@ class CharSelectSubState extends MusicBeatSubState var gfChill:CharSelectGF; var gfChillOut:CharSelectGF; + var barthing:FlxAtlasSprite; + var dipshitBacking:FlxSprite; + var chooseDipshit:FlxSprite; + var dipshitBlur:FlxSprite; + var transitionGradient:FlxSprite; + var curChar(default, set):String = "pico"; var nametag:Nametag; var camFollow:FlxObject; + var autoFollow:Bool = false; var availableChars:Map = new Map(); var pressedSelect:Bool = false; @@ -100,10 +113,17 @@ class CharSelectSubState extends MusicBeatSubState } } + var fadeShader:BlueFade = new BlueFade(); + override public function create():Void { super.create(); + autoFollow = false; + + var fadeShaderFilter:ShaderFilter = new ShaderFilter(fadeShader); + FlxG.camera.filters = [fadeShaderFilter]; + selectSound = new FunkinSound(); selectSound.loadEmbedded(Paths.sound('CS_select')); selectSound.pitch = 1; @@ -148,7 +168,7 @@ class CharSelectSubState extends MusicBeatSubState var stageSpr:FlxSprite = new FlxSprite(-40, 391); stageSpr.frames = Paths.getSparrowAtlas("charSelect/charSelectStage"); - stageSpr.animation.addByPrefix("idle", "stage", 24, true); + stageSpr.animation.addByPrefix("idle", "stage full instance 1", 24, true); stageSpr.animation.play("idle"); add(stageSpr); @@ -157,12 +177,15 @@ class CharSelectSubState extends MusicBeatSubState curtains.scrollFactor.set(1.4, 1.4); add(curtains); - var barthing:FlxAtlasSprite = new FlxAtlasSprite(0, 0, Paths.animateAtlas("charSelect/barThing")); + barthing = new FlxAtlasSprite(0, 0, Paths.animateAtlas("charSelect/barThing")); barthing.anim.play(""); barthing.blend = BlendMode.MULTIPLY; barthing.scrollFactor.set(0, 0); add(barthing); + barthing.y += 80; + FlxTween.tween(barthing, {y: barthing.y - 80}, 1.3, {ease: FlxEase.expoOut}); + var charLight:FlxSprite = new FlxSprite(800, 250); charLight.loadGraphic(Paths.image('charSelect/charLight')); add(charLight); @@ -193,24 +216,33 @@ class CharSelectSubState extends MusicBeatSubState fgBlur.blend = openfl.display.BlendMode.MULTIPLY; add(fgBlur); - var dipshitBlur:FlxSprite = new FlxSprite(419, -65); + dipshitBlur = new FlxSprite(419, -65); dipshitBlur.frames = Paths.getSparrowAtlas("charSelect/dipshitBlur"); - dipshitBlur.animation.addByPrefix('idle', "CHOOSE vertical", 24, true); + dipshitBlur.animation.addByPrefix('idle', "CHOOSE vertical offset instance 1", 24, true); dipshitBlur.blend = BlendMode.ADD; dipshitBlur.animation.play("idle"); add(dipshitBlur); - var dipshitBacking:FlxSprite = new FlxSprite(423, -17); + dipshitBacking = new FlxSprite(423, -17); dipshitBacking.frames = Paths.getSparrowAtlas("charSelect/dipshitBacking"); - dipshitBacking.animation.addByPrefix('idle', "CHOOSE horizontal", 24, true); + dipshitBacking.animation.addByPrefix('idle', "CHOOSE horizontal offset instance 1", 24, true); dipshitBacking.blend = BlendMode.ADD; dipshitBacking.animation.play("idle"); add(dipshitBacking); - var chooseDipshit:FlxSprite = new FlxSprite(426, -13); + dipshitBacking.y += 210; + FlxTween.tween(dipshitBacking, {y: dipshitBacking.y - 210}, 1.1, {ease: FlxEase.expoOut}); + + chooseDipshit = new FlxSprite(426, -13); chooseDipshit.loadGraphic(Paths.image('charSelect/chooseDipshit')); add(chooseDipshit); + chooseDipshit.y += 200; + FlxTween.tween(chooseDipshit, {y: chooseDipshit.y - 200}, 1, {ease: FlxEase.expoOut}); + + dipshitBlur.y += 220; + FlxTween.tween(dipshitBlur, {y: dipshitBlur.y - 220}, 1.2, {ease: FlxEase.expoOut}); + chooseDipshit.scrollFactor.set(); dipshitBacking.scrollFactor.set(); dipshitBlur.scrollFactor.set(); @@ -261,14 +293,14 @@ class CharSelectSubState extends MusicBeatSubState cursorConfirmed = new FlxSprite(0, 0); cursorConfirmed.scrollFactor.set(); cursorConfirmed.frames = Paths.getSparrowAtlas("charSelect/charSelectorConfirm"); - cursorConfirmed.animation.addByPrefix("idle", "cursor ACCEPTED", 24, true); + cursorConfirmed.animation.addByPrefix("idle", "cursor ACCEPTED instance 1", 24, true); cursorConfirmed.visible = false; add(cursorConfirmed); cursorDenied = new FlxSprite(0, 0); cursorDenied.scrollFactor.set(); cursorDenied.frames = Paths.getSparrowAtlas("charSelect/charSelectorDenied"); - cursorDenied.animation.addByPrefix("idle", "cursor DENIED", 24, false); + cursorDenied.animation.addByPrefix("idle", "cursor DENIED instance 1", 24, false); cursorDenied.visible = false; add(cursorDenied); @@ -278,6 +310,12 @@ class CharSelectSubState extends MusicBeatSubState initLocks(); + for (index => member in grpIcons.members) + { + member.y += 300; + FlxTween.tween(member, {y: member.y - 300}, 1, {ease: FlxEase.expoOut}); + } + cursor.scrollFactor.set(); cursorBlue.scrollFactor.set(); cursorDarkBlue.scrollFactor.set(); @@ -289,13 +327,12 @@ class CharSelectSubState extends MusicBeatSubState FlxG.debugger.addTrackerProfile(new TrackerProfile(CharSelectSubState, ["curChar", "grpXSpread", "grpYSpread"])); FlxG.debugger.track(this); - FlxG.sound.playMusic(Paths.music('charSelect/charSelectMusic')); - camFollow = new FlxObject(0, 0, 1, 1); add(camFollow); camFollow.screenCenter(); - FlxG.camera.follow(camFollow, LOCKON, 0.01); + // FlxG.camera.follow(camFollow, LOCKON, 0.01); + FlxG.camera.follow(camFollow, LOCKON); var temp:FlxSprite = new FlxSprite(); temp.loadGraphic(Paths.image('charSelect/placement')); @@ -303,6 +340,25 @@ class CharSelectSubState extends MusicBeatSubState temp.alpha = 0.0; Conductor.stepHit.add(spamOnStep); // FlxG.debugger.track(temp, "tempBG"); + + transitionGradient = new FlxSprite(0, 0).loadGraphic(Paths.image('freeplay/transitionGradient')); + transitionGradient.scale.set(1280, 1); + transitionGradient.flipY = true; + transitionGradient.updateHitbox(); + FlxTween.tween(transitionGradient, {y: -720}, 1, {ease: FlxEase.expoOut}); + add(transitionGradient); + + camFollow.screenCenter(); + camFollow.y -= 150; + fadeShader.fade(0.0, 1.0, 0.8, {ease: FlxEase.quadOut}); + FlxTween.tween(camFollow, {y: camFollow.y + 150}, 1.5, + { + ease: FlxEase.expoOut, + onComplete: function(_) { + autoFollow = true; + FlxG.camera.follow(camFollow, LOCKON, 0.01); + } + }); } var grpIcons:FlxSpriteGroup; @@ -373,6 +429,42 @@ class CharSelectSubState extends MusicBeatSubState } } + function goToFreeplay():Void + { + autoFollow = false; + + FlxTween.tween(cursor, {alpha: 0}, 0.8, {ease: FlxEase.expoOut}); + FlxTween.tween(cursorBlue, {alpha: 0}, 0.8, {ease: FlxEase.expoOut}); + FlxTween.tween(cursorDarkBlue, {alpha: 0}, 0.8, {ease: FlxEase.expoOut}); + FlxTween.tween(cursorConfirmed, {alpha: 0}, 0.8, {ease: FlxEase.expoOut}); + + FlxTween.tween(barthing, {y: barthing.y + 80}, 0.8, {ease: FlxEase.backIn}); + FlxTween.tween(dipshitBacking, {y: dipshitBacking.y + 210}, 0.8, {ease: FlxEase.backIn}); + FlxTween.tween(chooseDipshit, {y: chooseDipshit.y + 200}, 0.8, {ease: FlxEase.backIn}); + FlxTween.tween(dipshitBlur, {y: dipshitBlur.y + 220}, 0.8, {ease: FlxEase.backIn}); + for (index => member in grpIcons.members) + { + // member.y += 300; + FlxTween.tween(member, {y: member.y + 300}, 0.8, {ease: FlxEase.backIn}); + } + FlxG.camera.follow(camFollow, LOCKON); + FlxTween.tween(transitionGradient, {y: -150}, 0.8, {ease: FlxEase.backIn}); + fadeShader.fade(1.0, 0, 0.8, {ease: FlxEase.quadIn}); + FlxTween.tween(camFollow, {y: camFollow.y - 150}, 0.8, + { + ease: FlxEase.backIn, + onComplete: function(_) { + FlxG.switchState(FreeplayState.build( + { + { + character: curChar, + fromCharSelect: true + } + })); + } + }); + } + var holdTmrUp:Float = 0; var holdTmrDown:Float = 0; var holdTmrLeft:Float = 0; @@ -494,18 +586,20 @@ class CharSelectSubState extends MusicBeatSubState FlxG.sound.play(Paths.sound('CS_confirm')); - FlxTween.tween(FlxG.sound.music, {pitch: 0.1}, 1.5, {ease: FlxEase.quadInOut}); + FlxTween.tween(FlxG.sound.music, {pitch: 0.1}, 1, {ease: FlxEase.quadInOut}); + FlxTween.tween(FlxG.sound.music, {volume: 0.0}, 1.5, {ease: FlxEase.quadInOut}); playerChill.playAnimation("select"); gfChill.playAnimation("confirm"); pressedSelect = true; selectTimer.start(1.5, (_) -> { pressedSelect = false; - FlxG.switchState(FreeplayState.build( - { - { - character: curChar - } - })); + // FlxG.switchState(FreeplayState.build( + // { + // { + // character: curChar + // } + // })); + goToFreeplay(); }); } @@ -515,6 +609,7 @@ class CharSelectSubState extends MusicBeatSubState grpCursors.visible = true; FlxTween.globalManager.cancelTweensOf(FlxG.sound.music); + FlxTween.tween(FlxG.sound.music, {pitch: 1.0, volume: 1.0}, 1, {ease: FlxEase.quartInOut}); playerChill.playAnimation("deselect"); gfChill.playAnimation("deselect"); FlxTween.tween(FlxG.sound.music, {pitch: 1.0}, 1, @@ -547,9 +642,12 @@ class CharSelectSubState extends MusicBeatSubState updateLockAnims(); - camFollow.screenCenter(); - camFollow.x += cursorX * 10; - camFollow.y += cursorY * 10; + if (autoFollow == true) + { + camFollow.screenCenter(); + camFollow.x += cursorX * 10; + camFollow.y += cursorY * 10; + } cursorLocIntended.x = (cursorFactor * cursorX) + (FlxG.width / 2) - cursor.width / 2; cursorLocIntended.y = (cursorFactor * cursorY) + (FlxG.height / 2) - cursor.height / 2; @@ -567,6 +665,16 @@ class CharSelectSubState extends MusicBeatSubState cursorDarkBlue.y = MathUtil.coolLerp(cursorDarkBlue.y, cursorLocIntended.y, lerpAmnt * 0.2); } + public override function dispatchEvent(event:ScriptEvent):Void + { + // super.dispatchEvent(event) dispatches event to module scripts. + super.dispatchEvent(event); + + // Dispatch events (like onBeatHit) to props + ScriptEventDispatcher.callEvent(playerChill, event); + ScriptEventDispatcher.callEvent(gfChill, event); + } + function spamOnStep():Void { if (spamUp || spamDown || spamLeft || spamRight) diff --git a/source/funkin/ui/freeplay/AlbumRoll.hx b/source/funkin/ui/freeplay/AlbumRoll.hx index 64b86120c..4107369d2 100644 --- a/source/funkin/ui/freeplay/AlbumRoll.hx +++ b/source/funkin/ui/freeplay/AlbumRoll.hx @@ -41,6 +41,7 @@ class AlbumRoll extends FlxSpriteGroup var difficultyStars:DifficultyStars; var _exitMovers:Null; + var _exitMoversCharSel:Null; var albumData:Album; @@ -128,7 +129,7 @@ class AlbumRoll extends FlxSpriteGroup * Apply exit movers for the album roll. * @param exitMovers The exit movers to apply. */ - public function applyExitMovers(?exitMovers:FreeplayState.ExitMoverData):Void + public function applyExitMovers(?exitMovers:FreeplayState.ExitMoverData, ?exitMoversCharSel:FreeplayState.ExitMoverData):Void { if (exitMovers == null) { @@ -141,12 +142,30 @@ class AlbumRoll extends FlxSpriteGroup if (exitMovers == null) return; + if (exitMoversCharSel == null) + { + exitMoversCharSel = _exitMoversCharSel; + } + else + { + _exitMoversCharSel = exitMoversCharSel; + } + + if (exitMoversCharSel == null) return; + exitMovers.set([newAlbumArt, difficultyStars], { x: FlxG.width, speed: 0.4, wait: 0 }); + + exitMoversCharSel.set([newAlbumArt, difficultyStars], + { + y: -175, + speed: 0.8, + wait: 0.1 + }); } var titleTimer:Null = null; @@ -207,6 +226,13 @@ class AlbumRoll extends FlxSpriteGroup speed: 0.4, wait: 0 }); + + if (_exitMoversCharSel != null) _exitMoversCharSel.set([albumTitle], + { + y: -190, + speed: 0.8, + wait: 0.1 + }); } public function setDifficultyStars(?difficulty:Int):Void diff --git a/source/funkin/ui/freeplay/CapsuleOptionsMenu.hx b/source/funkin/ui/freeplay/CapsuleOptionsMenu.hx new file mode 100644 index 000000000..cb0fa7b28 --- /dev/null +++ b/source/funkin/ui/freeplay/CapsuleOptionsMenu.hx @@ -0,0 +1,176 @@ +package funkin.ui.freeplay; + +import funkin.graphics.shaders.PureColor; +import funkin.input.Controls; +import flixel.group.FlxSpriteGroup; +import funkin.graphics.FunkinSprite; +import flixel.util.FlxColor; +import flixel.util.FlxTimer; +import flixel.text.FlxText; +import flixel.text.FlxText.FlxTextAlign; + +@:nullSafety +class CapsuleOptionsMenu extends FlxSpriteGroup +{ + var capsuleMenuBG:FunkinSprite; + var parent:FreeplayState; + + var queueDestroy:Bool = false; + + var instrumentalIds:Array = ['']; + var currentInstrumentalIndex:Int = 0; + + var currentInstrumental:FlxText; + + public function new(parent:FreeplayState, x:Float = 0, y:Float = 0, instIds:Array):Void + { + super(x, y); + + this.parent = parent; + this.instrumentalIds = instIds; + + capsuleMenuBG = FunkinSprite.createSparrow(0, 0, 'freeplay/instBox/instBox'); + + capsuleMenuBG.animation.addByPrefix('open', 'open0', 24, false); + capsuleMenuBG.animation.addByPrefix('idle', 'idle0', 24, true); + capsuleMenuBG.animation.addByPrefix('open', 'open0', 24, false); + + currentInstrumental = new FlxText(0, 36, capsuleMenuBG.width, ''); + currentInstrumental.setFormat('VCR OSD Mono', 40, FlxTextAlign.CENTER, true); + + final PAD = 4; + var leftArrow = new InstrumentalSelector(parent, PAD, 30, false, parent.getControls()); + var rightArrow = new InstrumentalSelector(parent, capsuleMenuBG.width - leftArrow.width - PAD, 30, true, parent.getControls()); + + var label:FlxText = new FlxText(0, 5, capsuleMenuBG.width, 'INSTRUMENTAL'); + label.setFormat('VCR OSD Mono', 24, FlxTextAlign.CENTER, true); + + add(capsuleMenuBG); + add(leftArrow); + add(rightArrow); + add(label); + add(currentInstrumental); + + capsuleMenuBG.animation.finishCallback = function(_) { + capsuleMenuBG.animation.play('idle', true); + }; + capsuleMenuBG.animation.play('open', true); + } + + public override function update(elapsed:Float):Void + { + super.update(elapsed); + + if (queueDestroy) + { + destroy(); + return; + } + @:privateAccess + if (parent.controls.BACK) + { + close(); + return; + } + + var changedInst = false; + if (parent.getControls().UI_LEFT_P) + { + currentInstrumentalIndex = (currentInstrumentalIndex + 1) % instrumentalIds.length; + changedInst = true; + } + if (parent.getControls().UI_RIGHT_P) + { + currentInstrumentalIndex = (currentInstrumentalIndex - 1 + instrumentalIds.length) % instrumentalIds.length; + changedInst = true; + } + if (!changedInst && currentInstrumental.text == '') changedInst = true; + + if (changedInst) + { + currentInstrumental.text = instrumentalIds[currentInstrumentalIndex].toTitleCase() ?? ''; + if (currentInstrumental.text == '') currentInstrumental.text = 'Default'; + } + + if (parent.getControls().ACCEPT) + { + onConfirm(instrumentalIds[currentInstrumentalIndex] ?? ''); + } + } + + public function close():Void + { + // Play in reverse. + capsuleMenuBG.animation.play('open', true, true); + capsuleMenuBG.animation.finishCallback = function(_) { + parent.cleanupCapsuleOptionsMenu(); + queueDestroy = true; + }; + } + + /** + * Override this with `capsuleOptionsMenu.onConfirm = myFunction;` + */ + public dynamic function onConfirm(targetInstId:String):Void + { + throw 'onConfirm not implemented!'; + } +} + +/** + * The difficulty selector arrows to the left and right of the difficulty. + */ +class InstrumentalSelector extends FunkinSprite +{ + var controls:Controls; + var whiteShader:PureColor; + + var parent:FreeplayState; + + var baseScale:Float = 0.6; + + public function new(parent:FreeplayState, x:Float, y:Float, flipped:Bool, controls:Controls) + { + super(x, y); + + this.parent = parent; + + this.controls = controls; + + frames = Paths.getSparrowAtlas('freeplay/freeplaySelector'); + animation.addByPrefix('shine', 'arrow pointer loop', 24); + animation.play('shine'); + + whiteShader = new PureColor(FlxColor.WHITE); + + shader = whiteShader; + + flipX = flipped; + + scale.x = scale.y = 1 * baseScale; + updateHitbox(); + } + + override function update(elapsed:Float):Void + { + if (flipX && controls.UI_RIGHT_P) moveShitDown(); + if (!flipX && controls.UI_LEFT_P) moveShitDown(); + + super.update(elapsed); + } + + function moveShitDown():Void + { + offset.y -= 5; + + whiteShader.colorSet = true; + + scale.x = scale.y = 0.5 * baseScale; + + new FlxTimer().start(2 / 24, function(tmr) { + scale.x = scale.y = 1 * baseScale; + whiteShader.colorSet = false; + updateHitbox(); + }); + } +} diff --git a/source/funkin/ui/freeplay/CapsuleText.hx b/source/funkin/ui/freeplay/CapsuleText.hx index c3bcdb09b..aae72544e 100644 --- a/source/funkin/ui/freeplay/CapsuleText.hx +++ b/source/funkin/ui/freeplay/CapsuleText.hx @@ -10,6 +10,7 @@ import flixel.tweens.FlxEase; import flixel.util.FlxTimer; import flixel.tweens.FlxTween; import openfl.display.BlendMode; +import flixel.util.FlxColor; class CapsuleText extends FlxSpriteGroup { @@ -25,6 +26,8 @@ class CapsuleText extends FlxSpriteGroup public var tooLong:Bool = false; + var glowColor:FlxColor = 0xFF00ccff; + // 255, 27 normal // 220, 27 favourited @@ -38,7 +41,7 @@ class CapsuleText extends FlxSpriteGroup // whiteText.shader = new GaussianBlurShader(0.3); text = songTitle; - blurredText.color = 0xFF00ccff; + blurredText.color = glowColor; whiteText.color = 0xFFFFFFFF; add(blurredText); add(whiteText); @@ -51,6 +54,16 @@ class CapsuleText extends FlxSpriteGroup return text; } + public function applyStyle(styleData:FreeplayStyle):Void + { + glowColor = styleData.getCapsuleSelCol(); + blurredText.color = glowColor; + whiteText.textField.filters = [ + new openfl.filters.GlowFilter(glowColor, 1, 5, 5, 210, BitmapFilterQuality.MEDIUM), + // new openfl.filters.BlurFilter(5, 5, BitmapFilterQuality.LOW) + ]; + } + // ???? none // 255, 27 normal // 220, 27 favourited @@ -99,7 +112,7 @@ class CapsuleText extends FlxSpriteGroup whiteText.text = value; checkClipWidth(); whiteText.textField.filters = [ - new openfl.filters.GlowFilter(0x00ccff, 1, 5, 5, 210, BitmapFilterQuality.MEDIUM), + new openfl.filters.GlowFilter(glowColor, 1, 5, 5, 210, BitmapFilterQuality.MEDIUM), // new openfl.filters.BlurFilter(5, 5, BitmapFilterQuality.LOW) ]; @@ -186,7 +199,7 @@ class CapsuleText extends FlxSpriteGroup } else { - blurredText.color = 0xFF00aadd; + blurredText.color = glowColor; whiteText.color = 0xFFDDDDDD; whiteText.textField.filters = [ new openfl.filters.GlowFilter(0xDDDDDD, 1, 5, 5, 210, BitmapFilterQuality.MEDIUM), diff --git a/source/funkin/ui/freeplay/FreeplayDJ.hx b/source/funkin/ui/freeplay/FreeplayDJ.hx index b1528d906..51829d44d 100644 --- a/source/funkin/ui/freeplay/FreeplayDJ.hx +++ b/source/funkin/ui/freeplay/FreeplayDJ.hx @@ -15,7 +15,9 @@ class FreeplayDJ extends FlxAtlasSprite { // Represents the sprite's current status. // Without state machines I would have driven myself crazy years ago. - public var currentState:FreeplayDJState = Intro; + // Made this PRIVATE so we can keep track of everything that can alter the state! + // Add a function to this class if you want to edit this value from outside. + private var currentState:FreeplayDJState = Intro; // A callback activated when the intro animation finishes. public var onIntroDone:FlxSignal = new FlxSignal(); @@ -378,7 +380,7 @@ class FreeplayDJ extends FlxAtlasSprite public function toCharSelect():Void { - if (hasAnimation('charSelect')) + if (hasAnimation(playableCharData.getAnimationPrefix('charSelect'))) { currentState = CharSelect; var animPrefix = playableCharData.getAnimationPrefix('charSelect'); @@ -386,6 +388,7 @@ class FreeplayDJ extends FlxAtlasSprite } else { + FlxG.log.warn("Freeplay character does not have 'charSelect' animation!"); currentState = Confirm; // Call this immediately; otherwise, we get locked out of Character Select. onCharSelectComplete(); diff --git a/source/funkin/ui/freeplay/FreeplayScore.hx b/source/funkin/ui/freeplay/FreeplayScore.hx index da4c9f5d4..fee55ce7c 100644 --- a/source/funkin/ui/freeplay/FreeplayScore.hx +++ b/source/funkin/ui/freeplay/FreeplayScore.hx @@ -42,13 +42,20 @@ class FreeplayScore extends FlxTypedSpriteGroup return val; } - public function new(x:Float, y:Float, digitCount:Int, scoreShit:Int = 100) + public function new(x:Float, y:Float, digitCount:Int, scoreShit:Int = 100, ?styleData:FreeplayStyle) { super(x, y); for (i in 0...digitCount) { - add(new ScoreNum(x + (45 * i), y, 0)); + if (styleData == null) + { + add(new ScoreNum(x + (45 * i), y, 0)); + } + else + { + add(new ScoreNum(x + (45 * i), y, 0, styleData)); + } } this.scoreShit = scoreShit; @@ -76,16 +83,16 @@ class ScoreNum extends FlxSprite case 1: offset.x -= 15; case 5: - // set offsets - // offset.x += 0; - // offset.y += 10; + // set offsets + // offset.x += 0; + // offset.y += 10; case 7: - // offset.y += 6; + // offset.y += 6; case 4: - // offset.y += 5; + // offset.y += 5; case 9: - // offset.y += 5; + // offset.y += 5; default: centerOffsets(false); } @@ -99,14 +106,21 @@ class ScoreNum extends FlxSprite var numToString:Array = ["ZERO", "ONE", "TWO", "THREE", "FOUR", "FIVE", "SIX", "SEVEN", "EIGHT", "NINE"]; - public function new(x:Float, y:Float, ?initDigit:Int = 0) + public function new(x:Float, y:Float, ?initDigit:Int = 0, ?styleData:FreeplayStyle) { super(x, y); baseY = y; baseX = x; - frames = Paths.getSparrowAtlas('digital_numbers'); + if (styleData == null) + { + frames = Paths.getSparrowAtlas('digital_numbers'); + } + else + { + frames = Paths.getSparrowAtlas(styleData.getNumbersAssetKey()); + } for (i in 0...10) { diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index f0bad1718..e421efd8a 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -1,5 +1,6 @@ package funkin.ui.freeplay; +import funkin.ui.freeplay.backcards.*; import flixel.addons.transition.FlxTransitionableState; import flixel.FlxCamera; import flixel.FlxSprite; @@ -29,7 +30,9 @@ import funkin.graphics.shaders.AngleMask; import funkin.graphics.shaders.GaussianBlurShader; import funkin.graphics.shaders.HSVShader; import funkin.graphics.shaders.PureColor; +import funkin.graphics.shaders.BlueFade; import funkin.graphics.shaders.StrokeShader; +import openfl.filters.ShaderFilter; import funkin.input.Controls; import funkin.play.PlayStatePlaylist; import funkin.play.scoring.Scoring; @@ -49,6 +52,8 @@ import funkin.util.MathUtil; import funkin.util.SortUtil; import lime.utils.Assets; import openfl.display.BlendMode; +import funkin.data.freeplay.style.FreeplayStyleRegistry; +import funkin.data.song.SongData.SongMusicData; /** * Parameters used to initialize the FreeplayState. @@ -57,6 +62,8 @@ typedef FreeplayStateParams = { ?character:String, + ?fromCharSelect:Bool, + ?fromResults:FromResultsParams, }; @@ -143,7 +150,8 @@ class FreeplayState extends MusicBeatSubState var curSelected:Int = 0; var currentDifficulty:String = Constants.DEFAULT_DIFFICULTY; - var fp:FreeplayScore; + public var fp:FreeplayScore; + var txtCompletion:AtlasText; var lerpCompletion:Float = 0; var intendedCompletion:Float = 0; @@ -175,6 +183,8 @@ class FreeplayState extends MusicBeatSubState var letterSort:LetterSort; var exitMovers:ExitMoverData = new Map(); + var exitMoversCharSel:ExitMoverData = new Map(); + var stickerSubState:Null = null; /** @@ -198,40 +208,35 @@ class FreeplayState extends MusicBeatSubState var rankBg:FunkinSprite; var rankVignette:FlxSprite; - var backingTextYeah:FlxAtlasSprite; - var orangeBackShit:FunkinSprite; - var alsoOrangeLOL:FunkinSprite; - var pinkBack:FunkinSprite; - var confirmGlow:FlxSprite; - var confirmGlow2:FlxSprite; - var confirmTextGlow:FlxSprite; + var backingCard:Null = null; - var moreWays:BGScrollingText; - var funnyScroll:BGScrollingText; - var txtNuts:BGScrollingText; - var funnyScroll2:BGScrollingText; - var moreWays2:BGScrollingText; - var funnyScroll3:BGScrollingText; - - var bgDad:FlxSprite; - var cardGlow:FlxSprite; + public var bgDad:FlxSprite; var fromResultsParams:Null = null; var prepForNewRank:Bool = false; + var styleData:Null = null; + + var fromCharSelect:Null = null; + public function new(?params:FreeplayStateParams, ?stickers:StickerSubState) { currentCharacterId = params?.character ?? rememberedCharacterId; + styleData = FreeplayStyleRegistry.instance.fetchEntry(currentCharacterId); var fetchPlayableCharacter = function():PlayableCharacter { - var result = PlayerRegistry.instance.fetchEntry(params?.character ?? rememberedCharacterId); - if (result == null) throw 'No valid playable character with id ${params?.character}'; + var targetCharId = params?.character ?? rememberedCharacterId; + var result = PlayerRegistry.instance.fetchEntry(targetCharId); + if (result == null) throw 'No valid playable character with id ${targetCharId}'; return result; }; currentCharacter = fetchPlayableCharacter(); + styleData = FreeplayStyleRegistry.instance.fetchEntry(currentCharacter.getFreeplayStyleID()); rememberedCharacterId = currentCharacter?.id ?? Constants.DEFAULT_CHARACTER; + fromCharSelect = params?.fromCharSelect; + fromResultsParams = params?.fromResults; if (fromResultsParams?.playRankAnim == true) @@ -246,49 +251,40 @@ class FreeplayState extends MusicBeatSubState stickerSubState = stickers; } + switch (currentCharacterId) + { + case 'bf': + backingCard = new BoyfriendCard(currentCharacter); + case 'pico': + backingCard = new PicoCard(currentCharacter); + default: + backingCard = new BackingCard(currentCharacter); + } + // We build a bunch of sprites BEFORE create() so we can guarantee they aren't null later on. albumRoll = new AlbumRoll(); - fp = new FreeplayScore(460, 60, 7, 100); - cardGlow = new FlxSprite(-30, -30).loadGraphic(Paths.image('freeplay/cardGlow')); - confirmGlow = new FlxSprite(-30, 240).loadGraphic(Paths.image('freeplay/confirmGlow')); - confirmTextGlow = new FlxSprite(-8, 115).loadGraphic(Paths.image('freeplay/glowingText')); + fp = new FreeplayScore(460, 60, 7, 100, styleData); rankCamera = new FunkinCamera('rankCamera', 0, 0, FlxG.width, FlxG.height); funnyCam = new FunkinCamera('freeplayFunny', 0, 0, FlxG.width, FlxG.height); - funnyScroll = new BGScrollingText(0, 220, currentCharacter.getFreeplayDJText(1), FlxG.width / 2, false, 60); - funnyScroll2 = new BGScrollingText(0, 335, currentCharacter.getFreeplayDJText(1), FlxG.width / 2, false, 60); grpCapsules = new FlxTypedGroup(); grpDifficulties = new FlxTypedSpriteGroup(-300, 80); letterSort = new LetterSort(400, 75); grpSongs = new FlxTypedGroup(); - moreWays = new BGScrollingText(0, 160, currentCharacter.getFreeplayDJText(2), FlxG.width, true, 43); - moreWays2 = new BGScrollingText(0, 397, currentCharacter.getFreeplayDJText(2), FlxG.width, true, 43); - pinkBack = FunkinSprite.create('freeplay/pinkBack'); rankBg = new FunkinSprite(0, 0); rankVignette = new FlxSprite(0, 0).loadGraphic(Paths.image('freeplay/rankVignette')); sparks = new FlxSprite(0, 0); sparksADD = new FlxSprite(0, 0); txtCompletion = new AtlasText(1185, 87, '69', AtlasFont.FREEPLAY_CLEAR); - txtNuts = new BGScrollingText(0, 285, currentCharacter.getFreeplayDJText(3), FlxG.width / 2, true, 43); ostName = new FlxText(8, 8, FlxG.width - 8 - 8, 'OFFICIAL OST', 48); - orangeBackShit = new FunkinSprite(84, 440).makeSolidColor(Std.int(pinkBack.width), 75, 0xFFFEDA00); - - bgDad = new FlxSprite(pinkBack.width * 0.74, 0).loadGraphic(Paths.image('freeplay/freeplayBGdad')); - alsoOrangeLOL = new FunkinSprite(0, orangeBackShit.y).makeSolidColor(100, Std.int(orangeBackShit.height), 0xFFFFD400); - confirmGlow2 = new FlxSprite(confirmGlow.x, confirmGlow.y).loadGraphic(Paths.image('freeplay/confirmGlow2')); - funnyScroll3 = new BGScrollingText(0, orangeBackShit.y + 10, currentCharacter.getFreeplayDJText(1), FlxG.width / 2, 60); - backingTextYeah = new FlxAtlasSprite(640, 370, Paths.animateAtlas("freeplay/backing-text-yeah"), - { - FrameRate: 24.0, - Reversed: false, - // ?OnComplete:Void -> Void, - ShowPivot: false, - Antialiasing: true, - ScrollFactor: new FlxPoint(1, 1), - }); + bgDad = new FlxSprite(backingCard.pinkBack.width * 0.74, 0).loadGraphic(styleData == null ? 'freeplay/freeplayBGdad' : styleData.getBgAssetGraphic()); } + var fadeShader:BlueFade = new BlueFade(); + + public var angleMaskShader:AngleMask = new AngleMask(); + override function create():Void { super.create(); @@ -297,6 +293,9 @@ class FreeplayState extends MusicBeatSubState FlxTransitionableState.skipNextTransIn = true; + var fadeShaderFilter:ShaderFilter = new ShaderFilter(fadeShader); + funnyCam.filters = [fadeShaderFilter]; + if (stickerSubState != null) { this.persistentUpdate = true; @@ -317,6 +316,9 @@ class FreeplayState extends MusicBeatSubState isDebug = true; #end + // Block input until the intro finishes. + busy = true; + // Add a null entry that represents the RANDOM option songs.push(null); @@ -365,113 +367,13 @@ class FreeplayState extends MusicBeatSubState trace(FlxG.camera.initialZoom); trace(FlxCamera.defaultZoom); - pinkBack.color = 0xFFFFD4E9; // sets it to pink! - pinkBack.x -= pinkBack.width; - - FlxTween.tween(pinkBack, {x: 0}, 0.6, {ease: FlxEase.quartOut}); - add(pinkBack); - - add(orangeBackShit); - - add(alsoOrangeLOL); - - exitMovers.set([pinkBack, orangeBackShit, alsoOrangeLOL], - { - x: -pinkBack.width, - y: pinkBack.y, - speed: 0.4, - wait: 0 - }); - - FlxSpriteUtil.alphaMaskFlxSprite(orangeBackShit, pinkBack, orangeBackShit); - orangeBackShit.visible = false; - alsoOrangeLOL.visible = false; - - confirmTextGlow.blend = BlendMode.ADD; - confirmTextGlow.visible = false; - - confirmGlow.blend = BlendMode.ADD; - - confirmGlow.visible = false; - confirmGlow2.visible = false; - - add(confirmGlow2); - add(confirmGlow); - - add(confirmTextGlow); - - var grpTxtScrolls:FlxGroup = new FlxGroup(); - add(grpTxtScrolls); - grpTxtScrolls.visible = false; - - FlxG.debugger.addTrackerProfile(new TrackerProfile(BGScrollingText, ['x', 'y', 'speed', 'size'])); - - moreWays.funnyColor = 0xFFFFF383; - moreWays.speed = 6.8; - grpTxtScrolls.add(moreWays); - - exitMovers.set([moreWays], - { - x: FlxG.width * 2, - speed: 0.4, - }); - - funnyScroll.funnyColor = 0xFFFF9963; - funnyScroll.speed = -3.8; - grpTxtScrolls.add(funnyScroll); - - exitMovers.set([funnyScroll], - { - x: -funnyScroll.width * 2, - y: funnyScroll.y, - speed: 0.4, - wait: 0 - }); - - txtNuts.speed = 3.5; - grpTxtScrolls.add(txtNuts); - exitMovers.set([txtNuts], - { - x: FlxG.width * 2, - speed: 0.4, - }); - - funnyScroll2.funnyColor = 0xFFFF9963; - funnyScroll2.speed = -3.8; - grpTxtScrolls.add(funnyScroll2); - - exitMovers.set([funnyScroll2], - { - x: -funnyScroll2.width * 2, - speed: 0.5, - }); - - moreWays2.funnyColor = 0xFFFFF383; - moreWays2.speed = 6.8; - grpTxtScrolls.add(moreWays2); - - exitMovers.set([moreWays2], - { - x: FlxG.width * 2, - speed: 0.4 - }); - - funnyScroll3.funnyColor = 0xFFFEA400; - funnyScroll3.speed = -3.8; - grpTxtScrolls.add(funnyScroll3); - - exitMovers.set([funnyScroll3], - { - x: -funnyScroll3.width * 2, - speed: 0.3 - }); - - add(backingTextYeah); - - cardGlow.blend = BlendMode.ADD; - cardGlow.visible = false; - - add(cardGlow); + if (backingCard != null) + { + add(backingCard); + backingCard.init(); + backingCard.applyExitMovers(exitMovers, exitMoversCharSel); + backingCard.instance = this; + } if (currentCharacter?.getFreeplayDJData() != null) { @@ -482,9 +384,15 @@ class FreeplayState extends MusicBeatSubState speed: 0.5 }); add(dj); + exitMoversCharSel.set([dj], + { + y: -175, + speed: 0.8, + wait: 0.1 + }); } - bgDad.shader = new AngleMask(); + bgDad.shader = angleMaskShader; bgDad.visible = false; var blackOverlayBullshitLOLXD:FlxSprite = new FlxSprite(FlxG.width).makeGraphic(Std.int(bgDad.width), Std.int(bgDad.height), FlxColor.BLACK); @@ -504,8 +412,15 @@ class FreeplayState extends MusicBeatSubState wait: 0 }); + exitMoversCharSel.set([blackOverlayBullshitLOLXD, bgDad], + { + y: -100, + speed: 0.8, + wait: 0.1 + }); + add(bgDad); - FlxTween.tween(blackOverlayBullshitLOLXD, {x: pinkBack.width * 0.74}, 0.7, {ease: FlxEase.quintOut}); + // backingCard.pinkBack.width * 0.74 blackOverlayBullshitLOLXD.shader = bgDad.shader; @@ -525,6 +440,13 @@ class FreeplayState extends MusicBeatSubState wait: 0 }); + exitMoversCharSel.set([grpDifficulties], + { + y: -270, + speed: 0.8, + wait: 0.1 + }); + for (diffId in diffIdsTotal) { var diffSprite:DifficultySprite = new DifficultySprite(diffId); @@ -545,12 +467,21 @@ class FreeplayState extends MusicBeatSubState albumRoll.albumId = null; add(albumRoll); - albumRoll.applyExitMovers(exitMovers); - - var overhangStuff:FlxSprite = new FlxSprite().makeGraphic(FlxG.width, 64, FlxColor.BLACK); + var overhangStuff:FlxSprite = new FlxSprite().makeGraphic(FlxG.width, 164, FlxColor.BLACK); overhangStuff.y -= overhangStuff.height; - add(overhangStuff); - FlxTween.tween(overhangStuff, {y: 0}, 0.3, {ease: FlxEase.quartOut}); + + if (fromCharSelect == true) + { + blackOverlayBullshitLOLXD.x = 387.76; + overhangStuff.y = -100; + backingCard?.skipIntroTween(); + } + else + { + albumRoll.applyExitMovers(exitMovers, exitMoversCharSel); + FlxTween.tween(overhangStuff, {y: -100}, 0.3, {ease: FlxEase.quartOut}); + FlxTween.tween(blackOverlayBullshitLOLXD, {x: 387.76}, 0.7, {ease: FlxEase.quintOut}); + } var fnfFreeplay:FlxText = new FlxText(8, 8, 0, 'FREEPLAY', 48); fnfFreeplay.font = 'VCR OSD Mono'; @@ -568,11 +499,16 @@ class FreeplayState extends MusicBeatSubState wait: 0 }); + exitMoversCharSel.set([overhangStuff, fnfFreeplay, ostName], + { + y: -300, + speed: 0.8, + wait: 0.1 + }); + var sillyStroke:StrokeShader = new StrokeShader(0xFFFFFFFF, 2, 2); fnfFreeplay.shader = sillyStroke; ostName.shader = sillyStroke; - add(fnfFreeplay); - add(ostName); var fnfHighscoreSpr:FlxSprite = new FlxSprite(860, 70); fnfHighscoreSpr.frames = Paths.getSparrowAtlas('freeplay/highscore'); @@ -606,6 +542,13 @@ class FreeplayState extends MusicBeatSubState speed: 0.3 }); + exitMoversCharSel.set([letterSort], + { + y: -270, + speed: 0.8, + wait: 0.1 + }); + letterSort.changeSelectionCallback = (str) -> { switch (str) { @@ -635,22 +578,49 @@ class FreeplayState extends MusicBeatSubState speed: 0.3 }); - var diffSelLeft:DifficultySelector = new DifficultySelector(this, 20, grpDifficulties.y - 10, false, controls); - var diffSelRight:DifficultySelector = new DifficultySelector(this, 325, grpDifficulties.y - 10, true, controls); + exitMoversCharSel.set([fp, txtCompletion, fnfHighscoreSpr, txtCompletion, clearBoxSprite], + { + y: -270, + speed: 0.8, + wait: 0.1 + }); + + var diffSelLeft:DifficultySelector = new DifficultySelector(this, 20, grpDifficulties.y - 10, false, controls, styleData); + var diffSelRight:DifficultySelector = new DifficultySelector(this, 325, grpDifficulties.y - 10, true, controls, styleData); diffSelLeft.visible = false; diffSelRight.visible = false; add(diffSelLeft); add(diffSelRight); + // putting these here to fix the layering + add(overhangStuff); + add(fnfFreeplay); + add(ostName); + // be careful not to "add()" things in here unless it's to a group that's already added to the state // otherwise it won't be properly attatched to funnyCamera (relavent code should be at the bottom of create()) var onDJIntroDone = function() { + busy = false; + // when boyfriend hits dat shiii albumRoll.playIntro(); var daSong = grpCapsules.members[curSelected].songData; albumRoll.albumId = daSong?.albumId; + if (fromCharSelect == null) + { + // render optimisation + if (_parentState != null) _parentState.persistentDraw = false; + + FlxTween.color(bgDad, 0.6, 0xFF000000, 0xFFFFFFFF, + { + ease: FlxEase.expoOut, + onUpdate: function(_) { + angleMaskShader.extraColor = bgDad.color; + } + }); + } FlxTween.tween(grpDifficulties, {x: 90}, 0.6, {ease: FlxEase.quartOut}); @@ -664,6 +634,13 @@ class FreeplayState extends MusicBeatSubState speed: 0.26 }); + exitMoversCharSel.set([diffSelLeft, diffSelRight], + { + y: -270, + speed: 0.8, + wait: 0.1 + }); + new FlxTimer().start(1 / 24, function(handShit) { fnfHighscoreSpr.visible = true; fnfFreeplay.visible = true; @@ -682,17 +659,8 @@ class FreeplayState extends MusicBeatSubState }); }); - pinkBack.color = 0xFFFFD863; bgDad.visible = true; - orangeBackShit.visible = true; - alsoOrangeLOL.visible = true; - grpTxtScrolls.visible = true; - - // render optimisation - if (_parentState != null) _parentState.persistentDraw = false; - - cardGlow.visible = true; - FlxTween.tween(cardGlow, {alpha: 0, "scale.x": 1.2, "scale.y": 1.2}, 0.45, {ease: FlxEase.sineOut}); + backingCard?.introDone(); if (prepForNewRank && fromResultsParams != null) { @@ -735,6 +703,12 @@ class FreeplayState extends MusicBeatSubState { rankCamera.fade(0xFF000000, 0, false, null, true); } + + if (fromCharSelect == true) + { + enterFromCharSel(); + onDJIntroDone(); + } } var currentFilter:Null = null; @@ -790,7 +764,7 @@ class FreeplayState extends MusicBeatSubState var hsvShader:HSVShader = new HSVShader(); var randomCapsule:SongMenuItem = grpCapsules.recycle(SongMenuItem); - randomCapsule.init(FlxG.width, 0, null); + randomCapsule.init(FlxG.width, 0, null, styleData); randomCapsule.onConfirm = function() { capsuleOnConfirmRandom(randomCapsule); }; @@ -802,7 +776,14 @@ class FreeplayState extends MusicBeatSubState randomCapsule.favIconBlurred.visible = false; randomCapsule.ranking.visible = false; randomCapsule.blurredRanking.visible = false; - randomCapsule.initJumpIn(0, force); + if (fromCharSelect == false) + { + randomCapsule.initJumpIn(0, force); + } + else + { + randomCapsule.forcePosition(); + } randomCapsule.hsvShader = hsvShader; grpCapsules.add(randomCapsule); @@ -813,9 +794,9 @@ class FreeplayState extends MusicBeatSubState var funnyMenu:SongMenuItem = grpCapsules.recycle(SongMenuItem); - funnyMenu.init(FlxG.width, 0, tempSong); + funnyMenu.init(FlxG.width, 0, tempSong, styleData); funnyMenu.onConfirm = function() { - capsuleOnConfirmDefault(funnyMenu); + capsuleOnOpenDefault(funnyMenu); }; funnyMenu.y = funnyMenu.intendedY(i + 1) + 10; funnyMenu.targetPos.x = funnyMenu.x; @@ -1187,6 +1168,158 @@ class FreeplayState extends MusicBeatSubState }); } + function tryOpenCharSelect():Void + { + // Check if we have ACCESS to character select! + trace('Is Pico unlocked? ${PlayerRegistry.instance.fetchEntry('pico')?.isUnlocked()}'); + trace('Number of characters: ${PlayerRegistry.instance.countUnlockedCharacters()}'); + + if (PlayerRegistry.instance.countUnlockedCharacters() > 1) + { + trace('Opening character select!'); + } + else + { + trace('Not enough characters unlocked to open character select!'); + FunkinSound.playOnce(Paths.sound('cancelMenu')); + return; + } + + busy = true; + + FunkinSound.playOnce(Paths.sound('confirmMenu')); + + if (dj != null) + { + dj.toCharSelect(); + } + + // Get this character's transition delay, with a reasonable default. + var transitionDelay:Float = currentCharacter.getFreeplayDJData()?.getCharSelectTransitionDelay() ?? 0.25; + + new FlxTimer().start(transitionDelay, _ -> { + transitionToCharSelect(); + }); + } + + function transitionToCharSelect():Void + { + var transitionGradient = new FlxSprite(0, 720).loadGraphic(Paths.image('freeplay/transitionGradient')); + transitionGradient.scale.set(1280, 1); + transitionGradient.updateHitbox(); + transitionGradient.cameras = [rankCamera]; + exitMoversCharSel.set([transitionGradient], + { + y: -720, + speed: 0.8, + wait: 0.1 + }); + add(transitionGradient); + for (index => capsule in grpCapsules.members) + { + var distFromSelected:Float = Math.abs(index - curSelected) - 1; + if (distFromSelected < 5) + { + capsule.doLerp = false; + exitMoversCharSel.set([capsule], + { + y: -250, + speed: 0.8, + wait: 0.1 + }); + } + } + fadeShader.fade(1.0, 0.0, 0.8, {ease: FlxEase.quadIn}); + FlxG.sound.music.fadeOut(0.9, 0); + new FlxTimer().start(0.9, _ -> { + FlxG.switchState(new funkin.ui.charSelect.CharSelectSubState()); + }); + for (grpSpr in exitMoversCharSel.keys()) + { + var moveData:Null = exitMoversCharSel.get(grpSpr); + if (moveData == null) continue; + + for (spr in grpSpr) + { + if (spr == null) continue; + + var funnyMoveShit:MoveData = moveData; + + var moveDataY = funnyMoveShit.y ?? spr.y; + var moveDataSpeed = funnyMoveShit.speed ?? 0.2; + var moveDataWait = funnyMoveShit.wait ?? 0.0; + + FlxTween.tween(spr, {y: moveDataY + spr.y}, moveDataSpeed, {ease: FlxEase.backIn}); + } + } + backingCard?.enterCharSel(); + } + + function enterFromCharSel():Void + { + busy = true; + if (_parentState != null) _parentState.persistentDraw = false; + + var transitionGradient = new FlxSprite(0, 720).loadGraphic(Paths.image('freeplay/transitionGradient')); + transitionGradient.scale.set(1280, 1); + transitionGradient.updateHitbox(); + transitionGradient.cameras = [rankCamera]; + exitMoversCharSel.set([transitionGradient], + { + y: -720, + speed: 1.5, + wait: 0.1 + }); + add(transitionGradient); + // FlxTween.tween(transitionGradient, {alpha: 0}, 1, {ease: FlxEase.circIn}); + // for (index => capsule in grpCapsules.members) + // { + // var distFromSelected:Float = Math.abs(index - curSelected) - 1; + // if (distFromSelected < 5) + // { + // capsule.doLerp = false; + // exitMoversCharSel.set([capsule], + // { + // y: -250, + // speed: 0.8, + // wait: 0.1 + // }); + // } + // } + fadeShader.fade(0.0, 1.0, 0.8, {ease: FlxEase.quadIn}); + for (grpSpr in exitMoversCharSel.keys()) + { + var moveData:Null = exitMoversCharSel.get(grpSpr); + if (moveData == null) continue; + + for (spr in grpSpr) + { + if (spr == null) continue; + + var funnyMoveShit:MoveData = moveData; + + var moveDataY = funnyMoveShit.y ?? spr.y; + var moveDataSpeed = funnyMoveShit.speed ?? 0.2; + var moveDataWait = funnyMoveShit.wait ?? 0.0; + + spr.y += moveDataY; + FlxTween.tween(spr, {y: spr.y - moveDataY}, moveDataSpeed * 1.2, + { + ease: FlxEase.expoOut, + onComplete: function(_) { + for (index => capsule in grpCapsules.members) + { + capsule.doLerp = true; + fromCharSelect = false; + busy = false; + albumRoll.applyExitMovers(exitMovers, exitMoversCharSel); + } + } + }); + } + } + } + var touchY:Float = 0; var touchX:Float = 0; var dxTouch:Float = 0; @@ -1213,6 +1346,16 @@ class FreeplayState extends MusicBeatSubState super.update(elapsed); #if FEATURE_DEBUG_FUNCTIONS + if (FlxG.keys.justPressed.P) + { + FlxG.switchState(FreeplayState.build( + { + { + character: currentCharacterId == "pico" ? Constants.DEFAULT_CHARACTER : "pico", + } + })); + } + if (FlxG.keys.justPressed.T) { rankAnimStart(fromResultsParams ?? @@ -1224,16 +1367,6 @@ class FreeplayState extends MusicBeatSubState }); } - if (FlxG.keys.justPressed.P) - { - FlxG.switchState(FreeplayState.build( - { - { - character: currentCharacterId == "pico" ? Constants.DEFAULT_CHARACTER : "pico", - } - })); - } - // if (FlxG.keys.justPressed.H) // { // rankDisplayNew(fromResultsParams); @@ -1243,36 +1376,11 @@ class FreeplayState extends MusicBeatSubState // { // rankAnimSlam(fromResultsParams); // } - #end + #end // ^<-- FEATURE_DEBUG_FUNCTIONS if (controls.FREEPLAY_CHAR_SELECT && !busy) { - // Check if we have ACCESS to character select! - trace('Is Pico unlocked? ${PlayerRegistry.instance.fetchEntry('pico')?.isUnlocked()}'); - trace('Number of characters: ${PlayerRegistry.instance.countUnlockedCharacters()}'); - - if (PlayerRegistry.instance.countUnlockedCharacters() > 1) - { - if (dj != null) - { - busy = true; - // Transition to character select after animation - dj.onCharSelectComplete = function() { - FlxG.switchState(new funkin.ui.charSelect.CharSelectSubState()); - } - dj.toCharSelect(); - } - else - { - // Transition to character select immediately - FlxG.switchState(new funkin.ui.charSelect.CharSelectSubState()); - } - } - else - { - trace('Not enough characters unlocked to open character select!'); - FunkinSound.playOnce(Paths.sound('cancelMenu')); - } + tryOpenCharSelect(); } if (controls.FREEPLAY_FAVORITE && !busy) @@ -1535,23 +1643,7 @@ class FreeplayState extends MusicBeatSubState var longestTimer:Float = 0; - // FlxTween.color(bgDad, 0.33, 0xFFFFFFFF, 0xFF555555, {ease: FlxEase.quadOut}); - FlxTween.color(pinkBack, 0.25, 0xFFFFD863, 0xFFFFD0D5, {ease: FlxEase.quadOut}); - - cardGlow.visible = true; - cardGlow.alpha = 1; - cardGlow.scale.set(1, 1); - FlxTween.tween(cardGlow, {alpha: 0, "scale.x": 1.2, "scale.y": 1.2}, 0.25, {ease: FlxEase.sineOut}); - - orangeBackShit.visible = false; - alsoOrangeLOL.visible = false; - - moreWays.visible = false; - funnyScroll.visible = false; - txtNuts.visible = false; - funnyScroll2.visible = false; - moreWays2.visible = false; - funnyScroll3.visible = false; + backingCard?.disappear(); for (grpSpr in exitMovers.keys()) { @@ -1614,6 +1706,13 @@ class FreeplayState extends MusicBeatSubState } } + override function beatHit():Bool + { + backingCard?.beatHit(); + + return super.beatHit(); + } + public override function destroy():Void { super.destroy(); @@ -1780,7 +1879,86 @@ class FreeplayState extends MusicBeatSubState capsuleOnConfirmDefault(targetSong); } - function capsuleOnConfirmDefault(cap:SongMenuItem):Void + /** + * Called when hitting ENTER to open the instrumental list. + */ + function capsuleOnOpenDefault(cap:SongMenuItem):Void + { + var targetSongId:String = cap?.songData?.songId ?? 'unknown'; + var targetSongNullable:Null = SongRegistry.instance.fetchEntry(targetSongId); + if (targetSongNullable == null) + { + FlxG.log.warn('WARN: could not find song with id (${targetSongId})'); + return; + } + var targetSong:Song = targetSongNullable; + var targetDifficultyId:String = currentDifficulty; + var targetVariation:Null = targetSong.getFirstValidVariation(targetDifficultyId, currentCharacter); + var targetLevelId:Null = cap?.songData?.levelId; + PlayStatePlaylist.campaignId = targetLevelId ?? null; + + var targetDifficulty:Null = targetSong.getDifficulty(targetDifficultyId, targetVariation); + if (targetDifficulty == null) + { + FlxG.log.warn('WARN: could not find difficulty with id (${targetDifficultyId})'); + return; + } + + trace('target difficulty: ${targetDifficultyId}'); + trace('target variation: ${targetDifficulty?.variation ?? Constants.DEFAULT_VARIATION}'); + + var baseInstrumentalId:String = targetSong.getBaseInstrumentalId(targetDifficultyId, targetDifficulty?.variation ?? Constants.DEFAULT_VARIATION) ?? ''; + var altInstrumentalIds:Array = targetSong.listAltInstrumentalIds(targetDifficultyId, + targetDifficulty?.variation ?? Constants.DEFAULT_VARIATION) ?? []; + + if (altInstrumentalIds.length > 0) + { + var instrumentalIds = [baseInstrumentalId].concat(altInstrumentalIds); + openInstrumentalList(cap, instrumentalIds); + } + else + { + trace('NO ALTS'); + capsuleOnConfirmDefault(cap); + } + } + + public function getControls():Controls + { + return controls; + } + + function openInstrumentalList(cap:SongMenuItem, instrumentalIds:Array):Void + { + busy = true; + + capsuleOptionsMenu = new CapsuleOptionsMenu(this, cap.x + 175, cap.y + 115, instrumentalIds); + capsuleOptionsMenu.cameras = [funnyCam]; + capsuleOptionsMenu.zIndex = 10000; + add(capsuleOptionsMenu); + + capsuleOptionsMenu.onConfirm = function(targetInstId:String) { + capsuleOnConfirmDefault(cap, targetInstId); + }; + } + + var capsuleOptionsMenu:Null = null; + + public function cleanupCapsuleOptionsMenu():Void + { + this.busy = false; + + if (capsuleOptionsMenu != null) + { + remove(capsuleOptionsMenu); + capsuleOptionsMenu = null; + } + } + + /** + * Called when hitting ENTER to play the song. + */ + function capsuleOnConfirmDefault(cap:SongMenuItem, ?targetInstId:String):Void { busy = true; letterSort.inputEnabled = false; @@ -1807,8 +1985,9 @@ class FreeplayState extends MusicBeatSubState return; } - var baseInstrumentalId:String = targetDifficulty?.characters?.instrumental ?? ''; - var altInstrumentalIds:Array = targetDifficulty?.characters?.altInstrumentals ?? []; + var baseInstrumentalId:String = targetSong?.getBaseInstrumentalId(targetDifficultyId, targetDifficulty.variation ?? Constants.DEFAULT_VARIATION) ?? ''; + var altInstrumentalIds:Array = targetSong?.listAltInstrumentalIds(targetDifficultyId, + targetDifficulty.variation ?? Constants.DEFAULT_VARIATION) ?? []; var targetInstId:String = baseInstrumentalId; @@ -1818,6 +1997,8 @@ class FreeplayState extends MusicBeatSubState { targetInstId = altInstrumentalIds[0]; } + + if (targetInstId == null) targetInstId = baseInstrumentalId; #end // Visual and audio effects. @@ -1827,41 +2008,9 @@ class FreeplayState extends MusicBeatSubState grpCapsules.members[curSelected].forcePosition(); grpCapsules.members[curSelected].confirm(); - // FlxTween.color(bgDad, 0.33, 0xFFFFFFFF, 0xFF555555, {ease: FlxEase.quadOut}); - FlxTween.color(pinkBack, 0.33, 0xFFFFD0D5, 0xFF171831, {ease: FlxEase.quadOut}); - orangeBackShit.visible = false; - alsoOrangeLOL.visible = false; + backingCard?.confirm(); - confirmGlow.visible = true; - confirmGlow2.visible = true; - - backingTextYeah.playAnimation("BF back card confirm raw", false, false, false, 0); - confirmGlow2.alpha = 0; - confirmGlow.alpha = 0; - - FlxTween.tween(confirmGlow2, {alpha: 0.5}, 0.33, - { - ease: FlxEase.quadOut, - onComplete: function(_) { - confirmGlow2.alpha = 0.6; - confirmGlow.alpha = 1; - confirmTextGlow.visible = true; - confirmTextGlow.alpha = 1; - FlxTween.tween(confirmTextGlow, {alpha: 0.4}, 0.5); - FlxTween.tween(confirmGlow, {alpha: 0}, 0.5); - } - }); - - // confirmGlow - - moreWays.visible = false; - funnyScroll.visible = false; - txtNuts.visible = false; - funnyScroll2.visible = false; - moreWays2.visible = false; - funnyScroll3.visible = false; - - new FlxTimer().start(1, function(tmr:FlxTimer) { + new FlxTimer().start(styleData?.getStartDelay(), function(tmr:FlxTimer) { FunkinSound.emptyPartialQueue(); Paths.setCurrentLevel(cap?.songData?.levelId); @@ -1975,10 +2124,13 @@ class FreeplayState extends MusicBeatSubState if (previewSongId == null) return; var previewSong:Null = SongRegistry.instance.fetchEntry(previewSongId); - var songDifficulty = previewSong?.getDifficulty(currentDifficulty, + var currentVariation = previewSong?.getVariationsByCharacter(currentCharacter) ?? Constants.DEFAULT_VARIATION_LIST; + var songDifficulty:Null = previewSong?.getDifficulty(currentDifficulty, previewSong?.getVariationsByCharacter(currentCharacter) ?? Constants.DEFAULT_VARIATION_LIST); - var baseInstrumentalId:String = songDifficulty?.characters?.instrumental ?? ''; - var altInstrumentalIds:Array = songDifficulty?.characters?.altInstrumentals ?? []; + + var baseInstrumentalId:String = previewSong?.getBaseInstrumentalId(currentDifficulty, songDifficulty?.variation ?? Constants.DEFAULT_VARIATION) ?? ''; + var altInstrumentalIds:Array = previewSong?.listAltInstrumentalIds(currentDifficulty, + songDifficulty?.variation ?? Constants.DEFAULT_VARIATION) ?? []; var instSuffix:String = baseInstrumentalId; @@ -2005,13 +2157,19 @@ class FreeplayState extends MusicBeatSubState partialParams: { loadPartial: true, - start: 0.05, - end: 0.25 + start: 0, + end: 0.2 }, onLoad: function() { FlxG.sound.music.fadeIn(2, 0, 0.4); } }); + + if (songDifficulty != null) + { + Conductor.instance.mapTimeChanges(songDifficulty.timeChanges); + Conductor.instance.update(FlxG.sound?.music?.time ?? 0.0); + } } } @@ -2040,14 +2198,14 @@ class DifficultySelector extends FlxSprite var parent:FreeplayState; - public function new(parent:FreeplayState, x:Float, y:Float, flipped:Bool, controls:Controls) + public function new(parent:FreeplayState, x:Float, y:Float, flipped:Bool, controls:Controls, ?styleData:FreeplayStyle = null) { super(x, y); this.parent = parent; this.controls = controls; - frames = Paths.getSparrowAtlas('freeplay/freeplaySelector'); + frames = Paths.getSparrowAtlas(styleData == null ? 'freeplay/freeplaySelector' : styleData.getSelectorAssetKey()); animation.addByPrefix('shine', 'arrow pointer loop', 24); animation.play('shine'); diff --git a/source/funkin/ui/freeplay/FreeplayStyle.hx b/source/funkin/ui/freeplay/FreeplayStyle.hx new file mode 100644 index 000000000..5ced51e1c --- /dev/null +++ b/source/funkin/ui/freeplay/FreeplayStyle.hx @@ -0,0 +1,121 @@ +package funkin.ui.freeplay; + +import funkin.data.freeplay.style.FreeplayStyleData; +import funkin.data.freeplay.style.FreeplayStyleRegistry; +import funkin.data.animation.AnimationData; +import funkin.data.IRegistryEntry; +import flixel.graphics.FlxGraphic; +import flixel.util.FlxColor; + +/** + * A class representing the data for a style of the Freeplay menu. + */ +class FreeplayStyle implements IRegistryEntry +{ + /** + * The internal ID for this freeplay style. + */ + public final id:String; + + /** + * The full data for a freeplay style. + */ + public final _data:FreeplayStyleData; + + public function new(id:String) + { + this.id = id; + this._data = _fetchData(id); + + if (_data == null) + { + throw 'Could not parse album data for id: $id'; + } + } + + /** + * Get the background art as a graphic, ready to apply to a sprite. + * @return The built graphic + */ + public function getBgAssetGraphic():FlxGraphic + { + return FlxG.bitmap.add(Paths.image(getBgAssetKey())); + } + + /** + * Get the asset key for the background. + * @return The asset key + */ + public function getBgAssetKey():String + { + return _data.bgAsset; + } + + /** + * Get the asset key for the background. + * @return The asset key + */ + public function getSelectorAssetKey():String + { + return _data.selectorAsset; + } + + /** + * Get the asset key for the number assets. + * @return The asset key + */ + public function getCapsuleAssetKey():String + { + return _data.capsuleAsset; + } + + /** + * Get the asset key for the capsule art. + * @return The asset key + */ + public function getNumbersAssetKey():String + { + return _data.numbersAsset; + } + + /** + * Return the deselected color of the text outline + * for freeplay capsules. + * @return The deselected color + */ + public function getCapsuleDeselCol():FlxColor + { + return FlxColor.fromString(_data.capsuleTextColors[0]); + } + + /** + * Return the song selection transition delay. + * @return The start delay + */ + public function getStartDelay():Float + { + return _data.startDelay; + } + + public function toString():String + { + return 'Style($id)'; + } + + /** + * Return the selected color of the text outline + * for freeplay capsules. + * @return The selected color + */ + public function getCapsuleSelCol():FlxColor + { + return FlxColor.fromString(_data.capsuleTextColors[1]); + } + + public function destroy():Void {} + + static function _fetchData(id:String):Null + { + return FreeplayStyleRegistry.instance.parseEntryDataWithMigration(id, FreeplayStyleRegistry.instance.fetchEntryVersion(id)); + } +} diff --git a/source/funkin/ui/freeplay/ScriptedFreeplayStyle.hx b/source/funkin/ui/freeplay/ScriptedFreeplayStyle.hx new file mode 100644 index 000000000..b7013a6b2 --- /dev/null +++ b/source/funkin/ui/freeplay/ScriptedFreeplayStyle.hx @@ -0,0 +1,9 @@ +package funkin.ui.freeplay; + +/** + * A script that can be tied to a Freeplay style. + * Create a scripted class that extends FreeplayStyle to use this. + * This allows you to customize how a specific style works. + */ +@:hscriptClass +class ScriptedFreeplayStyle extends funkin.ui.freeplay.FreeplayStyle implements polymod.hscript.HScriptedClass {} diff --git a/source/funkin/ui/freeplay/SongMenuItem.hx b/source/funkin/ui/freeplay/SongMenuItem.hx index b4409d377..11ca44d54 100644 --- a/source/funkin/ui/freeplay/SongMenuItem.hx +++ b/source/funkin/ui/freeplay/SongMenuItem.hx @@ -88,7 +88,7 @@ class SongMenuItem extends FlxSpriteGroup super(x, y); capsule = new FlxSprite(); - capsule.frames = Paths.getSparrowAtlas('freeplay/freeplayCapsule'); + capsule.frames = Paths.getSparrowAtlas('freeplay/freeplayCapsule/capsule/freeplayCapsule'); capsule.animation.addByPrefix('selected', 'mp3 capsule w backing0', 24); capsule.animation.addByPrefix('unselected', 'mp3 capsule w backing NOT SELECTED', 24); // capsule.animation @@ -500,12 +500,23 @@ class SongMenuItem extends FlxSpriteGroup updateSelected(); } - public function init(?x:Float, ?y:Float, songData:Null):Void + public function init(?x:Float, ?y:Float, songData:Null, ?styleData:FreeplayStyle = null):Void { if (x != null) this.x = x; if (y != null) this.y = y; this.songData = songData; + // im so mad i have to do this but im pretty sure with the capsules recycling i cant call the new function properly :/ + // if thats possible someone Please change the new function to be something like + // capsule.frames = Paths.getSparrowAtlas(styleData == null ? 'freeplay/freeplayCapsule/capsule/freeplayCapsule' : styleData.getCapsuleAssetKey()); thank u luv u + if (styleData != null) + { + capsule.frames = Paths.getSparrowAtlas(styleData.getCapsuleAssetKey()); + capsule.animation.addByPrefix('selected', 'mp3 capsule w backing0', 24); + capsule.animation.addByPrefix('unselected', 'mp3 capsule w backing NOT SELECTED', 24); + songText.applyStyle(styleData); + } + // Update capsule text. songText.text = songData?.songName ?? 'Random'; // Update capsule character. @@ -727,7 +738,7 @@ class FreeplayRank extends FlxSprite switch (val) { case SHIT: - // offset.x -= 1; + // offset.x -= 1; case GOOD: // offset.x -= 1; offset.y -= 8; @@ -735,11 +746,11 @@ class FreeplayRank extends FlxSprite // offset.x -= 1; offset.y -= 8; case EXCELLENT: - // offset.y += 5; + // offset.y += 5; case PERFECT: - // offset.y += 5; + // offset.y += 5; case PERFECT_GOLD: - // offset.y += 5; + // offset.y += 5; default: centerOffsets(false); this.visible = false; @@ -796,9 +807,9 @@ class CapsuleNumber extends FlxSprite case 6: case 4: - // offset.y += 5; + // offset.y += 5; case 9: - // offset.y += 5; + // offset.y += 5; default: centerOffsets(false); } diff --git a/source/funkin/ui/freeplay/backcards/BackingCard.hx b/source/funkin/ui/freeplay/backcards/BackingCard.hx new file mode 100644 index 000000000..bb662cc8d --- /dev/null +++ b/source/funkin/ui/freeplay/backcards/BackingCard.hx @@ -0,0 +1,249 @@ +package funkin.ui.freeplay.backcards; + +import funkin.ui.freeplay.FreeplayState; +import flixel.FlxCamera; +import flixel.FlxSprite; +import flixel.group.FlxGroup; +import flixel.group.FlxGroup.FlxTypedGroup; +import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup; +import flixel.math.FlxAngle; +import flixel.math.FlxPoint; +import flixel.text.FlxText; +import flixel.tweens.FlxEase; +import flixel.tweens.FlxTween; +import flixel.util.FlxColor; +import flixel.util.FlxSpriteUtil; +import flixel.util.FlxTimer; +import funkin.graphics.adobeanimate.FlxAtlasSprite; +import funkin.graphics.FunkinSprite; +import funkin.ui.freeplay.charselect.PlayableCharacter; +import funkin.ui.MusicBeatSubState; +import lime.utils.Assets; +import openfl.display.BlendMode; +import flixel.group.FlxSpriteGroup; + +/** + * A class for the backing cards so they dont have to be part of freeplayState...... + */ +class BackingCard extends FlxSpriteGroup +{ + public var backingTextYeah:FlxAtlasSprite; + public var orangeBackShit:FunkinSprite; + public var alsoOrangeLOL:FunkinSprite; + public var pinkBack:FunkinSprite; + public var confirmGlow:FlxSprite; + public var confirmGlow2:FlxSprite; + public var confirmTextGlow:FlxSprite; + public var cardGlow:FlxSprite; + + var _exitMovers:Null; + var _exitMoversCharSel:Null; + + public var instance:FreeplayState; + + public function new(currentCharacter:PlayableCharacter, ?_instance:FreeplayState) + { + super(); + + if (_instance != null) instance = _instance; + + cardGlow = new FlxSprite(-30, -30).loadGraphic(Paths.image('freeplay/cardGlow')); + confirmGlow = new FlxSprite(-30, 240).loadGraphic(Paths.image('freeplay/confirmGlow')); + confirmTextGlow = new FlxSprite(-8, 115).loadGraphic(Paths.image('freeplay/glowingText')); + pinkBack = FunkinSprite.create('freeplay/pinkBack'); + orangeBackShit = new FunkinSprite(84, 440).makeSolidColor(Std.int(pinkBack.width), 75, 0xFFFEDA00); + alsoOrangeLOL = new FunkinSprite(0, orangeBackShit.y).makeSolidColor(100, Std.int(orangeBackShit.height), 0xFFFFD400); + confirmGlow2 = new FlxSprite(confirmGlow.x, confirmGlow.y).loadGraphic(Paths.image('freeplay/confirmGlow2')); + backingTextYeah = new FlxAtlasSprite(640, 370, Paths.animateAtlas("freeplay/backing-text-yeah"), + { + FrameRate: 24.0, + Reversed: false, + // ?OnComplete:Void -> Void, + ShowPivot: false, + Antialiasing: true, + ScrollFactor: new FlxPoint(1, 1), + }); + + pinkBack.color = 0xFFFFD4E9; // sets it to pink! + pinkBack.x -= pinkBack.width; + } + + /** + * Apply exit movers for the pieces of the backing card. + * @param exitMovers The exit movers to apply. + */ + public function applyExitMovers(?exitMovers:FreeplayState.ExitMoverData, ?exitMoversCharSel:FreeplayState.ExitMoverData):Void + { + if (exitMovers == null) + { + exitMovers = _exitMovers; + } + else + { + _exitMovers = exitMovers; + } + + if (exitMovers == null) return; + + if (exitMoversCharSel == null) + { + exitMoversCharSel = _exitMoversCharSel; + } + else + { + _exitMoversCharSel = exitMoversCharSel; + } + + if (exitMoversCharSel == null) return; + + exitMovers.set([pinkBack, orangeBackShit, alsoOrangeLOL], + { + x: -pinkBack.width, + y: pinkBack.y, + speed: 0.4, + wait: 0 + }); + + exitMoversCharSel.set([pinkBack], + { + y: -100, + speed: 0.8, + wait: 0.1 + }); + + exitMoversCharSel.set([orangeBackShit, alsoOrangeLOL], + { + y: -40, + speed: 0.8, + wait: 0.1 + }); + } + + /** + * Helper function to snap the back of the card to its final position. + * Used when returning from character select, as we dont want to play the full animation of everything sliding in. + */ + public function skipIntroTween():Void + { + FlxTween.cancelTweensOf(pinkBack); + pinkBack.x = 0; + } + + /** + * Called in create. Adds sprites and tweens. + */ + public function init():Void + { + FlxTween.tween(pinkBack, {x: 0}, 0.6, {ease: FlxEase.quartOut}); + add(pinkBack); + + add(orangeBackShit); + + add(alsoOrangeLOL); + + FlxSpriteUtil.alphaMaskFlxSprite(orangeBackShit, pinkBack, orangeBackShit); + orangeBackShit.visible = false; + alsoOrangeLOL.visible = false; + + confirmTextGlow.blend = BlendMode.ADD; + confirmTextGlow.visible = false; + + confirmGlow.blend = BlendMode.ADD; + + confirmGlow.visible = false; + confirmGlow2.visible = false; + + add(confirmGlow2); + add(confirmGlow); + + add(confirmTextGlow); + + add(backingTextYeah); + + cardGlow.blend = BlendMode.ADD; + cardGlow.visible = false; + + add(cardGlow); + } + + /** + * Called after the dj finishes their start animation. + */ + public function introDone():Void + { + pinkBack.color = 0xFFFFD863; + orangeBackShit.visible = true; + alsoOrangeLOL.visible = true; + cardGlow.visible = true; + FlxTween.tween(cardGlow, {alpha: 0, "scale.x": 1.2, "scale.y": 1.2}, 0.45, {ease: FlxEase.sineOut}); + } + + /** + * Called when selecting a song. + */ + public function confirm():Void + { + FlxTween.color(pinkBack, 0.33, 0xFFFFD0D5, 0xFF171831, {ease: FlxEase.quadOut}); + orangeBackShit.visible = false; + alsoOrangeLOL.visible = false; + + confirmGlow.visible = true; + confirmGlow2.visible = true; + + backingTextYeah.anim.play(""); + confirmGlow2.alpha = 0; + confirmGlow.alpha = 0; + + FlxTween.color(instance.bgDad, 0.5, 0xFFA8A8A8, 0xFF646464, + { + onUpdate: function(_) { + instance.angleMaskShader.extraColor = instance.bgDad.color; + } + }); + FlxTween.tween(confirmGlow2, {alpha: 0.5}, 0.33, + { + ease: FlxEase.quadOut, + onComplete: function(_) { + confirmGlow2.alpha = 0.6; + confirmGlow.alpha = 1; + confirmTextGlow.visible = true; + confirmTextGlow.alpha = 1; + FlxTween.tween(confirmTextGlow, {alpha: 0.4}, 0.5); + FlxTween.tween(confirmGlow, {alpha: 0}, 0.5); + FlxTween.color(instance.bgDad, 2, 0xFFCDCDCD, 0xFF555555, + { + ease: FlxEase.expoOut, + onUpdate: function(_) { + instance.angleMaskShader.extraColor = instance.bgDad.color; + } + }); + } + }); + } + + /** + * Called when entering character select, does nothing by default. + */ + public function enterCharSel():Void {} + + /** + * Called on each beat in freeplay state. + */ + public function beatHit():Void {} + + /** + * Called when exiting the freeplay menu. + */ + public function disappear():Void + { + FlxTween.color(pinkBack, 0.25, 0xFFFFD863, 0xFFFFD0D5, {ease: FlxEase.quadOut}); + + cardGlow.visible = true; + cardGlow.alpha = 1; + cardGlow.scale.set(1, 1); + FlxTween.tween(cardGlow, {alpha: 0, "scale.x": 1.2, "scale.y": 1.2}, 0.25, {ease: FlxEase.sineOut}); + + orangeBackShit.visible = false; + alsoOrangeLOL.visible = false; + } +} diff --git a/source/funkin/ui/freeplay/backcards/BoyfriendCard.hx b/source/funkin/ui/freeplay/backcards/BoyfriendCard.hx new file mode 100644 index 000000000..597fd1a34 --- /dev/null +++ b/source/funkin/ui/freeplay/backcards/BoyfriendCard.hx @@ -0,0 +1,238 @@ +package funkin.ui.freeplay.backcards; + +import funkin.ui.freeplay.FreeplayState; +import flixel.FlxCamera; +import flixel.FlxSprite; +import flixel.group.FlxGroup; +import flixel.group.FlxGroup.FlxTypedGroup; +import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup; +import flixel.math.FlxAngle; +import flixel.math.FlxPoint; +import flixel.text.FlxText; +import flixel.tweens.FlxEase; +import flixel.tweens.FlxTween; +import flixel.util.FlxColor; +import flixel.util.FlxSpriteUtil; +import flixel.util.FlxTimer; +import funkin.graphics.adobeanimate.FlxAtlasSprite; +import funkin.graphics.FunkinSprite; +import funkin.ui.freeplay.charselect.PlayableCharacter; +import funkin.ui.MusicBeatSubState; +import lime.utils.Assets; +import openfl.display.BlendMode; +import flixel.group.FlxSpriteGroup; + +class BoyfriendCard extends BackingCard +{ + public var moreWays:BGScrollingText; + public var funnyScroll:BGScrollingText; + public var txtNuts:BGScrollingText; + public var funnyScroll2:BGScrollingText; + public var moreWays2:BGScrollingText; + public var funnyScroll3:BGScrollingText; + + var glow:FlxSprite; + var glowDark:FlxSprite; + + public override function applyExitMovers(?exitMovers:FreeplayState.ExitMoverData, ?exitMoversCharSel:FreeplayState.ExitMoverData):Void + { + super.applyExitMovers(exitMovers, exitMoversCharSel); + if (exitMovers == null || exitMoversCharSel == null) return; + exitMovers.set([moreWays], + { + x: FlxG.width * 2, + speed: 0.4, + }); + exitMovers.set([funnyScroll], + { + x: -funnyScroll.width * 2, + y: funnyScroll.y, + speed: 0.4, + wait: 0 + }); + exitMovers.set([txtNuts], + { + x: FlxG.width * 2, + speed: 0.4, + }); + exitMovers.set([funnyScroll2], + { + x: -funnyScroll2.width * 2, + speed: 0.5, + }); + exitMovers.set([moreWays2], + { + x: FlxG.width * 2, + speed: 0.4 + }); + exitMovers.set([funnyScroll3], + { + x: -funnyScroll3.width * 2, + speed: 0.3 + }); + + exitMoversCharSel.set([moreWays, funnyScroll, txtNuts, funnyScroll2, moreWays2, funnyScroll3], + { + y: -60, + speed: 0.8, + wait: 0.1 + }); + } + + public override function enterCharSel():Void + { + FlxTween.tween(funnyScroll, {speed: 0}, 0.8, {ease: FlxEase.sineIn}); + FlxTween.tween(funnyScroll2, {speed: 0}, 0.8, {ease: FlxEase.sineIn}); + FlxTween.tween(moreWays, {speed: 0}, 0.8, {ease: FlxEase.sineIn}); + FlxTween.tween(moreWays2, {speed: 0}, 0.8, {ease: FlxEase.sineIn}); + FlxTween.tween(txtNuts, {speed: 0}, 0.8, {ease: FlxEase.sineIn}); + FlxTween.tween(funnyScroll3, {speed: 0}, 0.8, {ease: FlxEase.sineIn}); + } + + public override function new(currentCharacter:PlayableCharacter) + { + super(currentCharacter); + + funnyScroll = new BGScrollingText(0, 220, currentCharacter.getFreeplayDJText(1), FlxG.width / 2, false, 60); + funnyScroll2 = new BGScrollingText(0, 335, currentCharacter.getFreeplayDJText(1), FlxG.width / 2, false, 60); + moreWays = new BGScrollingText(0, 160, currentCharacter.getFreeplayDJText(2), FlxG.width, true, 43); + moreWays2 = new BGScrollingText(0, 397, currentCharacter.getFreeplayDJText(2), FlxG.width, true, 43); + txtNuts = new BGScrollingText(0, 285, currentCharacter.getFreeplayDJText(3), FlxG.width / 2, true, 43); + funnyScroll3 = new BGScrollingText(0, orangeBackShit.y + 10, currentCharacter.getFreeplayDJText(1), FlxG.width / 2, 60); + } + + public override function init():Void + { + FlxTween.tween(pinkBack, {x: 0}, 0.6, {ease: FlxEase.quartOut}); + add(pinkBack); + + add(orangeBackShit); + + add(alsoOrangeLOL); + + FlxSpriteUtil.alphaMaskFlxSprite(orangeBackShit, pinkBack, orangeBackShit); + orangeBackShit.visible = false; + alsoOrangeLOL.visible = false; + + confirmTextGlow.blend = BlendMode.ADD; + confirmTextGlow.visible = false; + + confirmGlow.blend = BlendMode.ADD; + + confirmGlow.visible = false; + confirmGlow2.visible = false; + + add(confirmGlow2); + add(confirmGlow); + + add(confirmTextGlow); + + add(backingTextYeah); + + cardGlow.blend = BlendMode.ADD; + cardGlow.visible = false; + + moreWays.visible = false; + funnyScroll.visible = false; + txtNuts.visible = false; + funnyScroll2.visible = false; + moreWays2.visible = false; + funnyScroll3.visible = false; + + moreWays.funnyColor = 0xFFFFF383; + moreWays.speed = 6.8; + add(moreWays); + + funnyScroll.funnyColor = 0xFFFF9963; + funnyScroll.speed = -3.8; + add(funnyScroll); + + txtNuts.speed = 3.5; + add(txtNuts); + + funnyScroll2.funnyColor = 0xFFFF9963; + funnyScroll2.speed = -3.8; + add(funnyScroll2); + + moreWays2.funnyColor = 0xFFFFF383; + moreWays2.speed = 6.8; + add(moreWays2); + + funnyScroll3.funnyColor = 0xFFFEA400; + funnyScroll3.speed = -3.8; + add(funnyScroll3); + + glowDark = new FlxSprite(-300, 330).loadGraphic(Paths.image('freeplay/beatglow')); + glowDark.blend = BlendMode.MULTIPLY; + add(glowDark); + + glow = new FlxSprite(-300, 330).loadGraphic(Paths.image('freeplay/beatglow')); + glow.blend = BlendMode.ADD; + add(glow); + + glowDark.visible = false; + glow.visible = false; + + add(cardGlow); + } + + var beatFreq:Int = 1; + var beatFreqList:Array = [1,2,4,8]; + + public override function beatHit():Void { + // increases the amount of beats that need to go by to pulse the glow because itd flash like craazy at high bpms..... + beatFreq = beatFreqList[Math.floor(Conductor.instance.bpm/140)]; + + if(Conductor.instance.currentBeat % beatFreq != 0) return; + FlxTween.cancelTweensOf(glow); + FlxTween.cancelTweensOf(glowDark); + + glow.alpha = 0.8; + FlxTween.tween(glow, {alpha: 0}, 16/24, {ease: FlxEase.quartOut}); + glowDark.alpha = 0; + FlxTween.tween(glowDark, {alpha: 0.6}, 18/24, {ease: FlxEase.quartOut}); + } + + public override function introDone():Void + { + super.introDone(); + moreWays.visible = true; + funnyScroll.visible = true; + txtNuts.visible = true; + funnyScroll2.visible = true; + moreWays2.visible = true; + funnyScroll3.visible = true; + // grpTxtScrolls.visible = true; + glowDark.visible = true; + glow.visible = true; + } + + public override function confirm():Void + { + super.confirm(); + // FlxTween.color(bgDad, 0.33, 0xFFFFFFFF, 0xFF555555, {ease: FlxEase.quadOut}); + + moreWays.visible = false; + funnyScroll.visible = false; + txtNuts.visible = false; + funnyScroll2.visible = false; + moreWays2.visible = false; + funnyScroll3.visible = false; + glowDark.visible = false; + glow.visible = false; + } + + public override function disappear():Void + { + super.disappear(); + + moreWays.visible = false; + funnyScroll.visible = false; + txtNuts.visible = false; + funnyScroll2.visible = false; + moreWays2.visible = false; + funnyScroll3.visible = false; + glowDark.visible = false; + glow.visible = false; + } +} diff --git a/source/funkin/ui/freeplay/backcards/NewCharacterCard.hx b/source/funkin/ui/freeplay/backcards/NewCharacterCard.hx new file mode 100644 index 000000000..a44ff88a6 --- /dev/null +++ b/source/funkin/ui/freeplay/backcards/NewCharacterCard.hx @@ -0,0 +1,278 @@ +package funkin.ui.freeplay.backcards; + +import funkin.ui.freeplay.FreeplayState; +import flash.display.BitmapData; +import flixel.FlxCamera; +import flixel.math.FlxMath; +import flixel.FlxSprite; +import flixel.group.FlxGroup; +import flixel.group.FlxGroup.FlxTypedGroup; +import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup; +import flixel.math.FlxAngle; +import flixel.math.FlxPoint; +import flixel.text.FlxText; +import flixel.tweens.FlxEase; +import flixel.tweens.FlxTween; +import flixel.util.FlxColor; +import flixel.util.FlxSpriteUtil; +import flixel.util.FlxTimer; +import funkin.graphics.adobeanimate.FlxAtlasSprite; +import funkin.graphics.FunkinSprite; +import funkin.ui.freeplay.charselect.PlayableCharacter; +import funkin.ui.MusicBeatSubState; +import lime.utils.Assets; +import openfl.display.BlendMode; +import flixel.group.FlxSpriteGroup; +import funkin.graphics.shaders.AdjustColorShader; +import flixel.addons.display.FlxTiledSprite; +import flixel.addons.display.FlxBackdrop; + +class NewCharacterCard extends BackingCard +{ + var confirmAtlas:FlxAtlasSprite; + + var darkBg:FlxSprite; + var lightLayer:FlxSprite; + var multiply1:FlxSprite; + var multiply2:FlxSprite; + var lightLayer2:FlxSprite; + var lightLayer3:FlxSprite; + var yellow:FlxSprite; + var multiplyBar:FlxSprite; + + var bruh:FlxSprite; + + public var friendFoe:BGScrollingText; + public var newUnlock1:BGScrollingText; + public var waiting:BGScrollingText; + public var newUnlock2:BGScrollingText; + public var friendFoe2:BGScrollingText; + public var newUnlock3:BGScrollingText; + + public override function applyExitMovers(?exitMovers:FreeplayState.ExitMoverData, ?exitMoversCharSel:FreeplayState.ExitMoverData):Void + { + super.applyExitMovers(exitMovers, exitMoversCharSel); + if (exitMovers == null || exitMoversCharSel == null) return; + exitMovers.set([friendFoe], + { + x: FlxG.width * 2, + speed: 0.4, + }); + exitMovers.set([newUnlock1], + { + x: -newUnlock1.width * 2, + y: newUnlock1.y, + speed: 0.4, + wait: 0 + }); + exitMovers.set([waiting], + { + x: FlxG.width * 2, + speed: 0.4, + }); + exitMovers.set([newUnlock2], + { + x: -newUnlock2.width * 2, + speed: 0.5, + }); + exitMovers.set([friendFoe2], + { + x: FlxG.width * 2, + speed: 0.4 + }); + exitMovers.set([newUnlock3], + { + x: -newUnlock3.width * 2, + speed: 0.3 + }); + + exitMoversCharSel.set([friendFoe, newUnlock1, waiting, newUnlock2, friendFoe2, newUnlock3, multiplyBar], { + y: -60, + speed: 0.8, + wait: 0.1 + }); + } + + public override function introDone():Void + { + // pinkBack.color = 0xFFFFD863; + + darkBg.visible = true; + friendFoe.visible = true; + newUnlock1.visible = true; + waiting.visible = true; + newUnlock2.visible = true; + friendFoe2.visible = true; + newUnlock3.visible = true; + multiplyBar.visible = true; + lightLayer.visible = true; + multiply1.visible = true; + multiply2.visible = true; + lightLayer2.visible = true; + yellow.visible = true; + lightLayer3.visible = true; + + cardGlow.visible = true; + FlxTween.tween(cardGlow, {alpha: 0, "scale.x": 1.2, "scale.y": 1.2}, 0.45, {ease: FlxEase.sineOut}); + } + + public override function enterCharSel():Void + { + FlxTween.tween(friendFoe, {speed: 0}, 0.8, {ease: FlxEase.sineIn}); + FlxTween.tween(newUnlock1, {speed: 0}, 0.8, {ease: FlxEase.sineIn}); + FlxTween.tween(waiting, {speed: 0}, 0.8, {ease: FlxEase.sineIn}); + FlxTween.tween(newUnlock2, {speed: 0}, 0.8, {ease: FlxEase.sineIn}); + FlxTween.tween(friendFoe2, {speed: 0}, 0.8, {ease: FlxEase.sineIn}); + FlxTween.tween(newUnlock3, {speed: 0}, 0.8, {ease: FlxEase.sineIn}); + } + + public override function init():Void + { + FlxTween.tween(pinkBack, {x: 0}, 0.6, {ease: FlxEase.quartOut}); + add(pinkBack); + + confirmTextGlow.blend = BlendMode.ADD; + confirmTextGlow.visible = false; + + confirmGlow.blend = BlendMode.ADD; + + confirmGlow.visible = false; + confirmGlow2.visible = false; + + friendFoe = new BGScrollingText(0, 163, "COULD IT BE A NEW FRIEND? OR FOE??", FlxG.width, true, 43); + newUnlock1 = new BGScrollingText(-440, 215, 'NEW UNLOCK!', FlxG.width / 2, true, 80); + waiting = new BGScrollingText(0, 286, "SOMEONE'S WAITING!", FlxG.width / 2, true, 43); + newUnlock2 = new BGScrollingText(-220, 331, 'NEW UNLOCK!', FlxG.width / 2, true, 80); + friendFoe2 = new BGScrollingText(0, 402, 'COULD IT BE A NEW FRIEND? OR FOE??', FlxG.width, true, 43); + newUnlock3 = new BGScrollingText(0, 458, 'NEW UNLOCK!', FlxG.width / 2, true, 80); + + darkBg = new FlxSprite(0, 0).loadGraphic(Paths.image('freeplay/backingCards/newCharacter/darkback')); + add(darkBg); + + friendFoe.funnyColor = 0xFF139376; + friendFoe.speed = -4; + add(friendFoe); + + newUnlock1.funnyColor = 0xFF99BDF2; + newUnlock1.speed = 2; + add(newUnlock1); + + waiting.funnyColor = 0xFF40EA84; + waiting.speed = -2; + add(waiting); + + newUnlock2.funnyColor = 0xFF99BDF2; + newUnlock2.speed = 2; + add(newUnlock2); + + friendFoe2.funnyColor = 0xFF139376; + friendFoe2.speed = -4; + add(friendFoe2); + + newUnlock3.funnyColor = 0xFF99BDF2; + newUnlock3.speed = 2; + add(newUnlock3); + + multiplyBar = new FlxSprite(-10, 440).loadGraphic(Paths.image('freeplay/backingCards/newCharacter/multiplyBar')); + multiplyBar.blend = BlendMode.MULTIPLY; + add(multiplyBar); + + lightLayer = new FlxSprite(-360, 230).loadGraphic(Paths.image('freeplay/backingCards/newCharacter/orange gradient')); + lightLayer.blend = BlendMode.ADD; + add(lightLayer); + + multiply1 = new FlxSprite(-15, -125).loadGraphic(Paths.image('freeplay/backingCards/newCharacter/red')); + multiply1.blend = BlendMode.MULTIPLY; + add(multiply1); + + multiply2 = new FlxSprite(-15, -125).loadGraphic(Paths.image('freeplay/backingCards/newCharacter/red')); + multiply2.blend = BlendMode.MULTIPLY; + add(multiply2); + + lightLayer2 = new FlxSprite(-360, 230).loadGraphic(Paths.image('freeplay/backingCards/newCharacter/orange gradient')); + lightLayer2.blend = BlendMode.ADD; + add(lightLayer2); + + yellow = new FlxSprite(0, 0).loadGraphic(Paths.image('freeplay/backingCards/newCharacter/yellow bg piece')); + yellow.blend = BlendMode.MULTIPLY; + add(yellow); + + lightLayer3 = new FlxSprite(-360, 290).loadGraphic(Paths.image('freeplay/backingCards/newCharacter/red gradient')); + lightLayer3.blend = BlendMode.ADD; + add(lightLayer3); + + cardGlow.blend = BlendMode.ADD; + cardGlow.visible = false; + + add(cardGlow); + + darkBg.visible = false; + friendFoe.visible = false; + newUnlock1.visible = false; + waiting.visible = false; + newUnlock2.visible = false; + friendFoe2.visible = false; + newUnlock3.visible = false; + multiplyBar.visible = false; + lightLayer.visible = false; + multiply1.visible = false; + multiply2.visible = false; + lightLayer2.visible = false; + yellow.visible = false; + lightLayer3.visible = false; + } + + var _timer:Float = 0; + + override public function update(elapsed:Float):Void + { + super.update(elapsed); + + _timer += elapsed * 2; + var sinTest:Float = (Math.sin(_timer) + 1) / 2; + lightLayer.alpha = FlxMath.lerp(0.4, 1, sinTest); + lightLayer2.alpha = FlxMath.lerp(0.2, 0.5, sinTest); + lightLayer3.alpha = FlxMath.lerp(0.1, 0.7, sinTest); + + multiply1.alpha = FlxMath.lerp(1, 0.21, sinTest); + multiply2.alpha = FlxMath.lerp(1, 0.21, sinTest); + + yellow.alpha = FlxMath.lerp(0.2, 0.72, sinTest); + + if (instance != null) + { + instance.angleMaskShader.extraColor = FlxColor.interpolate(0xFF2E2E46, 0xFF60607B, sinTest); + } + } + + public override function disappear():Void + { + FlxTween.color(pinkBack, 0.25, 0xFF05020E, 0xFFFFD0D5, {ease: FlxEase.quadOut}); + + darkBg.visible = false; + friendFoe.visible = false; + newUnlock1.visible = false; + waiting.visible = false; + newUnlock2.visible = false; + friendFoe2.visible = false; + newUnlock3.visible = false; + multiplyBar.visible = false; + lightLayer.visible = false; + multiply1.visible = false; + multiply2.visible = false; + lightLayer2.visible = false; + yellow.visible = false; + lightLayer3.visible = false; + + cardGlow.visible = true; + cardGlow.alpha = 1; + cardGlow.scale.set(1, 1); + FlxTween.tween(cardGlow, {alpha: 0, "scale.x": 1.2, "scale.y": 1.2}, 0.25, {ease: FlxEase.sineOut}); + } + + override public function confirm():Void + { + // confirmAtlas.visible = true; + // confirmAtlas.anim.play(""); + } +} diff --git a/source/funkin/ui/freeplay/backcards/PicoCard.hx b/source/funkin/ui/freeplay/backcards/PicoCard.hx new file mode 100644 index 000000000..f5db1ccc3 --- /dev/null +++ b/source/funkin/ui/freeplay/backcards/PicoCard.hx @@ -0,0 +1,314 @@ +package funkin.ui.freeplay.backcards; + +import funkin.ui.freeplay.FreeplayState; +import flash.display.BitmapData; +import flixel.FlxCamera; +import flixel.math.FlxMath; +import flixel.FlxSprite; +import flixel.group.FlxGroup; +import flixel.group.FlxGroup.FlxTypedGroup; +import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup; +import flixel.math.FlxAngle; +import flixel.math.FlxPoint; +import flixel.text.FlxText; +import flixel.tweens.FlxEase; +import flixel.tweens.FlxTween; +import flixel.util.FlxColor; +import flixel.util.FlxSpriteUtil; +import flixel.util.FlxTimer; +import funkin.graphics.adobeanimate.FlxAtlasSprite; +import funkin.graphics.FunkinSprite; +import funkin.ui.freeplay.charselect.PlayableCharacter; +import funkin.ui.MusicBeatSubState; +import lime.utils.Assets; +import openfl.display.BlendMode; +import flixel.group.FlxSpriteGroup; +import funkin.graphics.shaders.AdjustColorShader; +import flixel.addons.display.FlxTiledSprite; +import flixel.addons.display.FlxBackdrop; + +class PicoCard extends BackingCard +{ + var scrollBack:FlxBackdrop; + var scrollLower:FlxBackdrop; + var scrollTop:FlxBackdrop; + var scrollMiddle:FlxBackdrop; + + var glow:FlxSprite; + var glowDark:FlxSprite; + var blueBar:FlxSprite; + + var confirmAtlas:FlxAtlasSprite; + + public override function enterCharSel():Void + { + FlxTween.tween(scrollBack.velocity, {x: 0}, 0.8, {ease: FlxEase.sineIn}); + FlxTween.tween(scrollLower.velocity, {x: 0}, 0.8, {ease: FlxEase.sineIn}); + FlxTween.tween(scrollTop.velocity, {x: 0}, 0.8, {ease: FlxEase.sineIn}); + FlxTween.tween(scrollMiddle.velocity, {x: 0}, 0.8, {ease: FlxEase.sineIn}); + } + + public override function applyExitMovers(?exitMovers:FreeplayState.ExitMoverData, ?exitMoversCharSel:FreeplayState.ExitMoverData):Void + { + super.applyExitMovers(exitMovers, exitMoversCharSel); + if (exitMovers == null || exitMoversCharSel == null) return; + + exitMoversCharSel.set([scrollTop], + { + y: -90, + speed: 0.8, + wait: 0.1 + }); + + exitMoversCharSel.set([scrollMiddle], + { + y: -80, + speed: 0.8, + wait: 0.1 + }); + + exitMoversCharSel.set([blueBar], + { + y: -70, + speed: 0.8, + wait: 0.1 + }); + + exitMoversCharSel.set([scrollLower], + { + y: -60, + speed: 0.8, + wait: 0.1 + }); + + exitMoversCharSel.set([scrollBack], + { + y: -50, + speed: 0.8, + wait: 0.1 + }); + } + + public override function init():Void + { + FlxTween.tween(pinkBack, {x: 0}, 0.6, {ease: FlxEase.quartOut}); + add(pinkBack); + + confirmTextGlow.blend = BlendMode.ADD; + confirmTextGlow.visible = false; + + confirmGlow.blend = BlendMode.ADD; + + confirmGlow.visible = false; + confirmGlow2.visible = false; + + scrollBack = new FlxBackdrop(Paths.image('freeplay/backingCards/pico/lowerLoop'), X, 20); + scrollBack.setPosition(0, 200); + scrollBack.flipX = true; + scrollBack.alpha = 0.39; + scrollBack.velocity.x = 110; + add(scrollBack); + + scrollLower = new FlxBackdrop(Paths.image('freeplay/backingCards/pico/lowerLoop'), X, 20); + scrollLower.setPosition(0, 406); + scrollLower.velocity.x = -110; + add(scrollLower); + + blueBar = new FlxSprite(0, 239).loadGraphic(Paths.image('freeplay/backingCards/pico/blueBar')); + blueBar.blend = BlendMode.MULTIPLY; + blueBar.alpha = 0.4; + add(blueBar); + + scrollTop = new FlxBackdrop(null, X, 20); + scrollTop.setPosition(0, 80); + scrollTop.velocity.x = -220; + + scrollTop.frames = Paths.getSparrowAtlas('freeplay/backingCards/pico/topLoop'); + scrollTop.animation.addByPrefix('uzi', 'uzi info', 24, false); + scrollTop.animation.addByPrefix('sniper', 'sniper info', 24, false); + scrollTop.animation.addByPrefix('rocket launcher', 'rocket launcher info', 24, false); + scrollTop.animation.addByPrefix('rifle', 'rifle info', 24, false); + scrollTop.animation.addByPrefix('base', 'base', 24, false); + scrollTop.animation.play('base'); + + add(scrollTop); + + scrollMiddle = new FlxBackdrop(Paths.image('freeplay/backingCards/pico/middleLoop'), X, 15); + scrollMiddle.setPosition(0, 346); + add(scrollMiddle); + scrollMiddle.velocity.x = 220; + + glowDark = new FlxSprite(-300, 330).loadGraphic(Paths.image('freeplay/backingCards/pico/glow')); + glowDark.blend = BlendMode.MULTIPLY; + add(glowDark); + + glow = new FlxSprite(-300, 330).loadGraphic(Paths.image('freeplay/backingCards/pico/glow')); + glow.blend = BlendMode.ADD; + add(glow); + + blueBar.visible = false; + scrollBack.visible = false; + scrollLower.visible = false; + scrollTop.visible = false; + scrollMiddle.visible = false; + glow.visible = false; + glowDark.visible = false; + + confirmAtlas = new FlxAtlasSprite(5, 55, Paths.animateAtlas("freeplay/backingCards/pico/pico-confirm")); + confirmAtlas.visible = false; + add(confirmAtlas); + + cardGlow.blend = BlendMode.ADD; + cardGlow.visible = false; + add(cardGlow); + } + + override public function confirm():Void + { + confirmAtlas.visible = true; + confirmAtlas.anim.play(""); + + FlxTween.color(instance.bgDad, 10 / 24, 0xFFFFFFFF, 0xFF8A8A8A, + { + ease: FlxEase.expoOut, + onUpdate: function(_) { + instance.angleMaskShader.extraColor = instance.bgDad.color; + } + }); + + new FlxTimer().start(10 / 24, function(_) { + // shoot + FlxTween.color(instance.bgDad, 3 / 24, 0xFF343036, 0xFF696366, + { + ease: FlxEase.expoOut, + onUpdate: function(_) { + instance.angleMaskShader.extraColor = instance.bgDad.color; + } + }); + }); + + new FlxTimer().start(14 / 24, function(_) { + // shoot + FlxTween.color(instance.bgDad, 3 / 24, 0xFF27292D, 0xFF686A6F, + { + ease: FlxEase.expoOut, + onUpdate: function(_) { + instance.angleMaskShader.extraColor = instance.bgDad.color; + } + }); + }); + + new FlxTimer().start(18 / 24, function(_) { + // shoot + FlxTween.color(instance.bgDad, 3 / 24, 0xFF2D282D, 0xFF676164, + { + ease: FlxEase.expoOut, + onUpdate: function(_) { + instance.angleMaskShader.extraColor = instance.bgDad.color; + } + }); + }); + + new FlxTimer().start(21 / 24, function(_) { + // shoot + FlxTween.color(instance.bgDad, 3 / 24, 0xFF29292F, 0xFF62626B, + { + ease: FlxEase.expoOut, + onUpdate: function(_) { + instance.angleMaskShader.extraColor = instance.bgDad.color; + } + }); + }); + + new FlxTimer().start(24 / 24, function(_) { + // shoot + FlxTween.color(instance.bgDad, 3 / 24, 0xFF29232C, 0xFF808080, + { + ease: FlxEase.expoOut, + onUpdate: function(_) { + instance.angleMaskShader.extraColor = instance.bgDad.color; + } + }); + }); + } + + var beatFreq:Int = 1; + var beatFreqList:Array = [1,2,4,8]; + + public override function beatHit():Void { + // increases the amount of beats that need to go by to pulse the glow because itd flash like craazy at high bpms..... + beatFreq = beatFreqList[Math.floor(Conductor.instance.bpm/140)]; + + if(Conductor.instance.currentBeat % beatFreq != 0) return; + FlxTween.cancelTweensOf(glow); + FlxTween.cancelTweensOf(glowDark); + + glow.alpha = 1; + FlxTween.tween(glow, {alpha: 0}, 16/24, {ease: FlxEase.quartOut}); + glowDark.alpha = 0; + FlxTween.tween(glowDark, {alpha: 1}, 18/24, {ease: FlxEase.quartOut}); + } + + public override function introDone():Void + { + pinkBack.color = 0xFF98A2F3; + + blueBar.visible = true; + scrollBack.visible = true; + scrollLower.visible = true; + scrollTop.visible = true; + scrollMiddle.visible = true; + glowDark.visible = true; + glow.visible = true; + + cardGlow.visible = true; + FlxTween.tween(cardGlow, {alpha: 0, "scale.x": 1.2, "scale.y": 1.2}, 0.45, {ease: FlxEase.sineOut}); + } + + public override function disappear():Void + { + FlxTween.color(pinkBack, 0.25, 0xFF98A2F3, 0xFFFFD0D5, {ease: FlxEase.quadOut}); + + blueBar.visible = false; + scrollBack.visible = false; + scrollLower.visible = false; + scrollTop.visible = false; + scrollMiddle.visible = false; + glowDark.visible = false; + glow.visible = false; + + cardGlow.visible = true; + cardGlow.alpha = 1; + cardGlow.scale.set(1, 1); + FlxTween.tween(cardGlow, {alpha: 0, "scale.x": 1.2, "scale.y": 1.2}, 0.25, {ease: FlxEase.sineOut}); + } + + override public function update(elapsed:Float):Void + { + super.update(elapsed); + var scrollProgress:Float = Math.abs(scrollTop.x % (scrollTop.frameWidth + 20)); + + if (scrollTop.animation.curAnim.finished == true) + { + if (FlxMath.inBounds(scrollProgress, 500, 700) && scrollTop.animation.curAnim.name != 'sniper') + { + scrollTop.animation.play('sniper', true, false); + } + + if (FlxMath.inBounds(scrollProgress, 700, 1300) && scrollTop.animation.curAnim.name != 'rifle') + { + scrollTop.animation.play('rifle', true, false); + } + + if (FlxMath.inBounds(scrollProgress, 1450, 2000) && scrollTop.animation.curAnim.name != 'rocket launcher') + { + scrollTop.animation.play('rocket launcher', true, false); + } + + if (FlxMath.inBounds(scrollProgress, 0, 300) && scrollTop.animation.curAnim.name != 'uzi') + { + scrollTop.animation.play('uzi', true, false); + } + } + } +} diff --git a/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx b/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx index d4dd7aaa4..93d643ae4 100644 --- a/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx +++ b/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx @@ -9,6 +9,7 @@ import funkin.play.scoring.Scoring.ScoringRank; * An object used to retrieve data about a playable character (also known as "weeks"). * Can be scripted to override each function, for custom behavior. */ +@:nullSafety class PlayableCharacter implements IRegistryEntry { /** @@ -19,7 +20,7 @@ class PlayableCharacter implements IRegistryEntry /** * Playable character data as parsed from the JSON file. */ - public final _data:PlayerData; + public final _data:Null; /** * @param id The ID of the JSON file to parse. @@ -41,7 +42,7 @@ class PlayableCharacter implements IRegistryEntry public function getName():String { // TODO: Maybe add localization support? - return _data.name; + return _data?.name ?? "Unknown"; } /** @@ -50,7 +51,7 @@ class PlayableCharacter implements IRegistryEntry */ public function getOwnedCharacterIds():Array { - return _data.ownedChars; + return _data?.ownedChars ?? []; } /** @@ -59,17 +60,17 @@ class PlayableCharacter implements IRegistryEntry */ public function shouldShowUnownedChars():Bool { - return _data.showUnownedChars; + return _data?.showUnownedChars ?? false; } public function shouldShowCharacter(id:String):Bool { - if (_data.ownedChars.contains(id)) + if (getOwnedCharacterIds().contains(id)) { return true; } - if (_data.showUnownedChars) + if (shouldShowUnownedChars()) { var result = !PlayerRegistry.instance.isCharacterOwned(id); return result; @@ -78,19 +79,25 @@ class PlayableCharacter implements IRegistryEntry return false; } - public function getFreeplayDJData():PlayerFreeplayDJData + public function getFreeplayStyleID():String { - return _data.freeplayDJ; + return _data?.freeplayStyle ?? Constants.DEFAULT_FREEPLAY_STYLE; + } + + public function getFreeplayDJData():Null + { + return _data?.freeplayDJ; } public function getFreeplayDJText(index:Int):String { - return _data.freeplayDJ.getFreeplayDJText(index); + // Silly little placeholder + return _data?.freeplayDJ?.getFreeplayDJText(index) ?? 'GET FREAKY ON A FRIDAY'; } - public function getCharSelectData():PlayerCharSelectData + public function getCharSelectData():Null { - return _data.charSelect; + return _data?.charSelect; } /** @@ -99,7 +106,7 @@ class PlayableCharacter implements IRegistryEntry */ public function getResultsAnimationDatas(rank:ScoringRank):Array { - if (_data.results == null) + if (_data == null || _data.results == null) { return []; } @@ -119,12 +126,33 @@ class PlayableCharacter implements IRegistryEntry } } + public function getResultsMusicPath(rank:ScoringRank):String + { + switch (rank) + { + case PERFECT_GOLD: + return _data?.results?.music?.PERFECT_GOLD ?? "resultsPERFECT"; + case PERFECT: + return _data?.results?.music?.PERFECT ?? "resultsPERFECT"; + case EXCELLENT: + return _data?.results?.music?.EXCELLENT ?? "resultsEXCELLENT"; + case GREAT: + return _data?.results?.music?.GREAT ?? "resultsNORMAL"; + case GOOD: + return _data?.results?.music?.GOOD ?? "resultsNORMAL"; + case SHIT: + return _data?.results?.music?.SHIT ?? "resultsSHIT"; + default: + return _data?.results?.music?.GOOD ?? "resultsNORMAL"; + } + } + /** * Returns whether this character is unlocked. */ public function isUnlocked():Bool { - return _data.unlocked; + return _data?.unlocked ?? true; } /** diff --git a/source/funkin/ui/mainmenu/MainMenuState.hx b/source/funkin/ui/mainmenu/MainMenuState.hx index c777e685b..d2a9f046a 100644 --- a/source/funkin/ui/mainmenu/MainMenuState.hx +++ b/source/funkin/ui/mainmenu/MainMenuState.hx @@ -110,7 +110,17 @@ class MainMenuState extends MusicBeatState FlxTransitionableState.skipNextTransIn = true; FlxTransitionableState.skipNextTransOut = true; - openSubState(new FreeplayState()); + #if FEATURE_DEBUG_FUNCTIONS + // Debug function: Hold SHIFT when selecting Freeplay to swap character without the char select menu + var targetCharacter:Null = (FlxG.keys.pressed.SHIFT) ? (FreeplayState.rememberedCharacterId == "pico" ? "bf" : "pico") : null; + #else + var targetCharacter:Null = null; + #end + + openSubState(new FreeplayState( + { + character: targetCharacter + })); }); #if CAN_OPEN_LINKS diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx index fa03b229d..1653311df 100644 --- a/source/funkin/util/Constants.hx +++ b/source/funkin/util/Constants.hx @@ -258,6 +258,11 @@ class Constants */ public static final DEFAULT_NOTE_STYLE:String = 'funkin'; + /** + * The default freeplay style for characters. + */ + public static final DEFAULT_FREEPLAY_STYLE:String = 'bf'; + /** * The default pixel note style for songs. */ diff --git a/source/funkin/util/ReflectUtil.hx b/source/funkin/util/ReflectUtil.hx index 830edd31d..da98c820b 100644 --- a/source/funkin/util/ReflectUtil.hx +++ b/source/funkin/util/ReflectUtil.hx @@ -33,4 +33,19 @@ class ReflectUtil { return Type.getClassName(Type.getClass(obj)); } + + public static function getAnonymousFieldsOf(obj:Dynamic):Array + { + return Reflect.fields(obj); + } + + public static function getAnonymousField(obj:Dynamic, name:String):Dynamic + { + return Reflect.field(obj, name); + } + + public static function hasAnonymousField(obj:Dynamic, name:String):Bool + { + return Reflect.hasField(obj, name); + } }