Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions megamek/resources/megamek/client/messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,7 @@ BoardView1.ConversionMode.Aerodyne=Fighter
BoardView1.ConversionMode.UMU=UMU
BoardView1.ChargeAttackAction=Charges. Needs {0}
BoardView1.ChargeAttackAction1=(Charging)
BoardView1.WoodsClearingAction=Clearing Woods
BoardView1.CrewDead=CREW DEAD
BoardView1.DfaAttackAction1=(Executing DFA)
BoardView1.Drop=Drop
Expand Down Expand Up @@ -588,6 +589,8 @@ BoardView1.Tooltip.BuildingLine=Height: {0}; CF: {1} Armor: {2}
BoardView1.Tooltip.BldgBasementCollapsed=<BR><I><FONT COLOR=RED>(collapsed)</FONT><I>
BoardView1.Tooltip.Bridge={1} <BR>Height: {0}, CF: {2}
BoardView1.Tooltip.FuelTank={1} <BR>Height: {0}, CF: {2}<BR>Magnitude: {3}
BoardView1.Tooltip.WoodsClearing=Saw clearing in progress ({0} turn(s) remaining)
BoardView1.Tooltip.WoodsClearingComplete=Saw clearing complete
BoardView1.Tooltip.ArtyAutoHint1=You can show all players&#39;
BoardView1.Tooltip.ArtyAutoHint2=deployment zones on the board
BoardView1.Tooltip.ArtyAutoHint3=by pressing {0}.
Expand Down Expand Up @@ -3146,6 +3149,8 @@ PhysicalDisplay.protoPhysical=Proto-Frenzy
PhysicalDisplay.LayExplosivesAttackDialog.message=To Hit: {0} ({1}%) ({2})
PhysicalDisplay.LayExplosivesAttackDialog.title=Lay explosives on {0}?
PhysicalDisplay.explosives=Lay Explosives
PhysicalDisplay.clearWoods=Clear Woods
PhysicalDisplay.SelectClearWoodsHex=Click a wooded hex to clear with saw.
PhysicalDisplay.infantryCombat=Infantry Combat
PhysicalDisplay.InfantryCombatDialog.title=Initiate Infantry Combat?
PhysicalDisplay.InfantryCombatDialog.message={0} will engage in infantry vs. infantry combat at {1}. Continue?
Expand Down
4 changes: 4 additions & 0 deletions megamek/resources/megamek/common/report-messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,10 @@
4450=<data> tries to trip <data>. Needs <data> rolls <data>
4455=Retractable blade extended:
4456=The retractable blade is destroyed!
# Woods Clearing with Saw (TM pp.241-243)
4500=<data> (<data>) begins clearing woods at hex <data> with a saw.
4501=<data> (<data>) continues clearing woods at hex <data> with a saw.
4502=Woods at hex <data> have been reduced by saw clearing!
4550=Pheromone attack at <data>
4551=, but the pheromone attack is impossible (<data>).
4552=<span class='warning'><B>misses</B></span>
Expand Down
8 changes: 8 additions & 0 deletions megamek/src/megamek/client/Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,11 @@ protected void receiveIlluminatedHexes(Packet packet) throws InvalidPacketDataEx
game.setIlluminatedPositions(packet.getCoordsHashSet(0));
}

protected void receiveUpdateCutHexes(Packet packet) throws InvalidPacketDataException {
game.setHexesBeingCut(packet.getBoardLocationIntegerMap(0));
game.processGameEvent(new GameBoardChangeEvent(this));
}

protected void receiveRevealMinefield(Packet packet) throws InvalidPacketDataException {
Minefield minefield = packet.getMinefield(0);

Expand Down Expand Up @@ -1036,6 +1041,9 @@ protected boolean handleGameSpecificPacket(Packet packet) {
case REMOVE_MINEFIELD:
receiveRemoveMinefield(packet);
break;
case UPDATE_CUT_HEXES:
receiveUpdateCutHexes(packet);
break;
case UPDATE_GROUND_OBJECTS:
receiveUpdateGroundObjects(packet);
break;
Expand Down
14 changes: 13 additions & 1 deletion megamek/src/megamek/client/ui/clientGUI/ClientGUI.java
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,10 @@
import megamek.client.ui.clientGUI.boardview.RulerDialog;
import megamek.client.ui.clientGUI.boardview.overlay.BoardToastOverlay;
import megamek.client.ui.clientGUI.boardview.overlay.ChatterBoxOverlay;
import megamek.client.ui.clientGUI.boardview.overlay.ToastLevel;
import megamek.client.ui.clientGUI.boardview.overlay.KeyBindingsOverlay;
import megamek.client.ui.clientGUI.boardview.overlay.OffBoardTargetOverlay;
import megamek.client.ui.clientGUI.boardview.overlay.PlanetaryConditionsOverlay;
import megamek.client.ui.clientGUI.boardview.overlay.ToastLevel;
import megamek.client.ui.clientGUI.boardview.overlay.TurnDetailsOverlay;
import megamek.client.ui.clientGUI.boardview.overlay.UnitOverviewOverlay;
import megamek.client.ui.clientGUI.boardview.spriteHandler.*;
Expand Down Expand Up @@ -352,6 +352,7 @@ public class ClientGUI extends AbstractClientGUI
private FleeZoneSpriteHandler fleeZoneSpriteHandler;
private SensorRangeSpriteHandler sensorRangeSpriteHandler;
private CollapseWarningSpriteHandler collapseWarningSpriteHandler;
private SawClearingSpriteHandler sawClearingSpriteHandler;
private GroundObjectSpriteHandler groundObjectSpriteHandler;
private FiringSolutionSpriteHandler firingSolutionSpriteHandler;
private FiringArcSpriteHandler firingArcSpriteHandler;
Expand Down Expand Up @@ -697,6 +698,7 @@ private void initializeSpriteHandlers() {
FlareSpritesHandler flareSpritesHandler = new FlareSpritesHandler(this, client.getGame());
sensorRangeSpriteHandler = new SensorRangeSpriteHandler(this, client.getGame());
collapseWarningSpriteHandler = new CollapseWarningSpriteHandler(this);
sawClearingSpriteHandler = new SawClearingSpriteHandler(this, client.getGame());
groundObjectSpriteHandler = new GroundObjectSpriteHandler(this, client.getGame());
firingSolutionSpriteHandler = new FiringSolutionSpriteHandler(this, client);
firingArcSpriteHandler = new FiringArcSpriteHandler(this);
Expand All @@ -707,6 +709,7 @@ private void initializeSpriteHandlers() {
sensorRangeSpriteHandler,
flareSpritesHandler,
collapseWarningSpriteHandler,
sawClearingSpriteHandler,
groundObjectSpriteHandler,
firingSolutionSpriteHandler,
firingArcSpriteHandler,
Expand Down Expand Up @@ -3619,6 +3622,15 @@ public void showCollapseWarning(Collection<BoardLocation> warnList) {
collapseWarningSpriteHandler.setCFWarningSprites(warnList);
}

/**
* Shows saw clearing indicators on the given hexes in the BoardView.
*
* @param cutHexes a map of board locations to turns remaining for saw clearing
*/
public void showSawClearingHexes(Map<BoardLocation, Integer> cutHexes) {
sawClearingSpriteHandler.setSawClearingSprites(cutHexes);
}

/**
* Shows ground object icons in the given list of Coords in the BoardView
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/*
* Copyright (C) 2026 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 (GPL),
* version 3 or (at your option) any later version,
* as published by the Free Software Foundation.
*
* 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.
*
* A copy of the GPL should have been included with this project;
* if not, see <https://www.gnu.org/licenses/>.
*
* NOTICE: The MegaMek organization is a non-profit group of volunteers
* creating free software for the BattleTech community.
*
* MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks
* of The Topps Company, Inc. All Rights Reserved.
*
* Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of
* InMediaRes Productions, LLC.
*
* MechWarrior Copyright Microsoft Corporation. MegaMek was created under
* Microsoft's "Game Content Usage Rules"
* <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or
* affiliated with Microsoft.
*/
package megamek.client.ui.clientGUI.boardview.sprite;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Stroke;

import megamek.client.ui.clientGUI.boardview.BoardView;
import megamek.client.ui.tileset.HexTileset;
import megamek.client.ui.util.UIUtil;
import megamek.common.board.Coords;

/**
* Displays a circular saw blade indicator on hexes that are being cleared by saws. Shows a steel gray blade with
* triangular teeth and a turns-remaining number in the center, positioned in the lower portion of the hex.
*/
public class SawClearingSprite extends HexSprite {

private static final Color SAW_TOOTH_COLOR = new Color(100, 100, 110);
private static final Color SAW_BLADE_COLOR = new Color(170, 170, 180);
private static final Color SAW_INNER_COLOR = new Color(130, 130, 140);
private static final Color SAW_TEXT_OUTLINE_COLOR = new Color(40, 40, 50);
private static final Color SAW_OUTLINE_COLOR = new Color(60, 60, 70);

private static final int HEX_CENTER_X = HexTileset.HEX_W / 2;
private static final int BLADE_RADIUS = 10;
private static final int TOOTH_HEIGHT = 4;
private static final int NUM_TEETH = 12;
private static final int INNER_RADIUS = 6;
private static final int FONT_SIZE = 11;
private static final int BOTTOM_OFFSET = 20;

private final int turnsRemaining;

/**
* Creates a new saw clearing sprite for the given hex.
*
* @param boardView the parent board view
* @param loc the hex coordinates
* @param turnsRemaining the number of turns remaining to complete clearing
*/
public SawClearingSprite(BoardView boardView, Coords loc, int turnsRemaining) {
super(boardView, loc);
this.turnsRemaining = turnsRemaining;
}

@Override
public void prepare() {
Graphics2D graph = spriteSetup();
drawSawBlade(graph);
graph.dispose();
}

private Graphics2D spriteSetup() {
updateBounds();
image = createNewHexImage();
Graphics2D graph = (Graphics2D) image.getGraphics();
UIUtil.setHighQualityRendering(graph);
graph.scale(bv.getScale(), bv.getScale());
return graph;
}

private void drawSawBlade(Graphics2D graph) {
graph.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

// Position in lower portion of hex, centered horizontally
int centerX = HEX_CENTER_X;
int centerY = HexTileset.HEX_H - BOTTOM_OFFSET;

// Draw outer teeth (dark steel color)
graph.setColor(SAW_TOOTH_COLOR);
for (int i = 0; i < NUM_TEETH; i++) {
double angle = (2 * Math.PI * i) / NUM_TEETH;
double nextAngle = (2 * Math.PI * (i + 0.5)) / NUM_TEETH;
int outerX = centerX + (int) ((BLADE_RADIUS + TOOTH_HEIGHT) * Math.cos(angle));
int outerY = centerY + (int) ((BLADE_RADIUS + TOOTH_HEIGHT) * Math.sin(angle));
int leftX = centerX + (int) (BLADE_RADIUS * Math.cos(angle - 0.15));
int leftY = centerY + (int) (BLADE_RADIUS * Math.sin(angle - 0.15));
int rightX = centerX + (int) (BLADE_RADIUS * Math.cos(nextAngle));
int rightY = centerY + (int) (BLADE_RADIUS * Math.sin(nextAngle));
int[] xPoints = { outerX, leftX, rightX };
int[] yPoints = { outerY, leftY, rightY };
graph.fillPolygon(xPoints, yPoints, 3);
}

// Draw blade body (steel gray)
graph.setColor(SAW_BLADE_COLOR);
graph.fillOval(centerX - BLADE_RADIUS, centerY - BLADE_RADIUS,
BLADE_RADIUS * 2, BLADE_RADIUS * 2);

// Draw inner ring (darker)
graph.setColor(SAW_INNER_COLOR);
graph.fillOval(centerX - INNER_RADIUS, centerY - INNER_RADIUS,
INNER_RADIUS * 2, INNER_RADIUS * 2);

// Draw turns remaining number in the center of the blade
String turnsStr = String.valueOf(turnsRemaining);
Font turnsFont = new Font("Sans Serif", Font.BOLD, FONT_SIZE);
FontMetrics fm = graph.getFontMetrics(turnsFont);
int textWidth = fm.stringWidth(turnsStr);
int textX = centerX - (textWidth / 2);
int textY = centerY + (fm.getAscent() / 2) - 1;

// White text with dark outline for readability
graph.setFont(turnsFont);
graph.setColor(SAW_TEXT_OUTLINE_COLOR);
graph.drawString(turnsStr, textX - 1, textY);
graph.drawString(turnsStr, textX + 1, textY);
graph.drawString(turnsStr, textX, textY - 1);
graph.drawString(turnsStr, textX, textY + 1);
graph.setColor(Color.WHITE);
graph.drawString(turnsStr, textX, textY);

// Draw blade outline
graph.setColor(SAW_OUTLINE_COLOR);
Stroke oldStroke = graph.getStroke();
graph.setStroke(new BasicStroke(1));
graph.drawOval(centerX - BLADE_RADIUS, centerY - BLADE_RADIUS,
BLADE_RADIUS * 2, BLADE_RADIUS * 2);
graph.setStroke(oldStroke);
}

@Override
public boolean isBehindTerrain() {
return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Copyright (C) 2026 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 (GPL),
* version 3 or (at your option) any later version,
* as published by the Free Software Foundation.
*
* 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.
*
* A copy of the GPL should have been included with this project;
* if not, see <https://www.gnu.org/licenses/>.
*
* NOTICE: The MegaMek organization is a non-profit group of volunteers
* creating free software for the BattleTech community.
*
* MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks
* of The Topps Company, Inc. All Rights Reserved.
*
* Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of
* InMediaRes Productions, LLC.
*
* MechWarrior Copyright Microsoft Corporation. MegaMek was created under
* Microsoft's "Game Content Usage Rules"
* <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or
* affiliated with Microsoft.
*/
package megamek.client.ui.clientGUI.boardview.spriteHandler;

import java.util.Map;

import megamek.client.ui.clientGUI.AbstractClientGUI;
import megamek.client.ui.clientGUI.boardview.BoardView;
import megamek.client.ui.clientGUI.boardview.sprite.SawClearingSprite;
import megamek.common.board.BoardLocation;
import megamek.common.event.board.GameBoardChangeEvent;
import megamek.common.game.Game;

/**
* Manages saw clearing indicator sprites on the board view. Creates and removes {@link SawClearingSprite} instances to
* show which hexes are being cleared by vehicle-mounted saws.
*/
public class SawClearingSpriteHandler extends BoardViewSpriteHandler {

private final Game game;

public SawClearingSpriteHandler(AbstractClientGUI clientGUI, Game game) {
super(clientGUI);
this.game = game;
}

/**
* Updates the saw clearing sprites to reflect the given cut hex data.
*
* @param cutHexes a map of board locations to turns remaining, or null to clear
*/
public void setSawClearingSprites(Map<BoardLocation, Integer> cutHexes) {
clear();
if (clientGUI.boardViews().isEmpty()) {
return;
}
if (cutHexes != null) {
for (Map.Entry<BoardLocation, Integer> entry : cutHexes.entrySet()) {
BoardLocation location = entry.getKey();
BoardView boardView = (BoardView) clientGUI.getBoardView(location);
if (boardView != null) {
SawClearingSprite sprite = new SawClearingSprite(
boardView, location.coords(), entry.getValue());
currentSprites.add(sprite);
}
}
}
currentSprites.forEach(sprite -> sprite.bv.addSprite(sprite));
}

@Override
public void initialize() {
game.addGameListener(this);
}

@Override
public void dispose() {
clear();
game.removeGameListener(this);
}

@Override
public void gameBoardChanged(GameBoardChangeEvent e) {
setSawClearingSprites(game.getHexesBeingCut());
}
}
Loading
Loading