Input Buffering and Event-Driven Action System for Combat system

This code serves as a middle ground between the player’s input and the action they want to perform. It validates actions and handles player commands based on state changes. I’m unsure about the cancel logic, so I’d appreciate feedback on that and any other suggestions for improvement.

--[[
	Event driven action service.
	
	Flow:
		- User presses key, server receives it.
		- Server will run the perform function with the parameters
		- The :Perform() function will first validate the action
		
		- If the action isn't valid at the moment, queue the action if the player is still holding the key. (InputState boolean) 
		- If it is valid, run the action like normal.
		
	Note: ProcessQueuedAction and CheckForCancel functions are ran every time a state is changed. Hence why I check if the states are relevant before doing anything.
	
	This is my first time doing something like this, and I'm simply exploring other ways to handle actions in games. This is for a Battlegrounds game if you haven't guessed.
]]

local ServerStorage = game:GetService("ServerStorage")

local Modules = ServerStorage:WaitForChild("Modules")

local CooldownManager = require(Modules.CooldownManager)
local Invalidators = require(Modules.Invalidators)
local StateManager = require(Modules.StateManager)

local Actions = {}

function Actions:ValidateAction(playerObject, action)
	local InvalidatingStates = Invalidators[action] or {}
	local isAllowed = true --// assume we are allowed to do the action until proven otherwise
	
	for state, bool in InvalidatingStates do
		if StateManager:CheckState(playerObject, state) == true then
			isAllowed = false
			break
		end
	end
	
	local InCooldown = CooldownManager:CheckCooldown(playerObject, action)
	
	if InCooldown then
		isAllowed = false
	end
	
	return isAllowed
end

function Actions:CheckForCancel(playerObject, stateString: string, state: boolean)
	if stateString == "Hit" or stateString == "Stunned" then
		if not playerObject.CurrentAction then
			return
		end

		local invalidator = Invalidators[playerObject.CurrentAction.Name]
		if not invalidator then return end

		if invalidator[stateString] and state == true and playerObject.CurrentAction.Name ~= "Attacking" then
			playerObject.CurrentAction.Cancel()
			playerObject.CurrentAction = nil
		end
	end
end

function Actions:QueueActionToPlayer(playerObject, action, ...)
	local args = ...
	
	playerObject.QueuedAction = {
		Name = action,
		Callback = function()
			Actions:Perform(playerObject, action, args)
		end,
	}
	
	print("QUEUED")
end

function Actions:ClearQueue(playerObject)
	playerObject.QueuedAction = nil
end

function Actions:CheckQueue(playerObject)
	return playerObject.QueuedAction ~= nil
end

function Actions:ProcessQueuedAction(playerObject, stateString)
	local queuedAction = playerObject.QueuedAction
	if not queuedAction then return end
	
	
	local InvalidatingStates = Invalidators[queuedAction.Name]
	
	if not InvalidatingStates then
		return
	end
	
	local IsRelevant = InvalidatingStates[stateString] ~= nil
	
	if not IsRelevant then
		return
	end
	
	local isAllowed = Actions:ValidateAction(playerObject, queuedAction.Name)
	
	if isAllowed then
		queuedAction.Callback()
		Actions:ClearQueue(playerObject)
	end
	
end

--// the vararg itself is a table
function Actions:Perform(playerObject, action, ...)
	local ActionToPerform = require(script:FindFirstChild(action))
	local player = playerObject.player
	
	local args = ...
	local isAllowed = Actions:ValidateAction(playerObject, action)
	
	--// sorry for making this part so messy
	if not isAllowed then
		if args == nil then return end
		
		if args.InputState == true then
			Actions:QueueActionToPlayer(playerObject, action, args)
		elseif args.InputState == false then
			Actions:ClearQueue(playerObject)
			playerObject.CurrentAction = nil
		end
		
		return
	end
	
	playerObject.CurrentAction = {
		Name = action,
		Args = args,
		StartedAt = os.clock(),
		Cancel = function()
			if not args then
				args = {}
			end

			args.Cancel = true

			ActionToPerform(player, playerObject, args)
			playerObject.CurrentAction = nil
		end,
	}
	
	ActionToPerform(player, playerObject, args)
end

function Actions:UpdateOnStateChange(playerObject, stateString, state: boolean) --// update on state change
	Actions:CheckForCancel(playerObject, stateString, state)
	Actions:ProcessQueuedAction(playerObject, stateString)
end

--// perform an action without needing validation
function Actions:ForcePerform(playerObject, action, ...)
	local ActionToPerform = require(script:FindFirstChild(action))
	local player = playerObject.player
	
	local args = ...
	
	ActionToPerform(player, playerObject, args)
end

return Actions

What is the point of that?

Overengineering all over again.

function Actions.ForcePerform(playerObject, action, arg)
	local ActionToPerform = require(script:FindFirstChild(action))
	local player = playerObject.player
	ActionToPerform(player, playerObject, args)
end

Anyway why are you creating methods if you never get to use self? :skull:

1 Like

thanks for pointing that out lol i completely overlooked that

fair point

2 Likes