252 lines
8.3 KiB
Java
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;
|
|
}
|
|
}
|