From 06205c3028a77bdad3dccc278e58a84b58fd13c8 Mon Sep 17 00:00:00 2001 From: "Marco Antonio J. Costa" Date: Sun, 2 Aug 2026 19:01:54 -0300 Subject: [PATCH 1/5] Thread previous-tile movement mode into ActionPointCost Add an ActionPointCost overload taking the previous tile's movement mode explicitly, used to charge the one-time start-run penalty. The existing 4-arg overload forwards the soldier's live anim state, so real per-step movement and every other caller are unchanged. This lets a path-cost estimator - which does not move the soldier - supply its simulated prior mode instead of reading a frozen live anim state, so the estimate and the real deduction can share one cost function. Co-Authored-By: Claude Opus 4.8 --- Tactical/Points.cpp | 9 ++++++++- Tactical/Points.h | 7 +++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Tactical/Points.cpp b/Tactical/Points.cpp index 4c0ac1fcee..b6a8c1153c 100644 --- a/Tactical/Points.cpp +++ b/Tactical/Points.cpp @@ -391,6 +391,13 @@ INT16 TerrainBreathPoints(SOLDIERTYPE * pSoldier, INT32 sGridNo, INT8 bDir, UINT INT16 ActionPointCost( SOLDIERTYPE *pSoldier, INT32 sGridNo, INT8 bDir, UINT16 usMovementMode ) +{ + // Real, per-step movement: the soldier is physically at his current stance, so his + // live anim state IS the previous tile's mode. + return ActionPointCost( pSoldier, sGridNo, bDir, usMovementMode, pSoldier->usAnimState ); +} + +INT16 ActionPointCost( SOLDIERTYPE *pSoldier, INT32 sGridNo, INT8 bDir, UINT16 usMovementMode, UINT16 usPrevMovementMode ) { INT16 sTileCost, sSwitchValue; FLOAT sPoints = 0; @@ -535,7 +542,7 @@ INT16 ActionPointCost( SOLDIERTYPE *pSoldier, INT32 sGridNo, INT8 bDir, UINT16 u sPoints = max(1.0f, ( sPoints * (100 - (FLOAT)gSkillTraitValues.ubATAPsMovementReduction) / 100.0f ) ); } - if (usMovementMode == RUNNING && pSoldier->usAnimState != RUNNING) + if (usMovementMode == RUNNING && usPrevMovementMode != RUNNING) { // CHRISL if ((UsingNewInventorySystem() == true) && FindBackpackOnSoldier(pSoldier) != ITEM_NOT_FOUND) diff --git a/Tactical/Points.h b/Tactical/Points.h index 9bf9dec9f6..ce38335223 100644 --- a/Tactical/Points.h +++ b/Tactical/Points.h @@ -282,6 +282,13 @@ INT16 BaseAPsToShootOrStabNoModifier( INT16 bAPs, INT16 bAimSkill, OBJECTTYPE * INT16 BaseAPsToShootOrStabNoModifier( INT16 bAPs, INT16 bAimSkill, OBJECTTYPE * pObj ); INT16 TerrainActionPoints( SOLDIERTYPE *pSoldier, INT32 sGridno, INT8 bDir, INT8 bLevel ); +// Per-tile movement AP cost - the single source of truth for what a step costs. +// usPrevMovementMode is the mode the soldier was in on the PREVIOUS tile, used to +// charge the one-time "spin up to run" penalty exactly once. Real movement passes +// the soldier's live anim state; a path estimator must pass its simulated prior mode, +// because the soldier doesn't actually move while the path is being summed. +INT16 ActionPointCost( SOLDIERTYPE *pSoldier, INT32 sGridNo, INT8 bDir, UINT16 usMovementMode, UINT16 usPrevMovementMode ); +// Convenience overload: prev mode = the soldier's current anim state (correct for real, per-step movement). INT16 ActionPointCost( SOLDIERTYPE *pSoldier, INT32 sGridNo, INT8 bDir, UINT16 usMovementMode ); INT16 EstimateActionPointCost( SOLDIERTYPE *pSoldier, INT32 sGridNo, INT8 bDir, UINT16 usMovementMode, INT8 bPathIndex, INT8 bPathLength ); BOOLEAN SelectedMercCanAffordMove( ); From 86173feda18f8f1abb85ef653ebd4cf0e3001c1e Mon Sep 17 00:00:00 2001 From: "Marco Antonio J. Costa" Date: Sun, 2 Aug 2026 19:09:07 -0300 Subject: [PATCH 2/5] Add prev-mode overload of EstimateActionPointCost Forwards to the new ActionPointCost prev-mode overload so a path estimator can pass its simulated prior tile mode. Existing 6-arg callers use the soldier's live anim state and are unchanged. Co-Authored-By: Claude Opus 4.8 --- Tactical/Points.cpp | 7 ++++++- Tactical/Points.h | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Tactical/Points.cpp b/Tactical/Points.cpp index b6a8c1153c..8f0fc79b51 100644 --- a/Tactical/Points.cpp +++ b/Tactical/Points.cpp @@ -653,6 +653,11 @@ INT16 ActionPointCost( SOLDIERTYPE *pSoldier, INT32 sGridNo, INT8 bDir, UINT16 u } INT16 EstimateActionPointCost( SOLDIERTYPE *pSoldier, INT32 sGridNo, INT8 bDir, UINT16 usMovementMode, INT8 bPathIndex, INT8 bPathLength ) +{ + return EstimateActionPointCost( pSoldier, sGridNo, bDir, usMovementMode, bPathIndex, bPathLength, pSoldier->usAnimState ); +} + +INT16 EstimateActionPointCost( SOLDIERTYPE *pSoldier, INT32 sGridNo, INT8 bDir, UINT16 usMovementMode, INT8 bPathIndex, INT8 bPathLength, UINT16 usPrevMovementMode ) { // This action point cost code includes the penalty for having to change // stance after jumping a fence IF our path continues... @@ -723,7 +728,7 @@ INT16 EstimateActionPointCost( SOLDIERTYPE *pSoldier, INT32 sGridNo, INT8 bDir, } } - sPoints += ActionPointCost( pSoldier, sGridNo, bDir, usMovementMode ); + sPoints += ActionPointCost( pSoldier, sGridNo, bDir, usMovementMode, usPrevMovementMode ); return (sPoints); } diff --git a/Tactical/Points.h b/Tactical/Points.h index ce38335223..ad2fda698e 100644 --- a/Tactical/Points.h +++ b/Tactical/Points.h @@ -290,6 +290,10 @@ INT16 TerrainActionPoints( SOLDIERTYPE *pSoldier, INT32 sGridno, INT8 bDir, INT8 INT16 ActionPointCost( SOLDIERTYPE *pSoldier, INT32 sGridNo, INT8 bDir, UINT16 usMovementMode, UINT16 usPrevMovementMode ); // Convenience overload: prev mode = the soldier's current anim state (correct for real, per-step movement). INT16 ActionPointCost( SOLDIERTYPE *pSoldier, INT32 sGridNo, INT8 bDir, UINT16 usMovementMode ); +// bPathIndex/bPathLength give the fence-continuation context; usPrevMovementMode is the +// previous tile's mode (see ActionPointCost). Estimators summing a path pass their simulated +// prior mode; the shorter overload uses the soldier's live anim state. +INT16 EstimateActionPointCost( SOLDIERTYPE *pSoldier, INT32 sGridNo, INT8 bDir, UINT16 usMovementMode, INT8 bPathIndex, INT8 bPathLength, UINT16 usPrevMovementMode ); INT16 EstimateActionPointCost( SOLDIERTYPE *pSoldier, INT32 sGridNo, INT8 bDir, UINT16 usMovementMode, INT8 bPathIndex, INT8 bPathLength ); BOOLEAN SelectedMercCanAffordMove( ); From 6268f72a8734d2533de892a22a77c0d2ab80bbe5 Mon Sep 17 00:00:00 2001 From: "Marco Antonio J. Costa" Date: Sun, 2 Aug 2026 19:15:13 -0300 Subject: [PATCH 3/5] 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 c0de52c68e..894b3fe413 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 From a9371129b902a7311310f2bf674eeee6e5f4f1d4 Mon Sep 17 00:00:00 2001 From: "Marco Antonio J. Costa" Date: Sun, 2 Aug 2026 21:02:27 -0300 Subject: [PATCH 4/5] Route PlotPath footstep-colour budget through the shared cost The footstep-colour budget kept its own inline per-mode copy of the movement-cost modifiers (walk/crawl/swat/run), the last duplicate of the cost math left in PlotPath. Replace it with one EstimateActionPointCost call per stance, so the reachability colours use the same per-tile cost as the real spend, and drop the now-orphaned per-tile TerrainActionPoints recompute and its dead locals (sTileCost, sMovementAPsCost, sExtraCostStand). Cosmetic-only: footprint colours now include the diagonal x1.4 the old budget omitted, so they track the real reachable distance more closely. The water->walk terrain override stays; it keeps the start-run charge correct when a run path crosses water. Co-Authored-By: Claude Opus 4.8 --- Tactical/PATHAI.cpp | 129 ++++---------------------------------------- 1 file changed, 9 insertions(+), 120 deletions(-) diff --git a/Tactical/PATHAI.cpp b/Tactical/PATHAI.cpp index 894b3fe413..0c107eeb8d 100644 --- a/Tactical/PATHAI.cpp +++ b/Tactical/PATHAI.cpp @@ -4136,10 +4136,8 @@ void ErasePath(char bEraseOldOne) INT32 PlotPath( SOLDIERTYPE *pSold, INT32 sDestGridNo, INT8 bCopyRoute, INT8 bPlot, INT8 bStayOn, UINT16 usMovementMode, INT8 bStealth, INT8 bReverse , INT16 sAPBudget) { - INT16 sTileCost,sPoints=0,sAnimCost=0; + INT16 sPoints=0,sAnimCost=0; INT16 sPointsWalk=0,sPointsCrawl=0,sPointsRun=0,sPointsSwat=0; - INT16 sExtraCostStand; - FLOAT sMovementAPsCost = 0; // added by SANDRO INT32 iLastGrid, sTempGrid; INT32 iCnt; INT32 sOldGrid=0; @@ -4243,7 +4241,6 @@ INT32 PlotPath( SOLDIERTYPE *pSold, INT32 sDestGridNo, INT8 bCopyRoute, INT8 bPl for ( iCnt=0; iCnt < iLastGrid; iCnt++ ) { - sExtraCostStand = 0; // what is the next gridno in the path? sOldGrid = sTempGrid; @@ -4272,9 +4269,6 @@ INT32 PlotPath( SOLDIERTYPE *pSold, INT32 sDestGridNo, INT8 bCopyRoute, INT8 bPl const UINT16 usPrevMovementMode = (UINT16)usMovementModeBefore; usMovementModeBefore = usMovementModeToUseForAPs; - // 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 ) { bIgnoreNextCost = FALSE; @@ -4297,119 +4291,14 @@ INT32 PlotPath( SOLDIERTYPE *pSold, INT32 sDestGridNo, INT8 bCopyRoute, INT8 bPl if (bPlot && ( (gTacticalStatus.uiFlags & TURNBASED) && (gTacticalStatus.uiFlags & INCOMBAT) ) ) // OR USER OPTION ON... ***) { - // ATE; TODO: Put stuff in here to allow for fact of costs other than movement ( jump fence, open door ) - - // CHRISL: Adjusted system to use different move costs while wearing a backpack - //////////////////////////////////////////////////////////////////////////////////////////////////////////// - // SANDRO - This part was modified "a bit" - sMovementAPsCost = sTileCost; - - // 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 )) - { - sPointsWalk += max(1, (INT16)((sMovementAPsCost + APBPConstants[AP_MODIFIER_WALK]) * (100 - gSkillTraitValues.ubATAPsMovementReduction) / 100.0f + 0.5f)); - sPointsCrawl += max(1, (INT16)((sMovementAPsCost + APBPConstants[AP_MODIFIER_CRAWL]) * (100 - gSkillTraitValues.ubATAPsMovementReduction) / 100.0f + 0.5f)); - sPointsSwat += max(1, (INT16)((sMovementAPsCost + APBPConstants[AP_MODIFIER_SWAT]) * (100 - gSkillTraitValues.ubATAPsMovementReduction) / 100.0f + 0.5f)); - sPointsRun += max(1, (INT16)((sMovementAPsCost + APBPConstants[AP_MODIFIER_RUN]) * (100 - gSkillTraitValues.ubATAPsMovementReduction) / 100.0f + 0.5f)); - } - // Specify movement modes - else - { - sPointsWalk += sMovementAPsCost + APBPConstants[AP_MODIFIER_WALK]; - sPointsCrawl += sMovementAPsCost + APBPConstants[AP_MODIFIER_CRAWL]; - sPointsSwat += sMovementAPsCost + APBPConstants[AP_MODIFIER_SWAT]; - sPointsRun += sMovementAPsCost + APBPConstants[AP_MODIFIER_RUN]; - } - - // Moa: scuba fins and swimming background - if ( pSold->inv[LEGPOS].exists() && HasItemFlag( pSold->inv[LEGPOS].usItem, SCUBA_FINS ) ) - { - if ( TERRAIN_IS_HIGH_WATER( ubTerrainID) ) - { - sPointsWalk /= 2; - sPointsCrawl /= 2; - sPointsSwat /= 2; - sPointsRun /= 2; - } - else - { - sPointsWalk *= 2; - sPointsCrawl *= 2; - sPointsSwat *= 2; - sPointsRun *= 2; - } - } - if ( TERRAIN_IS_HIGH_WATER( ubTerrainID) ) - { - sPointsWalk = (sPointsWalk * (100 + pSold->GetBackgroundValue(BG_SWIMMING))) / 100; - sPointsCrawl = (sPointsCrawl * (100 + pSold->GetBackgroundValue(BG_SWIMMING))) / 100; - sPointsSwat = (sPointsSwat * (100 + pSold->GetBackgroundValue(BG_SWIMMING))) / 100; - sPointsRun = (sPointsRun * (100 + pSold->GetBackgroundValue(BG_SWIMMING))) / 100; - } - // walking with weapon raised? - if (!(pSold->MercInWater()) && ( (gAnimControl[ pSold->usAnimState ].uiFlags & ANIM_FIREREADY ) || (gAnimControl[ pSold->usAnimState ].uiFlags & ANIM_FIRE ) )) - { - sPointsWalk += APBPConstants[AP_MODIFIER_READY]; - } - // 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 )) - { - sPointsWalk += (max(0, (INT16)(APBPConstants[AP_STEALTH_MODIFIER] * (100 - gSkillTraitValues.ubSTStealthModeSpeedBonus) / 100.0f + 0.5f))); - sPointsCrawl += (max(0, (INT16)(APBPConstants[AP_STEALTH_MODIFIER] * (100 - gSkillTraitValues.ubSTStealthModeSpeedBonus) / 100.0f + 0.5f))); - sPointsSwat += (max(0, (INT16)(APBPConstants[AP_STEALTH_MODIFIER] * (100 - gSkillTraitValues.ubSTStealthModeSpeedBonus) / 100.0f + 0.5f))); - sPointsRun += (max(0, (INT16)(APBPConstants[AP_STEALTH_MODIFIER] * (100 - gSkillTraitValues.ubSTStealthModeSpeedBonus) / 100.0f + 0.5f))); - } - else - { - sPointsWalk += APBPConstants[AP_STEALTH_MODIFIER]; - sPointsCrawl += APBPConstants[AP_STEALTH_MODIFIER]; - sPointsSwat += APBPConstants[AP_STEALTH_MODIFIER]; - sPointsRun += APBPConstants[AP_STEALTH_MODIFIER]; - } - } - - // Flugente: riot shields lower movement speed - if ( pSold->IsRiotShieldEquipped( ) ) - { - sPointsWalk *= gItemSettings.fShieldMovementAPCostModifier; - sPointsCrawl *= gItemSettings.fShieldMovementAPCostModifier; - sPointsSwat *= gItemSettings.fShieldMovementAPCostModifier; - sPointsRun *= gItemSettings.fShieldMovementAPCostModifier; - } - - // Flugente: dragging someone - if ( pSold->IsDragging() ) - { - sPointsWalk *= gItemSettings.fDragAPCostModifier; - sPointsCrawl *= gItemSettings.fDragAPCostModifier; - sPointsSwat *= gItemSettings.fDragAPCostModifier; - sPointsRun *= gItemSettings.fDragAPCostModifier; - } - - // Check for backpack - // Moa: apply penalty for heavily packed backpack (wobble penalty) - sPointsWalk += usBackpackPenalty; - sPointsCrawl += usBackpackPenalty; - sPointsSwat += usBackpackPenalty; - sPointsRun += usBackpackPenalty; - - if ( sExtraCostStand ) - { - sPointsWalk += sExtraCostStand; - sPointsCrawl += sExtraCostStand; - sPointsSwat += sExtraCostStand; - sPointsRun += sExtraCostStand; - } - //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Footstep-colour budget: how far the path reaches in each stance, using the same + // per-tile cost source as everything else so the colours track the real spend. + // The one-time run start-up is seeded once in sPointsRun before the loop; passing + // each mode as its own previous mode keeps the per-tile start-run from repeating. + sPointsWalk += EstimateActionPointCost( pSold, sTempGrid, (INT8)guiPathingData[iCnt], WALKING, (INT8)iCnt, (INT8)iLastGrid, WALKING ); + sPointsCrawl += EstimateActionPointCost( pSold, sTempGrid, (INT8)guiPathingData[iCnt], CRAWLING, (INT8)iCnt, (INT8)iLastGrid, CRAWLING ); + sPointsSwat += EstimateActionPointCost( pSold, sTempGrid, (INT8)guiPathingData[iCnt], SWATTING, (INT8)iCnt, (INT8)iLastGrid, SWATTING ); + sPointsRun += EstimateActionPointCost( pSold, sTempGrid, (INT8)guiPathingData[iCnt], RUNNING, (INT8)iCnt, (INT8)iLastGrid, RUNNING ); } if ( iCnt == 0 && bPlot ) From 43b8da42a619fa2ae111c4ce3512a7dc9e97de99 Mon Sep 17 00:00:00 2001 From: "Marco Antonio J. Costa" Date: Mon, 3 Aug 2026 23:29:26 -0300 Subject: [PATCH 5/5] Fix cursor AP short on fence-then-diagonal run EstimateActionPointCost adds the post-fence start-run flat at the fence tile; real spend adds it on the next tile, with the diagonal x1.4 - cursor read 1 AP low. Sum ActionPointCost direct instead, and mark the fence landing non-running so the next tile re-charges start-run itself: right tile, right multiplier. Co-Authored-By: Claude Opus 4.8 --- Tactical/PATHAI.cpp | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/Tactical/PATHAI.cpp b/Tactical/PATHAI.cpp index 0c107eeb8d..eee559a1fd 100644 --- a/Tactical/PATHAI.cpp +++ b/Tactical/PATHAI.cpp @@ -4272,14 +4272,27 @@ INT32 PlotPath( SOLDIERTYPE *pSold, INT32 sDestGridNo, INT8 bCopyRoute, INT8 bPl if ( bIgnoreNextCost ) { bIgnoreNextCost = FALSE; + + // This is the fence's landing tile. The real jump ends in a stationary stance + // (SoldierGotoStationaryStance in HandleGotoNewGridNo), so the merc is no longer + // running here. Record that as the previous mode, so the NEXT tile re-charges the + // one-time start-run through ActionPointCost - on that tile, with that tile's own + // diagonal x1.4, exactly as the real per-step deduction does. WALKING is just a + // non-RUNNING marker; the only thing read from prev mode is "was I running?". + // ponytail: the fence post-state is written by hand here. Upgrade path: a typed + // "movement step" (fence/climb/door) carrying its own cost + post-state, folded by + // one path-summer shared with the real deduction and TacticalAI/Movement.cpp (which + // still sums with a frozen prev mode and has the same drift). + usMovementModeBefore = WALKING; } else { - // 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 ); + // Single source of truth: charge exactly what the real per-step movement deducts, + // by calling the very same ActionPointCost and threading the simulated previous-tile + // mode. No fence-continuation shortcut here - the start-run after a fence is produced + // by the reset above landing on the next tile, so it lands on the right tile with the + // right diagonal x1.4 instead of the flat approximation EstimateActionPointCost adds. + sPoints += ActionPointCost( pSold, sTempGrid, (INT8)guiPathingData[iCnt], usMovementModeToUseForAPs, usPrevMovementMode ); // A fence hop covers its landing tile too - skip that tile's separate cost, // mirroring the two-tile path advance in HandleGotoNewGridNo.