diff --git a/art/generateDoc.bat b/art/generateDoc.bat index 0e430256..595314b1 100644 --- a/art/generateDoc.bat +++ b/art/generateDoc.bat @@ -1,7 +1,7 @@ @ECHO OFF cd .. echo Building Game... -lime build windows --haxeflag="--macro include('scripting')" --haxeflag="-xml docs/doc.xml" -D DOCUMENTATION --no-output +lime build windows --haxeflag="-xml docs/doc.xml" -D doc-gen -D DOCUMENTATION --no-output echo art echo Generated the api xml file at docs/doc.xml diff --git a/mods/lua test/songs/luaTest.lua b/mods/lua test/songs/luaTest.lua index 198eb2bd..bd4a0563 100644 --- a/mods/lua test/songs/luaTest.lua +++ b/mods/lua test/songs/luaTest.lua @@ -16,8 +16,8 @@ function postCreate() createText('luaText', 'Bird Engine', posX + 50, 90, 0, 27, 'camHUD') addSprite('luaText', 'camHUD') - initShader('chromaticAberration') - addShader('camGame', 'chromaticAberration') + initShader('chrom', 'chromaticAberration') + addShader('camGame', 'chrom') print("created"); @@ -67,12 +67,16 @@ function beatHit(curBeat) if(math.fmod(curBeat, 2) == 0 and not hasTween) then --print(curBeat) shake('camHUD', 0.01, 0.1) - setShaderField('chromaticAberration', 'redOff', {ab1, 0}) - setShaderField('chromaticAberration', 'blueOff', {-ab1, 0}) + --setShaderField('chrom', 'redOff', {ab1, 0}) + --setShaderField('chrom', 'blueOff', {-ab1, 0}) + chrom.redOff = {ab1, 0} + chrom.blueOff = {-ab1, 0} hasTween = true elseif(math.fmod(curBeat, 4) == 0 and hasTween) then - setShaderField('chromaticAberration', 'redOff', {ab, 0}) - setShaderField('chromaticAberration', 'blueOff', {-ab, 0}) + --setShaderField('chrom', 'redOff', {ab, 0}) + --setShaderField('chrom', 'blueOff', {-ab, 0}) + chrom.redOff = {ab, 0} + chrom.blueOff = {-ab, 0} hasTween = false end diff --git a/source/funkin/backend/FlxAnimate.hx b/source/funkin/backend/FlxAnimate.hx index 5f92e9a4..63004509 100644 --- a/source/funkin/backend/FlxAnimate.hx +++ b/source/funkin/backend/FlxAnimate.hx @@ -7,14 +7,19 @@ import flixel.math.FlxAngle; import flixel.math.FlxRect; import flixel.graphics.frames.FlxFrame; import flixel.math.FlxPoint; +import openfl.display.BlendMode; class FlxAnimate extends flxanimate.FlxAnimate { static var rMatrix = new FlxMatrix(); - override function drawLimb(limb:FlxFrame, _rMatrix:FlxMatrix, ?colorTransform:ColorTransform) + override function drawLimb(limb:FlxFrame, _rMatrix:FlxMatrix, ?colorTransform:ColorTransform, ?blendMode:BlendMode) { if (alpha == 0 || colorTransform != null && (colorTransform.alphaMultiplier == 0 || colorTransform.alphaOffset == -255) || limb == null || limb.type == EMPTY) return; + + if (blendMode == null) + blendMode = BlendMode.NORMAL; + for (camera in cameras) { rMatrix.identity(); @@ -69,7 +74,7 @@ class FlxAnimate extends flxanimate.FlxAnimate { } rMatrix.translate(_point.x, _point.y); - camera.drawPixels(limb, null, rMatrix, colorTransform, blend, antialiasing, shaderEnabled ? shader : null); + camera.drawPixels(limb, null, rMatrix, colorTransform, blendMode, antialiasing, shaderEnabled ? shader : null); #if FLX_DEBUG FlxBasic.visibleCount++; #end diff --git a/source/funkin/backend/MusicBeatState.hx b/source/funkin/backend/MusicBeatState.hx index 54e51334..dc45057a 100644 --- a/source/funkin/backend/MusicBeatState.hx +++ b/source/funkin/backend/MusicBeatState.hx @@ -106,6 +106,7 @@ class MusicBeatState extends FlxState implements IBeatReceiver "SHADER" => new Map(), "TIMERS" => new Map(), "SOUNDS" => new Map(), + "VIDEOS" => new Map(), "SCRIPTS" => new Map() ]; #end diff --git a/source/funkin/backend/MusicBeatSubstate.hx b/source/funkin/backend/MusicBeatSubstate.hx index e2bbb312..ca1c2e2a 100644 --- a/source/funkin/backend/MusicBeatSubstate.hx +++ b/source/funkin/backend/MusicBeatSubstate.hx @@ -69,6 +69,23 @@ class MusicBeatSubstate extends FlxSubState implements IBeatReceiver public var scriptName:String = null; + @:unreflective public var luaScriptsAllowed:Bool = true; + #if ENABLE_LUA + /** + * Map containing all the objects created from Lua + */ + @:unreflective public var luaObjects(default, never):Map> = [ + "SPRITE" => new Map(), + "TEXT" => new Map(), + "TWEEN" => new Map(), + "SHADER" => new Map(), + "TIMERS" => new Map(), + "SOUNDS" => new Map(), + "VIDEOS" => new Map(), + "SCRIPTS" => new Map() + ]; + #end + /** * Game Controls. (All players / Solo) */ diff --git a/source/funkin/backend/assets/ModsFolder.hx b/source/funkin/backend/assets/ModsFolder.hx index cd1704ed..5884db40 100644 --- a/source/funkin/backend/assets/ModsFolder.hx +++ b/source/funkin/backend/assets/ModsFolder.hx @@ -93,6 +93,11 @@ class ModsFolder { public static function getModsList():Array { var mods:Array = []; #if MOD_SUPPORT + final modsList:Array = FileSystem.readDirectory(modsPath); + + if (modsList == null || modsList.length <= 0) + return mods; + for(modFolder in FileSystem.readDirectory(modsPath)) { if (FileSystem.isDirectory('${modsPath}${modFolder}')) { mods.push(modFolder); diff --git a/source/funkin/backend/scripting/LuaScript.hx b/source/funkin/backend/scripting/LuaScript.hx index e22b93df..b39a369e 100644 --- a/source/funkin/backend/scripting/LuaScript.hx +++ b/source/funkin/backend/scripting/LuaScript.hx @@ -1,12 +1,14 @@ package funkin.backend.scripting; -import funkin.backend.scripting.events.CancellableEvent; +import funkin.backend.scripting.events.CancellableEvent; import funkin.backend.scripting.lua.*; import haxe.DynamicAccess; import openfl.utils.Assets; +import hscript.IHScriptCustomBehaviour; + #if ENABLE_LUA import llua.State; import llua.Macro.*; @@ -56,7 +58,7 @@ class LuaScript extends Script{ state = LuaL.newstate(); Lua.set_callbacks_function(cpp.Callable.fromStaticFunction(callback_handler)); - LuaL.openlibs(state); + state.openlibs(); Lua.register_hxtrace_func(cpp.Callable.fromStaticFunction(print_function)); state.register_hxtrace_lib(); @@ -328,8 +330,13 @@ class LuaScript extends Script{ } public function onPointerIndex(obj:Dynamic, key:String) { - if (obj != null) - return Reflect.getProperty(obj, key); + if (obj != null) { + if(obj is IHScriptCustomBehaviour) + return cast(obj, IHScriptCustomBehaviour).hget(key); + else + return Reflect.getProperty(obj, key); + } + return null; } @@ -345,8 +352,13 @@ class LuaScript extends Script{ public function onPointerNewIndex(obj:Dynamic, key:String, val:Dynamic) { if (key == "__gc") return null; - if (obj != null) - Reflect.setProperty(obj, key, val); + if (obj != null) { + if(obj is IHScriptCustomBehaviour) + cast(obj, IHScriptCustomBehaviour).hset(key, val); + else + Reflect.setProperty(obj, key, val); + } + return null; } diff --git a/source/funkin/backend/scripting/lua/HScriptFunctions.hx b/source/funkin/backend/scripting/lua/HScriptFunctions.hx index 4ff79866..ead28e7f 100644 --- a/source/funkin/backend/scripting/lua/HScriptFunctions.hx +++ b/source/funkin/backend/scripting/lua/HScriptFunctions.hx @@ -1,6 +1,7 @@ package funkin.backend.scripting.lua; class HScriptFunctions { + //TODO: turn the hscript thing into a singleton public static function getHScriptFunctions(?instance:MusicBeatState, ?script:Script):Map { #if ENABLE_LUA return [ diff --git a/source/funkin/backend/scripting/lua/LuaPlayState.hx b/source/funkin/backend/scripting/lua/LuaPlayState.hx index b1ca92f3..22eb90c7 100644 --- a/source/funkin/backend/scripting/lua/LuaPlayState.hx +++ b/source/funkin/backend/scripting/lua/LuaPlayState.hx @@ -1,12 +1,9 @@ package funkin.backend.scripting.lua; #if ENABLE_LUA -import funkin.backend.assets.ModsFolder; import funkin.backend.system.Conductor; import funkin.backend.scripting.lua.LuaTools; import funkin.backend.chart.ChartData.ChartEvent; import funkin.game.PlayState; -import funkin.options.Options; -import flixel.util.typeLimit.OneOfThree; import flixel.FlxG; final class LuaPlayState { @@ -80,7 +77,7 @@ final class LuaPlayState { return [ "startCutscene" => function(prefix:String, ?cutsceneScriptPath:String) { PlayState.instance.startCutscene(prefix, cutsceneScriptPath, () -> { - PlayState.instance.scripts.luaCall("onStartCutscene", []); + PlayState.instance.scripts.luaCall("onStartCutscene", [prefix]); }); }, "callFunction" => function(func:String, ?args:Array) { diff --git a/source/funkin/backend/scripting/lua/LuaTools.hx b/source/funkin/backend/scripting/lua/LuaTools.hx index 8355d4ed..f6caf3c3 100644 --- a/source/funkin/backend/scripting/lua/LuaTools.hx +++ b/source/funkin/backend/scripting/lua/LuaTools.hx @@ -108,6 +108,9 @@ class LuaTools { else if(instance.luaObjects["TEXT"].exists(name)) { object = instance.luaObjects["TEXT"].get(name); } + else if(instance.luaObjects["VIDEOS"].exists(name)) { + object = instance.luaObjects["VIDEOS"].get(name); + } else if(object == null) { object = Reflect.getProperty(instance, name); } @@ -115,6 +118,18 @@ class LuaTools { return object; } + public static function removeLuaObject(instance:MusicBeatState, name:String) { + if(instance.luaObjects["SPRITE"].exists(name)) { + instance.luaObjects["SPRITE"].remove(name); + } + else if(instance.luaObjects["TEXT"].exists(name)) { + instance.luaObjects["TEXT"].remove(name); + } + else if(instance.luaObjects["VIDEOS"].exists(name)) { + instance.luaObjects["VIDEOS"].remove(name); + } + } + public static function getEase(?ease:String = '') { return switch(ease.toLowerCase().trim()) { @@ -169,7 +184,7 @@ class LuaTools { } public static function getColor(color:Dynamic):FlxColor { - return CoolUtil.getDefault(CoolUtil.getColorFromDynamic(color), FlxColor.BLACK); + return CoolUtil.getColorFromDynamic(color).getDefault(FlxColor.BLACK); } #end } diff --git a/source/funkin/backend/scripting/lua/ShaderFunctions.hx b/source/funkin/backend/scripting/lua/ShaderFunctions.hx index ff500c7f..b0c538ee 100644 --- a/source/funkin/backend/scripting/lua/ShaderFunctions.hx +++ b/source/funkin/backend/scripting/lua/ShaderFunctions.hx @@ -1,12 +1,11 @@ package funkin.backend.scripting.lua; #if ENABLE_LUA -import funkin.backend.shaders.CustomShader; import funkin.backend.scripting.lua.shaders.LuaShader; final class ShaderFunctions { public static function getShaderFunctions(instance:MusicBeatState, ?script:Script):Map { return [ - "initShader" => function(name:String, ?glslVersion:Int = 120) { + "initShader" => function(name:String, shader:String, ?glslVersion:Int = 120) { if(!Options.gameplayShaders) return; if(instance.luaObjects["SHADER"].exists(name)) { @@ -14,7 +13,7 @@ final class ShaderFunctions { return; } - var cShader = new LuaShader(name, Std.string(glslVersion)); + var cShader = new LuaShader(shader, Std.string(glslVersion)); instance.luaObjects["SHADER"].set(name, cShader); cast(script, LuaScript).set(name, cShader); diff --git a/source/funkin/backend/scripting/lua/SpriteFunctions.hx b/source/funkin/backend/scripting/lua/SpriteFunctions.hx index 76da1812..96d9c020 100644 --- a/source/funkin/backend/scripting/lua/SpriteFunctions.hx +++ b/source/funkin/backend/scripting/lua/SpriteFunctions.hx @@ -1,8 +1,10 @@ package funkin.backend.scripting.lua; -import flixel.text.FlxText.FlxTextBorderStyle; +import flixel.util.FlxColor; import flixel.text.FlxText; -import funkin.game.PlayState; + +import funkin.backend.scripting.events.PlayAnimEvent.PlayAnimContext; +import funkin.game.Character; final class SpriteFunctions { @@ -31,7 +33,7 @@ final class SpriteFunctions { }, "setText" => function(name:String, text:String = '') { var yourText:FunkinText = LuaTools.getObject(instance, name); - if(yourText != null){ + if(yourText != null && yourText is FlxText){ yourText.text = text; } }, @@ -65,10 +67,24 @@ final class SpriteFunctions { sprite = PlayState.instance.luaObjects["SPRITE"].get(name); */ if(sprite != null) { + if(!sprite.alive && !sprite.exists) //Check if it was removed, but not destroyed + sprite.revive(); instance.add(sprite); sprite.cameras = [LuaTools.getCamera(camera)]; } }, + "removeSprite" => function(name:String, destroy:Bool = true) { + var sprite:FlxSprite = LuaTools.getObject(instance, name); + + if(sprite != null) { + sprite.kill(); + instance.remove(sprite, true); + if(destroy) { + sprite.destroy(); + LuaTools.removeLuaObject(instance, name); + } + } + }, "setSpriteCamera" => function(name:String, ?camera:String = 'default') { var sprite:FlxSprite = LuaTools.getObject(instance, name); /* @@ -110,11 +126,15 @@ final class SpriteFunctions { }, "setSpriteColor" => function(name:String, ?r:Float = 1, ?g:Float = 1, ?b:Float = 1) { var sprite:FlxSprite = LuaTools.getObject(instance, name); + var newColor:FlxColor = FlxColor.fromRGBFloat(r, g, b); if(sprite != null) { + /* sprite.colorTransform.redMultiplier = r; sprite.colorTransform.greenMultiplier = g; sprite.colorTransform.blueMultiplier = b; + */ + sprite.color = newColor; } }, "setSpriteColorOffset" => function(name:String, ?r:Float = 0, ?g:Float = 0, ?b:Float = 0) { @@ -128,4 +148,104 @@ final class SpriteFunctions { } ]; } + + public static function getAnimatedSpriteFunctions(instance:MusicBeatState, ?script:Script):Map + { + return [ + "createAnimatedSprite" => function(name:String, ?imagePath:String, ?x:Float = 0, ?y:Float = 0) { + if (instance.luaObjects["SPRITE"].exists(name)) + return; + + var theSprite:FunkinSprite = new FunkinSprite(x, y); + if (imagePath != null && imagePath.length > 0) + theSprite.loadSprite(imagePath); + instance.luaObjects["SPRITE"].set(name, theSprite); + cast(script, LuaScript).set(name, theSprite); + }, + "addAnimationByPrefix" => function(name:String, anim:String, prefix:String, framerate:Int = 24, forced:Bool = false, type:String = 'none') { + if (instance.luaObjects["SPRITE"].exists(name)) + { + var sprite:FunkinSprite = LuaTools.getObject(instance, name); + + var animType:XMLAnimType = switch (type.toLowerCase()) + { + case 'beat': XMLAnimType.BEAT; + case 'loop' | 'looped': XMLAnimType.LOOP; + default: XMLAnimType.NONE; + } + + sprite.addAnim(anim, prefix, framerate, null, forced, animType); + } + }, + "addAnimationByIndices" => function(name:String, anim:String, prefix:String, indices:String, framerate:Int = 24, forced:Bool = false, type:String = 'none') { + if (instance.luaObjects["SPRITE"].exists(name)) + { + var sprite:FunkinSprite = LuaTools.getObject(instance, name); + + var animType:XMLAnimType = switch (type.toLowerCase()) + { + case 'beat': XMLAnimType.BEAT; + case 'loop' | 'looped': XMLAnimType.LOOP; + default: XMLAnimType.NONE; + } + + var strIndices:Array = indices.trim().split(','); + var inds:Array = []; + for (indice in strIndices) + inds.push(Std.parseInt(indice)); + + sprite.addAnim(anim, prefix, framerate, null, forced, inds, animType); + } + }, + "addOffset" => function(name:String, anim:String, x:Float = 0.0, y:Float = 0.0) { + if (instance.luaObjects["SPRITE"].exists(name)) { + var sprite:FunkinSprite = LuaTools.getObject(instance, name); + + sprite.addOffset(anim, x, y); + } + else { + var sprite:FlxSprite = LuaTools.getObject(instance, name); + + if (sprite != null) { + if (sprite is FunkinSprite) + { + cast(sprite, FunkinSprite).addOffset(anim, x, y); + } + } + } + }, + "playAnim" => function(name:String, anim:String, forced:Bool = false, reverse:Bool = false, initFrame:Int = 0, context:String = 'none') { + var animContext:PlayAnimContext = switch (context.toLowerCase()) + { + case 'sing' | 'singing': SING; + case 'dance' | 'dancing': DANCE; + case 'miss' | 'missed': MISS; + case 'lock' | 'locked': LOCK; + default: NONE; + } + + if (instance.luaObjects["SPRITE"].exists(name)) + { + var sprite:FunkinSprite = LuaTools.getObject(instance, name); + + if (sprite.hasAnim(anim)) + sprite.playAnim(anim, forced, animContext, reverse, initFrame); + } + else + { + var sprite:FlxSprite = LuaTools.getObject(instance, name); + + if (sprite != null) + { + if (sprite is Character) + { + cast(sprite, Character).playAnim(anim, forced, animContext, reverse, initFrame); + } + else if (sprite.animation.exists(anim)) + sprite.animation.play(anim, forced, reverse, initFrame); + } + } + } + ]; + } } \ No newline at end of file diff --git a/source/funkin/backend/scripting/lua/VideoFunctions.hx b/source/funkin/backend/scripting/lua/VideoFunctions.hx new file mode 100644 index 00000000..97a3d240 --- /dev/null +++ b/source/funkin/backend/scripting/lua/VideoFunctions.hx @@ -0,0 +1,61 @@ +package funkin.backend.scripting.lua; + +import flixel.util.FlxTimer; +import hxvlc.flixel.FlxVideoSprite; + +class VideoFunctions { + public static function getVideoFunctions(instance:MusicBeatState, ?script:Script):Map { + return [ + "createVideo" => function(name:String, ?videoPath:String = null, ?ext:String = "mp4", ?x:Float = 0, ?y:Float = 0) { + if(instance.luaObjects["VIDEOS"].exists(name)) + return; + + var theVideo:FlxVideoSprite = new FlxVideoSprite(x, y); + if(videoPath != null && videoPath.length > 0) { + if(!theVideo.load(Paths.video(videoPath, ext))) return; + } + + instance.luaObjects["VIDEOS"].set(name, theVideo); + cast(script, LuaScript).set(name, theVideo); + }, + "loadVideo" => function(name:String, ?videoPath:String = null, ?ext:String = "mp4") { + var video:FlxVideoSprite = LuaTools.getObject(instance, name); + + if(video != null) { + if(videoPath != null && videoPath.length > 0) { + if(!video.load(Paths.video(videoPath, ext))) return; + } + } + }, + "playVideo" => function(name:String, ?volume:Int) { + var video:FlxVideoSprite = LuaTools.getObject(instance, name); + + if(video != null) { + if(volume != null) { + video.autoVolumeHandle = false; + video.bitmap.volume = Std.int(FlxMath.bound(volume, 0, 100)); + } + new FlxTimer().start(0.001, (_) -> video.play()); + } + }, + "pauseVideo" => function(name:String) { + var video:FlxVideoSprite = LuaTools.getObject(instance, name); + + if(video != null) + video.pause(); + }, + "resumeVideo" => function(name:String) { + var video:FlxVideoSprite = LuaTools.getObject(instance, name); + + if(video != null) + video.resume(); + }, + "stopVideo" => function(name:String) { + var video:FlxVideoSprite = LuaTools.getObject(instance, name); + + if(video != null) + video.stop(); + } + ]; + } +} \ No newline at end of file diff --git a/source/funkin/backend/scripting/lua/shaders/LuaShader.hx b/source/funkin/backend/scripting/lua/shaders/LuaShader.hx index 6b3252e0..092dada1 100644 --- a/source/funkin/backend/scripting/lua/shaders/LuaShader.hx +++ b/source/funkin/backend/scripting/lua/shaders/LuaShader.hx @@ -1,5 +1,5 @@ package funkin.backend.scripting.lua.shaders; import funkin.backend.shaders.CustomShader; -// TODO: Create a shader class in which its variables can be accessed from lua (dot-like syntax) + typedef LuaShader = CustomShader; diff --git a/source/funkin/backend/system/Main.hx b/source/funkin/backend/system/Main.hx index 8a365468..b977f2be 100644 --- a/source/funkin/backend/system/Main.hx +++ b/source/funkin/backend/system/Main.hx @@ -49,6 +49,11 @@ class Main extends Sprite public static var game:FunkinGame; + /** + * The time since the game was focused last time in seconds. + */ + public static var timeSinceFocus(get, never):Float; + public static var time:Int = 0; // You can pretty much ignore everything from here on - your code should go in your states. @@ -151,6 +156,7 @@ class Main extends Sprite Conductor.init(); AudioSwitchFix.init(); EventManager.init(); + FlxG.signals.focusGained.add(onFocus); FlxG.signals.preStateSwitch.add(onStateSwitch); FlxG.signals.postStateSwitch.add(onStateSwitchPost); @@ -186,6 +192,10 @@ class Main extends Sprite {asset: diamond, width: 32, height: 32}, new FlxRect(-200, -200, FlxG.width * 1.4, FlxG.height * 1.4)); } + public static function onFocus() { + _tickFocused = FlxG.game.ticks; + } + private static function onStateSwitch() { scaleMode.resetSize(); } @@ -205,4 +215,9 @@ class Main extends Sprite MemoryUtil.clearMajor(); } + + private static var _tickFocused:Float = 0; + public static function get_timeSinceFocus():Float { + return (FlxG.game.ticks - _tickFocused) / 1000; + } } diff --git a/source/funkin/menus/FreeplayState.hx b/source/funkin/menus/FreeplayState.hx index f8310ebf..2c19fd3e 100644 --- a/source/funkin/menus/FreeplayState.hx +++ b/source/funkin/menus/FreeplayState.hx @@ -3,6 +3,7 @@ package funkin.menus; import funkin.backend.scripting.lua.LuaTools; import funkin.backend.chart.Chart; import funkin.backend.chart.ChartData.ChartMetaData; +import funkin.backend.system.Conductor; import haxe.io.Path; import openfl.text.TextField; import flixel.text.FlxText; @@ -240,7 +241,11 @@ class FreeplayState extends MusicBeatState autoplayElapsed += elapsed; if (!disableAutoPlay && !songInstPlaying && (autoplayElapsed > timeUntilAutoplay || FlxG.keys.justPressed.SPACE)) { if (curPlayingInst != (curPlayingInst = Paths.inst(songs[curSelected].name, songs[curSelected].difficulties[curDifficulty]))) { - var huh:Void->Void = function() FlxG.sound.playMusic(curPlayingInst, 0); + var huh:Void->Void = function() + { + FlxG.sound.playMusic(curPlayingInst, 0); + Conductor.changeBPM(songs[curSelected].bpm, songs[curSelected].beatsPerMeasure, songs[curSelected].stepsPerBeat); + } if(!disableAsyncLoading) Main.execAsync(huh); else huh(); } diff --git a/source/funkin/options/OptionsScreen.hx b/source/funkin/options/OptionsScreen.hx index 45580821..b703d994 100644 --- a/source/funkin/options/OptionsScreen.hx +++ b/source/funkin/options/OptionsScreen.hx @@ -45,7 +45,7 @@ class OptionsScreen extends FlxTypedSpriteGroup { if (members.length > 0) { members[curSelected].selected = true; - if (controls.ACCEPT || FlxG.mouse.justReleased) + if (controls.ACCEPT || (FlxG.mouse.justReleased && Main.timeSinceFocus > 0.25)) members[curSelected].onSelect(); if (controls.LEFT_P) members[curSelected].onChangeSelection(-1);