import javax.swing.*; import java.awt.*; import java.awt.image.BufferedImage; import java.util.ArrayList; import java.awt.event.*; import java.util.concurrent.atomic.AtomicInteger; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import javax.swing.AbstractAction; import javax.swing.ActionMap; import javax.swing.InputMap; import javax.swing.JComponent; import javax.swing.KeyStroke; public class GameFields extends JPanel implements ActionListener, KeyListener { // ── Instance Fields ──────────────────────────────────────────────────── private final int PANEL_WIDTH = 640; private final int PANEL_HEIGHT = 360; private final int MAX_Y = PANEL_HEIGHT - 40; // bottom bound for players public int gustWidth = 80; public int gustHeight = 200; // Menus / navigation public JPanel container; public CardLayout cardLayout; public String playerName1; public String playerName2; // Element collections public ArrayList fluids; public ArrayList windBoxes; public ArrayList rocks; public ArrayList walls; // Game-state flags private boolean isRunning = false; private boolean gameOver = false; // Physics private int bearVelocityY = 0; // separate Y-velocities so players don't share state private int sealVelocityY = 0; private final int gravity = 1; private final int JUMP_FORCE = -12; // negative = upward in screen-space // UI private JProgressBar bearYProgress; private JProgressBar sealYProgress; private JLabel timerLabel; // shows countdown on screen // Timers private Timer gameTimer; private Timer playTimer; private AtomicInteger secondsRemaining; // tracked so we can read it in paintComponent // Players public Player bear; public Player seal; private int animationCounter = 0; public Image[] bearSprites; public Image[] sealSprites; // ── Win zone (goal door) ─────────────────────────────────────────────── // Both players must reach their respective door to win. // bearDoor is safe for bear; sealDoor is safe for seal. private Rectangle bearDoor; private Rectangle sealDoor; private boolean bearWon = false; private boolean sealWon = false; // ══════════════════════════════════════════════════════════════════════ // Inner classes // ══════════════════════════════════════════════════════════════════════ /** A controllable character. player==false → bear; player==true → seal. */ public class Player { public int x, y; public int speed = 5; public Rectangle hitbox = new Rectangle(5, 10, 20, 30); public boolean player = false; // false = bear, true = seal public boolean onGround = false; // Animation public Image[] walkFrames; public int currentFrame = 0; // Input flags public boolean left, right, up; public Player(Image[] frames, boolean isSeal) { this.walkFrames = frames; this.player = isSeal; } public Image getCurrentImage() { if (walkFrames == null || walkFrames.length == 0) return null; return walkFrames[currentFrame]; } } public class Fluid { public int x, y; public int width = 80; public int height = 30; public Image img; public boolean player = false; // false → dangerous to bear; true → dangerous to seal public Fluid(Image img) { this.img = img; } } public class Gust { public int x, y; public int width = gustWidth; public int height = gustHeight; public Image img; public Gust(Image img) { this.img = img; } } /** Pushable rock obstacle. */ public class Rock { public Image img; public boolean collision; public Rectangle hitbox; public int velocityY = 0; // rocks obey gravity too public Rock() { this.img = null; this.collision = false; this.hitbox = new Rectangle(); } public Rock(Image img, int x, int y, int width, int height) { this.img = img; this.collision = false; this.hitbox = new Rectangle(x, y, width, height); } public int getX() { return hitbox.x; } public int getY() { return hitbox.y; } public int getWidth() { return hitbox.width; } public int getHeight() { return hitbox.height; } public void setPosition(int x, int y) { hitbox.setLocation(x, y); } public void draw(Graphics g) { if (img != null) { g.drawImage(img, hitbox.x, hitbox.y, hitbox.width, hitbox.height, null); } else { g.setColor(new Color(139, 90, 43)); g.fillRect(hitbox.x, hitbox.y, hitbox.width, hitbox.height); g.setColor(new Color(100, 60, 20)); g.drawRect(hitbox.x, hitbox.y, hitbox.width, hitbox.height); } } @Override public String toString() { return "Rock{hitbox=" + hitbox + ", collision=" + collision + "}"; } } /** Static impassable wall / platform. */ public class Wall { public Image img; public boolean collision; public Rectangle hitbox; public Wall() { this.img = null; this.collision = false; this.hitbox = new Rectangle(); } public Wall(Image img, int x, int y, int width, int height) { this.img = img; this.collision = false; this.hitbox = new Rectangle(x, y, width, height); } public int getX() { return hitbox.x; } public int getY() { return hitbox.y; } public int getWidth() { return hitbox.width; } public int getHeight() { return hitbox.height; } public void setPosition(int x, int y) { hitbox.setLocation(x, y); } public void draw(Graphics g) { if (img != null) { g.drawImage(img, hitbox.x, hitbox.y, hitbox.width, hitbox.height, null); } else { g.setColor(Color.GRAY); g.fillRect(hitbox.x, hitbox.y, hitbox.width, hitbox.height); g.setColor(Color.DARK_GRAY); g.drawRect(hitbox.x, hitbox.y, hitbox.width, hitbox.height); } } @Override public String toString() { return "Wall{hitbox=" + hitbox + ", collision=" + collision + "}"; } } // ══════════════════════════════════════════════════════════════════════ // Constructor // ══════════════════════════════════════════════════════════════════════ Image bearIdle = new ImageIcon("bearIdle.png").getImage(); Image bearWalk1 = new ImageIcon("bearWalk1.png").getImage(); Image bearWalk2 = new ImageIcon("bearWalk2.png").getImage(); Image bearWalk3 = new ImageIcon("bearWalk3.png").getImage(); Image sealIdle = new ImageIcon("sealIdle.png").getImage(); Image sealWalk1 = new ImageIcon("sealWalk1.png").getImage(); Image sealWalk2 = new ImageIcon("sealWalk2.png").getImage(); public GameFields() { setPreferredSize(new Dimension(PANEL_WIDTH, PANEL_HEIGHT)); setFocusable(true); setBackground(new Color(30, 30, 60)); this.addKeyListener(this); setFocusTraversalKeysEnabled(false); // Progress bars (show height progress) bearYProgress = new JProgressBar(0, MAX_Y); sealYProgress = new JProgressBar(0, MAX_Y); bearYProgress.setForeground(new Color(139, 90, 43)); // brown for bear sealYProgress.setForeground(new Color(70, 130, 180)); // steel blue for seal bearYProgress.setPreferredSize(new Dimension(100, 14)); sealYProgress.setPreferredSize(new Dimension(100, 14)); timerLabel = new JLabel("Time: "); timerLabel.setForeground(Color.WHITE); timerLabel.setFont(new Font("Arial", Font.BOLD, 16)); JPanel hud = new JPanel(new FlowLayout(FlowLayout.CENTER, 20, 2)); hud.setOpaque(false); hud.add(new JLabel("Bear:") {{ setForeground(Color.WHITE); }}); hud.add(bearYProgress); hud.add(timerLabel); hud.add(new JLabel("Seal:") {{ setForeground(Color.WHITE); }}); hud.add(sealYProgress); setLayout(new BorderLayout()); add(hud, BorderLayout.NORTH); // Pre-allocate element pools fluids = new ArrayList<>(); windBoxes = new ArrayList<>(); rocks = new ArrayList<>(); walls = new ArrayList<>(); // 6 fluid slots (indices 0-2 = bear-fluid; 3-5 = seal-fluid) for (int i = 0; i < 6; i++) { Fluid f = new Fluid(null); f.player = (i >= 3); // 0-2 dangerous to bear (false), 3-5 dangerous to seal (true) f.x = -9999; f.y = -9999; // park off-screen until placed fluids.add(f); } // 5 wall slots for (int i = 0; i < 5; i++) { Wall w = new Wall(null, -9999, -9999, 120, 20); walls.add(w); } // 3 rock slots for (int i = 0; i < 3; i++) { Rock r = new Rock(null, -9999, -9999, 30, 30); rocks.add(r); } // 2 gust slots for (int i = 0; i < 2; i++) { Gust g = new Gust(null); g.x = -9999; g.y = -9999; windBoxes.add(g); } // Players bearSprites = new Image[4]; bearSprites[0] = bearIdle; bearSprites[1] = bearWalk1; bearSprites[2] = bearWalk2; bearSprites[3] = bearWalk3; sealSprites = new Image[3]; sealSprites[0] = sealIdle; sealSprites[1] = sealWalk1; sealSprites[2] = sealWalk2; bear = new Player(bearSprites, false); seal = new Player(sealSprites, true); //setupKeyBindings(); // 60 fps game loop gameTimer = new Timer(1000 / 60, this); } // ══════════════════════════════════════════════════════════════════════ // Navigation helpers // ══════════════════════════════════════════════════════════════════════ public void setContainer(JPanel container, CardLayout layout) { this.container = container; this.cardLayout = layout; } /** * Called by a level button in LvlManager / MainMenu. * Resets state, assigns player names, and switches to the chosen card. */ public void levelStart(String name1, String name2, String level) { playerName1 = name1; playerName2 = name2; gameOver = false; bearWon = false; sealWon = false; bearVelocityY = 0; sealVelocityY = 0; isRunning = true; gameTimer.start(); cardLayout.show(container, level); // Ensure the panel can receive focus and request it this.setFocusable(true); this.requestFocus(); this.requestFocusInWindow(); repaint(); } // ══════════════════════════════════════════════════════════════════════ // Input // ══════════════════════════════════════════════════════════════════════ /*private void setupKeyBindings() { InputMap im = this.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); ActionMap am = this.getActionMap(); // BEAR — Arrow keys bindKey(im, am, KeyEvent.VK_LEFT, "bear_left", bear); bindKey(im, am, KeyEvent.VK_RIGHT, "bear_right", bear); bindKey(im, am, KeyEvent.VK_UP, "bear_up", bear); // SEAL — WASD bindKey(im, am, KeyEvent.VK_A, "seal_left", seal); bindKey(im, am, KeyEvent.VK_D, "seal_right", seal); bindKey(im, am, KeyEvent.VK_W, "seal_up", seal); } private void bindKey(InputMap im, ActionMap am, int keyCode, String name, Player p) { // 1. Create KeyStrokes for both Press and Release KeyStroke press = KeyStroke.getKeyStroke(keyCode, 0, false); KeyStroke release = KeyStroke.getKeyStroke(keyCode, 0, true); // 2. Put them in the InputMap with UNIQUE action keys im.put(press, name + "_P"); im.put(release, name + "_R"); // 3. Map those action keys to your ToggleMovementAction in the ActionMap // name.split("_")[1] extracts "left", "right", or "up" from "bear_left" String direction = name.split("_")[1]; am.put(name + "_P", new ToggleMovementAction(p, direction, true)); am.put(name + "_R", new ToggleMovementAction(p, direction, false)); /*im.put(KeyStroke.getKeyStroke(keyCode, 0, false), name + "_press"); am.put(name + "_press", new ToggleMovementAction(p, name.split("_")[1], true)); im.put(KeyStroke.getKeyStroke(keyCode, 0, true), name + "_release"); am.put(name + "_release", new ToggleMovementAction(p, name.split("_")[1], false)); } public class ToggleMovementAction extends AbstractAction { private final Player player; private final String direction; private final boolean pressed; public ToggleMovementAction(Player player, String direction, boolean pressed) { this.player = player; this.direction = direction; this.pressed = pressed; } @Override public void actionPerformed(ActionEvent e) { switch (direction) { case "left" -> player.left = pressed; case "right" -> player.right = pressed; case "up" -> player.up = pressed; } } }*/ // ══════════════════════════════════════════════════════════════════════ // Game loop // ══════════════════════════════════════════════════════════════════════ @Override public void actionPerformed(ActionEvent e) { if (isRunning) { move(); repaint(); if (gameOver) { gameTimer.stop(); if (playTimer != null) playTimer.stop(); } // Check win condition if (bearWon && sealWon) { gameOver = true; isRunning = false; } } } // ══════════════════════════════════════════════════════════════════════ // Physics & movement // ══════════════════════════════════════════════════════════════════════ public void move() { if (bear == null || seal == null) return; updatePlayerPhysics(bear); updatePlayerPhysics(seal); // Sync progress bars (invert: higher up = lower y value = more progress) bearYProgress.setValue(MAX_Y - Math.max(0, bear.y)); sealYProgress.setValue(MAX_Y - Math.max(0, seal.y)); // Fluid collision — wrong fluid type kills the player for (Fluid f : fluids) { if (checkCollision(bear, f)) { gameOver = true; return; } if (checkCollision(seal, f)) { gameOver = true; return; } } // Gust — push player upward for (Gust g : windBoxes) { if (checkCollision(bear, g)) bearVelocityY = Math.min(bearVelocityY, -8); if (checkCollision(seal, g)) sealVelocityY = Math.min(sealVelocityY, -8); } // Win-door checks if (bearDoor != null && bear.hitbox.intersects(bearDoor)) bearWon = true; if (sealDoor != null && seal.hitbox.intersects(sealDoor)) sealWon = true; } private void updatePlayerPhysics(Player p) { int oldX = p.x; int oldY = p.y; // ── Horizontal movement ── boolean moved = false; if (p.left) { p.x -= p.speed; moved = true; System.out.println("left key pressed");} if (p.right) { p.x += p.speed; moved = true; } p.hitbox.setLocation(p.x + 5, p.y + 10); // offset to keep hitbox centred in sprite if (checkSolidCollision(p)) { p.x = oldX; p.hitbox.setLocation(p.x + 5, p.y + 10); } // ── Vertical movement (gravity + jump) ── int vel = (p == bear) ? bearVelocityY : sealVelocityY; vel += gravity; // Jump: only allowed when standing on something if (p.up && p.onGround) { vel = JUMP_FORCE; p.onGround = false; } p.y += vel; p.hitbox.setLocation(p.x + 5, p.y + 10); p.onGround = false; if (checkSolidCollision(p)) { if (vel > 0) { // Landing on top of a surface p.y = oldY; vel = 0; p.onGround = true; } else { // Bumped head on ceiling p.y = oldY; vel = 0; } p.hitbox.setLocation(p.x + 5, p.y + 10); } // Store velocity back if (p == bear) bearVelocityY = vel; else sealVelocityY = vel; // Screen boundary clamping p.x = Math.max(0, Math.min(p.x, PANEL_WIDTH - 30)); p.y = Math.max(0, Math.min(p.y, PANEL_HEIGHT - 40)); if (p.y >= PANEL_HEIGHT - 40) { p.onGround = true; if (p == bear) bearVelocityY = 0; else sealVelocityY = 0; } p.hitbox.setLocation(p.x + 5, p.y + 10); // ── Animation ── if (moved && p.walkFrames != null && p.walkFrames.length > 1) { animationCounter++; if (animationCounter % 10 == 0) p.currentFrame = (p.currentFrame + 1) % p.walkFrames.length; } else { p.currentFrame = 0; } } // ── Solid-collision helpers ──────────────────────────────────────────── public boolean checkSolidCollision(Player p) { for (Wall w : walls) { if (w.hitbox.x < -1000) continue; // parked off-screen if (p.hitbox.intersects(w.hitbox)) return true; } for (Rock r : rocks) { if (r.hitbox.x < -1000) continue; if (p.hitbox.intersects(r.hitbox)) { // Try to push the rock horizontally int moveX = 0; if (p.left) moveX = -p.speed; if (p.right) moveX = p.speed; if (moveX != 0 && canRockMove(r, moveX, 0)) { r.hitbox.x += moveX; return false; // rock moved, player not blocked } return true; } } return false; } private boolean canRockMove(Rock r, int dx, int dy) { Rectangle future = new Rectangle(r.hitbox.x + dx, r.hitbox.y + dy, r.hitbox.width, r.hitbox.height); for (Wall w : walls) { if (w.hitbox.x < -1000) continue; if (future.intersects(w.hitbox)) return false; } for (Rock other : rocks) { if (other != r && other.hitbox.x > -1000 && future.intersects(other.hitbox)) return false; } return future.x >= 0 && future.x + future.width <= PANEL_WIDTH; } // ── Collision checkers ───────────────────────────────────────────────── /** Fluid is dangerous only when the player type matches the fluid type. */ public boolean checkCollision(Player a, Fluid b) { if (a.hitbox == null || b.x < -1000) return false; // player==false (bear) is harmed by fluid.player==false; same for seal if (a.player != b.player) return false; Rectangle fluidRect = new Rectangle(b.x, b.y, b.width, b.height); return a.hitbox.intersects(fluidRect); } public boolean checkCollision(Player a, Gust b) { if (b.x < -1000) return false; Rectangle gustRect = new Rectangle(b.x, b.y, b.width, b.height); return a.hitbox.intersects(gustRect); } // ══════════════════════════════════════════════════════════════════════ // Placement helpers (used by LvlManager) // ══════════════════════════════════════════════════════════════════════ public void placePlayer(Player play, int x, int y) { play.x = x; play.y = y; play.hitbox.setLocation(x + 5, y + 10); } public void placeFluid(Fluid flu, int x, int y) { flu.x = x; flu.y = y; } public void placeGust(Gust gus, int x, int y) { gus.x = x; gus.y = y; } public void placeRock(Rock rock, int x, int y) { rock.setPosition(x, y); } public void placeWall(Wall wall, int x, int y) { wall.setPosition(x, y); } /** Optionally place win doors (call from LvlManager per level). */ public void placeDoors(int bx, int by, int sx, int sy) { bearDoor = new Rectangle(bx, by, 30, 50); sealDoor = new Rectangle(sx, sy, 30, 50); } // ══════════════════════════════════════════════════════════════════════ // Difficulty / timer // ══════════════════════════════════════════════════════════════════════ public void setDifficulty(String in) { if (in.equals("Easy")) CountDown(new AtomicInteger(150)); else if (in.equals("Medium")) CountDown(new AtomicInteger(120)); else CountDown(new AtomicInteger(100)); } /** Starts (or restarts) the countdown and updates timerLabel each second. */ public void CountDown(AtomicInteger secs) { secondsRemaining = secs; if (playTimer != null) playTimer.stop(); playTimer = new Timer(1000, e -> { if (secondsRemaining.get() > 0) { timerLabel.setText("Time: " + secondsRemaining.getAndDecrement()); System.out.println(secondsRemaining); } else { gameOver = true; ((Timer) e.getSource()).stop(); timerLabel.setText("Time: 0"); } }); playTimer.start(); } @Override public void keyPressed(KeyEvent e) { int code = e.getKeyCode(); // BEAR Controls if (code == KeyEvent.VK_LEFT) { bear.left = true; System.out.println("left pressed"); } if (code == KeyEvent.VK_RIGHT) bear.right = true; if (code == KeyEvent.VK_UP) bear.up = true; // SEAL Controls if (code == KeyEvent.VK_A) seal.left = true; if (code == KeyEvent.VK_D) seal.right = true; if (code == KeyEvent.VK_W) seal.up = true; // Global Commands if (code == KeyEvent.VK_ESCAPE) cardLayout.show(container, "MENU"); } @Override public void keyReleased(KeyEvent e) { int code = e.getKeyCode(); // BEAR Controls if (code == KeyEvent.VK_LEFT) bear.left = false; if (code == KeyEvent.VK_RIGHT) bear.right = false; if (code == KeyEvent.VK_UP) bear.up = false; // SEAL Controls if (code == KeyEvent.VK_A) seal.left = false; if (code == KeyEvent.VK_D) seal.right = false; if (code == KeyEvent.VK_W) seal.up = false; } @Override public void keyTyped(KeyEvent e) { // Not needed for movement, but required by interface } // ══════════════════════════════════════════════════════════════════════ // Rendering // ══════════════════════════════════════════════════════════════════════ @Override protected void paintComponent(Graphics g) { super.paintComponent(g); draw(g); // HUD overlay if (isRunning) { g.setFont(new Font("Arial", Font.BOLD, 14)); // Player-name tags above their characters g.setColor(new Color(210, 140, 60)); if (bear != null) g.drawString(playerName1 != null ? playerName1 : "Bear", bear.x - 5, bear.y - 4); g.setColor(new Color(70, 160, 220)); if (seal != null) g.drawString(playerName2 != null ? playerName2 : "Seal", seal.x - 5, seal.y - 4); } if (gameOver) { // Semi-transparent overlay g.setColor(new Color(0, 0, 0, 160)); g.fillRect(0, 0, PANEL_WIDTH, PANEL_HEIGHT); g.setFont(new Font("Arial", Font.BOLD, 36)); if (bearWon && sealWon) { g.setColor(new Color(255, 215, 0)); drawCentered(g, "YOU WIN!", PANEL_HEIGHT / 2 - 20); } else { g.setColor(new Color(255, 80, 80)); drawCentered(g, "GAME OVER", PANEL_HEIGHT / 2 - 20); } g.setFont(new Font("Arial", Font.PLAIN, 18)); g.setColor(Color.WHITE); drawCentered(g, "Press R to restart or ESC for menu", PANEL_HEIGHT / 2 + 20); } } private void drawCentered(Graphics g, String text, int y) { FontMetrics fm = g.getFontMetrics(); g.drawString(text, (PANEL_WIDTH - fm.stringWidth(text)) / 2, y); } protected void draw(Graphics g) { // Background g.setColor(new Color(30, 30, 60)); g.fillRect(0, 0, PANEL_WIDTH, PANEL_HEIGHT); // Win doors if (bearDoor != null) { g.setColor(new Color(210, 140, 60, 180)); g.fillRect(bearDoor.x, bearDoor.y, bearDoor.width, bearDoor.height); g.setColor(Color.WHITE); g.drawRect(bearDoor.x, bearDoor.y, bearDoor.width, bearDoor.height); g.setFont(new Font("Arial", Font.BOLD, 9)); g.drawString("BEAR", bearDoor.x, bearDoor.y - 3); } if (sealDoor != null) { g.setColor(new Color(70, 160, 220, 180)); g.fillRect(sealDoor.x, sealDoor.y, sealDoor.width, sealDoor.height); g.setColor(Color.WHITE); g.drawRect(sealDoor.x, sealDoor.y, sealDoor.width, sealDoor.height); g.setFont(new Font("Arial", Font.BOLD, 9)); g.drawString("SEAL", sealDoor.x, sealDoor.y - 3); } // Walls and rocks for (Wall w : walls) if (w.hitbox.x > -1000) w.draw(g); for (Rock r : rocks) if (r.hitbox.x > -1000) r.draw(g); // Gusts (semi-transparent column) g.setColor(new Color(200, 255, 200, 60)); for (Gust gust : windBoxes) { if (gust.x > -1000) { g.fillRect(gust.x, gust.y, gust.width, gust.height); g.setColor(new Color(150, 255, 150, 120)); g.drawRect(gust.x, gust.y, gust.width, gust.height); g.setColor(new Color(200, 255, 200, 60)); } } // Fluids for (Fluid f : fluids) { if (f.x > -1000) { // bear-fluid = brown/red; seal-fluid = blue if (!f.player) g.setColor(new Color(180, 80, 0, 200)); else g.setColor(new Color(0, 120, 220, 200)); g.fillRect(f.x, f.y, f.width, f.height); g.setColor(Color.WHITE); g.drawRect(f.x, f.y, f.width, f.height); } } // Players if (bear != null) { if (bear.getCurrentImage() != null) { g.drawImage(bear.getCurrentImage(), bear.x, bear.y, 30, 40, null); } else { // Fallback: coloured rectangle g.setColor(new Color(210, 140, 60)); g.fillRoundRect(bear.x, bear.y, 30, 40, 8, 8); g.setColor(Color.WHITE); g.setFont(new Font("Arial", Font.BOLD, 9)); g.drawString("BEAR", bear.x, bear.y + 22); } } if (seal != null) { if (seal.getCurrentImage() != null) { g.drawImage(seal.getCurrentImage(), seal.x, seal.y, 30, 40, null); } else { g.setColor(new Color(70, 160, 220)); g.fillRoundRect(seal.x, seal.y, 30, 40, 8, 8); g.setColor(Color.WHITE); g.setFont(new Font("Arial", Font.BOLD, 9)); g.drawString("SEAL", seal.x, seal.y + 22); } } } }