diff --git a/ITSD-DT2025-26-Template/ITSD-DT2025-26-Template/app/events/CardClicked.java b/ITSD-DT2025-26-Template/ITSD-DT2025-26-Template/app/events/CardClicked.java index 29c37f9..1a57d99 100644 --- a/ITSD-DT2025-26-Template/ITSD-DT2025-26-Template/app/events/CardClicked.java +++ b/ITSD-DT2025-26-Template/ITSD-DT2025-26-Template/app/events/CardClicked.java @@ -5,6 +5,10 @@ import akka.actor.ActorRef; import structures.GameState; +import game.selection.SelectionGuards; +import game.selection.SelectionManager; +import game.selection.TargetingOverlay; +import structures.basic.Card; /** * Indicates that the user has clicked an object on the game canvas, in this case a card. @@ -20,12 +24,41 @@ */ public class CardClicked implements EventProcessor{ - @Override - public void processEvent(ActorRef out, GameState gameState, JsonNode message) { - - int handPosition = message.get("position").asInt(); - - - } + @Override + public void processEvent(ActorRef out, GameState gameState, JsonNode message) { + if (gameState == null || !gameState.gameInitalised) return; + int handPosition = message.get("position").asInt(); + if (!SelectionGuards.isValidHandPosition(handPosition)) return; + SelectionGuards.clearStaleSelectionIfNeeded(gameState); + + if (!SelectionGuards.hasCardInHand(gameState, handPosition)) { + SelectionManager.clearSelectionAndHighlights(out, gameState); + return; + } + if (SelectionGuards.isSameSelectedCard(gameState, handPosition)) { + SelectionManager.clearSelectionAndHighlights(out, gameState); + return; + } + + SelectionManager.selectCard(out, gameState, handPosition); + Card card = gameState.getP1HandAtPos(handPosition); + if (card == null) { + SelectionManager.clearSelectionAndHighlights(out, gameState); + return; + } + + // Wenbo’s responsibility starts here: + //compute legal summon/spell targets for this card + //then highlight those tiles + //The important rule for Wenbo is: + // use SelectionManager.selectCard(...) when switching to a card + // use TargetingOverlay.showTargetHighlights(...) to draw legal targets + // use SelectionManager.clearSelectionAndHighlights(...) only for real cancel + // do not wipe card selection just to redraw tiles, + // because my (abdullah) D4 responsibility is that the selected card stays visibly selected during targeting + //example only: + // Iterable legalTargets = ... + // TargetingOverlay.showTargetHighlights(out, gameState, legalTargets); + } } diff --git a/ITSD-DT2025-26-Template/ITSD-DT2025-26-Template/app/events/Heartbeat.java b/ITSD-DT2025-26-Template/ITSD-DT2025-26-Template/app/events/Heartbeat.java index 2fe23ba..bb4312a 100644 --- a/ITSD-DT2025-26-Template/ITSD-DT2025-26-Template/app/events/Heartbeat.java +++ b/ITSD-DT2025-26-Template/ITSD-DT2025-26-Template/app/events/Heartbeat.java @@ -1,14 +1,9 @@ package events; -import java.util.Set; import com.fasterxml.jackson.databind.JsonNode; import akka.actor.ActorRef; import structures.GameState; -import commands.BasicCommands; -import structures.basic.Card; -import structures.basic.Tile; -import utils.BasicObjectBuilders; /** * In the user’s browser, the game is running in an infinite loop, where there is around a 1 second delay @@ -24,25 +19,12 @@ */ public class Heartbeat implements EventProcessor{ - private static final int TILE_MODE_NORMAL = 0; - private static final int CARD_MODE_NORMAL = 0; @Override public void processEvent(ActorRef out, GameState gameState, JsonNode message) { if (gameState == null || !gameState.gameInitalised) return; if (gameState.getSelectionMode() != GameState.SelectionState.Mode.NONE) return; - //clear highlighted tiles - Set tilesToClear = gameState.consumeHighlightedTiles(); - for (GameState.Coord c : tilesToClear) { - Tile t = BasicObjectBuilders.loadTile(c.x, c.y); - BasicCommands.drawTile(out, t, TILE_MODE_NORMAL); - } - //clear highlighted cards - Set handToClear = gameState.consumeHighlightedHandPositions(); - for (Integer pos : handToClear) { - if (pos == null) continue; - Card card = gameState.getP1HandAtPos(pos); - if (card != null) BasicCommands.drawCard(out, card, pos, CARD_MODE_NORMAL); - } + + game.selection.SelectionManager.clearSelectionAndHighlights(out, gameState); } } diff --git a/ITSD-DT2025-26-Template/ITSD-DT2025-26-Template/app/events/OtherClicked.java b/ITSD-DT2025-26-Template/ITSD-DT2025-26-Template/app/events/OtherClicked.java index 163f400..4c1fe44 100644 --- a/ITSD-DT2025-26-Template/ITSD-DT2025-26-Template/app/events/OtherClicked.java +++ b/ITSD-DT2025-26-Template/ITSD-DT2025-26-Template/app/events/OtherClicked.java @@ -1,15 +1,9 @@ package events; -import java.util.Set; - import com.fasterxml.jackson.databind.JsonNode; import akka.actor.ActorRef; -import commands.BasicCommands; import structures.GameState; -import structures.basic.Card; -import structures.basic.Tile; -import utils.BasicObjectBuilders; /** * Indicates that the user has clicked an object on the game canvas, in this case @@ -24,33 +18,11 @@ */ public class OtherClicked implements EventProcessor{ - //mode 0 is the "normal" render mode for tiles and cards in the template. - private static final int TILE_MODE_NORMAL = 0; - private static final int CARD_MODE_NORMAL = 0; - - @Override - public void processEvent(ActorRef out, GameState gameState, JsonNode message) { + @Override + public void processEvent(ActorRef out, GameState gameState, JsonNode message) { if (gameState == null || !gameState.gameInitalised) return; - - //clear any highlighted tiles by drawing them again in normal mode - Set tilesToClear = gameState.consumeHighlightedTiles(); - for (GameState.Coord c : tilesToClear) { - Tile t = BasicObjectBuilders.loadTile(c.x, c.y); - BasicCommands.drawTile(out, t, TILE_MODE_NORMAL); - } - //clear any highlighted hand cards for player 1 by drawing them again in normal mode - Set handPositionsToClear = gameState.consumeHighlightedHandPositions(); - for (Integer pos : handPositionsToClear) { - if (pos == null) continue; - Card card = gameState.getP1HandAtPos(pos); - if (card != null) { - BasicCommands.drawCard(out, card, pos, CARD_MODE_NORMAL); - } - } - //clear selection state - gameState.clearSelection(); - - } + game.selection.SelectionManager.clearSelectionAndHighlights(out, gameState); + } } diff --git a/ITSD-DT2025-26-Template/ITSD-DT2025-26-Template/app/events/TileClicked.java b/ITSD-DT2025-26-Template/ITSD-DT2025-26-Template/app/events/TileClicked.java index 9f8b1f7..12e40e8 100644 --- a/ITSD-DT2025-26-Template/ITSD-DT2025-26-Template/app/events/TileClicked.java +++ b/ITSD-DT2025-26-Template/ITSD-DT2025-26-Template/app/events/TileClicked.java @@ -5,6 +5,8 @@ import akka.actor.ActorRef; import structures.GameState; +import game.selection.SelectionGuards; +import game.selection.SelectionManager; import java.util.HashSet; import java.util.Set; @@ -24,8 +26,9 @@ */ public class TileClicked implements EventProcessor{ - @Override - public void processEvent(ActorRef out, GameState gameState, JsonNode message) { + @Override + public void processEvent(ActorRef out, GameState gameState, JsonNode message) { + if (gameState == null || !gameState.gameInitalised) return; int tilex = message.get("tilex").asInt(); int tiley = message.get("tiley").asInt(); @@ -74,4 +77,41 @@ public void processEvent(ActorRef out, GameState gameState, JsonNode message) { } -} + if (!SelectionGuards.isValidBoardTile(gameState, tilex, tiley)) return; + SelectionGuards.clearStaleSelectionIfNeeded(gameState); + //Assumption: human player is player 1 + int friendlyPlayerId = 1; + + //for friendly occupied tile use Abdullah's selection helper + if (SelectionManager.handleFriendlyUnitTileClick(out, gameState, tilex, tiley, friendlyPlayerId)) { + return; + } + + //if a selected unit exists and clicked tile is highlighted, + // Yang handles movement/combat from here + if (SelectionManager.isUnitSelected(gameState) + && SelectionGuards.isHighlightedTile(gameState, tilex, tiley)) { + + Integer selectedUnitId = gameState.getSelectedUnitId(); + if (selectedUnitId == null) { + SelectionManager.clearSelectionAndHighlights(out, gameState); + return; + } + + Integer clickedUnitId = gameState.board.getUnitIdAt(tilex, tiley); + + if (clickedUnitId == null) { + // Yang: movement logic here + // move selected unit to (tilex, tiley) + return; + } else { + // Yang: combat logic here + // selected unit attacks clicked unit + return; + } + } + + //Anything else would clear selection + SelectionManager.clearSelectionAndHighlights(out, gameState); + } +} \ No newline at end of file diff --git a/ITSD-DT2025-26-Template/ITSD-DT2025-26-Template/app/events/UnitClicked.java b/ITSD-DT2025-26-Template/ITSD-DT2025-26-Template/app/events/UnitClicked.java new file mode 100644 index 0000000..2e82ac7 --- /dev/null +++ b/ITSD-DT2025-26-Template/ITSD-DT2025-26-Template/app/events/UnitClicked.java @@ -0,0 +1,12 @@ +package events; + +//Core behaviour of this class is as follows: +//If nothing selected: +// select this unit +//If another unit selected: +// switch selection +//If same unit selected: +// cancel selection + +public class UnitClicked { +} diff --git a/ITSD-DT2025-26-Template/ITSD-DT2025-26-Template/app/game/Board.java b/ITSD-DT2025-26-Template/ITSD-DT2025-26-Template/app/game/Board.java index 799579f..9040cb4 100644 --- a/ITSD-DT2025-26-Template/ITSD-DT2025-26-Template/app/game/Board.java +++ b/ITSD-DT2025-26-Template/ITSD-DT2025-26-Template/app/game/Board.java @@ -8,7 +8,7 @@ public class Board { public static final int WIDTH = 9; public static final int HEIGHT = 5; - //I used 1 based coordinates here which means x = 1 to 9 and y = 1 to 5 + //I used 0 based coordinates here which means x = 0 to 8 and y = 0 to 4 private final int minX; private final int minY; diff --git a/ITSD-DT2025-26-Template/ITSD-DT2025-26-Template/app/game/selection/RangeFinder.java b/ITSD-DT2025-26-Template/ITSD-DT2025-26-Template/app/game/selection/RangeFinder.java new file mode 100644 index 0000000..a0fcca4 --- /dev/null +++ b/ITSD-DT2025-26-Template/ITSD-DT2025-26-Template/app/game/selection/RangeFinder.java @@ -0,0 +1,168 @@ +package game.selection; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import structures.GameState; + +public final class RangeFinder { + private RangeFinder() {} + public static List getAdjacentTiles(GameState gameState, int x, int y) { + return getCardinalAdjacentTiles(gameState, x, y); + } + public static List getCardinalAdjacentTiles(GameState gameState, int x, int y) { + List result = new ArrayList<>(); + if (gameState == null || gameState.board == null) return result; + addIfInBounds(gameState, result, x, y - 1); + addIfInBounds(gameState, result, x, y + 1); + addIfInBounds(gameState, result, x - 1, y); + addIfInBounds(gameState, result, x + 1, y); + return result; + } + public static List getAdjacentTilesIncludingDiagonal(GameState gameState, int x, int y) { + List result = new ArrayList<>(); + if (gameState == null || gameState.board == null) return result; + for (int dx = -1; dx <= 1; dx++) { + for (int dy = -1; dy <= 1; dy++) { + if (dx == 0 && dy == 0) continue; + addIfInBounds(gameState, result, x + dx, y + dy); + } + } + return result; + } + public static List getAdjacentFreeTiles(GameState gameState, int x, int y) { + List result = new ArrayList<>(); + if (gameState == null || gameState.board == null) return result; + for (GameState.Coord c : getCardinalAdjacentTiles(gameState, x, y)) { + if (!gameState.board.isOccupied(c.x, c.y)) { + result.add(c); + } + } + return result; + } + public static List getAdjacentOccupiedTiles(GameState gameState, int x, int y) { + List result = new ArrayList<>(); + if (gameState == null || gameState.board == null) return result; + for (GameState.Coord c : getCardinalAdjacentTiles(gameState, x, y)) { + if (gameState.board.isOccupied(c.x, c.y)) { + result.add(c); + } + } + return result; + } + public static List getAdjacentEnemyTiles(GameState gameState, int x, int y, int friendlyPlayerId) { + return filterEnemyTiles(gameState, getCardinalAdjacentTiles(gameState, x, y), friendlyPlayerId); + } + public static List getAdjacentAllyTiles(GameState gameState, int x, int y, int friendlyPlayerId) { + List result = new ArrayList<>(); + if (gameState == null || gameState.board == null) return result; + for (GameState.Coord c : getAdjacentOccupiedTiles(gameState, x, y)) { + Integer unitId = gameState.board.getUnitIdAt(c.x, c.y); + if (unitId == null) continue; + + Integer ownerPlayerId = gameState.getUnitOwnerPlayerId(unitId); + if (ownerPlayerId != null && ownerPlayerId.intValue() == friendlyPlayerId) { + result.add(c); + } + } + return result; + } + public static List getAdjacentFreeOrEnemyTiles(GameState gameState, int x, int y, int friendlyPlayerId) { + List result = new ArrayList<>(); + if (gameState == null || gameState.board == null) return result; + for (GameState.Coord c : getCardinalAdjacentTiles(gameState, x, y)) { + if (!gameState.board.isOccupied(c.x, c.y)) { + result.add(c); + continue; + } + Integer unitId = gameState.board.getUnitIdAt(c.x, c.y); + if (unitId == null) continue; + Integer ownerPlayerId = gameState.getUnitOwnerPlayerId(unitId); + if (ownerPlayerId != null && ownerPlayerId.intValue() != friendlyPlayerId) { + result.add(c); + } + } + return result; + } + /** + * Standard movement: + * - 1 or 2 tiles in cardinal directions + * - 1 tile diagonally + * - destination must be free + * - 2-step cardinal move cannot jump through an occupied middle tile + */ + public static List getStandardMoveTiles(GameState gameState, int x, int y) { + Set result = new LinkedHashSet<>(); + if (gameState == null || gameState.board == null) return new ArrayList<>(result); + addIfFree(gameState, result, x - 1, y); + addIfFree(gameState, result, x + 1, y); + addIfFree(gameState, result, x, y - 1); + addIfFree(gameState, result, x, y + 1); + addTwoStepCardinalIfFree(gameState, result, x, y, -1, 0); + addTwoStepCardinalIfFree(gameState, result, x, y, 1, 0); + addTwoStepCardinalIfFree(gameState, result, x, y, 0, -1); + addTwoStepCardinalIfFree(gameState, result, x, y, 0, 1); + addIfFree(gameState, result, x - 1, y - 1); + addIfFree(gameState, result, x - 1, y + 1); + addIfFree(gameState, result, x + 1, y - 1); + addIfFree(gameState, result, x + 1, y + 1); + + return new ArrayList<>(result); + } + public static List getStandardAttackTiles(GameState gameState, int x, int y, int friendlyPlayerId) { + return filterEnemyTiles(gameState, getAdjacentTilesIncludingDiagonal(gameState, x, y), friendlyPlayerId); + } + + public static List getStandardMoveThenAttackTiles(GameState gameState, int x, int y, int friendlyPlayerId) { + Set result = new LinkedHashSet<>(); + if (gameState == null || gameState.board == null) return new ArrayList<>(result); + result.addAll(getStandardAttackTiles(gameState, x, y, friendlyPlayerId)); + for (GameState.Coord moveTile : getStandardMoveTiles(gameState, x, y)) { + result.addAll(getStandardAttackTiles(gameState, moveTile.x, moveTile.y, friendlyPlayerId)); + } + return new ArrayList<>(result); + } + + public static boolean isOrthogonallyAdjacent(int x1, int y1, int x2, int y2) { + int dx = Math.abs(x1 - x2); + int dy = Math.abs(y1 - y2); + return dx + dy == 1; + } + private static List filterEnemyTiles(GameState gameState, Iterable coords, int friendlyPlayerId) { + List result = new ArrayList<>(); + if (gameState == null || gameState.board == null || coords == null) return result; + + for (GameState.Coord c : coords) { + if (c == null) continue; + + Integer unitId = gameState.board.getUnitIdAt(c.x, c.y); + if (unitId == null) continue; + + Integer ownerPlayerId = gameState.getUnitOwnerPlayerId(unitId); + if (ownerPlayerId != null && ownerPlayerId.intValue() != friendlyPlayerId) { + result.add(c); + } + } + return result; + } + private static void addIfInBounds(GameState gameState, List result, int x, int y) { + if (gameState.board.inBounds(x, y)) { + result.add(new GameState.Coord(x, y)); + } + } + private static void addIfFree(GameState gameState, Set result, int x, int y) { + if (!gameState.board.inBounds(x, y)) return; + if (gameState.board.isOccupied(x, y)) return; + result.add(new GameState.Coord(x, y)); + } + private static void addTwoStepCardinalIfFree(GameState gameState, Set result, int x, int y, int dx, int dy) { + int midX = x + dx; + int midY = y + dy; + int targetX = x + (dx * 2); + int targetY = y + (dy * 2); + if (!gameState.board.inBounds(midX, midY) || !gameState.board.inBounds(targetX, targetY)) return; + if (gameState.board.isOccupied(midX, midY)) return; + if (gameState.board.isOccupied(targetX, targetY)) return; + result.add(new GameState.Coord(targetX, targetY)); + } +} \ No newline at end of file diff --git a/ITSD-DT2025-26-Template/ITSD-DT2025-26-Template/app/game/selection/SelectionGuards.java b/ITSD-DT2025-26-Template/ITSD-DT2025-26-Template/app/game/selection/SelectionGuards.java new file mode 100644 index 0000000..fbfcc55 --- /dev/null +++ b/ITSD-DT2025-26-Template/ITSD-DT2025-26-Template/app/game/selection/SelectionGuards.java @@ -0,0 +1,135 @@ +package game.selection; + +import structures.GameState; +import structures.basic.Card; + +/* + * Okay so this should check whether a selection-related action is safe before the game tries to use it. + * Check if a hand position is valid + * If the hand position is between 1 and 6, allow it. + * Otherwise reject it. + * Check if a card actually exists in that hand slot + * If the slot is valid and contains a card, return true. + * Otherwise return false. + * Check if a board tile is valid + * If the tile is inside the board boundaries, allow it. + * Otherwise reject it. + * Check if a tile is currently highlighted + * Look in the stored highlighted tiles. + * If the tile is there, return true. + * Check if a hand position is highlighted + * Look in the stored highlighted hand positions. + * If it is there, return true. + * Check if a unit is currently selected + * If selection mode is UNIT_SELECTED and there is a selected unit id, return true. + * Check if a card is currently selected + * If selection mode is CARD_SELECTED_TARGETING and the selected hand position still contains a card, return true. + * Check if there is no selection + * If selection mode is NONE, return true. + * Check if the current selection has become stale or invalid + * For example: + * selected card position is empty + * selected unit id is missing + * selection mode says something is selected but the actual object is gone + * If selection is stale, clear it + * Reset selection and highlights so the game does not get stuck or crash. + * Check if the clicked card is the same card already selected + * If yes, return true. + * Check if the clicked unit is the same unit already selected + * If yes, return true. + * Check if anything is highlighted at all + * If highlighted tiles or highlighted hand positions are not empty, return true. + */ + +/** + * Defensive guard helpers for selection and targeting flows. + * These methods are intentionally simple and side-effect free where possible. + * Purpose: + * - prevent invalid clicks/positions from corrupting state + * - provide one place for common safety checks + * - support SC-E2 basics (illegal actions never corrupt state / crash game) + */ +public final class SelectionGuards { + + private SelectionGuards() {} + + public static boolean isValidHandPosition(int handPos) { + return handPos >= 1 && handPos <= 6; + } + public static boolean hasCardInHand(GameState gameState, int handPos) { + if (gameState == null) return false; + if (!isValidHandPosition(handPos)) return false; + + Card card = gameState.getP1HandAtPos(handPos); + return card != null; + } + public static boolean isValidBoardTile(GameState gameState, int x, int y) { + if (gameState == null || gameState.board == null) return false; + return gameState.board.inBounds(x, y); + } + + public static boolean isHighlightedTile(GameState gameState, int x, int y) { + if (!isValidBoardTile(gameState, x, y)) return false; + return gameState.selection.highlightedTiles.contains(new GameState.Coord(x, y)); + } + public static boolean isHighlightedHandPosition(GameState gameState, int handPos) { + if (gameState == null) return false; + if (!isValidHandPosition(handPos)) return false; + return gameState.selection.highlightedHandPositions.contains(handPos); + } + + public static boolean hasSelectedUnit(GameState gameState) { + return gameState != null + && gameState.getSelectionMode() == GameState.SelectionState.Mode.UNIT_SELECTED + && gameState.getSelectedUnitId() != null; + } + public static boolean hasSelectedCard(GameState gameState) { + return gameState != null + && gameState.getSelectionMode() == GameState.SelectionState.Mode.CARD_SELECTED_TARGETING + && gameState.getSelectedHandPos() != null + && hasCardInHand(gameState, gameState.getSelectedHandPos()); + } + + public static boolean hasNoSelection(GameState gameState) { + return gameState == null + || gameState.getSelectionMode() == GameState.SelectionState.Mode.NONE; + } + + public static boolean hasStaleSelection(GameState gameState) { + if (gameState == null) return true; + GameState.SelectionState.Mode mode = gameState.getSelectionMode(); + if (mode == GameState.SelectionState.Mode.NONE) return false; + if (mode == GameState.SelectionState.Mode.UNIT_SELECTED) { + Integer selectedUnitId = gameState.getSelectedUnitId(); + if (selectedUnitId == null) return true; + return !gameState.isUnitOnBoard(selectedUnitId); + } + if (mode == GameState.SelectionState.Mode.CARD_SELECTED_TARGETING) { + Integer selectedHandPos = gameState.getSelectedHandPos(); + if (selectedHandPos == null) return true; + return !hasCardInHand(gameState, selectedHandPos); + } + return true; + } + public static void clearStaleSelectionIfNeeded(GameState gameState) { + if (gameState == null) return; + if (hasStaleSelection(gameState)) { + gameState.clearAllSelectionsAndHighlights(); + } + } + public static boolean isSameSelectedCard(GameState gameState, int handPos) { + return hasSelectedCard(gameState) + && gameState.getSelectedHandPos() != null + && gameState.getSelectedHandPos().intValue() == handPos; + } + public static boolean isSameSelectedUnit(GameState gameState, int unitId) { + return hasSelectedUnit(gameState) + && gameState.getSelectedUnitId() != null + && gameState.getSelectedUnitId().intValue() == unitId; + } + public static boolean hasAnyHighlights(GameState gameState) { + return gameState != null + && (!gameState.selection.highlightedTiles.isEmpty() + || !gameState.selection.highlightedHandPositions.isEmpty()); + } +} \ No newline at end of file diff --git a/ITSD-DT2025-26-Template/ITSD-DT2025-26-Template/app/game/selection/SelectionManager.java b/ITSD-DT2025-26-Template/ITSD-DT2025-26-Template/app/game/selection/SelectionManager.java new file mode 100644 index 0000000..53a73e5 --- /dev/null +++ b/ITSD-DT2025-26-Template/ITSD-DT2025-26-Template/app/game/selection/SelectionManager.java @@ -0,0 +1,182 @@ +package game.selection; + +import java.util.Set; + +import akka.actor.ActorRef; +import commands.BasicCommands; +import game.ui.UiConstants; +import structures.GameState; +import structures.basic.Card; +import structures.basic.Tile; + +/** + * Event files should call into this class instead of duplicating selection logic. + */ +public final class SelectionManager { + + private SelectionManager() {} + + /* + * Full cancel! clears board highlights, hand highlights, and selection state. + */ + public static void clearSelectionAndHighlights(ActorRef out, GameState gameState) { + if (gameState == null) return; + + clearTileHighlights(out, gameState); + clearHandHighlights(out, gameState); + gameState.clearSelection(); + } + public static void clearTileHighlights(ActorRef out, GameState gameState) { + if (gameState == null) return; + + Set tilesToClear = gameState.consumeHighlightedTiles(); + for (GameState.Coord c : tilesToClear) { + Tile tile = gameState.board.getTile(c.x, c.y); + if (tile != null) { + BasicCommands.drawTile(out, tile, UiConstants.TILE_NORMAL); + } + } + } + public static void clearHandHighlights(ActorRef out, GameState gameState) { + if (gameState == null) return; + + Set handToClear = gameState.consumeHighlightedHandPositions(); + for (Integer pos : handToClear) { + if (pos == null) continue; + + Card card = gameState.getP1HandAtPos(pos); + if (card != null) { + BasicCommands.drawCard(out, card, pos, UiConstants.CARD_NORMAL); + } + } + } + + public static void selectCard(ActorRef out, GameState gameState, int handPos) { + if (gameState == null) return; + + clearSelectionAndHighlights(out, gameState); + + Card card = gameState.getP1HandAtPos(handPos); + if (card == null) return; + + gameState.selectHandCardForTargeting(handPos); + gameState.addHighlightedHandPos(handPos); + BasicCommands.drawCard(out, card, handPos, UiConstants.CARD_SELECTED); + } + + /* + * Keep the same card selected while refreshing targeting tiles. + * Use this when the same selected card needs its valid board targets redrawn. It clears board + * highlights only, not the card selection itself. + */ + public static void refreshCardTargeting(ActorRef out, GameState gameState, int handPos) { + if (gameState == null) return; + + if (!SelectionGuards.hasCardInHand(gameState, handPos)) { + clearSelectionAndHighlights(out, gameState); + return; + } + + if (!SelectionGuards.isSameSelectedCard(gameState, handPos)) { + selectCard(out, gameState, handPos); + return; + } + + clearTileHighlights(out, gameState); + redrawSelectedCardIfNeeded(out, gameState); + } + /* + * Re-draw the currently selected card in selected mode. + * to be used after tile refresh or cleanup. + */ + public static void redrawSelectedCardIfNeeded(ActorRef out, GameState gameState) { + if (gameState == null) return; + if (!isCardSelected(gameState)) return; + + Integer handPos = gameState.getSelectedHandPos(); + if (handPos == null) { + clearSelectionAndHighlights(out, gameState); + return; + } + + Card card = gameState.getP1HandAtPos(handPos); + if (card == null) { + clearSelectionAndHighlights(out, gameState); + return; + } + + gameState.addHighlightedHandPos(handPos); + BasicCommands.drawCard(out, card, handPos, UiConstants.CARD_SELECTED); + } + public static void selectUnit(ActorRef out, GameState gameState, int unitId) { + if (gameState == null) return; + + clearSelectionAndHighlights(out, gameState); + gameState.selectUnit(unitId); + } + /* + * to be used from TileClicked when a friendly occupied tile is clicked. + * should return true if the click was handled as a friendly-unit selection action. + */ + public static boolean handleFriendlyUnitTileClick( + ActorRef out, + GameState gameState, + int tileX, + int tileY, + int friendlyPlayerId + ) { + if (gameState == null) return false; + if (!SelectionGuards.isValidBoardTile(gameState, tileX, tileY)) return false; + + Integer unitId = gameState.board.getUnitIdAt(tileX, tileY); + if (unitId == null) return false; + + Integer ownerPlayerId = gameState.getUnitOwnerPlayerId(unitId); + if (ownerPlayerId == null || ownerPlayerId.intValue() != friendlyPlayerId) return false; + + if (SelectionGuards.isSameSelectedUnit(gameState, unitId)) { + clearSelectionAndHighlights(out, gameState); + return true; + } + + selectUnit(out, gameState, unitId); + renderStandardUnitActionHighlights(out, gameState, unitId, friendlyPlayerId); + return true; + } + + /* + * Draw standard movement and attack reach for the selected unit. + * Green means legal move tile + * Red means enemy target tile + */ + public static void renderStandardUnitActionHighlights( + ActorRef out, + GameState gameState, + int unitId, + int friendlyPlayerId + ) { + if (gameState == null) return; + GameState.Coord unitPos = gameState.findUnitCoord(unitId); + if (unitPos == null) { + clearSelectionAndHighlights(out, gameState); + return; + } + TargetingOverlay.clearTileHighlights(out, gameState); + TargetingOverlay.appendMoveHighlights( + out, gameState, RangeFinder.getStandardMoveTiles(gameState, unitPos.x, unitPos.y) + ); + TargetingOverlay.appendAttackHighlights( + out, gameState, RangeFinder.getStandardMoveThenAttackTiles(gameState, unitPos.x, unitPos.y, friendlyPlayerId) + ); + } + + public static boolean isCardSelected(GameState gameState) { + return gameState != null + && gameState.getSelectionMode() == GameState.SelectionState.Mode.CARD_SELECTED_TARGETING; + } + + public static boolean isUnitSelected(GameState gameState) { + return gameState != null + && gameState.getSelectionMode() == GameState.SelectionState.Mode.UNIT_SELECTED; + } +} \ No newline at end of file diff --git a/ITSD-DT2025-26-Template/ITSD-DT2025-26-Template/app/game/selection/TargetingOverlay.java b/ITSD-DT2025-26-Template/ITSD-DT2025-26-Template/app/game/selection/TargetingOverlay.java new file mode 100644 index 0000000..b135e8e --- /dev/null +++ b/ITSD-DT2025-26-Template/ITSD-DT2025-26-Template/app/game/selection/TargetingOverlay.java @@ -0,0 +1,80 @@ +package game.selection; + +import akka.actor.ActorRef; +import commands.BasicCommands; +import game.ui.UiConstants; +import structures.GameState; +import structures.basic.Tile; + +/** + * The purpose of this class is to handle drawing and registering tile highlights for movement, + * attacks, and targeting. It will only manage overlay rendering and state bookkeeping. + * Note: It does not decide game legality. + */ +public final class TargetingOverlay { + + private TargetingOverlay() {} + public static void clearAll(ActorRef out, GameState gameState) { + SelectionManager.clearSelectionAndHighlights(out, gameState); + } + public static void clearTileHighlights(ActorRef out, GameState gameState) { + SelectionManager.clearTileHighlights(out, gameState); + } + public static void highlightMoveTile(ActorRef out, GameState gameState, int x, int y) { + highlightTile(out, gameState, x, y, UiConstants.TILE_MOVE); + } + public static void highlightAttackTile(ActorRef out, GameState gameState, int x, int y) { + highlightTile(out, gameState, x, y, UiConstants.TILE_ATTACK); + } + public static void highlightTargetTile(ActorRef out, GameState gameState, int x, int y) { + highlightTile(out, gameState, x, y, UiConstants.TILE_ATTACK); + } + public static void showMoveHighlights(ActorRef out, GameState gameState, Iterable coords) { + clearTileHighlights(out, gameState); + appendMoveHighlights(out, gameState, coords); + } + public static void showAttackHighlights(ActorRef out, GameState gameState, Iterable coords) { + clearTileHighlights(out, gameState); + appendAttackHighlights(out, gameState, coords); + } + public static void showTargetHighlights(ActorRef out, GameState gameState, Iterable coords) { + clearTileHighlights(out, gameState); + appendTargetHighlights(out, gameState, coords); + } + public static void appendMoveHighlights(ActorRef out, GameState gameState, Iterable coords) { + if (coords == null) return; + + for (GameState.Coord c : coords) { + if (c == null) continue; + highlightMoveTile(out, gameState, c.x, c.y); + } + } + + public static void appendAttackHighlights(ActorRef out, GameState gameState, Iterable coords) { + if (coords == null) return; + + for (GameState.Coord c : coords) { + if (c == null) continue; + highlightAttackTile(out, gameState, c.x, c.y); + } + } + + public static void appendTargetHighlights(ActorRef out, GameState gameState, Iterable coords) { + if (coords == null) return; + + for (GameState.Coord c : coords) { + if (c == null) continue; + highlightTargetTile(out, gameState, c.x, c.y); + } + } + private static void highlightTile(ActorRef out, GameState gameState, int x, int y, int mode) { + if (gameState == null) return; + if (!gameState.board.inBounds(x, y)) return; + + Tile tile = gameState.board.getTile(x, y); + if (tile == null) return; + + gameState.addHighlightedTile(x, y); + BasicCommands.drawTile(out, tile, mode); + } +} \ No newline at end of file diff --git a/ITSD-DT2025-26-Template/ITSD-DT2025-26-Template/app/game/ui/UiConstants.java b/ITSD-DT2025-26-Template/ITSD-DT2025-26-Template/app/game/ui/UiConstants.java new file mode 100644 index 0000000..1b7a0f4 --- /dev/null +++ b/ITSD-DT2025-26-Template/ITSD-DT2025-26-Template/app/game/ui/UiConstants.java @@ -0,0 +1,17 @@ +package game.ui; + +/** + * Shared UI render modes. + * Use these instead of hardcoded numbers in event/logic files. + */ +public final class UiConstants { + //tile highlight modes + public static final int TILE_NORMAL = 0; + public static final int TILE_MOVE = 1; + public static final int TILE_ATTACK = 2; + //card render modes + public static final int CARD_NORMAL = 0; + public static final int CARD_SELECTED = 1; + + private UiConstants() {} +} diff --git a/ITSD-DT2025-26-Template/ITSD-DT2025-26-Template/app/structures/GameState.java b/ITSD-DT2025-26-Template/ITSD-DT2025-26-Template/app/structures/GameState.java index db3d558..71af9bb 100644 --- a/ITSD-DT2025-26-Template/ITSD-DT2025-26-Template/app/structures/GameState.java +++ b/ITSD-DT2025-26-Template/ITSD-DT2025-26-Template/app/structures/GameState.java @@ -1,9 +1,12 @@ package structures; import java.util.HashSet; +import java.util.HashMap; +import java.util.Map; import java.util.Objects; import java.util.Set; import game.Board; +import structures.basic.Tile; import structures.basic.Card; /** @@ -14,25 +17,58 @@ * */ public class GameState { + //small helper to get tiles + public Tile getTile(int x, int y) { + return board.getTile(x, y); + } + public Integer getSelectedUnitId() { + return selection.selectedUnitId; + } + public Integer getSelectedHandPos() { + return selection.selectedHandPos; + } + public Coord findUnitCoord(int unitId) { + for (int x = board.minX(); x <= board.maxX(); x++) { + for (int y = board.minY(); y <= board.maxY(); y++) { + Integer current = board.getUnitIdAt(x, y); + if (current != null && current.intValue() == unitId) { + return new Coord(x, y); + } + } + } + return null; + } + public boolean isUnitOnBoard(int unitId) { + return findUnitCoord(unitId) != null; + } + public void registerUnitOwner(int unitId, int ownerPlayerId) { + unitOwnerById.put(unitId, ownerPlayerId); + } + public Integer getUnitOwnerPlayerId(int unitId) { + return unitOwnerById.get(unitId); + } + public void removeUnitOwner(int unitId) { + unitOwnerById.remove(unitId); + } public Set unitsActed; public boolean gameInitalised = false; public boolean something = false; public final SelectionState selection = new SelectionState(); - - //Store the current Player 1 hand so we can redraw cards (unhighlight) reliably. - //Index 0 to 5 relates to UI positions 1 to 6. - //If we implement a full Hand model later on, we can replace this + //When we implement a full Hand model later on, we can replace this public final Card[] p1Hand = new Card[6]; - public final Board board = new Board(); //default 1 based coordinates + public final Board board = new Board(0,0); //default 0 based coordinates + public final Map unitOwnerById = new HashMap<>(); + //we call these once during initialize public void resetForNewGame() { gameInitalised = true; selection.reset(); clearP1Hand(); board.clearOccupancy(); //added to reset unit placements without recreating tiles + unitOwnerById.clear(); } public void clearP1Hand() {