From 4cd2a622a966433a14dfa8ff5d656bf3a2e56b98 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 28 Mar 2024 04:34:20 -0400 Subject: [PATCH 1/6] Update ART submodule! --- art | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/art b/art index 03e7c2a23..00463685f 160000 --- a/art +++ b/art @@ -1 +1 @@ -Subproject commit 03e7c2a2353b184e45955c96d763b7cdf1acbc34 +Subproject commit 00463685fa570f0c853d08e250b46ef80f30bc48 From 8a9eee3aa3aab6280139fbc33b9c45e8a33017f9 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 28 Mar 2024 04:34:43 -0400 Subject: [PATCH 2/6] Rework Funkin Preloader and added "Touch to Play" graphic --- Project.xml | 12 +- source/funkin/Preloader.hx | 65 -- .../ui/transition/preload/FunkinPreloader.hx | 994 ++++++++++++++++++ source/funkin/ui/transition/preload/README.md | 17 + source/funkin/util/Constants.hx | 64 +- source/funkin/util/MathUtil.hx | 30 + 6 files changed, 1114 insertions(+), 68 deletions(-) delete mode 100644 source/funkin/Preloader.hx create mode 100644 source/funkin/ui/transition/preload/FunkinPreloader.hx create mode 100644 source/funkin/ui/transition/preload/README.md diff --git a/Project.xml b/Project.xml index ffc8382a4..c0da3c89a 100644 --- a/Project.xml +++ b/Project.xml @@ -4,12 +4,19 @@ - + + + + - + @@ -95,6 +102,7 @@ + diff --git a/source/funkin/Preloader.hx b/source/funkin/Preloader.hx deleted file mode 100644 index 157015ba6..000000000 --- a/source/funkin/Preloader.hx +++ /dev/null @@ -1,65 +0,0 @@ -package funkin; - -import flash.Lib; -import flash.display.Bitmap; -import flash.display.BitmapData; -import flash.display.Sprite; -import flixel.system.FlxBasePreloader; -import openfl.display.Sprite; -import funkin.util.CLIUtil; -import openfl.text.TextField; -import openfl.text.TextFormat; -import flixel.system.FlxAssets; - -@:bitmap('art/preloaderArt.png') -class LogoImage extends BitmapData {} - -class Preloader extends FlxBasePreloader -{ - public function new(MinDisplayTime:Float = 0, ?AllowedURLs:Array) - { - super(MinDisplayTime, AllowedURLs); - - CLIUtil.resetWorkingDir(); // Bug fix for drag-and-drop. - } - - var logo:Sprite; - var _text:TextField; - - override function create():Void - { - this._width = Lib.current.stage.stageWidth; - this._height = Lib.current.stage.stageHeight; - - _text = new TextField(); - _text.width = 500; - _text.text = "Loading FNF"; - _text.defaultTextFormat = new TextFormat(FlxAssets.FONT_DEFAULT, 16, 0xFFFFFFFF); - _text.embedFonts = true; - _text.selectable = false; - _text.multiline = false; - _text.wordWrap = false; - _text.autoSize = LEFT; - _text.x = 2; - _text.y = 2; - addChild(_text); - - var ratio:Float = this._width / 2560; // This allows us to scale assets depending on the size of the screen. - - logo = new Sprite(); - logo.addChild(new Bitmap(new LogoImage(0, 0))); // Sets the graphic of the sprite to a Bitmap object, which uses our embedded BitmapData class. - logo.scaleX = logo.scaleY = ratio; - logo.x = ((this._width) / 2) - ((logo.width) / 2); - logo.y = (this._height / 2) - ((logo.height) / 2); - // addChild(logo); // Adds the graphic to the NMEPreloader's buffer. - - super.create(); - } - - override function update(Percent:Float):Void - { - _text.text = "FNF: " + Math.round(Percent * 100) + "%"; - - super.update(Percent); - } -} diff --git a/source/funkin/ui/transition/preload/FunkinPreloader.hx b/source/funkin/ui/transition/preload/FunkinPreloader.hx new file mode 100644 index 000000000..89a8c1140 --- /dev/null +++ b/source/funkin/ui/transition/preload/FunkinPreloader.hx @@ -0,0 +1,994 @@ +package funkin.ui.transition.preload; + +import openfl.events.MouseEvent; +import flash.display.Bitmap; +import flash.display.BitmapData; +import flash.display.BlendMode; +import flash.display.Sprite; +import flash.Lib; +import flixel.system.FlxBasePreloader; +import funkin.modding.PolymodHandler; +import funkin.play.character.CharacterData.CharacterDataParser; +import funkin.util.MathUtil; +import lime.app.Future; +import lime.math.Rectangle; +import openfl.display.Sprite; +import openfl.text.TextField; +import openfl.text.TextFormat; +import openfl.text.TextFormatAlign; + +using StringTools; + +// Annotation embeds the asset in the executable for faster loading. +// Polymod can't override this, so we can't use this technique elsewhere. + +@:bitmap("art/preloaderArt.png") +class LogoImage extends BitmapData {} + +#if TOUCH_HERE_TO_PLAY +@:bitmap('art/touchHereToPlay.png') +class TouchHereToPlayImage extends BitmapData {} +#end + +/** + * This preloader displays a logo while the game downloads assets. + */ +class FunkinPreloader extends FlxBasePreloader +{ + /** + * The logo image width at the base resolution. + * Scaled up/down appropriately as needed. + */ + static final BASE_WIDTH:Float = 1280; + + /** + * Margin at the sides and bottom, around the loading bar. + */ + static final BAR_PADDING:Float = 20; + + static final BAR_HEIGHT:Int = 20; + + /** + * Logo takes this long (in seconds) to fade in. + */ + static final LOGO_FADE_TIME:Float = 2.5; + + // Ratio between window size and BASE_WIDTH + var ratio:Float = 0; + + var currentState:FunkinPreloaderState = FunkinPreloaderState.NotStarted; + + // private var downloadingAssetsStartTime:Float = -1; + private var downloadingAssetsPercent:Float = -1; + private var downloadingAssetsComplete:Bool = false; + + private var preloadingPlayAssetsPercent:Float = -1; + private var preloadingPlayAssetsStartTime:Float = -1; + private var preloadingPlayAssetsComplete:Bool = false; + + private var cachingGraphicsPercent:Float = -1; + private var cachingGraphicsStartTime:Float = -1; + private var cachingGraphicsComplete:Bool = false; + + private var cachingAudioPercent:Float = -1; + private var cachingAudioStartTime:Float = -1; + private var cachingAudioComplete:Bool = false; + + private var cachingDataPercent:Float = -1; + private var cachingDataStartTime:Float = -1; + private var cachingDataComplete:Bool = false; + + private var parsingSpritesheetsPercent:Float = -1; + private var parsingSpritesheetsStartTime:Float = -1; + private var parsingSpritesheetsComplete:Bool = false; + + private var parsingStagesPercent:Float = -1; + private var parsingStagesStartTime:Float = -1; + private var parsingStagesComplete:Bool = false; + + private var parsingCharactersPercent:Float = -1; + private var parsingCharactersStartTime:Float = -1; + private var parsingCharactersComplete:Bool = false; + + private var parsingSongsPercent:Float = -1; + private var parsingSongsStartTime:Float = -1; + private var parsingSongsComplete:Bool = false; + + private var initializingScriptsPercent:Float = -1; + + private var cachingCoreAssetsPercent:Float = -1; + + /** + * The timestamp when the other steps completed and the `Finishing up` step started. + */ + private var completeTime:Float = -1; + + // Graphics + var logo:Bitmap; + #if TOUCH_HERE_TO_PLAY + var touchHereToPlay:Bitmap; + #end + var progressBar:Bitmap; + var progressLeftText:TextField; + var progressRightText:TextField; + + public function new() + { + super(Constants.PRELOADER_MIN_STAGE_TIME, Constants.SITE_LOCK); + + // We can't even call trace() yet, until Flixel loads. + trace('Initializing custom preloader...'); + + this.siteLockTitleText = Constants.SITE_LOCK_TITLE; + this.siteLockBodyText = Constants.SITE_LOCK_DESC; + } + + override function create():Void + { + // Nothing happens in the base preloader. + super.create(); + + // Background color. + Lib.current.stage.color = Constants.COLOR_PRELOADER_BG; + + // Width and height of the preloader. + this._width = Lib.current.stage.stageWidth; + this._height = Lib.current.stage.stageHeight; + + // Scale assets to the screen size. + ratio = this._width / BASE_WIDTH / 2.0; + + // Create the logo. + logo = createBitmap(LogoImage, function(bmp:Bitmap) { + // Scale and center the logo. + // We have to do this inside the async call, after the image size is known. + bmp.scaleX = bmp.scaleY = ratio; + bmp.x = (this._width - bmp.width) / 2; + bmp.y = (this._height - bmp.height) / 2; + }); + addChild(logo); + + #if TOUCH_HERE_TO_PLAY + touchHereToPlay = createBitmap(TouchHereToPlayImage, function(bmp:Bitmap) { + // Scale and center the touch to start image. + // We have to do this inside the async call, after the image size is known. + bmp.scaleX = bmp.scaleY = ratio; + bmp.x = (this._width - bmp.width) / 2; + bmp.y = (this._height - bmp.height) / 2; + }); + touchHereToPlay.alpha = 0.0; + addChild(touchHereToPlay); + #end + + // Create the progress bar. + progressBar = new Bitmap(new BitmapData(1, BAR_HEIGHT, true, Constants.COLOR_PRELOADER_BAR)); + progressBar.x = BAR_PADDING; + progressBar.y = this._height - BAR_PADDING - BAR_HEIGHT; + addChild(progressBar); + + // Create the progress message. + progressLeftText = new TextField(); + + var progressLeftTextFormat = new TextFormat("VCR OSD Mono", 16, Constants.COLOR_PRELOADER_BAR, true); + progressLeftTextFormat.align = TextFormatAlign.LEFT; + progressLeftText.defaultTextFormat = progressLeftTextFormat; + + progressLeftText.selectable = false; + progressLeftText.width = this._width - BAR_PADDING * 2; + progressLeftText.text = 'Downloading assets...'; + progressLeftText.x = BAR_PADDING; + progressLeftText.y = this._height - BAR_PADDING - BAR_HEIGHT - 16 - 4; + addChild(progressLeftText); + + // Create the progress %. + progressRightText = new TextField(); + + var progressRightTextFormat = new TextFormat("VCR OSD Mono", 16, Constants.COLOR_PRELOADER_BAR, true); + progressRightTextFormat.align = TextFormatAlign.RIGHT; + progressRightText.defaultTextFormat = progressRightTextFormat; + + progressRightText.selectable = false; + progressRightText.width = this._width - BAR_PADDING * 2; + progressRightText.text = '0%'; + progressRightText.x = BAR_PADDING; + progressRightText.y = this._height - BAR_PADDING - BAR_HEIGHT - 16 - 4; + addChild(progressRightText); + } + + var lastElapsed:Float = 0.0; + + override function update(percent:Float):Void + { + var elapsed:Float = (Date.now().getTime() - this._startTime) / 1000.0; + // trace('Time since last frame: ' + (lastElapsed - elapsed)); + + downloadingAssetsPercent = percent; + var loadPercent:Float = updateState(percent, elapsed); + updateGraphics(loadPercent, elapsed); + + lastElapsed = elapsed; + } + + function updateState(percent:Float, elapsed:Float):Float + { + switch (currentState) + { + case FunkinPreloaderState.NotStarted: + if (downloadingAssetsPercent > 0.0) currentState = FunkinPreloaderState.DownloadingAssets; + + return percent; + + case FunkinPreloaderState.DownloadingAssets: + // Sometimes percent doesn't go to 100%, it's a floating point error. + if (downloadingAssetsPercent >= 1.0 + || (elapsed > Constants.PRELOADER_MIN_STAGE_TIME + && downloadingAssetsComplete)) currentState = FunkinPreloaderState.PreloadingPlayAssets; + + return percent; + + case FunkinPreloaderState.PreloadingPlayAssets: + if (preloadingPlayAssetsPercent < 0.0) + { + preloadingPlayAssetsStartTime = elapsed; + preloadingPlayAssetsPercent = 0.0; + + // This is quick enough to do synchronously. + // Assets.initialize(); + + /* + // Make a future to retrieve the manifest + var future:Future = Assets.preloadLibrary('gameplay'); + + future.onProgress((loaded:Int, total:Int) -> { + preloadingPlayAssetsPercent = loaded / total; + }); + future.onComplete((library:lime.utils.AssetLibrary) -> { + }); + */ + + // TODO: Reimplement this. + preloadingPlayAssetsPercent = 1.0; + preloadingPlayAssetsComplete = true; + return 0.0; + } + else if (Constants.PRELOADER_MIN_STAGE_TIME > 0) + { + var elapsedPreloadingPlayAssets:Float = elapsed - preloadingPlayAssetsStartTime; + if (preloadingPlayAssetsComplete && elapsedPreloadingPlayAssets >= Constants.PRELOADER_MIN_STAGE_TIME) + { + currentState = FunkinPreloaderState.InitializingScripts; + return 0.0; + } + else + { + // We need to return SIMULATED progress here. + if (preloadingPlayAssetsPercent < (elapsedPreloadingPlayAssets / Constants.PRELOADER_MIN_STAGE_TIME)) return preloadingPlayAssetsPercent; + else + return elapsedPreloadingPlayAssets / Constants.PRELOADER_MIN_STAGE_TIME; + } + } + else + { + if (preloadingPlayAssetsComplete) currentState = FunkinPreloaderState.InitializingScripts; + } + + return preloadingPlayAssetsPercent; + + case FunkinPreloaderState.InitializingScripts: + if (initializingScriptsPercent < 0.0) + { + initializingScriptsPercent = 0.0; + + /* + var future:Future> = []; // PolymodHandler.loadNoModsAsync(); + + future.onProgress((loaded:Int, total:Int) -> { + trace('PolymodHandler.loadNoModsAsync() progress: ' + loaded + '/' + total); + initializingScriptsPercent = loaded / total; + }); + future.onComplete((result:Array) -> { + trace('Completed initializing scripts: ' + result); + }); + */ + + initializingScriptsPercent = 1.0; + currentState = FunkinPreloaderState.CachingGraphics; + return 0.0; + } + + return initializingScriptsPercent; + + case CachingGraphics: + if (cachingGraphicsPercent < 0) + { + cachingGraphicsPercent = 0.0; + cachingGraphicsStartTime = elapsed; + + /* + var assetsToCache:Array = []; // Assets.listGraphics('core'); + + var future:Future> = []; // Assets.cacheAssets(assetsToCache); + future.onProgress((loaded:Int, total:Int) -> { + cachingGraphicsPercent = loaded / total; + }); + future.onComplete((_result) -> { + trace('Completed caching graphics.'); + }); + */ + + // TODO: Reimplement this. + cachingGraphicsPercent = 1.0; + cachingGraphicsComplete = true; + return 0.0; + } + else if (Constants.PRELOADER_MIN_STAGE_TIME > 0) + { + var elapsedCachingGraphics:Float = elapsed - cachingGraphicsStartTime; + if (cachingGraphicsComplete && elapsedCachingGraphics >= Constants.PRELOADER_MIN_STAGE_TIME) + { + currentState = FunkinPreloaderState.CachingAudio; + return 0.0; + } + else + { + if (cachingGraphicsPercent < (elapsedCachingGraphics / Constants.PRELOADER_MIN_STAGE_TIME)) + { + // Return real progress if it's lower. + return cachingGraphicsPercent; + } + else + { + // Return simulated progress if it's higher. + return elapsedCachingGraphics / Constants.PRELOADER_MIN_STAGE_TIME; + } + } + } + else + { + if (cachingGraphicsComplete) + { + currentState = FunkinPreloaderState.CachingAudio; + return 0.0; + } + else + { + return cachingGraphicsPercent; + } + } + + case CachingAudio: + if (cachingAudioPercent < 0) + { + cachingAudioPercent = 0.0; + cachingAudioStartTime = elapsed; + + var assetsToCache:Array = []; // Assets.listSound('core'); + + /* + var future:Future> = []; // Assets.cacheAssets(assetsToCache); + + future.onProgress((loaded:Int, total:Int) -> { + cachingAudioPercent = loaded / total; + }); + future.onComplete((_result) -> { + trace('Completed caching audio.'); + }); + */ + + // TODO: Reimplement this. + cachingAudioPercent = 1.0; + cachingAudioComplete = true; + return 0.0; + } + else if (Constants.PRELOADER_MIN_STAGE_TIME > 0) + { + var elapsedCachingAudio:Float = elapsed - cachingAudioStartTime; + if (cachingAudioComplete && elapsedCachingAudio >= Constants.PRELOADER_MIN_STAGE_TIME) + { + currentState = FunkinPreloaderState.CachingData; + return 0.0; + } + else + { + // We need to return SIMULATED progress here. + if (cachingAudioPercent < (elapsedCachingAudio / Constants.PRELOADER_MIN_STAGE_TIME)) + { + return cachingAudioPercent; + } + else + { + return elapsedCachingAudio / Constants.PRELOADER_MIN_STAGE_TIME; + } + } + } + else + { + if (cachingAudioComplete) + { + currentState = FunkinPreloaderState.CachingData; + return 0.0; + } + else + { + return cachingAudioPercent; + } + } + + case CachingData: + if (cachingDataPercent < 0) + { + cachingDataPercent = 0.0; + cachingDataStartTime = elapsed; + + var assetsToCache:Array = []; + var sparrowFramesToCache:Array = []; + + // Core files + // assetsToCache = assetsToCache.concat(Assets.listText('core')); + // assetsToCache = assetsToCache.concat(Assets.listJSON('core')); + // Core spritesheets + // assetsToCache = assetsToCache.concat(Assets.listXML('core')); + + // Gameplay files + // assetsToCache = assetsToCache.concat(Assets.listText('gameplay')); + // assetsToCache = assetsToCache.concat(Assets.listJSON('gameplay')); + // We're not caching gameplay spritesheets here because they're fetched on demand. + + /* + var future:Future> = []; + // Assets.cacheAssets(assetsToCache, true); + future.onProgress((loaded:Int, total:Int) -> { + cachingDataPercent = loaded / total; + }); + future.onComplete((_result) -> { + trace('Completed caching data.'); + }); + */ + cachingDataPercent = 1.0; + cachingDataComplete = true; + return 0.0; + } + else if (Constants.PRELOADER_MIN_STAGE_TIME > 0) + { + var elapsedCachingData:Float = elapsed - cachingDataStartTime; + if (cachingDataComplete && elapsedCachingData >= Constants.PRELOADER_MIN_STAGE_TIME) + { + currentState = FunkinPreloaderState.ParsingSpritesheets; + return 0.0; + } + else + { + // We need to return SIMULATED progress here. + if (cachingDataPercent < (elapsedCachingData / Constants.PRELOADER_MIN_STAGE_TIME)) return cachingDataPercent; + else + return elapsedCachingData / Constants.PRELOADER_MIN_STAGE_TIME; + } + } + else + { + if (cachingDataComplete) + { + currentState = FunkinPreloaderState.ParsingSpritesheets; + return 0.0; + } + } + + return cachingDataPercent; + + case ParsingSpritesheets: + if (parsingSpritesheetsPercent < 0) + { + parsingSpritesheetsPercent = 0.0; + parsingSpritesheetsStartTime = elapsed; + + // Core spritesheets + var sparrowFramesToCache = []; // Assets.listXML('core').map((xml:String) -> xml.replace('.xml', '').replace('core:assets/core/', '')); + // We're not caching gameplay spritesheets here because they're fetched on demand. + + /* + var future:Future> = []; // Assets.cacheSparrowFrames(sparrowFramesToCache, true); + future.onProgress((loaded:Int, total:Int) -> { + parsingSpritesheetsPercent = loaded / total; + }); + future.onComplete((_result) -> { + trace('Completed parsing spritesheets.'); + }); + */ + parsingSpritesheetsPercent = 1.0; + parsingSpritesheetsComplete = true; + return 0.0; + } + else if (Constants.PRELOADER_MIN_STAGE_TIME > 0) + { + var elapsedParsingSpritesheets:Float = elapsed - parsingSpritesheetsStartTime; + if (parsingSpritesheetsComplete && elapsedParsingSpritesheets >= Constants.PRELOADER_MIN_STAGE_TIME) + { + currentState = FunkinPreloaderState.ParsingStages; + return 0.0; + } + else + { + // We need to return SIMULATED progress here. + if (parsingSpritesheetsPercent < (elapsedParsingSpritesheets / Constants.PRELOADER_MIN_STAGE_TIME)) return parsingSpritesheetsPercent; + else + return elapsedParsingSpritesheets / Constants.PRELOADER_MIN_STAGE_TIME; + } + } + else + { + if (parsingSpritesheetsComplete) + { + currentState = FunkinPreloaderState.ParsingStages; + return 0.0; + } + } + + return parsingSpritesheetsPercent; + + case ParsingStages: + if (parsingStagesPercent < 0) + { + parsingStagesPercent = 0.0; + parsingStagesStartTime = elapsed; + + /* + // TODO: Reimplement this. + var future:Future> = []; // StageDataParser.loadStageCacheAsync(); + + future.onProgress((loaded:Int, total:Int) -> { + parsingStagesPercent = loaded / total; + }); + + future.onComplete((_result) -> { + trace('Completed parsing stages.'); + }); + */ + + parsingStagesPercent = 1.0; + parsingStagesComplete = true; + return 0.0; + } + else if (Constants.PRELOADER_MIN_STAGE_TIME > 0) + { + var elapsedParsingStages:Float = elapsed - parsingStagesStartTime; + if (parsingStagesComplete && elapsedParsingStages >= Constants.PRELOADER_MIN_STAGE_TIME) + { + currentState = FunkinPreloaderState.ParsingCharacters; + return 0.0; + } + else + { + // We need to return SIMULATED progress here. + if (parsingStagesPercent < (elapsedParsingStages / Constants.PRELOADER_MIN_STAGE_TIME)) return parsingStagesPercent; + else + return elapsedParsingStages / Constants.PRELOADER_MIN_STAGE_TIME; + } + } + else + { + if (parsingStagesComplete) + { + currentState = FunkinPreloaderState.ParsingCharacters; + return 0.0; + } + } + + return parsingStagesPercent; + + case ParsingCharacters: + if (parsingCharactersPercent < 0) + { + parsingCharactersPercent = 0.0; + parsingCharactersStartTime = elapsed; + + /* + // TODO: Reimplement this. + var future:Future> = []; // CharacterDataParser.loadCharacterCacheAsync(); + + future.onProgress((loaded:Int, total:Int) -> { + parsingCharactersPercent = loaded / total; + }); + + future.onComplete((_result) -> { + trace('Completed parsing characters.'); + }); + */ + + parsingCharactersPercent = 1.0; + parsingCharactersComplete = true; + return 0.0; + } + else if (Constants.PRELOADER_MIN_STAGE_TIME > 0) + { + var elapsedParsingCharacters:Float = elapsed - parsingCharactersStartTime; + if (parsingCharactersComplete && elapsedParsingCharacters >= Constants.PRELOADER_MIN_STAGE_TIME) + { + currentState = FunkinPreloaderState.ParsingSongs; + return 0.0; + } + else + { + // We need to return SIMULATED progress here. + if (parsingCharactersPercent < (elapsedParsingCharacters / Constants.PRELOADER_MIN_STAGE_TIME)) return parsingCharactersPercent; + else + return elapsedParsingCharacters / Constants.PRELOADER_MIN_STAGE_TIME; + } + } + else + { + if (parsingStagesComplete) + { + currentState = FunkinPreloaderState.ParsingSongs; + return 0.0; + } + } + + return parsingCharactersPercent; + + case ParsingSongs: + if (parsingSongsPercent < 0) + { + parsingSongsPercent = 0.0; + parsingSongsStartTime = elapsed; + + /* + // TODO: Reimplement this. + var future:Future> = ; + // SongDataParser.loadSongCacheAsync(); + + future.onProgress((loaded:Int, total:Int) -> { + parsingSongsPercent = loaded / total; + }); + + future.onComplete((_result) -> { + trace('Completed parsing songs.'); + }); + */ + + parsingSongsPercent = 1.0; + parsingSongsComplete = true; + + return 0.0; + } + else if (Constants.PRELOADER_MIN_STAGE_TIME > 0) + { + var elapsedParsingSongs:Float = elapsed - parsingSongsStartTime; + if (parsingSongsComplete && elapsedParsingSongs >= Constants.PRELOADER_MIN_STAGE_TIME) + { + currentState = FunkinPreloaderState.Complete; + return 0.0; + } + else + { + // We need to return SIMULATED progress here. + if (parsingSongsPercent < (elapsedParsingSongs / Constants.PRELOADER_MIN_STAGE_TIME)) + { + return parsingSongsPercent; + } + else + { + return elapsedParsingSongs / Constants.PRELOADER_MIN_STAGE_TIME; + } + } + } + else + { + if (parsingSongsComplete) + { + currentState = FunkinPreloaderState.Complete; + return 0.0; + } + else + { + return parsingSongsPercent; + } + } + case FunkinPreloaderState.Complete: + if (completeTime < 0) + { + completeTime = elapsed; + } + + return 1.0; + #if TOUCH_HERE_TO_PLAY + case FunkinPreloaderState.TouchHereToPlay: + if (completeTime < 0) + { + completeTime = elapsed; + } + + if (touchHereToPlay.alpha < 1.0) + { + touchHereToPlay.alpha = 1.0; + + addEventListener(MouseEvent.CLICK, onTouchHereToPlay); + } + + return 1.0; + #end + + default: + // Do nothing. + } + + return 0.0; + } + + #if TOUCH_HERE_TO_PLAY + function onTouchHereToPlay(e:MouseEvent):Void + { + removeEventListener(MouseEvent.CLICK, onTouchHereToPlay); + + // This is the actual thing that makes the game load. + immediatelyStartGame(); + } + #end + + static final TOTAL_STEPS:Int = 11; + static final ELLIPSIS_TIME:Float = 0.5; + + function updateGraphics(percent:Float, elapsed:Float):Void + { + // Render logo (including transitions) + if (completeTime > 0.0) + { + var elapsedFinished:Float = renderLogoFadeOut(elapsed); + // trace('Fading out logo... (' + elapsedFinished + 's)'); + if (elapsedFinished > LOGO_FADE_TIME) + { + #if TOUCH_HERE_TO_PLAY + // The logo has faded out, but we're not quite done yet. + // In order to prevent autoplay issues, we need the user to click after the loading finishes. + currentState = FunkinPreloaderState.TouchHereToPlay; + #else + immediatelyStartGame(); + #end + } + } + else + { + renderLogoFadeIn(elapsed); + } + + // Render progress bar + var maxWidth = this._width - BAR_PADDING * 2; + var barWidth = maxWidth * percent; + progressBar.width = barWidth; + + // Cycle ellipsis count to show loading + var ellipsisCount:Int = Std.int(elapsed / ELLIPSIS_TIME) % 3 + 1; + var ellipsis:String = ''; + for (i in 0...ellipsisCount) + ellipsis += '.'; + + // Render status text + switch (currentState) + { + // case FunkinPreloaderState.NotStarted: + default: + updateProgressLeftText('Loading (0/$TOTAL_STEPS)$ellipsis'); + case FunkinPreloaderState.DownloadingAssets: + updateProgressLeftText('Downloading assets (1/$TOTAL_STEPS)$ellipsis'); + case FunkinPreloaderState.PreloadingPlayAssets: + updateProgressLeftText('Preloading assets (2/$TOTAL_STEPS)$ellipsis'); + case FunkinPreloaderState.InitializingScripts: + updateProgressLeftText('Initializing scripts (3/$TOTAL_STEPS)$ellipsis'); + case FunkinPreloaderState.CachingGraphics: + updateProgressLeftText('Caching graphics (4/$TOTAL_STEPS)$ellipsis'); + case FunkinPreloaderState.CachingAudio: + updateProgressLeftText('Caching audio (5/$TOTAL_STEPS)$ellipsis'); + case FunkinPreloaderState.CachingData: + updateProgressLeftText('Caching data (6/$TOTAL_STEPS)$ellipsis'); + case FunkinPreloaderState.ParsingSpritesheets: + updateProgressLeftText('Parsing spritesheets (7/$TOTAL_STEPS)$ellipsis'); + case FunkinPreloaderState.ParsingStages: + updateProgressLeftText('Parsing stages (8/$TOTAL_STEPS)$ellipsis'); + case FunkinPreloaderState.ParsingCharacters: + updateProgressLeftText('Parsing characters (9/$TOTAL_STEPS)$ellipsis'); + case FunkinPreloaderState.ParsingSongs: + updateProgressLeftText('Parsing songs (10/$TOTAL_STEPS)$ellipsis'); + case FunkinPreloaderState.Complete: + updateProgressLeftText('Finishing up ($TOTAL_STEPS/$TOTAL_STEPS)$ellipsis'); + #if TOUCH_HERE_TO_PLAY + case FunkinPreloaderState.TouchHereToPlay: + updateProgressLeftText(null); + #end + } + + var percentage:Int = Math.floor(percent * 100); + trace('Preloader state: ' + currentState + ' (' + percentage + '%, ' + elapsed + 's)'); + + // Render percent text + progressRightText.text = '$percentage%'; + + super.update(percent); + } + + function updateProgressLeftText(text:Null):Void + { + if (progressLeftText != null) + { + if (text == null) + { + progressLeftText.alpha = 0.0; + } + else if (progressLeftText.text != text) + { + // We have to keep updating the text format, because the font can take a frame or two to load. + var progressLeftTextFormat = new TextFormat("VCR OSD Mono", 16, Constants.COLOR_PRELOADER_BAR, true); + progressLeftTextFormat.align = TextFormatAlign.LEFT; + progressLeftText.defaultTextFormat = progressLeftTextFormat; + progressLeftText.text = text; + } + } + } + + function immediatelyStartGame():Void + { + _loaded = true; + } + + /** + * Fade out the logo. + * @param elapsed Elapsed time since the preloader started. + * @return Elapsed time since the logo started fading out. + */ + function renderLogoFadeOut(elapsed:Float):Float + { + // Fade-out takes LOGO_FADE_TIME seconds. + var elapsedFinished = elapsed - completeTime; + + logo.alpha = 1.0 - MathUtil.easeInOutCirc(elapsedFinished / LOGO_FADE_TIME); + logo.scaleX = (1.0 - MathUtil.easeInBack(elapsedFinished / LOGO_FADE_TIME)) * ratio; + logo.scaleY = (1.0 - MathUtil.easeInBack(elapsedFinished / LOGO_FADE_TIME)) * ratio; + logo.x = (this._width - logo.width) / 2; + logo.y = (this._height - logo.height) / 2; + + // Fade out progress bar too. + progressBar.alpha = logo.alpha; + progressLeftText.alpha = logo.alpha; + progressRightText.alpha = logo.alpha; + + return elapsedFinished; + } + + function renderLogoFadeIn(elapsed:Float):Void + { + // Fade-in takes LOGO_FADE_TIME seconds. + logo.alpha = MathUtil.easeInOutCirc(elapsed / LOGO_FADE_TIME); + logo.scaleX = MathUtil.easeOutBack(elapsed / LOGO_FADE_TIME) * ratio; + logo.scaleY = MathUtil.easeOutBack(elapsed / LOGO_FADE_TIME) * ratio; + logo.x = (this._width - logo.width) / 2; + logo.y = (this._height - logo.height) / 2; + } + + #if html5 + // These fields only exist on Web builds. + + /** + * Format the layout of the site lock screen. + */ + override function createSiteLockFailureScreen():Void + { + addChild(createSiteLockFailureBackground(Constants.COLOR_PRELOADER_LOCK_BG, Constants.COLOR_PRELOADER_LOCK_BG)); + addChild(createSiteLockFailureIcon(Constants.COLOR_PRELOADER_LOCK_FG, 0.9)); + addChild(createSiteLockFailureText(30)); + } + + /** + * Format the text of the site lock screen. + */ + override function adjustSiteLockTextFields(titleText:TextField, bodyText:TextField, hyperlinkText:TextField):Void + { + var titleFormat = titleText.defaultTextFormat; + titleFormat.align = TextFormatAlign.CENTER; + titleFormat.color = Constants.COLOR_PRELOADER_LOCK_FONT; + titleText.setTextFormat(titleFormat); + + var bodyFormat = bodyText.defaultTextFormat; + bodyFormat.align = TextFormatAlign.CENTER; + bodyFormat.color = Constants.COLOR_PRELOADER_LOCK_FONT; + bodyText.setTextFormat(bodyFormat); + + var hyperlinkFormat = hyperlinkText.defaultTextFormat; + hyperlinkFormat.align = TextFormatAlign.CENTER; + hyperlinkFormat.color = Constants.COLOR_PRELOADER_LOCK_LINK; + hyperlinkText.setTextFormat(hyperlinkFormat); + } + #end + + override function destroy():Void + { + // Ensure the graphics are properly destroyed and GC'd. + removeChild(logo); + removeChild(progressBar); + logo = progressBar = null; + super.destroy(); + } + + override function onLoaded():Void + { + super.onLoaded(); + // We're not ACTUALLY finished. + // This function gets called when the DownloadingAssets step is done. + // We need to wait for the other steps, then the logo to fade out. + _loaded = false; + downloadingAssetsComplete = true; + } +} + +enum FunkinPreloaderState +{ + /** + * The state before downloading has begun. + * Moves to either `DownloadingAssets` or `CachingGraphics` based on platform. + */ + NotStarted; + + /** + * Downloading assets. + * On HTML5, Lime will do this for us, before calling `onLoaded`. + * On Desktop, this step will be completed immediately, and we'll go straight to `CachingGraphics`. + */ + DownloadingAssets; + + /** + * Preloading play assets. + * Loads the `manifest.json` for the `gameplay` library. + * If we make the base preloader do this, it will download all the assets as well, + * so we have to do it ourselves. + */ + PreloadingPlayAssets; + + /** + * Loading FireTongue, loading Polymod, parsing and instantiating module scripts. + */ + InitializingScripts; + + /** + * Loading all graphics from the `core` library to the cache. + */ + CachingGraphics; + + /** + * Loading all audio from the `core` library to the cache. + */ + CachingAudio; + + /** + * Loading all data files from the `core` library to the cache. + */ + CachingData; + + /** + * Parsing all XML files from the `core` library into FlxFramesCollections and caching them. + */ + ParsingSpritesheets; + + /** + * Parsing stage data and scripts. + */ + ParsingStages; + + /** + * Parsing character data and scripts. + */ + ParsingCharacters; + + /** + * Parsing song data and scripts. + */ + ParsingSongs; + + /** + * Finishing up. + */ + Complete; + + #if TOUCH_HERE_TO_PLAY + /** + * Touch Here to Play is displayed. + */ + TouchHereToPlay; + #end +} diff --git a/source/funkin/ui/transition/preload/README.md b/source/funkin/ui/transition/preload/README.md new file mode 100644 index 000000000..91efcf8e8 --- /dev/null +++ b/source/funkin/ui/transition/preload/README.md @@ -0,0 +1,17 @@ +# funkin.ui.loading.preload + +This package contains code powering the HTML5 preloader screen. + +The preloader performs the following tasks: +- **Downloading assets**: Downloads the `core` asset library and loads its manifest +- **Preloading play assets**: Downloads the `gameplay` asset library (manifest only) +- **Initializing scripts**: Downloads and registers stage scripts, character scripts, song scripts, and module scripts. +- **Caching graphics**: Downloads all graphics from the `core` asset library, uploads them to the GPU, then dumps them from RAM. This prepares them to be used very quickly in-game. +- **Caching audio**: Downloads all audio files from the `core` asset library, and caches them. This prepares them to be used very quickly in-game. +- **Caching data**: Downloads and caches all TXT files, all JSON files (it also parses them), and XML files (from the `core` library only). This prepares them to be used in the next steps. +- **Parsing stages**: Parses all stage data and instantiates associated stage scripts. This prepares them to be used in-game. +- **Parsing characters**: Parses all character data and instantiates associated character scripts. This prepares them to be used in-game. +- **Parsing songs**: Parses all song data and instantiates associated song scripts. This prepares them to be used in-game. +- **Finishing up**: Waits for the screen to fade out. Then, it loads the first state of the app. + +Due to the first few steps not being relevant on desktop, and due to this preloader being built in Lime rather than HaxeFlixel because of how Lime handles asset loading, this preloader is not used on desktop. The splash loader is used instead. diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx index c7bc03139..5d355f2da 100644 --- a/source/funkin/util/Constants.hx +++ b/source/funkin/util/Constants.hx @@ -1,8 +1,9 @@ package funkin.util; +import flixel.system.FlxBasePreloader; import flixel.util.FlxColor; -import lime.app.Application; import funkin.data.song.SongData.SongTimeFormat; +import lime.app.Application; /** * A store of unchanging, globally relevant values. @@ -59,6 +60,16 @@ class Constants */ // ============================== + /** + * Preloader sitelock. + * Matching is done by `FlxStringUtil.getDomain`, so any URL on the domain will work. + * The first link in this list is the one users will be redirected to if they try to access the game from a different URL. + */ + public static final SITE_LOCK:Array = [ + "https://www.newgrounds.com/portal/view/770371", // Newgrounds, baybee! + FlxBasePreloader.LOCAL // localhost for dev stuff + ]; + /** * Link to download the game on Itch.io. */ @@ -116,6 +127,44 @@ class Constants 0xFFCC1111 // right (3) ]; + /** + * Color for the preloader background + */ + public static final COLOR_PRELOADER_BG:FlxColor = 0xFF000000; + + /** + * Color for the preloader progress bar + */ + public static final COLOR_PRELOADER_BAR:FlxColor = 0xFF00FF00; + + /** + * Color for the preloader site lock background + */ + public static final COLOR_PRELOADER_LOCK_BG:FlxColor = 0xFF1B1717; + + /** + * Color for the preloader site lock foreground + */ + public static final COLOR_PRELOADER_LOCK_FG:FlxColor = 0xB96F10; + + /** + * Color for the preloader site lock text + */ + public static final COLOR_PRELOADER_LOCK_FONT:FlxColor = 0xCCCCCC; + + /** + * Color for the preloader site lock link + */ + public static final COLOR_PRELOADER_LOCK_LINK:FlxColor = 0xEEB211; + + /** + * LANGUAGE + */ + // ============================== + public static final SITE_LOCK_TITLE:String = "You Loser!"; + + public static final SITE_LOCK_DESC:String = "This isn't Newgrounds!\nGo play Friday Night Funkin' on Newgrounds:"; + /** * GAME DEFAULTS */ @@ -289,6 +338,19 @@ class Constants */ public static final MP3_DELAY_MS:Float = 528 / 44100 * Constants.MS_PER_SEC; + /** + * Each step of the preloader has to be on screen at least this long. + * + * 0 = The preloader immediately moves to the next step when it's ready. + * 1 = The preloader waits for 1 second before moving to the next step. + * The progress bare is automatically rescaled to match. + */ + #if debug + public static final PRELOADER_MIN_STAGE_TIME:Float = 1.0; + #else + public static final PRELOADER_MIN_STAGE_TIME:Float = 0.1; + #end + /** * HEALTH VALUES */ diff --git a/source/funkin/util/MathUtil.hx b/source/funkin/util/MathUtil.hx index 5db8f8d76..93b9ca666 100644 --- a/source/funkin/util/MathUtil.hx +++ b/source/funkin/util/MathUtil.hx @@ -48,6 +48,36 @@ class MathUtil return Math.log(value) / Math.log(base); } + public static function easeInOutCirc(x:Float):Float + { + if (x <= 0.0) return 0.0; + if (x >= 1.0) return 1.0; + var result:Float = (x < 0.5) ? (1 - Math.sqrt(1 - 4 * x * x)) / 2 : (Math.sqrt(1 - 4 * (1 - x) * (1 - x)) + 1) / 2; + return (result == Math.NaN) ? 1.0 : result; + } + + public static function easeInOutBack(x:Float, ?c:Float = 1.70158):Float + { + if (x <= 0.0) return 0.0; + if (x >= 1.0) return 1.0; + var result:Float = (x < 0.5) ? (2 * x * x * ((c + 1) * 2 * x - c)) / 2 : (1 - 2 * (1 - x) * (1 - x) * ((c + 1) * 2 * (1 - x) - c)) / 2; + return (result == Math.NaN) ? 1.0 : result; + } + + public static function easeInBack(x:Float, ?c:Float = 1.70158):Float + { + if (x <= 0.0) return 0.0; + if (x >= 1.0) return 1.0; + return (1 + c) * x * x * x - c * x * x; + } + + public static function easeOutBack(x:Float, ?c:Float = 1.70158):Float + { + if (x <= 0.0) return 0.0; + if (x >= 1.0) return 1.0; + return 1 + (c + 1) * Math.pow(x - 1, 3) + c * Math.pow(x - 1, 2); + } + /** * Get the base-2 logarithm of a value. * @param x value From ef2cb4d9fcce1be15de1a139adf9fdd6ead90e63 Mon Sep 17 00:00:00 2001 From: Jenny Crowe Date: Thu, 28 Mar 2024 11:05:38 -0700 Subject: [PATCH 3/6] Convert zoom modifiers from additive to multiplicative --- source/funkin/play/PlayState.hx | 94 +++++++++---------- source/funkin/play/cutscene/VideoCutscene.hx | 2 +- .../funkin/play/event/FocusCameraSongEvent.hx | 4 + .../play/event/SetCameraBopSongEvent.hx | 4 +- .../funkin/play/event/ZoomCameraSongEvent.hx | 6 +- source/funkin/util/Constants.hx | 5 +- 6 files changed, 59 insertions(+), 56 deletions(-) diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 169809a63..5b0837dcf 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -245,20 +245,26 @@ class PlayState extends MusicBeatSubState /** * The current camera zoom level without any modifiers applied. */ - public var currentCameraZoom:Float = FlxCamera.defaultZoom * 1.05; + public var currentCameraZoom:Float = FlxCamera.defaultZoom; /** - * currentCameraZoom is increased every beat, and lerped back to this value every frame, creating a smooth 'zoom-in' effect. - * Defaults to 1.05, but may be larger or smaller depending on the current stage. - * Tweened via the `ZoomCamera` song event in direct mode. + * Multiplier for currentCameraZoom for camera bops. + * Lerped back to 1.0x every frame. */ - public var defaultCameraZoom:Float = FlxCamera.defaultZoom * 1.05; + public var cameraBopMultiplier:Float = 1.0; /** - * Camera zoom applied on top of currentCameraZoom. - * Tweened via the `ZoomCamera` song event in additive mode. + * Default camera zoom for the current stage. + * If we aren't in a stage, just use the default zoom (1.05x). */ - public var additiveCameraZoom:Float = 0; + public var stageZoom(get, never):Float; + + function get_stageZoom():Float + { + if (currentStage != null) return currentStage.camZoom; + else + return FlxCamera.defaultZoom * 1.05; + } /** * The current HUD camera zoom level. @@ -268,16 +274,18 @@ class PlayState extends MusicBeatSubState public var defaultHUDCameraZoom:Float = FlxCamera.defaultZoom * 1.0; /** - * Intensity of the gameplay camera zoom. - * @default `1.5%` + * Camera bop intensity multiplier. + * Applied to cameraBopMultiplier on camera bops (usually every beat). + * @default `101.5%` */ - public var cameraZoomIntensity:Float = Constants.DEFAULT_ZOOM_INTENSITY; + public var cameraBopIntensity:Float = Constants.DEFAULT_BOP_INTENSITY; /** * Intensity of the HUD camera zoom. + * Need to make this a multiplier later. Just shoving in 0.015 for now so it doesn't break. * @default `3.0%` */ - public var hudCameraZoomIntensity:Float = Constants.DEFAULT_ZOOM_INTENSITY * 2.0; + public var hudCameraZoomIntensity:Float = 0.015 * 2.0; /** * How many beats (quarter notes) between camera zooms. @@ -857,8 +865,8 @@ class PlayState extends MusicBeatSubState regenNoteData(); // Reset camera zooming - cameraZoomIntensity = Constants.DEFAULT_ZOOM_INTENSITY; - hudCameraZoomIntensity = Constants.DEFAULT_ZOOM_INTENSITY * 2.0; + cameraBopIntensity = Constants.DEFAULT_BOP_INTENSITY; + hudCameraZoomIntensity = 0.015 * 2.0; cameraZoomRate = Constants.DEFAULT_ZOOM_RATE; health = Constants.HEALTH_STARTING; @@ -953,11 +961,12 @@ class PlayState extends MusicBeatSubState if (health > Constants.HEALTH_MAX) health = Constants.HEALTH_MAX; if (health < Constants.HEALTH_MIN) health = Constants.HEALTH_MIN; - // Lerp the camera zoom towards the target level. + // Apply camera zoom + multipliers. if (subState == null) { - currentCameraZoom = FlxMath.lerp(defaultCameraZoom, currentCameraZoom, 0.95); - FlxG.camera.zoom = currentCameraZoom + additiveCameraZoom; + cameraBopMultiplier = FlxMath.lerp(1.0, cameraBopMultiplier, 0.95); // Lerp bop multiplier back to 1.0x + var zoomPlusBop = currentCameraZoom * cameraBopMultiplier; // Apply camera bop multiplier. + FlxG.camera.zoom = zoomPlusBop; // Actually apply the zoom to the camera. camHUD.zoom = FlxMath.lerp(defaultHUDCameraZoom, camHUD.zoom, 0.95); } @@ -1363,15 +1372,15 @@ class PlayState extends MusicBeatSubState // activeNotes.sort(SortUtil.byStrumtime, FlxSort.DESCENDING); } - // Only zoom camera if we are zoomed by less than 35%. + // Only bop camera if zoom level is below 135% if (Preferences.zoomCamera - && FlxG.camera.zoom < (1.35 * defaultCameraZoom) + && FlxG.camera.zoom < (1.35 * FlxCamera.defaultZoom) && cameraZoomRate > 0 && Conductor.instance.currentBeat % cameraZoomRate == 0) { - // Zoom camera in (1.5%) - currentCameraZoom += cameraZoomIntensity * defaultCameraZoom; - // Hud zooms double (3%) + // Set zoom multiplier for camera bop. + cameraBopMultiplier = cameraBopIntensity; + // HUD camera zoom still uses old system. To change. (+3%) camHUD.zoom += hudCameraZoomIntensity * defaultHUDCameraZoom; } // trace('Not bopping camera: ${FlxG.camera.zoom} < ${(1.35 * defaultCameraZoom)} && ${cameraZoomRate} > 0 && ${Conductor.instance.currentBeat} % ${cameraZoomRate} == ${Conductor.instance.currentBeat % cameraZoomRate}}'); @@ -1562,12 +1571,11 @@ class PlayState extends MusicBeatSubState { if (PlayState.instance.isMinimalMode) return; // Apply camera zoom level from stage data. - defaultCameraZoom = currentStage.camZoom; - currentCameraZoom = defaultCameraZoom; + currentCameraZoom = stageZoom; FlxG.camera.zoom = currentCameraZoom; - // Reset additive zoom. - additiveCameraZoom = 0; + // Reset bop multiplier. + cameraBopMultiplier = 1.0; } /** @@ -3141,38 +3149,24 @@ class PlayState extends MusicBeatSubState /** * Tweens the camera zoom to the desired amount. */ - public function tweenCameraZoom(?zoom:Float, ?duration:Float, ?directMode:Bool, ?ease:NullFloat>):Void + public function tweenCameraZoom(?zoom:Float, ?duration:Float, ?direct:Bool, ?ease:NullFloat>):Void { // Cancel the current tween if it's active. cancelCameraZoomTween(); - var targetZoom = zoom * FlxCamera.defaultZoom; + // Direct mode: Set zoom directly. + // Stage mode: Set zoom as a multiplier of the current stage's default zoom. + var targetZoom = zoom * (direct ? FlxCamera.defaultZoom : stageZoom); - if (directMode) // Direct mode: Tween defaultCameraZoom for basic "smooth" zooms. + if (duration == 0) { - if (duration == 0) - { - // Instant zoom. No tween needed. - defaultCameraZoom = targetZoom; - } - else - { - // Zoom tween! Caching it so we can cancel/pause it later if needed. - cameraZoomTween = FlxTween.tween(this, {defaultCameraZoom: targetZoom}, duration, {ease: ease}); - } + // Instant zoom. No tween needed. + currentCameraZoom = targetZoom; } - else // Additive mode: Tween additiveCameraZoom for ease-based zooms. + else { - if (duration == 0) - { - // Instant zoom. No tween needed. - additiveCameraZoom = targetZoom; - } - else - { - // Zoom tween! Caching it so we can cancel/pause it later if needed. - cameraZoomTween = FlxTween.tween(this, {additiveCameraZoom: targetZoom}, duration, {ease: ease}); - } + // Zoom tween! Caching it so we can cancel/pause it later if needed. + cameraZoomTween = FlxTween.tween(this, {currentCameraZoom: targetZoom}, duration, {ease: ease}); } } diff --git a/source/funkin/play/cutscene/VideoCutscene.hx b/source/funkin/play/cutscene/VideoCutscene.hx index 3da51185f..0c05bc876 100644 --- a/source/funkin/play/cutscene/VideoCutscene.hx +++ b/source/funkin/play/cutscene/VideoCutscene.hx @@ -311,7 +311,7 @@ class VideoCutscene blackScreen = null; } }); - FlxTween.tween(FlxG.camera, {zoom: PlayState.instance.defaultCameraZoom}, transitionTime, + FlxTween.tween(FlxG.camera, {zoom: PlayState.instance.stageZoom}, transitionTime, { ease: FlxEase.quadInOut, onComplete: function(twn:FlxTween) { diff --git a/source/funkin/play/event/FocusCameraSongEvent.hx b/source/funkin/play/event/FocusCameraSongEvent.hx index d4ab4f24f..e2db2fa79 100644 --- a/source/funkin/play/event/FocusCameraSongEvent.hx +++ b/source/funkin/play/event/FocusCameraSongEvent.hx @@ -132,6 +132,9 @@ class FocusCameraSongEvent extends SongEvent { case 'INSTANT': PlayState.instance.tweenCameraToFollowPoint(0); + case 'classic': + var classicDur = 1.0; // This is probably a fixed duration given how old zoom works. Need to sus it out. + PlayState.instance.tweenCameratoFollowPoint(classicDur); // Need to create an ease function to recreate classic follow-style movement. default: var durSeconds = Conductor.instance.stepLengthMs * duration / 1000; @@ -230,6 +233,7 @@ class FocusCameraSongEvent extends SongEvent 'Elastic In' => 'elasticIn', 'Elastic Out' => 'elasticOut', 'Elastic In/Out' => 'elasticInOut', + 'Classic' => 'classic' ] } ]); diff --git a/source/funkin/play/event/SetCameraBopSongEvent.hx b/source/funkin/play/event/SetCameraBopSongEvent.hx index a82577a5f..9d3e785ed 100644 --- a/source/funkin/play/event/SetCameraBopSongEvent.hx +++ b/source/funkin/play/event/SetCameraBopSongEvent.hx @@ -50,8 +50,8 @@ class SetCameraBopSongEvent extends SongEvent var intensity:Null = data.getFloat('intensity'); if (intensity == null) intensity = 1.0; - PlayState.instance.cameraZoomIntensity = Constants.DEFAULT_ZOOM_INTENSITY * intensity; - PlayState.instance.hudCameraZoomIntensity = Constants.DEFAULT_ZOOM_INTENSITY * intensity * 2.0; + PlayState.instance.cameraBopIntensity = Constants.DEFAULT_BOP_INTENSITY * intensity; + PlayState.instance.hudCameraZoomIntensity = 1.015 * intensity * 2.0; PlayState.instance.cameraZoomRate = rate; trace('Set camera zoom rate to ${PlayState.instance.cameraZoomRate}'); } diff --git a/source/funkin/play/event/ZoomCameraSongEvent.hx b/source/funkin/play/event/ZoomCameraSongEvent.hx index b913aebe7..f5d54b673 100644 --- a/source/funkin/play/event/ZoomCameraSongEvent.hx +++ b/source/funkin/play/event/ZoomCameraSongEvent.hx @@ -79,6 +79,9 @@ class ZoomCameraSongEvent extends SongEvent { case 'INSTANT': PlayState.instance.tweenCameraZoom(zoom, 0, isDirectMode); + case 'classic': + var classicDur = 1.0; // This is probably a fixed duration given how old zoom works. Need to sus it out. + PlayState.instance.tweenCameraZoom(zoom, 1.0; true); // Need to create an ease function to recreate classic lerp-style zooming. default: var durSeconds = Conductor.instance.stepLengthMs * duration / 1000; @@ -132,7 +135,7 @@ class ZoomCameraSongEvent extends SongEvent title: 'Mode', defaultValue: 'direct', type: SongEventFieldType.ENUM, - keys: ['Additive' => 'additive', 'Direct' => 'direct'] + keys: ['Stage' => 'stage', 'Direct' => 'direct'] }, { name: 'ease', @@ -163,6 +166,7 @@ class ZoomCameraSongEvent extends SongEvent 'Elastic In' => 'elasticIn', 'Elastic Out' => 'elasticOut', 'Elastic In/Out' => 'elasticInOut', + 'Classic' => 'classic' ] } ]); diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx index c7bc03139..15378b5a4 100644 --- a/source/funkin/util/Constants.hx +++ b/source/funkin/util/Constants.hx @@ -163,9 +163,10 @@ class Constants public static final DEFAULT_VARIATION_LIST:Array = ['default', 'erect', 'pico']; /** - * The default intensity for camera zooms. + * The default intensity multiplier for camera bops. + * Prolly needs to be tuned bc it's a multiplier now. */ - public static final DEFAULT_ZOOM_INTENSITY:Float = 0.015; + public static final DEFAULT_BOP_INTENSITY:Float = 1.015; /** * The default rate for camera zooms (in beats per zoom). From c658862f48e8840fadf343832028f336495631e4 Mon Sep 17 00:00:00 2001 From: Jenny Crowe Date: Thu, 28 Mar 2024 11:07:13 -0700 Subject: [PATCH 4/6] submod update ughghghhhh --- art | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/art b/art index 00463685f..03e7c2a23 160000 --- a/art +++ b/art @@ -1 +1 @@ -Subproject commit 00463685fa570f0c853d08e250b46ef80f30bc48 +Subproject commit 03e7c2a2353b184e45955c96d763b7cdf1acbc34 From 76885c421d7e521a97dba1025a50cb82fbf924fa Mon Sep 17 00:00:00 2001 From: Jenny Crowe Date: Thu, 28 Mar 2024 12:34:13 -0700 Subject: [PATCH 5/6] Cleanup + Focus Camera: Use Tween toggle converted to "Classic" ease type. --- source/funkin/play/PlayState.hx | 9 ++ .../funkin/play/event/FocusCameraSongEvent.hx | 106 ++++++++---------- .../funkin/play/event/ZoomCameraSongEvent.hx | 16 ++- 3 files changed, 64 insertions(+), 67 deletions(-) diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 7a264243e..de8597c17 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -3104,6 +3104,15 @@ class PlayState extends MusicBeatSubState FlxG.camera.focusOn(cameraFollowPoint.getPosition()); } + /** + * Sets the camera follow point's position and tweens the camera there. + */ + public function tweenCameraToPosition(?x:Float, ?y:Float, ?duration:Float, ?ease:NullFloat>):Void + { + cameraFollowPoint.setPosition(x, y); + tweenCameraToFollowPoint(duration, ease); + } + /** * Disables camera following and tweens the camera to the follow point manually. */ diff --git a/source/funkin/play/event/FocusCameraSongEvent.hx b/source/funkin/play/event/FocusCameraSongEvent.hx index e2db2fa79..1bcac9ad3 100644 --- a/source/funkin/play/event/FocusCameraSongEvent.hx +++ b/source/funkin/play/event/FocusCameraSongEvent.hx @@ -70,83 +70,76 @@ class FocusCameraSongEvent extends SongEvent if (char == null) char = cast data.value; - var useTween:Null = data.getBool('useTween'); - if (useTween == null) useTween = false; var duration:Null = data.getFloat('duration'); if (duration == null) duration = 4.0; var ease:Null = data.getString('ease'); - if (ease == null) ease = 'linear'; + if (ease == null) ease = 'CLASSIC'; + + var currentStage = PlayState.instance.currentStage; + + // Get target position based on char. + var targetX:Float = posX; + var targetY:Float = posY; switch (char) { - case -1: // Position + case -1: // Position ("focus" on origin) trace('Focusing camera on static position.'); - var xTarget:Float = posX; - var yTarget:Float = posY; - PlayState.instance.cameraFollowPoint.setPosition(xTarget, yTarget); - case 0: // Boyfriend - // Focus the camera on the player. - if (PlayState.instance.currentStage.getBoyfriend() == null) + case 0: // Boyfriend (focus on player) + if (currentStage.getBoyfriend() == null) { trace('No BF to focus on.'); return; } trace('Focusing camera on player.'); - var xTarget:Float = PlayState.instance.currentStage.getBoyfriend().cameraFocusPoint.x + posX; - var yTarget:Float = PlayState.instance.currentStage.getBoyfriend().cameraFocusPoint.y + posY; + var bfPoint = currentStage.getBoyfriend().cameraFocusPoint; + targetX += bfPoint.x; + targetY += bfPoint.y; - PlayState.instance.cameraFollowPoint.setPosition(xTarget, yTarget); - case 1: // Dad - // Focus the camera on the dad. - if (PlayState.instance.currentStage.getDad() == null) + case 1: // Dad (focus on opponent) + if (currentStage.getDad() == null) { trace('No dad to focus on.'); return; } - trace('Focusing camera on dad.'); - trace(PlayState.instance.currentStage.getDad()); - var xTarget:Float = PlayState.instance.currentStage.getDad().cameraFocusPoint.x + posX; - var yTarget:Float = PlayState.instance.currentStage.getDad().cameraFocusPoint.y + posY; + trace('Focusing camera on opponent.'); + var dadPoint = currentStage.getDad().cameraFocusPoint; + targetX += dadPoint.x; + targetY += dadPoint.y; - PlayState.instance.cameraFollowPoint.setPosition(xTarget, yTarget); - case 2: // Girlfriend - // Focus the camera on the girlfriend. - if (PlayState.instance.currentStage.getGirlfriend() == null) + case 2: // Girlfriend (focus on girlfriend) + if (currentStage.getGirlfriend() == null) { trace('No GF to focus on.'); return; } trace('Focusing camera on girlfriend.'); - var xTarget:Float = PlayState.instance.currentStage.getGirlfriend().cameraFocusPoint.x + posX; - var yTarget:Float = PlayState.instance.currentStage.getGirlfriend().cameraFocusPoint.y + posY; + var gfPoint = currentStage.getGirlfriend().cameraFocusPoint; + targetX += gfPoint.x; + targetY += gfPoint.y; - PlayState.instance.cameraFollowPoint.setPosition(xTarget, yTarget); default: trace('Unknown camera focus: ' + data); } - if (useTween) + // Apply tween based on ease. + switch (ease) { - switch (ease) - { - case 'INSTANT': - PlayState.instance.tweenCameraToFollowPoint(0); - case 'classic': - var classicDur = 1.0; // This is probably a fixed duration given how old zoom works. Need to sus it out. - PlayState.instance.tweenCameratoFollowPoint(classicDur); // Need to create an ease function to recreate classic follow-style movement. - default: - var durSeconds = Conductor.instance.stepLengthMs * duration / 1000; - - var easeFunction:NullFloat> = Reflect.field(FlxEase, ease); - if (easeFunction == null) - { - trace('Invalid ease function: $ease'); - return; - } - - PlayState.instance.tweenCameraToFollowPoint(durSeconds, easeFunction); - } + case 'CLASSIC': // Old-school. No ease. Just set follow point. + PlayState.instance.cancelCameraFollowTween(); + PlayState.instance.cameraFollowPoint.setPosition(targetX, targetY); + case 'INSTANT': // Instant ease. Duration is automatically 0. + PlayState.instance.tweenCameraToPosition(targetX, targetY, 0); + default: + var durSeconds = Conductor.instance.stepLengthMs * duration / 1000; + var easeFunction:NullFloat> = Reflect.field(FlxEase, ease); + if (easeFunction == null) + { + trace('Invalid ease function: $ease'); + return; + } + PlayState.instance.tweenCameraToPosition(targetX, targetY, durSeconds, easeFunction); } } @@ -190,12 +183,6 @@ class FocusCameraSongEvent extends SongEvent type: SongEventFieldType.FLOAT, units: "px" }, - { - name: 'useTween', - title: 'Use Tween', - type: SongEventFieldType.BOOL, - defaultValue: false - }, { name: 'duration', title: 'Duration', @@ -211,7 +198,9 @@ class FocusCameraSongEvent extends SongEvent type: SongEventFieldType.ENUM, keys: [ 'Linear' => 'linear', - 'Instant' => 'INSTANT', + 'Sine In' => 'sineIn', + 'Sine Out' => 'sineOut', + 'Sine In/Out' => 'sineInOut', 'Quad In' => 'quadIn', 'Quad Out' => 'quadOut', 'Quad In/Out' => 'quadInOut', @@ -224,16 +213,17 @@ class FocusCameraSongEvent extends SongEvent 'Quint In' => 'quintIn', 'Quint Out' => 'quintOut', 'Quint In/Out' => 'quintInOut', + 'Expo In' => 'expoIn', + 'Expo Out' => 'expoOut', + 'Expo In/Out' => 'expoInOut', 'Smooth Step In' => 'smoothStepIn', 'Smooth Step Out' => 'smoothStepOut', 'Smooth Step In/Out' => 'smoothStepInOut', - 'Sine In' => 'sineIn', - 'Sine Out' => 'sineOut', - 'Sine In/Out' => 'sineInOut', 'Elastic In' => 'elasticIn', 'Elastic Out' => 'elasticOut', 'Elastic In/Out' => 'elasticInOut', - 'Classic' => 'classic' + 'Instant (Ignores duration)' => 'INSTANT', + 'Classic (Ignores duration)' => 'CLASSIC' ] } ]); diff --git a/source/funkin/play/event/ZoomCameraSongEvent.hx b/source/funkin/play/event/ZoomCameraSongEvent.hx index f5d54b673..46c746333 100644 --- a/source/funkin/play/event/ZoomCameraSongEvent.hx +++ b/source/funkin/play/event/ZoomCameraSongEvent.hx @@ -79,12 +79,8 @@ class ZoomCameraSongEvent extends SongEvent { case 'INSTANT': PlayState.instance.tweenCameraZoom(zoom, 0, isDirectMode); - case 'classic': - var classicDur = 1.0; // This is probably a fixed duration given how old zoom works. Need to sus it out. - PlayState.instance.tweenCameraZoom(zoom, 1.0; true); // Need to create an ease function to recreate classic lerp-style zooming. default: var durSeconds = Conductor.instance.stepLengthMs * duration / 1000; - var easeFunction:NullFloat> = Reflect.field(FlxEase, ease); if (easeFunction == null) { @@ -145,6 +141,9 @@ class ZoomCameraSongEvent extends SongEvent keys: [ 'Linear' => 'linear', 'Instant' => 'INSTANT', + 'Sine In' => 'sineIn', + 'Sine Out' => 'sineOut', + 'Sine In/Out' => 'sineInOut', 'Quad In' => 'quadIn', 'Quad Out' => 'quadOut', 'Quad In/Out' => 'quadInOut', @@ -157,16 +156,15 @@ class ZoomCameraSongEvent extends SongEvent 'Quint In' => 'quintIn', 'Quint Out' => 'quintOut', 'Quint In/Out' => 'quintInOut', + 'Expo In' => 'expoIn', + 'Expo Out' => 'expoOut', + 'Expo In/Out' => 'expoInOut', 'Smooth Step In' => 'smoothStepIn', 'Smooth Step Out' => 'smoothStepOut', 'Smooth Step In/Out' => 'smoothStepInOut', - 'Sine In' => 'sineIn', - 'Sine Out' => 'sineOut', - 'Sine In/Out' => 'sineInOut', 'Elastic In' => 'elasticIn', 'Elastic Out' => 'elasticOut', - 'Elastic In/Out' => 'elasticInOut', - 'Classic' => 'classic' + 'Elastic In/Out' => 'elasticInOut' ] } ]); From e2851818b387e2055b7beb9f1c85c56b613a6167 Mon Sep 17 00:00:00 2001 From: Jenny Crowe Date: Thu, 28 Mar 2024 17:47:38 -0700 Subject: [PATCH 6/6] Zoom Camera: Property tweaks --- source/funkin/play/event/ZoomCameraSongEvent.hx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/source/funkin/play/event/ZoomCameraSongEvent.hx b/source/funkin/play/event/ZoomCameraSongEvent.hx index 46c746333..748abda19 100644 --- a/source/funkin/play/event/ZoomCameraSongEvent.hx +++ b/source/funkin/play/event/ZoomCameraSongEvent.hx @@ -101,9 +101,9 @@ class ZoomCameraSongEvent extends SongEvent * ``` * { * 'zoom': FLOAT, // Target zoom level. - * 'duration': FLOAT, // Optional duration in steps. - * 'mode': ENUM, // Whether to set additive zoom or direct zoom. - * 'ease': ENUM, // Optional easing function. + * 'duration': FLOAT, // Duration in steps. + * 'mode': ENUM, // Whether zoom is relative to the stage or absolute zoom. + * 'ease': ENUM, // Easing function. * } * @return SongEventSchema */ @@ -129,9 +129,9 @@ class ZoomCameraSongEvent extends SongEvent { name: 'mode', title: 'Mode', - defaultValue: 'direct', + defaultValue: 'stage', type: SongEventFieldType.ENUM, - keys: ['Stage' => 'stage', 'Direct' => 'direct'] + keys: ['Stage zoom' => 'stage', 'Absolute zoom' => 'direct'] }, { name: 'ease',