From 5ba16402e4e306e45bc98a28abfd0cba94862152 Mon Sep 17 00:00:00 2001 From: Urban Modig Date: Tue, 2 Sep 2025 21:30:08 +0200 Subject: [PATCH] Deathanimation working --- .../se/urmo/game/entities/ghost/Ghost.java | 8 +- .../se/urmo/game/entities/pacman/PacMan.java | 108 +++++++++++++++--- .../java/se/urmo/game/state/GhostManager.java | 7 ++ .../java/se/urmo/game/state/PlayingState.java | 13 +-- .../java/se/urmo/game/util/Direction.java | 14 ++- 5 files changed, 120 insertions(+), 30 deletions(-) diff --git a/src/main/java/se/urmo/game/entities/ghost/Ghost.java b/src/main/java/se/urmo/game/entities/ghost/Ghost.java index 526194d..b804ad7 100644 --- a/src/main/java/se/urmo/game/entities/ghost/Ghost.java +++ b/src/main/java/se/urmo/game/entities/ghost/Ghost.java @@ -1,5 +1,6 @@ package se.urmo.game.entities.ghost; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import se.urmo.game.collision.GhostCollisionChecker; import se.urmo.game.entities.BaseAnimated; @@ -13,10 +14,8 @@ import se.urmo.game.map.GameMap; import se.urmo.game.state.GhostManager; import se.urmo.game.state.LevelManager; import se.urmo.game.util.Direction; -import se.urmo.game.util.MiscUtil; import se.urmo.game.util.MyPoint; -import java.awt.Color; import java.awt.Graphics; import java.awt.Point; import java.awt.image.BufferedImage; @@ -49,6 +48,8 @@ public class Ghost extends BaseAnimated { private final GhostStrategy fearStrategy = new FearStrategy(); private int frightenedTimer = 0; private boolean isBlinking = false; + @Setter + private boolean frozen; public Ghost(GhostCollisionChecker collisionChecker, GhostStrategy strategy, GhostStrategy scaterStrategy, int animation, LevelManager levelManager) { @@ -78,7 +79,7 @@ public class Ghost extends BaseAnimated { } public void update(PacMan pacman, GameMap map) { - //updateAnimationTick(); + if (frozen) return; if (mode == GhostMode.FRIGHTENED) { updateInFrightendMode(); } @@ -223,4 +224,5 @@ public class Ghost extends BaseAnimated { public void resetModes() { mode = GhostMode.CHASE; } + } diff --git a/src/main/java/se/urmo/game/entities/pacman/PacMan.java b/src/main/java/se/urmo/game/entities/pacman/PacMan.java index bf1a7c7..ef57d4c 100644 --- a/src/main/java/se/urmo/game/entities/pacman/PacMan.java +++ b/src/main/java/se/urmo/game/entities/pacman/PacMan.java @@ -14,10 +14,15 @@ import se.urmo.game.util.MyPoint; import java.awt.*; import java.awt.image.BufferedImage; import java.util.Arrays; +import java.util.stream.Stream; @Slf4j public class PacMan extends BaseAnimated { + private enum PacmanState { + ALIVE, DYING, DEAD + } + public static final int PACMAN_SIZE = 32; public static final int PACMAN_OFFSET = PACMAN_SIZE / 2; private static final int COLLISION_BOX_SIZE = 16; @@ -26,7 +31,7 @@ public class PacMan extends BaseAnimated { private static final int ANIMATION_UPDATE_FREQUENCY = 10; private static final double BASE_SPEED = 0.40; private boolean moving = false; - private final BufferedImage[][] spriteSheets; + private final BufferedImage[][] spriteSheets;// [row][col] private MyPoint position; private final CollisionChecker collisionChecker; private final LevelManager levelManager; @@ -35,6 +40,16 @@ public class PacMan extends BaseAnimated { private Direction direction = Direction.NONE; private double pacmanLevelSpeed; + private static BufferedImage[] deathFramesBase; // original, flattened [0..7] + private BufferedImage[] deathFrames; // working copy + private long lastChangeNs; + private static final long FRAME_NS = 80_000_000L; // 80 ms + + // animation state + @Setter + private PacmanState state = PacmanState.ALIVE; + private int deathFrameIdx = 0; + public PacMan(CollisionChecker collisionChecker, LevelManager levelManager) { super(ANIMATION_UPDATE_FREQUENCY, 4); this.collisionChecker = collisionChecker; @@ -43,13 +58,19 @@ public class PacMan extends BaseAnimated { 26 * GameMap.MAP_TILESIZE + GameMap.OFFSET_X, 13 * GameMap.MAP_TILESIZE + GameMap.OFFSET_Y + ((double) GameMap.MAP_TILESIZE / 2)); this.startPosition = this.position; - this.spriteSheets = loadAnimation(); + Sprites spriteSheets1 = loadAnimation(); + this.spriteSheets = spriteSheets1.spriteSheets; + this.deathFramesBase = spriteSheets1.deathFrames; this.pacmanLevelSpeed = this.levelManager.getPacmanLevelSpeed(); } - private BufferedImage[][] loadAnimation() { + record Sprites(BufferedImage[][] spriteSheets, BufferedImage[] deathFrames) { + } + + private Sprites loadAnimation() { BufferedImage[][] image = new BufferedImage[3][4]; BufferedImage[][] spriteMap = new BufferedImage[6][4]; + BufferedImage[] deathFrames; BufferedImage img = LoadSave.GetSpriteAtlas("sprites/PacManAssets-PacMan.png"); for (int row = 0; row < 3; row++) { @@ -59,7 +80,7 @@ public class PacMan extends BaseAnimated { } spriteMap[Direction.RIGHT.ordinal()] = image[0]; spriteMap[Direction.LEFT.ordinal()] = Arrays.stream(image[0]) - .map(i -> LoadSave.rotate(i, 180)) + .map(i -> LoadSave.rotate(i, Direction.LEFT.angel)) .toArray(BufferedImage[]::new); spriteMap[Direction.DOWN.ordinal()] = Arrays.stream(image[0]) .map(i -> LoadSave.rotate(i, 90)) @@ -67,22 +88,64 @@ public class PacMan extends BaseAnimated { spriteMap[Direction.UP.ordinal()] = Arrays.stream(image[0]) .map(i -> LoadSave.rotate(i, 270)) .toArray(BufferedImage[]::new); - spriteMap[4] = image[1]; - spriteMap[5] = image[2]; - return spriteMap; + deathFrames = Stream.concat(Arrays.stream(image[1]), Arrays.stream(image[2])) + .toArray(BufferedImage[]::new); + + + return new Sprites(spriteMap, deathFrames); } public void draw(Graphics g) { - g.drawImage( - spriteSheets[direction == Direction.NONE ? 0 : direction.ordinal()][aniIndex], - (int) position.x - PACMAN_OFFSET, - (int) position.y - PACMAN_OFFSET, - PACMAN_SIZE, - PACMAN_SIZE, null); + switch (state) { + case ALIVE -> drawAlive(g); + case DYING -> drawDead(g); + } + } + + private void drawAlive(Graphics g) { + if (state != PacmanState.ALIVE) return; // ignore if not dying/dead + g.drawImage( + spriteSheets[direction == Direction.NONE ? 0 : direction.ordinal()][aniIndex], + (int) position.x - PACMAN_OFFSET, + (int) position.y - PACMAN_OFFSET, + PACMAN_SIZE, + PACMAN_SIZE, null); + } + + private void drawDead(Graphics g) { + if (state == PacmanState.ALIVE) return; // ignore if not dying/dead + + g.drawImage( + deathFrames[deathFrameIdx], + (int) position.x - PACMAN_OFFSET, + (int) position.y - PACMAN_OFFSET, + PACMAN_SIZE, + PACMAN_SIZE, + null + ); } public void update() { - if(moving) { + switch (state) { + case ALIVE -> updateAlive(); + case DYING -> updateDead(); + } + } + + private void updateDead() { + if (state != PacmanState.DYING) return; + + long now = System.nanoTime(); + while (now - lastChangeNs >= FRAME_NS && deathFrameIdx < deathFramesBase.length - 1) { + deathFrameIdx++; + lastChangeNs += FRAME_NS; // carry over exact cadence + } + } + + private void updateAlive() { + if (state != PacmanState.ALIVE) return; + + if (moving) { MyPoint mpoint = switch (direction) { case RIGHT -> new MyPoint(position.x + getSpeed(), position.y); case LEFT -> new MyPoint(position.x - getSpeed(), position.y); @@ -99,6 +162,21 @@ public class PacMan extends BaseAnimated { } } + // called by PlayingState when collision (non-frightened) + public void startDeathAnimation() { + log.info("Starting death animation"); + state = PacmanState.DYING; + deathFrameIdx = 0; + lastChangeNs = System.nanoTime(); // reset stopwatch right now + deathFrames = Arrays.stream(deathFramesBase) + .map(img -> LoadSave.rotate(img, direction.angel)) + .toArray(BufferedImage[]::new); + } + + public boolean isDeathDone() { + return state == PacmanState.DEAD; + } + private double getSpeed() { return BASE_SPEED * pacmanLevelSpeed; } @@ -114,6 +192,8 @@ public class PacMan extends BaseAnimated { public void reset() { resetPosition(); aniIndex = 0; // reset animation to start + state = PacmanState.ALIVE; + deathFrameIdx = 0; } public Image getLifeIcon() { diff --git a/src/main/java/se/urmo/game/state/GhostManager.java b/src/main/java/se/urmo/game/state/GhostManager.java index 000928d..2ed9131 100644 --- a/src/main/java/se/urmo/game/state/GhostManager.java +++ b/src/main/java/se/urmo/game/state/GhostManager.java @@ -1,6 +1,7 @@ package se.urmo.game.state; import lombok.Getter; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import se.urmo.game.collision.GhostCollisionChecker; import se.urmo.game.entities.ghost.strategy.BlinkyStrategy; @@ -41,6 +42,7 @@ public class GhostManager { 5000, 20000, // scatter 5s, chase 20s 5000, Integer.MAX_VALUE // scatter 5s, then chase forever }; + private boolean frozen; public GhostManager(GhostCollisionChecker ghostCollisionChecker, AnimationManager animationManager, LevelManager levelManager) { this.levelManager = levelManager; @@ -64,6 +66,7 @@ public class GhostManager { } public void update(PacMan pacman, GameMap map) { + long now = System.currentTimeMillis(); if (phaseIndex < cycleDurations.length) { int duration = cycleDurations[phaseIndex]; @@ -97,4 +100,8 @@ public class GhostManager { setMode(GhostMode.SCATTER); ghosts.forEach(Ghost::resetPosition); } + + public void setFrozen(boolean frozen) { + this.ghosts.forEach(ghost -> ghost.setFrozen(frozen)); + } } diff --git a/src/main/java/se/urmo/game/state/PlayingState.java b/src/main/java/se/urmo/game/state/PlayingState.java index 0ffb08d..3ddcaad 100644 --- a/src/main/java/se/urmo/game/state/PlayingState.java +++ b/src/main/java/se/urmo/game/state/PlayingState.java @@ -88,22 +88,19 @@ public class PlayingState implements GameState { case LIFE_LOST -> { // Freeze, then reset round (keep dot state) if (phaseElapsed() >= LIFE_LOST_MS) { + pacman.reset(); deathInProgress = false; - resetAfterLifeLost(); + ghostManager.setFrozen(false); setPhase(RoundPhase.READY); if (lives <= 0) { endGame(); } } + pacman.update(); } } } - private void resetAfterLifeLost() { - pacman.reset(); // to start tile, direction stopped - ghostManager.reset(); // to house - } - private void advanceLevel() { levelManager.nextLevel(); map.reset(); @@ -142,8 +139,8 @@ public class PlayingState implements GameState { @Override public void render(Graphics2D g) { map.draw(g); - pacman.draw(g); ghostManager.draw(g); + pacman.draw(g); fruitManager.draw(g); drawUI(g); @@ -215,6 +212,8 @@ public class PlayingState implements GameState { ghost.resetPosition(); ghost.setMode(GhostMode.CHASE); // end frightend } else { + ghostManager.setFrozen(true); + pacman.startDeathAnimation(); deathInProgress = true; // Pac-Man loses a life lives--; diff --git a/src/main/java/se/urmo/game/util/Direction.java b/src/main/java/se/urmo/game/util/Direction.java index 3dfee43..244f819 100644 --- a/src/main/java/se/urmo/game/util/Direction.java +++ b/src/main/java/se/urmo/game/util/Direction.java @@ -1,18 +1,20 @@ package se.urmo.game.util; public enum Direction { - RIGHT(1, 0), - LEFT(-1, 0), - DOWN(0, 1), - UP(0, -1), - NONE(0, 0); + RIGHT(1, 0, 0), + LEFT(-1, 0 , 180), + DOWN(0, 1, 90), + UP(0, -1, 270), + NONE(0, 0, 0); public final int dx; public final int dy; + public final int angel; - Direction(int dx, int dy) { + Direction(int dx, int dy, int angel) { this.dx = dx; this.dy = dy; + this.angel = angel; } public Direction opposite() {