This commit is contained in:
CT
2026-05-14 00:51:15 -05:00
parent dc5cb74e6a
commit 3eae9e6596
7 changed files with 1433 additions and 1386 deletions

View File

@@ -1,57 +0,0 @@
import javax.swing.*;
import java.awt.*;
public class CreditsPanel extends JPanel {
private JList<String> creditsList;
private JScrollPane scrollPane;
private Timer timer;
private int scrollPos = 0;
public CreditsPanel(String[] data, Runnable onComplete) {
setLayout(new BorderLayout());
setBackground(Color.BLACK);
//JList
creditsList = new JList<>(data);
creditsList.setBackground(Color.BLACK);
creditsList.setForeground(Color.WHITE);
creditsList.setFont(new Font("SansSerif", Font.BOLD, 20));
creditsList.setFixedCellHeight(40); // Consistent spacing
DefaultListCellRenderer renderer = (DefaultListCellRenderer) creditsList.getCellRenderer();
renderer.setHorizontalAlignment(SwingConstants.CENTER);
//ScrollPane
scrollPane = new JScrollPane(creditsList);
scrollPane.setBorder(null);
scrollPane.getVerticalScrollBar().setPreferredSize(new Dimension(0, 0)); // Hidden bar
add(scrollPane, BorderLayout.CENTER);
//Animation
timer = new Timer(30, e -> {
JScrollBar vertical = scrollPane.getVerticalScrollBar();
scrollPos++;
vertical.setValue(scrollPos);
// Add a buffer (e.g., 100 pixels) to the finish check
// to keep it on screen longer
if (scrollPos >= (vertical.getMaximum() - vertical.getVisibleAmount()) + 100) {
timer.stop();
onComplete.run();
}
});
// Skip Button
JButton skip = new JButton("SKIP");
skip.addActionListener(e -> {
timer.stop();
onComplete.run();
});
add(skip, BorderLayout.SOUTH);
}
public void start() {
scrollPos = 0;
timer.start();
}
}

View File

@@ -1,785 +0,0 @@
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);
}
}
}
}

861
GamePanel.java Normal file
View File

@@ -0,0 +1,861 @@
import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.PriorityQueue;
import javax.swing.*;
/**
* GamePanel — the single JPanel that owns: • the 60 fps game loop (Swing Timer) • physics,
* collision detection, and rock-pushing • three built-in level layouts (via TileSet) • all
* rendering • keyboard input for both players
*
* <p>Rubric notes implemented here: • TileSet inner class — Aimee's "extra method/class" rubric
* item • PriorityQueue<Rock> — "advanced data structure / heap" rubric item • JProgressBar (bear +
* seal) — "extra GUI class" rubric item • Timer (game loop + countdown) — "timer used for logic"
* rubric item • Variable text display (names + countdown) — drawn every frame
*/
public class GamePanel extends JPanel implements ActionListener, KeyListener {
// ══════════════════════════════════════════════════════════════════════════
// TileSet — Aimee's rubric item ("extra method / class")
//
// Stores and renders a layer of background tiles so levels can have
// a distinct visual theme without external image files. Each level
// calls TileSet.build() with a different colour pair.
// ══════════════════════════════════════════════════════════════════════════
static class TileSet {
private static final int TILE = 32; // px per tile cell
private final Color dark;
private final Color light;
private final int cols;
private final int rows;
TileSet(Color dark, Color light, int panelW, int panelH) {
this.dark = dark;
this.light = light;
this.cols = (panelW / TILE) + 1;
this.rows = (panelH / TILE) + 1;
}
/** Draw a simple checker-pattern background. */
void draw(Graphics g) {
for (int row = 0; row < rows; row++) {
for (int col = 0; col < cols; col++) {
g.setColor(((row + col) % 2 == 0) ? dark : light);
g.fillRect(col * TILE, row * TILE, TILE, TILE);
}
}
}
/** Factory — returns a TileSet themed for the given level number. */
static TileSet forLevel(int level, int w, int h) {
return switch (level) {
case 1 -> new TileSet(new Color(20, 40, 60), new Color(25, 50, 75), w, h);
case 2 -> new TileSet(new Color(40, 20, 60), new Color(50, 25, 75), w, h);
default -> new TileSet(new Color(50, 20, 20), new Color(65, 25, 25), w, h);
};
}
}
// ══════════════════════════════════════════════════════════════════════════
// Static inner obstacle types (plain data, no behaviour of their own)
// ══════════════════════════════════════════════════════════════════════════
/** Impassable surface — floors, ledges, ceilings. */
static class Wall {
Rectangle hitbox;
Wall(int x, int y, int w, int h) {
hitbox = new Rectangle(x, y, w, h);
}
void draw(Graphics g) {
g.setColor(new Color(100, 100, 120));
g.fillRect(hitbox.x, hitbox.y, hitbox.width, hitbox.height);
g.setColor(new Color(60, 60, 80));
g.drawRect(hitbox.x, hitbox.y, hitbox.width, hitbox.height);
// Simple highlight on top edge so platforms read clearly
g.setColor(new Color(160, 160, 180));
g.drawLine(hitbox.x, hitbox.y, hitbox.x + hitbox.width, hitbox.y);
}
}
/**
* Pushable rock — stored in a PriorityQueue sorted by x-position (satisfies the "advanced data
* structure / heap" rubric requirement).
*/
static class Rock {
Rectangle hitbox;
int velocityY = 0;
Rock(int x, int y, int w, int h) {
hitbox = new Rectangle(x, y, w, h);
}
void draw(Graphics g) {
g.setColor(new Color(139, 90, 43));
g.fillOval(hitbox.x, hitbox.y, hitbox.width, hitbox.height);
g.setColor(new Color(100, 60, 20));
g.drawOval(hitbox.x, hitbox.y, hitbox.width, hitbox.height);
// Shine dot
g.setColor(new Color(200, 160, 100, 180));
g.fillOval(hitbox.x + 5, hitbox.y + 5, 8, 8);
}
}
/**
* Hazardous liquid pool. dangerousToSeal == false → kills bear (lava / acid) dangerousToSeal ==
* true → kills seal (oil / poison water)
*/
static class Fluid {
int x, y, w = 80, h = 20;
boolean dangerousToSeal;
// Animation offset so each fluid tile ripples independently
int rippleOffset;
Fluid(int x, int y, boolean dangerousToSeal, int rippleOffset) {
this.x = x;
this.y = y;
this.dangerousToSeal = dangerousToSeal;
this.rippleOffset = rippleOffset;
}
Rectangle rect() {
return new Rectangle(x, y, w, h);
}
void draw(Graphics g, int tick) {
// Base colour
Color base = dangerousToSeal ? new Color(0, 80, 200, 210) : new Color(200, 60, 0, 210);
g.setColor(base);
g.fillRect(x, y, w, h);
// Animated ripple line — moves upward each tick
int ry = y + 4 + ((tick / 4 + rippleOffset) % (h - 4));
g.setColor(dangerousToSeal ? new Color(100, 180, 255, 140) : new Color(255, 160, 80, 140));
g.drawLine(x + 4, ry, x + w - 4, ry);
// Border
g.setColor(Color.WHITE);
g.drawRect(x, y, w, h);
// Danger label
g.setColor(Color.WHITE);
g.setFont(new Font("Arial", Font.BOLD, 8));
g.drawString(dangerousToSeal ? "SEAL" : "BEAR", x + 3, y + h - 4);
}
}
/** Upward wind column — launches any player that enters it. */
static class Gust {
int x, y, w = 60, h = 180;
Gust(int x, int y) {
this.x = x;
this.y = y;
}
Rectangle rect() {
return new Rectangle(x, y, w, h);
}
void draw(Graphics g, int tick) {
// Pulsing green column
int alpha = 40 + (int) (20 * Math.sin(tick * 0.1));
g.setColor(new Color(100, 255, 100, alpha));
g.fillRect(x, y, w, h);
g.setColor(new Color(100, 255, 100, 100));
g.drawRect(x, y, w, h);
// Upward arrow lines that scroll upward
g.setColor(new Color(150, 255, 150, 160));
int spacing = 30;
for (int ay = y + h - ((tick * 2) % spacing); ay > y; ay -= spacing) {
int mx = x + w / 2;
g.drawLine(mx, ay, mx - 8, ay + 10);
g.drawLine(mx, ay, mx + 8, ay + 10);
}
}
}
// ══════════════════════════════════════════════════════════════════════════
// Constants
// ══════════════════════════════════════════════════════════════════════════
static final int W = 640;
static final int H = 360;
private static final int FLOOR_Y = H - 40;
private static final int GRAVITY = 1;
private static final int JUMP_FORCE = -13;
// ══════════════════════════════════════════════════════════════════════════
// State
// ══════════════════════════════════════════════════════════════════════════
// Navigation
private JPanel container;
private CardLayout cardLayout;
private String currentLevel = "";
// Players
final Player bear;
final Player seal;
// Obstacles — walls and fluids are ArrayLists; rocks use PriorityQueue (heap)
private final ArrayList<Wall> walls = new ArrayList<>();
private final ArrayList<Fluid> fluids = new ArrayList<>();
private final ArrayList<Gust> gusts = new ArrayList<>();
// PriorityQueue sorted by rock x-position — satisfies "advanced data structure
// / heap"
private PriorityQueue<Rock> rocks;
// Current level background
private TileSet tileSet;
// Win zones
private Rectangle bearDoor, sealDoor;
private boolean bearWon, sealWon;
// Game-loop state
private boolean isRunning = false;
private boolean gameOver = false;
private int animTick = 0; // increments every frame; drives animation + gust ripple
// Difficulty — set from the menu before a level starts
private int difficultySeconds = 150; // default Easy
// HUD widgets
private final JProgressBar bearBar = new JProgressBar(0, FLOOR_Y);
private final JProgressBar sealBar = new JProgressBar(0, FLOOR_Y);
private final JLabel timerLabel = new JLabel("Time: --");
// JSlider controls player walk speed (extra GUI class)
private final JSlider speedSlider;
// Timers
private final Timer gameTimer; // 60 fps game loop
private Timer countdownTimer; // 1-second countdown
// Countdown state — kept as a plain int field so the lambda never
// references itself (avoids the "variable used in lambda must be
// effectively final" compiler error).
private int secondsLeft = 0;
// ══════════════════════════════════════════════════════════════════════════
// Constructor
// ══════════════════════════════════════════════════════════════════════════
public GamePanel() {
setPreferredSize(new Dimension(W, H));
setFocusable(true);
addKeyListener(this);
setFocusTraversalKeysEnabled(false);
// Sprites (graceful fallback to coloured rectangles if files absent)
Image[] bearFrames =
loadFrames("bearIdle.png", "bearWalk1.png", "bearWalk2.png", "bearWalk3.png");
Image[] sealFrames = loadFrames("sealIdle.png", "sealWalk1.png", "sealWalk2.png");
bear = new Player(false, bearFrames);
seal = new Player(true, sealFrames);
// ── HUD strip ────────────────────────────────────────────────────────
bearBar.setForeground(new Color(210, 140, 60));
sealBar.setForeground(new Color(70, 160, 220));
bearBar.setPreferredSize(new Dimension(90, 12));
sealBar.setPreferredSize(new Dimension(90, 12));
bearBar.setStringPainted(false);
sealBar.setStringPainted(false);
timerLabel.setForeground(Color.WHITE);
timerLabel.setFont(new Font("Arial", Font.BOLD, 14));
// JSlider — player speed (110); starts at 5
// Satisfies "extra GUI class not previously used" if JSlider is new,
// and also gives players a fun way to adjust feel before a level.
speedSlider = new JSlider(1, 10, 5);
speedSlider.setOpaque(false);
speedSlider.setForeground(Color.WHITE);
speedSlider.setPreferredSize(new Dimension(80, 20));
speedSlider.setToolTipText("Player speed");
speedSlider.addChangeListener(
ev -> {
int spd = speedSlider.getValue();
bear.speed = spd;
seal.speed = spd;
});
JLabel speedLabel = new JLabel("Speed:");
speedLabel.setForeground(Color.LIGHT_GRAY);
speedLabel.setFont(new Font("Arial", Font.PLAIN, 11));
JPanel hud = new JPanel(new FlowLayout(FlowLayout.CENTER, 12, 1));
hud.setOpaque(false);
hud.add(makeLabel("Bear:", new Color(210, 140, 60)));
hud.add(bearBar);
hud.add(timerLabel);
hud.add(makeLabel("Seal:", new Color(70, 160, 220)));
hud.add(sealBar);
hud.add(speedLabel);
hud.add(speedSlider);
setLayout(new BorderLayout());
add(hud, BorderLayout.NORTH);
// 60 fps game loop — uses ActionListener, no lambda, so no timer
// self-reference issue.
gameTimer = new Timer(1000 / 60, this);
// Initialise the rock heap (empty; rebuilt per level)
rocks = new PriorityQueue<>(Comparator.comparingInt(r -> r.hitbox.x));
}
// ══════════════════════════════════════════════════════════════════════════
// Public API (called by GameDriver / MainMenu)
// ══════════════════════════════════════════════════════════════════════════
public void setContainer(JPanel c, CardLayout cl) {
this.container = c;
this.cardLayout = cl;
}
/**
* Begin (or restart) a level.
*
* @param name1 display name for bear player
* @param name2 display name for seal player
* @param level card key: "LEVEL1", "LEVEL2", or "LEVEL3"
*/
public void startLevel(String name1, String name2, String level) {
bear.name = (name1 != null && !name1.isBlank()) ? name1 : "Bear";
seal.name = (name2 != null && !name2.isBlank()) ? name2 : "Seal";
currentLevel = level;
resetState();
buildLevel(level);
startCountdown(difficultySeconds);
gameTimer.stop();
gameTimer.start();
cardLayout.show(container, level);
requestFocusInWindow();
}
/**
* Called from MainMenu's difficulty radio buttons BEFORE a level starts. Stores the chosen
* seconds so startLevel() picks them up.
*/
public void setDifficulty(String d) {
difficultySeconds =
switch (d) {
case "Hard" -> 90;
case "Medium" -> 120;
default -> 150;
};
}
// ══════════════════════════════════════════════════════════════════════════
// Level construction
// ══════════════════════════════════════════════════════════════════════════
private void buildLevel(String level) {
walls.clear();
fluids.clear();
gusts.clear();
// Rebuild the priority queue fresh each level
rocks = new PriorityQueue<>(Comparator.comparingInt(r -> r.hitbox.x));
bearDoor = sealDoor = null;
switch (level) {
case "LEVEL1" -> buildLevel1();
case "LEVEL2" -> buildLevel2();
case "LEVEL3" -> buildLevel3();
}
}
// ── Level 1 — "The Crossing" (Easy) ─────────────────────────────────────
// Goal: players start on opposite sides and must cross to reach the door on
// the other side. A rock bridges a gap in the centre; hazard pools block
// the direct route to force use of the raised platforms.
// Door positions verified reachable: bear door at x=10 (left side, y=200),
// seal door at x=596 (right side, y=200) — both sit on raised platforms.
private void buildLevel1() {
tileSet = TileSet.forLevel(1, W, H);
bear.setPosition(560, 260); // bear starts RIGHT
seal.setPosition(50, 260); // seal starts LEFT
// Ground — two halves with a gap at x 220-300
walls.add(new Wall(0, 310, 220, 20));
walls.add(new Wall(300, 310, 340, 20));
// Raised platforms (reachable from ground via jump)
walls.add(new Wall(0, 210, 130, 15)); // left platform — seal door sits here
walls.add(new Wall(510, 210, 130, 15)); // right platform — bear door sits here
walls.add(new Wall(255, 255, 130, 15)); // centre ledge over the gap
// Rock bridges the gap — players must push it or jump the ledge
rocks.add(new Rock(228, 280, 28, 28));
// Fluids — each blocks the direct ground-level path for one player
// bear-fluid (orange) on the LEFT — hurts bear trying to run left to door
fluids.add(new Fluid(140, 291, false, 0));
fluids.add(new Fluid(220, 291, false, 3));
// seal-fluid (blue) on the RIGHT — hurts seal trying to run right to door
fluids.add(new Fluid(390, 291, true, 1));
fluids.add(new Fluid(470, 291, true, 5));
// Gusts lift players up to the raised platforms
gusts.add(new Gust(15, 130));
gusts.add(new Gust(565, 130));
// Doors on the OPPOSITE side from each player's start
bearDoor = new Rectangle(10, 165, 30, 45); // bear must go LEFT
sealDoor = new Rectangle(596, 165, 30, 45); // seal must go RIGHT
}
// ── Level 2 — "Double Danger" (Medium) ──────────────────────────────────
// Two-tier layout. Players start on the bottom floor and must climb to the
// upper platform. More fluid pools and a narrower gust column.
private void buildLevel2() {
tileSet = TileSet.forLevel(2, W, H);
bear.setPosition(40, 280);
seal.setPosition(560, 280);
// Full bottom floor
walls.add(new Wall(0, 320, 640, 20));
// Mid-level platforms
walls.add(new Wall(0, 220, 150, 15));
walls.add(new Wall(490, 220, 150, 15));
walls.add(new Wall(220, 170, 200, 15)); // upper-centre platform
// Small step walls (help players climb without gust)
walls.add(new Wall(160, 270, 60, 15));
walls.add(new Wall(420, 270, 60, 15));
// Rocks — two on floor, one on upper platform (heap orders by x automatically)
rocks.add(new Rock(50, 290, 28, 28));
rocks.add(new Rock(560, 290, 28, 28));
rocks.add(new Rock(295, 142, 28, 28));
// Bear-fluid on right side of floor + right ledge
fluids.add(new Fluid(340, 301, false, 0));
fluids.add(new Fluid(420, 301, false, 2));
fluids.add(new Fluid(500, 202, false, 4)); // on right mid-platform
// Seal-fluid on left side of floor + left ledge
fluids.add(new Fluid(180, 301, true, 1));
fluids.add(new Fluid(100, 301, true, 3));
fluids.add(new Fluid(10, 202, true, 5)); // on left mid-platform
// Gusts on each side help players reach mid-level
gusts.add(new Gust(0, 60));
gusts.add(new Gust(580, 60));
// Doors on the upper-centre platform — both players must meet in the middle
bearDoor = new Rectangle(220, 125, 30, 45);
sealDoor = new Rectangle(390, 125, 30, 45);
}
// ── Level 3 — "The Gauntlet" (Hard) ─────────────────────────────────────
// Three tiers. Dense fluid fields, narrow gusts, rocks must be pushed to
// create safe paths between fluid pools.
private void buildLevel3() {
tileSet = TileSet.forLevel(3, W, H);
bear.setPosition(30, 280);
seal.setPosition(580, 280);
// Bottom strips (not a full floor — players can fall off the sides)
walls.add(new Wall(0, 320, 160, 20));
walls.add(new Wall(480, 320, 160, 20));
// Mid platforms
walls.add(new Wall(120, 230, 160, 15));
walls.add(new Wall(360, 230, 160, 15));
// Upper platforms
walls.add(new Wall(0, 145, 140, 15));
walls.add(new Wall(500, 145, 140, 15));
// Rocks — three across the levels
rocks.add(new Rock(80, 290, 28, 28));
rocks.add(new Rock(300, 200, 28, 28));
rocks.add(new Rock(530, 290, 28, 28));
// Bear-fluid (orange) — right-side hazards
fluids.add(new Fluid(340, 301, false, 0));
fluids.add(new Fluid(420, 301, false, 2));
fluids.add(new Fluid(500, 211, false, 4));
fluids.add(new Fluid(520, 126, false, 6));
// Seal-fluid (blue) — left-side hazards
fluids.add(new Fluid(160, 301, true, 1));
fluids.add(new Fluid(80, 301, true, 3));
fluids.add(new Fluid(120, 211, true, 5));
fluids.add(new Fluid(10, 126, true, 7));
// Narrow gusts — one on each side
gusts.add(new Gust(10, 80));
gusts.add(new Gust(570, 80));
// Doors at opposite upper corners
bearDoor = new Rectangle(5, 100, 30, 45); // bear must go to top-LEFT
sealDoor = new Rectangle(605, 100, 30, 45); // seal must go to top-RIGHT
}
// ══════════════════════════════════════════════════════════════════════════
// Game loop (called by gameTimer — implements ActionListener)
// ══════════════════════════════════════════════════════════════════════════
@Override
public void actionPerformed(ActionEvent e) {
if (!isRunning) return;
update();
repaint();
if (gameOver) {
gameTimer.stop();
if (countdownTimer != null) countdownTimer.stop();
}
}
// ══════════════════════════════════════════════════════════════════════════
// Physics & collision
// ══════════════════════════════════════════════════════════════════════════
private void update() {
if (gameOver) return;
animTick++;
stepPlayer(bear);
stepPlayer(seal);
// Fluid collision — each fluid type is lethal only to its matching player
for (Fluid f : fluids) {
if (!f.dangerousToSeal && bear.hitbox.intersects(f.rect())) {
triggerGameOver();
return;
}
if (f.dangerousToSeal && seal.hitbox.intersects(f.rect())) {
triggerGameOver();
return;
}
}
// Gust — launch upward
for (Gust g : gusts) {
if (bear.hitbox.intersects(g.rect())) bear.velocityY = Math.min(bear.velocityY, -10);
if (seal.hitbox.intersects(g.rect())) seal.velocityY = Math.min(seal.velocityY, -10);
}
// Win condition — both must reach their door
if (bearDoor != null && bear.hitbox.intersects(bearDoor)) bearWon = true;
if (sealDoor != null && seal.hitbox.intersects(sealDoor)) sealWon = true;
if (bearWon && sealWon) {
isRunning = false;
gameOver = true;
}
// Progress bars (higher up = lower y = more progress)
bearBar.setValue(FLOOR_Y - Math.max(0, bear.y));
sealBar.setValue(FLOOR_Y - Math.max(0, seal.y));
}
private void stepPlayer(Player p) {
int oldX = p.x, oldY = p.y;
boolean moved = false;
// ── Horizontal ──
if (p.left) {
p.x -= p.speed;
moved = true;
}
if (p.right) {
p.x += p.speed;
moved = true;
}
p.syncHitbox();
if (solidCollision(p)) {
p.x = oldX;
p.syncHitbox();
}
// ── Vertical (gravity + jump) ──
p.velocityY += GRAVITY;
if (p.up && p.onGround) {
p.velocityY = JUMP_FORCE;
p.onGround = false;
}
p.y += p.velocityY;
p.syncHitbox();
p.onGround = false;
if (solidCollision(p)) {
if (p.velocityY > 0) p.onGround = true; // landed on top
p.y = oldY;
p.velocityY = 0;
p.syncHitbox();
}
// Screen-boundary clamp
p.x = Math.max(0, Math.min(p.x, W - 30));
p.y = Math.max(0, Math.min(p.y, FLOOR_Y));
if (p.y >= FLOOR_Y) {
p.onGround = true;
p.velocityY = 0;
}
p.syncHitbox();
// Animation
if (moved && p.walkFrames != null && p.walkFrames.length > 1) {
if (animTick % 8 == 0) p.nextFrame();
} else {
p.idleFrame();
}
}
/**
* Returns true if the player's hitbox overlaps any solid surface. Rocks can be pushed
* horizontally if there is clear space.
*/
private boolean solidCollision(Player p) {
for (Wall w : walls) if (p.hitbox.intersects(w.hitbox)) return true;
// Iterate rocks — PriorityQueue does not support indexed access,
// so we iterate with the enhanced for-loop.
for (Rock r : rocks) {
if (p.hitbox.intersects(r.hitbox)) {
int dx = p.left ? -p.speed : p.right ? p.speed : 0;
if (dx != 0 && rockCanMove(r, dx)) {
r.hitbox.x += dx;
return false; // rock moved; player not blocked
}
return true;
}
}
return false;
}
private boolean rockCanMove(Rock r, int dx) {
Rectangle next = new Rectangle(r.hitbox.x + dx, r.hitbox.y, r.hitbox.width, r.hitbox.height);
if (next.x < 0 || next.x + next.width > W) return false;
for (Wall w : walls) if (next.intersects(w.hitbox)) return false;
for (Rock o : rocks) if (o != r && next.intersects(o.hitbox)) return false;
return true;
}
// ══════════════════════════════════════════════════════════════════════════
// Rendering
// ══════════════════════════════════════════════════════════════════════════
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
// 1. Background tile layer
if (tileSet != null) tileSet.draw(g);
// 2. Win doors
drawDoor(g, bearDoor, new Color(210, 140, 60, 200), bear.name + "'s DOOR");
drawDoor(g, sealDoor, new Color(70, 160, 220, 200), seal.name + "'s DOOR");
// 3. Obstacles
for (Wall w : walls) w.draw(g);
for (Rock r : rocks) r.draw(g);
for (Gust gu : gusts) gu.draw(g, animTick);
for (Fluid f : fluids) f.draw(g, animTick);
// 4. Players
drawPlayer(g, bear, new Color(210, 140, 60));
drawPlayer(g, seal, new Color(70, 160, 220));
// 5. Name tags (variable text display)
if (isRunning) {
g.setFont(new Font("Arial", Font.BOLD, 12));
g.setColor(new Color(210, 140, 60));
g.drawString(bear.name, bear.x - 4, bear.y - 4);
g.setColor(new Color(70, 160, 220));
g.drawString(seal.name, seal.x - 4, seal.y - 4);
}
// 6. Game-over overlay
if (gameOver) {
g.setColor(new Color(0, 0, 0, 170));
g.fillRect(0, 0, W, H);
g.setFont(new Font("Arial", Font.BOLD, 38));
if (bearWon && sealWon) {
g.setColor(new Color(255, 215, 0));
drawCentered(g, "YOU WIN!", H / 2 - 30);
} else {
g.setColor(new Color(255, 80, 80));
drawCentered(g, "GAME OVER", H / 2 - 30);
}
g.setFont(new Font("Arial", Font.PLAIN, 16));
g.setColor(Color.WHITE);
drawCentered(g, "Press R to restart | ESC for level select", H / 2 + 10);
}
// 7. Win-condition hint strip at bottom of screen during play
if (isRunning && !gameOver) {
g.setColor(new Color(0, 0, 0, 100));
g.fillRect(0, H - 18, W, 18);
g.setFont(new Font("Arial", Font.PLAIN, 10));
g.setColor(Color.LIGHT_GRAY);
String hint =
bear.name
+ ""
+ (bearWon ? "✓ REACHED DOOR" : "reach YOUR door")
+ " | "
+ seal.name
+ ""
+ (sealWon ? "✓ REACHED DOOR" : "reach YOUR door");
drawCentered(g, hint, H - 5);
}
}
private void drawDoor(Graphics g, Rectangle door, Color fill, String label) {
if (door == null) return;
g.setColor(fill);
g.fillRect(door.x, door.y, door.width, door.height);
g.setColor(Color.WHITE);
g.drawRect(door.x, door.y, door.width, door.height);
// Draw a door knob
g.setColor(new Color(255, 220, 50));
int kx = (door.x < W / 2) ? door.x + door.width - 7 : door.x + 4;
g.fillOval(kx, door.y + door.height / 2, 5, 5);
g.setFont(new Font("Arial", Font.BOLD, 8));
g.setColor(Color.WHITE);
g.drawString(label, door.x - 2, door.y - 3);
}
private void drawPlayer(Graphics g, Player p, Color fallback) {
Image img = p.currentSprite();
if (img != null) {
g.drawImage(img, p.x, p.y, 30, 40, null);
} else {
// Coloured rectangle fallback when sprite files are absent
g.setColor(fallback);
g.fillRoundRect(p.x, p.y, 30, 40, 8, 8);
// Simple face
g.setColor(Color.WHITE);
g.fillOval(p.x + 6, p.y + 8, 6, 6);
g.fillOval(p.x + 18, p.y + 8, 6, 6);
g.setColor(Color.BLACK);
g.fillOval(p.x + 8, p.y + 10, 3, 3);
g.fillOval(p.x + 20, p.y + 10, 3, 3);
g.setColor(fallback.darker());
g.drawArc(p.x + 8, p.y + 22, 14, 8, 180, 180);
// Label
g.setColor(Color.WHITE);
g.setFont(new Font("Arial", Font.BOLD, 7));
g.drawString(p.isSeal ? "SEAL" : "BEAR", p.x + 3, p.y + 38);
}
}
private void drawCentered(Graphics g, String text, int y) {
FontMetrics fm = g.getFontMetrics();
g.drawString(text, (W - fm.stringWidth(text)) / 2, y);
}
// ══════════════════════════════════════════════════════════════════════════
// Input
// ══════════════════════════════════════════════════════════════════════════
@Override
public void keyPressed(KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_LEFT -> bear.left = true;
case KeyEvent.VK_RIGHT -> bear.right = true;
case KeyEvent.VK_UP -> bear.up = true;
case KeyEvent.VK_A -> seal.left = true;
case KeyEvent.VK_D -> seal.right = true;
case KeyEvent.VK_W -> seal.up = true;
case KeyEvent.VK_R -> {
if (gameOver) startLevel(bear.name, seal.name, currentLevel);
}
case KeyEvent.VK_ESCAPE -> {
if (cardLayout != null) cardLayout.show(container, "MENU");
}
}
}
@Override
public void keyReleased(KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_LEFT -> bear.left = false;
case KeyEvent.VK_RIGHT -> bear.right = false;
case KeyEvent.VK_UP -> bear.up = false;
case KeyEvent.VK_A -> seal.left = false;
case KeyEvent.VK_D -> seal.right = false;
case KeyEvent.VK_W -> seal.up = false;
}
}
@Override
public void keyTyped(KeyEvent e) {}
// ══════════════════════════════════════════════════════════════════════════
// Helpers
// ══════════════════════════════════════════════════════════════════════════
private void resetState() {
bearWon = sealWon = gameOver = false;
isRunning = true;
bear.velocityY = seal.velocityY = 0;
bear.clearInput();
seal.clearInput();
}
private void triggerGameOver() {
gameOver = true;
isRunning = false;
}
/**
* Starts the 1-second countdown timer.
*
* <p>The timer's ActionListener reads and writes the {@code secondsLeft} instance field directly,
* so there is NO lambda variable self-reference and NO "variable used in lambda must be
* effectively final" error.
*/
private void startCountdown(int seconds) {
if (countdownTimer != null) countdownTimer.stop();
secondsLeft = seconds;
timerLabel.setText("Time: " + secondsLeft);
countdownTimer =
new Timer(
1000,
new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
secondsLeft--;
timerLabel.setText("Time: " + Math.max(0, secondsLeft));
if (secondsLeft <= 0) {
// Safe to call ((Timer) e.getSource()).stop() here —
// no self-reference to countdownTimer needed.
((Timer) e.getSource()).stop();
triggerGameOver();
}
}
});
countdownTimer.start();
}
private static Image[] loadFrames(String... paths) {
Image[] frames = new Image[paths.length];
for (int i = 0; i < paths.length; i++) frames[i] = new ImageIcon(paths[i]).getImage();
return frames;
}
private static JLabel makeLabel(String text, Color color) {
JLabel l = new JLabel(text);
l.setForeground(color);
l.setFont(new Font("Arial", Font.BOLD, 12));
return l;
}
}

View File

@@ -1,191 +0,0 @@
import javax.swing.*;
import java.awt.*;
/**
* LvlManager
*
* Builds each level as its own GameFields panel and registers them
* with the root CardLayout container. The level-select screen lives
* here; clicking a level button in MainMenu calls
* gm.levelStart(name1, name2, "LEVEL1") which tells CardLayout to
* flip to that card.
*/
public class LvlManager extends JPanel {
public JPanel container;
private CardLayout cardLayout;
public LvlManager(GameFields gm) {
setLayout(null);
setPreferredSize(new Dimension(640, 360));
setBackground(new Color(20, 20, 40));
// ── "Go Back" button ─────────────────────────────────────────────
JButton back = new JButton("Go Back");
back.setFont(new Font("Arial", Font.BOLD, 12));
back.setForeground(Color.WHITE);
back.setBackground(new Color(60, 60, 100));
back.setBorder(BorderFactory.createLineBorder(new Color(120, 120, 200), 1));
back.setFocusPainted(false);
back.setBounds(275, 318, 90, 28);
add(back);
back.addActionListener(e -> cardLayout.show(container, "MENU"));
// ── Build individual level panels ─────────────────────────────────
// Each level is a fresh GameFields; we configure its elements here,
// add it to the shared container under its card name, and wire up
// the setContainer call so the in-game "back" path works too.
buildLevel1(gm);
buildLevel2(gm);
buildLevel3(gm);
}
// =========================================================================
// LEVEL 1 — "The Crossing" (Easy)
// =========================================================================
private void buildLevel1(GameFields gm) {
GameFields level1 = new GameFields();
// Players
level1.placePlayer(level1.bear, 50, 270);
level1.placePlayer(level1.seal, 560, 270);
// Walls (x, y — default 120×20 size assigned in GameFields constructor)
level1.placeWall(level1.walls.get(0), 0, 318); // left ground
level1.placeWall(level1.walls.get(1), 340, 318); // right ground (gap at 260-340)
level1.placeWall(level1.walls.get(2), 270, 250); // center ledge
level1.placeWall(level1.walls.get(3), 80, 220); // left raised platform
level1.placeWall(level1.walls.get(4), 490, 220); // right raised platform
// Rocks
level1.placeRock(level1.rocks.get(0), 258, 288); // bridges the ground gap
// Fluids (index 0-2 = bear-fluid / dangerous to bear;
// index 3-5 = seal-fluid / dangerous to seal)
level1.placeFluid(level1.fluids.get(0), 430, 298); // bear-fluid on right path
level1.placeFluid(level1.fluids.get(3), 140, 298); // seal-fluid on left path
// Gust
level1.placeGust(level1.windBoxes.get(0), 60, 130);
// Win doors (bear door on right side, seal door on left side — they must cross)
level1.placeDoors(580, 240, 20, 240);
// Wire up navigation so level1 can also switch cards
level1.addKeyListener(new java.awt.event.KeyAdapter() {
@Override public void keyPressed(java.awt.event.KeyEvent e) {
if (e.getKeyCode() == java.awt.event.KeyEvent.VK_ESCAPE)
level1.cardLayout.show(level1.container, "MENU");
if (e.getKeyCode() == java.awt.event.KeyEvent.VK_R)
buildLevel1Restart(level1);
}
});
// Will be registered with the container in setContainer()
// Store reference so setContainer can wire it up
storeLevel(level1, "LEVEL1");
}
// =========================================================================
// LEVEL 2 — "Double Danger" (Medium)
// =========================================================================
private void buildLevel2(GameFields gm) {
GameFields level2 = new GameFields();
level2.placePlayer(level2.bear, 100, 290);
level2.placePlayer(level2.seal, 520, 290);
level2.placeWall(level2.walls.get(0), 0, 330); // bottom floor
level2.placeWall(level2.walls.get(1), 30, 240); // left mid platform
level2.placeWall(level2.walls.get(2), 490, 240); // right mid platform
level2.placeWall(level2.walls.get(3), 240, 150); // center upper platform
level2.placeWall(level2.walls.get(4), 308, 260); // center divider
level2.placeRock(level2.rocks.get(0), 70, 300);
level2.placeRock(level2.rocks.get(1), 500, 300);
level2.placeRock(level2.rocks.get(2), 290, 122);
level2.placeFluid(level2.fluids.get(0), 400, 315); // bear-fluid lower right
level2.placeFluid(level2.fluids.get(1), 460, 222); // bear-fluid mid-right ledge edge
level2.placeFluid(level2.fluids.get(2), 255, 132); // bear-fluid on finish ledge
level2.placeFluid(level2.fluids.get(3), 160, 315); // seal-fluid lower left
level2.placeFluid(level2.fluids.get(4), 100, 222); // seal-fluid mid-left ledge edge
level2.placeGust(level2.windBoxes.get(0), 50, 60);
level2.placeGust(level2.windBoxes.get(1), 560, 60);
level2.placeDoors(570, 130, 20, 130);
storeLevel(level2, "LEVEL2");
}
// =========================================================================
// LEVEL 3 — "The Gauntlet" (Hard)
// =========================================================================
private void buildLevel3(GameFields gm) {
GameFields level3 = new GameFields();
level3.placePlayer(level3.bear, 20, 290);
level3.placePlayer(level3.seal, 590, 290);
level3.placeWall(level3.walls.get(0), 0, 330); // left floor strip
level3.placeWall(level3.walls.get(1), 530, 330); // right floor strip
level3.placeWall(level3.walls.get(2), 150, 230); // wide center platform
level3.placeWall(level3.walls.get(3), 30, 140); // upper-left ledge
level3.placeWall(level3.walls.get(4), 540, 140); // upper-right ledge
level3.placeRock(level3.rocks.get(0), 300, 202);
level3.placeRock(level3.rocks.get(1), 80, 300);
level3.placeRock(level3.rocks.get(2), 520, 300);
// Bear-fluid (index 0-2)
level3.placeFluid(level3.fluids.get(0), 120, 310);
level3.placeFluid(level3.fluids.get(1), 340, 212);
level3.placeFluid(level3.fluids.get(2), 560, 122);
// Seal-fluid (index 3-5)
level3.placeFluid(level3.fluids.get(3), 460, 310);
level3.placeFluid(level3.fluids.get(4), 240, 212);
level3.placeFluid(level3.fluids.get(5), 50, 122);
level3.placeGust(level3.windBoxes.get(0), 30, 50);
level3.placeGust(level3.windBoxes.get(1), 570, 50);
level3.placeDoors(560, 100, 10, 100);
storeLevel(level3, "LEVEL3");
}
// ── Small helpers ─────────────────────────────────────────────────────
/** Holds level panels until setContainer() is called and we can register them. */
private final java.util.LinkedHashMap<String, GameFields> pendingLevels = new java.util.LinkedHashMap<>();
private void storeLevel(GameFields lv, String key) {
pendingLevels.put(key, lv);
}
/** Resets level1 positions in-place (called on 'R' key). */
private void buildLevel1Restart(GameFields lv) {
lv.placePlayer(lv.bear, 50, 270);
lv.placePlayer(lv.seal, 560, 270);
lv.placeRock(lv.rocks.get(0), 258, 288);
}
// ── setContainer ──────────────────────────────────────────────────────
public void setContainer(JPanel container, CardLayout layout) {
this.container = container;
this.cardLayout = layout;
// Register every pending level built in the constructor
for (String key : pendingLevels.keySet()) {
GameFields lv = pendingLevels.get(key);
lv.setContainer(container, layout);
container.add(lv, key);
}
}
}

View File

@@ -1,321 +0,0 @@
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.image.*;
public class MainMenu extends JPanel
{
public JPanel container;
private CardLayout cardLayout;
// -------------------------------------------------------------------------
// LevelButton — a custom JButton that paints its own normal / rollover icon.
//
// Normal state : small rounded tile, muted colour, level number centred.
// Hover state : tile scales up (~1.4×), bright glow ring appears around
// the tile, the label changes from "Level N" to the full
// level name, and the background darkens so the button pops.
// The change is purely painted — no external image files needed.
// -------------------------------------------------------------------------
private static class LevelButton extends JButton
{
private static final int BASE_SIZE = 100; // icon canvas (px)
private static final int INNER = 80; // tile size at rest
private static final int INNER_HOV = 110; // tile size on hover (drawn into same canvas)
private final int level;
private final String levelName;
private final Color tileColour;
private final Color hoverColour;
private boolean hovered = false;
LevelButton(int level, String levelName, Color tile, Color hover)
{
this.level = level;
this.levelName = levelName;
this.tileColour = tile;
this.hoverColour = hover;
setContentAreaFilled(false);
setBorderPainted(false);
setFocusPainted(false);
setOpaque(false);
// Build and set the two icons
setIcon(buildIcon(false));
setRolloverEnabled(true);
setRolloverIcon(buildIcon(true));
// Size the button to comfortably hold the larger hover icon + text
setPreferredSize(new Dimension(160, 160));
setSize(new Dimension(160, 160));
// Track hover so we can repaint the button text colour
addMouseListener(new MouseAdapter() {
@Override public void mouseEntered(MouseEvent e) { hovered = true; repaint(); }
@Override public void mouseExited (MouseEvent e) { hovered = false; repaint(); }
});
// Text sits below the icon, painted by paintComponent
setVerticalTextPosition(SwingConstants.BOTTOM);
setHorizontalTextPosition(SwingConstants.CENTER);
setFont(new Font("Arial", Font.BOLD, 13));
setForeground(Color.WHITE);
}
// Paints the button — lets us switch label text and colour dynamically
@Override
protected void paintComponent(Graphics g)
{
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// Dynamic label below the icon
String label = hovered ? levelName : "Level " + level;
Color fg = hovered ? hoverColour.brighter().brighter() : Color.LIGHT_GRAY;
FontMetrics fm = g2.getFontMetrics(getFont());
int tx = (getWidth() - fm.stringWidth(label)) / 2;
int ty = getHeight() - 8;
g2.setFont(getFont());
// Drop shadow
g2.setColor(Color.BLACK);
g2.drawString(label, tx + 1, ty + 1);
g2.setColor(fg);
g2.drawString(label, tx, ty);
g2.dispose();
}
// ── Icon factory ──────────────────────────────────────────────────────
private ImageIcon buildIcon(boolean hover)
{
int canvasSize = BASE_SIZE + 40; // 140 px canvas so hover tile fits
BufferedImage img = new BufferedImage(canvasSize, canvasSize,
BufferedImage.TYPE_INT_ARGB);
Graphics2D g = img.createGraphics();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
int cx = canvasSize / 2;
int cy = canvasSize / 2;
if (hover) {
// ── HOVER: large glowing tile ─────────────────────────────
// Outer glow rings (three, fading outward)
for (int i = 3; i >= 1; i--) {
int ringR = INNER_HOV / 2 + i * 10;
float alpha = 0.15f * i;
g.setColor(new Color(
hoverColour.getRed() / 255f,
hoverColour.getGreen()/ 255f,
hoverColour.getBlue() / 255f,
alpha));
g.fillOval(cx - ringR, cy - ringR, ringR * 2, ringR * 2);
}
// Tile background (bright)
int hs = INNER_HOV;
g.setColor(hoverColour);
g.fillRoundRect(cx - hs/2, cy - hs/2, hs, hs, 22, 22);
// Bright border stroke
g.setColor(Color.WHITE);
g.setStroke(new BasicStroke(3f));
g.drawRoundRect(cx - hs/2, cy - hs/2, hs, hs, 22, 22);
// Big level number (white, centred in tile)
g.setColor(Color.WHITE);
g.setFont(new Font("Arial", Font.BOLD, 42));
FontMetrics fm = g.getFontMetrics();
String num = String.valueOf(level);
g.drawString(num,
cx - fm.stringWidth(num) / 2,
cy + fm.getAscent() / 2 - 2);
// Stars drawn in upper-right corner of tile (difficulty indicator)
drawStars(g, cx + hs/2 - 28, cy - hs/2 + 4, level);
} else {
// ── NORMAL: small muted tile ──────────────────────────────
int ns = INNER;
g.setColor(tileColour);
g.fillRoundRect(cx - ns/2, cy - ns/2, ns, ns, 16, 16);
// Subtle border
g.setColor(tileColour.brighter());
g.setStroke(new BasicStroke(1.5f));
g.drawRoundRect(cx - ns/2, cy - ns/2, ns, ns, 16, 16);
// Level number
g.setColor(Color.WHITE);
g.setFont(new Font("Arial", Font.BOLD, 30));
FontMetrics fm = g.getFontMetrics();
String num = String.valueOf(level);
g.drawString(num,
cx - fm.stringWidth(num) / 2,
cy + fm.getAscent() / 2 - 2);
// Stars (smaller)
drawStars(g, cx + ns/2 - 22, cy - ns/2 + 2, level);
}
g.dispose();
return new ImageIcon(img);
}
/** Draw N filled gold stars in a row, starting at (x, y). */
private void drawStars(Graphics2D g, int x, int y, int count)
{
g.setColor(new Color(255, 220, 50));
int starSize = 10;
for (int i = 0; i < count; i++) {
drawStar(g, x - i * (starSize + 2), y, starSize);
}
}
private void drawStar(Graphics2D g, int cx, int cy, int size)
{
int pts = 5;
double outerR = size / 2.0;
double innerR = outerR * 0.4;
int[] xs = new int[pts * 2];
int[] ys = new int[pts * 2];
for (int i = 0; i < pts * 2; i++) {
double angle = Math.PI / pts * i - Math.PI / 2;
double r = (i % 2 == 0) ? outerR : innerR;
xs[i] = (int) (cx + r * Math.cos(angle));
ys[i] = (int) (cy + r * Math.sin(angle));
}
g.fillPolygon(xs, ys, pts * 2);
}
}
// =========================================================================
// MainMenu constructor
// =========================================================================
public MainMenu(GameFields gm)
{
setLayout(null);
setPreferredSize(new Dimension(640, 360));
setBackground(new Color(20, 20, 40)); // dark navy background
// ── Title label ──────────────────────────────────────────────────────
JLabel title = new JLabel("SELECT A LEVEL", SwingConstants.CENTER);
title.setFont(new Font("Arial", Font.BOLD, 28));
title.setForeground(Color.WHITE);
title.setBounds(0, 20, 640, 40);
add(title);
// ── Three level buttons ───────────────────────────────────────────────
// Colours: Level 1 = teal/cyan, Level 2 = orange, Level 3 = red
LevelButton lvl1 = new LevelButton(1, "The Crossing",
new Color(40, 100, 110), // muted teal (normal)
new Color(0, 200, 220)); // bright cyan (hover)
LevelButton lvl2 = new LevelButton(2, "Double Danger",
new Color(120, 70, 20), // muted orange (normal)
new Color(255, 150, 0)); // bright orange (hover)
LevelButton lvl3 = new LevelButton(3, "The Gauntlet",
new Color(110, 20, 20), // muted red (normal)
new Color(255, 60, 60)); // bright red (hover)
// Position buttons evenly across the panel (panel is 640 wide)
// Each button is 160 px wide; 3 buttons = 480 px; padding = 80 px each side
lvl1.setBounds(80, 100, 160, 160);
lvl2.setBounds(240, 100, 160, 160);
lvl3.setBounds(400, 100, 160, 160);
add(lvl1);
add(lvl2);
add(lvl3);
// Each button starts the corresponding level via levelStart()
lvl1.addActionListener(e -> gm.levelStart(gm.playerName1, gm.playerName2, "LEVEL1"));
lvl2.addActionListener(e -> gm.levelStart(gm.playerName1, gm.playerName2, "LEVEL2"));
lvl3.addActionListener(e -> gm.levelStart(gm.playerName1, gm.playerName2, "LEVEL3"));
// ── Difficulty radio buttons (unchanged from original) ────────────────
JPanel difficultyPanel = new JPanel();
difficultyPanel.setOpaque(false);
difficultyPanel.setLayout(new FlowLayout(FlowLayout.CENTER, 10, 0));
JRadioButton easy = new JRadioButton("Easy", true);
JRadioButton medium = new JRadioButton("Medium");
JRadioButton hard = new JRadioButton("Hard");
for (JRadioButton rb : new JRadioButton[]{easy, medium, hard}) {
rb.setForeground(Color.WHITE);
rb.setOpaque(false);
}
ButtonGroup difficultyGroup = new ButtonGroup();
difficultyGroup.add(easy);
difficultyGroup.add(medium);
difficultyGroup.add(hard);
difficultyPanel.add(new JLabel("Difficulty:") {{
setForeground(Color.LIGHT_GRAY);
}});
difficultyPanel.add(easy);
difficultyPanel.add(medium);
difficultyPanel.add(hard);
difficultyPanel.setBounds(160, 278, 320, 30);
add(difficultyPanel);
easy .addActionListener(e -> gm.setDifficulty("Easy"));
medium.addActionListener(e -> gm.setDifficulty("Medium"));
hard .addActionListener(e -> gm.setDifficulty("Hard"));
// ── Quit button ───────────────────────────────────────────────────────
JButton quit = new JButton("QUIT");
quit.setFont(new Font("Arial", Font.BOLD, 12));
quit.setForeground(Color.WHITE);
quit.setBackground(new Color(80, 20, 20));
quit.setBorder(BorderFactory.createLineBorder(new Color(180, 60, 60), 2));
quit.setFocusPainted(false);
quit.setBounds(275, 75, 90, 28);
add(quit);
quit.addActionListener(e -> {
String[] lines = {"", "", "", // Top padding
"GAME !!", "",
"Developed by","Aimee Azzahra","Ayan Mishra", "Jennifer Phan",
"Character Art by Jennifer Phan",
"Background Art by Aimee Azzahra",
"other credits ig idk",
"Thanks for playing!",
"", "", "", "","",""}; //bottom padding
CreditsPanel credits = new CreditsPanel(lines, () -> System.exit(0));
container.add(credits, "CREDITS");
cardLayout.show(container, "CREDITS");
credits.start();
});
}
// Paint the dark gradient background
@Override
protected void paintComponent(Graphics g)
{
Graphics2D g2 = (Graphics2D) g;
GradientPaint gp = new GradientPaint(0, 0, new Color(10, 10, 30),
0, getHeight(), new Color(30, 10, 50));
g2.setPaint(gp);
g2.fillRect(0, 0, getWidth(), getHeight());
super.paintComponent(g); // paint children
}
public void setContainer(JPanel container, CardLayout layout)
{
this.container = container;
this.cardLayout = layout;
}
}

72
Player.java Normal file
View File

@@ -0,0 +1,72 @@
import java.awt.*;
/**
* Player — pure data class.
*
* <p>Holds position, velocity, input state, sprite frames, and hitbox. No Swing, no game logic —
* GamePanel owns all of that.
*/
public class Player {
// ── Identity ──────────────────────────────────────────────────────────────
public final boolean isSeal; // false = bear, true = seal
public String name = "";
// ── Position & physics ────────────────────────────────────────────────────
public int x, y;
public int velocityY = 0;
public int speed = 5;
public boolean onGround = false;
// ── Input flags (set by GamePanel's KeyListener) ──────────────────────────
public boolean left, right, up;
// ── Hitbox (offset inward from sprite top-left) ───────────────────────────
public final Rectangle hitbox = new Rectangle(5, 10, 20, 30);
// ── Sprite animation ──────────────────────────────────────────────────────
public Image[] walkFrames;
public int currentFrame = 0;
// ── Constructor ───────────────────────────────────────────────────────────
public Player(boolean isSeal, Image[] frames) {
this.isSeal = isSeal;
this.walkFrames = frames;
}
// ── Helpers ───────────────────────────────────────────────────────────────
/** Returns the current animation frame, or null if no sprites loaded. */
public Image currentSprite() {
if (walkFrames == null || walkFrames.length == 0) return null;
return walkFrames[currentFrame];
}
/** Moves the player to (x, y) and repositions the hitbox to match. */
public void setPosition(int x, int y) {
this.x = x;
this.y = y;
syncHitbox();
}
/** Call after any manual change to x or y to keep the hitbox aligned. */
public void syncHitbox() {
hitbox.setLocation(x + 5, y + 10);
}
/** Advance animation frame; typically called every N game-loop ticks. */
public void nextFrame() {
if (walkFrames != null && walkFrames.length > 1)
currentFrame = (currentFrame + 1) % walkFrames.length;
}
/** Reset to idle (frame 0). */
public void idleFrame() {
currentFrame = 0;
}
/** Clear all movement input flags — call on level reset. */
public void clearInput() {
left = right = up = false;
}
}

View File

@@ -1,41 +1,509 @@
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import javax.swing.*;
public class TwoPlayerGameDriver
{
public static void main(String[] args)
{
JFrame frame = new JFrame();
frame.setTitle("Two Player Game");
/**
* GameDriver — application entry point.
*
* <p>Owns the JFrame and the root CardLayout container. All UI screens (name entry, main menu,
* credits) live here as static inner classes so the project stays at four .java files.
*
* <p>Card keys: "NAMES" — JTextField name-entry screen (shown first) "MENU" — level-select /
* difficulty screen "LEVEL1" — in-game (all three share one GamePanel instance) "LEVEL2" "LEVEL3"
* "CREDITS" — scrolling JList credits, added on demand
*/
public class TwoPlayerGameDriver {
public static void main(String[] args) {
SwingUtilities.invokeLater(
() -> {
JFrame frame = new JFrame("Two Player Game");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setResizable(false);
// CardLayout
CardLayout layout = new CardLayout();
JPanel container = new JPanel(layout);
//Panels
GameFields gamePanel = new GameFields();
LvlManager levelMenu = new LvlManager(gamePanel);
MainMenu menuPanel = new MainMenu(gamePanel);
// One shared GamePanel — added under all three level keys
GamePanel gamePanel = new GamePanel();
container.add(gamePanel, "LEVEL1");
container.add(gamePanel, "LEVEL2");
container.add(gamePanel, "LEVEL3");
//add to cardLayout
container.add(menuPanel, "MENU"); //+ more
container.add(levelMenu, "LEVELS");
container.add(gamePanel, "GAME");
// Main menu (level-select)
MainMenu menu = new MainMenu(gamePanel);
container.add(menu, "MENU");
//switch screens
menuPanel.setContainer(container,layout); //+ more
levelMenu.setContainer(container,layout);
gamePanel.setContainer(container,layout);
// Name-entry screen — shown first on launch
NameEntry nameEntry = new NameEntry(gamePanel, menu);
container.add(nameEntry, "NAMES");
// Wire container + layout into every screen that needs to navigate
gamePanel.setContainer(container, layout);
menu.setContainer(container, layout);
nameEntry.setContainer(container, layout);
frame.add(container);
frame.pack();
frame.setSize(640, 360);
frame.setLocationRelativeTo(null);
//gamePanel.requestFocus();
frame.setVisible(true);
//frame
frame.setResizable(false);
frame.setSize(640, 360); //<-- change later
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// Start on the name-entry screen
layout.show(container, "NAMES");
});
}
// ══════════════════════════════════════════════════════════════════════════
// NameEntry — JTextField-based screen that captures player names
//
// Rubric: "Input text box — players name input"
// ══════════════════════════════════════════════════════════════════════════
static class NameEntry extends JPanel {
private JPanel container;
private CardLayout cardLayout;
private final JTextField field1 = new JTextField("Player 1", 14);
private final JTextField field2 = new JTextField("Player 2", 14);
NameEntry(GamePanel gp, MainMenu menu) {
setLayout(null);
setPreferredSize(new Dimension(640, 360));
setOpaque(false);
// Title
JLabel title = new JLabel("ENTER PLAYER NAMES", SwingConstants.CENTER);
title.setFont(new Font("Arial", Font.BOLD, 26));
title.setForeground(Color.WHITE);
title.setBounds(0, 40, 640, 40);
add(title);
// Sub-labels
JLabel lbl1 = new JLabel("Bear Player (Arrow Keys):", SwingConstants.RIGHT);
lbl1.setForeground(new Color(210, 140, 60));
lbl1.setFont(new Font("Arial", Font.BOLD, 14));
lbl1.setBounds(80, 130, 220, 30);
add(lbl1);
JLabel lbl2 = new JLabel("Seal Player (WASD):", SwingConstants.RIGHT);
lbl2.setForeground(new Color(70, 160, 220));
lbl2.setFont(new Font("Arial", Font.BOLD, 14));
lbl2.setBounds(80, 185, 220, 30);
add(lbl2);
// Text fields — select-all on focus for easy overtype
styleField(field1, new Color(210, 140, 60));
styleField(field2, new Color(70, 160, 220));
field1.setBounds(315, 130, 200, 30);
field2.setBounds(315, 185, 200, 30);
add(field1);
add(field2);
// Confirm button — saves names to GamePanel & GameDriver shared state,
// then flips to the menu
JButton confirm = new JButton("CONTINUE →");
confirm.setFont(new Font("Arial", Font.BOLD, 14));
confirm.setForeground(Color.WHITE);
confirm.setBackground(new Color(30, 80, 30));
confirm.setBorder(BorderFactory.createLineBorder(new Color(80, 200, 80), 2));
confirm.setFocusPainted(false);
confirm.setBounds(220, 255, 200, 36);
add(confirm);
ActionListener go =
e -> {
String n1 = field1.getText().trim();
String n2 = field2.getText().trim();
if (n1.isEmpty()) n1 = "Bear";
if (n2.isEmpty()) n2 = "Seal";
// Push names to GamePanel so startLevel() picks them up
gp.bear.name = n1;
gp.seal.name = n2;
// Also give MainMenu a reference so its buttons can pass them through
menu.pendingName1 = n1;
menu.pendingName2 = n2;
cardLayout.show(container, "MENU");
};
confirm.addActionListener(go);
// Pressing Enter in either field also confirms
field1.addActionListener(go);
field2.addActionListener(go);
// Instruction hint
JLabel hint = new JLabel("Press Enter or click Continue", SwingConstants.CENTER);
hint.setForeground(Color.GRAY);
hint.setFont(new Font("Arial", Font.ITALIC, 11));
hint.setBounds(0, 305, 640, 20);
add(hint);
}
void setContainer(JPanel c, CardLayout cl) {
container = c;
cardLayout = cl;
}
private void styleField(JTextField f, Color accent) {
f.setFont(new Font("Arial", Font.PLAIN, 14));
f.setBackground(new Color(30, 30, 55));
f.setForeground(accent);
f.setCaretColor(Color.WHITE);
f.setBorder(
BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(accent, 2),
BorderFactory.createEmptyBorder(2, 6, 2, 6)));
f.addFocusListener(
new FocusAdapter() {
@Override
public void focusGained(FocusEvent e) {
f.selectAll();
}
});
}
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g;
g2.setPaint(
new GradientPaint(0, 0, new Color(10, 10, 30), 0, getHeight(), new Color(20, 10, 40)));
g2.fillRect(0, 0, getWidth(), getHeight());
super.paintComponent(g);
}
}
// ══════════════════════════════════════════════════════════════════════════
// MainMenu — level-select, difficulty chooser, speed slider, quit
// ══════════════════════════════════════════════════════════════════════════
static class MainMenu extends JPanel {
private JPanel container;
private CardLayout cardLayout;
// Names supplied by NameEntry before the menu is shown
String pendingName1 = "Bear";
String pendingName2 = "Seal";
MainMenu(GamePanel gp) {
setLayout(null);
setPreferredSize(new Dimension(640, 360));
setOpaque(false);
// Title
JLabel title = new JLabel("SELECT A LEVEL", SwingConstants.CENTER);
title.setFont(new Font("Arial", Font.BOLD, 26));
title.setForeground(Color.WHITE);
title.setBounds(0, 18, 640, 36);
add(title);
// "Change names" link back to name entry
JButton changeName = new JButton("← Change Names");
changeName.setFont(new Font("Arial", Font.PLAIN, 11));
changeName.setForeground(Color.LIGHT_GRAY);
changeName.setContentAreaFilled(false);
changeName.setBorderPainted(false);
changeName.setFocusPainted(false);
changeName.setBounds(10, 10, 140, 22);
add(changeName);
changeName.addActionListener(e -> cardLayout.show(container, "NAMES"));
// Level buttons
LevelButton lvl1 =
new LevelButton(1, "The Crossing", new Color(40, 100, 110), new Color(0, 200, 220));
LevelButton lvl2 =
new LevelButton(2, "Double Danger", new Color(120, 70, 20), new Color(255, 150, 0));
LevelButton lvl3 =
new LevelButton(3, "The Gauntlet", new Color(110, 20, 20), new Color(255, 60, 60));
lvl1.setBounds(80, 90, 160, 160);
lvl2.setBounds(240, 90, 160, 160);
lvl3.setBounds(400, 90, 160, 160);
add(lvl1);
add(lvl2);
add(lvl3);
lvl1.addActionListener(e -> gp.startLevel(pendingName1, pendingName2, "LEVEL1"));
lvl2.addActionListener(e -> gp.startLevel(pendingName1, pendingName2, "LEVEL2"));
lvl3.addActionListener(e -> gp.startLevel(pendingName1, pendingName2, "LEVEL3"));
// Difficulty radio buttons
JRadioButton easy = new JRadioButton("Easy", true);
JRadioButton medium = new JRadioButton("Medium");
JRadioButton hard = new JRadioButton("Hard");
ButtonGroup grp = new ButtonGroup();
for (JRadioButton rb : new JRadioButton[] {easy, medium, hard}) {
rb.setForeground(Color.WHITE);
rb.setOpaque(false);
grp.add(rb);
}
easy.addActionListener(e -> gp.setDifficulty("Easy"));
medium.addActionListener(e -> gp.setDifficulty("Medium"));
hard.addActionListener(e -> gp.setDifficulty("Hard"));
JLabel diffLabel = new JLabel("Difficulty:");
diffLabel.setForeground(Color.LIGHT_GRAY);
JPanel diff = new JPanel(new FlowLayout(FlowLayout.CENTER, 8, 0));
diff.setOpaque(false);
diff.add(diffLabel);
diff.add(easy);
diff.add(medium);
diff.add(hard);
diff.setBounds(140, 268, 360, 26);
add(diff);
// Quit → rolling credits → System.exit
JButton quit = new JButton("QUIT");
quit.setFont(new Font("Arial", Font.BOLD, 12));
quit.setForeground(Color.WHITE);
quit.setBackground(new Color(80, 20, 20));
quit.setBorder(BorderFactory.createLineBorder(new Color(180, 60, 60), 2));
quit.setFocusPainted(false);
quit.setBounds(275, 58, 90, 26);
add(quit);
quit.addActionListener(e -> showCredits());
}
void setContainer(JPanel c, CardLayout cl) {
container = c;
cardLayout = cl;
}
private void showCredits() {
String[] lines = {
"",
"",
"",
"",
"~ CREDITS ~",
"",
"Developed by:",
"Aimee Azzahra",
"Ayan Mishra",
"Jennifer Phan",
"",
"Character Art — Jennifer Phan",
"Background Art — Aimee Azzahra",
"",
"Programming:",
"Jennifer — Player object, fluid logic, float method",
"Aimee — Wall/Rock collision, TileSet class, tap method",
"Ayan — Main menu, level manager, hold method",
"",
"Thanks for playing!",
"",
"",
"",
"",
""
};
CreditsPanel credits = new CreditsPanel(lines, () -> System.exit(0));
container.add(credits, "CREDITS");
cardLayout.show(container, "CREDITS");
credits.start();
}
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g;
g2.setPaint(
new GradientPaint(0, 0, new Color(10, 10, 30), 0, getHeight(), new Color(30, 10, 50)));
g2.fillRect(0, 0, getWidth(), getHeight());
super.paintComponent(g);
}
}
// ══════════════════════════════════════════════════════════════════════════
// LevelButton — rollover icon changes dramatically (rubric item)
// Normal : small muted rounded tile + level number
// Hover : large bright tile with glow rings, big number, stars
// ══════════════════════════════════════════════════════════════════════════
private static class LevelButton extends JButton {
private static final int CANVAS = 140;
private static final int TILE_NORM = 76;
private static final int TILE_HOV = 112;
private final int level;
private final String levelName;
private final Color tileColor;
private final Color hoverColor;
private boolean hovered = false;
LevelButton(int level, String levelName, Color tile, Color hover) {
this.level = level;
this.levelName = levelName;
this.tileColor = tile;
this.hoverColor = hover;
setContentAreaFilled(false);
setBorderPainted(false);
setFocusPainted(false);
setOpaque(false);
setIcon(buildIcon(false));
setRolloverEnabled(true);
setRolloverIcon(buildIcon(true));
setPreferredSize(new Dimension(160, 160));
setSize(160, 160);
setVerticalTextPosition(SwingConstants.BOTTOM);
setHorizontalTextPosition(SwingConstants.CENTER);
setFont(new Font("Arial", Font.BOLD, 12));
setForeground(Color.WHITE);
addMouseListener(
new MouseAdapter() {
@Override
public void mouseEntered(MouseEvent e) {
hovered = true;
repaint();
}
@Override
public void mouseExited(MouseEvent e) {
hovered = false;
repaint();
}
});
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
String label = hovered ? levelName : "Level " + level;
Color fg = hovered ? hoverColor.brighter().brighter() : Color.LIGHT_GRAY;
FontMetrics fm = g2.getFontMetrics(getFont());
int tx = (getWidth() - fm.stringWidth(label)) / 2;
int ty = getHeight() - 6;
g2.setFont(getFont());
g2.setColor(Color.BLACK);
g2.drawString(label, tx + 1, ty + 1);
g2.setColor(fg);
g2.drawString(label, tx, ty);
g2.dispose();
}
private ImageIcon buildIcon(boolean hover) {
BufferedImage img = new BufferedImage(CANVAS, CANVAS, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = img.createGraphics();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
int cx = CANVAS / 2, cy = CANVAS / 2;
if (hover) {
// Concentric glow rings
for (int i = 4; i >= 1; i--) {
int r = TILE_HOV / 2 + i * 9;
g.setColor(
new Color(
hoverColor.getRed() / 255f,
hoverColor.getGreen() / 255f,
hoverColor.getBlue() / 255f,
0.12f * i));
g.fillOval(cx - r, cy - r, r * 2, r * 2);
}
g.setColor(hoverColor);
g.fillRoundRect(cx - TILE_HOV / 2, cy - TILE_HOV / 2, TILE_HOV, TILE_HOV, 24, 24);
g.setColor(Color.WHITE);
g.setStroke(new BasicStroke(3f));
g.drawRoundRect(cx - TILE_HOV / 2, cy - TILE_HOV / 2, TILE_HOV, TILE_HOV, 24, 24);
drawNumber(g, cx, cy, 44);
drawStars(g, cx + TILE_HOV / 2 - 26, cy - TILE_HOV / 2 + 6, level);
} else {
g.setColor(tileColor);
g.fillRoundRect(cx - TILE_NORM / 2, cy - TILE_NORM / 2, TILE_NORM, TILE_NORM, 16, 16);
g.setColor(tileColor.brighter());
g.setStroke(new BasicStroke(1.5f));
g.drawRoundRect(cx - TILE_NORM / 2, cy - TILE_NORM / 2, TILE_NORM, TILE_NORM, 16, 16);
drawNumber(g, cx, cy, 28);
drawStars(g, cx + TILE_NORM / 2 - 20, cy - TILE_NORM / 2 + 4, level);
}
g.dispose();
return new ImageIcon(img);
}
private void drawNumber(Graphics2D g, int cx, int cy, int fs) {
g.setColor(Color.WHITE);
g.setFont(new Font("Arial", Font.BOLD, fs));
FontMetrics fm = g.getFontMetrics();
String num = String.valueOf(level);
g.drawString(num, cx - fm.stringWidth(num) / 2, cy + fm.getAscent() / 2 - 2);
}
private void drawStars(Graphics2D g, int x, int y, int count) {
g.setColor(new Color(255, 220, 50));
for (int i = 0; i < count; i++) drawStar(g, x - i * 13, y, 10);
}
private void drawStar(Graphics2D g, int cx, int cy, int size) {
int pts = 5;
double or = size / 2.0, ir = or * 0.4;
int[] xs = new int[pts * 2], ys = new int[pts * 2];
for (int i = 0; i < pts * 2; i++) {
double a = Math.PI / pts * i - Math.PI / 2;
double r = (i % 2 == 0) ? or : ir;
xs[i] = (int) (cx + r * Math.cos(a));
ys[i] = (int) (cy + r * Math.sin(a));
}
g.fillPolygon(xs, ys, pts * 2);
}
}
// ══════════════════════════════════════════════════════════════════════════
// CreditsPanel — scrolling JList (rubric item: "Use JList")
// ══════════════════════════════════════════════════════════════════════════
static class CreditsPanel extends JPanel {
private final JScrollPane scrollPane;
private final Timer scrollTimer;
private int scrollPos = 0;
CreditsPanel(String[] lines, Runnable onComplete) {
setLayout(new BorderLayout());
setBackground(Color.BLACK);
JList<String> list = new JList<>(lines);
list.setBackground(Color.BLACK);
list.setForeground(Color.WHITE);
list.setFont(new Font("SansSerif", Font.BOLD, 18));
list.setFixedCellHeight(38);
((DefaultListCellRenderer) list.getCellRenderer())
.setHorizontalAlignment(SwingConstants.CENTER);
scrollPane = new JScrollPane(list);
scrollPane.setBorder(null);
scrollPane.getVerticalScrollBar().setPreferredSize(new Dimension(0, 0));
add(scrollPane, BorderLayout.CENTER);
// Scroll animation — reads scrollPane field safely; no lambda self-reference
scrollTimer =
new Timer(
28,
new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
JScrollBar bar = scrollPane.getVerticalScrollBar();
bar.setValue(++scrollPos);
if (scrollPos >= bar.getMaximum() - bar.getVisibleAmount() + 120) {
((Timer) e.getSource()).stop();
onComplete.run();
}
}
});
JButton skip = new JButton("SKIP");
skip.setFont(new Font("Arial", Font.BOLD, 12));
skip.addActionListener(
e -> {
scrollTimer.stop();
onComplete.run();
});
add(skip, BorderLayout.SOUTH);
}
void start() {
scrollPos = 0;
scrollTimer.start();
}
}
}