diff --git a/megamek/data/scenarios/Kell Hounds/DeathOfTheLegion/DeathOfTheLegion.mms b/megamek/data/scenarios/Kell Hounds/DeathOfTheLegion/DeathOfTheLegion.mms
index 810a9602d1d..8e226dacf96 100644
--- a/megamek/data/scenarios/Kell Hounds/DeathOfTheLegion/DeathOfTheLegion.mms
+++ b/megamek/data/scenarios/Kell Hounds/DeathOfTheLegion/DeathOfTheLegion.mms
@@ -33,8 +33,6 @@ map:
modify: rotate
factions:
-#- name: Obser
-
- name: Mek Company, Kell Hounds
camo: Mercs/Kell Hounds.jpg
diff --git a/megamek/data/scenarios/Kell Hounds/LoweringTheBoom/LoweringTheBoom.mms b/megamek/data/scenarios/Kell Hounds/LoweringTheBoom/LoweringTheBoom.mms
index 521949d1b0b..370cd895a40 100644
--- a/megamek/data/scenarios/Kell Hounds/LoweringTheBoom/LoweringTheBoom.mms
+++ b/megamek/data/scenarios/Kell Hounds/LoweringTheBoom/LoweringTheBoom.mms
@@ -51,6 +51,9 @@ factions:
atleast: 4
modify: onlyatend
+ fleefrom:
+ border: north
+
units:
- fullname: Thunderbolt TDR-5S
id: 101
@@ -250,9 +253,6 @@ messages:
as many Meks as possible.
Be careful! Some of your Meks have already sustained damage.
-
- *Technical note: you can currently retreat off any edge of the battlefield and it will count for victory.
- If you do this, Princess will be sad.*
image: loweringboom_map.png
trigger:
type: and
diff --git a/megamek/data/scenarios/Kell Hounds/ToSaveAPrince/ToSaveAPrince.mms b/megamek/data/scenarios/Kell Hounds/ToSaveAPrince/ToSaveAPrince.mms
index c101fd98268..b3754b986d3 100644
--- a/megamek/data/scenarios/Kell Hounds/ToSaveAPrince/ToSaveAPrince.mms
+++ b/megamek/data/scenarios/Kell Hounds/ToSaveAPrince/ToSaveAPrince.mms
@@ -43,6 +43,9 @@ factions:
camo: Draconis Combine/Dieron Regulars/Dieron Regulars.jpg
deploy: N
+ fleefrom:
+ border: south
+
victory:
- trigger:
type: fledunits
@@ -164,6 +167,7 @@ factions:
piloting: 4
gunnery: 5
+# OPFOR -----------------------
- name: Kell Hounds, Second Battalion
camo: Mercs/Kell Hounds.jpg
@@ -321,9 +325,6 @@ messages:
Meks off the southern map edge by the end of round 15.
The temperature in this desert area is at 70°C, adding heat to all Meks.
-
- *Technical note: you can currently retreat off any edge of the battlefield and it will count for victory.
- If you do this, Princess will be sad.*
image: tosaveaprince_map.png
trigger:
type: and
@@ -389,6 +390,7 @@ end:
- trigger:
type: killedunits
units: [ 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112 ]
+ # can't get through with half the force anymore when 7 are killed
atleast: 7
- trigger:
diff --git a/megamek/docs/Scenarios/ScenarioV2 HowTo.mms b/megamek/docs/Scenarios/ScenarioV2 HowTo.mms
index 62eeb3c0a76..e20f5c71a8d 100644
--- a/megamek/docs/Scenarios/ScenarioV2 HowTo.mms
+++ b/megamek/docs/Scenarios/ScenarioV2 HowTo.mms
@@ -188,6 +188,19 @@ factions:
offset: 0
# width is 3 by default
width: 1
+ # OR
+ deploy:
+ # see also area definitions
+ area:
+ union:
+ first:
+ circle:
+ center: [ 10, 10 ]
+ radius: 7
+ second:
+ list:
+ - [2,2]
+ - [5,5]
minefields: # optional, availability depending on game type
- conventional: 2
@@ -491,3 +504,161 @@ trigger:
- type: phasestart
phase: movement
+trigger:
+ # The positions condition is met when the given number(s) of units are in the given area
+ type: positions
+ area:
+ border:
+ edges: north
+ maxdistance: 3
+ # Optional: limit the test to the player's units
+ player: Player A
+ # Optional: a list of units to limit the check to. This makes sense most of time to avoid counting MekWarriors
+ # or other spawns; when giving unit IDs, the player limitation is redundant
+ # It also makes sense to set fixed IDs for all units to make sure this works correctly
+ units: [ 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112 ]
+ # At least the given number of units must be in the given area, can be alone or combined with atmost
+ atleast: 7
+ # At most the given number of units must be in the given area, can be alone or combined with atleast
+ atmost: 10
+ # OR: the exact number of units must be in the given area; this cannot be combined with atmost/atleast
+ count: 2
+
+trigger:
+ # This is a simpler way to write a position condition that is met when the unit is in the given area
+ type: position
+ area:
+ border:
+ edges: north
+ maxdistance: 3
+ # The unit ID to be checked
+ unit: 201
+
+# ###############################################
+# Areas
+# are used to define places on the map. They are either a single shape or a combination of shapes. They are
+# never given as a list, only a single element that is either the shape or the combination type.
+# Areas need not be contiguous
+area:
+ # Combinations are union, difference and intersection (as in "Constructive Solid Geometry")
+ # Each combination requires the "first:" and "second:" area to be given. These are areas in turn, i.e.,
+ # they are themselves either shapes or combinations. In other words, this can be nested to any depth.
+ union:
+ first:
+ # A hex circle (or more like, hex-shape) is all hexes around the center at a distance of at most the
+ # given radius (the circle is filled). To get only the hexes at the distance 7, use a difference
+ # of two circles, the second of radius 6 can be used.
+ circle:
+ center: [ 10, 10 ]
+ radius: 7
+ # In union and intersection, it does not matter which area is first and second. In a difference, the second
+ # area is subtracted from the first, so reversing the two changes the result.
+ second:
+ # A list is simply a list of hex coordinates
+ list:
+ - [2,2]
+ - [5,5]
+
+area:
+ difference:
+ first:
+ # A rectangle is given by its corners. The order of the values does not matter, i.e. the corners can be
+ # upper left and lower right or upper right and lower left in any order. The rectangle is filled and includes
+ # its border
+ rectangle:
+ - [ 2, 2 ]
+ - [ 5, 5 ]
+ second:
+ # Subtracting a smaller rectangle leaves the border of the first rectangle
+ rectangle:
+ - [ 4, 4 ]
+ - [ 3, 3 ]
+
+area:
+ # There are two versions of halfplane
+ # One is cartesian, i.e. vertical or horizontal, i.e. all hexes above, below, to left or to right of a
+ # given coordinate value, including the coordinate (line) itself
+ halfplane:
+ coordinate: 4
+ # The direction the halfplane extends to: above, below, left or right. A toleft halfplane includes all
+ # hexes of x <= coordinate
+ extends: above
+
+ # The other is delimited by a hex row in one of the 3 directions N/S, NE/SW and NW/SE. The plane extends to
+ # either the right or left of that (there is no above/below, as the hex row cannot be horizontal). The
+ # directions are, as always N = 0, SE = 2 ...; opposite directions have the same result
+ halfplane:
+ point: [4,5]
+ direction: 2
+ # The direction the halfplane extends to: above, below, to_left or to_right. A toleft halfplane includes all
+ # hexes of x <= coordinate
+ extends: left
+
+area:
+ intersection:
+ first:
+ # A line along one of the hex row directions (N = 0, SE = 2; opposite directions have the same result)
+ # through one hex; the line is infinite
+ line:
+ point: [ 0, 5 ]
+ direction: 1
+ second:
+ union:
+ first:
+ # A ray along one of the hex row directions (N = 0, SE = 2) starting at a hex; the ray is similar to the
+ # line with the same values but it is cut off at the hex (the ray includes the start hex)
+ ray:
+ point: [ 0, 5 ]
+ direction: 1
+ second:
+ # This area is the north border of the board (all hexes with y = 0)
+ border: north
+
+area:
+ # Two or more borders of the board can be given as a list.
+ # The absolute hexes that these represent depend on
+ # the rectangle that the area is applied to (e.g. the board size)
+ # The east (left) border is all hexes at x = 0; south is all hexes at y = board height; west all hexes at
+ # x = board width
+ border: [ south, east, west ]
+
+area:
+ # for a thicker border or inset border, use the "edges:" node
+ border:
+ edges: [ east, north ]
+ # optional: the minimum distance from the edge; 0 means start at the edge hexes
+ mindistance: 2
+ # optional: the maximum distance from the edge
+ maxdistance: 3
+
+area:
+ # The empty area has no hexes. Can be used to prevent units from fleeing the board
+ empty:
+
+area:
+ # the area can be given as a terrain type
+ terrain:
+ # required: the terrain type to include in the area
+ type: woods
+ # optional: the terrain level to include; when omitted, any terrain level is included
+ level: 1
+ # OR optional: a range of terrain levels to include
+ minlevel: 1
+ maxlevel: 2
+ # optional: the minimum distance from any hex with the terrain; 0 means only the hexes themselves
+ mindistance: 2
+ # optional: the maximum distance from any hex with the terrain
+ # be careful with distances of more than 3 or so on big boards: this leads to exploding calculation times
+ maxdistance: 3
+
+area:
+ # the area can be given as hex levels to include
+ # either a single hex level
+ hexlevel: 0
+
+area:
+ # OR a range
+ hexlevel:
+ minlevel: 1
+ # optional: the maximum hex level
+ maxlevel: 2
diff --git a/megamek/i18n/megamek/client/messages.properties b/megamek/i18n/megamek/client/messages.properties
index b5cdb04be8c..60ae1373333 100644
--- a/megamek/i18n/megamek/client/messages.properties
+++ b/megamek/i18n/megamek/client/messages.properties
@@ -1111,6 +1111,8 @@ CommonMenuBar.viewToggleFovDarkenTooltip=Darkens hexes with no LOS to the
CommonMenuBar.viewToggleIsometric=Isometric View
CommonMenuBar.viewToggleFieldOfFire=Field of Fire
CommonMenuBar.viewToggleFieldOfFireToolTip=Outline firing arcs for selected weapons
+CommonMenuBar.viewToggleFleeZone=Toggle Flee Zone
+CommonMenuBar.viewToggleFleeZoneToolTip=Shows or hides the flee zone of the current unit (the hexes from which it can escape the battlefield)
CommonMenuBar.viewToggleSensorRange=Visual & Sensor Ranges
CommonMenuBar.viewToggleSensorRangeToolTip=Outline Visual & Sensor Ranges
CommonMenuBar.viewToggleFiringSolutions=Firing Solutions
diff --git a/megamek/src/megamek/client/bot/princess/BotGeometry.java b/megamek/src/megamek/client/bot/princess/BotGeometry.java
index 0ea49b734b8..caa2b2b73a9 100644
--- a/megamek/src/megamek/client/bot/princess/BotGeometry.java
+++ b/megamek/src/megamek/client/bot/princess/BotGeometry.java
@@ -108,17 +108,19 @@ public String toString() {
* Coords stores x and y values. Since these are hexes, coordinates with odd x
* values are a half-hex down. Directions work clockwise around the hex,
* starting with zero at the top.
- * -y
- * 0
- * _____
- * 5 / \ 1
- * -x / \ +x
- * \ /
- * 4 \_____/ 2
- * 3
- * +y
+ *
+ * -y
+ * 0
+ * _____
+ * 5 / \ 1
+ * -x / \ +x
+ * \ /
+ * 4 \_____/ 2
+ * 3
+ * +y
+ *
* ------------------------------
- * Direction is stored as above, but the meaning of 'intercept' depends
+ *
Direction is stored as above, but the meaning of 'intercept' depends
* on the direction. For directions 0, 3, intercept means the y=0 intercept
* for directions 1, 2, 4, 5 intercept is the x=0 intercept
*/
@@ -144,6 +146,8 @@ public HexLine(Coords c, int dir) {
* returns -1 if the point is to the left of the line
* +1 if the point is to the right of the line
* and 0 if the point is on the line
+ * Note that this evaluation depends on the "view" direction of this line. The
+ * result is reversed for HexLines of opposite directions, e.g. directions 0 and 3.
*/
public int judgePoint(Coords c) {
HexLine comparor = new HexLine(c, getDirection());
@@ -156,6 +160,24 @@ public int judgePoint(Coords c) {
return 0;
}
+ /**
+ * @return -1 if the point is to the left of the line,
+ * +1 if the point is to the right of the line
+ * and 0 if the point is on the line
+ * Note that this evaluation is independent of the "view" direction of this line. The
+ * result is the same for HexLines of opposite directions, e.g. directions 0 and 3.
+ */
+ public int isAbsoluteLeftOrRight(Coords c) {
+ HexLine comparor = new HexLine(c, getDirection());
+ if (comparor.getIntercept() == getIntercept()) {
+ return 0;
+ } else if (comparor.getIntercept() < getIntercept()) {
+ return ((getDirection() == 2) || (getDirection() == 5)) ? 1 : -1;
+ } else {
+ return ((getDirection() == 2) || (getDirection() == 5)) ? -1 : 1;
+ }
+ }
+
/**
* returns -1 if the area is entirely to the left of the line
* returns +1 if the area is entirely to the right of the line
diff --git a/megamek/src/megamek/client/ui/swing/ClientGUI.java b/megamek/src/megamek/client/ui/swing/ClientGUI.java
index 906a6043246..6f073d5a440 100644
--- a/megamek/src/megamek/client/ui/swing/ClientGUI.java
+++ b/megamek/src/megamek/client/ui/swing/ClientGUI.java
@@ -166,6 +166,7 @@ public class ClientGUI extends AbstractClientGUI implements BoardViewListener,
public static final String VIEW_TOGGLE_HEXCOORDS = "viewToggleHexCoords";
public static final String VIEW_LABELS = "viewLabels";
public static final String VIEW_TOGGLE_FIELD_OF_FIRE = "viewToggleFieldOfFire";
+ public static final String VIEW_TOGGLE_FLEE_ZONE = "viewToggleFleeZone";
public static final String VIEW_TOGGLE_SENSOR_RANGE = "viewToggleSensorRange";
public static final String VIEW_TOGGLE_FOV_DARKEN = "viewToggleFovDarken";
public static final String VIEW_TOGGLE_FOV_HIGHLIGHT = "viewToggleFovHighlight";
@@ -244,6 +245,7 @@ public class ClientGUI extends AbstractClientGUI implements BoardViewListener,
private BoardView bv;
private MovementEnvelopeSpriteHandler movementEnvelopeHandler;
private MovementModifierSpriteHandler movementModifierSpriteHandler;
+ private FleeZoneSpriteHandler fleeZoneSpriteHandler;
private SensorRangeSpriteHandler sensorRangeSpriteHandler;
private CollapseWarningSpriteHandler collapseWarningSpriteHandler;
private GroundObjectSpriteHandler groundObjectSpriteHandler;
@@ -351,6 +353,8 @@ public class ClientGUI extends AbstractClientGUI implements BoardViewListener,
private Coords currentHex;
+ private boolean showFleeZone = false;
+
// endregion Variable Declarations
/**
@@ -509,10 +513,11 @@ private void initializeSpriteHandlers() {
groundObjectSpriteHandler = new GroundObjectSpriteHandler(bv, client.getGame());
firingSolutionSpriteHandler = new FiringSolutionSpriteHandler(bv, client);
firingArcSpriteHandler = new FiringArcSpriteHandler(bv, this);
+ fleeZoneSpriteHandler = new FleeZoneSpriteHandler(bv);
- spriteHandlers.addAll(List.of(movementEnvelopeHandler, movementModifierSpriteHandler,
- sensorRangeSpriteHandler, flareSpritesHandler, collapseWarningSpriteHandler,
- groundObjectSpriteHandler, firingSolutionSpriteHandler, firingArcSpriteHandler));
+ spriteHandlers.addAll(List.of(movementEnvelopeHandler, movementModifierSpriteHandler, sensorRangeSpriteHandler,
+ flareSpritesHandler, collapseWarningSpriteHandler, groundObjectSpriteHandler, firingSolutionSpriteHandler,
+ firingArcSpriteHandler, fleeZoneSpriteHandler));
spriteHandlers.forEach(BoardViewSpriteHandler::initialize);
}
@@ -925,6 +930,9 @@ public void actionPerformed(ActionEvent event) {
GUIP.setShowFieldOfFire(!GUIP.getShowFieldOfFire());
bv.getPanel().repaint();
break;
+ case VIEW_TOGGLE_FLEE_ZONE:
+ toggleFleeZone();
+ break;
case VIEW_TOGGLE_SENSOR_RANGE:
GUIP.setShowSensorRange(!GUIP.getShowSensorRange());
break;
@@ -2217,6 +2225,7 @@ public void gamePhaseChange(GamePhaseChangeEvent e) {
clientGuiPanel.validate();
cb.moveToEnd();
+ hideFleeZone();
}
@Override
@@ -3093,4 +3102,19 @@ public void setCurrentHex(Hex hex) {
public void setCurrentHex(Coords hex) {
currentHex = hex;
}
+
+ private void toggleFleeZone() {
+ showFleeZone = !showFleeZone;
+ if (showFleeZone && unitDisplay.getCurrentEntity() != null) {
+ Game game = client.getGame();
+ fleeZoneSpriteHandler.renewSprites(game.getFleeZone(unitDisplay.getCurrentEntity()).getCoords(game.getBoard()));
+ } else {
+ fleeZoneSpriteHandler.clear();
+ }
+ }
+
+ public void hideFleeZone() {
+ showFleeZone = false;
+ fleeZoneSpriteHandler.clear();
+ }
}
diff --git a/megamek/src/megamek/client/ui/swing/CommonMenuBar.java b/megamek/src/megamek/client/ui/swing/CommonMenuBar.java
index fdd393370c4..6c19d2c52a5 100644
--- a/megamek/src/megamek/client/ui/swing/CommonMenuBar.java
+++ b/megamek/src/megamek/client/ui/swing/CommonMenuBar.java
@@ -142,6 +142,7 @@ public class CommonMenuBar extends JMenuBar implements ActionListener, IPreferen
getString("CommonMenuBar.viewToggleSensorRange"));
private final JCheckBoxMenuItem toggleFieldOfFire = new JCheckBoxMenuItem(
getString("CommonMenuBar.viewToggleFieldOfFire"));
+ private final JMenuItem toggleFleeZone = new JMenuItem(getString("CommonMenuBar.viewToggleFleeZone"));
private final JCheckBoxMenuItem toggleFovHighlight = new JCheckBoxMenuItem(
getString("CommonMenuBar.viewToggleFovHighlight"));
private final JCheckBoxMenuItem toggleFovDarken = new JCheckBoxMenuItem(
@@ -343,6 +344,8 @@ public CommonMenuBar() {
initMenuItem(toggleFieldOfFire, menu, VIEW_TOGGLE_FIELD_OF_FIRE);
toggleFieldOfFire.setSelected(GUIP.getShowFieldOfFire());
toggleFieldOfFire.setToolTipText(Messages.getString("CommonMenuBar.viewToggleFieldOfFireToolTip"));
+ initMenuItem(toggleFleeZone, menu, VIEW_TOGGLE_FLEE_ZONE);
+ toggleFleeZone.setToolTipText(Messages.getString("CommonMenuBar.viewToggleFleeZoneToolTip"));
initMenuItem(toggleFiringSolutions, menu, VIEW_TOGGLE_FIRING_SOLUTIONS);
toggleFiringSolutions.setToolTipText(Messages.getString("CommonMenuBar.viewToggleFiringSolutionsToolTip"));
toggleFiringSolutions.setSelected(GUIP.getShowFiringSolutions());
@@ -547,6 +550,7 @@ private synchronized void updateEnabledStates() {
viewUnitOverview.setEnabled(isInGameBoardView);
toggleSensorRange.setEnabled(isInGameBoardView);
toggleFieldOfFire.setEnabled(isInGameBoardView);
+ toggleFleeZone.setEnabled(isInGameBoardView);
toggleFovHighlight.setEnabled(isInGameBoardView);
toggleFovDarken.setEnabled(isInGameBoardView);
toggleFiringSolutions.setEnabled(isInGameBoardView);
diff --git a/megamek/src/megamek/client/ui/swing/boardview/FleeZoneSpriteHandler.java b/megamek/src/megamek/client/ui/swing/boardview/FleeZoneSpriteHandler.java
new file mode 100644
index 00000000000..6d970156c9b
--- /dev/null
+++ b/megamek/src/megamek/client/ui/swing/boardview/FleeZoneSpriteHandler.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved.
+ *
+ * This file is part of MegaMek.
+ *
+ * MegaMek is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * MegaMek is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MegaMek. If not, see .
+ */
+package megamek.client.ui.swing.boardview;
+
+import megamek.common.Coords;
+
+import java.util.Collection;
+
+public class FleeZoneSpriteHandler extends BoardViewSpriteHandler {
+
+ public FleeZoneSpriteHandler(BoardView boardView) {
+ super(boardView);
+ }
+
+ @Override
+ public void initialize() { }
+
+ @Override
+ public void dispose() {
+ clear();
+ }
+
+ public void renewSprites(Collection coords) {
+ clear();
+ coords.stream().map(c -> new FieldofFireSprite(boardView, 1, c, 63)).forEach(currentSprites::add);
+ boardView.addSprites(currentSprites);
+ }
+}
diff --git a/megamek/src/megamek/client/ui/swing/unitDisplay/UnitDisplay.java b/megamek/src/megamek/client/ui/swing/unitDisplay/UnitDisplay.java
index 9695a4e5646..6a618fc5965 100644
--- a/megamek/src/megamek/client/ui/swing/unitDisplay/UnitDisplay.java
+++ b/megamek/src/megamek/client/ui/swing/unitDisplay/UnitDisplay.java
@@ -464,6 +464,7 @@ public void displayEntity(Entity en) {
updateDisplay();
if (clientgui != null) {
clientgui.clearFieldOfFire();
+ clientgui.hideFleeZone();
}
}
diff --git a/megamek/src/megamek/common/AbstractGame.java b/megamek/src/megamek/common/AbstractGame.java
index 56b38c72cee..5cb724344b1 100644
--- a/megamek/src/megamek/common/AbstractGame.java
+++ b/megamek/src/megamek/common/AbstractGame.java
@@ -36,6 +36,8 @@
import megamek.common.event.GameListener;
import megamek.common.event.GameNewActionEvent;
import megamek.common.force.Forces;
+import megamek.common.hexarea.HexArea;
+import megamek.logging.MMLogger;
import megamek.server.scriptedevent.TriggeredEvent;
/**
@@ -45,6 +47,8 @@
*/
public abstract class AbstractGame implements IGame {
+ private static final MMLogger LOGGER = MMLogger.create(AbstractGame.class);
+
private static final int AWAITING_FIRST_TURN = -1;
/** The players present in the game mapped to their id as key */
@@ -444,4 +448,39 @@ public void reset() {
currentRound = -1;
forces = new Forces(this);
}
+
+ /**
+ * Returns true when the given unit can flee from the given coords, as set either for the unit itself or for its owner.
+ *
+ * @param unit The unit that wants to flee
+ * @param coords The hex coords it wants to flee from
+ * @return True when it can indeed flee
+ */
+ public boolean canFleeFrom(Deployable unit, Coords coords) {
+ if ((unit == null) || (coords == null)) {
+ LOGGER.warn("Received null unit or coords!");
+ return false;
+ } else {
+ return getFleeZone(unit).containsCoords(coords, getBoard());
+ }
+ }
+
+ /**
+ * Returns the {@link HexArea} a given unit can flee from, as set either for the unit itself or for its owner.
+ *
+ * @param unit The unit that wants to flee
+ * @return The area it may flee from
+ */
+ public HexArea getFleeZone(Deployable unit) {
+ if (unit == null) {
+ LOGGER.warn("Received null unit!");
+ return HexArea.EMPTY_AREA;
+ } else if (unit.hasFleeZone()) {
+ return unit.getFleeZone();
+ } else if ((unit instanceof InGameObject inGameObject) && hasPlayer(inGameObject.getOwnerId())) {
+ return getPlayer(inGameObject.getOwnerId()).getFleeZone();
+ } else {
+ return HexArea.EMPTY_AREA;
+ }
+ }
}
diff --git a/megamek/src/megamek/common/Board.java b/megamek/src/megamek/common/Board.java
index a9a134e876c..b63879876b3 100644
--- a/megamek/src/megamek/common/Board.java
+++ b/megamek/src/megamek/common/Board.java
@@ -34,14 +34,16 @@
import megamek.common.enums.BasementType;
import megamek.common.event.BoardEvent;
import megamek.common.event.BoardListener;
+import megamek.common.hexarea.HexArea;
import megamek.common.util.fileUtils.MegaMekFile;
import megamek.logging.MMLogger;
public class Board implements Serializable {
+ @Serial
+ private static final long serialVersionUID = -5744058872091016636L;
private static final MMLogger logger = MMLogger.create(Board.class);
// region Variable Declarations
- private static final long serialVersionUID = -5744058872091016636L;
public static final String BOARD_REQUEST_ROTATION = "rotate:";
@@ -129,8 +131,17 @@ public class Board implements Serializable {
private final int boardId = 0;
+ /**
+ * The board's deployment zones. These may come as terrains from the board file or they may be set by code. The field is
+ * transient as zones can be reconstructed from terrain and the areas field and may have many coords.
+ */
private transient Map> deploymentZones = null;
+ /**
+ * HexAreas that are set by code to be deployment zones.
+ */
+ private final Map areas = new HashMap<>();
+
// endregion Variable Declarations
// region Constructors
@@ -971,9 +982,8 @@ public boolean isLegalDeployment(Coords c, int zoneType, int startingWidth, int
return (c.getX() >= (width / 3)) && (c.getX() <= ((2 * width) / 3)) && (c.getY() >= (height / 3))
&& (c.getY() <= ((2 * height) / 3));
default: // this could signify a custom deployment zone
- Set customDeploymentZone = getCustomDeploymentZone(
- Board.decodeCustomDeploymentZoneID(zoneType));
- return (customDeploymentZone != null) && customDeploymentZone.contains(c);
+ Set customDeploymentZone = getCustomDeploymentZone(decodeCustomDeploymentZoneID(zoneType));
+ return customDeploymentZone.contains(c);
}
}
@@ -1995,8 +2005,7 @@ public static int IntListAsExits(List list) {
}
/**
- * Worker function that initializes any custom deployment zones present on the
- * board
+ * Worker function that initializes any custom deployment zones present on the board
*/
private void initializeDeploymentZones() {
deploymentZones = new HashMap<>();
@@ -2013,12 +2022,31 @@ private void initializeDeploymentZones() {
}
}
}
+ areas.forEach(this::convertDeploymentZone);
+ }
+
+ /**
+ * Converts a custom deployment zone from the hex area definition to board hexes; also translates the ID. Note that the deploymentZones
+ * field must not be null.
+ */
+ private void convertDeploymentZone(int zoneId, HexArea hexArea) {
+ deploymentZones.put(zoneId - NUM_ZONES_X2, hexArea.getCoords(this));
+ }
+
+ /**
+ * Adds a deployment zone with the given ID and the hexes described by the given HexArea to this board, replacing the previously present
+ * zone of that ID, if there had been one. Note that the zone Id can be outside those reachable by board files; e.g. the zone Id can be
+ * 1000. Note however that zone IDs in the range of 0 to 50 should be avoided as they'll overwrite terrain deployment zones.
+ *
+ * @param zoneId The zone Id
+ * @param hexArea The hexes comprising this deployment zone
+ */
+ public void addDeploymentZone(int zoneId, HexArea hexArea) {
+ areas.put(zoneId, hexArea);
}
/**
- * Resets the "intermediate" deployment zones associated with this board, in
- * case
- * the deployment zones change
+ * Resets the "intermediate" deployment zones associated with this board, in case the deployment zones change
*/
public void resetDeploymentZones() {
deploymentZones = null;
@@ -2043,24 +2071,20 @@ public Set getCustomDeploymentZone(int zoneID) {
initializeDeploymentZones();
}
- return deploymentZones.getOrDefault(zoneID, null);
+ return deploymentZones.getOrDefault(zoneID, Set.of());
}
/**
- * Use this method to convert a deployment zone ID as represented in the UI zone
- * selectors
- * (e.g. in the PlayerSettingsDialog) to a deployment zone ID as stored in the
- * board.
+ * Use this method to convert a deployment zone ID as represented in the UI zone selectors (e.g. in the PlayerSettingsDialog) to a
+ * deployment zone ID as stored in the board.
*/
public static int decodeCustomDeploymentZoneID(int zoneID) {
return zoneID - NUM_ZONES_X2;
}
/**
- * Use this method to convert a deployment zone ID as stored in the board to a
- * number
- * suitable for representation in the UI zone selectors (e.g.
- * PlayerSettingsDialog)
+ * Use this method to convert a deployment zone ID as stored in the board to a number suitable for representation in the UI zone
+ * selectors (e.g. PlayerSettingsDialog)
*/
public static int encodeCustomDeploymentZoneID(int zoneID) {
return zoneID + NUM_ZONES_X2;
diff --git a/megamek/src/megamek/common/Deployable.java b/megamek/src/megamek/common/Deployable.java
index 67e42e67c06..34bed081d77 100644
--- a/megamek/src/megamek/common/Deployable.java
+++ b/megamek/src/megamek/common/Deployable.java
@@ -18,18 +18,19 @@
*/
package megamek.common;
+import megamek.common.hexarea.HexArea;
+
/**
- * This interface is implemented by those units (by InGameObjects) that can be deployed either
- * offboard or on a board. There are InGameObjects that are only targets (HexTarget) and may thus not
- * actually be deployable. All Deployable objects could theoretically be listed in the lobby's unit list.
+ * This interface is implemented by those units (by InGameObjects) that can be deployed either offboard or on a board. There are
+ * InGameObjects that are only targets (HexTarget) and may thus not actually be deployable. All Deployable objects could theoretically be
+ * listed in the lobby's unit list.
*/
public interface Deployable {
/**
- * Returns true when this unit/object is deployed, i.e. it has arrived in the game and may
- * perform actions or be targeted by actions. Usually that means it has a fixed position on a board.
- * Offboard units also count as undeployed as long as they cannot perform actions and as deployed when they
- * can.
+ * Returns true when this unit/object is deployed, i.e. it has arrived in the game and may perform actions or be targeted by actions.
+ * Usually that means it has a fixed position on a board. Offboard units also count as undeployed as long as they cannot perform actions
+ * and as deployed when they can.
*/
boolean isDeployed();
@@ -37,4 +38,22 @@ public interface Deployable {
* Returns the round that this unit/object is to be deployed on the board or offboard.
*/
int getDeployRound();
+
+ /**
+ * @return True if this unit has its own area it is allowed to flee the board(s) from; false if the unit's owner should be asked
+ * instead.
+ */
+ default boolean hasFleeZone() {
+ return false;
+ }
+
+ /**
+ * @return The area of the board(s) this unit is allowed to flee from; the return value is only valid when {@link #hasFleeZone()}
+ * returns true. Normally this method should not be called, use {@link AbstractGame#canFleeFrom(Deployable, Coords)} instead.
+ * @see AbstractGame#canFleeFrom(Deployable, Coords)
+ * @see #hasFleeZone()
+ */
+ default HexArea getFleeZone() {
+ return HexArea.EMPTY_AREA;
+ }
}
diff --git a/megamek/src/megamek/common/Entity.java b/megamek/src/megamek/common/Entity.java
index 21274d7a14b..bbff056fd0f 100644
--- a/megamek/src/megamek/common/Entity.java
+++ b/megamek/src/megamek/common/Entity.java
@@ -54,6 +54,7 @@
import megamek.common.equipment.WeaponMounted;
import megamek.common.event.GameEntityChangeEvent;
import megamek.common.force.Force;
+import megamek.common.hexarea.HexArea;
import megamek.common.icons.Camouflage;
import megamek.common.jacksonadapters.EntityDeserializer;
import megamek.common.options.GameOptions;
@@ -910,6 +911,9 @@ public abstract class Entity extends TurnOrdered implements Transporter, Targeta
*/
protected Base64Image icon = new Base64Image();
+ private boolean hasFleeZone = false;
+ private HexArea fleeZone = HexArea.EMPTY_AREA;
+
/**
* Generates a new, blank, entity.
*/
@@ -9591,9 +9595,7 @@ public boolean canFlee() {
&& !isShutDown()
&& !getCrew().isUnconscious()
&& (getSwarmTargetId() == NONE)
- && (isOffBoard() || ((pos != null)
- && ((pos.getX() == 0) || (pos.getX() == (getGame().getBoard().getWidth() - 1))
- || (pos.getY() == 0) || (pos.getY() == (getGame().getBoard().getHeight() - 1)))));
+ && (isOffBoard() || ((pos != null) && game.canFleeFrom(this, pos)));
}
public void setEverSeenByEnemy(boolean b) {
@@ -15825,4 +15827,34 @@ && getCrew().hasEdgeRemaining()
public boolean hasFlotationHull() {
return hasWorkingMisc(MiscType.F_FLOTATION_HULL);
}
+
+ @Override
+ public boolean hasFleeZone() {
+ return hasFleeZone;
+ }
+
+ @Override
+ public HexArea getFleeZone() {
+ return fleeZone;
+ }
+
+ /**
+ * Sets the board area this unit may flee from. The area may be empty, in which case the unit may not flee. Also sets this unit to know
+ * that it has a flee zone and the owning player should not be asked to provide this information.
+ *
+ * @param fleeZone The new flee zone
+ */
+ public void setFleeZone(HexArea fleeZone) {
+ this.fleeZone = fleeZone;
+ hasFleeZone = true;
+ }
+
+ /**
+ * Resets the flee information this unit has. After calling this method, the unit will no longer consider to have its own flee area; the
+ * game will refer to the unit's owner to see if it can flee from a hex.
+ */
+ public void removeFleeZone() {
+ fleeZone = HexArea.EMPTY_AREA;
+ hasFleeZone = false;
+ }
}
diff --git a/megamek/src/megamek/common/IGame.java b/megamek/src/megamek/common/IGame.java
index 82d1384aed3..fbfb88b1951 100644
--- a/megamek/src/megamek/common/IGame.java
+++ b/megamek/src/megamek/common/IGame.java
@@ -204,6 +204,14 @@ default boolean shouldSkipCurrentPhase() {
@Nullable
Player getPlayer(int id);
+ /**
+ * @param id A player ID
+ * @return True when there is a player for the given ID
+ */
+ default boolean hasPlayer(int id) {
+ return getPlayer(id) != null;
+ }
+
/**
* @return The current players as a list. Implementations should make sure that
* this list can be safely modified.
diff --git a/megamek/src/megamek/common/Player.java b/megamek/src/megamek/common/Player.java
index 7be0dce0504..0ebc0e2abb5 100644
--- a/megamek/src/megamek/common/Player.java
+++ b/megamek/src/megamek/common/Player.java
@@ -25,6 +25,8 @@
import java.util.Vector;
import megamek.client.ui.swing.util.PlayerColour;
+import megamek.common.hexarea.BorderHexArea;
+import megamek.common.hexarea.HexArea;
import megamek.common.icons.Camouflage;
import megamek.common.options.OptionsConstants;
@@ -102,6 +104,8 @@ public final class Player extends TurnOrdered {
//Voting should not be stored in save game so marked transient
private transient boolean votedToAllowTeamChange = false;
private transient boolean votedToAllowGameMaster = false;
+
+ private HexArea fleeArea = new BorderHexArea(true, true, true, true);
//endregion Variable Declarations
//region Constructors
@@ -731,4 +735,22 @@ public Player copy() {
return copy;
}
+
+ /**
+ * @return The area of the board this player's units are allowed to flee from; An empty area as return value means they
+ * may not flee at all.
+ */
+ public HexArea getFleeZone() {
+ return fleeArea;
+ }
+
+ /**
+ * Sets the board area this player's units may flee from. The area may be empty, in which case the units may not flee.
+ *
+ * @param fleeArea The new flee area.
+ * @see megamek.common.hexarea.BorderHexArea
+ */
+ public void setFleeZone(HexArea fleeArea) {
+ this.fleeArea = fleeArea;
+ }
}
diff --git a/megamek/src/megamek/common/hexarea/AbstractHexArea.java b/megamek/src/megamek/common/hexarea/AbstractHexArea.java
new file mode 100644
index 00000000000..7c523a850b6
--- /dev/null
+++ b/megamek/src/megamek/common/hexarea/AbstractHexArea.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved.
+ *
+ * This file is part of MegaMek.
+ *
+ * MegaMek is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * MegaMek is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MegaMek. If not, see .
+ */
+package megamek.common.hexarea;
+
+import megamek.common.Board;
+import megamek.common.Coords;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * This is a base class for HexAreas that provides an implementation for {@link #getCoords(Board)}. The {@link #isSmall()} method can be
+ * overridden when a shape has not too many hexes and is independent of the board. A HexArea composed of only small shapes can be evaluated
+ * quickly even on a big board.
+ */
+abstract class AbstractHexArea implements HexArea {
+
+ /**
+ * @return True if this shape is, by itself, finite and small enough and absolute (independent of a board) that its coords can be given
+ * directly. If false, its coords cannot be retrieved, only {@link #containsCoords(Coords, Board)} can be used. Always call this method
+ * and only if it returns true, call {@link #getCoords()}.
+ * @apiNote By default, this method returns false. It may be overridden to return true for finite, small shapes, such as a hex circle of
+ * diameter 4. In that case, getCoords must also be overriden to return the coords of this shape.
+ */
+ boolean isSmall() {
+ // Some shapes, even if finite, have 10000 or more Coords. It may be good to avoid retrieving
+ // those to find the resulting Coords on a small board.
+ // On the other hand, the board may have 10000 hexes and this shape may only have a handful,
+ // making it better to process these coords directly rather than cycle the whole board.
+ // This method exists so both cases can be dealt with as efficiently as possible.
+ return false;
+ }
+
+ /**
+ * Returns all coords of this shape, if it is finite and small enough and an absolute shape. Only use this when {@link #isSmall()}
+ * returns true - it will throw an exception otherwise.
+ *
+ * @return All Coords of this shape
+ * @throws IllegalStateException when this method is called on a shape where {@link #isSmall()} returns false
+ * @apiNote Throws an exception by default. Override together with {@link #isSmall()} for small board-independent shapes.
+ */
+ Set getCoords() {
+ throw new IllegalStateException("Can only be used on small, finite shapes.");
+ }
+
+ @Override
+ public final Set getCoords(Board board) {
+ if (isSmall()) {
+ return getCoords().stream().filter(board::contains).collect(Collectors.toSet());
+ } else {
+ Set result = new HashSet<>();
+ for (int y = 0; y < board.getHeight(); y++) {
+ for (int x = 0; x < board.getWidth(); x++) {
+ Coords coords = new Coords(x, y);
+ if (containsCoords(coords, board)) {
+ result.add(coords);
+ }
+ }
+ }
+ return result;
+ }
+ }
+}
diff --git a/megamek/src/megamek/common/hexarea/BorderHexArea.java b/megamek/src/megamek/common/hexarea/BorderHexArea.java
new file mode 100644
index 00000000000..3ada2d1d42a
--- /dev/null
+++ b/megamek/src/megamek/common/hexarea/BorderHexArea.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved.
+ *
+ * This file is part of MegaMek.
+ *
+ * MegaMek is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * MegaMek is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MegaMek. If not, see .
+ */
+package megamek.common.hexarea;
+
+import megamek.common.Board;
+import megamek.common.Coords;
+
+/**
+ * This class represents one or more of the edge of any given board. It is a relative shape, i.e. its hexes depend on the
+ * board that the HexArea is given when checking its coords.
+ */
+public class BorderHexArea extends AbstractHexArea {
+
+ private final boolean north;
+ private final boolean south;
+ private final boolean east;
+ private final boolean west;
+ private final int minInset;
+ private final int maxInset;
+
+ /**
+ * Creates a border hex area for the given edges of the board. minInset gives the minimum distance from the board edge; 0 means the edge
+ * itself. maxInset gives the maximum distance from the board edge; 0 means the edge itself. maxInset is always set to at least the
+ * value of minInset. Negative values are set to 0.
+ *
+ * @param north When true, includes the hexes of the upper board edge (at y = 0)
+ * @param south When true, includes the hexes of the lower board edge (at y = board height)
+ * @param west When true, includes the hexes of the left board edge (at x = 0)
+ * @param east When true, includes the hexes of the right board edge (at x = board width)
+ * @param minInset the distance from the edges the area begins
+ * @param maxInset the distance from the edges the area ends
+ */
+ public BorderHexArea(boolean north, boolean south, boolean east, boolean west, int minInset, int maxInset) {
+ this.north = north;
+ this.south = south;
+ this.east = east;
+ this.west = west;
+ this.minInset = Math.max(minInset, 0);
+ this.maxInset = Math.max(minInset, maxInset);
+ }
+
+ /**
+ * Creates a border hex area for the given edges of the board.
+ *
+ * @param north When true, includes the hexes of the upper board edge (at y = 0)
+ * @param south When true, includes the hexes of the lower board edge (at y = board height)
+ * @param west When true, includes the hexes of the left board edge (at x = 0)
+ * @param east When true, includes the hexes of the right board edge (at x = board width)
+ */
+ public BorderHexArea(boolean north, boolean south, boolean east, boolean west) {
+ this(north, south, east, west, 0, 0);
+ }
+
+ @Override
+ public boolean containsCoords(Coords coords, Board board) {
+ return (north && (coords.getY() >= minInset) && (coords.getY() <= maxInset))
+ || (west && (coords.getX() >= minInset) && (coords.getX() <= maxInset))
+ || (south && (coords.getY() <= board.getHeight() - 1 - minInset) && (coords.getY() >= board.getHeight() - 1 - maxInset))
+ || (east && (coords.getX() <= board.getWidth() - 1 - minInset) && (coords.getX() >= board.getWidth() - 1 - maxInset));
+ }
+}
diff --git a/megamek/src/megamek/common/hexarea/CircleHexArea.java b/megamek/src/megamek/common/hexarea/CircleHexArea.java
new file mode 100644
index 00000000000..15839071d38
--- /dev/null
+++ b/megamek/src/megamek/common/hexarea/CircleHexArea.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved.
+ *
+ * This file is part of MegaMek.
+ *
+ * MegaMek is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * MegaMek is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MegaMek. If not, see .
+ */
+package megamek.common.hexarea;
+
+import megamek.common.Board;
+import megamek.common.Coords;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * This class represents a hex area that is a filled "circle" around a center (all hexes up to a given maximum distance).
+ */
+public class CircleHexArea extends AbstractHexArea {
+
+ private final Coords center;
+ private final int radius;
+
+ /**
+ * Creates a hex circle around the given center with the given radius. The circle includes all hexes within (it is filled). A radius of
+ * 0 is the center only, a radius of 1 includes the hexes adjacent to the center (7 hexes all in all).
+ *
+ * @param center The center coords
+ * @param radius The radius of the circle
+ */
+ public CircleHexArea(Coords center, int radius) {
+ this.center = center;
+ this.radius = radius;
+ }
+
+ @Override
+ public boolean containsCoords(Coords coords, Board board) {
+ return (coords != null) && (coords.distance(center) <= radius);
+ }
+
+ @Override
+ public boolean isSmall() {
+ return radius < 15;
+ }
+
+ @Override
+ public Set getCoords() {
+ if (isSmall()) {
+ return new HashSet<>(center.allAtDistanceOrLess(radius));
+ } else {
+ return super.getCoords();
+ }
+ }
+}
diff --git a/megamek/src/megamek/common/hexarea/EmptyHexArea.java b/megamek/src/megamek/common/hexarea/EmptyHexArea.java
new file mode 100644
index 00000000000..148b2b66dbe
--- /dev/null
+++ b/megamek/src/megamek/common/hexarea/EmptyHexArea.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved.
+ *
+ * This file is part of MegaMek.
+ *
+ * MegaMek is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * MegaMek is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MegaMek. If not, see .
+ */
+package megamek.common.hexarea;
+
+import megamek.common.Board;
+import megamek.common.Coords;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * This HexArea has no coords at all. It can be used as a placeholder to avoid null values.
+ */
+public class EmptyHexArea extends AbstractHexArea {
+
+ @Override
+ public boolean containsCoords(Coords coords, Board board) {
+ return false;
+ }
+
+ @Override
+ public boolean isSmall() {
+ return true;
+ }
+
+ @Override
+ public Set getCoords() {
+ return new HashSet<>();
+ }
+}
diff --git a/megamek/src/megamek/common/hexarea/HalfPlaneHexArea.java b/megamek/src/megamek/common/hexarea/HalfPlaneHexArea.java
new file mode 100644
index 00000000000..9198d76d8ab
--- /dev/null
+++ b/megamek/src/megamek/common/hexarea/HalfPlaneHexArea.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved.
+ *
+ * This file is part of MegaMek.
+ *
+ * MegaMek is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * MegaMek is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MegaMek. If not, see .
+ */
+package megamek.common.hexarea;
+
+import megamek.common.Board;
+import megamek.common.Coords;
+
+/**
+ * This class represents a half plane shape. The plane is delimited by the given coordinate, which is either a hex column or row (x or y)
+ * depending on the halfPlaneDirection given. A half plane with coordinate 5 and direction ABOVE extends from Coords (x, 5) upwards, i.e.
+ * (0, 0) is within that plane, (0, 10) is not. The given coordinate itself is part of the half plane.
+ */
+public class HalfPlaneHexArea extends AbstractHexArea {
+
+ public enum HalfPlaneType {ABOVE, BELOW, RIGHT, LEFT}
+
+ private final int coordinate;
+ private final HalfPlaneType halfPlaneDirection;
+
+ /**
+ * Creates a half plane shape. The plane is delimited by the given coordinate, which is either a hex column or row (x or y) depending on
+ * the halfPlaneDirection given. A half plane with coordinate 5 and direction ABOVE extends from Coords (x, 5) upwards, i.e. (0, 0) is
+ * within that plane, (0, 10) is not. The given coordinate itself is part of the half plane.
+ *
+ * @param coordinate The x or y value where the half plane starts/ends
+ * @param halfPlaneDirection The direction in which the half plane extends
+ */
+ public HalfPlaneHexArea(int coordinate, HalfPlaneType halfPlaneDirection) {
+ this.coordinate = coordinate;
+ this.halfPlaneDirection = halfPlaneDirection;
+ }
+
+ @Override
+ public boolean containsCoords(Coords coords, Board board) {
+ return switch (halfPlaneDirection) {
+ case ABOVE -> coords.getY() <= coordinate;
+ case BELOW -> coords.getY() >= coordinate;
+ case RIGHT -> coords.getX() >= coordinate;
+ case LEFT -> coords.getX() <= coordinate;
+ };
+ }
+}
diff --git a/megamek/src/megamek/common/hexarea/HexArea.java b/megamek/src/megamek/common/hexarea/HexArea.java
new file mode 100644
index 00000000000..0b4a1bed5b0
--- /dev/null
+++ b/megamek/src/megamek/common/hexarea/HexArea.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved.
+ *
+ * This file is part of MegaMek.
+ *
+ * MegaMek is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * MegaMek is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MegaMek. If not, see .
+ */
+package megamek.common.hexarea;
+
+import megamek.common.*;
+import megamek.common.annotations.Nullable;
+import megamek.server.trigger.UnitPositionTrigger;
+
+import java.io.Serializable;
+import java.util.Set;
+
+/**
+ * This class represents an area composed of hexes. The area can be a basic shape or be defined by adding, subtracting or intersecting basic
+ * shapes. Areas can be used to define deployment zones in code using {@link Board#addDeploymentZone(int, HexArea)}, to set a zone where
+ * units may flee the board from in {@link Entity#setFleeZone(HexArea)} and in positional triggers for events
+ * ({@link UnitPositionTrigger}).
+ * Note:
+ *
- A HexArea can be empty if its shapes result in no valid hexes;
+ *
- A HexArea can be infinite; therefore, its hexes can only be retrieved by limiting the results to a Board;
+ *
- A HexArea can be absolute (independent of the board's size and contents) or relative to the board;
+ *
- A HexArea can appear empty when its shapes do not contain any hexes within the given board;
+ *
- A HexArea does not have to be contiguous;
+ *
- HexAreas are typically lightweight as they don't store their hexes (unless ListHexArea is misused to store thousands of hexes),
+ * only the rules to create the hexes;
+ *
HexArea is immutable.
+ *
Note that the shape can have any complexity by being itself constructed from other shapes. For example, the intersection of two
+ * circles can be created by calling
+ *
{@code
+ * new HexAreaIntersection(
+ * new HexCircleShape(new Coords(20, 5), 14),
+ * new HexCircleShape(new Coords(0, 5), 14));}
+ *
+ * @see HexAreaUnion
+ * @see HexAreaDifference
+ * @see HexAreaIntersection
+ * @see BorderHexArea
+ */
+public interface HexArea extends Serializable {
+
+ /**
+ * This area can be used whenever an empty area is required.
+ */
+ HexArea EMPTY_AREA = new EmptyHexArea();
+
+ /**
+ * Returns true if this shape contains the given coords. Returns false when the given coords is null. If this shape is absolute, i.e.
+ * does not depend on parameters outside itself, the board does not matter. Some shapes however may be relative to the board size, e.g.
+ * a shape that returns the borders of the board; or even board contents, such as terrain.
+ *
+ * @param coords The coords that are tested if they are part of this shape
+ * @param board The board to limit the area coords to
+ * @return True if this shape contains the coords
+ */
+ boolean containsCoords(@Nullable Coords coords, Board board);
+
+ /**
+ * Returns a set of the coords of this area that are part of the given board.
+ *
+ * @param board The board to limit the results to
+ * @return Coords of this shape that lie on the board
+ */
+ Set getCoords(Board board);
+}
diff --git a/megamek/src/megamek/common/hexarea/HexAreaDifference.java b/megamek/src/megamek/common/hexarea/HexAreaDifference.java
new file mode 100644
index 00000000000..b636fef903e
--- /dev/null
+++ b/megamek/src/megamek/common/hexarea/HexAreaDifference.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved.
+ *
+ * This file is part of MegaMek.
+ *
+ * MegaMek is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * MegaMek is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MegaMek. If not, see .
+ */
+package megamek.common.hexarea;
+
+import megamek.common.Board;
+import megamek.common.Coords;
+
+import java.util.Set;
+
+/**
+ * This class represents the subtraction (difference) of two HexAreaShapes. The order of the two given shapes is relevant.
+ */
+public class HexAreaDifference extends AbstractHexArea {
+
+ private final HexArea firstShape;
+ private final HexArea secondShape;
+
+ /**
+ * Creates an area that is the difference between the two given hex areas, firstShape - secondShape.
+ *
+ * @param firstShape The first of the two shapes; the ordering of the shapes is relevant
+ * @param secondShape The second of the two shapes
+ */
+ public HexAreaDifference(HexArea firstShape, HexArea secondShape) {
+ this.firstShape = firstShape;
+ this.secondShape = secondShape;
+ }
+
+ @Override
+ public boolean containsCoords(Coords coords, Board board) {
+ return firstShape.containsCoords(coords, board) && !secondShape.containsCoords(coords, board);
+ }
+
+ @Override
+ public boolean isSmall() {
+ return (firstShape instanceof AbstractHexArea firstAbstractHexArea) && firstAbstractHexArea.isSmall()
+ && (secondShape instanceof AbstractHexArea secondAbstractHexArea) && secondAbstractHexArea.isSmall();
+ }
+
+ @Override
+ public Set getCoords() {
+ if (isSmall()) {
+ Set result = ((AbstractHexArea) firstShape).getCoords();
+ result.removeAll(((AbstractHexArea) secondShape).getCoords());
+ return result;
+ } else {
+ return super.getCoords();
+ }
+ }
+}
diff --git a/megamek/src/megamek/common/hexarea/HexAreaIntersection.java b/megamek/src/megamek/common/hexarea/HexAreaIntersection.java
new file mode 100644
index 00000000000..594a0badf63
--- /dev/null
+++ b/megamek/src/megamek/common/hexarea/HexAreaIntersection.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved.
+ *
+ * This file is part of MegaMek.
+ *
+ * MegaMek is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * MegaMek is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MegaMek. If not, see .
+ */
+package megamek.common.hexarea;
+
+import megamek.common.Board;
+import megamek.common.Coords;
+
+import java.util.Set;
+
+/**
+ * This class represents the intersection of two HexAreaShapes.
+ */
+public class HexAreaIntersection extends AbstractHexArea {
+
+ private final HexArea firstShape;
+ private final HexArea secondShape;
+
+ /**
+ * Creates an intersection of the two given shapes.
+ *
+ * @param firstShape The first of the two shapes; the ordering of the shapes is not relevant
+ * @param secondShape The second of the two shapes
+ */
+ public HexAreaIntersection(HexArea firstShape, HexArea secondShape) {
+ this.firstShape = firstShape;
+ this.secondShape = secondShape;
+ }
+
+ @Override
+ public boolean containsCoords(Coords coords, Board board) {
+ return firstShape.containsCoords(coords,board) && secondShape.containsCoords(coords, board);
+ }
+
+ @Override
+ public boolean isSmall() {
+ return (firstShape instanceof AbstractHexArea firstAbstractHexArea) && firstAbstractHexArea.isSmall()
+ && (secondShape instanceof AbstractHexArea secondAbstractHexArea) && secondAbstractHexArea.isSmall();
+ }
+
+ @Override
+ public Set getCoords() {
+ if (isSmall()) {
+ Set result = ((AbstractHexArea) firstShape).getCoords();
+ result.retainAll(((AbstractHexArea) secondShape).getCoords());
+ return result;
+ } else {
+ return super.getCoords();
+ }
+ }
+}
diff --git a/megamek/src/megamek/common/hexarea/HexAreaUnion.java b/megamek/src/megamek/common/hexarea/HexAreaUnion.java
new file mode 100644
index 00000000000..35ade178dec
--- /dev/null
+++ b/megamek/src/megamek/common/hexarea/HexAreaUnion.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved.
+ *
+ * This file is part of MegaMek.
+ *
+ * MegaMek is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * MegaMek is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MegaMek. If not, see .
+ */
+package megamek.common.hexarea;
+
+import megamek.common.Board;
+import megamek.common.Coords;
+
+import java.util.Set;
+
+/**
+ * This class represents the addition (union) of two HexAreaShapes.
+ */
+public class HexAreaUnion extends AbstractHexArea {
+
+ private final HexArea firstShape;
+ private final HexArea secondShape;
+
+ /**
+ * Creates an addition (union) of two given shapes.
+ *
+ * @param firstShape The first of the two shapes; the ordering of the shapes is not relevant
+ * @param secondShape The second of the two shapes
+ */
+ public HexAreaUnion(HexArea firstShape, HexArea secondShape) {
+ this.firstShape = firstShape;
+ this.secondShape = secondShape;
+ }
+
+ @Override
+ public boolean containsCoords(Coords coords, Board board) {
+ return firstShape.containsCoords(coords, board) || secondShape.containsCoords(coords, board);
+ }
+
+ @Override
+ public boolean isSmall() {
+ return (firstShape instanceof AbstractHexArea firstAbstractHexArea) && firstAbstractHexArea.isSmall()
+ && (secondShape instanceof AbstractHexArea secondAbstractHexArea) && secondAbstractHexArea.isSmall();
+ }
+
+ @Override
+ public Set getCoords() {
+ if (isSmall()) {
+ Set result = ((AbstractHexArea) firstShape).getCoords();
+ result.addAll(((AbstractHexArea) secondShape).getCoords());
+ return result;
+ } else {
+ return super.getCoords();
+ }
+ }
+}
diff --git a/megamek/src/megamek/common/hexarea/HexLevelArea.java b/megamek/src/megamek/common/hexarea/HexLevelArea.java
new file mode 100644
index 00000000000..ea62c151fd4
--- /dev/null
+++ b/megamek/src/megamek/common/hexarea/HexLevelArea.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved.
+ *
+ * This file is part of MegaMek.
+ *
+ * MegaMek is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * MegaMek is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MegaMek. If not, see .
+ */
+package megamek.common.hexarea;
+
+import megamek.common.Board;
+import megamek.common.Coords;
+
+/**
+ * This area is defined by the hex level (aka floor height) being in the range defined by the given level(s).
+ */
+public class HexLevelArea extends AbstractHexArea {
+
+ private final int minLevel;
+ private final int maxLevel;
+
+ /**
+ * Creates an area defined by the hex level (aka floor height) being in the range defined by the given levels. maxLevel must be equal or
+ * greater than minLevel or the area is empty.
+ *
+ * @param minLevel The minimum hex level to include in the area
+ * @param maxLevel The maximum hex level to include in the area
+ */
+ public HexLevelArea(int minLevel, int maxLevel) {
+ this.minLevel = minLevel;
+ this.maxLevel = maxLevel;
+ }
+
+ /**
+ * Creates an area defined by the hex level (aka floor height) being equal to the given level.
+ *
+ * @param level The hex level to include in the area
+ */
+ public HexLevelArea(int level) {
+ this(level, level);
+ }
+
+ @Override
+ public boolean containsCoords(Coords coords, Board board) {
+ return board.contains(coords) && (board.getHex(coords).getLevel() >= minLevel) && (board.getHex(coords).getLevel() <= maxLevel);
+ }
+}
diff --git a/megamek/src/megamek/common/hexarea/LineHexArea.java b/megamek/src/megamek/common/hexarea/LineHexArea.java
new file mode 100644
index 00000000000..d9ecb1d0086
--- /dev/null
+++ b/megamek/src/megamek/common/hexarea/LineHexArea.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved.
+ *
+ * This file is part of MegaMek.
+ *
+ * MegaMek is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * MegaMek is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MegaMek. If not, see .
+ */
+package megamek.common.hexarea;
+
+import megamek.common.Board;
+import megamek.common.Coords;
+
+/**
+ * This class represents a line of hexes through the given point in the given hex row direction. The direction must be between 0 and 5.
+ * Opposite directions are equal, e.g. directions 1 and 4 result in the same line.
+ */
+public class LineHexArea extends AbstractHexArea {
+
+ private final Coords point;
+ private final int direction;
+
+ /**
+ * Creates a line of hexes through the given point in the given direction. The direction must be between 0 and 5. Opposite directions
+ * are equal, e.g. directions 1 and 4 result in the same line.
+ *
+ * @param point A hex that this line goes through
+ * @param direction The direction of the line, 0 = N, 2 = SE ...
+ */
+ public LineHexArea(Coords point, int direction) {
+ this.point = point;
+ this.direction = direction;
+ }
+
+ @Override
+ public boolean containsCoords(Coords coords, Board board) {
+ return (direction >= 0) && (direction <= 5)
+ && (point.equals(coords)
+ || point.isOnHexRow(direction, coords)
+ || point.isOnHexRow((direction + 3) % 6, coords));
+ }
+}
diff --git a/megamek/src/megamek/common/hexarea/ListHexArea.java b/megamek/src/megamek/common/hexarea/ListHexArea.java
new file mode 100644
index 00000000000..5f6e67f3380
--- /dev/null
+++ b/megamek/src/megamek/common/hexarea/ListHexArea.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved.
+ *
+ * This file is part of MegaMek.
+ *
+ * MegaMek is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * MegaMek is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MegaMek. If not, see .
+ */
+package megamek.common.hexarea;
+
+import megamek.common.Board;
+import megamek.common.Coords;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * This class represents a shape formed by a given list of one or more Coords.
+ */
+public class ListHexArea extends AbstractHexArea {
+
+ private final Set coordList = new HashSet<>();
+
+ /**
+ * Creates a shape containing the given coords. The coords in the list need not be contiguous.
+ *
+ * @param coordList A collection of coords to form the shape
+ */
+ public ListHexArea(Collection coordList) {
+ this.coordList.addAll(coordList);
+ }
+
+ /**
+ * Creates a shape containing the given coord(s). The coords in the list need not be contiguous.
+ *
+ * @param coords A coord to form the shape
+ * @param moreCoords optional further coords to form the shape
+ */
+ public ListHexArea(Coords coords, Coords... moreCoords) {
+ coordList.add(coords);
+ if ((moreCoords != null) && moreCoords.length > 0) {
+ coordList.addAll(Arrays.asList(moreCoords));
+ }
+ }
+
+ @Override
+ public boolean containsCoords(Coords coords, Board board) {
+ return coordList.contains(coords);
+ }
+
+ @Override
+ public boolean isSmall() {
+ return coordList.size() < 1000;
+ }
+
+ @Override
+ public Set getCoords() {
+ return new HashSet<>(coordList);
+ }
+}
diff --git a/megamek/src/megamek/common/hexarea/RayHexArea.java b/megamek/src/megamek/common/hexarea/RayHexArea.java
new file mode 100644
index 00000000000..41ca3922b18
--- /dev/null
+++ b/megamek/src/megamek/common/hexarea/RayHexArea.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved.
+ *
+ * This file is part of MegaMek.
+ *
+ * MegaMek is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * MegaMek is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MegaMek. If not, see .
+ */
+package megamek.common.hexarea;
+
+import megamek.common.Board;
+import megamek.common.Coords;
+
+/**
+ * This class represents a line of hexes starting at the given point and extending in the given hex row direction. The direction must be
+ * between 0 and 5. Contrary to HexLineShape, this shape does not extend in both directions, so opposite directions result in different
+ * rays.
+ */
+public class RayHexArea extends AbstractHexArea {
+
+ private final Coords point;
+ private final int direction;
+
+ /**
+ * Creates a line of hexes starting at the given point in the given direction. The direction must be between 0 and 5.
+ *
+ * @param point A hex that this line goes through
+ * @param direction The direction of the line, 0 = N, 2 = SE ...
+ */
+ public RayHexArea(Coords point, int direction) {
+ this.point = point;
+ this.direction = direction;
+ }
+
+ @Override
+ public boolean containsCoords(Coords coords, Board board) {
+ return (direction >= 0) && (direction <= 5)
+ && (point.equals(coords) || point.isOnHexRow(direction, coords));
+ }
+}
diff --git a/megamek/src/megamek/common/hexarea/RectangleHexArea.java b/megamek/src/megamek/common/hexarea/RectangleHexArea.java
new file mode 100644
index 00000000000..545b596e791
--- /dev/null
+++ b/megamek/src/megamek/common/hexarea/RectangleHexArea.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved.
+ *
+ * This file is part of MegaMek.
+ *
+ * MegaMek is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * MegaMek is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MegaMek. If not, see .
+ */
+package megamek.common.hexarea;
+
+import megamek.common.Board;
+import megamek.common.Coords;
+
+/**
+ * This class represents a rectangle shape. The rectangle includes the corner coordinates.
+ */
+public class RectangleHexArea extends AbstractHexArea {
+
+ private final int x1;
+ private final int y1;
+ private final int x2;
+ private final int y2;
+
+ /**
+ * Creates a rectangle shape. The rectangle includes the corner coordinates. The coordinates do not have to be sorted, i.e. x1 > x2 and
+ * x1 < x2 have the same result. When x1 = x2 and/or y1 = y2, the rectangle consists of a single line or single hex.
+ *
+ * @param x1 The first x corner coordinate
+ * @param x2 The second x corner coordinate
+ * @param y1 The first y corner coordinate
+ * @param y2 The second y corner coordinate
+ */
+
+ public RectangleHexArea(int x1, int y1, int x2, int y2) {
+ this.x1 = Math.min(x1, x2);
+ this.x2 = Math.max(x1, x2);
+ this.y1 = Math.min(y1, y2);
+ this.y2 = Math.max(y1, y2);
+ }
+
+ @Override
+ public boolean containsCoords(Coords coords, Board board) {
+ return (coords.getX() >= x1) && (coords.getX() <= x2) && (coords.getY() >= y1) && (coords.getY() <= y2);
+ }
+}
diff --git a/megamek/src/megamek/common/hexarea/RowHalfPlaneHexArea.java b/megamek/src/megamek/common/hexarea/RowHalfPlaneHexArea.java
new file mode 100644
index 00000000000..13474ba3697
--- /dev/null
+++ b/megamek/src/megamek/common/hexarea/RowHalfPlaneHexArea.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved.
+ *
+ * This file is part of MegaMek.
+ *
+ * MegaMek is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * MegaMek is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MegaMek. If not, see .
+ */
+
+package megamek.common.hexarea;
+
+import megamek.client.bot.princess.BotGeometry;
+import megamek.common.Board;
+import megamek.common.Coords;
+
+import java.util.Objects;
+
+/**
+ * This class represents a half plane shape. The plane is delimited by a hex line in any hex row direction
+ * and extends either to the left or right of the line. When the line is vertical (direction 0 or 3), the result
+ * is the same as a vertical HexHalfPlaneShape. When the line is tilted (directions 1, 2, 4 and 5), the plane
+ * extends to the (lower or upper) right or left.
+ */
+public class RowHalfPlaneHexArea extends AbstractHexArea {
+
+ public enum HalfPlaneType { RIGHT, LEFT }
+
+ private final HalfPlaneType planeDirection;
+ private final BotGeometry.HexLine hexLine;
+
+ /**
+ * Creates a half plane, delimited by a hex line through the given point going in the given direction,
+ * wherein the plane extends to either the left or right as given by the planeDirection. Opposite directions
+ * (e.g., 0 and 3) result in the same half plane.
+ *
+ * @param point A hex that the hex line goes through
+ * @param direction The direction of the hex line, 0 = N, 2 = SE ...
+ * @param planeDirection The direction the plane extends to
+ */
+ public RowHalfPlaneHexArea(Coords point, int direction, HalfPlaneType planeDirection) {
+ this.planeDirection = Objects.requireNonNull(planeDirection);
+ hexLine = new BotGeometry.HexLine(Objects.requireNonNull(point), direction);
+ }
+
+ @Override
+ public boolean containsCoords(Coords coords, Board board) {
+ int comparison = hexLine.isAbsoluteLeftOrRight(coords);
+ return (comparison == 0)
+ || ((comparison == 1) && (planeDirection == HalfPlaneType.RIGHT))
+ || ((comparison == -1) && (planeDirection == HalfPlaneType.LEFT));
+ }
+}
diff --git a/megamek/src/megamek/common/hexarea/TerrainHexArea.java b/megamek/src/megamek/common/hexarea/TerrainHexArea.java
new file mode 100644
index 00000000000..3293ac2b2ef
--- /dev/null
+++ b/megamek/src/megamek/common/hexarea/TerrainHexArea.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved.
+ *
+ * This file is part of MegaMek.
+ *
+ * MegaMek is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * MegaMek is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MegaMek. If not, see .
+ */
+package megamek.common.hexarea;
+
+import megamek.common.Board;
+import megamek.common.Coords;
+import megamek.common.Hex;
+
+/**
+ * This class represents a hex area that is based on the terrain of the hex itself or even terrain in some distance to the present hex. The
+ * terrain can be limited to terrain levels and a minimum and maximum distance from the present hex can be used.
+ */
+public class TerrainHexArea extends AbstractHexArea {
+
+ private final int terrainType;
+ private final int minLevel;
+ private final int maxLevel;
+ private final int minDistance;
+ private final int maxDistance;
+
+ /**
+ * Creates a hex area that includes hexes of the board that have the given terrain type of the given minimum and maximum level in hexes
+ * that are the given minimum to maximum distance away. *NOTE* be careful with distances other than 0 as this multiplies calculation
+ * times.
+ *
+ * @param terrainType The terrain type, e.g. Terrains.WATER
+ * @param minLevel the minimum terrain level
+ * @param maxLevel the maximum terrain level
+ * @param minDistance the minimum distance to the present hex when detecting included hexes
+ * @param maxDistance the maximum distance to the present hex when detecting included hexes
+ */
+ public TerrainHexArea(int terrainType, int minLevel, int maxLevel, int minDistance, int maxDistance) {
+ this.terrainType = terrainType;
+ this.minLevel = minLevel;
+ this.maxLevel = maxLevel;
+ this.minDistance = minDistance;
+ this.maxDistance = maxDistance;
+ }
+
+ /**
+ * Creates a hex area that includes hexes of the board that have the given terrain type.
+ *
+ * @param terrainType The terrain type, e.g. Terrains.WATER
+ */
+ public TerrainHexArea(int terrainType) {
+ this(terrainType, 0, Integer.MAX_VALUE, 0, 0);
+ }
+
+ /**
+ * Creates a hex area that includes hexes of the board that have the given terrain type of the given level-
+ *
+ * @param terrainType The terrain type, e.g. Terrains.WATER
+ * @param terrainLevel The terrain level
+ */
+ public TerrainHexArea(int terrainType, int terrainLevel) {
+ this(terrainType, terrainLevel, terrainLevel, 0, 0);
+ }
+
+ /**
+ * Creates a hex area that includes hexes of the board that have the given terrain type of the given minimum and maximum level.
+ *
+ * @param terrainType The terrain type, e.g. Terrains.WATER
+ * @param minLevel the minimum terrain level
+ * @param maxLevel the maximum terrain level
+ */
+ public TerrainHexArea(int terrainType, int minLevel, int maxLevel) {
+ this(terrainType, minLevel, maxLevel, 0, 0);
+ }
+
+ @Override
+ public boolean containsCoords(Coords coords, Board board) {
+ if (maxDistance == 0) {
+ return board.contains(coords) && hasCorrectTerrain(board.getHex(coords));
+ } else {
+ for (int distance = minDistance; distance <= maxDistance; distance++) {
+ for (Coords coordsToCheck : coords.allAtDistance(distance)) {
+ if (hasCorrectTerrain(board.getHex(coordsToCheck))) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ }
+
+ private boolean hasCorrectTerrain(Hex hex) {
+ return hex.containsTerrain(terrainType)
+ && (hex.terrainLevel(terrainType) >= minLevel) && (hex.terrainLevel(terrainType) <= maxLevel);
+ }
+}
diff --git a/megamek/src/megamek/common/jacksonadapters/CoordsDeserializer.java b/megamek/src/megamek/common/jacksonadapters/CoordsDeserializer.java
new file mode 100644
index 00000000000..d506d6fe4ad
--- /dev/null
+++ b/megamek/src/megamek/common/jacksonadapters/CoordsDeserializer.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved.
+ *
+ * This file is part of MegaMek.
+ *
+ * MegaMek is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * MegaMek is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MegaMek. If not, see .
+ */
+package megamek.common.jacksonadapters;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import megamek.common.Coords;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static megamek.common.jacksonadapters.MMUReader.requireFields;
+
+public class CoordsDeserializer {
+
+ private static final String X = "x";
+ private static final String Y = "y";
+ private static final String MESSAGE = "Illegal Coords definition. Use either a list of two values or x: and y:";
+
+ public static Coords parseNode(JsonNode coordsNode) {
+ if (coordsNode.isArray()) {
+ List xyList = new ArrayList<>();
+ coordsNode.elements().forEachRemaining(n -> xyList.add(n.asInt()));
+ if (xyList.size() == 2) {
+ return new Coords(xyList.get(0) - 1, xyList.get(1) - 1);
+ } else {
+ throw new IllegalArgumentException(MESSAGE);
+ }
+ } else if (coordsNode.has(X) || coordsNode.has(Y)) {
+ requireFields("Coords", coordsNode, X, Y);
+ return new Coords(coordsNode.get(X).asInt() - 1, coordsNode.get(Y).asInt() - 1);
+ } else {
+ throw new IllegalArgumentException(MESSAGE);
+ }
+ }
+}
diff --git a/megamek/src/megamek/common/jacksonadapters/EntityDeserializer.java b/megamek/src/megamek/common/jacksonadapters/EntityDeserializer.java
index 8d6810c015e..235a160930a 100644
--- a/megamek/src/megamek/common/jacksonadapters/EntityDeserializer.java
+++ b/megamek/src/megamek/common/jacksonadapters/EntityDeserializer.java
@@ -59,6 +59,8 @@ public class EntityDeserializer extends StdDeserializer {
private static final String AMMO = "ammo";
private static final String SLOT = "slot";
private static final String SHOTS = "shots";
+ public static final String FLEE_AREA = "fleefrom";
+ private static final String AREA = "area";
public EntityDeserializer() {
this(null);
@@ -89,6 +91,7 @@ public Entity deserialize(JsonParser jp, DeserializationContext ctxt) throws IOE
assignRemaining(entity, node);
assignCrits(entity, node);
assignAmmos(entity, node);
+ assignFleeArea(entity, node);
return entity;
}
@@ -110,13 +113,9 @@ private Entity loadEntity(JsonNode node) {
private void assignPosition(Entity entity, JsonNode node) {
try {
if (node.has(AT)) {
- List xyList = new ArrayList<>();
- node.get(AT).elements().forEachRemaining(n -> xyList.add(n.asInt()));
- setDeployedPosition(entity, new Coords(xyList.get(0), xyList.get(1)));
-
+ setDeployedPosition(entity, CoordsDeserializer.parseNode(node.get(AT)));
} else if (node.has(X) || node.has(Y)) {
- requireFields("Entity", node, X, Y);
- setDeployedPosition(entity, new Coords(node.get(X).asInt(), node.get(Y).asInt()));
+ setDeployedPosition(entity, CoordsDeserializer.parseNode(node));
}
} catch (Exception e) {
throw new IllegalArgumentException("Illegal position information for entity " + entity, e);
@@ -320,6 +319,17 @@ private void assignAmmo(Entity entity, JsonNode node, int location) {
}
}
+ private void assignFleeArea(Entity entity, JsonNode node) {
+ if (node.has(FLEE_AREA)) {
+ // allow using or omitting "area:"
+ if (node.get(FLEE_AREA).has(AREA)) {
+ entity.setFleeZone(HexAreaDeserializer.parseShape(node.get(FLEE_AREA).get(AREA)));
+ } else {
+ entity.setFleeZone(HexAreaDeserializer.parseShape(node.get(FLEE_AREA)));
+ }
+ }
+ }
+
/**
* Returns all Integers of a node as a List. The node may be either of the form "node: singleNumber", in
* which case the List will only contain singleNumber, or it may be an array node of the form
diff --git a/megamek/src/megamek/common/jacksonadapters/HexAreaDeserializer.java b/megamek/src/megamek/common/jacksonadapters/HexAreaDeserializer.java
new file mode 100644
index 00000000000..dac3358abbf
--- /dev/null
+++ b/megamek/src/megamek/common/jacksonadapters/HexAreaDeserializer.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved.
+ *
+ * This file is part of MegaMek.
+ *
+ * MegaMek is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * MegaMek is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MegaMek. If not, see .
+ */
+package megamek.common.jacksonadapters;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import megamek.common.Coords;
+import megamek.common.Terrains;
+import megamek.common.hexarea.*;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public final class HexAreaDeserializer {
+
+ // Possible future improvements:
+ // read board-relative sizes, like mindistance = 50%W for half board width
+ // read rectangle as UL corner and W/H
+
+ private static final String UNION = "union";
+ private static final String DIFFERENCE = "difference";
+ private static final String INTERSECTION = "intersection";
+ private static final String FIRST = "first";
+ private static final String SECOND = "second";
+
+ private static final String CIRCLE = "circle";
+ private static final String CENTER = "center";
+ private static final String RADIUS = "radius";
+ private static final String HALF_PLANE = "halfplane";
+ private static final String COORDINATE = "coordinate";
+ private static final String DIRECTION = "direction";
+ private static final String EXTENDS = "extends";
+ private static final String LIST = "list";
+ private static final String RECTANGLE = "rectangle";
+ private static final String LINE = "line";
+ private static final String POINT = "point";
+ private static final String RAY = "ray";
+ private static final String BORDER = "border";
+ private static final String MIN_DISTANCE = "mindistance";
+ private static final String MAX_DISTANCE = "maxdistance";
+ private static final String NORTH = "north";
+ private static final String SOUTH = "south";
+ private static final String WEST = "west";
+ private static final String EAST = "east";
+ private static final String EDGES = "edges";
+ private static final String EMPTY = "empty";
+ private static final String TERRAIN = "terrain";
+ private static final String TYPE = "type";
+ private static final String LEVEL = "level";
+ private static final String HEX_LEVEL = "hexlevel";
+ private static final String MIN_LEVEL = "minlevel";
+ private static final String MAX_LEVEL = "maxlevel";
+
+ /**
+ * Parses a HexArea from the given YAML node. The node should be below the "area:" level.
+ *
+ * @param node The node to parse
+ * @return A HexArea parsed from the given node
+ */
+ public static HexArea parseShape(JsonNode node) {
+ if (node.has(UNION)) {
+ return parseUnion(node.get(UNION));
+ } else if (node.has(DIFFERENCE)) {
+ return parseDifference(node.get(DIFFERENCE));
+ } else if (node.has(INTERSECTION)) {
+ return parseIntersection(node.get(INTERSECTION));
+ } else if (node.has(CIRCLE)) {
+ return parseCircle(node.get(CIRCLE));
+ } else if (node.has(HALF_PLANE)) {
+ return parseHalfPlane(node.get(HALF_PLANE));
+ } else if (node.has(LIST)) {
+ return parseList(node.get(LIST));
+ } else if (node.has(RECTANGLE)) {
+ return parseRectangle(node.get(RECTANGLE));
+ } else if (node.has(LINE)) {
+ return parseLine(node.get(LINE));
+ } else if (node.has(RAY)) {
+ return parseRay(node.get(RAY));
+ } else if (node.has(BORDER)) {
+ return parseBorder(node.get(BORDER));
+ } else if (node.has(EMPTY)) {
+ return HexArea.EMPTY_AREA;
+ } else if (node.has(TERRAIN)) {
+ return parseTerrainArea(node.get(TERRAIN));
+ } else if (node.has(HEX_LEVEL)) {
+ return parseHexLevelArea(node.get(HEX_LEVEL));
+ } else {
+ throw new IllegalStateException("Cannot parse area node!");
+ }
+ }
+
+ private static HexArea parseUnion(JsonNode node) {
+ MMUReader.requireFields("HexArea", node, FIRST, SECOND);
+ return new HexAreaUnion(parseShape(node.get(FIRST)), parseShape(node.get(SECOND)));
+ }
+
+ private static HexArea parseDifference(JsonNode node) {
+ MMUReader.requireFields("HexArea", node, FIRST, SECOND);
+ return new HexAreaDifference(parseShape(node.get(FIRST)), parseShape(node.get(SECOND)));
+ }
+
+ private static HexArea parseIntersection(JsonNode node) {
+ MMUReader.requireFields("HexArea", node, FIRST, SECOND);
+ return new HexAreaIntersection(parseShape(node.get(FIRST)), parseShape(node.get(SECOND)));
+ }
+
+ private static HexArea parseCircle(JsonNode node) {
+ MMUReader.requireFields("HexArea", node, CENTER, RADIUS);
+ return new CircleHexArea(CoordsDeserializer.parseNode(node.get(CENTER)), node.get(RADIUS).intValue());
+ }
+
+ private static HexArea parseList(JsonNode node) {
+ if (node.isArray()) {
+ Set coords = new HashSet<>();
+ node.forEach(n -> coords.add(CoordsDeserializer.parseNode(n)));
+ return new ListHexArea(coords);
+ } else {
+ return new ListHexArea(CoordsDeserializer.parseNode(node));
+ }
+ }
+
+ private static HexArea parseRectangle(JsonNode node) {
+ if (node.isArray()) {
+ List coords = new ArrayList<>();
+ node.forEach(n -> coords.add(CoordsDeserializer.parseNode(n)));
+ if (coords.size() == 2) {
+ return new RectangleHexArea(coords.get(0).getX(), coords.get(0).getY(), coords.get(1).getX(), coords.get(1).getY());
+ } else {
+ throw new IllegalArgumentException("A Rectangle must be defined by two corner coords!");
+ }
+ } else {
+ throw new IllegalArgumentException("A Rectangle must be defined by two corner coords!");
+ }
+ }
+
+ private static HexArea parseHalfPlane(JsonNode node) {
+ MMUReader.requireFields("Halfplane HexArea", node, DIRECTION);
+ if (node.has(COORDINATE)) {
+ int coordinate = node.get(COORDINATE).intValue() - 1;
+ var type = HalfPlaneHexArea.HalfPlaneType.valueOf(node.get(EXTENDS).asText().toUpperCase());
+ return new HalfPlaneHexArea(coordinate, type);
+ } else {
+ Coords point = CoordsDeserializer.parseNode(node.get(POINT));
+ int direction = node.get(DIRECTION).intValue();
+ var type = RowHalfPlaneHexArea.HalfPlaneType.valueOf(node.get(EXTENDS).asText().toUpperCase());
+ return new RowHalfPlaneHexArea(point, direction, type);
+ }
+ }
+
+ private static HexArea parseLine(JsonNode node) {
+ MMUReader.requireFields("Line HexArea", node, POINT, DIRECTION);
+ return new LineHexArea(CoordsDeserializer.parseNode(node.get(POINT)), node.get(DIRECTION).asInt());
+ }
+
+ private static HexArea parseRay(JsonNode node) {
+ MMUReader.requireFields("Ray HexArea", node, POINT, DIRECTION);
+ return new RayHexArea(CoordsDeserializer.parseNode(node.get(POINT)), node.get(DIRECTION).asInt());
+ }
+
+ private static HexArea parseBorder(JsonNode node) {
+ if (node.has(EDGES)) {
+ List borders = TriggerDeserializer.parseArrayOrSingleNode(node.get(EDGES), NORTH, SOUTH, EAST, WEST);
+ int mindistance = 0;
+ int maxdistance = 0;
+ if (node.has(MIN_DISTANCE)) {
+ mindistance = node.get(MIN_DISTANCE).intValue();
+ }
+ if (node.has(MAX_DISTANCE)) {
+ maxdistance = node.get(MAX_DISTANCE).intValue();
+ }
+ maxdistance = Math.max(maxdistance, mindistance);
+ return new BorderHexArea(borders.contains(NORTH), borders.contains(SOUTH), borders.contains(EAST), borders.contains(WEST),
+ mindistance, maxdistance);
+ } else {
+ List borders = TriggerDeserializer.parseArrayOrSingleNode(node, NORTH, SOUTH, EAST, WEST);
+ return new BorderHexArea(borders.contains(NORTH), borders.contains(SOUTH), borders.contains(EAST), borders.contains(WEST));
+ }
+ }
+
+ private static HexArea parseTerrainArea(JsonNode node) {
+ int minLevel = 0;
+ int maxLevel = Integer.MAX_VALUE;
+ int terrainType = Terrains.getType(node.get(TYPE).asText());
+ if (terrainType == 0) {
+ throw new IllegalArgumentException("Invalid terrain type: " + node.get(TERRAIN).asText());
+ }
+ if (node.has(LEVEL)) {
+ minLevel = node.get(LEVEL).intValue();
+ } else if (node.has(MIN_LEVEL)) {
+ minLevel = node.get(MIN_LEVEL).intValue();
+ if (node.has(MAX_LEVEL)) {
+ maxLevel = node.get(MAX_LEVEL).intValue();
+ }
+ }
+
+ int minDistance = 0;
+ int maxDistance = 0;
+ if (node.has(MIN_DISTANCE)) {
+ minDistance = node.get(MIN_DISTANCE).intValue();
+ if (node.has(MAX_DISTANCE)) {
+ maxDistance = node.get(MAX_DISTANCE).intValue();
+ }
+ }
+ return new TerrainHexArea(terrainType, minLevel, maxLevel, minDistance, maxDistance);
+ }
+
+ private static HexArea parseHexLevelArea(JsonNode node) {
+ if (node.isValueNode()) {
+ return new HexLevelArea(node.asInt());
+ } else {
+ int minLevel = Integer.MIN_VALUE;
+ int maxLevel = Integer.MAX_VALUE;
+ if (node.has(MIN_LEVEL)) {
+ minLevel = node.get(MIN_LEVEL).intValue();
+ }
+ if (node.has(MAX_LEVEL)) {
+ maxLevel = node.get(MAX_LEVEL).intValue();
+ }
+ return new HexLevelArea(minLevel, maxLevel);
+ }
+ }
+
+ private HexAreaDeserializer() { }
+}
diff --git a/megamek/src/megamek/common/jacksonadapters/TriggerDeserializer.java b/megamek/src/megamek/common/jacksonadapters/TriggerDeserializer.java
index 9ed0be2468e..a3cbf756038 100644
--- a/megamek/src/megamek/common/jacksonadapters/TriggerDeserializer.java
+++ b/megamek/src/megamek/common/jacksonadapters/TriggerDeserializer.java
@@ -23,6 +23,7 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import megamek.common.enums.GamePhase;
+import megamek.common.hexarea.HexArea;
import megamek.server.trigger.*;
import java.io.IOException;
@@ -46,6 +47,8 @@ public class TriggerDeserializer extends StdDeserializer {
private static final String TYPE_ACTIVEUNITS = "activeunits";
private static final String TYPE_KILLEDUNITS = "killedunits";
private static final String TYPE_KILLEDUNIT = "killedunit";
+ private static final String TYPE_UNIT_POSITION = "position";
+ private static final String TYPE_UNITS_POSITION = "positions";
private static final String TYPE_BATTLEFIELD_CONTROL = "battlefieldcontrol";
private static final String PLAYER = "player";
private static final String COUNT = "count";
@@ -59,6 +62,7 @@ public class TriggerDeserializer extends StdDeserializer {
private static final String ONCE = "once";
private static final String NOT = "not";
private static final String AT_END = "atend";
+ private static final String AREA = "area";
public TriggerDeserializer() {
this(null);
@@ -90,6 +94,8 @@ public static Trigger parseNode(JsonNode node) {
case TYPE_KILLEDUNITS -> parseKilledUnitsTrigger(node);
case TYPE_KILLEDUNIT -> parseKilledUnitTrigger(node);
case TYPE_BATTLEFIELD_CONTROL -> new BattlefieldControlTrigger();
+ case TYPE_UNIT_POSITION -> parseUnitPositionTrigger(node);
+ case TYPE_UNITS_POSITION -> parseUnitsPositionTrigger(node);
case NOT -> parseNotTrigger(node);
case ROUND -> new RoundTrigger(node.get(ROUND).asInt());
default -> throw new IllegalStateException("Unexpected value: " + type);
@@ -216,6 +222,36 @@ private static Trigger parseKilledUnitsTrigger(JsonNode triggerNode) {
return new KilledUnitsTrigger(player, unitIds, minCount, maxCount);
}
+ private static Trigger parseUnitPositionTrigger(JsonNode triggerNode) {
+ HexArea area = HexAreaDeserializer.parseShape(triggerNode.get(AREA));
+ return new UnitPositionTrigger(area, triggerNode.get(UNIT).asInt());
+ }
+
+ private static Trigger parseUnitsPositionTrigger(JsonNode triggerNode) {
+ HexArea area = HexAreaDeserializer.parseShape(triggerNode.get(AREA));
+ int minCount = Integer.MIN_VALUE;
+ int maxCount = Integer.MAX_VALUE;
+ if (triggerNode.has(AT_MOST)) {
+ maxCount = triggerNode.get(AT_MOST).asInt();
+ }
+ if (triggerNode.has(AT_LEAST)) {
+ minCount = triggerNode.get(AT_LEAST).asInt();
+ }
+ if (triggerNode.has(COUNT)) {
+ minCount = triggerNode.get(COUNT).asInt();
+ maxCount = triggerNode.get(COUNT).asInt();
+ }
+ String player = "";
+ List unitIds = new ArrayList<>();
+ if (triggerNode.has(PLAYER)) {
+ player = triggerNode.get(PLAYER).asText();
+ }
+ if (triggerNode.has(UNITS)) {
+ triggerNode.get(UNITS).iterator().forEachRemaining(id -> unitIds.add(id.asInt()));
+ }
+ return new UnitPositionTrigger(area, player, unitIds, minCount, maxCount);
+ }
+
private static Trigger parseKilledUnitTrigger(JsonNode triggerNode) {
return new KilledUnitsTrigger(triggerNode.get(UNIT).asInt());
}
diff --git a/megamek/src/megamek/common/scenario/ScenarioV2.java b/megamek/src/megamek/common/scenario/ScenarioV2.java
index 28cdf5d17a2..255d7fd0955 100644
--- a/megamek/src/megamek/common/scenario/ScenarioV2.java
+++ b/megamek/src/megamek/common/scenario/ScenarioV2.java
@@ -39,14 +39,10 @@
import megamek.common.enums.GamePhase;
import megamek.common.force.Force;
import megamek.common.force.Forces;
+import megamek.common.hexarea.HexArea;
import megamek.common.icons.Camouflage;
import megamek.common.icons.FileCamouflage;
-import megamek.common.jacksonadapters.BoardDeserializer;
-import megamek.common.jacksonadapters.CarryableDeserializer;
-import megamek.common.jacksonadapters.MMUReader;
-import megamek.common.jacksonadapters.MessageDeserializer;
-import megamek.common.jacksonadapters.TriggerDeserializer;
-import megamek.common.jacksonadapters.VictoryDeserializer;
+import megamek.common.jacksonadapters.*;
import megamek.common.planetaryconditions.PlanetaryConditions;
import megamek.common.strategicBattleSystems.SBFGame;
import megamek.logging.MMLogger;
@@ -69,10 +65,13 @@ public class ScenarioV2 implements Scenario {
private static final String END = "end";
private static final String TRIGGER = "trigger";
private static final String VICTORY = "victory";
+ private static final String AREA = "area";
private final JsonNode node;
private final File scenariofile;
+ private final List deploymentAreas = new ArrayList<>();
+
private static final ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory());
ScenarioV2(File scenariofile) throws IOException {
@@ -133,6 +132,10 @@ public IGame createGame() throws IOException, ScenarioLoaderException {
game.setupTeams();
game.setBoard(0, createBoard());
+ int zone = 1000;
+ for (HexArea hexArea : deploymentAreas) {
+ game.getBoard().addDeploymentZone(zone++, hexArea);
+ }
if ((game instanceof PlanetaryConditionsUsing)) {
parsePlanetaryConditions((PlanetaryConditionsUsing) game);
}
@@ -213,6 +216,10 @@ private void parseDeployment(JsonNode playerNode, Player player) {
if (playerNode.has(DEPLOY)) {
if (!playerNode.get(DEPLOY).isContainerNode()) {
edge = playerNode.get(DEPLOY).textValue();
+ } else if (playerNode.get(DEPLOY).has(AREA)) {
+ deploymentAreas.add(HexAreaDeserializer.parseShape(playerNode.get(DEPLOY).get(AREA)));
+ player.setStartingPos(1000 + deploymentAreas.size() - 1);
+ return;
} else {
JsonNode deployNode = playerNode.get(DEPLOY);
if (deployNode.has(DEPLOY_EDGE)) {
@@ -280,6 +287,14 @@ private List readPlayers(IGame game) throws ScenarioLoaderException, IOE
teamId = playerNode.has(PARAM_TEAM) ? playerNode.get(PARAM_TEAM).intValue() : teamId + 1;
player.setTeam(Math.min(teamId, Player.TEAM_NAMES.length - 1));
+ // The flee area
+ if (playerNode.has(EntityDeserializer.FLEE_AREA)) {
+ JsonNode fleeNode = playerNode.get(EntityDeserializer.FLEE_AREA);
+ // allow using or omitting "area:"
+ JsonNode areaNode = fleeNode.has(AREA) ? fleeNode.get(AREA) : fleeNode;
+ player.setFleeZone(HexAreaDeserializer.parseShape(areaNode));
+ }
+
// TODO minefields
// Carryables
diff --git a/megamek/src/megamek/server/trigger/UnitPositionTrigger.java b/megamek/src/megamek/server/trigger/UnitPositionTrigger.java
new file mode 100644
index 00000000000..c6d3383ce3d
--- /dev/null
+++ b/megamek/src/megamek/server/trigger/UnitPositionTrigger.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved.
+ *
+ * This file is part of MegaMek.
+ *
+ * MegaMek is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * MegaMek is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MegaMek. If not, see .
+ */
+package megamek.server.trigger;
+
+import megamek.common.*;
+import megamek.common.annotations.Nullable;
+import megamek.common.hexarea.HexArea;
+import megamek.logging.MMLogger;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * This Trigger reacts when the count of units in a certain board area is in a given range.
+ */
+public class UnitPositionTrigger implements Trigger {
+
+ private static final MMLogger LOGGER = MMLogger.create(KilledUnitsTrigger.class);
+
+ private final HexArea area;
+ private final String playerName;
+ private final int minUnitCount;
+ private final int maxUnitCount;
+ private final List unitIds;
+
+ /**
+ * Creates a Trigger that reacts when the count of units that are in the given area is between the given min and maxUnitCount. When the
+ * playerName is blank, units of all players are counted, otherwise only those of the given player. When a list of unit IDs is given,
+ * only those units will be considered. Note that destroyed units can spawn pilots, so providing an ID list to prevent counting the
+ * wrong units usually makes sense. Note that this trigger will react multiple times. Use {@link OnceTrigger} to limit it to
+ * one-time-only.
+ *
+ * @param playerName A player name to limit the checked units to; may be null
+ * @param unitIds A list of Ids to limit the checked units to; when empty, all units are considered
+ * @param minUnitCount the minimum unit count to react to
+ * @param maxUnitCount the maximum unit count to react to
+ * @param area The area to check
+ */
+ public UnitPositionTrigger(HexArea area, @Nullable String playerName, List unitIds, int minUnitCount, int maxUnitCount) {
+ this.playerName = Objects.requireNonNullElse(playerName, "");
+ this.minUnitCount = minUnitCount;
+ this.maxUnitCount = maxUnitCount;
+ this.unitIds = (unitIds == null) ? new ArrayList<>() : new ArrayList<>(unitIds);
+ this.area = area;
+ }
+
+ /**
+ * Creates a Trigger that reacts when the given unit is in the given area. Note that this trigger will react multiple times. Use
+ * {@link OnceTrigger} to limit it to one-time-only.
+ *
+ * @param area The area to check
+ * @param unitId The unit to look at
+ */
+ public UnitPositionTrigger(HexArea area, int unitId) {
+ this(area, null, List.of(unitId), 1, 1);
+ }
+
+ /**
+ * Creates a Trigger that reacts when the count of units that are in the given area is equal to the given count. When a list of unit IDs
+ * is given, only those units will be considered. Note that destroyed units can spawn pilots, so providing an ID list to prevent
+ * counting the wrong units usually makes sense. Note that this trigger will react multiple times. Use {@link OnceTrigger} to limit it
+ * to one-time-only.
+ *
+ * @param area The area to check
+ * @param unitIds A list of Ids to limit the checked units to; when empty, all units are considered
+ * @param killedUnitCount The count of killed units to react to
+ */
+ public UnitPositionTrigger(HexArea area, List unitIds, int killedUnitCount) {
+ this(area, null, unitIds, killedUnitCount, killedUnitCount);
+ }
+
+ @Override
+ public boolean isTriggered(IGame game, TriggerSituation event) {
+ if (game instanceof Game) {
+ List allUnits = game.getInGameObjects();
+ allUnits.addAll(game.getGraveyard());
+ long matchingUnitCount = allUnits.stream()
+ .filter(this::matchesIdList)
+ .filter(e -> matchesPlayerName(game.getPlayer(e.getOwnerId())))
+ .filter(e -> e instanceof Entity)
+ .map(e -> (Entity) e)
+ .filter(e -> area.containsCoords(e.getPosition(), game.getBoard()))
+ .count();
+ return (matchingUnitCount >= minUnitCount) && (matchingUnitCount <= maxUnitCount);
+ } else {
+ LOGGER.warn("UnitPositionTrigger is currently only available for TW games.");
+ return false;
+ }
+ }
+
+ private boolean matchesPlayerName(Player player) {
+ return playerName.isBlank() || playerName.equals(player.getName());
+ }
+
+ private boolean matchesIdList(InGameObject unit) {
+ return unitIds.isEmpty() || unitIds.contains(unit.getId());
+ }
+}
diff --git a/megamek/testresources/data/scenarios/test_setups/DeploymentTest.mms b/megamek/testresources/data/scenarios/test_setups/DeploymentTest.mms
index ac3cea49e7b..ddff3492f56 100644
--- a/megamek/testresources/data/scenarios/test_setups/DeploymentTest.mms
+++ b/megamek/testresources/data/scenarios/test_setups/DeploymentTest.mms
@@ -26,4 +26,3 @@ factions:
- fullname: Fensalir Combat WiGE
# - fullname: Foot Platoon (MG)
-