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