Skip to content

Commit 1299db9

Browse files
authored
Merge pull request #8467 from IllianiBird/mapSizeGeneration
Improvement: Updated Scenario Map Size Generation
2 parents 5a72625 + 7f7d0ae commit 1299db9

File tree

12 files changed

+245
-47
lines changed

12 files changed

+245
-47
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Copyright (C) 2025 The MegaMek Team. All Rights Reserved.
2+
#
3+
# This file is part of MekHQ.
4+
#
5+
# MekHQ is free software: you can redistribute it and/or modify
6+
# it under the terms of the GNU General Public License (GPL),
7+
# version 3 or (at your option) any later version,
8+
# as published by the Free Software Foundation.
9+
#
10+
# MekHQ is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty
12+
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
13+
# See the GNU General Public License for more details.
14+
#
15+
# A copy of the GPL should have been included with this project;
16+
# if not, see <https://www.gnu.org/licenses/>.
17+
#
18+
# NOTICE: The MegaMek organization is a non-profit group of volunteers
19+
# creating free software for the BattleTech community.
20+
#
21+
# MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks
22+
# of The Topps Company, Inc. All Rights Reserved.
23+
#
24+
# Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of
25+
# InMediaRes Productions, LLC.
26+
#
27+
# MechWarrior Copyright Microsoft Corporation. MegaMek was created under
28+
# Microsoft's "Game Content Usage Rules"
29+
# <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or
30+
# affiliated with Microsoft.
31+
# suppress inspection "UnusedProperty" for the whole file
32+
BoardScalingType.SMALL.label=Small
33+
BoardScalingType.NORMAL.label=Normal
34+
BoardScalingType.LARGE.label=Large
35+
BoardScalingType.HUGE.label=Huge

MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1817,6 +1817,10 @@ lblSkillLevel.text=Difficulty \u2714
18171817
lblSkillLevel.tooltip=<html>This is the difficulty level for generated scenarios.\
18181818
<br>\
18191819
<br><b>Recommended:</b> New players are recommended to start with Green or Ultra-Green.</html>
1820+
lblBoardScalingType.text=Scenario Map Size <span style="color:#C344C3;">\u2605</span>
1821+
lblBoardScalingType.tooltip=Adjusts the size of scenario maps. 'Normal' uses the Total War recommended one map sheet \
1822+
per 4 units (rounded down). Larger map sizes allow for more tactical maneuvering at the expense of favoring longer \
1823+
ranged units. While small map sizes increase scenario speed while favoring melee units.
18201824
# Callsigns
18211825
lblAutoGeneratedCallSignsPanel.text=Auto-Generated OpFor Call Signs
18221826
lblAutoGenerateOpForCallSigns.text=Enable Auto-generated OpFor Call Signs
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright (C) 2025 The MegaMek Team. All Rights Reserved.
3+
*
4+
* This file is part of MekHQ.
5+
*
6+
* MekHQ 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+
* MekHQ 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. MekHQ 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 mekhq.campaign.campaignOptions;
34+
35+
import static mekhq.utilities.MHQInternationalization.getTextAt;
36+
37+
public enum BoardScalingType {
38+
SMALL("SMALL", -1, 1),
39+
NORMAL("NORMAL", 0, 1),
40+
LARGE("LARGE", 1, 2),
41+
HUGE("HUGE", 2, 3);
42+
43+
private static final String RESOURCE_BUNDLE = "mekhq.resources.BoardScalingType";
44+
45+
private final String lookupName;
46+
private final String label;
47+
private final int heightModifier;
48+
private final int minimumWidth;
49+
50+
BoardScalingType(String lookupName, int heightModifier, int minimumWidth) {
51+
this.lookupName = lookupName;
52+
this.heightModifier = heightModifier;
53+
this.minimumWidth = minimumWidth;
54+
this.label = generateLabel();
55+
}
56+
57+
public String getLookupName() {
58+
return lookupName;
59+
}
60+
61+
@Override
62+
public String toString() {
63+
return label;
64+
}
65+
66+
public int getHeightModifier() {
67+
return heightModifier;
68+
}
69+
70+
public int getMinimumWidth() {
71+
return minimumWidth;
72+
}
73+
74+
private String generateLabel() {
75+
return getTextAt(RESOURCE_BUNDLE, "BoardScalingType." + lookupName + ".label");
76+
}
77+
78+
public static BoardScalingType parseFromLookupName(String lookupName) {
79+
for (BoardScalingType type : BoardScalingType.values()) {
80+
if (type.lookupName.equals(lookupName)) {
81+
return type;
82+
}
83+
}
84+
return NORMAL;
85+
}
86+
}

MekHQ/src/mekhq/campaign/campaignOptions/CampaignOptions.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,7 @@ public static String getTechLevelName(final int techLevel) {
626626
private boolean useAdvancedScouting;
627627
private boolean noSeedForces;
628628
private SkillLevel skillLevel;
629+
private BoardScalingType boardScalingType;
629630

630631
// Contract Operations
631632
private int moraleVictoryEffect;
@@ -1282,6 +1283,7 @@ public CampaignOptions() {
12821283
useAdvancedScouting = false;
12831284
noSeedForces = false;
12841285
setSkillLevel(SkillLevel.REGULAR);
1286+
boardScalingType = BoardScalingType.NORMAL;
12851287
autoResolveMethod = AutoResolveMethod.PRINCESS;
12861288
autoResolveVictoryChanceEnabled = false;
12871289
autoResolveNumberOfScenarios = 100;
@@ -4950,6 +4952,14 @@ public void setSkillLevel(final SkillLevel skillLevel) {
49504952
this.skillLevel = skillLevel;
49514953
}
49524954

4955+
public BoardScalingType getBoardScalingType() {
4956+
return boardScalingType;
4957+
}
4958+
4959+
public void setBoardScalingType(final BoardScalingType boardScalingType) {
4960+
this.boardScalingType = boardScalingType;
4961+
}
4962+
49534963
public boolean isAeroRecruitsHaveUnits() {
49544964
return aeroRecruitsHaveUnits;
49554965
}

MekHQ/src/mekhq/campaign/campaignOptions/CampaignOptionsMarshaller.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1056,6 +1056,10 @@ public static void writeCampaignOptionsToXML(final CampaignOptions campaignOptio
10561056

10571057
// region AtB Tab
10581058
MHQXMLUtility.writeSimpleXMLTag(pw, indent, "skillLevel", campaignOptions.getSkillLevel().name());
1059+
MHQXMLUtility.writeSimpleXMLTag(pw,
1060+
indent,
1061+
"boardScalingType",
1062+
campaignOptions.getBoardScalingType().getLookupName());
10591063
MHQXMLUtility.writeSimpleXMLTag(pw, indent, "autoResolveMethod", campaignOptions.getAutoResolveMethod().name());
10601064
MHQXMLUtility.writeSimpleXMLTag(pw,
10611065
indent,

MekHQ/src/mekhq/campaign/campaignOptions/CampaignOptionsUnmarshaller.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -828,6 +828,8 @@ private static boolean parseNodeName(Version version, String nodeName, CampaignO
828828
case "isEnableSalvageFlagByDefault" ->
829829
campaignOptions.setEnableSalvageFlagByDefault(parseBoolean(nodeContents));
830830
case "skillLevel" -> campaignOptions.setSkillLevel(SkillLevel.parseFromString(nodeContents));
831+
case "boardScalingType" ->
832+
campaignOptions.setBoardScalingType(BoardScalingType.parseFromLookupName(nodeContents));
831833
case "autoResolveMethod" -> campaignOptions.setAutoResolveMethod(AutoResolveMethod.valueOf(nodeContents));
832834
case "autoResolveVictoryChanceEnabled" -> campaignOptions.setAutoResolveVictoryChanceEnabled(parseBoolean(
833835
nodeContents));

MekHQ/src/mekhq/campaign/mission/AtBDynamicScenario.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,8 +235,8 @@ public int getMapY() {
235235
}
236236

237237
@Override
238-
public void setMapSize() {
239-
AtBDynamicScenarioFactory.setScenarioMapSize(this);
238+
public void setMapSize(Campaign campaign) {
239+
AtBDynamicScenarioFactory.setScenarioMapSize(this, campaign);
240240
}
241241

242242
/**

MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java

Lines changed: 70 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
*/
3333
package mekhq.campaign.mission;
3434

35+
import static java.lang.Math.floor;
3536
import static java.lang.Math.max;
3637
import static java.lang.Math.min;
3738
import static java.lang.Math.round;
@@ -111,9 +112,10 @@
111112
import mekhq.campaign.Hangar;
112113
import mekhq.campaign.againstTheBot.AtBConfiguration;
113114
import mekhq.campaign.camOpsReputation.IUnitRating;
115+
import mekhq.campaign.campaignOptions.BoardScalingType;
114116
import mekhq.campaign.campaignOptions.CampaignOptions;
115-
import mekhq.campaign.force.CombatTeam;
116117
import mekhq.campaign.enums.DragoonRating;
118+
import mekhq.campaign.force.CombatTeam;
117119
import mekhq.campaign.force.Force;
118120
import mekhq.campaign.mission.AtBDynamicScenario.BenchedEntityData;
119121
import mekhq.campaign.mission.ScenarioForceTemplate.ForceAlignment;
@@ -280,7 +282,7 @@ public static void finalizeScenario(AtBDynamicScenario scenario, AtBContract con
280282

281283
// approximate estimate, anyway.
282284
scenario.setForceCount(generatedLanceCount + (playerForceUnitCount / 4));
283-
setScenarioMapSize(scenario);
285+
setScenarioMapSize(scenario, campaign);
284286
scenario.setScenarioMap(campaign.getCampaignOptions().getFixedMapChance());
285287
setDeploymentZones(scenario);
286288
setDestinationZones(scenario);
@@ -1872,57 +1874,83 @@ private static void setPlanetaryConditions(AtBDynamicScenario scenario, AtBContr
18721874
}
18731875

18741876
/**
1875-
* Sets dynamic AtB-sized base map size for the given scenario.
1877+
* Calculates and sets the dimensions of the scenario map based on the forces involved and the scenario template
1878+
* parameters.
18761879
*
1877-
* @param scenario The scenario to process.
1880+
* <p>This method determines the map size using one of two strategies defined in the {@code ScenarioTemplate}:
1881+
* <ul>
1882+
* <li><b>Standard AtB Sizing:</b> If enabled, the size is calculated based on the total unit count (player +
1883+
* bot forces). It follows the "Total Warfare" suggestion of roughly one map sheet per 4 units. Infantry
1884+
* units in bot forces are excluded from this count to prevent excessive map sizes.
1885+
* </li>
1886+
* <li><b>Template Base Sizing:</b> Uses fixed base dimensions defined in the template, optionally scaled by
1887+
* increments based on the number of forces involved.
1888+
* </li>
1889+
* </ul>
1890+
*
1891+
* <p>Additionally, if the template allows rotation, there is a 50% chance the calculated X and Y dimensions
1892+
* will be swapped.</p>
1893+
*
1894+
* @param scenario The dynamic scenario object to update with the new map dimensions.
1895+
* @param campaign The current campaign context, used to retrieve unit counts and entity lists.
18781896
*/
1879-
public static void setScenarioMapSize(AtBDynamicScenario scenario) {
1897+
public static void setScenarioMapSize(AtBDynamicScenario scenario, Campaign campaign) {
18801898
int mapSizeX;
18811899
int mapSizeY;
18821900
ScenarioTemplate template = scenario.getTemplate();
1883-
1884-
// if the template says to use standard AtB sizing, determine it randomly here
1885-
if (template.mapParameters.isUseStandardAtBSizing()) {
1886-
int roll = randomInt(20) + 1;
1887-
if (roll < 6) {
1888-
mapSizeX = 20;
1889-
mapSizeY = 10;
1890-
} else if (roll < 11) {
1891-
mapSizeX = 10;
1892-
mapSizeY = 20;
1893-
} else if (roll < 13) {
1894-
mapSizeX = 30;
1895-
mapSizeY = 10;
1896-
} else if (roll < 15) {
1897-
mapSizeX = 10;
1898-
mapSizeY = 30;
1899-
} else if (roll < 19) {
1900-
mapSizeX = 20;
1901-
mapSizeY = 20;
1902-
} else if (roll == 19) {
1903-
mapSizeX = 40;
1904-
mapSizeY = 10;
1905-
} else {
1906-
mapSizeX = 10;
1907-
mapSizeY = 40;
1901+
ScenarioMapParameters mapParameters = template.mapParameters;
1902+
if (mapParameters.isUseStandardAtBSizing()) {
1903+
CampaignOptions campaignOptions = campaign.getCampaignOptions();
1904+
BoardScalingType boardScaling = campaignOptions.getBoardScalingType();
1905+
int heightModifier = boardScaling.getHeightModifier();
1906+
int minimumWidth = boardScaling.getMinimumWidth();
1907+
1908+
// We're using this as a shortcut, rather than fetching the player force directly
1909+
int unitCount = getUnitCountWithoutUsingASeedForce(campaign);
1910+
for (BotForce botForce : scenario.getBotForces()) {
1911+
for (Entity entity : botForce.getFullEntityList(campaign)) {
1912+
// We don't count infantry (on the OpFor side) to avoid large infantry fights generating massive
1913+
// scenario maps
1914+
if (!entity.isInfantry()) {
1915+
unitCount++;
1916+
}
1917+
}
19081918
}
1909-
// otherwise, the map width/height have been specified explicitly
1919+
1920+
int mapSheetWidth = 16;
1921+
int mapSheetHeight = 17;
1922+
1923+
// TW suggests one map sheet per 4 units. We floor as we want to veer towards smaller maps, rather than
1924+
// larger.
1925+
double totalMapSheets = floor(unitCount / 4.0);
1926+
1927+
// We want to keep scenario heights low to avoid players needing to spend several turns just traveling.
1928+
// We received feedback that while this allowed for more tactical maneuvers, it wasn't fun.
1929+
int mapSheetsTall = totalMapSheets >= 4 ? (int) floor(totalMapSheets / 2.0) : 1;
1930+
mapSheetsTall = max(1, mapSheetsTall + heightModifier + mapParameters.getAdditionalMapSheetTall());
1931+
1932+
// This creates a wide area of engagement which should help reduce the tendency for forces to 'death ball'
1933+
int mapSheetsWide = (int) floor(totalMapSheets / mapSheetsTall);
1934+
mapSheetsWide = max(minimumWidth, mapSheetsWide + mapParameters.getAdditionalMapSheetWide());
1935+
1936+
mapSizeX = mapSheetWidth * mapSheetsWide;
1937+
mapSizeY = mapSheetHeight * mapSheetsTall;
19101938
} else {
1911-
mapSizeX = template.mapParameters.getBaseWidth();
1912-
mapSizeY = template.mapParameters.getBaseHeight();
1913-
}
1939+
mapSizeX = mapParameters.getBaseWidth();
1940+
mapSizeY = mapParameters.getBaseHeight();
19141941

1915-
// increment map size by template-specified increments
1916-
mapSizeX += template.mapParameters.getWidthScalingIncrement() * scenario.getForceCount();
1917-
mapSizeY += template.mapParameters.getHeightScalingIncrement() * scenario.getForceCount();
1942+
// increment map size by template-specified increments
1943+
mapSizeX += mapParameters.getWidthScalingIncrement() * scenario.getForceCount();
1944+
mapSizeY += mapParameters.getHeightScalingIncrement() * scenario.getForceCount();
1945+
}
19181946

19191947
// 50/50 odds to rotate the map 90 degrees if specified.
1920-
if (template.mapParameters.isAllowRotation()) {
1948+
if (mapParameters.isAllowRotation()) {
19211949
int roll = randomInt(20) + 1;
19221950
if (roll <= 10) {
1923-
int swap = mapSizeX;
1924-
mapSizeX = mapSizeY;
1925-
mapSizeY = swap;
1951+
// Ignore the IDE telling you this is wrong
1952+
scenario.setMapSizeX(mapSizeY);
1953+
scenario.setMapSizeY(mapSizeX);
19261954
}
19271955
}
19281956

@@ -2364,7 +2392,7 @@ private static List<Entity> fillTransport(AtBScenario scenario, Entity transport
23642392

23652393
double bayCapacity = bay.getUnused();
23662394
int remainingCount = (int) max(1,
2367-
Math.floor(bayCapacity / IUnitGenerator.FOOT_PLATOON_INFANTRY_WEIGHT));
2395+
floor(bayCapacity / IUnitGenerator.FOOT_PLATOON_INFANTRY_WEIGHT));
23682396
while (remainingCount > 0) {
23692397

23702398
// Set base random generation parameters

MekHQ/src/mekhq/campaign/mission/AtBScenario.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ private void initBattle(Campaign campaign) {
353353
if (campaignOptions.isUseWeatherConditions()) {
354354
setWeatherConditions(campaignOptions.isUseNoTornadoes());
355355
}
356-
setMapSize();
356+
setMapSize(campaign);
357357
setMapFile();
358358
if (isStandardScenario()) {
359359
forceCount = 1;
@@ -465,7 +465,7 @@ public void setPlanetaryConditions(Mission mission, Campaign campaign) {
465465
}
466466
}
467467

468-
public void setMapSize() {
468+
public void setMapSize(Campaign campaign) {
469469
int roll = Compute.randomInt(20) + 1;
470470
if (roll < 6) {
471471
setMapSizeX(20);

0 commit comments

Comments
 (0)