From 4af03075dfcfa5d1e4c44e18e3695ecd7c04b6a1 Mon Sep 17 00:00:00 2001 From: "Marco Antonio J. Costa" Date: Sun, 2 Aug 2026 19:15:13 -0300 Subject: [PATCH] Route PlotPath tile cost through the shared ActionPointCost PlotPath carried its own inline copy of the per-tile movement-cost math, which had drifted from ActionPointCost (the function the real per-step movement and the AI path estimate both already use). That divergence is what made the movement cursor mis-predict the real AP spend - e.g. the start-run penalty getting the diagonal x1.4 in the real cost but not the cursor. Replace the inline switch (and its fence/start-run special-cases) with a per-tile EstimateActionPointCost call, threading the simulated previous tile mode so the one-time start-run charge lands exactly once. The cursor estimate now equals what movement deducts, by construction. Behavioural: player movement-cursor AP numbers (and path reachability colouring that keys off the same total) now match the real spend. Real per-step deduction and AI estimates are unchanged - they already used ActionPointCost. The footstep-colour budget still has its own cost copy; left for a follow-up. Co-Authored-By: Claude Opus 4.8 --- Tactical/PATHAI.cpp | 224 +++----------------------------------------- 1 file changed, 14 insertions(+), 210 deletions(-) diff --git a/Tactical/PATHAI.cpp b/Tactical/PATHAI.cpp index c0de52c68..894b3fe41 100644 --- a/Tactical/PATHAI.cpp +++ b/Tactical/PATHAI.cpp @@ -4138,7 +4138,7 @@ INT32 PlotPath( SOLDIERTYPE *pSold, INT32 sDestGridNo, INT8 bCopyRoute, INT8 bPl { INT16 sTileCost,sPoints=0,sAnimCost=0; INT16 sPointsWalk=0,sPointsCrawl=0,sPointsRun=0,sPointsSwat=0; - INT16 sExtraCostStand,sExtraCostSwat,sExtraCostCrawl; + INT16 sExtraCostStand; FLOAT sMovementAPsCost = 0; // added by SANDRO INT32 iLastGrid, sTempGrid; INT32 iCnt; @@ -4151,7 +4151,6 @@ INT32 PlotPath( SOLDIERTYPE *pSold, INT32 sDestGridNo, INT8 bCopyRoute, INT8 bPl LEVELNODE *pNode; UINT16 usMovementModeToUseForAPs; BOOLEAN bIgnoreNextCost = FALSE; - INT32 sTestGridNo; if ( bPlot && gusPathShown ) { @@ -4191,23 +4190,12 @@ INT32 PlotPath( SOLDIERTYPE *pSold, INT32 sDestGridNo, INT8 bCopyRoute, INT8 bPl // Add to points, those needed to start from different stance! sPoints = sPoints + MinAPsToStartMovement( pSold, usMovementMode ); - // We should reduce points for starting to run if first tile is a fence... - sTestGridNo = NewGridNo(pSold->sGridNo, DirectionInc( (UINT8)guiPathingData[0])); - // WANNE: Quickfix for wrong pathing data (direction). This fixes crash that could rarly occur if ((UINT8)guiPathingData[0] > 7) { guiPathingData[0] = 0; } - if ( gubWorldMovementCosts[ sTestGridNo ][ guiPathingData[0] ][ pSold->pathing.bLevel] == TRAVELCOST_FENCE ) - { - if ( usMovementMode == RUNNING && pSold->usAnimState != RUNNING ) - { - sPoints -= GetAPsStartRun( pSold ); // changed by SANDRO - } - } - if (bStayOn) { iLastGrid = giPathDataSize+1; @@ -4256,8 +4244,6 @@ INT32 PlotPath( SOLDIERTYPE *pSold, INT32 sDestGridNo, INT8 bCopyRoute, INT8 bPl for ( iCnt=0; iCnt < iLastGrid; iCnt++ ) { sExtraCostStand = 0; - sExtraCostSwat = 0; - sExtraCostCrawl = 0; // what is the next gridno in the path? sOldGrid = sTempGrid; @@ -4281,14 +4267,12 @@ INT32 PlotPath( SOLDIERTYPE *pSold, INT32 sDestGridNo, INT8 bCopyRoute, INT8 bPl usMovementModeToUseForAPs = WALKING; } - //shadooow: moved inside loop because it can happen we (re)start running after we walked in water - if (sSwitchValue != TRAVELCOST_FENCE && usMovementModeToUseForAPs == RUNNING && usMovementModeBefore != RUNNING) - { - sPoints += GetAPsStartRun(pSold); - } + // The previous tile's mode drives the one-time start-run charge; capture it before + // advancing, so this tile's cost sees the prior mode and the next tile sees this one. + const UINT16 usPrevMovementMode = (UINT16)usMovementModeBefore; usMovementModeBefore = usMovementModeToUseForAPs; - - // get the tile cost for that tile based on WALKING + + // get the tile cost for that tile based on WALKING (the footstep-colour budget below still needs it) sTileCost = TerrainActionPoints( pSold, sTempGrid, (INT8)guiPathingData[iCnt], pSold->pathing.bLevel ); if ( bIgnoreNextCost ) @@ -4297,196 +4281,16 @@ INT32 PlotPath( SOLDIERTYPE *pSold, INT32 sDestGridNo, INT8 bCopyRoute, INT8 bPl } else { - // ATE: If we have a 'special cost, like jump fence... + // Single source of truth: charge exactly what the real per-step movement code + // deducts (ActionPointCost), routed through EstimateActionPointCost so the + // fence-continuation term is included, summed tile by tile. This is what keeps + // the cursor estimate and the real spend from ever drifting apart. + sPoints += EstimateActionPointCost( pSold, sTempGrid, (INT8)guiPathingData[iCnt], usMovementModeToUseForAPs, (INT8)iCnt, (INT8)iLastGrid, usPrevMovementMode ); + + // A fence hop covers its landing tile too - skip that tile's separate cost, + // mirroring the two-tile path advance in HandleGotoNewGridNo. if ( sSwitchValue == TRAVELCOST_FENCE ) - { - sPoints = sPoints + sTileCost; - bIgnoreNextCost = TRUE; - - // If we are changing stance ( either before or after getting there.... - // We need to reflect that... - switch( usMovementModeToUseForAPs ) - { - case RUNNING: - case WALKING : - case WALKING_WEAPON_RDY: - case WALKING_DUAL_RDY: - case WALKING_ALTERNATIVE_RDY : - - // silversurfer: It doesn't matter if we continue moving after the jump. If we were standing before we will stand up again. -/* // Add here cost to go from crouch to stand AFTER fence hop.... - // Since it's AFTER.. make sure we will be moving after jump... - if ( ( iCnt + 2 ) < iLastGrid ) - { - sExtraCostStand += GetAPsCrouch(pSold, TRUE); - - // ATE: if running, charge extra point to start again - if ( usMovementModeToUseForAPs == RUNNING ) - { - sExtraCostStand += GetAPsStartRun(pSold); - } - - sPoints = sPoints + sExtraCostStand; - }*/ - // Add cost to stand up after jump - sExtraCostStand += GetAPsCrouch(pSold, TRUE); - if ( ( iCnt + 2 ) < iLastGrid && usMovementModeToUseForAPs == RUNNING ) - { - sExtraCostStand += GetAPsStartRun(pSold); - } - sPoints = sPoints + sExtraCostStand; - break; - - case SWATTING: - case CROUCHEDMOVE_RIFLE_READY: - case CROUCHEDMOVE_PISTOL_READY: - case CROUCHEDMOVE_DUAL_READY: - - // Add cost to stand up once BEFORE.... - sExtraCostSwat += GetAPsCrouch(pSold, TRUE); - sPoints = sPoints + sExtraCostSwat; - break; - - case CRAWLING: - - // Add cost to stand up before and go prone again after jumping - sExtraCostCrawl += GetAPsCrouch(pSold, TRUE) + ( 2 * GetAPsProne(pSold, TRUE) ); - sPoints = sPoints + sExtraCostCrawl; - - // Can't do it here..... - break; - - } - } - else if (sTileCost > 0) - { - // else, movement is adjusted based on mode... - - if (sSwitchValue == TRAVELCOST_NOT_STANDING) - { - switch( usMovementModeToUseForAPs ) - { - case RUNNING: - case WALKING : - case WALKING_WEAPON_RDY: - case WALKING_DUAL_RDY: - case WALKING_ALTERNATIVE_RDY : - // charge crouch APs for ducking head! - sExtraCostStand += GetAPsCrouch(pSold, TRUE); - break; - - default: - break; - } - } - - // so, then we must modify it for other movement styles and accumulate - // CHRISL: Force display path to calculate AP cost differently if we're wearing a backpack - /////////////////////////////////////////////////////////////////////////////////////////////////////////// - // SANDRO - This part have been modified "a bit" (see also "TerrainActionPoints" in "points.cpp") - // Check movement modifiers - switch( usMovementModeToUseForAPs ) - { - case RUNNING: - sMovementAPsCost = sTileCost + APBPConstants[AP_MODIFIER_RUN]; - break; - case WALKING: - if ( 0 && !(pSold->MercInWater()) && ( (gAnimControl[ pSold->usAnimState ].uiFlags & ANIM_FIREREADY ) || (gAnimControl[ pSold->usAnimState ].uiFlags & ANIM_FIRE ) )) - { - sMovementAPsCost = sTileCost + APBPConstants[AP_MODIFIER_WALK] + APBPConstants[AP_MODIFIER_READY]; - } - else - { - sMovementAPsCost = sTileCost + APBPConstants[AP_MODIFIER_WALK]; - } - break; - case WALKING_ALTERNATIVE_RDY : - sMovementAPsCost = sTileCost + APBPConstants[AP_MODIFIER_WALK]; - break; - case WALKING_WEAPON_RDY: - case WALKING_DUAL_RDY: - sMovementAPsCost = sTileCost + APBPConstants[AP_MODIFIER_WALK] + APBPConstants[AP_MODIFIER_READY]; - break; - case SWATTING: - case SIDE_STEP_CROUCH_RIFLE: - case SIDE_STEP_CROUCH_PISTOL: - case SIDE_STEP_CROUCH_DUAL: - case CROUCHEDMOVE_RIFLE_READY: - case CROUCHEDMOVE_PISTOL_READY: - case CROUCHEDMOVE_DUAL_READY: - sMovementAPsCost = sTileCost + APBPConstants[AP_MODIFIER_SWAT]; - break; - case CRAWLING: - sMovementAPsCost = sTileCost + APBPConstants[AP_MODIFIER_CRAWL]; - break; - default: - sMovementAPsCost = sTileCost; - break; - } - - // Check for reverse mode - if ( pSold->bReverse || bReverse ) - sMovementAPsCost += APBPConstants[AP_REVERSE_MODIFIER]; - - // STOMP traits - Athletics trait decreases movement cost - if ( gGameOptions.fNewTraitSystem && HAS_SKILL_TRAIT( pSold, ATHLETICS_NT )) - { - sMovementAPsCost = max(1, (sMovementAPsCost * (100 - gSkillTraitValues.ubATAPsMovementReduction) / 100.0f) ); - } - - // Moa: scuba fins and swimming background - if ( pSold->inv[LEGPOS].exists() && HasItemFlag( pSold->inv[LEGPOS].usItem, SCUBA_FINS ) ) - { - if ( TERRAIN_IS_HIGH_WATER( ubTerrainID) ) - sMovementAPsCost /= 2; - else - sMovementAPsCost *= 2; - } - if ( TERRAIN_IS_HIGH_WATER( ubTerrainID) ) - sMovementAPsCost = sMovementAPsCost * (100 + pSold->GetBackgroundValue(BG_SWIMMING)) / 100.0f; - - // Check if doors if not player's merc (they have to open them manually) - if ( sSwitchValue == TRAVELCOST_DOOR && pSold->bTeam != gbPlayerNum ) - { - sMovementAPsCost += GetAPsToOpenDoor( pSold ) + GetAPsToOpenDoor( pSold ); // Include open and close costs! - } - // Check for stealth mode - if (pSold->bStealthMode || bStealth) - { - // STOMP traits - Stealthy trait decreases stealth AP modifier - if (gGameOptions.fNewTraitSystem && HAS_SKILL_TRAIT(pSold, STEALTHY_NT)) - { - sMovementAPsCost += max(0.0f, (APBPConstants[AP_STEALTH_MODIFIER] * (100.0f - gSkillTraitValues.ubSTStealthModeSpeedBonus) / 100.0f)); - } - else - { - sMovementAPsCost += APBPConstants[AP_STEALTH_MODIFIER]; - } - } - - // Flugente: riot shields lower movement speed - if ( pSold->IsRiotShieldEquipped( ) ) - { - sMovementAPsCost *= gItemSettings.fShieldMovementAPCostModifier; - } - - // Flugente: dragging someone - if ( pSold->IsDragging( ) ) - { - sMovementAPsCost *= gItemSettings.fDragAPCostModifier; - } - - // Check for backpack - sMovementAPsCost += usBackpackPenalty; - - // moving diagonally - if (guiPathingData[iCnt] & 1) - sMovementAPsCost *= 1.4f; - - sPoints += (INT16)(sMovementAPsCost + 0.5f) + sExtraCostStand; - /////////////////////////////////////////////////////////////////////////////////////////////////////////// - } } // THIS NEXT SECTION ONLY NEEDS TO HAPPEN FOR CURSOR UI FEEDBACK, NOT ACTUAL COSTING