Merge pull request #1169 from FunkinCrew/mobile-main-menu

This commit is contained in:
MoonDroid
2025-06-10 17:52:38 +07:00
committed by GitHub
13 changed files with 369 additions and 30 deletions
+1 -1
Submodule assets updated: 8dfe00b517...422f7d5738
+10 -1
View File
@@ -14,6 +14,7 @@ class Preferences
{
/**
* FPS
* Always the refresh rate of the display on mobile, or 60 on web.
* @default `60`
*/
public static var framerate(get, set):Int;
@@ -27,7 +28,7 @@ class Preferences
if (refreshRate < 60) refreshRate = 60;
return Save?.instance?.options?.framerate ?? refreshRate;
return refreshRate;
#else
return Save?.instance?.options?.framerate ?? 60;
#end
@@ -125,12 +126,16 @@ class Preferences
/**
* If enabled, an FPS and memory counter will be displayed even if this is not a debug build.
* Always disabled on mobile.
* @default `false`
*/
public static var debugDisplay(get, set):Bool;
static function get_debugDisplay():Bool
{
#if mobile
return false;
#end
return Save?.instance?.options?.debugDisplay;
}
@@ -190,12 +195,16 @@ class Preferences
/**
* If enabled, the game will automatically pause when tabbing out.
* Always enabled on mobile.
* @default `true`
*/
public static var autoPause(get, set):Bool;
static function get_autoPause():Bool
{
#if mobile
return true;
#end
return Save?.instance?.options?.autoPause ?? true;
}
+26 -2
View File
@@ -5,11 +5,16 @@ import flixel.util.FlxColor;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import funkin.util.HapticUtil;
import flixel.util.FlxSignal;
class FunkinBackButton extends FunkinButton
{
var restingOpacity:Float;
public var onConfirmEnd(default, null):FlxSignal = new FlxSignal();
var instant:Bool = false;
/**
* Creates a new FunkinBackButton instance.
*
@@ -17,8 +22,11 @@ class FunkinBackButton extends FunkinButton
* @param yPos The y position of the object.
* @param theColor Button's optional color.
* @param confirmCallback An optional callback function that will be triggered when the object is clicked.
* @param restOpacity An optional float that is the alpha the button will be when not selected/hovered over.
* @param instant An optional flag that makes the button not play the full animation before calling the callback.
*/
public function new(?xPos:Float = 0, ?yPos:Float = 0, ?theColor:FlxColor = FlxColor.WHITE, ?confirmCallback:Void->Void, ?_restOpacity:Float = 0.3):Void
public function new(?xPos:Float = 0, ?yPos:Float = 0, ?theColor:FlxColor = FlxColor.WHITE, ?confirmCallback:Void->Void, ?_restOpacity:Float = 0.3,
_instant:Bool = false):Void
{
super(xPos, yPos);
@@ -36,9 +44,18 @@ class FunkinBackButton extends FunkinButton
alpha = restingOpacity;
ignoreDownHandler = true;
instant = _instant;
if (instant)
{
onUp.add(confirmCallback);
}
else
{
onConfirmEnd.add(confirmCallback);
}
onUp.add(playConfirmAnim);
if (confirmCallback != null) onUp.add(confirmCallback);
onDown.add(playHoldAnim);
onOut.add(playOutAnim);
}
@@ -60,6 +77,13 @@ class FunkinBackButton extends FunkinButton
HapticUtil.vibrate(0, 0.05, 0.5);
animation.play('confirm');
if (!instant)
{
animation.onFinish.add(function(name:String) {
onConfirmEnd.dispatch();
});
}
onUp.remove(playConfirmAnim);
onDown.remove(playHoldAnim);
onOut.remove(playOutAnim);
@@ -0,0 +1,103 @@
package funkin.mobile.ui.mainmenu;
import flixel.FlxG;
import flixel.util.FlxColor;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import funkin.util.HapticUtil;
import flixel.util.FlxSignal;
import flixel.util.FlxTimer;
import funkin.audio.FunkinSound;
class FunkinOptionsButton extends FunkinButton
{
public var onConfirmEnd(default, null):FlxSignal = new FlxSignal();
var instant:Bool = false;
/**
* Creates a new FunkinOptionsButton instance.
*
* @param xPos The x position of the object.
* @param yPos The y position of the object.
* @param confirmCallback An optional callback function that will be triggered when the object is clicked.
* @param instant An optional flag that makes the button not play the full animation before calling the callback.
*/
public function new(?xPos:Float = 0, ?yPos:Float = 0, ?confirmCallback:Void->Void, _instant:Bool = false):Void
{
super(xPos, yPos);
frames = Paths.getSparrowAtlas("mainmenu/optionsButton");
animation.addByIndices('idle', 'options', [0], "", 24, false);
animation.addByIndices('hold', 'options', [3], "", 24, false);
animation.addByIndices('confirm', 'options', [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], "", 24, false);
animation.play("idle");
scale.set(0.7, 0.7);
updateHitbox();
ignoreDownHandler = true;
instant = _instant;
if (instant)
{
onUp.add(confirmCallback);
}
else
{
onConfirmEnd.add(confirmCallback);
}
onUp.add(playConfirmAnim);
onDown.add(playHoldAnim);
onOut.add(playOutAnim);
}
function playHoldAnim():Void
{
FlxTween.cancelTweensOf(this);
HapticUtil.vibrate(0, 0.01, 0.2);
animation.play('hold');
}
function playConfirmAnim():Void
{
FlxTween.cancelTweensOf(this);
HapticUtil.vibrate(0, 0.05, 0.5);
animation.play('confirm');
FunkinSound.playOnce(Paths.sound('confirmMenu'));
new FlxTimer().start(0.05, function(_) {
HapticUtil.vibrate(0, 0.01, 0.2);
}, 4);
if (!instant)
{
animation.onFinish.add(function(name:String) {
onConfirmEnd.dispatch();
});
}
onUp.remove(playConfirmAnim);
onDown.remove(playHoldAnim);
onOut.remove(playOutAnim);
}
function playOutAnim():Void
{
FlxTween.cancelTweensOf(this);
HapticUtil.vibrate(0, 0.01, 0.2);
animation.play('idle');
}
override public function update(elapsed:Float):Void
{
super.update(elapsed);
#if android
if (FlxG.android.justReleased.BACK) onDown.dispatch();
#end
}
}
+5
View File
@@ -141,6 +141,7 @@ class AdMobUtil
*/
public static inline function addBanner(size:Int = AdmobBannerSize.BANNER, align:Int = AdmobBannerAlign.BOTTOM_CENTER):Void
{
if (InAppPurchasesUtil.isPurchased("no_ads")) return;
Admob.showBanner([AdMobUtil.ADMOB_PUBLISHER, AdMobUtil.BANNER_AD_UNIT_ID].join('/'), size, align);
}
@@ -159,6 +160,8 @@ class AdMobUtil
*/
public static function loadInterstitial(onInterstitialFinish:Void->Void):Void
{
if (InAppPurchasesUtil.isPurchased("no_ads")) return;
function interstitialEvent(event:AdmobEvent):Void
{
if (event.name == AdmobEvent.INTERSTITIAL_LOADED)
@@ -187,6 +190,8 @@ class AdMobUtil
*/
public static function loadRewarded(onRewardedFinish:Void->Void):Void
{
if (InAppPurchasesUtil.isPurchased("no_ads")) return;
function rewardedEvent(event:AdmobEvent):Void
{
if (event.name == AdmobEvent.REWARDED_LOADED)
+31 -1
View File
@@ -9,6 +9,9 @@ import funkin.audio.FunkinSound;
import funkin.util.TouchUtil;
import funkin.util.SwipeUtil;
import funkin.ui.Page.PageName;
import flixel.tweens.FlxEase;
import funkin.util.HapticUtil;
import flixel.tweens.FlxTween;
class MenuTypedList<T:MenuListItem> extends FlxTypedGroup<T>
{
@@ -146,6 +149,8 @@ class MenuTypedList<T:MenuListItem> extends FlxTypedGroup<T>
if (item.available && isTouchingItem && TouchUtil.justPressed)
{
var prevIndex:Int = selectedIndex;
if (!_isMainMenuState && selectedIndex != i)
{
newIndex = i;
@@ -153,9 +158,34 @@ class MenuTypedList<T:MenuListItem> extends FlxTypedGroup<T>
}
else
{
FunkinSound.playOnce(Paths.sound('scrollMenu'), 0.4);
selectItem(i);
}
accept();
if (_isMainMenuState)
{
if (prevIndex == i)
{
FlxTween.cancelTweensOf(item);
item.scale.set(1.1, 1.1);
FlxTween.tween(item.scale, {x: 1, y: 1}, 0.3, {ease: FlxEase.backOut});
HapticUtil.vibrate(0, 0.05, 0.5);
accept();
}
else
{
FlxTween.cancelTweensOf(item);
item.scale.set(0.94, 0.94);
FlxTween.tween(item.scale, {x: 1, y: 1}, 0.3, {ease: FlxEase.backOut});
HapticUtil.vibrate(0, 0.01, 0.2);
}
}
else
{
accept();
}
break;
}
+21 -2
View File
@@ -18,6 +18,7 @@ import funkin.graphics.FunkinCamera;
import funkin.mobile.ui.FunkinHitbox;
import funkin.mobile.input.PreciseInputHandler;
import funkin.mobile.ui.FunkinBackButton;
import funkin.mobile.ui.mainmenu.FunkinOptionsButton;
import funkin.play.notes.NoteDirection;
#end
@@ -67,6 +68,7 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
#if mobile
public var hitbox:Null<FunkinHitbox>;
public var backButton:Null<FunkinBackButton>;
public var optionsButton:Null<FunkinOptionsButton>;
public var camControls:Null<FunkinCamera>;
public function addHitbox(visible:Bool = true, initInput:Bool = true, ?schemeOverride:String, ?directionsOverride:Array<NoteDirection>,
@@ -95,7 +97,7 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
}
public function addBackButton(?xPos:Float = 0, ?yPos:Float = 0, ?color:FlxColor = FlxColor.WHITE, ?confirmCallback:Void->Void = null,
?restOpacity:Float = 0.3):Void
?restOpacity:Float = 0.3, ?instant:Bool = false):Void
{
if (backButton != null) remove(backButton);
@@ -106,10 +108,27 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
camControls.bgColor = 0x0;
}
backButton = new FunkinBackButton(xPos, yPos, color, confirmCallback, restOpacity);
backButton = new FunkinBackButton(xPos, yPos, color, confirmCallback, restOpacity, instant);
backButton.cameras = [camControls];
add(backButton);
}
// this should get moved post ui update but this is easier rn lolll
public function addOptionsButton(?xPos:Float = 0, ?yPos:Float = 0, ?confirmCallback:Void->Void = null, ?instant:Bool = false):Void
{
if (optionsButton != null) remove(optionsButton);
if (camControls == null)
{
camControls = new FunkinCamera('camControls');
FlxG.cameras.add(camControls, false);
camControls.bgColor = 0x0;
}
optionsButton = new FunkinOptionsButton(xPos, yPos, confirmCallback, instant);
optionsButton.cameras = [camControls];
add(optionsButton);
}
#end
override function create()
+2 -2
View File
@@ -80,7 +80,7 @@ class MusicBeatSubState extends FlxSubState implements IEventHandler
}
public function addBackButton(?xPos:Float = 0, ?yPos:Float = 0, ?color:FlxColor = FlxColor.WHITE, ?confirmCallback:Void->Void = null,
?restOpacity:Float = 0.3):Void
?restOpacity:Float = 0.3, ?instant:Bool = false):Void
{
if (backButton != null) remove(backButton);
@@ -91,7 +91,7 @@ class MusicBeatSubState extends FlxSubState implements IEventHandler
camControls.bgColor = 0x0;
}
backButton = new FunkinBackButton(xPos, yPos, color, confirmCallback, restOpacity);
backButton = new FunkinBackButton(xPos, yPos, color, confirmCallback, restOpacity, instant);
backButton.cameras = [camControls];
add(backButton);
}
+1 -1
View File
@@ -763,7 +763,7 @@ class FreeplayState extends MusicBeatSubState
rankBg.alpha = 0;
#if FEATURE_TOUCH_CONTROLS
addBackButton(FlxG.width, FlxG.height - 200, FlxColor.WHITE, goBack);
addBackButton(FlxG.width, FlxG.height - 200, FlxColor.WHITE, goBack, 0.3, true);
FlxTween.tween(backButton, {x: FlxG.width - 230}, 0.5, {ease: FlxEase.expoOut});
#end
+81 -9
View File
@@ -25,12 +25,17 @@ import funkin.ui.Prompt;
import funkin.util.WindowUtil;
import funkin.util.TouchUtil;
import funkin.api.newgrounds.Referral;
import funkin.ui.mainmenu.UpgradeSparkle;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
#if FEATURE_DISCORD_RPC
import funkin.api.discord.DiscordClient;
#end
#if FEATURE_NEWGROUNDS
import funkin.api.newgrounds.NewgroundsClient;
#end
#if mobile
import funkin.mobile.util.InAppPurchasesUtil;
#end
class MainMenuState extends MusicBeatState
{
@@ -43,6 +48,11 @@ class MainMenuState extends MusicBeatState
static var rememberedSelectedIndex:Int = 0;
// TODO: this needs to eventually reflect the actual state of whether the player has upgraded or not.
// this should never be false on non-mobile targets.
var hasUpgraded:Bool = false;
var upgradeSparkles:FlxTypedSpriteGroup<UpgradeSparkle>;
public function new(?_overrideMusic:Bool = false)
{
super();
@@ -60,6 +70,13 @@ class MainMenuState extends MusicBeatState
transIn = FlxTransitionableState.defaultTransIn;
transOut = FlxTransitionableState.defaultTransOut;
#if mobile
hasUpgraded = InAppPurchasesUtil.isPurchased("no_ads");
#else
// just to make sure its never accidentally turned off
hasUpgraded = true;
#end
if (!overrideMusic) playMenuMusic();
// We want the state to always be able to begin with being able to accept inputs and show the anims of the menu items.
@@ -123,25 +140,48 @@ class MainMenuState extends MusicBeatState
var targetCharacter:Null<String> = null;
#end
if (!hasUpgraded)
{
for (i in 0...upgradeSparkles.length)
{
upgradeSparkles.members[i].cancelSparkle();
}
}
openSubState(new FreeplayState(
{
character: targetCharacter
}));
});
#if FEATURE_OPEN_URL
// In order to prevent popup blockers from triggering,
// we need to open the link as an immediate result of a keypress event,
// so we can't wait for the flicker animation to complete.
var hasPopupBlocker = #if web true #else false #end;
#if desktop
createMenuItem('merch', 'mainmenu/merch', selectMerch, hasPopupBlocker);
#end
#end
if (hasUpgraded)
{
#if FEATURE_OPEN_URL
// In order to prevent popup blockers from triggering,
// we need to open the link as an immediate result of a keypress event,
// so we can't wait for the flicker animation to complete.
var hasPopupBlocker = #if web true #else false #end;
createMenuItem('merch', 'mainmenu/merch', selectMerch, hasPopupBlocker);
#end
}
else
{
upgradeSparkles = new FlxTypedSpriteGroup<UpgradeSparkle>();
add(upgradeSparkles);
createMenuItem('upgrade', 'mainmenu/upgrade', function() {
#if mobile
InAppPurchasesUtil.purchase("no_ads");
FlxG.resetState();
#end
});
}
#if !mobile
createMenuItem('options', 'mainmenu/options', function() {
startExitState(() -> new funkin.ui.options.OptionsState());
});
#end
createMenuItem('credits', 'mainmenu/credits', function() {
startExitState(() -> new funkin.ui.credits.CreditsState());
@@ -167,6 +207,28 @@ class MainMenuState extends MusicBeatState
menuItems.selectItem(rememberedSelectedIndex);
if (!hasUpgraded)
{
// the upgrade item
var targetItem = menuItems.members[2];
for (i in 0...8)
{
var sparkle:UpgradeSparkle = new UpgradeSparkle(targetItem.x - (targetItem.width / 2), targetItem.y - (targetItem.height / 2), targetItem.width,
targetItem.height, FlxG.random.bool(80));
upgradeSparkles.add(sparkle);
sparkle.scrollFactor.x = 0.0;
sparkle.scrollFactor.y = 0.4;
}
subStateClosed.add(_ -> {
for (i in 0...upgradeSparkles.length)
{
upgradeSparkles.members[i].restartSparkle();
}
});
}
resetCamStuff();
// reset camera when debug menu is closed
@@ -184,7 +246,12 @@ class MainMenuState extends MusicBeatState
// FlxG.camera.setScrollBounds(bg.x, bg.x + bg.width, bg.y, bg.y + bg.height * 1.2);
#if mobile
camFollow.y = 355;
addBackButton(FlxG.width - 230, FlxG.height - 200, FlxColor.WHITE, goBack, 1.0);
addOptionsButton(35, FlxG.height - 210, function() {
startExitState(() -> new funkin.ui.options.OptionsState());
});
#end
super.create();
@@ -310,6 +377,11 @@ class MainMenuState extends MusicBeatState
}
});
#if mobile
FlxTween.tween(optionsButton, {alpha: 0}, duration, {ease: FlxEase.quadOut});
FlxTween.tween(backButton, {alpha: 0}, duration, {ease: FlxEase.quadOut});
#end
new FlxTimer().start(duration, function(_) FlxG.switchState(state));
}
@@ -0,0 +1,74 @@
package funkin.ui.mainmenu;
import flixel.FlxSprite;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.util.FlxTimer;
import flixel.math.FlxRect;
class UpgradeSparkle extends FlxSprite
{
var sparkleTimer:FlxTimer;
public var bounds:FlxRect;
public function new(_x:Float, _y:Float, _width:Float, _height:Float, big:Bool)
{
super(0, 0);
bounds = FlxRect.get(_x, _y, _width, _height);
if (big)
{
loadGraphic(Paths.image('mainmenu/upgradeshine_big'));
}
else
{
loadGraphic(Paths.image('mainmenu/upgradeshine_small'));
}
sparkleTimer = new FlxTimer().start(FlxG.random.float(2, 7), sparkleEffect);
visible = false;
}
public function cancelSparkle():Void
{
if (sparkleTimer != null) sparkleTimer.cancel();
FlxTween.cancelTweensOf(this);
alpha = 0;
}
public function restartSparkle():Void
{
if (sparkleTimer != null) sparkleTimer.cancel();
FlxTween.cancelTweensOf(this);
alpha = 1;
sparkleTimer = new FlxTimer().start(FlxG.random.float(2, 7), sparkleEffect);
}
function sparkleEffect(timer:FlxTimer):Void
{
visible = true;
alpha = 1;
flipX = !flipX;
var targetScale:Float = FlxG.random.float(0.6, 1);
scale.set(targetScale, targetScale);
updateHitbox();
setPosition(FlxG.random.float(bounds.x, bounds.x + bounds.width), FlxG.random.float(bounds.y, bounds.y + bounds.height));
x -= width / 2;
y -= height / 2;
angle += 70;
var targetVelocity:Float = FlxG.random.float(120, 300);
if (FlxG.random.bool(50)) targetVelocity = targetVelocity * -1;
angularVelocity = targetVelocity;
angularDrag = 200;
var targetTime:Float = FlxG.random.float(0.3, 0.8);
FlxTween.tween(this.scale, {x: targetScale * 0.001, y: targetScale * 0.001}, targetTime, {ease: FlxEase.backIn});
FlxTween.tween(this, {alpha: 0}, targetTime, {ease: FlxEase.quintIn});
sparkleTimer = new FlxTimer().start(FlxG.random.float(2, 7), sparkleEffect);
}
}
+9 -8
View File
@@ -108,15 +108,11 @@ class OptionsMenu extends Page<OptionsMenuPageName>
createItem("PREFERENCES", function() codex.switchPage(Preferences));
#if mobile
if (FlxG.gamepads.numActiveGamepads > 0)
{
createItem("CONTROLS", function() codex.switchPage(Controls));
}
createItem("CONTROL SCHEMES", function() {
FlxG.state.openSubState(new ControlsSchemeMenu());
});
#else
createItem("CONTROLS", function() codex.switchPage(Controls));
#end
createItem("CONTROLS", function() codex.switchPage(Controls));
// createItem("CONTROL SCHEMES", function() {
// FlxG.state.openSubState(new ControlsSchemeMenu());
// });
createItem("INPUT OFFSETS", function() {
OptionsState.rememberedSelectedIndex = items.selectedIndex;
#if web
@@ -161,7 +157,12 @@ class OptionsMenu extends Page<OptionsMenuPageName>
createItem("CLEAR SAVE DATA", function() {
promptClearSaveData();
});
#if NO_FEATURE_TOUCH_CONTROLS
createItem("EXIT", exit);
#else
var backButton:FunkinBackButton = new FunkinBackButton(FlxG.width - 230, FlxG.height - 200, exit, 1.0);
add(backButton);
#end
items.selectItem(OptionsState.rememberedSelectedIndex);
}
+5 -3
View File
@@ -76,8 +76,8 @@ class PreferencesMenu extends Page<OptionsState.OptionsMenuPageName>
itemDesc.text = preferenceDesc[items.selectedIndex];
});
#if mobile
var backButton:FunkinBackButton = new FunkinBackButton(FlxG.width * 0.83, FlxG.height * 0.75, exit);
#if FEATURE_TOUCH_CONTROLS
var backButton:FunkinBackButton = new FunkinBackButton(FlxG.width - 230, FlxG.height - 200, exit, 1.0);
add(backButton);
#end
}
@@ -139,13 +139,13 @@ class PreferencesMenu extends Page<OptionsState.OptionsMenuPageName>
createPrefItemCheckbox('Camera Zooms', 'If disabled, camera stops bouncing to the song.', function(value:Bool):Void {
Preferences.zoomCamera = value;
}, Preferences.zoomCamera);
#if !mobile
createPrefItemCheckbox('Debug Display', 'If enabled, FPS and other debug stats will be displayed.', function(value:Bool):Void {
Preferences.debugDisplay = value;
}, Preferences.debugDisplay);
createPrefItemCheckbox('Pause on Unfocus', 'If enabled, game automatically pauses when it loses focus.', function(value:Bool):Void {
Preferences.autoPause = value;
}, Preferences.autoPause);
#if !mobile
createPrefItemCheckbox('Launch in Fullscreen', 'Automatically launch the game in fullscreen on startup', function(value:Bool):Void {
Preferences.autoFullscreen = value;
}, Preferences.autoFullscreen);
@@ -173,10 +173,12 @@ class PreferencesMenu extends Page<OptionsState.OptionsMenuPageName>
case WindowVSyncMode.ADAPTIVE: "Adaptive";
});
#end
#if !mobile
createPrefItemNumber('FPS', 'The maximum framerate that the game targets.', function(value:Float) {
Preferences.framerate = Std.int(value);
}, null, Preferences.framerate, 30, 500, 5, 0);
#end
#end
#if FEATURE_SCREENSHOTS
createPrefItemCheckbox('Hide Mouse', 'If enabled, the mouse will be hidden when taking a screenshot.', function(value:Bool):Void {