{"id":1127,"date":"2026-06-10T02:56:05","date_gmt":"2026-06-10T02:56:05","guid":{"rendered":"https:\/\/rivals2.com\/workshop-dev\/?post_type=knowledgebase&#038;p=1127"},"modified":"2026-06-18T14:16:37","modified_gmt":"2026-06-18T14:16:37","slug":"set-up-character-ai-coming-soon","status":"publish","type":"knowledgebase","link":"https:\/\/rivals2.com\/workshop\/knowledge-base\/character-creation\/set-up-character-ai-coming-soon\/","title":{"rendered":"Set Up Character CPU Logic"},"content":{"rendered":"<section class='bpress-notice has-title has-icon is-style-warning'><div class=\"bpress-notice-icon\"><span class=\"bp-exclamation-circle\"><\/span><\/div><div class=\"bpress-notice-title\">Warning<\/div><div class=\"bpress-notice-content\">This article is a stub! We&#8217;ll have more info on this topic coming soon.<\/div><\/section>\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">CPU Recovery<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">In order for the CPU character to choose a recovery option while offstage, you will need to override the <code>GetCpuRecoveryOptions()<\/code> 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 <code>self:IsBasicRecoveryMoveValid()<\/code>, which takes the following parameters:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>VecToLedgeLocation <\/strong>&#8211; A vector from the character to the nearest ledge. This parameter is supplied directly in <code><code>GetCpuRecoveryOptions()<\/code><\/code>.<\/li>\n\n\n\n<li><strong>WallXPos <\/strong>&#8211; The x position of the nearest wall. This parameter is also supplied directly in <code>GetCpuRecoveryOptions()<\/code>.<\/li>\n\n\n\n<li><strong>WallMinHeight <\/strong>&#8211; The y position of the lowest point on the nearest wall. This parameter is also supplied directly in <code>GetCpuRecoveryOptions()<\/code>.<\/li>\n\n\n\n<li><strong>RecoveryMoveRangeIncludingLedgeGrabBox <\/strong>&#8211; 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&#8217;s location when their ledge grab box appears at its maximum range. Add the character&#8217;s maximum ledge grab offset to that value. If the move can be angled, you will want to call <code>self:IsBasicRecoveryMoveValid()<\/code> for both the non-angled and maximum angled version of this move.<\/li>\n\n\n\n<li><strong>AerialDriftSlopeValue<\/strong> &#8211; The ratio of the character&#8217;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 <code>local AerialDriftSlopeValue = 13.5 \/ 28.5<\/code> where the character&#8217;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.<\/li>\n\n\n\n<li><strong>MoveCanWalljump<\/strong> &#8211; Whether this move can walljump.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Here&#8217;s a complete example of how Clairen&#8217;s CPU checks for the validity of her recovery options:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>function Clairen:GetCpuRecoveryOptions(VecToLedgeLocation, WallXPos, WallMinHeight, LedgeOccupied)\n\tlocal RecoveryOptions = {}\n\t\n\tlocal AerialDriftSlopeValue = 13.5 \/ 28.35\n\n\t-- USPECIAL\n\tlocal UspecialLedgeGrabBoxCornerOffset = Vector2D.new( 150.0, 200.0 )\n\tlocal UspecialNormalDistance = Vector2D.new( 435.0, 641.0 ) + UspecialLedgeGrabBoxCornerOffset\n\tlocal UspecialAngledDistance = Vector2D.new( 605.0, 577.0 ) + UspecialLedgeGrabBoxCornerOffset\n\n\tif ( self:IsBasicRecoveryMoveValid( VecToLedgeLocation, WallXPos, WallMinHeight, UspecialNormalDistance, AerialDriftSlopeValue, false )\n\t\tor self:IsBasicRecoveryMoveValid( VecToLedgeLocation, WallXPos, WallMinHeight, UspecialAngledDistance, AerialDriftSlopeValue, false ) ) then\n\n\t\ttable.insert( RecoveryOptions, ERivalsCharacterAttack.Uspecial )\n\tend\n\n\t-- FSPECIAL\n\tlocal FspecialLedgeGrabBoxCornerOffset = Vector2D.new( 150.0, 170.0 )\n\tlocal FspecialDistance = Vector2D.new( 400.0, 0.0 ) + FspecialLedgeGrabBoxCornerOffset\n\n\tif ( self:IsBasicRecoveryMoveValid( VecToLedgeLocation, WallXPos, WallMinHeight, FspecialDistance, AerialDriftSlopeValue, true ) ) then\n\t\ttable.insert( RecoveryOptions, ERivalsCharacterAttack.Fspecial )\n\tend\n\n\tlocal MyLocation = self:GetLocation2D()\n\n\t-- Last ditch effort\n\tif ( #RecoveryOptions == 0 and MyLocation.Y &lt; -UspecialNormalDistance.Y ) then\n\t\ttable.insert( RecoveryOptions, ERivalsCharacterAttack.Uspecial )\n\tend\n\n\treturn RecoveryOptions\nend<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">For more complex recovery moves, you will have to determine if it&#8217;s valid on your own. For example, here&#8217;s how Zetterburn determines if Up Special is valid:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>-- USPECIAL\nlocal UspecialTravelDistance = 800.0\nlocal UspecialLedgeGrabBoxCornerOffset = Vector2D.new( 170.0 * math.sign( VecToLedgeLocation.X ), 230.0 )\nlocal UspecialLedgeGrabBoxCornerToLedge = VecToLedgeLocation - UspecialLedgeGrabBoxCornerOffset\nlocal UspecialLedgeGrabCornerLocation = self:GetLocation2D() + UspecialLedgeGrabBoxCornerOffset\nlocal UspecialXCutoff = UspecialTravelDistance - 40.0 + ( UspecialLedgeGrabCornerLocation.Y * AerialDriftSlopValue ) -- Subtract 40 to account for Zetterburn accelerating to his max drift\nlocal InUspecialZoneAboveLedge = UspecialLedgeGrabBoxCornerToLedge.Y &lt;= 0.0 &amp;&amp; math.square( UspecialLedgeGrabBoxCornerToLedge.X ) &lt; math.square( UspecialXCutoff )\n\nif UspecialLedgeGrabBoxCornerToLedge:SizeSquared() &gt; math.square( UspecialTravelDistance ) || InUspecalZoneAboveLedge then\n\ttable.insert( RecoveryOptions, ERivalsCharacterAttack.Uspecial )\nend<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">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 <code>UpdateCpuInputsRecovery()<\/code>. This function is run every frame while recovering, regardless of what state your character is in. The general flow of this function is:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">1. Release all currently held inputs:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>for _, InputAction in pairs(ERivalsBufferedInputAction) do\n\t\tself.InputHistory:SetActionDown( InputAction, false )\n\tend<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">2. Get the relevant data of your character&#8217;s position offstage, usually something like:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>local MyLocation = self:GetLocation2D()\n\tlocal MyState = self:GetState()\n\tlocal JoystickPos = self.InputHistory:GetJoystickPosition( 1 )\n\tlocal StageData = self:GetCurrentStageData()\n\tlocal WallXPos = StageData:GetWallXPosition()\n\tlocal RecoveryDestination = Vector.new( 0.0 )\n\tlocal LedgeOccupied = false\n\tlocal ClosestLedge = self:GetClosestLedge( RecoveryDestination )\n\tif ( ClosestLedge ) then\n\t\tLedgeOccupied = ClosestLedge:IsOccupied()\n\tend\n\nlocal VecToRecoveryDestination = Vector.new()\n\tVecToRecoveryDestination.X = RecoveryDestination.X - MyLocation.X\n\tVecToRecoveryDestination.Y = RecoveryDestination.Y - MyLocation.Y\n\tVecToRecoveryDestination.Z = RecoveryDestination.Z<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">3. Decide whether to start using a recovery move, and return from the function if so:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>if ( self:UpdateCpuInputsRecoveryBasic( JoystickPos, -800.0 ) ) then\n\t\tlocal RecoveryOptions = self:GetCpuRecoveryOptionsTable( VecToRecoveryDestination, WallXPos, StageData:GetWallMinHeight(), LedgeOccupied )\n\t\tif ( #RecoveryOptions &gt; 0 ) then\n\t\t\tlocal RandomRecoveryMoveIndex = self:GetRandomInt( #RecoveryOptions - 1 ) + 1\n\t\t\tself:SetCpuAttackInputs( RecoveryOptions&#091;RandomRecoveryMoveIndex], 0.0, true )\n\t\tend\n\n\t\treturn\n\tend<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">4. Check if you&#8217;re performing a specific attack and run logic if so. This example from Clairen.lua has logic for Up Special and Forward Special:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>if ( self:IsPerformingAttack( ERivalsCharacterAttack.Uspecial ) ) then\n\t\tif ( self:GetWindow() &lt; 2 ) then\n\t\t\tif ( ( MyLocation.Y &lt; -790.0 or self:CpuShouldPerformAction( 0.5, 0.15 ) ) and self:GetFacingDirectionFloat() * MyLocation.X &lt; 0.0 ) then\n\t\t\t\tJoystickPos = Vector2D.ZeroVector\n\t\t\tend\n\t\telseif ( self:GetWindow() == 7 ) then\n\t\t\tif ( MyLocation.Y &lt; -220.0 or LedgeOccupied or math.square( VecToRecoveryDestination.X ) &gt; math.square( 180.0 ) or self:CpuShouldPerformAction( 0.5, 0.1 ) ) then\n\t\t\t\tself.InputHistory:SetActionDown( ERivalsBufferedInputAction.Special, true ) -- Use Uspecial 2\n\t\t\tend\n\t\tend\n\telseif ( self:IsPerformingAttack( ERivalsCharacterAttack.Fspecial ) ) then\n\t\t-- Always hold toward center stage\n\t\tJoystickPos = Vector2D.new( -math.sign( MyLocation.X ), 0.0 )\n\t\tif ( VecToRecoveryDestination:SizeSquared() &gt; math.square( 200.0 ) or LedgeOccupied ) then\n\t\t\tself:SpamCpuInput( ERivalsBufferedInputAction.Jump )\n\t\tend\n\tend<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">5. Finally, set the joystick position based on the <code>JoystickPos<\/code> variable defined earlier in this function:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>self.InputHistory:SetJoystickPosition( JoystickPos )<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">For reference, here is the full <code>UpdateCpuInputsRecovery()<\/code> function from Clairen.lua:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>function Clairen:UpdateCpuInputsRecovery()\n\tlocal MyLocation = self:GetLocation2D()\n\tlocal MyState = self:GetState()\n\tlocal JoystickPos = self.InputHistory:GetJoystickPosition( 1 )\n\n\tfor _, InputAction in pairs(ERivalsBufferedInputAction) do\n\t\tself.InputHistory:SetActionDown( InputAction, false )\n\tend\n\n\tlocal StageData = self:GetCurrentStageData()\n\tlocal WallXPos = StageData:GetWallXPosition()\n\tlocal RecoveryDestination = Vector.new( 0.0 )\n\tlocal LedgeOccupied = false\n\tlocal ClosestLedge = self:GetClosestLedge( RecoveryDestination )\n\tif ( ClosestLedge ) then\n\t\tLedgeOccupied = ClosestLedge:IsOccupied()\n\tend\n\n\tlocal VecToRecoveryDestination = Vector.new()\n\tVecToRecoveryDestination.X = RecoveryDestination.X - MyLocation.X\n\tVecToRecoveryDestination.Y = RecoveryDestination.Y - MyLocation.Y\n\tVecToRecoveryDestination.Z = RecoveryDestination.Z\n\n\tif ( self:UpdateCpuInputsRecoveryBasic( JoystickPos, -800.0 ) ) then\n\t\tlocal RecoveryOptions = self:GetCpuRecoveryOptionsTable( VecToRecoveryDestination, WallXPos, StageData:GetWallMinHeight(), LedgeOccupied )\n\t\tif ( #RecoveryOptions &gt; 0 ) then\n\t\t\tlocal RandomRecoveryMoveIndex = self:GetRandomInt( #RecoveryOptions - 1 ) + 1\n\t\t\tself:SetCpuAttackInputs( RecoveryOptions&#091;RandomRecoveryMoveIndex], 0.0, true )\n\t\tend\n\n\t\treturn\n\tend\n\n\tif ( self:IsPerformingAttack( ERivalsCharacterAttack.Uspecial ) ) then\n\t\tif ( self:GetWindow() &lt; 2 ) then\n\t\t\tif ( ( MyLocation.Y &lt; -790.0 or self:CpuShouldPerformAction( 0.5, 0.15 ) ) and self:GetFacingDirectionFloat() * MyLocation.X &lt; 0.0 ) then\n\t\t\t\tJoystickPos = Vector2D.ZeroVector\n\t\t\tend\n\t\telseif ( self:GetWindow() == 7 ) then\n\t\t\tif ( MyLocation.Y &lt; -220.0 or LedgeOccupied or math.square( VecToRecoveryDestination.X ) &gt; math.square( 180.0 ) or self:CpuShouldPerformAction( 0.5, 0.1 ) ) then\n\t\t\t\tself.InputHistory:SetActionDown( ERivalsBufferedInputAction.Special, true ) -- Use Uspecial 2\n\t\t\tend\n\t\tend\n\telseif ( self:IsPerformingAttack( ERivalsCharacterAttack.Fspecial ) ) then\n\t\t-- Always hold toward center stage\n\t\tJoystickPos = Vector2D.new( -math.sign( MyLocation.X ), 0.0 )\n\t\tif ( VecToRecoveryDestination:SizeSquared() &gt; math.square( 200.0 ) or LedgeOccupied ) then\n\t\t\tself:SpamCpuInput( ERivalsBufferedInputAction.Jump )\n\t\tend\n\tend\n\n\tself.InputHistory:SetJoystickPosition( JoystickPos )\nend<\/code><\/pre>\n","protected":false},"excerpt":{"rendered":"<p>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, [&hellip;]<\/p>\n","protected":false},"author":9,"featured_media":0,"comment_status":"open","ping_status":"open","template":"","knowledgebase_cat":[4],"knowledgebase_tag":[],"class_list":["post-1127","knowledgebase","type-knowledgebase","status-publish","hentry","knowledgebase_cat-character-creation"],"_links":{"self":[{"href":"https:\/\/rivals2.com\/workshop\/wp-json\/wp\/v2\/knowledgebase\/1127","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/rivals2.com\/workshop\/wp-json\/wp\/v2\/knowledgebase"}],"about":[{"href":"https:\/\/rivals2.com\/workshop\/wp-json\/wp\/v2\/types\/knowledgebase"}],"author":[{"embeddable":true,"href":"https:\/\/rivals2.com\/workshop\/wp-json\/wp\/v2\/users\/9"}],"replies":[{"embeddable":true,"href":"https:\/\/rivals2.com\/workshop\/wp-json\/wp\/v2\/comments?post=1127"}],"version-history":[{"count":8,"href":"https:\/\/rivals2.com\/workshop\/wp-json\/wp\/v2\/knowledgebase\/1127\/revisions"}],"predecessor-version":[{"id":1492,"href":"https:\/\/rivals2.com\/workshop\/wp-json\/wp\/v2\/knowledgebase\/1127\/revisions\/1492"}],"wp:attachment":[{"href":"https:\/\/rivals2.com\/workshop\/wp-json\/wp\/v2\/media?parent=1127"}],"wp:term":[{"taxonomy":"knowledgebase_cat","embeddable":true,"href":"https:\/\/rivals2.com\/workshop\/wp-json\/wp\/v2\/knowledgebase_cat?post=1127"},{"taxonomy":"knowledgebase_tag","embeddable":true,"href":"https:\/\/rivals2.com\/workshop\/wp-json\/wp\/v2\/knowledgebase_tag?post=1127"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}