GameOverState WIP

This commit is contained in:
Urban Modig
2025-09-01 21:48:34 +02:00
parent fcd686fad0
commit b13f87f77b
6 changed files with 168 additions and 37 deletions

View File

@ -0,0 +1,94 @@
package se.urmo.game.state;
import se.urmo.game.main.GamePanel;
import se.urmo.game.util.GameFonts;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.event.KeyEvent;
public class GameOverState implements GameState {
private final GameStateManager gsm;
private int finalScore = 0;
private int finalLevel = 0;
private final HighScoreManager highScores; // optional
private boolean saved = false;
private long startMs = System.currentTimeMillis();
public GameOverState(GameStateManager gsm, HighScoreManager highScores) {
this.gsm = gsm;
this.highScores = highScores;
}
@Override
public void update() {
if (!saved) {
if (highScores != null) highScores.submit(finalScore);
saved = true;
}
// could auto-return to menu after N seconds if you like
}
@Override
public void render(Graphics2D g) {
// Clear / dim background
g.setColor(new Color(0,0,0,200));
g.fillRect(0,0, GamePanel.SCREEN_WIDTH, GamePanel.SCREEN_HEIGHT);
// Title
g.setColor(Color.RED);
g.setFont(GameFonts.arcade(32f));
drawCentered(g, "GAME OVER", GamePanel.SCREEN_HEIGHT/2 - 40);
// Stats
g.setColor(Color.WHITE);
g.setFont(GameFonts.arcade(18f));
drawCentered(g, "SCORE: " + finalScore, GamePanel.SCREEN_HEIGHT/2);
drawCentered(g, "LEVEL: " + finalLevel, GamePanel.SCREEN_HEIGHT/2 + 24);
// Prompt
g.setFont(GameFonts.arcade(12f));
drawCentered(g, "Press..", GamePanel.SCREEN_HEIGHT/2 + 64);
drawCentered(g, "ENTER to restart • ESC for menu", GamePanel.SCREEN_HEIGHT/2 + 78);
}
private void drawCentered(Graphics2D g, String text, int y) {
var fm = g.getFontMetrics();
int x = (GamePanel.SCREEN_WIDTH - fm.stringWidth(text)) / 2;
g.drawString(text, x, y);
}
@Override
public void keyPressed(KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_ENTER -> restartNewGame();
case KeyEvent.VK_ESCAPE -> goToMenu();
}
}
@Override
public void keyReleased(KeyEvent e) {
}
private void restartNewGame() {
// Build a fresh PlayingState with everything reset
gsm.setState(GameStateType.PLAYING);
}
private void goToMenu() {
// MenuState menu = new MenuState(gsm);
// gsm.register(StateId.MENU, menu);
// gsm.set(StateId.MENU);
}
public void setScore(int score) {
this.finalScore = score;
}
public void setLevel(int level) {
this.finalLevel = level;
}
}

View File

@ -15,8 +15,10 @@ public class GameStateManager {
public GameStateManager(Game game) { public GameStateManager(Game game) {
this.game = game; this.game = game;
states.put(GameStateType.PLAYING, new PlayingState(game, this)); GameOverState gameOverState = new GameOverState(this, new HighScoreManager());
setState(GameStateType.PLAYING); states.put(GameStateType.PLAYING, new PlayingState(game, this, gameOverState));
states.put(GameStateType.GAME_OVER, gameOverState);
setState(GameStateType.GAME_OVER);
} }
public void setState(GameStateType type) { public void setState(GameStateType type) {

View File

@ -0,0 +1,7 @@
package se.urmo.game.state;
public class HighScoreManager {
public void submit(int finalScore) {
}
}

View File

@ -12,17 +12,17 @@ import se.urmo.game.map.GameMap;
import se.urmo.game.map.MapTile; import se.urmo.game.map.MapTile;
import se.urmo.game.map.TileType; import se.urmo.game.map.TileType;
import se.urmo.game.util.Direction; import se.urmo.game.util.Direction;
import se.urmo.game.util.GameFonts;
import java.awt.*; import java.awt.*;
import java.awt.event.KeyEvent; import java.awt.event.KeyEvent;
import java.io.InputStream;
@Slf4j @Slf4j
public class PlayingState implements GameState { public class PlayingState implements GameState {
public static final int REMAINING_LIVES = 0; public static final int REMAINING_LIVES = 0;
private final Game game; private final Game game;
private final GameStateManager gameStateManager; private final GameStateManager gameStateManager;
private final Font arcadeFont; private final GameOverState gameOverState;
// Core components // Core components
private final GhostManager ghostManager; private final GhostManager ghostManager;
@ -35,7 +35,7 @@ public class PlayingState implements GameState {
// Durations (tune to taste) // Durations (tune to taste)
private static final int READY_MS = 1500; private static final int READY_MS = 1500;
private static final int LEVEL_COMPLETE_MS= 1500; private static final int LEVEL_COMPLETE_MS = 1500;
private static final int LIFE_LOST_MS = 2000; private static final int LIFE_LOST_MS = 2000;
// Score/Lives // Score/Lives
@ -48,9 +48,10 @@ public class PlayingState implements GameState {
private long phaseStartMs = System.currentTimeMillis(); private long phaseStartMs = System.currentTimeMillis();
private boolean deathInProgress; private boolean deathInProgress;
public PlayingState(Game game, GameStateManager gameStateManager) { public PlayingState(Game game, GameStateManager gameStateManager, GameOverState gameOverState) {
this.game = game; this.game = game;
this.gameStateManager = gameStateManager; this.gameStateManager = gameStateManager;
this.gameOverState = gameOverState;
this.map = new GameMap("maps/map1.csv"); this.map = new GameMap("maps/map1.csv");
this.animationManager = new AnimationManager(); this.animationManager = new AnimationManager();
this.levelManager = new LevelManager(); this.levelManager = new LevelManager();
@ -58,7 +59,6 @@ public class PlayingState implements GameState {
this.animationManager.register(pacman); this.animationManager.register(pacman);
this.ghostManager = new GhostManager(new GhostCollisionChecker(map), animationManager, levelManager); this.ghostManager = new GhostManager(new GhostCollisionChecker(map), animationManager, levelManager);
this.fruitManager = new FruitManager(levelManager); this.fruitManager = new FruitManager(levelManager);
this.arcadeFont = loadArcadeFont();
} }
@Override @Override
@ -88,8 +88,12 @@ public class PlayingState implements GameState {
case LIFE_LOST -> { case LIFE_LOST -> {
// Freeze, then reset round (keep dot state) // Freeze, then reset round (keep dot state)
if (phaseElapsed() >= LIFE_LOST_MS) { if (phaseElapsed() >= LIFE_LOST_MS) {
deathInProgress = false;
resetAfterLifeLost(); resetAfterLifeLost();
setPhase(RoundPhase.READY); setPhase(RoundPhase.READY);
if (lives <= 0) {
endGame();
}
} }
} }
} }
@ -122,14 +126,14 @@ public class PlayingState implements GameState {
Point pacmanScreenPos = pacman.getPosition(); Point pacmanScreenPos = pacman.getPosition();
MapTile tile = map.getTile(pacmanScreenPos); MapTile tile = map.getTile(pacmanScreenPos);
boolean wasRemoved = map.removeTileImage(pacmanScreenPos); boolean wasRemoved = map.removeTileImage(pacmanScreenPos);
if(wasRemoved && tile.getTileType() == TileType.LARGE_PELLET){ if (wasRemoved && tile.getTileType() == TileType.LARGE_PELLET) {
ghostManager.setFrightMode(); ghostManager.setFrightMode();
} }
if(wasRemoved){ if (wasRemoved) {
dotsEaten++; dotsEaten++;
fruitManager.dotEaten(dotsEaten); fruitManager.dotEaten(dotsEaten);
score+=tile.getTileType().getScore(); score += tile.getTileType().getScore();
if(dotsEaten == map.numberOfDots()){ if (dotsEaten == map.numberOfDots()) {
setPhase(RoundPhase.LEVEL_COMPLETE); setPhase(RoundPhase.LEVEL_COMPLETE);
} }
} }
@ -153,17 +157,17 @@ public class PlayingState implements GameState {
} }
private void drawCenterText(Graphics2D g, String text) { private void drawCenterText(Graphics2D g, String text) {
g.setFont(arcadeFont.deriveFont(18F)); g.setFont(GameFonts.arcade(18F));
g.setColor(Color.YELLOW); g.setColor(Color.YELLOW);
var fm = g.getFontMetrics(); var fm = g.getFontMetrics();
int cx = GameMap.OFFSET_X + map.columns() * GameMap.MAP_TILESIZE/2; int cx = GameMap.OFFSET_X + map.columns() * GameMap.MAP_TILESIZE / 2;
int cy = GameMap.OFFSET_Y + map.rows() * GameMap.MAP_TILESIZE/2; int cy = GameMap.OFFSET_Y + map.rows() * GameMap.MAP_TILESIZE / 2;
g.drawString(text, cx - fm.stringWidth(text)/2, cy); g.drawString(text, cx - fm.stringWidth(text) / 2, cy);
} }
private void drawUI(Graphics2D g) { private void drawUI(Graphics2D g) {
g.setColor(Color.WHITE); g.setColor(Color.WHITE);
g.setFont(arcadeFont); g.setFont(GameFonts.arcade(18F));
// Score (above map, left) // Score (above map, left)
g.drawString("Your Score", 48, 48); g.drawString("Your Score", 48, 48);
@ -202,6 +206,7 @@ public class PlayingState implements GameState {
private void checkCollisions() { private void checkCollisions() {
for (Ghost ghost : ghostManager.getGhosts()) { for (Ghost ghost : ghostManager.getGhosts()) {
if (deathInProgress) return; // guard if (deathInProgress) return; // guard
//if(overlap(pacman, ghost)
double dist = pacman.distanceTo(ghost.getPosition()); double dist = pacman.distanceTo(ghost.getPosition());
if (dist < GameMap.MAP_TILESIZE / 2.0) { if (dist < GameMap.MAP_TILESIZE / 2.0) {
if (ghost.isFrightened()) { if (ghost.isFrightened()) {
@ -213,28 +218,26 @@ public class PlayingState implements GameState {
deathInProgress = true; deathInProgress = true;
// Pac-Man loses a life // Pac-Man loses a life
lives--; lives--;
if(lives >= REMAINING_LIVES)
setPhase(RoundPhase.LIFE_LOST); setPhase(RoundPhase.LIFE_LOST);
else
endGame();
} }
} }
} }
} }
// private boolean overlaps(PacMan p, Ghost g) {
// // center-distance or AABB; center distance keeps the arcade feel
// double dx = p.getCenterX() - g.getCenterX();
// double dy = p.getCenterY() - g.getCenterY();
// double r = map.getTileSize() * 0.45; // tune threshold
// return (dx*dx + dy*dy) <= r*r;
// }
private void endGame() { private void endGame() {
setPhase(RoundPhase.GAME_OVER); gameOverState.setScore(score);
gameOverState.setLevel(levelManager.getCurrentLevel().getLevel());
gameStateManager.setState(GameStateType.GAME_OVER); gameStateManager.setState(GameStateType.GAME_OVER);
} }
private Font loadArcadeFont() {
try (InputStream is = getClass().getResourceAsStream("/fonts/PressStart2P-Regular.ttf")) {
return Font.createFont(Font.TRUETYPE_FONT, is).deriveFont(16f);
} catch (Exception e) {
return new Font("Monospaced", Font.BOLD, 16);
}
}
public void setScore(int score) { public void setScore(int score) {
this.score += score; this.score += score;
} }

View File

@ -4,6 +4,5 @@ public enum RoundPhase {
READY, // "READY!" shown briefly before play starts READY, // "READY!" shown briefly before play starts
PLAYING, // normal gameplay PLAYING, // normal gameplay
LEVEL_COMPLETE, // all dots eaten; freeze briefly then advance level LEVEL_COMPLETE, // all dots eaten; freeze briefly then advance level
LIFE_LOST, // Pac-Man hit; freeze briefly then reset positions LIFE_LOST // Pac-Man hit; freeze briefly then reset positions}
GAME_OVER
} }

View File

@ -0,0 +1,26 @@
package se.urmo.game.util;
import java.awt.Font;
import java.io.InputStream;
public enum GameFonts {
ARCADE_FONT("/fonts/PressStart2P-Regular.ttf");
private final Font font;
GameFonts(String s) {
font = loadArcadeFont(s);
}
private static Font loadArcadeFont(String name) {
try (InputStream is = GameFonts.class.getResourceAsStream(name)) {
return Font.createFont(Font.TRUETYPE_FONT, is).deriveFont(16f);
} catch (Exception e) {
return new Font("Monospaced", Font.BOLD, 16);
}
}
public static Font arcade(float textSize) {
return ARCADE_FONT.font.deriveFont(textSize);
}
}