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.5where 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