package lect78_impl.view; import java.awt.*; import java.awt.event.*; import java.util.*; import java.util.List; import javax.swing.*; import javax.swing.Timer; import lect78_impl.controller.Controls; import lect78_impl.controller.Game; import lect78_impl.model.*; /** * @overview A panel that handles game rendering and input for real-time gameplay */ public class GamePanel extends JPanel { private Game game; private Map keyStates; private boolean isFirstPaint; // Current projectile type selection for each player private Map playerProjectileTypes; // View-related state - tanks with visible range circles private Map tanksShowingRange; // Map tank ID to time when range should hide private static final long RANGE_DISPLAY_DURATION = 3000; // 3 seconds to show range // Sprite cache to avoid creating new sprites repeatedly private Map spriteCache; private static final int SPRITE_SIZE = 50; private static final int PROJECTILE_SIZE = 20; /** * @effects Initialize this as a new GamePanel */ public GamePanel() { this.keyStates = new HashMap<>(); this.isFirstPaint = true; this.tanksShowingRange = new HashMap<>(); this.spriteCache = new HashMap<>(); this.playerProjectileTypes = new HashMap<>(); // Set panel size based on board size int gridSize = 8; // Default grid size int cellSize = Board.CELL_SIZE; setPreferredSize(new Dimension(gridSize * cellSize, gridSize * cellSize)); setBackground(Color.WHITE); setFocusable(true); setupInputHandling(); // Start a timer to clean up expired range displays Timer rangeCleanupTimer = new Timer(100, e -> cleanupExpiredRanges()); rangeCleanupTimer.start(); } /** * @effects Removes expired range displays */ private void cleanupExpiredRanges() { long currentTime = System.currentTimeMillis(); Set expiredTanks = new HashSet<>(); // Find expired range displays for (Map.Entry entry : tanksShowingRange.entrySet()) { if (currentTime > entry.getValue()) { expiredTanks.add(entry.getKey()); } } // Remove expired entries if (!expiredTanks.isEmpty()) { for (Integer tankId : expiredTanks) { tanksShowingRange.remove(tankId); } // Repaint to update display repaint(); } } /** * @effects Sets the game for this panel to render */ public void setGame(Game game) { this.game = game; } /** * @effects Gets or creates a projectile sprite */ private Sprite getProjectileSprite(String projectileType) { String imagePath = "lect78_impl/assets/images/"; switch (projectileType) { case "AP": imagePath += "ap_projectile.png"; break; case "HE": imagePath += "he_projectile.png"; break; default: imagePath += "standard_projectile.png"; } String cacheKey = "projectile_" + projectileType; if (!spriteCache.containsKey(cacheKey)) { spriteCache.put(cacheKey, new Sprite(imagePath, PROJECTILE_SIZE, PROJECTILE_SIZE)); } return spriteCache.get(cacheKey); } /** * @effects Gets or creates a sprite for the given path */ private Sprite getSprite(String path) { if (!spriteCache.containsKey(path)) { spriteCache.put(path, new Sprite(path, SPRITE_SIZE, SPRITE_SIZE)); } return spriteCache.get(path); } private void setupInputHandling() { addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (game == null) return; // Convert mouse coordinates to grid coordinates int gridX = e.getX() / Board.CELL_SIZE; int gridY = e.getY() / Board.CELL_SIZE; System.out.println("Grid cell clicked: (" + gridX + ", " + gridY + ")"); // Get clicked cell Position clickedPos = new Position(gridX, gridY); Cell clickedCell = game.getBoard().getCell(clickedPos); if (clickedCell != null && !clickedCell.isEmpty()) { Tank clickedTank = clickedCell.getTank(); showTankInfo(clickedTank); } } }); addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { keyStates.put(e.getKeyCode(), true); // Projectile type switching - Player 1 (1,2,3 keys) if (e.getKeyCode() == KeyEvent.VK_1) { playerProjectileTypes.put(1, "Standard"); System.out.println("Player 1 switched to Standard projectiles"); } else if (e.getKeyCode() == KeyEvent.VK_2) { playerProjectileTypes.put(1, "AP"); System.out.println("Player 1 switched to AP projectiles"); } else if (e.getKeyCode() == KeyEvent.VK_3) { playerProjectileTypes.put(1, "HE"); System.out.println("Player 1 switched to HE projectiles"); } // Projectile type switching - Player 2 (NumPad 1,2,3 keys) if (e.getKeyCode() == KeyEvent.VK_NUMPAD1) { playerProjectileTypes.put(2, "Standard"); System.out.println("Player 2 switched to Standard projectiles"); } else if (e.getKeyCode() == KeyEvent.VK_NUMPAD2) { playerProjectileTypes.put(2, "AP"); System.out.println("Player 2 switched to AP projectiles"); } else if (e.getKeyCode() == KeyEvent.VK_NUMPAD3) { playerProjectileTypes.put(2, "HE"); System.out.println("Player 2 switched to HE projectiles"); } } @Override public void keyReleased(KeyEvent e) { keyStates.put(e.getKeyCode(), false); } }); } /** * @effects Shows information about a tank and displays its range */ private void showTankInfo(Tank tank) { if (tank == null) return; System.out.println("Tank info: " + tank + " at position " + tank.getPosition()); // Show range for this tank showTankRange(tank); } /** * @effects Shows the range circle for a tank */ private void showTankRange(Tank tank) { // Set range display to expire after the duration tanksShowingRange.put(tank.getId(), System.currentTimeMillis() + RANGE_DISPLAY_DURATION); // Repaint to show the range immediately repaint(); } /** * @effects Updates the game state based on input */ public void update() { if (game == null) return; // First, let the game update its state (projectiles, etc.) game.update(); // Process input for all players simultaneously processAllPlayersInput(); // Redraw the panel repaint(); } /** * @effects Processes input for all players */ private void processAllPlayersInput() { if (game.getGameState() != Game.GameState.RUNNING) return; // Process input for each player for (Player player : game.getPlayers()) { processPlayerInput(player); } } /** * @effects Processes input for a specific player */ private void processPlayerInput(Player player) { Tank activeTank = player.getActiveTank(); if (activeTank == null || activeTank.isDestroyed()) return; Controls controls = player.getControls(); // Handle movement boolean moved = false; if (isKeyPressed(controls.getMoveUp()) && activeTank.canMove()) { Position newPos = activeTank.calculateMovementPosition(true); moved = game.processMovement(player, newPos); } else if (isKeyPressed(controls.getMoveDown()) && activeTank.canMove()) { Position newPos = activeTank.calculateMovementPosition(false); moved = game.processMovement(player, newPos); } // Handle rotation if (isKeyPressed(controls.getRotateLeft()) && activeTank.canRotate()) { activeTank.rotate(false); } else if (isKeyPressed(controls.getRotateRight()) && activeTank.canRotate()) { activeTank.rotate(true); } // Handle firing if (isKeyPressed(controls.getFireKey())) { System.out.println("Fire key pressed for Player " + player.getId()); System.out.println("- Active tank: " + activeTank); System.out.println("- Can fire: " + activeTank.canFire()); String projectileType = playerProjectileTypes.getOrDefault(player.getId(), "Standard"); System.out.println("- Projectile type: " + projectileType); boolean fired = game.processShot(player, projectileType); System.out.println("- Shot processed: " + fired); if (fired) { showTankRange(activeTank); } } } /** * @effects Checks if a key is currently pressed */ private boolean isKeyPressed(int keyCode) { return keyStates.getOrDefault(keyCode, false); } /** * @effects Checks if a tank's range is currently being shown */ private boolean isTankRangeVisible(int tankId) { return tanksShowingRange.containsKey(tankId); } /** * @effects Draw the 8x8 grid with coordinates */ private void drawGrid(Graphics2D g2d) { int cellSize = Board.CELL_SIZE; int gridSize = game.getBoard().getSize(); // Set grid line properties g2d.setStroke(new BasicStroke(2)); g2d.setColor(Color.BLACK); // Draw vertical lines for (int i = 0; i <= gridSize; i++) { int x = i * cellSize; g2d.drawLine(x, 0, x, gridSize * cellSize); } // Draw horizontal lines for (int i = 0; i <= gridSize; i++) { int y = i * cellSize; g2d.drawLine(0, y, gridSize * cellSize, y); } // Draw coordinates in each cell g2d.setFont(new Font("Arial", Font.PLAIN, 12)); for (int y = 0; y < gridSize; y++) { for (int x = 0; x < gridSize; x++) { String coord = "(" + x + "," + y + ")"; FontMetrics metrics = g2d.getFontMetrics(); int textX = x * cellSize + (cellSize - metrics.stringWidth(coord)) / 2; int textY = y * cellSize + (cellSize + metrics.getHeight()) / 2; g2d.drawString(coord, textX, textY); } } } /** * @effects Draw the tanks on the board */ private void drawTanks(Graphics2D g2d) { if (game == null) return; for (Player player : game.getPlayers()) { for (Tank tank : player.getTankSet().getAllTanks()) { if (!tank.isDestroyed()) { // Draw range circle if active for this tank if (isTankRangeVisible(tank.getId())) { drawTankRange(g2d, tank); } // Get and position the sprite Sprite sprite = getSprite(tank.getSpritePath()); updateSpritePosition(sprite, tank); sprite.setRotation(tank.getAngle()); // Draw the tank sprite sprite.draw(g2d); // Draw tank info Position pos = tank.getPosition(); int cellSize = Board.CELL_SIZE; int x = pos.getX() * cellSize; int y = pos.getY() * cellSize; g2d.setColor(Color.BLACK); g2d.setFont(new Font("Arial", Font.BOLD, 12)); String info = "P" + player.getId() + " HP:" + tank.getCurrentHP(); g2d.drawString(info, x + 5, y + cellSize - 5); // Draw selected projectile type String projectileType = playerProjectileTypes.getOrDefault(player.getId(), "Standard"); String typeLabel = "STD"; if (projectileType.equals("AP")) { typeLabel = "AP"; } else if (projectileType.equals("HE")) { typeLabel = "HE"; } g2d.setColor(Color.BLUE); g2d.drawString(typeLabel, x + cellSize - 25, y + cellSize - 5); // Draw bounds for debugging Rectangle bounds = sprite.getBounds(); g2d.setColor(Color.RED); g2d.drawRect(bounds.x, bounds.y, bounds.width, bounds.height); } } } } /** * @effects Updates a sprite's position based on a tank's position */ private void updateSpritePosition(Sprite sprite, Tank tank) { Position pos = tank.getPosition(); int cellSize = Board.CELL_SIZE; int centerX = pos.getX() * cellSize + (cellSize - SPRITE_SIZE) / 2; int centerY = pos.getY() * cellSize + (cellSize - SPRITE_SIZE) / 2; sprite.setPosition(centerX, centerY); } /** * @effects Draw the range circle around a tank */ private void drawTankRange(Graphics2D g2d, Tank tank) { // Calculate range circle parameters int cellSize = Board.CELL_SIZE; int rangeDiameter = tank.getRange() * 2 * cellSize; Position pos = tank.getPosition(); int centerX = pos.getX() * cellSize + cellSize / 2; int centerY = pos.getY() * cellSize + cellSize / 2; // Draw semi-transparent range circle with increased opacity g2d.setColor(new Color(255, 0, 0, 128)); // More visible red g2d.fillOval( centerX - rangeDiameter/2, centerY - rangeDiameter/2, rangeDiameter, rangeDiameter ); // Draw range number and stats g2d.setColor(Color.BLACK); g2d.setFont(new Font("Arial", Font.BOLD, 14)); String stats = String.format("Range: %d HP: %d/%d DMG: %d ARM: %d", tank.getRange(), tank.getCurrentHP(), tank.getHitPoint(), tank.getDamage(), tank.getArmor()); g2d.drawString(stats, centerX - 120, centerY + rangeDiameter/2 + 20); } /** * @effects Draw the projectiles */ private void drawProjectiles(Graphics2D g2d) { if (game == null) return; for (Projectile proj : game.getProjectiles()) { // Determine projectile type and get appropriate sprite String projectileType = "Standard"; if (proj instanceof ProjectileAP) { projectileType = "AP"; } else if (proj instanceof ProjectileHE) { projectileType = "HE"; } Sprite projSprite = getProjectileSprite(projectileType); // Position the sprite int cellSize = Board.CELL_SIZE; Position pos = proj.getPosition(); int x = pos.getX() * cellSize + (cellSize - PROJECTILE_SIZE) / 2; int y = pos.getY() * cellSize + (cellSize - PROJECTILE_SIZE) / 2; projSprite.setPosition(x, y); projSprite.setRotation(proj.getAngle()); // Draw the projectile projSprite.draw(g2d); } } /** * @effects Draw game over message if applicable */ private void drawGameOverMessage(Graphics2D g2d) { if (game == null || game.getGameState() != Game.GameState.GAME_OVER) return; // Draw semi-transparent overlay g2d.setColor(new Color(0, 0, 0, 128)); g2d.fillRect(0, 0, getWidth(), getHeight()); // Draw game over message g2d.setColor(Color.WHITE); g2d.setFont(new Font("Arial", Font.BOLD, 36)); FontMetrics metrics = g2d.getFontMetrics(); String message = "GAME OVER"; int x = (getWidth() - metrics.stringWidth(message)) / 2; int y = getHeight() / 2 - 20; g2d.drawString(message, x, y); // Draw winner message Player winner = game.getWinner(); String winnerMessage = (winner != null) ? "Player " + winner.getId() + " wins!" : "It's a draw!"; g2d.setFont(new Font("Arial", Font.BOLD, 24)); metrics = g2d.getFontMetrics(); x = (getWidth() - metrics.stringWidth(winnerMessage)) / 2; y = getHeight() / 2 + 20; g2d.drawString(winnerMessage, x, y); } /** * @effects Draw game controls and help info */ private void drawControlsInfo(Graphics2D g2d) { g2d.setColor(Color.BLACK); g2d.setFont(new Font("Arial", Font.PLAIN, 12)); // Player 1 controls String p1Controls = "Player 1: WASD to move/rotate, SPACE to fire. 1,2,3 to change projectile type"; g2d.drawString(p1Controls, 10, getHeight() - 30); // Player 2 controls String p2Controls = "Player 2: Arrow keys to move/rotate, ENTER to fire. NumPad 1,2,3 to change projectile type"; g2d.drawString(p2Controls, 10, getHeight() - 15); } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2d = (Graphics2D) g; // Enable anti-aliasing g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); // Draw background g2d.setColor(Color.LIGHT_GRAY); g2d.fillRect(0, 0, getWidth(), getHeight()); // Draw the grid drawGrid(g2d); if (game != null) { // Debug information if (isFirstPaint) { System.out.println("First paint - Panel size: " + getWidth() + "x" + getHeight()); System.out.println("Number of players: " + game.getPlayers().size()); isFirstPaint = false; } // Draw game elements drawTanks(g2d); drawProjectiles(g2d); drawGameOverMessage(g2d); } // Draw controls info drawControlsInfo(g2d); } }