Files
PacMan/src/main/java/se/urmo/game/state/PlayingState.java
2025-09-05 23:25:09 +02:00

252 lines
8.3 KiB
Java

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;
}
}