Refactor ghost behavior and improve mode transition logic
Replaced hardcoded coordinates with `Ghost.getHouseEntrance` for clarity and flexibility. Introduced `requestModeChange` to streamline state transitions. Updated collision and movement logic to handle ghost-specific tile interactions, enhancing gameplay accuracy.
This commit is contained in:
@ -1,9 +1,10 @@
|
||||
package se.urmo.game.collision;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import se.urmo.game.util.MyPoint;
|
||||
import se.urmo.game.entities.ghost.Ghost;
|
||||
import se.urmo.game.map.GameMap;
|
||||
import se.urmo.game.util.Direction;
|
||||
import se.urmo.game.util.MyPoint;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ -15,19 +16,16 @@ public class GhostCollisionChecker {
|
||||
this.map = map;
|
||||
}
|
||||
|
||||
public List<Direction> calculateDirectionAlternatives(MyPoint position) {
|
||||
List<Direction> intersection = map.directionAlternatives((int) position.x, (int) position.y);
|
||||
public List<Direction> calculateDirectionAlternatives(Ghost ghost, MyPoint position) {
|
||||
List<Direction> intersection = map.directionAlternatives(ghost, (int) position.x, (int) position.y);
|
||||
log.info("Possible travel directions: {}", intersection);
|
||||
return intersection;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public MyPoint canMoveTo(Direction dir, double x, double y) {
|
||||
public MyPoint canMoveTo(Ghost ghost, Direction dir, double x, double y) {
|
||||
// -1 is because else we endup in next tile
|
||||
//Point pp = new Point((int) (x + dir.dx * (GameMap.MAP_TILESIZE/2 - 1)), (int) (y + dir.dy * (GameMap.MAP_TILESIZE/2 -1)));
|
||||
|
||||
return ! map.isSolidXY(
|
||||
return !map.isSolidXY(ghost,
|
||||
(int) (x) + dir.dx * (GameMap.MAP_TILESIZE/2 - 1),
|
||||
(int) (y) + dir.dy * (GameMap.MAP_TILESIZE/2 - 1)) ? new MyPoint(x,y) : null;
|
||||
}
|
||||
|
||||
@ -58,6 +58,10 @@ public class Ghost extends BaseAnimated {
|
||||
private static final MyPoint startPosition = new MyPoint(
|
||||
GameMap.colToScreen(13) + ((double) GameMap.MAP_TILESIZE / 2),
|
||||
GameMap.rowToScreen(12) + ((double) GameMap.MAP_TILESIZE / 2));
|
||||
@Getter
|
||||
private static final MyPoint houseEntrance = new MyPoint(
|
||||
GameMap.colToScreen(13) + ((double) GameMap.MAP_TILESIZE / 2),
|
||||
GameMap.rowToScreen(10) + ((double) GameMap.MAP_TILESIZE / 2));
|
||||
|
||||
public Ghost(GhostCollisionChecker collisionChecker, GhostStrategy chaseStrategy, GhostStrategy scaterStrategy, int animation, LevelManager levelManager) {
|
||||
super(ANIMATION_UPDATE_FREQUENCY, GhostManager.MAX_SPRITE_FRAMES);
|
||||
@ -81,6 +85,9 @@ public class Ghost extends BaseAnimated {
|
||||
(int) position.y - GHOST_SIZE / 2,
|
||||
GHOST_SIZE,
|
||||
GHOST_SIZE, null);
|
||||
// g.setColor(Color.YELLOW);
|
||||
// g.fillRect((int) Ghost.startPosition.x, (int) Ghost.startPosition.y, 2, 2);
|
||||
// g.fillRect((int) Ghost.houseEntrance.x, (int) Ghost.houseEntrance.y, 2, 2);
|
||||
}
|
||||
|
||||
public void update(PacMan pacman, GameMap map) {
|
||||
@ -94,6 +101,15 @@ public class Ghost extends BaseAnimated {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by a state itself to request a transition to any other state
|
||||
* This bypasses the priority system since the state itself is requesting the change
|
||||
*/
|
||||
public void requestModeChange(GhostMode mode) {
|
||||
currentState = states.get(mode);
|
||||
}
|
||||
|
||||
|
||||
public boolean isFrightened() {
|
||||
return states.get(GhostMode.FRIGHTENED) == currentState;
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ import se.urmo.game.util.MyPoint;
|
||||
|
||||
import java.awt.Point;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
@ -61,7 +62,7 @@ public abstract class AbstractGhostModeImpl implements GhostState {
|
||||
ghost.setDirection(chooseDirection(
|
||||
ghost,
|
||||
prioritizeDirections(
|
||||
ghost.getCollisionChecker().calculateDirectionAlternatives(getPosition())),
|
||||
ghost.getCollisionChecker().calculateDirectionAlternatives(ghost, getPosition())),
|
||||
getStrategy().chooseTarget(ghost, pacman, map)));
|
||||
|
||||
log.debug("Ghost moving to {}", getPosition());
|
||||
@ -83,7 +84,7 @@ public abstract class AbstractGhostModeImpl implements GhostState {
|
||||
}
|
||||
|
||||
private void moveTo(Ghost ghost, MyPoint newPosition) {
|
||||
MyPoint destination = ghost.getCollisionChecker().canMoveTo(
|
||||
MyPoint destination = ghost.getCollisionChecker().canMoveTo(ghost,
|
||||
getDirection(), newPosition.x, newPosition.y);
|
||||
if (destination != null) {
|
||||
ghost.setPosition(destination);
|
||||
@ -113,23 +114,31 @@ public abstract class AbstractGhostModeImpl implements GhostState {
|
||||
.map(Map.Entry::getKey)
|
||||
.toList();
|
||||
|
||||
// Calculate the direction that has the lowest distance to the target
|
||||
Direction best = directions.getFirst();
|
||||
double bestDist = Double.MAX_VALUE;
|
||||
|
||||
MyPoint position = getPosition();
|
||||
for (Direction d : directions) {
|
||||
// Create a record to hold direction and distance
|
||||
record DirectionDistance(Direction direction, double distance) {
|
||||
}
|
||||
|
||||
// Stream through directions and find the one with the minimum distance to the target
|
||||
List<DirectionDistance> dd = directions.stream()
|
||||
.map(d -> {
|
||||
double nx = position.x + d.dx * GameMap.MAP_TILESIZE;
|
||||
double ny = position.y + d.dy * GameMap.MAP_TILESIZE;
|
||||
double dist = target.distance(nx, ny);
|
||||
if (dist < bestDist) {
|
||||
bestDist = dist;
|
||||
best = d;
|
||||
}
|
||||
}
|
||||
return new DirectionDistance(d, dist);
|
||||
})
|
||||
.toList();
|
||||
|
||||
log.debug("Ghost coming from {}, choosing {}, from {} (dist={})",
|
||||
ghost.getPrevDirection(), best, directions, bestDist);
|
||||
log.debug("Target: {}, Position: {}", target, getPosition());
|
||||
log.debug("Directions: {}", dd);
|
||||
|
||||
Direction best = dd.stream()
|
||||
.min(Comparator.comparingDouble(DirectionDistance::distance))
|
||||
.map(DirectionDistance::direction)
|
||||
.orElse(directions.getFirst()); // Fallback to first direction if stream is empty
|
||||
|
||||
log.debug("Ghost coming from {}, choosing {}, from {}",
|
||||
ghost.getPrevDirection(), best, directions);
|
||||
return best;
|
||||
}
|
||||
|
||||
|
||||
@ -18,25 +18,19 @@ public class EatenGhostMode extends AbstractGhostModeImpl {
|
||||
|
||||
public EatenGhostMode(Ghost ghost) {
|
||||
super(ghost, new EatenStrategy(), ghost.getLevelManager(), 9);
|
||||
// Eaten mode uses a specific strategy to return home
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(Ghost ghost, PacMan pacman, GameMap map) {
|
||||
// Check if ghost has reached its starting position
|
||||
if (getPosition().asPoint().distance(Ghost.getStartPosition().asPoint()) < 10) {
|
||||
if (getPosition().asPoint().distance(Ghost.getHouseEntrance().asPoint()) < 10) {
|
||||
log.debug("Ghost reached home, returning to chase mode");
|
||||
ghost.setMode(GhostMode.CHASE);
|
||||
ghost.requestModeChange(GhostMode.CHASE);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update position using eaten strategy
|
||||
updatePosition(ghost, pacman, map);
|
||||
}
|
||||
|
||||
// @Override
|
||||
// public void enter(Ghost ghost) {}
|
||||
|
||||
@Override
|
||||
public BufferedImage[] getAnimation() {
|
||||
return EATEN_ANIMATION;
|
||||
|
||||
@ -57,7 +57,7 @@ public class FrightenedGhostMode extends AbstractGhostModeImpl {
|
||||
// Check if frightened mode should end
|
||||
if (frightenedTimer <= 0) {
|
||||
log.debug("Frightened mode ended");
|
||||
ghost.setMode(GhostMode.CHASE);
|
||||
ghost.requestModeChange(GhostMode.CHASE);
|
||||
frightenedTimer = FRIGHTENED_DURATION_TICKS;
|
||||
isBlinking = false;
|
||||
}
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
package se.urmo.game.entities.ghost.mode;
|
||||
|
||||
public enum GhostMode {
|
||||
FRIGHTENED,
|
||||
EATEN,
|
||||
HOUSE,
|
||||
FROZEN,
|
||||
CHASE,
|
||||
SCATTER
|
||||
// Highest priority first
|
||||
EATEN, // 0: Ghost was eaten - highest priority
|
||||
FRIGHTENED,// 1: Ghost is frightened by power pellet
|
||||
FROZEN, // 2: Game is paused/frozen
|
||||
HOUSE, // 3: Ghost is in the house
|
||||
SCATTER, // 4: Ghost is scattering to corners
|
||||
CHASE // 5: Ghost is chasing Pacman - lowest priority
|
||||
|
||||
}
|
||||
|
||||
@ -6,8 +6,6 @@ import se.urmo.game.entities.ghost.strategy.HouseStrategy;
|
||||
import se.urmo.game.entities.pacman.PacMan;
|
||||
import se.urmo.game.map.GameMap;
|
||||
|
||||
import java.awt.Point;
|
||||
|
||||
@Slf4j
|
||||
public class HouseGhostMode extends AbstractGhostModeImpl {
|
||||
public HouseGhostMode(Ghost ghost) {
|
||||
@ -16,10 +14,9 @@ public class HouseGhostMode extends AbstractGhostModeImpl {
|
||||
|
||||
@Override
|
||||
public void update(Ghost ghost, PacMan pacman, GameMap map) {
|
||||
if (getPosition().asPoint().distance(new Point(232, 154)) < 1) {
|
||||
if (getPosition().asPoint().distance(Ghost.getHouseEntrance().asPoint()) < 15) {
|
||||
log.debug("Ghost left home, switching to chase mode");
|
||||
ghost.setMode(GhostMode.CHASE);
|
||||
return;
|
||||
ghost.requestModeChange(GhostMode.CHASE);
|
||||
}
|
||||
updatePosition(ghost, pacman, map);
|
||||
}
|
||||
|
||||
@ -34,7 +34,7 @@ public class ScatterGhostMode extends AbstractGhostModeImpl {
|
||||
scatterTimer--;
|
||||
if (scatterTimer <= 0) {
|
||||
log.debug("Scatter mode timed out, returning to chase");
|
||||
ghost.setMode(GhostMode.CHASE);
|
||||
ghost.requestModeChange(GhostMode.CHASE);
|
||||
scatterTimer = SCATTER_DURATION;
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,8 +9,6 @@ import java.awt.Point;
|
||||
public class EatenStrategy implements GhostStrategy {
|
||||
@Override
|
||||
public Point chooseTarget(Ghost ghost, PacMan pacman, GameMap map) {
|
||||
return new Point(
|
||||
13 * GameMap.MAP_TILESIZE + GameMap.OFFSET_X + (GameMap.MAP_TILESIZE / 2),
|
||||
4 * GameMap.MAP_TILESIZE + GameMap.OFFSET_Y + (GameMap.MAP_TILESIZE / 2));
|
||||
return Ghost.getHouseEntrance().asPoint();
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,7 +17,7 @@ public class FearStrategy implements GhostStrategy {
|
||||
// Frightened ghosts do not target Pacman.
|
||||
// Instead, they pick a random adjacent valid tile.
|
||||
Point ghostPos = ghost.getPosition().asPoint();
|
||||
List<Direction> neighbors = map.directionAlternatives(ghostPos.x, ghostPos.y);
|
||||
List<Direction> neighbors = map.directionAlternatives(ghost, ghostPos.x, ghostPos.y);
|
||||
|
||||
if (neighbors.isEmpty()) {
|
||||
return ghost.getPosition().asPoint(); // stuck
|
||||
|
||||
@ -9,6 +9,6 @@ import java.awt.Point;
|
||||
public class HouseStrategy implements GhostStrategy {
|
||||
@Override
|
||||
public Point chooseTarget(Ghost ghost, PacMan pacman, GameMap map) {
|
||||
return new Point(232, 154);
|
||||
return new Point(232, 280);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package se.urmo.game.map;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import se.urmo.game.entities.BaseAnimated;
|
||||
import se.urmo.game.entities.ghost.Ghost;
|
||||
import se.urmo.game.util.Direction;
|
||||
|
||||
import java.awt.Graphics;
|
||||
@ -114,18 +116,35 @@ public class GameMap {
|
||||
return b;
|
||||
}
|
||||
|
||||
public boolean isSolidXY(int screenX, int screenY) {
|
||||
return isSolid(screenToRow(screenY), screenToCol(screenX));
|
||||
public boolean isSolidXY(Ghost ghost, int screenX, int screenY) {
|
||||
return isSolid(ghost, screenToRow(screenY), screenToCol(screenX));
|
||||
}
|
||||
|
||||
|
||||
public boolean isSolid(int row, int col) {
|
||||
public boolean isSolid(BaseAnimated entity, int row, int col) {
|
||||
// Check for out of bounds
|
||||
if (col >= columns() || col < 0) return true;
|
||||
if (row >= rows() || row < 0) return true;
|
||||
|
||||
// Get the tile information
|
||||
MapTile mapTile = mapData[row][col];
|
||||
boolean solid = mapTile.isSolid();
|
||||
|
||||
// Special case: If the entity is a Ghost, it can pass through certain solid tiles
|
||||
// This allows ghosts to move through areas that would be solid for other entities
|
||||
if (entity instanceof Ghost) {
|
||||
// For Ghost entities, check if this is a ghost-passable tile
|
||||
// You might want to refine this logic based on specific tile types or ghost states
|
||||
|
||||
// Example: Allow ghosts to pass through ghost house door
|
||||
if (mapTile.getTileType() == TileType.DOOR) {
|
||||
return false; // Not solid for ghosts
|
||||
}
|
||||
}
|
||||
|
||||
//log.debug("[{}][{}] {}", row, col, mapTile.getTileType());
|
||||
return solid;
|
||||
|
||||
}
|
||||
|
||||
public boolean removeTileImage(Point screen) {
|
||||
@ -220,13 +239,13 @@ public class GameMap {
|
||||
|
||||
}
|
||||
|
||||
public List<Direction> directionAlternatives(int screenX, int screenY) {
|
||||
public List<Direction> directionAlternatives(BaseAnimated entity, int screenX, int screenY) {
|
||||
int row = (screenY - GameMap.OFFSET_Y) / GameMap.MAP_TILESIZE;
|
||||
int col = (screenX - GameMap.OFFSET_X) / GameMap.MAP_TILESIZE;
|
||||
|
||||
record DirectionCheck(int rowOffset, int colOffset, Direction direction) {
|
||||
}
|
||||
log.debug("At [{}][{}]", row, col);
|
||||
log.debug("At ({},{}), [{}][{}]", screenX, screenY, row, col);
|
||||
return Stream.of(
|
||||
new DirectionCheck(0, 1, Direction.RIGHT),
|
||||
new DirectionCheck(0, -1, Direction.LEFT),
|
||||
@ -236,7 +255,7 @@ public class GameMap {
|
||||
.filter(dc -> {
|
||||
int r = row + dc.rowOffset;
|
||||
int c = col + dc.colOffset;
|
||||
boolean solid = isSolid(r, c);
|
||||
boolean solid = isSolid(entity, r, c);
|
||||
log.debug("[{}][{}] {} is {}", r, c, dc.direction, solid ? "solid" : " not solid");
|
||||
return !solid;
|
||||
})
|
||||
|
||||
@ -43,7 +43,7 @@ public enum TileType {
|
||||
TILE_35(35, true, SpriteLocation.MAP, 3, 1, false ,0),
|
||||
TILE_36(36, true, SpriteLocation.MAP, 3, 2, false ,0),
|
||||
TILE_37(37, true, SpriteLocation.MAP, 3, 3, false ,0),
|
||||
TILE_38(38, true, SpriteLocation.MAP, 3, 4, false ,0),
|
||||
DOOR(38, true, SpriteLocation.MAP, 3, 4, false, 0),
|
||||
TILE_39(39, true, SpriteLocation.MAP, 3, 5, false ,0),
|
||||
TILE_40(40, true, SpriteLocation.MAP, 3, 6, false ,0),
|
||||
TILE_41(41, true, SpriteLocation.MAP, 3, 7, false ,0),
|
||||
|
||||
Reference in New Issue
Block a user