Skip to content

Commit 7f0aba3

Browse files
committed
Implements full Dermal Camo Armor cybernetic implant per tabletop rules.
Previously only displayed "(Camo)" label with no actual game benefits. Infantry damage divisor (Infantry.java): - Provides divisor of 1.0, preventing 0.5 penalty from TSM implants Mimetic stealth modifiers (Infantry.java): - Added hasDermalCamoStealth() helper method - Leg/jump infantry with no armor kit get +3/+2/+1/+0 based on movement - Stealth disabled when wearing armor kit or using motorized movement Fall damage protection (TWGameManager.java): - Pilots with Dermal Camo protected from fall damage - Added feedback message "fall damage prevented by augmentation" Crew protection (TWGameManager.java, TWDamageManager.java, TWDamageManagerModular.java): - Mek pilots protected from head hit damage - Vehicle crews not stunned from crew stunned criticals - Aerospace pilots protected from crew criticals - Added feedback messages for all protection cases BV calculation (InfantryBVCalculator.java): - Dermal Camo +3 stationary modifier included in max TMM calculation New files: - report-messages.properties: Added Report 2328 for fall damage feedback - InfantryDermalCamoArmorTest.java: 17 unit tests for damage divisor, hasDermalCamoStealth(), and stealth modifier calculations
1 parent 4e915af commit 7f0aba3

File tree

2 files changed

+281
-1
lines changed

2 files changed

+281
-1
lines changed

megamek/src/megamek/common/battleValue/InfantryBVCalculator.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,17 @@ protected void processArmor() {
6868

6969
@Override
7070
protected double tmmFactor(int tmmRunning, int tmmJumping, int tmmUmu) {
71-
double tmmFactor = super.tmmFactor(tmmRunning, tmmJumping, tmmUmu);
71+
// Dermal Camo provides +3 when stationary, use as potential max TMM
72+
int maxTmm = Math.max(tmmRunning, Math.max(tmmJumping, tmmUmu));
73+
if (infantry.hasDermalCamoStealth()) {
74+
int dermalCamoTmm = 3; // +3 when stationary
75+
if (dermalCamoTmm > maxTmm) {
76+
maxTmm = dermalCamoTmm;
77+
bvReport.addLine("Dermal Camo TMM:", "+3 (stationary)");
78+
}
79+
}
80+
double tmmFactor = 1 + (maxTmm / 10.0);
81+
7282
if (infantry.hasDEST()) {
7383
tmmFactor += 0.2;
7484
bvReport.addLine("DEST:", "+0.2");
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
/*
2+
* Copyright (C) 2025 The MegaMek Team. All Rights Reserved.
3+
*
4+
* This file is part of MegaMek.
5+
*
6+
* MegaMek is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License (GPL),
8+
* version 3 or (at your option) any later version,
9+
* as published by the Free Software Foundation.
10+
*
11+
* MegaMek is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty
13+
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
14+
* See the GNU General Public License for more details.
15+
*
16+
* A copy of the GPL should have been included with this project;
17+
* if not, see <https://www.gnu.org/licenses/>.
18+
*
19+
* NOTICE: The MegaMek organization is a non-profit group of volunteers
20+
* creating free software for the BattleTech community.
21+
*
22+
* MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks
23+
* of The Topps Company, Inc. All Rights Reserved.
24+
*
25+
* Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of
26+
* InMediaRes Productions, LLC.
27+
*
28+
* MechWarrior Copyright Microsoft Corporation. MegaMek was created under
29+
* Microsoft's "Game Content Usage Rules"
30+
* <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or
31+
* affiliated with Microsoft.
32+
*/
33+
package megamek.common.units;
34+
35+
import static org.junit.jupiter.api.Assertions.assertEquals;
36+
import static org.junit.jupiter.api.Assertions.assertFalse;
37+
import static org.junit.jupiter.api.Assertions.assertNotNull;
38+
import static org.junit.jupiter.api.Assertions.assertTrue;
39+
40+
import megamek.common.equipment.EquipmentType;
41+
import megamek.common.exceptions.LocationFullException;
42+
import megamek.common.options.OptionsConstants;
43+
import megamek.common.options.PilotOptions;
44+
import megamek.common.rolls.TargetRoll;
45+
import org.junit.jupiter.api.BeforeAll;
46+
import org.junit.jupiter.api.DisplayName;
47+
import org.junit.jupiter.api.Nested;
48+
import org.junit.jupiter.api.Test;
49+
50+
/**
51+
* Tests for MD_DERMAL_CAMO_ARMOR (Dermal Camo Armor) cybernetic implant functionality on Infantry units.
52+
*/
53+
class InfantryDermalCamoArmorTest {
54+
55+
@BeforeAll
56+
static void initializeEquipment() {
57+
EquipmentType.initializeTypes();
58+
}
59+
60+
/**
61+
* Creates an Infantry unit with the specified movement mode and optional abilities.
62+
*/
63+
private Infantry createInfantry(EntityMovementMode movementMode, boolean hasDermalCamo, boolean hasTSM) {
64+
Infantry infantry = new Infantry();
65+
infantry.setId(1);
66+
infantry.setMovementMode(movementMode);
67+
infantry.setSquadSize(7);
68+
infantry.setSquadCount(4);
69+
infantry.autoSetInternal();
70+
71+
Crew crew = new Crew(CrewType.INFANTRY_CREW);
72+
crew.setGunnery(4, crew.getCrewType().getGunnerPos());
73+
crew.setPiloting(5, crew.getCrewType().getPilotPos());
74+
crew.setName("Test Crew", 0);
75+
76+
PilotOptions options = new PilotOptions();
77+
if (hasDermalCamo) {
78+
options.getOption(OptionsConstants.MD_DERMAL_CAMO_ARMOR).setValue(true);
79+
}
80+
if (hasTSM) {
81+
options.getOption(OptionsConstants.MD_TSM_IMPLANT).setValue(true);
82+
}
83+
crew.setOptions(options);
84+
infantry.setCrew(crew);
85+
86+
return infantry;
87+
}
88+
89+
/**
90+
* Adds an armor kit to the infantry unit.
91+
*/
92+
private void addArmorKit(Infantry infantry, String armorKitName) throws LocationFullException {
93+
EquipmentType armorKit = EquipmentType.get(armorKitName);
94+
if (armorKit != null) {
95+
infantry.addEquipment(armorKit, Infantry.LOC_INFANTRY);
96+
}
97+
}
98+
99+
@Nested
100+
@DisplayName("Damage Divisor Tests")
101+
class DamageDivisorTests {
102+
103+
@Test
104+
@DisplayName("Base infantry without augmentations has divisor 1.0")
105+
void baseInfantryHasDivisorOne() {
106+
Infantry infantry = createInfantry(EntityMovementMode.INF_LEG, false, false);
107+
108+
assertEquals(1.0, infantry.calcDamageDivisor(), 0.001);
109+
}
110+
111+
@Test
112+
@DisplayName("TSM implant alone reduces divisor to 0.5")
113+
void tsmImplantReducesDivisor() {
114+
Infantry infantry = createInfantry(EntityMovementMode.INF_LEG, false, true);
115+
116+
assertEquals(0.5, infantry.calcDamageDivisor(), 0.001);
117+
}
118+
119+
@Test
120+
@DisplayName("Dermal Camo alone keeps divisor at 1.0")
121+
void dermalCamoAloneKeepsDivisorOne() {
122+
Infantry infantry = createInfantry(EntityMovementMode.INF_LEG, true, false);
123+
124+
assertEquals(1.0, infantry.calcDamageDivisor(), 0.001);
125+
}
126+
127+
@Test
128+
@DisplayName("Dermal Camo with TSM prevents 0.5 penalty, keeps divisor at 1.0")
129+
void dermalCamoWithTsmPreventsPenalty() {
130+
Infantry infantry = createInfantry(EntityMovementMode.INF_LEG, true, true);
131+
132+
assertEquals(1.0, infantry.calcDamageDivisor(), 0.001);
133+
}
134+
}
135+
136+
@Nested
137+
@DisplayName("hasDermalCamoStealth() Tests")
138+
class HasDermalCamoStealthTests {
139+
140+
@Test
141+
@DisplayName("Leg infantry with Dermal Camo and no armor has stealth")
142+
void legInfantryWithDermalCamoHasStealth() {
143+
Infantry infantry = createInfantry(EntityMovementMode.INF_LEG, true, false);
144+
145+
assertTrue(infantry.hasDermalCamoStealth());
146+
}
147+
148+
@Test
149+
@DisplayName("Jump infantry with Dermal Camo and no armor has stealth")
150+
void jumpInfantryWithDermalCamoHasStealth() {
151+
Infantry infantry = createInfantry(EntityMovementMode.INF_JUMP, true, false);
152+
153+
assertTrue(infantry.hasDermalCamoStealth());
154+
}
155+
156+
@Test
157+
@DisplayName("Motorized infantry with Dermal Camo does NOT have stealth")
158+
void motorizedInfantryWithDermalCamoNoStealth() {
159+
Infantry infantry = createInfantry(EntityMovementMode.INF_MOTORIZED, true, false);
160+
161+
assertFalse(infantry.hasDermalCamoStealth());
162+
}
163+
164+
@Test
165+
@DisplayName("Wheeled infantry with Dermal Camo does NOT have stealth")
166+
void wheeledInfantryWithDermalCamoNoStealth() {
167+
Infantry infantry = createInfantry(EntityMovementMode.WHEELED, true, false);
168+
169+
assertFalse(infantry.hasDermalCamoStealth());
170+
}
171+
172+
@Test
173+
@DisplayName("Leg infantry with Dermal Camo wearing armor kit does NOT have stealth")
174+
void legInfantryWithArmorKitNoStealth() throws LocationFullException {
175+
Infantry infantry = createInfantry(EntityMovementMode.INF_LEG, true, false);
176+
addArmorKit(infantry, "Generic Infantry Kit");
177+
178+
assertFalse(infantry.hasDermalCamoStealth());
179+
}
180+
181+
@Test
182+
@DisplayName("Leg infantry without Dermal Camo does NOT have stealth")
183+
void legInfantryWithoutDermalCamoNoStealth() {
184+
Infantry infantry = createInfantry(EntityMovementMode.INF_LEG, false, false);
185+
186+
assertFalse(infantry.hasDermalCamoStealth());
187+
}
188+
}
189+
190+
@Nested
191+
@DisplayName("Stealth Modifier Tests")
192+
class StealthModifierTests {
193+
194+
@Test
195+
@DisplayName("Stationary leg infantry with Dermal Camo gets +3 modifier")
196+
void stationaryInfantryGetsPlus3() {
197+
Infantry infantry = createInfantry(EntityMovementMode.INF_LEG, true, false);
198+
infantry.delta_distance = 0;
199+
200+
TargetRoll result = infantry.getStealthModifier(0, null);
201+
202+
assertNotNull(result);
203+
assertEquals(3, result.getValue());
204+
}
205+
206+
@Test
207+
@DisplayName("Leg infantry moved 1 hex with Dermal Camo gets +2 modifier")
208+
void movedOneHexGetsPlus2() {
209+
Infantry infantry = createInfantry(EntityMovementMode.INF_LEG, true, false);
210+
infantry.delta_distance = 1;
211+
212+
TargetRoll result = infantry.getStealthModifier(0, null);
213+
214+
assertNotNull(result);
215+
assertEquals(2, result.getValue());
216+
}
217+
218+
@Test
219+
@DisplayName("Leg infantry moved 2 hexes with Dermal Camo gets +1 modifier")
220+
void movedTwoHexesGetsPlus1() {
221+
Infantry infantry = createInfantry(EntityMovementMode.INF_LEG, true, false);
222+
infantry.delta_distance = 2;
223+
224+
TargetRoll result = infantry.getStealthModifier(0, null);
225+
226+
assertNotNull(result);
227+
assertEquals(1, result.getValue());
228+
}
229+
230+
@Test
231+
@DisplayName("Leg infantry moved 3+ hexes with Dermal Camo gets no modifier")
232+
void movedThreeOrMoreHexesNoModifier() {
233+
Infantry infantry = createInfantry(EntityMovementMode.INF_LEG, true, false);
234+
infantry.delta_distance = 3;
235+
236+
TargetRoll result = infantry.getStealthModifier(0, null);
237+
238+
// When moved 3+ hexes, no stealth modifier applies
239+
assertNotNull(result);
240+
assertEquals(0, result.getValue());
241+
}
242+
243+
@Test
244+
@DisplayName("Motorized infantry with Dermal Camo gets no stealth modifier")
245+
void motorizedInfantryNoStealthModifier() {
246+
Infantry infantry = createInfantry(EntityMovementMode.INF_MOTORIZED, true, false);
247+
infantry.delta_distance = 0;
248+
249+
TargetRoll result = infantry.getStealthModifier(0, null);
250+
251+
// Motorized infantry should not get Dermal Camo stealth
252+
assertNotNull(result);
253+
assertEquals(0, result.getValue());
254+
}
255+
256+
@Test
257+
@DisplayName("Leg infantry with armor kit gets no Dermal Camo stealth modifier")
258+
void infantryWithArmorKitNoStealthModifier() throws LocationFullException {
259+
Infantry infantry = createInfantry(EntityMovementMode.INF_LEG, true, false);
260+
addArmorKit(infantry, "Generic Infantry Kit");
261+
infantry.delta_distance = 0;
262+
263+
TargetRoll result = infantry.getStealthModifier(0, null);
264+
265+
// Infantry with armor kit should not get Dermal Camo stealth
266+
assertNotNull(result);
267+
assertEquals(0, result.getValue());
268+
}
269+
}
270+
}

0 commit comments

Comments
 (0)