From f849260bfadebed4b92283dd00ef119a52f1ce21 Mon Sep 17 00:00:00 2001 From: Ferywir <65760459+Ferywir@users.noreply.github.com> Date: Mon, 29 Jun 2026 11:18:38 +0200 Subject: [PATCH] fix(combat): stop attacks looping on a village razed earlier in the same tick [#298] (#309) When several catapult waves land in the same resolution tick, sendunitsComplete() fetches them all into one in-memory batch. The first wave can raze the target village: handleVillageDestruction() -> DelVillage() already bounces every still-in-flight attack straight home (symmetric return time) and deletes the vdata row, leaving the wdata tile as an empty valley. The loop then kept resolving the remaining waves from the in-memory batch against the now-deleted village. getMInfo() LEFT-JOINs wdata onto the missing vdata, so every vdata column (including wref) comes back NULL. The return trip in finalizeReturnOrDeath() is then computed from a NULL wref (NULL coordinates), producing a bogus arrival time that strands the troops, and a duplicate of the bounce DelVillage() had already issued. The attacker sees the waves "coming to nowhere and never coming back" (issue #298). Track the tiles razed during the current batch and skip any follow-up wave that targets one of them: those troops were already bounced home by DelVillage(). No change to the battle maths; for a normal (non-razed) target the set stays empty, so behaviour is preserved. Co-authored-by: Claude Opus 4.8 --- GameEngine/Automation.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/GameEngine/Automation.php b/GameEngine/Automation.php index acc325e3..6a432a5f 100644 --- a/GameEngine/Automation.php +++ b/GameEngine/Automation.php @@ -2962,8 +2962,23 @@ class Automation { // cache instead of querying row by row $this->preloadBattleData($dataarray); + // tiles whose village was razed earlier in this same batch (issue #298) + $razedTargets = []; + // calculate battles foreach($dataarray as $data) { + // If an earlier attack in this same batch razed the target village, + // its still-in-flight follow-up waves were already bounced straight + // home by DelVillage() (issue #298). Re-resolving them here would + // fight a now-deleted (phantom) village: getMInfo() returns NULL + // vdata columns, so the return trip is computed from a NULL wref + // (NULL coordinates) — yielding a bogus arrival time that strands + // the troops — and would also duplicate the bounce. Skip them. + if (isset($razedTargets[$data['to']])) { + $data_num++; + continue; + } + //set base things $totaltraped_att = 0; for($i = 1; $i <= 11; $i++) ${'traped'.$i} = 0; @@ -3352,6 +3367,13 @@ class Automation { // delete the target village if it was destroyed — extracted to handleVillageDestruction() [#155] $this->handleVillageDestruction($village_destroyed, $can_destroy, $data, $to, $varray); + + // remember the razed tile so any follow-up wave still queued in + // this same batch is skipped instead of fighting a phantom + // (now-deleted) village — see the guard at the top of the loop (issue #298) + if ($village_destroyed == 1 && $can_destroy == 1) { + $razedTargets[$data['to']] = true; + } }else{ //units attack string for battleraport $unitssend_att1 = ''.$data['t1'].','.$data['t2'].','.$data['t3'].','.$data['t4'].','.$data['t5'].','.$data['t6'].','.$data['t7'].','.$data['t8'].','.$data['t9'].','.$data['t10'].'';