From 22477a5571236bd3cccccf4b4488efff8e90a870 Mon Sep 17 00:00:00 2001 From: Ferywir <65760459+Ferywir@users.noreply.github.com> Date: Tue, 25 Aug 2026 09:13:24 +0200 Subject: [PATCH] fix(combat): stop the dead hero being removed twice from the troop count [#379] (#380) --- .../Automation/AutomationBattleResolution.php | 40 +++++++++++++++---- GameEngine/Database/DatabaseTroopQueries.php | 29 +++++++++++++- 2 files changed, 60 insertions(+), 9 deletions(-) diff --git a/GameEngine/Automation/AutomationBattleResolution.php b/GameEngine/Automation/AutomationBattleResolution.php index c443908f..0e56f199 100644 --- a/GameEngine/Automation/AutomationBattleResolution.php +++ b/GameEngine/Automation/AutomationBattleResolution.php @@ -1231,9 +1231,24 @@ trait AutomationBattleResolution { if ($unitlist) { $owndead['hero'] = (isset($battlepart['deadherodef']) ? $battlepart['deadherodef'] : ''); - $unitModifications_units[] = 'hero'; - $unitModifications_amounts[] = $owndead['hero']; - $unitModifications_modes[] = 0; + /** + * BUG FIXED (issue #379): units.hero ended up at -1. + * + * The value is still reported (it feeds the battle report and the + * points), but it must NOT be subtracted from units.hero here any + * more. Battle::applyHeroBattleDamage() already clears the column + * itself ("UPDATE units u JOIN vdata v SET u.hero = 0 WHERE + * v.owner = ") the moment the hero dies, and that runs + * inside calculateBattle(), i.e. BEFORE this method. + * + * So the column went 1 -> 0 there and 0 - 1 -> -1 here: the + * village kept showing "-1 Hero" in the troop list while the + * Hero's Mansion correctly offered a revive. + * + * One writer only: the death is owned by applyHeroBattleDamage(), + * which is also the only place that knows about the hero's other + * possible locations (reinforcements, another village). + */ } // modify units in DB @@ -1330,10 +1345,21 @@ trait AutomationBattleResolution { } if ($enforce['hero'] > 0) { - $enforceModificationsById[$enforce['id']]['units'][] = 'hero'; - $enforceModificationsById[$enforce['id']]['amounts'][] = $battlepart['deadheroref'][$enforce['id']]; - $enforceModificationsById[$enforce['id']]['modes'][] = 0; - + /** + * BUG FIXED (issue #379), same double subtraction as in + * applyOwnDefenceCasualties(): a reinforcement hero who died + * was already removed by Battle::applyHeroBattleDamage() + * ("UPDATE enforcement e JOIN vdata v SET e.hero = 0"), so + * subtracting him again here drove enforcement.hero to -1. + * + * $enforce still reads 1 because getEnforceVillage() answers + * from the request cache filled before the battle, so the + * stale row hid the problem instead of preventing it. + * + * The dead count below is kept: it is what the reinforcement + * report and $wrong (which decides whether the wiped-out + * reinforcement row is deleted) are built from. + */ $dead['hero'] = $battlepart['deadheroref'][$enforce['id']]; $alldead['hero'] += $dead['hero']; $wrong = $dead['hero'] != $enforce['hero']; diff --git a/GameEngine/Database/DatabaseTroopQueries.php b/GameEngine/Database/DatabaseTroopQueries.php index 0eb6dfab..33792ba4 100644 --- a/GameEngine/Database/DatabaseTroopQueries.php +++ b/GameEngine/Database/DatabaseTroopQueries.php @@ -614,7 +614,25 @@ trait DatabaseTroopQueries { //Fixed part of negative troops (double troops) - by InCube $array_amt[$i] = (int) $array_amt[$i] < 0 ? 0 : $array_amt[$i]; //Fixed part of negative troops (double troops) - by InCube - $units .= $unit.' = '.$unit.' '.(($array_mode[$i] == 1)? '+':'-').' '.($array_amt[$i] ? $array_amt[$i] : 0).(($number > $i+1) ? ', ' : ''); + $amount = (int) ($array_amt[$i] ? $array_amt[$i] : 0); + + /** + * The guard above only clamps the AMOUNT, never the RESULT, so any + * caller subtracting more than the column holds wrote a NEGATIVE + * troop count (issue #379: "-1 Hero" in the village troop list, + * after the hero was removed once by Battle::applyHeroBattleDamage() + * and a second time by the casualty pipeline). + * + * A negative troop count is never a valid game state, so floor the + * subtraction at 0 in SQL. Additions are untouched. + */ + if ($array_mode[$i] == 1) { + $units .= $unit.' = '.$unit.' + '.$amount; + } else { + $units .= $unit.' = GREATEST('.$unit.' - '.$amount.', 0)'; + } + + $units .= (($number > $i+1) ? ', ' : ''); } $q = "UPDATE ".TB_PREFIX."units set $units WHERE vref = $vref"; return mysqli_query($this->dblink, $q); @@ -881,7 +899,14 @@ trait DatabaseTroopQueries { foreach ($unit as $index => $unitType) { $unitType = ($unitType != 'hero' ? 'u' . $this->escape($unitType) : $unitType); - $pairs[] = $unitType . ' = ' . $unitType . (!(int) $mode[$index] ? ' - ' : ' + ') . (int) $amt[$index]; + + // Same floor as modifyUnit(): a reinforcement column must never go + // negative either (issue #379). + if ((int) $mode[$index]) { + $pairs[] = $unitType . ' = ' . $unitType . ' + ' . (int) $amt[$index]; + } else { + $pairs[] = $unitType . ' = GREATEST(' . $unitType . ' - ' . (int) $amt[$index] . ', 0)'; + } } $q = "UPDATE " . TB_PREFIX . "enforcement SET ".implode(', ', $pairs)." WHERE id = $id";