diff --git a/.github/actions/setup-haxe/action.yml b/.github/actions/setup-haxe/action.yml index 5a9f7b293..b22fc6f69 100644 --- a/.github/actions/setup-haxe/action.yml +++ b/.github/actions/setup-haxe/action.yml @@ -60,8 +60,8 @@ runs: haxelib --debug --never deleterepo || true haxelib --debug --never newrepo echo "HAXEPATH=$(haxelib config)" >> "$GITHUB_ENV" - haxelib --debug --never git haxelib https://github.com/HaxeFoundation/haxelib.git master - haxelib --debug --global install hmm + haxelib --debug --always --global git haxelib https://github.com/FunkinCrew/haxelib.git funkin-patches --skip-dependencies + haxelib --debug --global --always git hmm https://github.com/FunkinCrew/hmm funkin-patches echo "TIMER_DEPS=$(date +%s)" >> "$GITHUB_ENV" - name: Restore cached dependencies diff --git a/assets b/assets index 005c96f85..62c4a8203 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 005c96f85f4304865acb196e7cc4d6d83f9d76d8 +Subproject commit 62c4a8203362c1b434de0d376046ebccb96da254 diff --git a/hmm.json b/hmm.json index e9aa678ef..aad0be8cf 100644 --- a/hmm.json +++ b/hmm.json @@ -104,7 +104,7 @@ "type": "git", "dir": null, "url": "https://github.com/HaxeFoundation/hxcpp", - "ref": "01cfee282a9a783e10c5a7774a3baaf547e6b0a7" + "ref": "8dc8020f8465027de6c2aaaed90718bc693651ed" }, { "name": "hxcpp-debug-server", @@ -178,7 +178,7 @@ "name": "openfl", "type": "git", "dir": null, - "ref": "228c1b5063911e2ad75cef6e3168ef0a4b9f9134", + "ref": "8306425c497766739510ab29e876059c96f77bd2", "url": "https://github.com/FunkinCrew/openfl" }, { diff --git a/source/funkin/data/BaseRegistry.hx b/source/funkin/data/BaseRegistry.hx index 118516bec..83413ad00 100644 --- a/source/funkin/data/BaseRegistry.hx +++ b/source/funkin/data/BaseRegistry.hx @@ -263,7 +263,7 @@ abstract class BaseRegistry & Constructible + public function parseEntryDataWithMigration(id:String, version:Null):Null { if (version == null) { diff --git a/source/funkin/data/notestyle/CHANGELOG.md b/source/funkin/data/notestyle/CHANGELOG.md new file mode 100644 index 000000000..d85c11cad --- /dev/null +++ b/source/funkin/data/notestyle/CHANGELOG.md @@ -0,0 +1,31 @@ +# Note 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.1.0] +### Added +- Added several new `assets`: + - `countdownThree` + - `countdownTwo` + - `countdownOne` + - `countdownGo` + - `judgementSick` + - `judgementGood` + - `judgementBad` + - `judgementShit` + - `comboNumber0` + - `comboNumber1` + - `comboNumber2` + - `comboNumber3` + - `comboNumber4` + - `comboNumber5` + - `comboNumber6` + - `comboNumber7` + - `comboNumber8` + - `comboNumber9` + +## [1.0.0] +Initial version. diff --git a/source/funkin/data/notestyle/NoteStyleData.hx b/source/funkin/data/notestyle/NoteStyleData.hx index fcdb3b4f9..39162b896 100644 --- a/source/funkin/data/notestyle/NoteStyleData.hx +++ b/source/funkin/data/notestyle/NoteStyleData.hx @@ -74,6 +74,84 @@ typedef NoteStyleAssetsData = */ @:optional var holdNoteCover:NoteStyleAssetData; + + /** + * The THREE sound (and an optional pre-READY graphic). + */ + @:optional + var countdownThree:NoteStyleAssetData; + + /** + * The TWO sound and READY graphic. + */ + @:optional + var countdownTwo:NoteStyleAssetData; + + /** + * The ONE sound and SET graphic. + */ + @:optional + var countdownOne:NoteStyleAssetData; + + /** + * The GO sound and GO! graphic. + */ + @:optional + var countdownGo:NoteStyleAssetData; + + /** + * The SICK! judgement. + */ + @:optional + var judgementSick:NoteStyleAssetData; + + /** + * The GOOD! judgement. + */ + @:optional + var judgementGood:NoteStyleAssetData; + + /** + * The BAD! judgement. + */ + @:optional + var judgementBad:NoteStyleAssetData; + + /** + * The SHIT! judgement. + */ + @:optional + var judgementShit:NoteStyleAssetData; + + @:optional + var comboNumber0:NoteStyleAssetData; + + @:optional + var comboNumber1:NoteStyleAssetData; + + @:optional + var comboNumber2:NoteStyleAssetData; + + @:optional + var comboNumber3:NoteStyleAssetData; + + @:optional + var comboNumber4:NoteStyleAssetData; + + @:optional + var comboNumber5:NoteStyleAssetData; + + @:optional + var comboNumber6:NoteStyleAssetData; + + @:optional + var comboNumber7:NoteStyleAssetData; + + @:optional + var comboNumber8:NoteStyleAssetData; + + @:optional + var comboNumber9:NoteStyleAssetData; } /** @@ -120,7 +198,8 @@ typedef NoteStyleAssetData = /** * The structure of this data depends on the asset. */ - var data:T; + @:optional + var data:Null; } typedef NoteStyleData_Note = @@ -131,7 +210,14 @@ typedef NoteStyleData_Note = var right:UnnamedAnimationData; } +typedef NoteStyleData_Countdown = +{ + var audioPath:String; +} + typedef NoteStyleData_HoldNote = {} +typedef NoteStyleData_Judgement = {} +typedef NoteStyleData_ComboNum = {} /** * Data on animations for each direction of the strumline. diff --git a/source/funkin/data/notestyle/NoteStyleRegistry.hx b/source/funkin/data/notestyle/NoteStyleRegistry.hx index 5e9fa9a3d..36d1b9200 100644 --- a/source/funkin/data/notestyle/NoteStyleRegistry.hx +++ b/source/funkin/data/notestyle/NoteStyleRegistry.hx @@ -11,9 +11,9 @@ class NoteStyleRegistry extends BaseRegistry * Handle breaking changes by incrementing this value * and adding migration to the `migrateNoteStyleData()` function. */ - public static final NOTE_STYLE_DATA_VERSION:thx.semver.Version = "1.0.0"; + public static final NOTE_STYLE_DATA_VERSION:thx.semver.Version = "1.1.0"; - public static final NOTE_STYLE_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x"; + public static final NOTE_STYLE_DATA_VERSION_RULE:thx.semver.VersionRule = "1.1.x"; public static var instance(get, never):NoteStyleRegistry; static var _instance:Null = null; diff --git a/source/funkin/data/stage/StageRegistry.hx b/source/funkin/data/stage/StageRegistry.hx index a03371296..1f0504247 100644 --- a/source/funkin/data/stage/StageRegistry.hx +++ b/source/funkin/data/stage/StageRegistry.hx @@ -93,8 +93,8 @@ class StageRegistry extends BaseRegistry public function listBaseGameStageIds():Array { return [ - "mainStage", "spookyMansion", "phillyTrain", "limoRide", "mallXmas", "mallEvil", "school", "schoolEvil", "tankmanBattlefield", "phillyStreets", - "phillyBlazin", + "mainStage", "spookyMansion", "phillyTrain", "phillyTrainErect", "limoRide", "limoRideErect", "mallXmas", "mallEvil", "school", "schoolEvil", + "tankmanBattlefield", "phillyStreets", "phillyBlazin", ]; } diff --git a/source/funkin/graphics/shaders/AdjustColorShader.hx b/source/funkin/graphics/shaders/AdjustColorShader.hx new file mode 100644 index 000000000..2b0970eeb --- /dev/null +++ b/source/funkin/graphics/shaders/AdjustColorShader.hx @@ -0,0 +1,55 @@ +package funkin.graphics.shaders; + +import flixel.addons.display.FlxRuntimeShader; +import funkin.Paths; +import openfl.utils.Assets; + +class AdjustColorShader extends FlxRuntimeShader +{ + public var hue(default, set):Float; + public var saturation(default, set):Float; + public var brightness(default, set):Float; + public var contrast(default, set):Float; + + public function new() + { + super(Assets.getText(Paths.frag('adjustColor'))); + // FlxG.debugger.addTrackerProfile(new TrackerProfile(HSVShader, ['hue', 'saturation', 'brightness', 'contrast'])); + hue = 0; + saturation = 0; + brightness = 0; + contrast = 0; + } + + function set_hue(value:Float):Float + { + this.setFloat('hue', value); + this.hue = value; + + return this.hue; + } + + function set_saturation(value:Float):Float + { + this.setFloat('saturation', value); + this.saturation = value; + + return this.saturation; + } + + function set_brightness(value:Float):Float + { + this.setFloat('brightness', value); + this.brightness = value; + + return this.brightness; + } + + function set_contrast(value:Float):Float + { + this.setFloat('contrast', value); + this.contrast = value; + + return this.contrast; + } +} diff --git a/source/funkin/graphics/shaders/RuntimePostEffectShader.hx b/source/funkin/graphics/shaders/RuntimePostEffectShader.hx index 9f49da075..d39f57efe 100644 --- a/source/funkin/graphics/shaders/RuntimePostEffectShader.hx +++ b/source/funkin/graphics/shaders/RuntimePostEffectShader.hx @@ -2,6 +2,7 @@ package funkin.graphics.shaders; import flixel.FlxCamera; import flixel.FlxG; +import flixel.graphics.frames.FlxFrame; import flixel.addons.display.FlxRuntimeShader; import lime.graphics.opengl.GLProgram; import lime.utils.Log; @@ -32,6 +33,9 @@ class RuntimePostEffectShader extends FlxRuntimeShader // equals (camera.viewLeft, camera.viewTop, camera.viewRight, camera.viewBottom) uniform vec4 uCameraBounds; + // equals (frame.left, frame.top, frame.right, frame.bottom) + uniform vec4 uFrameBounds; + // screen coord -> world coord conversion // returns world coord in px vec2 screenToWorld(vec2 screenCoord) { @@ -56,6 +60,25 @@ class RuntimePostEffectShader extends FlxRuntimeShader return (worldCoord - offset) / scale; } + // screen coord -> frame coord conversion + // returns normalized frame coord + vec2 screenToFrame(vec2 screenCoord) { + float left = uFrameBounds.x; + float top = uFrameBounds.y; + float right = uFrameBounds.z; + float bottom = uFrameBounds.w; + float width = right - left; + float height = bottom - top; + + float clampedX = clamp(screenCoord.x, left, right); + float clampedY = clamp(screenCoord.y, top, bottom); + + return vec2( + (clampedX - left) / (width), + (clampedY - top) / (height) + ); + } + // internally used to get the maximum `openfl_TextureCoordv` vec2 bitmapCoordScale() { return openfl_TextureCoordv / screenCoord; @@ -80,6 +103,8 @@ class RuntimePostEffectShader extends FlxRuntimeShader { super(fragmentSource, null, glVersion); uScreenResolution.value = [FlxG.width, FlxG.height]; + uCameraBounds.value = [0, 0, FlxG.width, FlxG.height]; + uFrameBounds.value = [0, 0, FlxG.width, FlxG.height]; } // basically `updateViewInfo(FlxG.width, FlxG.height, FlxG.camera)` is good @@ -89,6 +114,12 @@ class RuntimePostEffectShader extends FlxRuntimeShader uCameraBounds.value = [camera.viewLeft, camera.viewTop, camera.viewRight, camera.viewBottom]; } + public function updateFrameInfo(frame:FlxFrame) + { + // NOTE: uv.width is actually the right pos and uv.height is the bottom pos + uFrameBounds.value = [frame.uv.x, frame.uv.y, frame.uv.width, frame.uv.height]; + } + override function __createGLProgram(vertexSource:String, fragmentSource:String):GLProgram { try diff --git a/source/funkin/graphics/shaders/RuntimeRainShader.hx b/source/funkin/graphics/shaders/RuntimeRainShader.hx index 239276bbe..68a203179 100644 --- a/source/funkin/graphics/shaders/RuntimeRainShader.hx +++ b/source/funkin/graphics/shaders/RuntimeRainShader.hx @@ -32,6 +32,14 @@ class RuntimeRainShader extends RuntimePostEffectShader return time = value; } + public var spriteMode(default, set):Bool = false; + + function set_spriteMode(value:Bool):Bool + { + this.setBool('uSpriteMode', value); + return spriteMode = value; + } + // The scale of the rain depends on the world coordinate system, so higher resolution makes // the raindrops smaller. This parameter can be used to adjust the total scale of the scene. // The size of the raindrops is proportional to the value of this parameter. diff --git a/source/funkin/play/Countdown.hx b/source/funkin/play/Countdown.hx index 55c2a8992..643883a43 100644 --- a/source/funkin/play/Countdown.hx +++ b/source/funkin/play/Countdown.hx @@ -11,6 +11,9 @@ import funkin.modding.events.ScriptEvent.CountdownScriptEvent; import flixel.util.FlxTimer; import funkin.util.EaseUtil; import funkin.audio.FunkinSound; +import openfl.utils.Assets; +import funkin.data.notestyle.NoteStyleRegistry; +import funkin.play.notes.notestyle.NoteStyle; class Countdown { @@ -19,6 +22,24 @@ class Countdown */ public static var countdownStep(default, null):CountdownStep = BEFORE; + /** + * Which alternate graphic/sound on countdown to use. + * This is set via the current notestyle. + * For example, in Week 6 it is `pixel`. + */ + public static var soundSuffix:String = ''; + + /** + * Which alternate graphic on countdown to use. + * You can set this via script. + * For example, in Week 6 it is `-pixel`. + */ + public static var graphicSuffix:String = ''; + + static var noteStyle:NoteStyle; + + static var fallbackNoteStyle:Null; + /** * The currently running countdown. This will be null if there is no countdown running. */ @@ -30,7 +51,7 @@ class Countdown * This will automatically stop and restart the countdown if it is already running. * @returns `false` if the countdown was cancelled by a script. */ - public static function performCountdown(isPixelStyle:Bool):Bool + public static function performCountdown():Bool { countdownStep = BEFORE; var cancelled:Bool = propagateCountdownEvent(countdownStep); @@ -65,10 +86,10 @@ class Countdown // PlayState.instance.dispatchEvent(new SongTimeScriptEvent(SONG_BEAT_HIT, 0, 0)); // Countdown graphic. - showCountdownGraphic(countdownStep, isPixelStyle); + showCountdownGraphic(countdownStep); // Countdown sound. - playCountdownSound(countdownStep, isPixelStyle); + playCountdownSound(countdownStep); // Event handling bullshit. var cancelled:Bool = propagateCountdownEvent(countdownStep); @@ -177,122 +198,69 @@ class Countdown } /** - * Retrieves the graphic to use for this step of the countdown. - * TODO: Make this less dumb. Unhardcode it? Use modules? Use notestyles? - * - * This is public so modules can do lol funny shit. + * Reset the countdown configuration to the default. */ - public static function showCountdownGraphic(index:CountdownStep, isPixelStyle:Bool):Void + public static function reset() { - var spritePath:String = null; + noteStyle = null; + } + + /** + * Retrieve the note style data (if we haven't already) + * @param noteStyleId The id of the note style to fetch. Defaults to the one used by the current PlayState. + * @param force Fetch the note style from the registry even if we've already fetched it. + */ + static function fetchNoteStyle(?noteStyleId:String, force:Bool = false):Void + { + if (noteStyle != null && !force) return; + + if (noteStyleId == null) noteStyleId = PlayState.instance?.currentChart?.noteStyle; + + noteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyleId); + if (noteStyle == null) noteStyle = NoteStyleRegistry.instance.fetchDefault(); + } + + /** + * Retrieves the graphic to use for this step of the countdown. + */ + public static function showCountdownGraphic(index:CountdownStep):Void + { + fetchNoteStyle(); + + var countdownSprite = noteStyle.buildCountdownSprite(index); + if (countdownSprite == null) return; var fadeEase = FlxEase.cubeInOut; - - if (isPixelStyle) - { - fadeEase = EaseUtil.stepped(8); - switch (index) - { - case TWO: - spritePath = 'weeb/pixelUI/ready-pixel'; - case ONE: - spritePath = 'weeb/pixelUI/set-pixel'; - case GO: - spritePath = 'weeb/pixelUI/date-pixel'; - default: - // null - } - } - else - { - switch (index) - { - case TWO: - spritePath = 'ready'; - case ONE: - spritePath = 'set'; - case GO: - spritePath = 'go'; - default: - // null - } - } - - if (spritePath == null) return; - - var countdownSprite:FunkinSprite = FunkinSprite.create(spritePath); - countdownSprite.scrollFactor.set(0, 0); - - if (isPixelStyle) countdownSprite.setGraphicSize(Std.int(countdownSprite.width * Constants.PIXEL_ART_SCALE)); - - countdownSprite.antialiasing = !isPixelStyle; - - countdownSprite.updateHitbox(); - countdownSprite.screenCenter(); + if (noteStyle.isCountdownSpritePixel(index)) fadeEase = EaseUtil.stepped(8); // Fade sprite in, then out, then destroy it. - FlxTween.tween(countdownSprite, {y: countdownSprite.y += 100}, Conductor.instance.beatLengthMs / 1000, + FlxTween.tween(countdownSprite, {alpha: 0}, Conductor.instance.beatLengthMs / 1000, { - ease: FlxEase.cubeInOut, + ease: fadeEase, onComplete: function(twn:FlxTween) { countdownSprite.destroy(); } }); - FlxTween.tween(countdownSprite, {alpha: 0}, Conductor.instance.beatLengthMs / 1000, - { - ease: fadeEase - }); - + countdownSprite.cameras = [PlayState.instance.camHUD]; PlayState.instance.add(countdownSprite); + countdownSprite.screenCenter(); + + var offsets = noteStyle.getCountdownSpriteOffsets(index); + countdownSprite.x += offsets[0]; + countdownSprite.y += offsets[1]; } /** * Retrieves the sound file to use for this step of the countdown. - * TODO: Make this less dumb. Unhardcode it? Use modules? Use notestyles? - * - * This is public so modules can do lol funny shit. */ - public static function playCountdownSound(index:CountdownStep, isPixelStyle:Bool):Void + public static function playCountdownSound(step:CountdownStep):FunkinSound { - var soundPath:String = null; + fetchNoteStyle(); + var path = noteStyle.getCountdownSoundPath(step); + if (path == null) return null; - if (isPixelStyle) - { - switch (index) - { - case THREE: - soundPath = 'intro3-pixel'; - case TWO: - soundPath = 'intro2-pixel'; - case ONE: - soundPath = 'intro1-pixel'; - case GO: - soundPath = 'introGo-pixel'; - default: - // null - } - } - else - { - switch (index) - { - case THREE: - soundPath = 'intro3'; - case TWO: - soundPath = 'intro2'; - case ONE: - soundPath = 'intro1'; - case GO: - soundPath = 'introGo'; - default: - // null - } - } - - if (soundPath == null) return; - - FunkinSound.playOnce(Paths.sound(soundPath), Constants.COUNTDOWN_VOLUME); + return FunkinSound.playOnce(path, Constants.COUNTDOWN_VOLUME); } public static function decrement(step:CountdownStep):CountdownStep diff --git a/source/funkin/play/PauseSubState.hx b/source/funkin/play/PauseSubState.hx index d0c759b16..6f8908eea 100644 --- a/source/funkin/play/PauseSubState.hx +++ b/source/funkin/play/PauseSubState.hx @@ -306,7 +306,7 @@ class PauseSubState extends MusicBeatSubState metadataDifficulty.setFormat(Paths.font('vcr.ttf'), 32, FlxColor.WHITE, FlxTextAlign.RIGHT); if (PlayState.instance?.currentDifficulty != null) { - metadataDifficulty.text += PlayState.instance.currentDifficulty.toTitleCase(); + metadataDifficulty.text += PlayState.instance.currentDifficulty.replace('-', ' ').toTitleCase(); } metadataDifficulty.scrollFactor.set(0, 0); metadata.add(metadataDifficulty); diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index da066dd6a..b6a17947f 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -696,12 +696,7 @@ class PlayState extends MusicBeatSubState initMinimalMode(); } initStrumlines(); - - // Initialize the judgements and combo meter. - comboPopUps = new PopUpStuff(); - comboPopUps.zIndex = 900; - add(comboPopUps); - comboPopUps.cameras = [camHUD]; + initPopups(); #if discord_rpc // Initialize Discord Rich Presence. @@ -902,7 +897,7 @@ class PlayState extends MusicBeatSubState health = Constants.HEALTH_STARTING; songScore = 0; Highscore.tallies.combo = 0; - Countdown.performCountdown(currentStageId.startsWith('school')); + Countdown.performCountdown(); needsReset = false; } @@ -1729,6 +1724,21 @@ class PlayState extends MusicBeatSubState opponentStrumline.fadeInArrows(); } + /** + * Configures the judgement and combo popups. + */ + function initPopups():Void + { + var noteStyleId:String = currentChart.noteStyle; + var noteStyle:NoteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyleId); + if (noteStyle == null) noteStyle = NoteStyleRegistry.instance.fetchDefault(); + // Initialize the judgements and combo meter. + comboPopUps = new PopUpStuff(noteStyle); + comboPopUps.zIndex = 900; + add(comboPopUps); + comboPopUps.cameras = [camHUD]; + } + /** * Initializes the Discord Rich Presence. */ @@ -1859,7 +1869,7 @@ class PlayState extends MusicBeatSubState public function startCountdown():Void { // If Countdown.performCountdown returns false, then the countdown was canceled by a script. - var result:Bool = Countdown.performCountdown(currentStageId.startsWith('school')); + var result:Bool = Countdown.performCountdown(); if (!result) return; isInCutscene = false; @@ -3020,6 +3030,7 @@ class PlayState extends MusicBeatSubState GameOverSubState.reset(); PauseSubState.reset(); + Countdown.reset(); // Clear the static reference to this state. instance = null; diff --git a/source/funkin/play/character/BaseCharacter.hx b/source/funkin/play/character/BaseCharacter.hx index 432881164..15e196dc1 100644 --- a/source/funkin/play/character/BaseCharacter.hx +++ b/source/funkin/play/character/BaseCharacter.hx @@ -118,22 +118,6 @@ class BaseCharacter extends Bopper */ public var cameraFocusPoint(default, null):FlxPoint = new FlxPoint(0, 0); - override function set_animOffsets(value:Array):Array - { - if (animOffsets == null) value = [0, 0]; - if ((animOffsets[0] == value[0]) && (animOffsets[1] == value[1])) return value; - - // Make sure animOffets are halved when scale is 0.5. - var xDiff = (animOffsets[0] * this.scale.x / (this.isPixel ? 6 : 1)) - value[0]; - var yDiff = (animOffsets[1] * this.scale.y / (this.isPixel ? 6 : 1)) - value[1]; - - // Call the super function so that camera focus point is not affected. - super.set_x(this.x + xDiff); - super.set_y(this.y + yDiff); - - return animOffsets = value; - } - /** * If the x position changes, other than via changing the animation offset, * then we need to update the camera focus point. diff --git a/source/funkin/play/components/HealthIcon.hx b/source/funkin/play/components/HealthIcon.hx index 2442b0dc5..0e24d73fb 100644 --- a/source/funkin/play/components/HealthIcon.hx +++ b/source/funkin/play/components/HealthIcon.hx @@ -150,13 +150,17 @@ class HealthIcon extends FunkinSprite { if (characterId == 'bf-old') { + isPixel = PlayState.instance.currentStage.getBoyfriend().isPixel; PlayState.instance.currentStage.getBoyfriend().initHealthIcon(false); } else { characterId = 'bf-old'; + isPixel = false; loadCharacter(characterId); } + + lerpIconSize(true); } /** @@ -200,31 +204,45 @@ class HealthIcon extends FunkinSprite if (bopEvery != 0) { - // Lerp the health icon back to its normal size, - // while maintaining aspect ratio. - if (this.width > this.height) - { - // Apply linear interpolation while accounting for frame rate. - var targetSize:Int = Std.int(MathUtil.coolLerp(this.width, HEALTH_ICON_SIZE * this.size.x, 0.15)); - - setGraphicSize(targetSize, 0); - } - else - { - var targetSize:Int = Std.int(MathUtil.coolLerp(this.height, HEALTH_ICON_SIZE * this.size.y, 0.15)); - - setGraphicSize(0, targetSize); - } + lerpIconSize(); // Lerp the health icon back to its normal angle. this.angle = MathUtil.coolLerp(this.angle, 0, 0.15); - - this.updateHitbox(); } this.updatePosition(); } + /** + * Does the calculation to lerp the icon size. Usually called every frame, but can be forced to the target size. + * Mainly forced when changing to old icon to not have a weird lerp related to changing from pixel icon to non-pixel old icon + * @param force Force the icon immedialtely to be the target size. Defaults to false. + */ + function lerpIconSize(force:Bool = false):Void + { + // Lerp the health icon back to its normal size, + // while maintaining aspect ratio. + if (this.width > this.height) + { + // Apply linear interpolation while accounting for frame rate. + var targetSize:Int = Std.int(MathUtil.coolLerp(this.width, HEALTH_ICON_SIZE * this.size.x, 0.15)); + + if (force) targetSize = Std.int(HEALTH_ICON_SIZE * this.size.x); + + setGraphicSize(targetSize, 0); + } + else + { + var targetSize:Int = Std.int(MathUtil.coolLerp(this.height, HEALTH_ICON_SIZE * this.size.y, 0.15)); + + if (force) targetSize = Std.int(HEALTH_ICON_SIZE * this.size.y); + + setGraphicSize(0, targetSize); + } + + this.updateHitbox(); + } + /** * Update the position (and status) of the health icon. */ @@ -412,6 +430,8 @@ class HealthIcon extends FunkinSprite isLegacyStyle = !isNewSpritesheet(charId); + trace(' Loading health icon for character: $charId (legacy: $isLegacyStyle)'); + if (!isLegacyStyle) { loadSparrow('icons/icon-$charId'); diff --git a/source/funkin/play/components/PopUpStuff.hx b/source/funkin/play/components/PopUpStuff.hx index 1bdfd98a8..911c3578c 100644 --- a/source/funkin/play/components/PopUpStuff.hx +++ b/source/funkin/play/components/PopUpStuff.hx @@ -8,58 +8,51 @@ import funkin.graphics.FunkinSprite; import funkin.play.PlayState; import funkin.util.TimerUtil; import funkin.util.EaseUtil; +import openfl.utils.Assets; +import funkin.data.notestyle.NoteStyleRegistry; +import funkin.play.notes.notestyle.NoteStyle; +@:nullSafety class PopUpStuff extends FlxTypedGroup { - public var offsets:Array = [0, 0]; + /** + * The current note style to use. This determines which graphics to display. + * For example, Week 6 uses the `pixel` note style, and mods can create their own. + */ + var noteStyle:NoteStyle; - override public function new() + override public function new(noteStyle:NoteStyle) { super(); + + this.noteStyle = noteStyle; } - public function displayRating(daRating:String):Void + public function displayRating(daRating:Null) { - var perfStart:Float = TimerUtil.start(); - if (daRating == null) daRating = "good"; - var ratingPath:String = daRating; - - if (PlayState.instance.currentStageId.startsWith('school')) ratingPath = "weeb/pixelUI/" + ratingPath + "-pixel"; - - var rating:FunkinSprite = FunkinSprite.create(0, 0, ratingPath); - rating.scrollFactor.set(0.2, 0.2); + var rating:Null = noteStyle.buildJudgementSprite(daRating); + if (rating == null) return; rating.zIndex = 1000; - rating.x = (FlxG.width * 0.474) + offsets[0]; - // rating.x -= FlxG.camera.scroll.x * 0.2; - rating.y = (FlxG.camera.height * 0.45 - 60) + offsets[1]; + + rating.x = (FlxG.width * 0.474); + rating.x -= rating.width / 2; + rating.y = (FlxG.camera.height * 0.45 - 60); + rating.y -= rating.height / 2; + + var offsets = noteStyle.getJudgementSpriteOffsets(daRating); + rating.x += offsets[0]; + rating.y += offsets[1]; + rating.acceleration.y = 550; rating.velocity.y -= FlxG.random.int(140, 175); rating.velocity.x -= FlxG.random.int(0, 10); add(rating); - var fadeEase = null; - - if (PlayState.instance.currentStageId.startsWith('school')) - { - rating.setGraphicSize(Std.int(rating.width * Constants.PIXEL_ART_SCALE * 0.7)); - rating.antialiasing = false; - rating.pixelPerfectRender = true; - rating.pixelPerfectPosition = true; - fadeEase = EaseUtil.stepped(2); - } - else - { - rating.setGraphicSize(Std.int(rating.width * 0.65)); - rating.antialiasing = true; - } - rating.updateHitbox(); - - rating.x -= rating.width / 2; - rating.y -= rating.height / 2; + var fadeEase = noteStyle.isJudgementSpritePixel(daRating) ? EaseUtil.stepped(2) : null; FlxTween.tween(rating, {alpha: 0}, 0.2, { @@ -70,62 +63,10 @@ class PopUpStuff extends FlxTypedGroup startDelay: Conductor.instance.beatLengthMs * 0.001, ease: fadeEase }); - - trace('displayRating took: ${TimerUtil.seconds(perfStart)}'); } - public function displayCombo(?combo:Int = 0):Int + public function displayCombo(combo:Int = 0):Void { - var perfStart:Float = TimerUtil.start(); - - if (combo == null) combo = 0; - - var pixelShitPart1:String = ""; - var pixelShitPart2:String = ''; - - if (PlayState.instance.currentStageId.startsWith('school')) - { - pixelShitPart1 = 'weeb/pixelUI/'; - pixelShitPart2 = '-pixel'; - } - var comboSpr:FunkinSprite = FunkinSprite.create(pixelShitPart1 + 'combo' + pixelShitPart2); - comboSpr.y = (FlxG.camera.height * 0.44) + offsets[1]; - comboSpr.x = (FlxG.width * 0.507) + offsets[0]; - // comboSpr.x -= FlxG.camera.scroll.x * 0.2; - - comboSpr.acceleration.y = 600; - comboSpr.velocity.y -= 150; - comboSpr.velocity.x += FlxG.random.int(1, 10); - - // add(comboSpr); - - var fadeEase = null; - - if (PlayState.instance.currentStageId.startsWith('school')) - { - comboSpr.setGraphicSize(Std.int(comboSpr.width * Constants.PIXEL_ART_SCALE * 1)); - comboSpr.antialiasing = false; - comboSpr.pixelPerfectRender = true; - comboSpr.pixelPerfectPosition = true; - fadeEase = EaseUtil.stepped(2); - } - else - { - comboSpr.setGraphicSize(Std.int(comboSpr.width * 0.7)); - comboSpr.antialiasing = true; - } - comboSpr.updateHitbox(); - - FlxTween.tween(comboSpr, {alpha: 0}, 0.2, - { - onComplete: function(tween:FlxTween) { - remove(comboSpr, true); - comboSpr.destroy(); - }, - startDelay: Conductor.instance.beatLengthMs * 0.001, - ease: fadeEase - }); - var seperatedScore:Array = []; var tempCombo:Int = combo; @@ -140,31 +81,27 @@ class PopUpStuff extends FlxTypedGroup // seperatedScore.reverse(); var daLoop:Int = 1; - for (i in seperatedScore) + for (digit in seperatedScore) { - var numScore:FunkinSprite = FunkinSprite.create(0, comboSpr.y, pixelShitPart1 + 'num' + Std.int(i) + pixelShitPart2); + var numScore:Null = noteStyle.buildComboNumSprite(digit); + if (numScore == null) continue; - if (PlayState.instance.currentStageId.startsWith('school')) - { - numScore.setGraphicSize(Std.int(numScore.width * Constants.PIXEL_ART_SCALE * 1)); - numScore.antialiasing = false; - numScore.pixelPerfectRender = true; - numScore.pixelPerfectPosition = true; - } - else - { - numScore.setGraphicSize(Std.int(numScore.width * 0.45)); - numScore.antialiasing = true; - } - numScore.updateHitbox(); + numScore.x = (FlxG.width * 0.507) - (36 * daLoop) - 65; + trace('numScore($daLoop) = ${numScore.x}'); + numScore.y = (FlxG.camera.height * 0.44); + + var offsets = noteStyle.getComboNumSpriteOffsets(digit); + numScore.x += offsets[0]; + numScore.y += offsets[1]; - numScore.x = comboSpr.x - (36 * daLoop) - 65; //- 90; numScore.acceleration.y = FlxG.random.int(250, 300); numScore.velocity.y -= FlxG.random.int(130, 150); numScore.velocity.x = FlxG.random.float(-5, 5); add(numScore); + var fadeEase = noteStyle.isComboNumSpritePixel(digit) ? EaseUtil.stepped(2) : null; + FlxTween.tween(numScore, {alpha: 0}, 0.2, { onComplete: function(tween:FlxTween) { @@ -177,9 +114,5 @@ class PopUpStuff extends FlxTypedGroup daLoop++; } - - trace('displayCombo took: ${TimerUtil.seconds(perfStart)}'); - - return combo; } } diff --git a/source/funkin/play/notes/StrumlineNote.hx b/source/funkin/play/notes/StrumlineNote.hx index 40d893255..d8230aa28 100644 --- a/source/funkin/play/notes/StrumlineNote.hx +++ b/source/funkin/play/notes/StrumlineNote.hx @@ -75,6 +75,13 @@ class StrumlineNote extends FlxSprite function setup(noteStyle:NoteStyle):Void { + if (noteStyle == null) + { + // If you get an exception on this line, check the debug console. + // You probably have a parsing error in your note style's JSON file. + throw "FATAL ERROR: Attempted to initialize PlayState with an invalid NoteStyle."; + } + noteStyle.applyStrumlineFrames(this); noteStyle.applyStrumlineAnimations(this, this.direction); diff --git a/source/funkin/play/notes/notestyle/NoteStyle.hx b/source/funkin/play/notes/notestyle/NoteStyle.hx index 3993cce52..ee07703f1 100644 --- a/source/funkin/play/notes/notestyle/NoteStyle.hx +++ b/source/funkin/play/notes/notestyle/NoteStyle.hx @@ -1,5 +1,6 @@ package funkin.play.notes.notestyle; +import funkin.play.Countdown; import flixel.graphics.frames.FlxAtlasFrames; import flixel.graphics.frames.FlxFramesCollection; import funkin.data.animation.AnimationData; @@ -16,6 +17,7 @@ using funkin.data.animation.AnimationData.AnimationDataUtil; * Holds the data for what assets to use for a note style, * and provides convenience methods for building sprites based on them. */ +@:nullSafety class NoteStyle implements IRegistryEntry { /** @@ -42,12 +44,8 @@ class NoteStyle implements IRegistryEntry this.id = id; _data = _fetchData(id); - if (_data == null) - { - throw 'Could not parse note style data for id: $id'; - } - - this.fallback = NoteStyleRegistry.instance.fetchEntry(getFallbackID()); + var fallbackID = _data.fallback; + if (fallbackID != null) this.fallback = NoteStyleRegistry.instance.fetchEntry(fallbackID); } /** @@ -72,7 +70,7 @@ class NoteStyle implements IRegistryEntry * Get the note style ID of the parent note style. * @return The string ID, or `null` if there is no parent. */ - function getFallbackID():Null + public function getFallbackID():Null { return _data.fallback; } @@ -80,7 +78,7 @@ class NoteStyle implements IRegistryEntry public function buildNoteSprite(target:NoteSprite):Void { // Apply the note sprite frames. - var atlas:FlxAtlasFrames = buildNoteFrames(false); + var atlas:Null = buildNoteFrames(false); if (atlas == null) { @@ -89,7 +87,7 @@ class NoteStyle implements IRegistryEntry target.frames = atlas; - target.antialiasing = !_data.assets.note.isPixel; + target.antialiasing = !(_data.assets?.note?.isPixel ?? false); // Apply the animations. buildNoteAnimations(target); @@ -99,21 +97,30 @@ class NoteStyle implements IRegistryEntry target.updateHitbox(); } - var noteFrames:FlxAtlasFrames = null; + var noteFrames:Null = null; - function buildNoteFrames(force:Bool = false):FlxAtlasFrames + function buildNoteFrames(force:Bool = false):Null { - if (!FunkinSprite.isTextureCached(Paths.image(getNoteAssetPath()))) + var noteAssetPath = getNoteAssetPath(); + if (noteAssetPath == null) return null; + + if (!FunkinSprite.isTextureCached(Paths.image(noteAssetPath))) { - FlxG.log.warn('Note texture is not cached: ${getNoteAssetPath()}'); + FlxG.log.warn('Note texture is not cached: ${noteAssetPath}'); } // Purge the note frames if the cached atlas is invalid. - if (noteFrames?.parent?.isDestroyed ?? false) noteFrames = null; + @:nullSafety(Off) + { + if (noteFrames?.parent?.isDestroyed ?? false) noteFrames = null; + } if (noteFrames != null && !force) return noteFrames; - noteFrames = Paths.getSparrowAtlas(getNoteAssetPath(), getNoteAssetLibrary()); + var noteAssetPath = getNoteAssetPath(); + if (noteAssetPath == null) return null; + + noteFrames = Paths.getSparrowAtlas(noteAssetPath, getNoteAssetLibrary()); if (noteFrames == null) { @@ -123,17 +130,18 @@ class NoteStyle implements IRegistryEntry return noteFrames; } - function getNoteAssetPath(raw:Bool = false):String + function getNoteAssetPath(raw:Bool = false):Null { if (raw) { var rawPath:Null = _data?.assets?.note?.assetPath; - if (rawPath == null) return fallback.getNoteAssetPath(true); + if (rawPath == null && fallback != null) return fallback.getNoteAssetPath(true); return rawPath; } // library:path - var parts = getNoteAssetPath(true).split(Constants.LIBRARY_SEPARATOR); + var parts = getNoteAssetPath(true)?.split(Constants.LIBRARY_SEPARATOR) ?? []; + if (parts.length == 0) return null; if (parts.length == 1) return getNoteAssetPath(true); return parts[1]; } @@ -141,57 +149,63 @@ class NoteStyle implements IRegistryEntry function getNoteAssetLibrary():Null { // library:path - var parts = getNoteAssetPath(true).split(Constants.LIBRARY_SEPARATOR); + var parts = getNoteAssetPath(true)?.split(Constants.LIBRARY_SEPARATOR) ?? []; + if (parts.length == 0) return null; if (parts.length == 1) return null; return parts[0]; } function buildNoteAnimations(target:NoteSprite):Void { - var leftData:AnimationData = fetchNoteAnimationData(LEFT); - target.animation.addByPrefix('purpleScroll', leftData.prefix, leftData.frameRate, leftData.looped, leftData.flipX, leftData.flipY); - var downData:AnimationData = fetchNoteAnimationData(DOWN); - target.animation.addByPrefix('blueScroll', downData.prefix, downData.frameRate, downData.looped, downData.flipX, downData.flipY); - var upData:AnimationData = fetchNoteAnimationData(UP); - target.animation.addByPrefix('greenScroll', upData.prefix, upData.frameRate, upData.looped, upData.flipX, upData.flipY); - var rightData:AnimationData = fetchNoteAnimationData(RIGHT); - target.animation.addByPrefix('redScroll', rightData.prefix, rightData.frameRate, rightData.looped, rightData.flipX, rightData.flipY); + var leftData:Null = fetchNoteAnimationData(LEFT); + if (leftData != null) target.animation.addByPrefix('purpleScroll', leftData.prefix ?? '', leftData.frameRate ?? 24, leftData.looped ?? false, + leftData.flipX, leftData.flipY); + var downData:Null = fetchNoteAnimationData(DOWN); + if (downData != null) target.animation.addByPrefix('blueScroll', downData.prefix ?? '', downData.frameRate ?? 24, downData.looped ?? false, + downData.flipX, downData.flipY); + var upData:Null = fetchNoteAnimationData(UP); + if (upData != null) target.animation.addByPrefix('greenScroll', upData.prefix ?? '', upData.frameRate ?? 24, upData.looped ?? false, upData.flipX, + upData.flipY); + var rightData:Null = fetchNoteAnimationData(RIGHT); + if (rightData != null) target.animation.addByPrefix('redScroll', rightData.prefix ?? '', rightData.frameRate ?? 24, rightData.looped ?? false, + rightData.flipX, rightData.flipY); } public function isNoteAnimated():Bool { - return _data.assets.note.animated; + return _data.assets?.note?.animated ?? false; } public function getNoteScale():Float { - return _data.assets.note.scale; + return _data.assets?.note?.scale ?? 1.0; } - function fetchNoteAnimationData(dir:NoteDirection):AnimationData + function fetchNoteAnimationData(dir:NoteDirection):Null { var result:Null = switch (dir) { - case LEFT: _data.assets.note.data.left.toNamed(); - case DOWN: _data.assets.note.data.down.toNamed(); - case UP: _data.assets.note.data.up.toNamed(); - case RIGHT: _data.assets.note.data.right.toNamed(); + case LEFT: _data.assets?.note?.data?.left?.toNamed(); + case DOWN: _data.assets?.note?.data?.down?.toNamed(); + case UP: _data.assets?.note?.data?.up?.toNamed(); + case RIGHT: _data.assets?.note?.data?.right?.toNamed(); }; - return (result == null) ? fallback.fetchNoteAnimationData(dir) : result; + return (result == null && fallback != null) ? fallback.fetchNoteAnimationData(dir) : result; } - public function getHoldNoteAssetPath(raw:Bool = false):String + public function getHoldNoteAssetPath(raw:Bool = false):Null { if (raw) { // TODO: figure out why ?. didn't work here var rawPath:Null = (_data?.assets?.holdNote == null) ? null : _data?.assets?.holdNote?.assetPath; - return (rawPath == null) ? fallback.getHoldNoteAssetPath(true) : rawPath; + return (rawPath == null && fallback != null) ? fallback.getHoldNoteAssetPath(true) : rawPath; } // library:path - var parts = getHoldNoteAssetPath(true).split(Constants.LIBRARY_SEPARATOR); + var parts = getHoldNoteAssetPath(true)?.split(Constants.LIBRARY_SEPARATOR) ?? []; + if (parts.length == 0) return null; if (parts.length == 1) return Paths.image(parts[0]); return Paths.image(parts[1], parts[0]); } @@ -199,15 +213,15 @@ class NoteStyle implements IRegistryEntry public function isHoldNotePixel():Bool { var data = _data?.assets?.holdNote; - if (data == null) return fallback.isHoldNotePixel(); - return data.isPixel; + if (data == null && fallback != null) return fallback.isHoldNotePixel(); + return data?.isPixel ?? false; } public function fetchHoldNoteScale():Float { var data = _data?.assets?.holdNote; - if (data == null) return fallback.fetchHoldNoteScale(); - return data.scale; + if (data == null && fallback != null) return fallback.fetchHoldNoteScale(); + return data?.scale ?? 1.0; } public function applyStrumlineFrames(target:StrumlineNote):Void @@ -215,7 +229,7 @@ class NoteStyle implements IRegistryEntry // TODO: Add support for multi-Sparrow. // Will be less annoying after this is merged: https://github.com/HaxeFlixel/flixel/pull/2772 - var atlas:FlxAtlasFrames = Paths.getSparrowAtlas(getStrumlineAssetPath(), getStrumlineAssetLibrary()); + var atlas:FlxAtlasFrames = Paths.getSparrowAtlas(getStrumlineAssetPath() ?? '', getStrumlineAssetLibrary()); if (atlas == null) { @@ -224,31 +238,30 @@ class NoteStyle implements IRegistryEntry target.frames = atlas; - target.scale.x = _data.assets.noteStrumline.scale; - target.scale.y = _data.assets.noteStrumline.scale; - target.antialiasing = !_data.assets.noteStrumline.isPixel; + target.scale.set(_data.assets.noteStrumline?.scale ?? 1.0); + target.antialiasing = !(_data.assets.noteStrumline?.isPixel ?? false); } - function getStrumlineAssetPath(raw:Bool = false):String + function getStrumlineAssetPath(raw:Bool = false):Null { if (raw) { var rawPath:Null = _data?.assets?.noteStrumline?.assetPath; - if (rawPath == null) return fallback.getStrumlineAssetPath(true); + if (rawPath == null && fallback != null) return fallback.getStrumlineAssetPath(true); return rawPath; } // library:path - var parts = getStrumlineAssetPath(true).split(Constants.LIBRARY_SEPARATOR); - if (parts.length == 1) return getStrumlineAssetPath(true); + var parts = getStrumlineAssetPath(true)?.split(Constants.LIBRARY_SEPARATOR) ?? []; + if (parts.length <= 1) return getStrumlineAssetPath(true); return parts[1]; } function getStrumlineAssetLibrary():Null { // library:path - var parts = getStrumlineAssetPath(true).split(Constants.LIBRARY_SEPARATOR); - if (parts.length == 1) return null; + var parts = getStrumlineAssetPath(true)?.split(Constants.LIBRARY_SEPARATOR) ?? []; + if (parts.length <= 1) return null; return parts[0]; } @@ -259,60 +272,592 @@ class NoteStyle implements IRegistryEntry function getStrumlineAnimationData(dir:NoteDirection):Array { - var result:Array = switch (dir) + var result:Array> = switch (dir) { case NoteDirection.LEFT: [ - _data.assets.noteStrumline.data.leftStatic.toNamed('static'), - _data.assets.noteStrumline.data.leftPress.toNamed('press'), - _data.assets.noteStrumline.data.leftConfirm.toNamed('confirm'), - _data.assets.noteStrumline.data.leftConfirmHold.toNamed('confirm-hold'), + _data.assets.noteStrumline?.data?.leftStatic?.toNamed('static'), + _data.assets.noteStrumline?.data?.leftPress?.toNamed('press'), + _data.assets.noteStrumline?.data?.leftConfirm?.toNamed('confirm'), + _data.assets.noteStrumline?.data?.leftConfirmHold?.toNamed('confirm-hold'), ]; case NoteDirection.DOWN: [ - _data.assets.noteStrumline.data.downStatic.toNamed('static'), - _data.assets.noteStrumline.data.downPress.toNamed('press'), - _data.assets.noteStrumline.data.downConfirm.toNamed('confirm'), - _data.assets.noteStrumline.data.downConfirmHold.toNamed('confirm-hold'), + _data.assets.noteStrumline?.data?.downStatic?.toNamed('static'), + _data.assets.noteStrumline?.data?.downPress?.toNamed('press'), + _data.assets.noteStrumline?.data?.downConfirm?.toNamed('confirm'), + _data.assets.noteStrumline?.data?.downConfirmHold?.toNamed('confirm-hold'), ]; case NoteDirection.UP: [ - _data.assets.noteStrumline.data.upStatic.toNamed('static'), - _data.assets.noteStrumline.data.upPress.toNamed('press'), - _data.assets.noteStrumline.data.upConfirm.toNamed('confirm'), - _data.assets.noteStrumline.data.upConfirmHold.toNamed('confirm-hold'), + _data.assets.noteStrumline?.data?.upStatic?.toNamed('static'), + _data.assets.noteStrumline?.data?.upPress?.toNamed('press'), + _data.assets.noteStrumline?.data?.upConfirm?.toNamed('confirm'), + _data.assets.noteStrumline?.data?.upConfirmHold?.toNamed('confirm-hold'), ]; case NoteDirection.RIGHT: [ - _data.assets.noteStrumline.data.rightStatic.toNamed('static'), - _data.assets.noteStrumline.data.rightPress.toNamed('press'), - _data.assets.noteStrumline.data.rightConfirm.toNamed('confirm'), - _data.assets.noteStrumline.data.rightConfirmHold.toNamed('confirm-hold'), + _data.assets.noteStrumline?.data?.rightStatic?.toNamed('static'), + _data.assets.noteStrumline?.data?.rightPress?.toNamed('press'), + _data.assets.noteStrumline?.data?.rightConfirm?.toNamed('confirm'), + _data.assets.noteStrumline?.data?.rightConfirmHold?.toNamed('confirm-hold'), ]; + default: []; }; - return result; + return thx.Arrays.filterNull(result); } - public function applyStrumlineOffsets(target:StrumlineNote) + public function applyStrumlineOffsets(target:StrumlineNote):Void { - target.x += _data.assets.noteStrumline.offsets[0]; - target.y += _data.assets.noteStrumline.offsets[1]; + var offsets = _data?.assets?.noteStrumline?.offsets ?? [0.0, 0.0]; + target.x += offsets[0]; + target.y += offsets[1]; } public function getStrumlineScale():Float { - return _data.assets.noteStrumline.scale; + return _data?.assets?.noteStrumline?.scale ?? 1.0; } public function isNoteSplashEnabled():Bool { var data = _data?.assets?.noteSplash?.data; - if (data == null) return fallback.isNoteSplashEnabled(); - return data.enabled; + if (data == null) return fallback?.isNoteSplashEnabled() ?? false; + return data.enabled ?? false; } public function isHoldNoteCoverEnabled():Bool { var data = _data?.assets?.holdNoteCover?.data; - if (data == null) return fallback.isHoldNoteCoverEnabled(); - return data.enabled; + if (data == null) return fallback?.isHoldNoteCoverEnabled() ?? false; + return data.enabled ?? false; + } + + /** + * Build a sprite for the given step of the countdown. + * @param step + * @return A `FunkinSprite`, or `null` if no graphic is available for this step. + */ + public function buildCountdownSprite(step:Countdown.CountdownStep):Null + { + var result = new FunkinSprite(); + + switch (step) + { + case THREE: + if (_data.assets.countdownThree == null) return fallback?.buildCountdownSprite(step); + var assetPath = buildCountdownSpritePath(step); + if (assetPath == null) return null; + result.loadTexture(assetPath); + result.scale.x = _data.assets.countdownThree?.scale ?? 1.0; + result.scale.y = _data.assets.countdownThree?.scale ?? 1.0; + case TWO: + if (_data.assets.countdownTwo == null) return fallback?.buildCountdownSprite(step); + var assetPath = buildCountdownSpritePath(step); + if (assetPath == null) return null; + result.loadTexture(assetPath); + result.scale.x = _data.assets.countdownTwo?.scale ?? 1.0; + result.scale.y = _data.assets.countdownTwo?.scale ?? 1.0; + case ONE: + if (_data.assets.countdownOne == null) return fallback?.buildCountdownSprite(step); + var assetPath = buildCountdownSpritePath(step); + if (assetPath == null) return null; + result.loadTexture(assetPath); + result.scale.x = _data.assets.countdownOne?.scale ?? 1.0; + result.scale.y = _data.assets.countdownOne?.scale ?? 1.0; + case GO: + if (_data.assets.countdownGo == null) return fallback?.buildCountdownSprite(step); + var assetPath = buildCountdownSpritePath(step); + if (assetPath == null) return null; + result.loadTexture(assetPath); + result.scale.x = _data.assets.countdownGo?.scale ?? 1.0; + result.scale.y = _data.assets.countdownGo?.scale ?? 1.0; + default: + // TODO: Do something here? + return null; + } + + result.scrollFactor.set(0, 0); + result.antialiasing = !isCountdownSpritePixel(step); + result.updateHitbox(); + + return result; + } + + function buildCountdownSpritePath(step:Countdown.CountdownStep):Null + { + var basePath:Null = null; + switch (step) + { + case THREE: + basePath = _data.assets.countdownThree?.assetPath; + case TWO: + basePath = _data.assets.countdownTwo?.assetPath; + case ONE: + basePath = _data.assets.countdownOne?.assetPath; + case GO: + basePath = _data.assets.countdownGo?.assetPath; + default: + basePath = null; + } + + if (basePath == null) return fallback?.buildCountdownSpritePath(step); + + var parts = basePath?.split(Constants.LIBRARY_SEPARATOR) ?? []; + if (parts.length < 1) return null; + if (parts.length == 1) return parts[0]; + + return parts[1]; + } + + function buildCountdownSpriteLibrary(step:Countdown.CountdownStep):Null + { + var basePath:Null = null; + switch (step) + { + case THREE: + basePath = _data.assets.countdownThree?.assetPath; + case TWO: + basePath = _data.assets.countdownTwo?.assetPath; + case ONE: + basePath = _data.assets.countdownOne?.assetPath; + case GO: + basePath = _data.assets.countdownGo?.assetPath; + default: + basePath = null; + } + + if (basePath == null) return fallback?.buildCountdownSpriteLibrary(step); + + var parts = basePath?.split(Constants.LIBRARY_SEPARATOR) ?? []; + if (parts.length <= 1) return null; + + return parts[0]; + } + + public function isCountdownSpritePixel(step:Countdown.CountdownStep):Bool + { + switch (step) + { + case THREE: + var result = _data.assets.countdownThree?.isPixel; + if (result == null && fallback != null) result = fallback.isCountdownSpritePixel(step); + return result ?? false; + case TWO: + var result = _data.assets.countdownTwo?.isPixel; + if (result == null && fallback != null) result = fallback.isCountdownSpritePixel(step); + return result ?? false; + case ONE: + var result = _data.assets.countdownOne?.isPixel; + if (result == null && fallback != null) result = fallback.isCountdownSpritePixel(step); + return result ?? false; + case GO: + var result = _data.assets.countdownGo?.isPixel; + if (result == null && fallback != null) result = fallback.isCountdownSpritePixel(step); + return result ?? false; + default: + return false; + } + } + + public function getCountdownSpriteOffsets(step:Countdown.CountdownStep):Array + { + switch (step) + { + case THREE: + var result = _data.assets.countdownThree?.offsets; + if (result == null && fallback != null) result = fallback.getCountdownSpriteOffsets(step); + return result ?? [0, 0]; + case TWO: + var result = _data.assets.countdownTwo?.offsets; + if (result == null && fallback != null) result = fallback.getCountdownSpriteOffsets(step); + return result ?? [0, 0]; + case ONE: + var result = _data.assets.countdownOne?.offsets; + if (result == null && fallback != null) result = fallback.getCountdownSpriteOffsets(step); + return result ?? [0, 0]; + case GO: + var result = _data.assets.countdownGo?.offsets; + if (result == null && fallback != null) result = fallback.getCountdownSpriteOffsets(step); + return result ?? [0, 0]; + default: + return [0, 0]; + } + } + + public function getCountdownSoundPath(step:Countdown.CountdownStep, raw:Bool = false):Null + { + if (raw) + { + // TODO: figure out why ?. didn't work here + var rawPath:Null = switch (step) + { + case Countdown.CountdownStep.THREE: + _data.assets.countdownThree?.data?.audioPath; + case Countdown.CountdownStep.TWO: + _data.assets.countdownTwo?.data?.audioPath; + case Countdown.CountdownStep.ONE: + _data.assets.countdownOne?.data?.audioPath; + case Countdown.CountdownStep.GO: + _data.assets.countdownGo?.data?.audioPath; + default: + null; + } + + return (rawPath == null && fallback != null) ? fallback.getCountdownSoundPath(step, true) : rawPath; + } + + // library:path + var parts = getCountdownSoundPath(step, true)?.split(Constants.LIBRARY_SEPARATOR) ?? []; + if (parts.length == 0) return null; + if (parts.length == 1) return Paths.image(parts[0]); + return Paths.sound(parts[1], parts[0]); + } + + public function buildJudgementSprite(rating:String):Null + { + var result = new FunkinSprite(); + + switch (rating) + { + case "sick": + if (_data.assets.judgementSick == null) return fallback?.buildJudgementSprite(rating); + var assetPath = buildJudgementSpritePath(rating); + if (assetPath == null) return null; + result.loadTexture(assetPath); + result.scale.x = _data.assets.judgementSick?.scale ?? 1.0; + result.scale.y = _data.assets.judgementSick?.scale ?? 1.0; + case "good": + if (_data.assets.judgementGood == null) return fallback?.buildJudgementSprite(rating); + var assetPath = buildJudgementSpritePath(rating); + if (assetPath == null) return null; + result.loadTexture(assetPath); + result.scale.x = _data.assets.judgementGood?.scale ?? 1.0; + result.scale.y = _data.assets.judgementGood?.scale ?? 1.0; + case "bad": + if (_data.assets.judgementBad == null) return fallback?.buildJudgementSprite(rating); + var assetPath = buildJudgementSpritePath(rating); + if (assetPath == null) return null; + result.loadTexture(assetPath); + result.scale.x = _data.assets.judgementBad?.scale ?? 1.0; + result.scale.y = _data.assets.judgementBad?.scale ?? 1.0; + case "shit": + if (_data.assets.judgementShit == null) return fallback?.buildJudgementSprite(rating); + var assetPath = buildJudgementSpritePath(rating); + if (assetPath == null) return null; + result.loadTexture(assetPath); + result.scale.x = _data.assets.judgementShit?.scale ?? 1.0; + result.scale.y = _data.assets.judgementShit?.scale ?? 1.0; + default: + return null; + } + + result.scrollFactor.set(0.2, 0.2); + var isPixel = isJudgementSpritePixel(rating); + result.antialiasing = !isPixel; + result.pixelPerfectRender = isPixel; + result.pixelPerfectPosition = isPixel; + result.updateHitbox(); + + return result; + } + + public function isJudgementSpritePixel(rating:String):Bool + { + switch (rating) + { + case "sick": + var result = _data.assets.judgementSick?.isPixel; + if (result == null && fallback != null) result = fallback.isJudgementSpritePixel(rating); + return result ?? false; + case "good": + var result = _data.assets.judgementGood?.isPixel; + if (result == null && fallback != null) result = fallback.isJudgementSpritePixel(rating); + return result ?? false; + case "bad": + var result = _data.assets.judgementBad?.isPixel; + if (result == null && fallback != null) result = fallback.isJudgementSpritePixel(rating); + return result ?? false; + case "GO": + var result = _data.assets.judgementShit?.isPixel; + if (result == null && fallback != null) result = fallback.isJudgementSpritePixel(rating); + return result ?? false; + default: + return false; + } + } + + function buildJudgementSpritePath(rating:String):Null + { + var basePath:Null = null; + switch (rating) + { + case "sick": + basePath = _data.assets.judgementSick?.assetPath; + case "good": + basePath = _data.assets.judgementGood?.assetPath; + case "bad": + basePath = _data.assets.judgementBad?.assetPath; + case "shit": + basePath = _data.assets.judgementShit?.assetPath; + default: + basePath = null; + } + + if (basePath == null) return fallback?.buildJudgementSpritePath(rating); + + var parts = basePath?.split(Constants.LIBRARY_SEPARATOR) ?? []; + if (parts.length < 1) return null; + if (parts.length == 1) return parts[0]; + + return parts[1]; + } + + public function getJudgementSpriteOffsets(rating:String):Array + { + switch (rating) + { + case "sick": + var result = _data.assets.judgementSick?.offsets; + if (result == null && fallback != null) result = fallback.getJudgementSpriteOffsets(rating); + return result ?? [0, 0]; + case "good": + var result = _data.assets.judgementGood?.offsets; + if (result == null && fallback != null) result = fallback.getJudgementSpriteOffsets(rating); + return result ?? [0, 0]; + case "bad": + var result = _data.assets.judgementBad?.offsets; + if (result == null && fallback != null) result = fallback.getJudgementSpriteOffsets(rating); + return result ?? [0, 0]; + case "shit": + var result = _data.assets.judgementShit?.offsets; + if (result == null && fallback != null) result = fallback.getJudgementSpriteOffsets(rating); + return result ?? [0, 0]; + default: + return [0, 0]; + } + } + + public function buildComboNumSprite(digit:Int):Null + { + var result = new FunkinSprite(); + + switch (digit) + { + case 0: + if (_data.assets.comboNumber0 == null) return fallback?.buildComboNumSprite(digit); + var assetPath = buildComboNumSpritePath(digit); + if (assetPath == null) return null; + result.loadTexture(assetPath); + result.scale.x = _data.assets.comboNumber0?.scale ?? 1.0; + result.scale.y = _data.assets.comboNumber0?.scale ?? 1.0; + case 1: + if (_data.assets.comboNumber1 == null) return fallback?.buildComboNumSprite(digit); + var assetPath = buildComboNumSpritePath(digit); + if (assetPath == null) return null; + result.loadTexture(assetPath); + result.scale.x = _data.assets.comboNumber1?.scale ?? 1.0; + result.scale.y = _data.assets.comboNumber1?.scale ?? 1.0; + case 2: + if (_data.assets.comboNumber2 == null) return fallback?.buildComboNumSprite(digit); + var assetPath = buildComboNumSpritePath(digit); + if (assetPath == null) return null; + result.loadTexture(assetPath); + result.scale.x = _data.assets.comboNumber2?.scale ?? 1.0; + result.scale.y = _data.assets.comboNumber2?.scale ?? 1.0; + case 3: + if (_data.assets.comboNumber3 == null) return fallback?.buildComboNumSprite(digit); + var assetPath = buildComboNumSpritePath(digit); + if (assetPath == null) return null; + result.loadTexture(assetPath); + result.scale.x = _data.assets.comboNumber3?.scale ?? 1.0; + result.scale.y = _data.assets.comboNumber3?.scale ?? 1.0; + case 4: + if (_data.assets.comboNumber4 == null) return fallback?.buildComboNumSprite(digit); + var assetPath = buildComboNumSpritePath(digit); + if (assetPath == null) return null; + result.loadTexture(assetPath); + result.scale.x = _data.assets.comboNumber4?.scale ?? 1.0; + result.scale.y = _data.assets.comboNumber4?.scale ?? 1.0; + case 5: + if (_data.assets.comboNumber5 == null) return fallback?.buildComboNumSprite(digit); + var assetPath = buildComboNumSpritePath(digit); + if (assetPath == null) return null; + result.loadTexture(assetPath); + result.scale.x = _data.assets.comboNumber5?.scale ?? 1.0; + result.scale.y = _data.assets.comboNumber5?.scale ?? 1.0; + case 6: + if (_data.assets.comboNumber6 == null) return fallback?.buildComboNumSprite(digit); + var assetPath = buildComboNumSpritePath(digit); + if (assetPath == null) return null; + result.loadTexture(assetPath); + result.scale.x = _data.assets.comboNumber6?.scale ?? 1.0; + result.scale.y = _data.assets.comboNumber6?.scale ?? 1.0; + case 7: + if (_data.assets.comboNumber7 == null) return fallback?.buildComboNumSprite(digit); + var assetPath = buildComboNumSpritePath(digit); + if (assetPath == null) return null; + result.loadTexture(assetPath); + result.scale.x = _data.assets.comboNumber7?.scale ?? 1.0; + result.scale.y = _data.assets.comboNumber7?.scale ?? 1.0; + case 8: + if (_data.assets.comboNumber8 == null) return fallback?.buildComboNumSprite(digit); + var assetPath = buildComboNumSpritePath(digit); + if (assetPath == null) return null; + result.loadTexture(assetPath); + result.scale.x = _data.assets.comboNumber8?.scale ?? 1.0; + result.scale.y = _data.assets.comboNumber8?.scale ?? 1.0; + case 9: + if (_data.assets.comboNumber9 == null) return fallback?.buildComboNumSprite(digit); + var assetPath = buildComboNumSpritePath(digit); + if (assetPath == null) return null; + result.loadTexture(assetPath); + result.scale.x = _data.assets.comboNumber9?.scale ?? 1.0; + result.scale.y = _data.assets.comboNumber9?.scale ?? 1.0; + default: + return null; + } + + var isPixel = isComboNumSpritePixel(digit); + result.antialiasing = !isPixel; + result.pixelPerfectRender = isPixel; + result.pixelPerfectPosition = isPixel; + result.updateHitbox(); + + return result; + } + + public function isComboNumSpritePixel(digit:Int):Bool + { + switch (digit) + { + case 0: + var result = _data.assets.comboNumber0?.isPixel; + if (result == null && fallback != null) result = fallback.isComboNumSpritePixel(digit); + return result ?? false; + case 1: + var result = _data.assets.comboNumber1?.isPixel; + if (result == null && fallback != null) result = fallback.isComboNumSpritePixel(digit); + return result ?? false; + case 2: + var result = _data.assets.comboNumber2?.isPixel; + if (result == null && fallback != null) result = fallback.isComboNumSpritePixel(digit); + return result ?? false; + case 3: + var result = _data.assets.comboNumber3?.isPixel; + if (result == null && fallback != null) result = fallback.isComboNumSpritePixel(digit); + return result ?? false; + case 4: + var result = _data.assets.comboNumber4?.isPixel; + if (result == null && fallback != null) result = fallback.isComboNumSpritePixel(digit); + return result ?? false; + case 5: + var result = _data.assets.comboNumber5?.isPixel; + if (result == null && fallback != null) result = fallback.isComboNumSpritePixel(digit); + return result ?? false; + case 6: + var result = _data.assets.comboNumber6?.isPixel; + if (result == null && fallback != null) result = fallback.isComboNumSpritePixel(digit); + return result ?? false; + case 7: + var result = _data.assets.comboNumber7?.isPixel; + if (result == null && fallback != null) result = fallback.isComboNumSpritePixel(digit); + return result ?? false; + case 8: + var result = _data.assets.comboNumber8?.isPixel; + if (result == null && fallback != null) result = fallback.isComboNumSpritePixel(digit); + return result ?? false; + case 9: + var result = _data.assets.comboNumber9?.isPixel; + if (result == null && fallback != null) result = fallback.isComboNumSpritePixel(digit); + return result ?? false; + default: + return false; + } + } + + function buildComboNumSpritePath(digit:Int):Null + { + var basePath:Null = null; + switch (digit) + { + case 0: + basePath = _data.assets.comboNumber0?.assetPath; + case 1: + basePath = _data.assets.comboNumber1?.assetPath; + case 2: + basePath = _data.assets.comboNumber2?.assetPath; + case 3: + basePath = _data.assets.comboNumber3?.assetPath; + case 4: + basePath = _data.assets.comboNumber4?.assetPath; + case 5: + basePath = _data.assets.comboNumber5?.assetPath; + case 6: + basePath = _data.assets.comboNumber6?.assetPath; + case 7: + basePath = _data.assets.comboNumber7?.assetPath; + case 8: + basePath = _data.assets.comboNumber8?.assetPath; + case 9: + basePath = _data.assets.comboNumber9?.assetPath; + default: + basePath = null; + } + + if (basePath == null) return fallback?.buildComboNumSpritePath(digit); + + var parts = basePath?.split(Constants.LIBRARY_SEPARATOR) ?? []; + if (parts.length < 1) return null; + if (parts.length == 1) return parts[0]; + + return parts[1]; + } + + public function getComboNumSpriteOffsets(digit:Int):Array + { + switch (digit) + { + case 0: + var result = _data.assets.comboNumber0?.offsets; + if (result == null && fallback != null) result = fallback.getComboNumSpriteOffsets(digit); + return result ?? [0, 0]; + case 1: + var result = _data.assets.comboNumber1?.offsets; + if (result == null && fallback != null) result = fallback.getComboNumSpriteOffsets(digit); + return result ?? [0, 0]; + case 2: + var result = _data.assets.comboNumber2?.offsets; + if (result == null && fallback != null) result = fallback.getComboNumSpriteOffsets(digit); + return result ?? [0, 0]; + case 3: + var result = _data.assets.comboNumber3?.offsets; + if (result == null && fallback != null) result = fallback.getComboNumSpriteOffsets(digit); + return result ?? [0, 0]; + case 4: + var result = _data.assets.comboNumber4?.offsets; + if (result == null && fallback != null) result = fallback.getComboNumSpriteOffsets(digit); + return result ?? [0, 0]; + case 5: + var result = _data.assets.comboNumber5?.offsets; + if (result == null && fallback != null) result = fallback.getComboNumSpriteOffsets(digit); + return result ?? [0, 0]; + case 6: + var result = _data.assets.comboNumber6?.offsets; + if (result == null && fallback != null) result = fallback.getComboNumSpriteOffsets(digit); + return result ?? [0, 0]; + case 7: + var result = _data.assets.comboNumber7?.offsets; + if (result == null && fallback != null) result = fallback.getComboNumSpriteOffsets(digit); + return result ?? [0, 0]; + case 8: + var result = _data.assets.comboNumber8?.offsets; + if (result == null && fallback != null) result = fallback.getComboNumSpriteOffsets(digit); + return result ?? [0, 0]; + case 9: + var result = _data.assets.comboNumber9?.offsets; + if (result == null && fallback != null) result = fallback.getComboNumSpriteOffsets(digit); + return result ?? [0, 0]; + default: + return [0, 0]; + } } public function destroy():Void {} @@ -322,8 +867,17 @@ class NoteStyle implements IRegistryEntry return 'NoteStyle($id)'; } - static function _fetchData(id:String):Null + static function _fetchData(id:String):NoteStyleData { - return NoteStyleRegistry.instance.parseEntryDataWithMigration(id, NoteStyleRegistry.instance.fetchEntryVersion(id)); + var result = NoteStyleRegistry.instance.parseEntryDataWithMigration(id, NoteStyleRegistry.instance.fetchEntryVersion(id)); + + if (result == null) + { + throw 'Could not parse note style data for id: $id'; + } + else + { + return result; + } } } diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index 2e7e13f51..841600848 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -277,7 +277,8 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry