From 9543e9db47922ec501581fc44c9554cc4bb216ce Mon Sep 17 00:00:00 2001 From: HammerGS Date: Fri, 11 Sep 2026 21:54:55 -0600 Subject: [PATCH 1/5] Fix #8931: crash an aero that weapon fire knocks to the ground Fixes #8931 Altitude stripped by weapon fire was applied without checking whether the unit had reached the ground, so a fighter could sit at altitude zero having never crashed. It was then stuck: the velocity nag refused every move because it still counted as airborne, and eject, shut down and flying off the map were all blocked with it. Three of the four places that take altitude off an aero already check for a crash afterwards: the out-of-control heat path, the control-roll path and movement. The weapon-attack path did not. It now does, using the same checkCrash and processCrash pair, which returns false in space and on board types where altitude zero is legal. Reported state corroborated by the reporter's game log, which shows the Aria losing 2, 2, 2 then 1 altitude to weapon fire with no crash reported. Their save could not be loaded to confirm the final altitude directly; it predates the removal of Player.numMfConv. Not changed: Entity.isAirborne reads (!isDestroyed() && altitude > 0) || mode == AERODYNE || mode == SPHEROID so any aerodyne counts as airborne at altitude zero, and when destroyed. That is why the velocity nag fired at all. 309 call sites depend on it, so it needs its own change and its own testing. Co-Authored-By: Claude Opus 5 (1M context) --- megamek/src/megamek/server/totalWarfare/TWGameManager.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/megamek/src/megamek/server/totalWarfare/TWGameManager.java b/megamek/src/megamek/server/totalWarfare/TWGameManager.java index bd758fe49cf..2ba3316e0b8 100644 --- a/megamek/src/megamek/server/totalWarfare/TWGameManager.java +++ b/megamek/src/megamek/server/totalWarfare/TWGameManager.java @@ -10410,6 +10410,12 @@ public boolean accept(Entity entity) { entity.setAltitude(entity.getAltitude() - aero.getAltLoss()); aero.setAltLossThisRound(aero.getAltLoss()); aero.resetAltLoss(); + // Altitude knocked off by weapon fire can reach the ground, and a unit that arrives there has + // crashed. Without this the fighter sits at altitude zero still counted as airborne, unable to + // spend the velocity it is holding and unable to do anything else either (issue #8931). + if (checkCrash(entity)) { + addReport(processCrash(entity, aero.getCurrentVelocity(), entity.getPosition())); + } entityUpdate(entity.getId()); } } From af41ea14b1a6f32fb034948515e971ac9a2b8e2f Mon Sep 17 00:00:00 2001 From: HammerGS Date: Sat, 12 Sep 2026 16:29:54 -0600 Subject: [PATCH 2/5] Correct what the altitude loss actually is The earlier commit called this altitude knocked off by weapon fire. It is not. WeaponAttackAction.getAltitudeLoss is the altitude the attacker spends making an air-to-ground attack: 2 for a dive bomb, 1 for a standard attack, 0 for strafing or an altitude bomb. The fix is unchanged and still correct. An aero that spends its last altitude attacking a ground target ends up on the ground, which is a crash, and nothing checked for one. Only the description was wrong, and it made the issue impossible to reproduce from the steps given. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Y1Lb2sbXg7jYcZoGDgyhWB --- megamek/src/megamek/server/totalWarfare/TWGameManager.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/megamek/src/megamek/server/totalWarfare/TWGameManager.java b/megamek/src/megamek/server/totalWarfare/TWGameManager.java index 2ba3316e0b8..753390e1526 100644 --- a/megamek/src/megamek/server/totalWarfare/TWGameManager.java +++ b/megamek/src/megamek/server/totalWarfare/TWGameManager.java @@ -10410,8 +10410,9 @@ public boolean accept(Entity entity) { entity.setAltitude(entity.getAltitude() - aero.getAltLoss()); aero.setAltLossThisRound(aero.getAltLoss()); aero.resetAltLoss(); - // Altitude knocked off by weapon fire can reach the ground, and a unit that arrives there has - // crashed. Without this the fighter sits at altitude zero still counted as airborne, unable to + // This is altitude the attacker spends on its own air-to-ground attacks: 2 for a dive bomb, + // 1 for a standard attack. Spending the last of it puts the unit on the ground, which is a + // crash. Without this the fighter sits at altitude zero still counted as airborne, unable to // spend the velocity it is holding and unable to do anything else either (issue #8931). if (checkCrash(entity)) { addReport(processCrash(entity, aero.getCurrentVelocity(), entity.getPosition())); From f62fe1df58d7761970a2f832a68a9f2d41306a34 Mon Sep 17 00:00:00 2001 From: HammerGS Date: Sat, 12 Sep 2026 17:14:06 -0600 Subject: [PATCH 3/5] Say the altitude was spent on an attack run, not lost to enemy fire Report 9095 read "loses N altitude(s) from weapon attack", which sounds like the unit was shot down. It fires only when a unit spends its own altitude making an air-to-ground attack. The reporter's log reads as a fighter being driven into the ground by enemy fire; it was dive bombing. English, Spanish and Russian updated together. The Russian stays as \uXXXX escapes to match the rest of that file. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Y1Lb2sbXg7jYcZoGDgyhWB --- megamek/resources/megamek/common/report-messages.properties | 2 +- megamek/resources/megamek/common/report-messages_es.properties | 2 +- megamek/resources/megamek/common/report-messages_ru.properties | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/megamek/resources/megamek/common/report-messages.properties b/megamek/resources/megamek/common/report-messages.properties index 472d20e17e7..d0ec21710c6 100755 --- a/megamek/resources/megamek/common/report-messages.properties +++ b/megamek/resources/megamek/common/report-messages.properties @@ -1453,7 +1453,7 @@ 9080= () launches missile(s) from . 9085= () targets () for attack. 9090=\ The attack over-penetrates!!! -9095= () loses altitude(s) from weapon attack. +9095= () loses altitude(s) making an attack run. #9100 - critical hits 9100=Possible critical hit () 9101=; needs , rolls : diff --git a/megamek/resources/megamek/common/report-messages_es.properties b/megamek/resources/megamek/common/report-messages_es.properties index 140ca2ede5c..74a67922d03 100644 --- a/megamek/resources/megamek/common/report-messages_es.properties +++ b/megamek/resources/megamek/common/report-messages_es.properties @@ -1049,7 +1049,7 @@ 9080= () lanza misiles desde . 9085= () apunta a () para el ataque. 9090=\ \u00A1\u00A1\u00A1El ataque sobrepenetra !!! -9095= () pierde de altitud por ataque con arma. +9095= () pierde de altitud al realizar una pasada de ataque. # 9100 - golpes cr\u00EDticos 9100=Posible golpe cr\u00EDtico () 9101=; necesita , saca : diff --git a/megamek/resources/megamek/common/report-messages_ru.properties b/megamek/resources/megamek/common/report-messages_ru.properties index f1cd4a6850f..5d8ce117e26 100644 --- a/megamek/resources/megamek/common/report-messages_ru.properties +++ b/megamek/resources/megamek/common/report-messages_ru.properties @@ -885,7 +885,7 @@ 9080= () \u0437\u0430\u043F\u0443\u0441\u043A\u0430\u0435\u0442 \u0440\u0430\u043A\u0435\u0442\u0443. 9085= () \u043F\u0440\u0438\u0446\u0435\u043B\u0438\u0432\u0430\u0435\u0442\u0441\u044F \u0432 () \u0434\u043B\u044F \u0430\u0442\u0430\u043A\u0438. 9090=\ \u0410\u0442\u0430\u043A\u0430 \u043F\u0440\u043E\u0431\u0438\u0432\u0430\u0435\u0442 \u043D\u0430\u0441\u043A\u0432\u043E\u0437\u044C!!! -9095= () \u0442\u0435\u0440\u044F\u0435\u0442 \u0432\u044B\u0441\u043E\u0442\u044B \u043E\u0442 \u0430\u0442\u0430\u043A\u0438 \u043E\u0440\u0443\u0436\u0438\u0435\u043C. +9095= () \u0442\u0435\u0440\u044F\u0435\u0442 \u0432\u044B\u0441\u043E\u0442\u044B, \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u044F \u0437\u0430\u0445\u043E\u0434 \u043D\u0430 \u0430\u0442\u0430\u043A\u0443. #9100 - \u043A\u0440\u0438\u0442\u0438\u0447\u0435\u0441\u043A\u0438\u0435 \u043F\u043E\u043F\u0430\u0434\u0430\u043D\u0438\u044F 9100=\u0412\u043E\u0437\u043C\u043E\u0436\u043D\u043E\u0435 \u043A\u0440\u0438\u0442\u0438\u0447\u0435\u0441\u043A\u043E\u0435 \u043F\u043E\u043F\u0430\u0434\u0430\u043D\u0438\u0435 () 9101=; \u043D\u0443\u0436\u043D\u043E , \u0432\u044B\u0431\u0440\u0430\u0441\u044B\u0432\u0430\u0435\u0442 : From 7e94a99cfcab1742a62083ce39f13c36c117f7d9 Mon Sep 17 00:00:00 2001 From: HammerGS Date: Sat, 12 Sep 2026 18:35:14 -0600 Subject: [PATCH 4/5] Let an aero on the ground end its turn The unused-velocity gate asks isAirborne(), which is true for any aerodyne whatever its altitude, so a fighter sitting on the ground still looked like it was flying and was refused permission to end its turn. It has no way to comply. On a ground map one velocity point costs sixteen hexes of movement, and at altitude 0 every move step is illegal because the aero movement section is gated behind altitude above zero. Move refused and not-moving refused leaves the unit stranded for good. Now exempts an aero at altitude 0 on a ground map. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Y1Lb2sbXg7jYcZoGDgyhWB --- .../client/ui/panels/phaseDisplay/MovementDisplay.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/megamek/src/megamek/client/ui/panels/phaseDisplay/MovementDisplay.java b/megamek/src/megamek/client/ui/panels/phaseDisplay/MovementDisplay.java index 11e54a66a76..5e4f1fc54a7 100644 --- a/megamek/src/megamek/client/ui/panels/phaseDisplay/MovementDisplay.java +++ b/megamek/src/megamek/client/ui/panels/phaseDisplay/MovementDisplay.java @@ -2195,8 +2195,14 @@ private boolean checkNags() { // Check for unused velocity for airborne and spacecraft. if (needNagForOther()) { if ((currentlySelectedEntity != null) && (null != cmd) && currentlySelectedEntity.isAero()) { - boolean airborneOrSpaceborne = currentlySelectedEntity.isAirborne() || - currentlySelectedEntity.isSpaceborne(); + // An aerodyne reports itself airborne at any altitude, so a fighter sitting on the ground + // still looks like it is flying. Left in, this refuses to end the turn over velocity the unit + // has no way to spend: on a ground map one point costs sixteen hexes of movement, and every + // move step is illegal at altitude 0 anyway. That combination strands the unit for good. + boolean isOnTheGround = (currentlySelectedEntity.getAltitude() == 0) + && game.getBoard(currentlySelectedEntity).isGround(); + boolean airborneOrSpaceborne = (currentlySelectedEntity.isAirborne() && !isOnTheGround) + || currentlySelectedEntity.isSpaceborne(); boolean unusedVelocity; if (null != cmd.getLastStep()) { From 739da54535b52ccf0ab19f138c76f45e2060e780 Mon Sep 17 00:00:00 2001 From: HammerGS Date: Sat, 12 Sep 2026 19:28:35 -0600 Subject: [PATCH 5/5] Drop the crash check; the rules already prevent that state An attack that would take the attacker to altitude 0 is refused before it can be declared: // You can't make attacks that would lower you to zero altitude if (altitudeLoss >= (attacker.getAltitude() + altLossThisRound)) { return Messages.getString("WeaponAttackAction.TooMuchAltLoss"); } At altitude 1 a standard attack costs 1 and is refused; at altitude 2 a dive bomb costs 2 and is refused. A fighter cannot legally fly itself into the ground on an attack run, so the crash check guarded a state legal play does not produce. The reporter's fighter reached altitude 0 without ever attacking, which bypasses that guard entirely and is a separate fault still open. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Y1Lb2sbXg7jYcZoGDgyhWB --- megamek/src/megamek/server/totalWarfare/TWGameManager.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/megamek/src/megamek/server/totalWarfare/TWGameManager.java b/megamek/src/megamek/server/totalWarfare/TWGameManager.java index 753390e1526..bd758fe49cf 100644 --- a/megamek/src/megamek/server/totalWarfare/TWGameManager.java +++ b/megamek/src/megamek/server/totalWarfare/TWGameManager.java @@ -10410,13 +10410,6 @@ public boolean accept(Entity entity) { entity.setAltitude(entity.getAltitude() - aero.getAltLoss()); aero.setAltLossThisRound(aero.getAltLoss()); aero.resetAltLoss(); - // This is altitude the attacker spends on its own air-to-ground attacks: 2 for a dive bomb, - // 1 for a standard attack. Spending the last of it puts the unit on the ground, which is a - // crash. Without this the fighter sits at altitude zero still counted as airborne, unable to - // spend the velocity it is holding and unable to do anything else either (issue #8931). - if (checkCrash(entity)) { - addReport(processCrash(entity, aero.getCurrentVelocity(), entity.getPosition())); - } entityUpdate(entity.getId()); } }