mirror of
https://github.com/ninjamuffin99/Funkin.git
synced 2025-08-30 18:34:51 +00:00
519 lines
13 KiB
Haxe
519 lines
13 KiB
Haxe
package funkin.ui;
|
|
|
|
import flixel.FlxSprite;
|
|
import flixel.util.FlxColor;
|
|
import flixel.effects.FlxFlicker;
|
|
import flixel.group.FlxGroup.FlxTypedGroup;
|
|
import flixel.util.FlxSignal.FlxTypedSignal;
|
|
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;
|
|
|
|
@:nullSafety
|
|
class MenuTypedList<T:MenuListItem> extends FlxTypedGroup<T>
|
|
{
|
|
// Pause input variable
|
|
public static var pauseInput:Bool = false;
|
|
|
|
public var selectedIndex(default, null):Int = 0;
|
|
public var selectedItem(get, never):T;
|
|
|
|
/** Called when a new item is highlighted */
|
|
public var onChange(default, null) = new FlxTypedSignal<T->Void>();
|
|
|
|
/** Called when an item is accepted */
|
|
public var onAcceptPress(default, null) = new FlxTypedSignal<T->Void>();
|
|
|
|
/** The navigation control scheme to use */
|
|
public var navControls:NavControls;
|
|
|
|
/** Set to false to disable nav control */
|
|
public var enabled:Bool = true;
|
|
|
|
/** */
|
|
public var wrapMode:WrapMode = Both;
|
|
|
|
var byName = new Map<String, T>();
|
|
|
|
/** Set to true, internally to disable controls, without affecting vars like `enabled` */
|
|
public var busy:Bool = false;
|
|
|
|
// bit awkward because BACK is also a menu control and this doesn't affect that
|
|
// #if mobile
|
|
|
|
/** touchBuddy over here helps with the touch input! Because overlap for touch does not account for the graphic, only the hitbox.
|
|
* And, `FlxG.pixelPerfectOverlap` uses two FlxSprites, so we can't use the `FlxTouch` object */
|
|
public var touchBuddy:FlxSprite;
|
|
|
|
// #end
|
|
|
|
/** Only used in Options, basically acts the same as OptionsState's `currentName`, it's the current name of the current page in OptionsState.
|
|
* Why is it needed? Because touch control's a bitch. Thats why. */
|
|
public var currentPage:Null<PageName>;
|
|
|
|
// Helper variable
|
|
var _isMainMenuState:Bool = false;
|
|
|
|
public function new(navControls:NavControls = Vertical, ?wrapMode:WrapMode)
|
|
{
|
|
this.navControls = navControls;
|
|
|
|
if (wrapMode != null) this.wrapMode = wrapMode;
|
|
else
|
|
{
|
|
this.wrapMode = switch (navControls)
|
|
{
|
|
case Horizontal: Horizontal;
|
|
case Vertical: Vertical;
|
|
default: Both;
|
|
}
|
|
}
|
|
|
|
touchBuddy = new FlxSprite().makeGraphic(10, 10);
|
|
_isMainMenuState = Std.isOfType(FlxG.state, funkin.ui.mainmenu.MainMenuState);
|
|
|
|
super();
|
|
}
|
|
|
|
public function addItem(name:String, item:T):T
|
|
{
|
|
if (length == selectedIndex) item.select();
|
|
|
|
byName[name] = item;
|
|
return add(item);
|
|
}
|
|
|
|
public function resetItem(oldName:String, newName:String, ?callback:Void->Void):Null<T>
|
|
{
|
|
var item = byName[oldName];
|
|
if (item == null) throw 'No item named: $oldName';
|
|
byName.remove(oldName);
|
|
byName[newName] = item;
|
|
item.setItem(newName, callback);
|
|
|
|
return item;
|
|
}
|
|
|
|
override function update(elapsed:Float)
|
|
{
|
|
super.update(elapsed);
|
|
|
|
if (enabled && !busy && !pauseInput) updateControls();
|
|
}
|
|
|
|
inline function updateControls():Void
|
|
{
|
|
var controls = PlayerSettings.player1.controls;
|
|
|
|
var wrapX = wrapMode.match(Horizontal | Both);
|
|
var wrapY = wrapMode.match(Vertical | Both);
|
|
|
|
var newIndex = 0;
|
|
|
|
// Define unified input handlers
|
|
final inputUp:Bool = controls.UI_UP_P || (!_isMainMenuState && SwipeUtil.swipeUp);
|
|
final inputDown:Bool = controls.UI_DOWN_P || (!_isMainMenuState && SwipeUtil.swipeDown);
|
|
final inputLeft:Bool = controls.UI_LEFT_P || (!_isMainMenuState && SwipeUtil.swipeLeft);
|
|
final inputRight:Bool = controls.UI_RIGHT_P || (!_isMainMenuState && SwipeUtil.swipeRight);
|
|
|
|
// Keepin' these for keyboard/controller support on mobile platforms
|
|
newIndex = switch (navControls)
|
|
{
|
|
case Vertical: navList(inputUp, inputDown, wrapY);
|
|
case Horizontal: navList(inputLeft, inputRight, wrapX);
|
|
case Both: navList(inputLeft || inputUp, inputRight || inputDown, !wrapMode.match(None));
|
|
|
|
case Columns(num): navGrid(num, inputLeft, inputRight, wrapX, inputUp, inputDown, wrapY);
|
|
case Rows(num): navGrid(num, inputUp, inputDown, wrapY, inputLeft, inputRight, wrapX);
|
|
};
|
|
|
|
#if FEATURE_TOUCH_CONTROLS
|
|
// Update touch position
|
|
if (TouchUtil.pressed)
|
|
{
|
|
touchBuddy.setPosition(TouchUtil.touch.x, TouchUtil.touch.y);
|
|
}
|
|
|
|
if (funkin.mobile.input.ControlsHandler.usingExternalInputDevice)
|
|
{
|
|
if (newIndex != selectedIndex)
|
|
{
|
|
FunkinSound.playOnce(Paths.sound('scrollMenu'), 0.4);
|
|
selectItem(newIndex);
|
|
}
|
|
}
|
|
else if (TouchUtil.pressed)
|
|
{
|
|
for (i in 0...members.length)
|
|
{
|
|
final item = members[i];
|
|
final menuCamera = FlxG.cameras.list[1];
|
|
|
|
final itemOverlaps:Bool = !_isMainMenuState && TouchUtil.overlaps(item, menuCamera);
|
|
final itemPixelOverlap:Bool = _isMainMenuState && FlxG.pixelPerfectOverlap(touchBuddy, item, 0);
|
|
|
|
final isTouchingItem:Bool = itemOverlaps || itemPixelOverlap;
|
|
|
|
if (item.available && isTouchingItem && TouchUtil.justPressed)
|
|
{
|
|
var prevIndex:Int = selectedIndex;
|
|
|
|
if (!_isMainMenuState && selectedIndex != i)
|
|
{
|
|
newIndex = i;
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
FunkinSound.playOnce(Paths.sound('scrollMenu'), 0.4);
|
|
selectItem(i);
|
|
}
|
|
|
|
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, 1);
|
|
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.5);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
accept();
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (newIndex != selectedIndex && !_isMainMenuState)
|
|
{
|
|
FunkinSound.playOnce(Paths.sound('scrollMenu'), 0.4);
|
|
selectItem(newIndex);
|
|
}
|
|
#else
|
|
if (newIndex != selectedIndex)
|
|
{
|
|
FunkinSound.playOnce(Paths.sound('scrollMenu'), 0.4);
|
|
selectItem(newIndex);
|
|
}
|
|
#end
|
|
|
|
// Todo: bypass popup blocker on firefox
|
|
if (controls.ACCEPT) accept();
|
|
|
|
return;
|
|
}
|
|
|
|
function navAxis(index:Int, size:Int, prev:Bool, next:Bool, allowWrap:Bool):Int
|
|
{
|
|
if (prev == next) return index;
|
|
|
|
if (prev)
|
|
{
|
|
if (index > 0) index--;
|
|
else if (allowWrap) index = size - 1;
|
|
}
|
|
else
|
|
{
|
|
if (index < size - 1) index++;
|
|
else if (allowWrap) index = 0;
|
|
}
|
|
|
|
return index;
|
|
}
|
|
|
|
/**
|
|
* Controls navigation on a linear list of items such as Vertical.
|
|
* @param prev
|
|
* @param next
|
|
* @param allowWrap
|
|
*/
|
|
inline function navList(prev:Bool, next:Bool, allowWrap:Bool)
|
|
{
|
|
return navAxis(selectedIndex, length, prev, next, allowWrap);
|
|
}
|
|
|
|
/**
|
|
* Controls navigation on a grid
|
|
* @param latSize The size of the fixed axis of the grid, or the "lateral axis"
|
|
* @param latPrev Whether the 'prev' key is pressed along the fixed-lengthed axis. eg: "left" in Column mode
|
|
* @param latNext Whether the 'next' key is pressed along the fixed-lengthed axis. eg: "right" in Column mode
|
|
* @param prev Whether the 'prev' key is pressed along the variable-lengthed axis. eg: "up" in Column mode
|
|
* @param next Whether the 'next' key is pressed along the variable-lengthed axis. eg: "down" in Column mode
|
|
* @param allowWrap unused
|
|
*/
|
|
function navGrid(latSize:Int, latPrev:Bool, latNext:Bool, latAllowWrap:Bool, prev:Bool, next:Bool, allowWrap:Bool):Int
|
|
{
|
|
// The grid length along the variable-length axis
|
|
var size = Math.ceil(length / latSize);
|
|
// The selected position along the variable-length axis
|
|
var index = Math.floor(selectedIndex / latSize);
|
|
// The selected position along the fixed axis
|
|
var latIndex = selectedIndex % latSize;
|
|
|
|
latIndex = navAxis(latIndex, latSize, latPrev, latNext, latAllowWrap);
|
|
index = navAxis(index, size, prev, next, allowWrap);
|
|
|
|
return Std.int(Math.min(length - 1, index * latSize + latIndex));
|
|
}
|
|
|
|
public function accept():Void
|
|
{
|
|
var menuItem:T = members[selectedIndex];
|
|
|
|
if (!menuItem.available) return;
|
|
|
|
onAcceptPress.dispatch(menuItem);
|
|
|
|
if (menuItem.fireInstantly) menuItem.callback();
|
|
else
|
|
{
|
|
busy = true;
|
|
FunkinSound.playOnce(Paths.sound('confirmMenu'));
|
|
FlxFlicker.flicker(menuItem, 1, 0.06, true, false, function(_) {
|
|
busy = false;
|
|
menuItem.callback();
|
|
});
|
|
}
|
|
}
|
|
|
|
public function cancelAccept():Void
|
|
{
|
|
FlxFlicker.stopFlickering(members[selectedIndex]);
|
|
busy = false;
|
|
}
|
|
|
|
/**
|
|
* Selects an item in the list. If the item is not available, it will select the next available item.
|
|
* @param index The index of the item to select.
|
|
*/
|
|
public function selectItem(index:Int):Void
|
|
{
|
|
members[selectedIndex].idle();
|
|
|
|
if (!members[index].available)
|
|
{
|
|
if (index < selectedIndex)
|
|
{
|
|
final newIndex:Int = (index - 1 < 0) ? index + 1 : index - 1;
|
|
selectItem(newIndex);
|
|
return;
|
|
}
|
|
else if (index > selectedIndex)
|
|
{
|
|
final newIndex:Int = (index + 1 > members.length) ? index - 1 : index + 1;
|
|
selectItem(newIndex);
|
|
return;
|
|
}
|
|
}
|
|
|
|
selectedIndex = index;
|
|
|
|
var selectedMenuItem:T = members[selectedIndex];
|
|
selectedMenuItem.select();
|
|
onChange.dispatch(selectedMenuItem);
|
|
}
|
|
|
|
public function has(name:String):Bool
|
|
{
|
|
return byName.exists(name);
|
|
}
|
|
|
|
public function getItem(name:String):Null<T>
|
|
{
|
|
return byName[name];
|
|
}
|
|
|
|
override function destroy():Void
|
|
{
|
|
super.destroy();
|
|
byName.clear();
|
|
onChange.removeAll();
|
|
onAcceptPress.removeAll();
|
|
}
|
|
|
|
inline function get_selectedItem():T
|
|
{
|
|
return members[selectedIndex];
|
|
}
|
|
}
|
|
|
|
@:nullSafety
|
|
class MenuListItem extends FlxSprite
|
|
{
|
|
public var callback:Void->Void;
|
|
public var name:String;
|
|
public var available:Bool;
|
|
|
|
/**
|
|
* Set to true for things like opening URLs otherwise, it may it get blocked.
|
|
*/
|
|
public var fireInstantly:Bool = false;
|
|
|
|
public var selected(get, never):Bool;
|
|
|
|
function get_selected()
|
|
return alpha == 1.0;
|
|
|
|
public function new(x = 0.0, y = 0.0, name:String, callback, available:Bool = true)
|
|
{
|
|
super(x, y);
|
|
|
|
// This is just here to satisfy the null-safety checker
|
|
// setData still needs to be called since other classes may override it
|
|
this.name = name;
|
|
this.callback = callback;
|
|
this.available = available;
|
|
setData(name, callback, available);
|
|
idle();
|
|
}
|
|
|
|
function setData(name:String, ?callback:Void->Void, available:Bool):Void
|
|
{
|
|
this.name = name;
|
|
|
|
if (callback != null) this.callback = callback;
|
|
|
|
this.available = available;
|
|
}
|
|
|
|
/**
|
|
* Calls setData and resets/redraws the state of the item
|
|
* @param name the label.
|
|
* @param callback Unchanged if null.
|
|
*/
|
|
public function setItem(name:String, ?callback:Void->Void):Void
|
|
{
|
|
setData(name, callback, available);
|
|
|
|
if (selected) select();
|
|
else
|
|
idle();
|
|
}
|
|
|
|
public function idle():Void
|
|
{
|
|
alpha = 0.6;
|
|
}
|
|
|
|
public function select():Void
|
|
{
|
|
alpha = 1.0;
|
|
}
|
|
}
|
|
|
|
@:nullSafety
|
|
class MenuTypedItem<T:FlxSprite> extends MenuListItem
|
|
{
|
|
public var label(default, set):Null<T>;
|
|
|
|
public function new(x = 0.0, y = 0.0, label:T, name:String, callback, available:Bool = true)
|
|
{
|
|
super(x, y, name, callback, available);
|
|
// set label after super otherwise setters fuck up
|
|
this.label = label;
|
|
}
|
|
|
|
/**
|
|
* Use this when you only want to show the label
|
|
*/
|
|
public function setEmptyBackground()
|
|
{
|
|
var oldWidth = width;
|
|
var oldHeight = height;
|
|
makeGraphic(1, 1, 0x0);
|
|
width = oldWidth;
|
|
height = oldHeight;
|
|
}
|
|
|
|
function set_label(value:Null<T>):Null<T>
|
|
{
|
|
if (value != null)
|
|
{
|
|
value.x = x;
|
|
value.y = y;
|
|
value.alpha = alpha;
|
|
}
|
|
return this.label = value;
|
|
}
|
|
|
|
override function update(elapsed:Float)
|
|
{
|
|
super.update(elapsed);
|
|
if (label != null) label.update(elapsed);
|
|
}
|
|
|
|
override function draw()
|
|
{
|
|
super.draw();
|
|
if (label != null)
|
|
{
|
|
label.cameras = cameras;
|
|
label.scrollFactor.copyFrom(scrollFactor);
|
|
label.draw();
|
|
}
|
|
}
|
|
|
|
override function set_alpha(value:Float):Float
|
|
{
|
|
super.set_alpha(value);
|
|
|
|
if (label != null) label.alpha = alpha;
|
|
|
|
return alpha;
|
|
}
|
|
|
|
override function set_x(value:Float):Float
|
|
{
|
|
super.set_x(value);
|
|
|
|
if (label != null) label.x = x;
|
|
|
|
return x;
|
|
}
|
|
|
|
override function set_y(Value:Float):Float
|
|
{
|
|
super.set_y(Value);
|
|
|
|
if (label != null) label.y = y;
|
|
|
|
return y;
|
|
}
|
|
}
|
|
|
|
enum NavControls
|
|
{
|
|
Horizontal;
|
|
Vertical;
|
|
Both;
|
|
Columns(num:Int);
|
|
Rows(num:Int);
|
|
}
|
|
|
|
enum WrapMode
|
|
{
|
|
Horizontal;
|
|
Vertical;
|
|
Both;
|
|
None;
|
|
}
|