diff --git a/src/main/java/se/urmo/game/collision/GhostCollisionChecker.java b/src/main/java/se/urmo/game/collision/GhostCollisionChecker.java index 849dcea..7743e1b 100644 --- a/src/main/java/se/urmo/game/collision/GhostCollisionChecker.java +++ b/src/main/java/se/urmo/game/collision/GhostCollisionChecker.java @@ -18,33 +18,12 @@ public class GhostCollisionChecker { } public List calculateDirectionAlternatives(Point position) { - List intersection = directionAlternatives(position); + List intersection = map.directionAlternatives(position); log.info("Possible travel directions: {}", intersection); return intersection; } - public List directionAlternatives(Point position) { - int row = (position.y - GameMap.OFFSET_Y) / GameMap.MAP_TILESIZE; - int col = (position.x - GameMap.OFFSET_X) / GameMap.MAP_TILESIZE; - record DirectionCheck(int rowOffset, int colOffset, Direction direction) {} - log.debug("At [{}][{}]", row, col); - return Stream.of( - new DirectionCheck(0, 1, Direction.RIGHT), - new DirectionCheck(0, -1, Direction.LEFT), - new DirectionCheck(1, 0, Direction.DOWN), - new DirectionCheck(-1, 0, Direction.UP) - ) - .filter(dc -> { - int r = row + dc.rowOffset; - int c = col + dc.colOffset; - boolean solid = map.isSolid(r, c); - log.debug("[{}][{}] {} is {}", r, c, dc.direction, solid ? "solid" : " not solid"); - return !solid; - }) - .map(DirectionCheck::direction) - .toList(); - } public Point canMoveTo(Direction dir, Point pos) { // -1 is because else we endup in next tile diff --git a/src/main/java/se/urmo/game/entities/FearStrategy.java b/src/main/java/se/urmo/game/entities/FearStrategy.java new file mode 100644 index 0000000..8747a86 --- /dev/null +++ b/src/main/java/se/urmo/game/entities/FearStrategy.java @@ -0,0 +1,32 @@ +package se.urmo.game.entities; + +import se.urmo.game.map.GameMap; +import se.urmo.game.util.Direction; + +import java.awt.Point; +import java.util.List; +import java.util.Random; + +public class FearStrategy implements GhostStrategy { + private final Random random = new Random(); + + @Override + public Point chooseTarget(Ghost ghost, PacMan pacman, GameMap map) { + // Frightened ghosts do not target Pacman. + // Instead, they pick a random adjacent valid tile. + List neighbors = map.directionAlternatives(ghost.getPosition()); + + if (neighbors.isEmpty()) { + return ghost.getPosition(); // stuck + } + + //Transform directions to actual Points + List potentialTargets = neighbors.stream() + .map(d -> new Point( + ghost.getPosition().x + d.dx * GameMap.MAP_TILESIZE, + ghost.getPosition().y + d.dy * GameMap.MAP_TILESIZE)).toList(); + + // Pick a random valid neighbor + return potentialTargets.get(random.nextInt(neighbors.size())); + } +} diff --git a/src/main/java/se/urmo/game/entities/Ghost.java b/src/main/java/se/urmo/game/entities/Ghost.java index 752d202..4e62ca1 100644 --- a/src/main/java/se/urmo/game/entities/Ghost.java +++ b/src/main/java/se/urmo/game/entities/Ghost.java @@ -3,6 +3,7 @@ package se.urmo.game.entities; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import se.urmo.game.collision.GhostCollisionChecker; +import se.urmo.game.main.Game; import se.urmo.game.map.GameMap; import se.urmo.game.state.GhostManager; import se.urmo.game.util.Direction; @@ -18,16 +19,20 @@ import java.util.stream.Collectors; @Slf4j public class Ghost { + private static final int WARNING_THRESHOLD = 180; // 3 seconds of warning private static final int COLLISION_BOX_SIZE = 16; private static final int GHOST_MOVEMENT_UPDATE_FREQUENCY = 2; public static final int GHOST_SIZE = 32; private static final int ANIMATION_UPDATE_FREQUENCY = 25; private static final int COLLISION_BOX_OFFSET = COLLISION_BOX_SIZE / 2; private static final BufferedImage COLLISION_BOX = MiscUtil.createOutlinedBox(COLLISION_BOX_SIZE, COLLISION_BOX_SIZE, Color.black, 2); + private static final int FRIGHTENED_DURATION_TICKS = 10 * Game.UPS_SET; private final GhostCollisionChecker collisionChecker; private final GhostStrategy chaseStrategy; private final Point startPos; + private final BufferedImage[] fearAnimation; + private final BufferedImage[] baseAnimation; @Getter private Point position; @@ -36,23 +41,29 @@ public class Ghost { private int aniIndex = 0; private final GhostStrategy scaterStrategy; private GhostStrategy currentStrategy; - private final BufferedImage[] animation; + private BufferedImage[] animation; private int movementTick = 0; private Direction direction; private Direction prevDirection; private GhostMode mode; + private final GhostStrategy fearStrategy = new FearStrategy(); + private int frightenedTimer = 0; + private boolean isBlinking = false; - public Ghost(GhostCollisionChecker collisionChecker, GhostStrategy strategy, GhostStrategy scaterStrategy, BufferedImage[] animation) { + + public Ghost(GhostCollisionChecker collisionChecker, GhostStrategy strategy, GhostStrategy scaterStrategy, BufferedImage[] animation, BufferedImage[] fearAnimation) { this.collisionChecker = collisionChecker; this.chaseStrategy = strategy; this.scaterStrategy = scaterStrategy; - this.animation = animation; + this.baseAnimation = animation; + this.fearAnimation = fearAnimation; position = new Point( 13 * GameMap.MAP_TILESIZE + GameMap.OFFSET_X + (GameMap.MAP_TILESIZE / 2), 4 * GameMap.MAP_TILESIZE + GameMap.OFFSET_Y + (GameMap.MAP_TILESIZE / 2) ); startPos = position; this.currentStrategy = chaseStrategy; + this.animation = baseAnimation; } public void draw(Graphics g) { @@ -71,17 +82,36 @@ public class Ghost { public void update(PacMan pacman, GameMap map) { updateAnimationTick(); + if(mode == GhostMode.FRIGHTENED){ + updateInFrightendMode(); + } updatePosition(pacman, map); } + private void updateInFrightendMode() { + frightenedTimer--; + if(frightenedTimer <= WARNING_THRESHOLD){ + isBlinking = (frightenedTimer / 25) % 2 == 0; + animation = isBlinking ? fearAnimation : baseAnimation; + } + if(frightenedTimer <= 0){ + setMode(GhostMode.CHASE); + } + } + private void updatePosition(PacMan pacman, GameMap map) { // only move ghost on update interval - this is basically ghost speed; if (movementTick >= GHOST_MOVEMENT_UPDATE_FREQUENCY) { - chooseDirection(pacman, map); + if (map.isAligned(position)) { + //log.info("Evaluating possible directions"); + prevDirection = direction; + direction = chooseDirection( + prioritize(collisionChecker.calculateDirectionAlternatives(position)), + currentStrategy.chooseTarget(this, pacman, map)); + //log.info("selecting direction {}", direction); + } - Point newPosition = getNewPosition(); - - move(newPosition); + moveTo(getNewPosition()); movementTick = 0; } else movementTick++; @@ -89,6 +119,7 @@ public class Ghost { /** * Given a position and a direction - calculate the new position + * Moves one pixel in the given direction * * @return new position */ @@ -101,25 +132,7 @@ public class Ghost { } - /** - * Choose a new direction when 'aligned' ie when in the exact middle of a tile - * else continue with the existing direction. - * - * @param pacman - * @param map - */ - private void chooseDirection(PacMan pacman, GameMap map) { - if (isAlligned(position)) { - log.info("Evaluating possible directions"); - prevDirection = direction; - direction = chooseDirection( - prioritize(collisionChecker.calculateDirectionAlternatives(position)), - currentStrategy.chooseTarget(this, pacman, map)); - log.info("selecting direction {}", direction); - } - } - - private void move(Point newPosition) { + private void moveTo(Point newPosition) { Point destination = collisionChecker.canMoveTo(direction, newPosition); if (destination != null) { @@ -137,14 +150,7 @@ public class Ghost { } - private boolean isAlligned(Point pos) { - int row = pos.x % GameMap.MAP_TILESIZE; - int col = pos.y % GameMap.MAP_TILESIZE; - return row == GameMap.MAP_TILESIZE / 2 && col == GameMap.MAP_TILESIZE / 2; - } - private Direction chooseDirection(Map options, Point target) { - // Find the lowest priority int lowestPriority = options.values().stream() .mapToInt(Integer::intValue) @@ -189,15 +195,23 @@ public class Ghost { public void setMode(GhostMode mode) { this.mode = mode; switch (mode) { - case CHASE -> currentStrategy = chaseStrategy; + case CHASE -> { + animation = baseAnimation; + currentStrategy = chaseStrategy; + } case SCATTER -> currentStrategy = scaterStrategy; - case FRIGHTENED -> currentStrategy = null; + case FRIGHTENED -> { + frightenedTimer = FRIGHTENED_DURATION_TICKS; + isBlinking = false; + animation = fearAnimation; + currentStrategy = fearStrategy; + } case EATEN -> currentStrategy = null; } } public boolean isFrightened() { - return false; + return mode == GhostMode.FRIGHTENED; } public void resetPosition() { diff --git a/src/main/java/se/urmo/game/map/GameMap.java b/src/main/java/se/urmo/game/map/GameMap.java index 3f09fe5..cd5810d 100644 --- a/src/main/java/se/urmo/game/map/GameMap.java +++ b/src/main/java/se/urmo/game/map/GameMap.java @@ -1,12 +1,14 @@ package se.urmo.game.map; import lombok.extern.slf4j.Slf4j; +import se.urmo.game.util.Direction; import java.awt.*; import java.awt.image.BufferedImage; import java.io.*; import java.util.List; import java.util.Objects; +import java.util.stream.Stream; @Slf4j public class GameMap { @@ -194,4 +196,33 @@ public class GameMap { return mapData[r][c]; } + + public List directionAlternatives(Point screen) { + int row = (screen.y - GameMap.OFFSET_Y) / GameMap.MAP_TILESIZE; + int col = (screen.x - GameMap.OFFSET_X) / GameMap.MAP_TILESIZE; + + record DirectionCheck(int rowOffset, int colOffset, Direction direction) {} + log.debug("At [{}][{}]", row, col); + return Stream.of( + new DirectionCheck(0, 1, Direction.RIGHT), + new DirectionCheck(0, -1, Direction.LEFT), + new DirectionCheck(1, 0, Direction.DOWN), + new DirectionCheck(-1, 0, Direction.UP) + ) + .filter(dc -> { + int r = row + dc.rowOffset; + int c = col + dc.colOffset; + boolean solid = isSolid(r, c); + log.debug("[{}][{}] {} is {}", r, c, dc.direction, solid ? "solid" : " not solid"); + return !solid; + }) + .map(DirectionCheck::direction) + .toList(); + } + + public boolean isAligned(Point pos) { + int row = pos.x % GameMap.MAP_TILESIZE; + int col = pos.y % GameMap.MAP_TILESIZE; + return row == GameMap.MAP_TILESIZE / 2 && col == GameMap.MAP_TILESIZE / 2; + } } diff --git a/src/main/java/se/urmo/game/state/GhostManager.java b/src/main/java/se/urmo/game/state/GhostManager.java index 8026664..600dfdf 100644 --- a/src/main/java/se/urmo/game/state/GhostManager.java +++ b/src/main/java/se/urmo/game/state/GhostManager.java @@ -46,11 +46,11 @@ public class GhostManager { loadAnimation(); // Create ghosts with their strategies - Ghost blinky = new Ghost(ghostCollisionChecker, new BlinkyStrategy(), new ScatterToTopRight(), image[0]); + Ghost blinky = new Ghost(ghostCollisionChecker, new BlinkyStrategy(), new ScatterToTopRight(), image[0], image[9]); ghosts.add(blinky); - ghosts.add(new Ghost(ghostCollisionChecker, new PinkyStrategy(),new ScatterToTopLeft(), image[2])); - ghosts.add(new Ghost(ghostCollisionChecker,new InkyStrategy(blinky), new ScatterToBottomRight(), image[1])); - ghosts.add(new Ghost(ghostCollisionChecker, new ClydeStrategy(), new ScatterToBottomLeft(), image[3])); + //ghosts.add(new Ghost(ghostCollisionChecker, new PinkyStrategy(),new ScatterToTopLeft(), image[2], image[9])); + //ghosts.add(new Ghost(ghostCollisionChecker,new InkyStrategy(blinky), new ScatterToBottomRight(), image[1], image[9])); + //ghosts.add(new Ghost(ghostCollisionChecker, new ClydeStrategy(), new ScatterToBottomLeft(), image[3], image[8])); setMode(GhostMode.CHASE); } @@ -67,7 +67,7 @@ public class GhostManager { public void setMode(GhostMode mode) { this.globalMode = mode; - log.info("Mode changed to {}", globalMode); + log.debug("Mode changed to {}", globalMode); for (Ghost g : ghosts) { g.setMode(mode); } @@ -97,4 +97,8 @@ public class GhostManager { ghost.draw(g); } } + + public void setFrightMode() { + this.setMode(GhostMode.FRIGHTENED); + } } diff --git a/src/main/java/se/urmo/game/state/PlayingState.java b/src/main/java/se/urmo/game/state/PlayingState.java index e099023..331abf3 100644 --- a/src/main/java/se/urmo/game/state/PlayingState.java +++ b/src/main/java/se/urmo/game/state/PlayingState.java @@ -5,10 +5,12 @@ import lombok.extern.slf4j.Slf4j; import se.urmo.game.collision.CollisionChecker; import se.urmo.game.collision.GhostCollisionChecker; import se.urmo.game.entities.Ghost; +import se.urmo.game.entities.GhostMode; import se.urmo.game.entities.PacMan; import se.urmo.game.main.Game; 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 java.awt.*; @@ -48,7 +50,9 @@ public class PlayingState implements GameState { Point pacmanScreenPos = pacman.getPosition(); MapTile tile = map.getTile(pacmanScreenPos); boolean wasRemoved = map.removeTileImage(pacmanScreenPos); - + if(wasRemoved && tile.getTileType() == TileType.LARGE_PELLET){ + ghostManager.setFrightMode(); + } if(wasRemoved){ score+=tile.getTileType().getScore(); } @@ -107,6 +111,7 @@ public class PlayingState implements GameState { // Pac-Man eats ghost score += 200; ghost.resetPosition(); + ghost.setMode(GhostMode.CHASE); // end frightend } else { // Pac-Man loses a life lives--;