786 lines
31 KiB
Java
786 lines
31 KiB
Java
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<Fluid> fluids;
|
|
public ArrayList<Gust> windBoxes;
|
|
public ArrayList<Rock> rocks;
|
|
public ArrayList<Wall> 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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|