package se.urmo.game.state; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import se.urmo.game.collision.CollisionChecker; import se.urmo.game.collision.GhostCollisionChecker; import se.urmo.game.entities.ghost.Ghost; import se.urmo.game.entities.ghost.mode.GhostMode; import se.urmo.game.entities.pacman.PacMan; import se.urmo.game.main.AnimationManager; import se.urmo.game.main.FruitManager; import se.urmo.game.main.GameStateManager; import se.urmo.game.main.GhostManager; import se.urmo.game.main.LevelManager; import se.urmo.game.map.GameMap; import se.urmo.game.map.MapTile; import se.urmo.game.map.TileType; import se.urmo.game.util.Direction; import se.urmo.game.util.GameFonts; import se.urmo.game.util.GameStateType; import java.awt.Color; import java.awt.Graphics2D; import java.awt.Point; import java.awt.event.KeyEvent; @Slf4j public class PlayingState implements GameState { private final GameStateManager gameStateManager; private final GameOverState gameOverState; // Core components private final GhostManager ghostManager; private final FruitManager fruitManager; private final LevelManager levelManager; private final AnimationManager animationManager; private PacMan pacman; @Getter private GameMap map; // Durations (tune to taste) private static final int READY_MS = 1500; private static final int LEVEL_COMPLETE_MS = 1500; private static final int LIFE_LOST_MS = 2000; // Score/Lives private int score = 0; private int lives = 3; private int dotsEaten = 0; // Phase + timers private RoundPhase phase = RoundPhase.PLAYING; private long phaseStartMs = System.currentTimeMillis(); private boolean deathInProgress; private int frightMultiplier; public PlayingState(GameStateManager gameStateManager, GameOverState gameOverState) { this.gameStateManager = gameStateManager; this.gameOverState = gameOverState; this.map = new GameMap("maps/map1.csv"); this.animationManager = new AnimationManager(); this.levelManager = new LevelManager(); this.pacman = new PacMan(new CollisionChecker(map), levelManager); this.animationManager.register(pacman); this.ghostManager = new GhostManager(new GhostCollisionChecker(map), animationManager, levelManager); this.fruitManager = new FruitManager(levelManager); } @Override public void update() { switch (phase) { case READY -> { // Freeze everything during READY if (phaseElapsed() >= READY_MS) { setPhase(RoundPhase.PLAYING); } } case PLAYING -> { animationManager.updateAll(); pacman.update(); ghostManager.update(pacman, map); fruitManager.update(pacman, this); checkCollisions(); handleDots(); } case LEVEL_COMPLETE -> { // Freeze, then advance level if (phaseElapsed() >= LEVEL_COMPLETE_MS) { advanceLevel(); setPhase(RoundPhase.READY); } } case LIFE_LOST -> { // Freeze, then reset round (keep dot state) if (phaseElapsed() >= LIFE_LOST_MS) { pacman.setState(PacMan.PacmanState.ALIVE); deathInProgress = false; ghostManager.setFrozen(false); setPhase(RoundPhase.READY); if (lives <= 0) { endGame(); } } pacman.update(); } } } private void advanceLevel() { levelManager.nextLevel(); map.reset(); ghostManager.reset(); fruitManager.reset(); pacman.setState(PacMan.PacmanState.ALIVE); dotsEaten = 0; } private void setPhase(RoundPhase next) { phase = next; phaseStartMs = System.currentTimeMillis(); } private long phaseElapsed() { return System.currentTimeMillis() - phaseStartMs; } private void handleDots() { Point pacmanScreenPos = pacman.getPosition(); MapTile tile = map.getTile(pacmanScreenPos); boolean wasRemoved = map.removeTileImage(pacmanScreenPos); if (wasRemoved && tile.getTileType() == TileType.LARGE_PELLET) { ghostManager.setFrightMode(); frightMultiplier = 1; } if (wasRemoved) { dotsEaten++; fruitManager.dotEaten(dotsEaten); score += tile.getTileType().getScore(); if (dotsEaten == map.numberOfDots()) { setPhase(RoundPhase.LEVEL_COMPLETE); } } } @Override public void render(Graphics2D g) { map.draw(g); ghostManager.draw(g); pacman.draw(g); fruitManager.draw(g); drawUI(g); // Phase overlays switch (phase) { case READY -> drawCenterText(g, "READY!"); case LEVEL_COMPLETE -> drawCenterText(g, "LEVEL " + levelManager.getCurrentLevel().getLevel() + " COMPLETE!"); case LIFE_LOST -> drawCenterText(g, "LIFE LOST"); default -> { /* no overlay */ } } } private void drawCenterText(Graphics2D g, String text) { g.setFont(GameFonts.arcade(18F)); g.setColor(Color.YELLOW); var fm = g.getFontMetrics(); int cx = GameMap.OFFSET_X + map.columns() * GameMap.MAP_TILESIZE / 2; int cy = GameMap.OFFSET_Y + map.rows() * GameMap.MAP_TILESIZE / 2; g.drawString(text, cx - fm.stringWidth(text) / 2, cy); } private void drawUI(Graphics2D g) { g.setColor(Color.WHITE); g.setFont(GameFonts.arcade(18F)); // Score (above map, left) g.drawString("Your Score", 48, 48); g.drawString("" + score, 48, 72); // Lives (below map, left) for (int i = 1; i < lives; i++) { g.drawImage(pacman.getLifeIcon(), 6 * GameMap.OFFSET_X - i * 24, map.rows() * GameMap.MAP_TILESIZE + GameMap.OFFSET_Y, null); } } @Override public void keyPressed(KeyEvent e) { if (phase != RoundPhase.PLAYING) return; switch (e.getKeyCode()) { case KeyEvent.VK_W -> move(Direction.UP); case KeyEvent.VK_S -> move(Direction.DOWN); case KeyEvent.VK_A -> move(Direction.LEFT); case KeyEvent.VK_D -> move(Direction.RIGHT); } } private void move(Direction direction) { pacman.setMoving(true); pacman.setDirection(direction); } @Override public void keyReleased(KeyEvent e) { pacman.setMoving(false); } private void checkCollisions() { for (Ghost ghost : ghostManager.getGhosts()) { if (deathInProgress) return; // guard //if(overlap(pacman, ghost) double dist = pacman.distanceTo(ghost.getPosition().asPoint()); if (dist < GameMap.MAP_TILESIZE / 2.0) { if (ghost.isEaten()) return; if (ghost.isFrightened()) { // Pac-Man eats ghost score += 200 * (1 << (ghostManager.getGhosts().size() - ghostManager.isFrightened())); ghost.setMode(GhostMode.EATEN); } else { ghostManager.setFrozen(true); pacman.setState(PacMan.PacmanState.DYING); deathInProgress = true; // Pac-Man loses a life lives--; setPhase(RoundPhase.LIFE_LOST); } } } } // 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() { gameOverState.setScore(score); gameOverState.setLevel(levelManager.getCurrentLevel().getLevel()); gameStateManager.setState(GameStateType.GAME_OVER); } public void setScore(int score) { this.score += score; } }