Set Up Character CPU Logic

On this page

Warning
This article is a stub! We’ll have more info on this topic coming soon.

CPU Recovery

In order for the CPU character to choose a recovery option while offstage, you will need to override the GetCpuRecoveryOptions() function. This function returns an array of all the attacks that are currently a viable recovery option. To determine whether a basic recovery move will be able to make it to the ledge, the stage, or (optionally) the wall, you can use self:IsBasicRecoveryMoveValid(), which takes the following parameters:

  • VecToLedgeLocation – A vector from the character to the nearest ledge. This parameter is supplied directly in GetCpuRecoveryOptions().
  • WallXPos – The x position of the nearest wall. This parameter is also supplied directly in GetCpuRecoveryOptions().
  • WallMinHeight – The y position of the lowest point on the nearest wall. This parameter is also supplied directly in GetCpuRecoveryOptions().
  • RecoveryMoveRangeIncludingLedgeGrabBox – The maximum range of this recovery move, including the ledge grab box. This needs to be calculated manually, usually by spawning the character at location [0, 0] in training mode and then performing the attack while facing right, then recording the character’s location when their ledge grab box appears at its maximum range. Add the character’s maximum ledge grab offset to that value. If the move can be angled, you will want to call self:IsBasicRecoveryMoveValid() for both the non-angled and maximum angled version of this move.
  • AerialDriftSlopeValue – The ratio of the character’s maximum horizontal speed to their maximum vertical speed when falling after performing the move. This needs to be calculated manually by going into the game and performing the move in training mode, then holding fully forward while in special fall until your character reaches their maximum horizontal AND vertical speed, then recording those values. It should look like local AerialDriftSlopeValue = 13.5 / 28.5 where the character’s maximum horizontal speed while falling was 13.5 and their maximum vertical speed while falling was 28.5. This number does not have to be exact.
  • MoveCanWalljump – Whether this move can walljump.

After checking for the validity of all relevant moves, you will want to include a last-ditch effort attempt if no valid options are found. This makes it so your CPU at least tries to make it back instead of just accepting defeat.

Here’s a complete example of how Clairen’s CPU checks for the validity of her recovery options:

function Clairen:GetCpuRecoveryOptions(VecToLedgeLocation, WallXPos, WallMinHeight, LedgeOccupied)
	local RecoveryOptions = {}
	
	local AerialDriftSlopeValue = 13.5 / 28.35

	-- USPECIAL
	local UspecialLedgeGrabBoxCornerOffset = Vector2D.new( 150.0, 200.0 )
	local UspecialNormalDistance = Vector2D.new( 435.0, 641.0 ) + UspecialLedgeGrabBoxCornerOffset
	local UspecialAngledDistance = Vector2D.new( 605.0, 577.0 ) + UspecialLedgeGrabBoxCornerOffset

	if ( self:IsBasicRecoveryMoveValid( VecToLedgeLocation, WallXPos, WallMinHeight, UspecialNormalDistance, AerialDriftSlopeValue, false )
		or self:IsBasicRecoveryMoveValid( VecToLedgeLocation, WallXPos, WallMinHeight, UspecialAngledDistance, AerialDriftSlopeValue, false ) ) then

		table.insert( RecoveryOptions, ERivalsCharacterAttack.Uspecial )
	end

	-- FSPECIAL
	local FspecialLedgeGrabBoxCornerOffset = Vector2D.new( 150.0, 170.0 )
	local FspecialDistance = Vector2D.new( 400.0, 0.0 ) + FspecialLedgeGrabBoxCornerOffset

	if ( self:IsBasicRecoveryMoveValid( VecToLedgeLocation, WallXPos, WallMinHeight, FspecialDistance, AerialDriftSlopeValue, true ) ) then
		table.insert( RecoveryOptions, ERivalsCharacterAttack.Fspecial )
	end

	local MyLocation = self:GetLocation2D()

	-- Last ditch effort
	if ( #RecoveryOptions == 0 and MyLocation.Y < -UspecialNormalDistance.Y ) then
		table.insert( RecoveryOptions, ERivalsCharacterAttack.Uspecial )
	end

	return RecoveryOptions
end

For more complex recovery moves, you will have to determine if it’s valid on your own. For example, here’s how Zetterburn determines if Up Special is valid:

-- USPECIAL
local UspecialTravelDistance = 800.0
local UspecialLedgeGrabBoxCornerOffset = Vector2D.new( 170.0 * math.sign( VecToLedgeLocation.X ), 230.0 )
local UspecialLedgeGrabBoxCornerToLedge = VecToLedgeLocation - UspecialLedgeGrabBoxCornerOffset
local UspecialLedgeGrabCornerLocation = self:GetLocation2D() + UspecialLedgeGrabBoxCornerOffset
local UspecialXCutoff = UspecialTravelDistance - 40.0 + ( UspecialLedgeGrabCornerLocation.Y * AerialDriftSlopValue ) -- Subtract 40 to account for Zetterburn accelerating to his max drift
local InUspecialZoneAboveLedge = UspecialLedgeGrabBoxCornerToLedge.Y <= 0.0 && math.square( UspecialLedgeGrabBoxCornerToLedge.X ) < math.square( UspecialXCutoff )

if UspecialLedgeGrabBoxCornerToLedge:SizeSquared() > math.square( UspecialTravelDistance ) || InUspecalZoneAboveLedge then
	table.insert( RecoveryOptions, ERivalsCharacterAttack.Uspecial )
end

Once the CPU has selected a recovery move, it now needs to know what to do while using that recovery move. To add this functionality, you will need to overwrite UpdateCpuInputsRecovery(). This function is run every frame while recovering, regardless of what state your character is in. The general flow of this function is:

1. Release all currently held inputs:

for _, InputAction in pairs(ERivalsBufferedInputAction) do
		self.InputHistory:SetActionDown( InputAction, false )
	end

2. Get the relevant data of your character’s position offstage, usually something like:

local MyLocation = self:GetLocation2D()
	local MyState = self:GetState()
	local JoystickPos = self.InputHistory:GetJoystickPosition( 1 )
	local StageData = self:GetCurrentStageData()
	local WallXPos = StageData:GetWallXPosition()
	local RecoveryDestination = Vector.new( 0.0 )
	local LedgeOccupied = false
	local ClosestLedge = self:GetClosestLedge( RecoveryDestination )
	if ( ClosestLedge ) then
		LedgeOccupied = ClosestLedge:IsOccupied()
	end

local VecToRecoveryDestination = Vector.new()
	VecToRecoveryDestination.X = RecoveryDestination.X - MyLocation.X
	VecToRecoveryDestination.Y = RecoveryDestination.Y - MyLocation.Y
	VecToRecoveryDestination.Z = RecoveryDestination.Z

3. Decide whether to start using a recovery move, and return from the function if so:

if ( self:UpdateCpuInputsRecoveryBasic( JoystickPos, -800.0 ) ) then
		local RecoveryOptions = self:GetCpuRecoveryOptionsTable( VecToRecoveryDestination, WallXPos, StageData:GetWallMinHeight(), LedgeOccupied )
		if ( #RecoveryOptions > 0 ) then
			local RandomRecoveryMoveIndex = self:GetRandomInt( #RecoveryOptions - 1 ) + 1
			self:SetCpuAttackInputs( RecoveryOptions[RandomRecoveryMoveIndex], 0.0, true )
		end

		return
	end

4. Check if you’re performing a specific attack and run logic if so. This example from Clairen.lua has logic for Up Special and Forward Special:

if ( self:IsPerformingAttack( ERivalsCharacterAttack.Uspecial ) ) then
		if ( self:GetWindow() < 2 ) then
			if ( ( MyLocation.Y < -790.0 or self:CpuShouldPerformAction( 0.5, 0.15 ) ) and self:GetFacingDirectionFloat() * MyLocation.X < 0.0 ) then
				JoystickPos = Vector2D.ZeroVector
			end
		elseif ( self:GetWindow() == 7 ) then
			if ( MyLocation.Y < -220.0 or LedgeOccupied or math.square( VecToRecoveryDestination.X ) > math.square( 180.0 ) or self:CpuShouldPerformAction( 0.5, 0.1 ) ) then
				self.InputHistory:SetActionDown( ERivalsBufferedInputAction.Special, true ) -- Use Uspecial 2
			end
		end
	elseif ( self:IsPerformingAttack( ERivalsCharacterAttack.Fspecial ) ) then
		-- Always hold toward center stage
		JoystickPos = Vector2D.new( -math.sign( MyLocation.X ), 0.0 )
		if ( VecToRecoveryDestination:SizeSquared() > math.square( 200.0 ) or LedgeOccupied ) then
			self:SpamCpuInput( ERivalsBufferedInputAction.Jump )
		end
	end

5. Finally, set the joystick position based on the JoystickPos variable defined earlier in this function:

self.InputHistory:SetJoystickPosition( JoystickPos )

For reference, here is the full UpdateCpuInputsRecovery() function from Clairen.lua:

function Clairen:UpdateCpuInputsRecovery()
	local MyLocation = self:GetLocation2D()
	local MyState = self:GetState()
	local JoystickPos = self.InputHistory:GetJoystickPosition( 1 )

	for _, InputAction in pairs(ERivalsBufferedInputAction) do
		self.InputHistory:SetActionDown( InputAction, false )
	end

	local StageData = self:GetCurrentStageData()
	local WallXPos = StageData:GetWallXPosition()
	local RecoveryDestination = Vector.new( 0.0 )
	local LedgeOccupied = false
	local ClosestLedge = self:GetClosestLedge( RecoveryDestination )
	if ( ClosestLedge ) then
		LedgeOccupied = ClosestLedge:IsOccupied()
	end

	local VecToRecoveryDestination = Vector.new()
	VecToRecoveryDestination.X = RecoveryDestination.X - MyLocation.X
	VecToRecoveryDestination.Y = RecoveryDestination.Y - MyLocation.Y
	VecToRecoveryDestination.Z = RecoveryDestination.Z

	if ( self:UpdateCpuInputsRecoveryBasic( JoystickPos, -800.0 ) ) then
		local RecoveryOptions = self:GetCpuRecoveryOptionsTable( VecToRecoveryDestination, WallXPos, StageData:GetWallMinHeight(), LedgeOccupied )
		if ( #RecoveryOptions > 0 ) then
			local RandomRecoveryMoveIndex = self:GetRandomInt( #RecoveryOptions - 1 ) + 1
			self:SetCpuAttackInputs( RecoveryOptions[RandomRecoveryMoveIndex], 0.0, true )
		end

		return
	end

	if ( self:IsPerformingAttack( ERivalsCharacterAttack.Uspecial ) ) then
		if ( self:GetWindow() < 2 ) then
			if ( ( MyLocation.Y < -790.0 or self:CpuShouldPerformAction( 0.5, 0.15 ) ) and self:GetFacingDirectionFloat() * MyLocation.X < 0.0 ) then
				JoystickPos = Vector2D.ZeroVector
			end
		elseif ( self:GetWindow() == 7 ) then
			if ( MyLocation.Y < -220.0 or LedgeOccupied or math.square( VecToRecoveryDestination.X ) > math.square( 180.0 ) or self:CpuShouldPerformAction( 0.5, 0.1 ) ) then
				self.InputHistory:SetActionDown( ERivalsBufferedInputAction.Special, true ) -- Use Uspecial 2
			end
		end
	elseif ( self:IsPerformingAttack( ERivalsCharacterAttack.Fspecial ) ) then
		-- Always hold toward center stage
		JoystickPos = Vector2D.new( -math.sign( MyLocation.X ), 0.0 )
		if ( VecToRecoveryDestination:SizeSquared() > math.square( 200.0 ) or LedgeOccupied ) then
			self:SpamCpuInput( ERivalsBufferedInputAction.Jump )
		end
	end

	self.InputHistory:SetJoystickPosition( JoystickPos )
end

Previous Article

Controlling Visibility

Next Article

Status Behaviors