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) -