yo
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
785
GameFields.java
785
GameFields.java
@@ -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
861
GamePanel.java
Normal 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 (1–10); 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;
|
||||
}
|
||||
}
|
||||
191
LvlManager.java
191
LvlManager.java
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
321
MainMenu.java
321
MainMenu.java
@@ -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
72
Player.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user