package se.urmo.game.entities.pacman; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import se.urmo.game.collision.CollisionChecker; import se.urmo.game.entities.BaseAnimated; import se.urmo.game.graphics.SpriteLocation; import se.urmo.game.graphics.SpriteSheetManager; import se.urmo.game.main.LevelManager; import se.urmo.game.map.GameMap; import se.urmo.game.util.Direction; import se.urmo.game.util.LoadSave; import se.urmo.game.util.MyPoint; import java.awt.Graphics; import java.awt.Image; import java.awt.Point; import java.awt.Rectangle; import java.awt.image.BufferedImage; import java.util.Arrays; import java.util.stream.Stream; @Slf4j public class PacMan extends BaseAnimated { public static final int PACMAN_SIZE = 32; public static final int PACMAN_OFFSET = PACMAN_SIZE / 2; private static final int COLLISION_BOX_SIZE = 16; private static final int COLLISION_BOX_OFFSET = (PACMAN_SIZE - COLLISION_BOX_SIZE) / 2; private static final int ANIMATION_UPDATE_FREQUENCY = 10; private static final double BASE_SPEED = 0.40; private static final long FRAME_NS = 80_000_000L; // 80 ms public static final int PACMAN_SPRITE_FRAMES = 4; private final MyPoint startPosition; private final CollisionChecker collisionChecker; private final LevelManager levelManager; private final Sprites sprites; private boolean moving = false; private MyPoint position; @Setter @Getter private Direction direction = Direction.NONE; private BufferedImage[] deathFrames; // working copy private long lastChangeNs; // animation state @Setter private PacmanState state = PacmanState.ALIVE; private int deathFrameIdx = 0; public PacMan(CollisionChecker collisionChecker, LevelManager levelManager) { super(ANIMATION_UPDATE_FREQUENCY, PACMAN_SPRITE_FRAMES); this.collisionChecker = collisionChecker; this.levelManager = levelManager; this.position = new MyPoint( 26 * GameMap.MAP_TILESIZE + GameMap.OFFSET_X, 13 * GameMap.MAP_TILESIZE + GameMap.OFFSET_Y + ((double) GameMap.MAP_TILESIZE / 2)); this.startPosition = this.position; this.sprites = loadAnimation(); } private Sprites loadAnimation() { BufferedImage[][] spriteMap = new BufferedImage[6][PACMAN_SPRITE_FRAMES]; BufferedImage[] deathFrames; BufferedImage[] animation = SpriteSheetManager.get(SpriteLocation.PACMAN).getAnimation(0); spriteMap[Direction.RIGHT.ordinal()] = animation; spriteMap[Direction.LEFT.ordinal()] = Arrays.stream(animation) .map(i -> LoadSave.rotate(i, Direction.LEFT.angel)) .toArray(BufferedImage[]::new); spriteMap[Direction.DOWN.ordinal()] = Arrays.stream(animation) .map(i -> LoadSave.rotate(i, Direction.DOWN.angel)) .toArray(BufferedImage[]::new); spriteMap[Direction.UP.ordinal()] = Arrays.stream(animation) .map(i -> LoadSave.rotate(i, Direction.UP.angel)) .toArray(BufferedImage[]::new); deathFrames = Stream.concat( Arrays.stream(SpriteSheetManager.get(SpriteLocation.PACMAN).getAnimation(1)), Arrays.stream(SpriteSheetManager.get(SpriteLocation.PACMAN).getAnimation(2))) .toArray(BufferedImage[]::new); return new Sprites(spriteMap, deathFrames); } public void draw(Graphics g) { 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( sprites.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() { 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 < sprites.deathFrames.length - 1) { // FRAME_NS has passed and not all frames has been drawn deathFrameIdx++; lastChangeNs += FRAME_NS; // carry over exact cadence } } private void updateAlive() { if (state != PacmanState.ALIVE) return; if (moving) { MyPoint destination = collisionChecker.getValidDestination(direction, getNewPosition(), COLLISION_BOX_SIZE, COLLISION_BOX_SIZE); if (destination != null) { position = destination; } } } private MyPoint getNewPosition() { return new MyPoint(position.x + direction.dx * getSpeed(), position.y + direction.dy * getSpeed()); } public void startDeathAnimation() { state = PacmanState.DYING; deathFrameIdx = 0; lastChangeNs = System.nanoTime(); // reset stopwatch right now deathFrames = Arrays.stream(sprites.deathFrames) .map(img -> LoadSave.rotate(img, direction.angel)) .toArray(BufferedImage[]::new); } private double getSpeed() { return BASE_SPEED * levelManager.getPacmanLevelSpeed(); } public double distanceTo(Point point) { return new Point((int) position.x, (int) position.y).distance(point); } public void reset() { position = startPosition; aniIndex = 0; // reset animation to start state = PacmanState.ALIVE; deathFrameIdx = 0; } public Image getLifeIcon() { return sprites.spriteSheets[0][1]; } public Rectangle getBounds() { return new Rectangle( (int) (position.x - COLLISION_BOX_OFFSET), (int) (position.y - COLLISION_BOX_OFFSET), COLLISION_BOX_SIZE, COLLISION_BOX_SIZE); } public Point getPosition() { return new Point((int) position.x, (int) position.y); } public void setMoving(boolean b) { moving = b; paused = !b; } private enum PacmanState { ALIVE, DYING, DEAD } record Sprites(BufferedImage[][] spriteSheets, BufferedImage[] deathFrames) { } }