diff --git a/megamek/src/megamek/client/ui/swing/MovementDisplay.java b/megamek/src/megamek/client/ui/swing/MovementDisplay.java index e2a60d5b773..dfce83d335c 100644 --- a/megamek/src/megamek/client/ui/swing/MovementDisplay.java +++ b/megamek/src/megamek/client/ui/swing/MovementDisplay.java @@ -570,7 +570,7 @@ public void performAction() { final Entity ce = ce(); boolean isAero = ce.isAero(); // first check if jumping is available at all - if (!isAero && !ce.isImmobile() && (ce.getJumpMP() > 0) + if (!isAero && !ce.isImmobileForJump() && (ce.getJumpMP() > 0) && !(ce.isStuck() && !ce.canUnstickByJumping())) { if (gear != MovementDisplay.GEAR_JUMP) { if (!((cmd.getLastStep() != null) @@ -813,7 +813,7 @@ private void updateButtons() { setWalkEnabled(!ce.isImmobile() && ((ce.getWalkMP() > 0) || (ce.getRunMP() > 0)) && !ce.isStuck()); - setJumpEnabled(!isAero && !ce.isImmobile() && !ce.isProne() + setJumpEnabled(!isAero && !ce.isImmobileForJump() && !ce.isProne() // Conventional infantry also uses jump MP for VTOL and UMU MP && ((ce.getJumpMP() > 0) && (!ce.isConventionalInfantry() || ce.getMovementMode().isJumpInfantry())) && !(ce.isStuck() && !ce.canUnstickByJumping())); diff --git a/megamek/src/megamek/common/Compute.java b/megamek/src/megamek/common/Compute.java index 91ff99544c3..3530c29f784 100644 --- a/megamek/src/megamek/common/Compute.java +++ b/megamek/src/megamek/common/Compute.java @@ -1072,7 +1072,13 @@ public static ToHitData getImmobileMod(Targetable target, int aimingAt, AimingMo if ((target instanceof Mech) && (aimingAt == Mech.LOC_HEAD) && aimingMode.isImmobile()) { return new ToHitData(3, "aiming at head"); } - return new ToHitData(-4, "target immobile"); + ToHitData immobileTHD = new ToHitData(-4, "target immobile"); + if(target instanceof Tank) { + // An "immobilized" but jumping CV is not actually immobile for targeting purposes + // (See issue #3917) + return ((Tank)target).moved == EntityMovementType.MOVE_JUMP ? null : immobileTHD; + } + return immobileTHD; } return null; } diff --git a/megamek/src/megamek/common/Entity.java b/megamek/src/megamek/common/Entity.java index 7c61b692d53..36c39a9fbc3 100644 --- a/megamek/src/megamek/common/Entity.java +++ b/megamek/src/megamek/common/Entity.java @@ -1751,6 +1751,10 @@ public boolean isImmobile() { return isImmobile(true); } + public boolean isImmobileForJump() { + return isImmobile(); + } + /** * Is this entity shut down, or if applicable is the crew unconscious? * @param checkCrew If true, consider the fitness of the crew when determining diff --git a/megamek/src/megamek/common/MoveStep.java b/megamek/src/megamek/common/MoveStep.java index e181a674ac7..76c9a36fdb9 100644 --- a/megamek/src/megamek/common/MoveStep.java +++ b/megamek/src/megamek/common/MoveStep.java @@ -3250,6 +3250,11 @@ public boolean isMovementPossible(Game game, Coords src, int srcEl, CachedEntity return true; } + // Motive hit has immobilized CV, but it still wants to (and can) jump: okay! + if (movementType == EntityMovementType.MOVE_JUMP && (entity instanceof Tank) && !entity.isImmobileForJump()) { + return true; + } + // super-easy, but not any more if (entity.isImmobile() && !entity.isBracing()) { return false; diff --git a/megamek/src/megamek/common/Tank.java b/megamek/src/megamek/common/Tank.java index 1939156bcbf..985af9cf416 100644 --- a/megamek/src/megamek/common/Tank.java +++ b/megamek/src/megamek/common/Tank.java @@ -567,6 +567,23 @@ public boolean isPermanentlyImmobilized(boolean checkCrew) { return super.isPermanentlyImmobilized(checkCrew) || isMovementHit(); } + /** + * Per https://bg.battletech.com/forums/index.php/topic,78336.msg1869386.html#msg1869386 + * CVs with working engines and Jump Jets should still have the option to jump during the movement + * phase, even if reduced to 0 MP by motive hits, or rolling 12 on the Motive System Damage table. + */ + @Override + public boolean isImmobileForJump() { + // *Can* jump unless 0 Jump MP, or 1+ Jump MP but engine is critted, or crew unconscious/dead. + boolean jumpImmobile = ( + super.isImmobile(true) || + super.isPermanentlyImmobilized(true) || + (getJumpMP() == 0) || + (isEngineHit()) + ); + return jumpImmobile; + } + @Override public boolean hasCommandConsoleBonus() { if (!hasWorkingMisc(MiscType.F_COMMAND_CONSOLE) || isCommanderHit() || isUsingConsoleCommander()) { diff --git a/megamek/src/megamek/server/GameManager.java b/megamek/src/megamek/server/GameManager.java index 1a566c6e304..d1ec7ba523f 100644 --- a/megamek/src/megamek/server/GameManager.java +++ b/megamek/src/megamek/server/GameManager.java @@ -33657,7 +33657,7 @@ private Vector vehicleMotiveDamage(Tank te, int modifier, boolean noRoll default: break; } - // Apply vehicle effectiveness...except for jumps. + // Apply vehicle effectiveness...except for hits from jumps. if (game.getOptions().booleanOption(OptionsConstants.ADVCOMBAT_TACOPS_VEHICLE_EFFECTIVE) && !jumpDamage) { modifier = Math.max(modifier - 1, 0); @@ -33722,7 +33722,7 @@ private Vector vehicleMotiveDamage(Tank te, int modifier, boolean noRoll // unsure how to *report* any outcomes from there. Note that these treat // being reduced to 0 MP and being actually immobilized as the same thing, // which for these particular purposes may or may not be the intent of - // the rules in all cases. + // the rules in all cases (for instance, motive-immobilized CVs can still jump). // Immobile hovercraft on water sink... if (!te.isOffBoard() && (te.getMovementMode() == EntityMovementMode.HOVER && (te.isMovementHitPending() || (te.getWalkMP() <= 0)) diff --git a/megamek/unittests/megamek/common/EntityTest.java b/megamek/unittests/megamek/common/EntityTest.java index 9132c7145fb..2e6a13be223 100644 --- a/megamek/unittests/megamek/common/EntityTest.java +++ b/megamek/unittests/megamek/common/EntityTest.java @@ -26,8 +26,7 @@ import java.io.File; import java.util.ArrayList; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -73,14 +72,14 @@ public void testCalculateBattleValue() { actual = testEntity.calculateBattleValue(true, true); assertEquals(expected, actual); } - + @Test public void testCalculateWeight() { - File f; + File f; MechFileParser mfp; Entity e; int expectedWeight, computedWeight; - + // Test 1/1 try { f = new File("data/mechfiles/mechs/3050U/Exterminator EXT-4A.mtf"); @@ -93,4 +92,65 @@ public void testCalculateWeight() { fail(ex.getMessage()); } } + + /** + * Verify new Tank method .isImmobilizedForJump() returns correct values in + * various states. Note: vehicles cannot lose individual Jump Jets via crits, + * so this is not tested. + */ + @Test + public void testIsImmobilizedForJump() { + File f; + MechFileParser mfp; + Entity e; + + // Test 1/1 + try { + f = new File("data/mechfiles/vehicles/3050U/Kanga Medium Hovertank.blk"); + mfp = new MechFileParser(f); + e = mfp.getEntity(); + Tank t = (Tank) e; + Crew c = t.getCrew(); + + // 1 Crew condition + // 1.a Killed crew should prevent jumping; live crew should allow jumping + c.setDead(true); + assertTrue(t.isImmobileForJump()); + c.resetGameState(); + assertFalse(t.isImmobileForJump()); + + // 1.b Unconscious crew should prevent jumping; conscious crew should allow jumping + c.setUnconscious(true); + assertTrue(t.isImmobileForJump()); + c.resetGameState(); + assertFalse(t.isImmobileForJump()); + + // 1.c Stunned crew should _not_ prevent jumping + t.setStunnedTurns(1); + assertFalse(t.isImmobileForJump()); + t.setStunnedTurns(0); + + // 2. Engine condition + // 2.a Engine hit should prevent jumping; fixing engine should enable jumping + t.engineHit(); + assertTrue(t.isImmobileForJump()); + t.engineFix(); + assertFalse(t.isImmobileForJump()); + + // 2.b Shutdown should prevent jumping; restarting should enable jumping + t.setShutDown(true); + assertTrue(t.isImmobileForJump()); + t.setShutDown(false); + assertFalse(t.isImmobileForJump()); + + // 3. Immobilization due to massive damage motive hit / reducing MP to 0 should + // _not_ prevent jumping + t.setMotiveDamage(t.getOriginalWalkMP()); + assertFalse(t.isImmobileForJump()); + t.setMotiveDamage(0); + + } catch (Exception ex) { + fail(ex.getMessage()); + } + } }