Compare commits

..

30 Commits

Author SHA1 Message Date
5b2626ad5d Added last pellets 2025-09-07 17:28:47 +02:00
c65787dfc9 Implemented sound - first rev. 2025-09-07 17:26:09 +02:00
fd85d8cd0f Added TUNNEL TileType 2025-09-07 12:15:29 +02:00
32e884ec36 minor fix 2025-09-06 21:57:25 +02:00
5f653d3252 ScorePopup working 2025-09-06 21:53:18 +02:00
4b262362be Fixed FrightenedGhostMode blinking 2025-09-06 21:18:55 +02:00
c696e666d0 Added highscore 2025-09-06 20:55:57 +02:00
ec89422e81 Changed ghost/packman-collision 2025-09-06 20:42:33 +02:00
a8dc81984f Fixed so that ghosts dont accidently go back in house 2025-09-06 20:30:12 +02:00
8865a196f8 Added logging 2025-09-06 20:26:56 +02:00
80f7897da6 Fixed issue with freezing ghosts 2025-09-06 20:16:13 +02:00
c1c998c1cd 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.
2025-09-06 18:55:25 +02:00
291f929064 Refactor Ghost mode handling to prioritize mode transitions
Reordered GhostMode enum for logical priority and updated `setMode` to enforce transitions only to higher-priority modes. Added `currentMode` method for determining the Ghost's active mode, ensuring cleaner and safer state management.
2025-09-06 00:42:40 +02:00
e299a4173e Added HOUSE mode 2025-09-06 00:12:08 +02:00
d551b464b1 Moved mode-related in to package 2025-09-05 23:25:09 +02:00
5f118a75f6 Ghost refactoring 2025-09-05 23:22:45 +02:00
ebbc82b70e Minor fixes 2025-09-04 23:12:39 +02:00
c475d3cb02 Minor fixes 2025-09-03 21:10:34 +02:00
6dabc63892 Refactor Pacman state handling to use setState method.
Centralized Pacman state transitions into a new `setState` method to improve clarity and consistency. Removed redundant `reset` and `startDeathAnimation` methods, merging their logic into state management. Adjusted game state transitions to align with the new approach.
2025-09-03 20:58:57 +02:00
2f9106c1c9 Minor change 2025-09-03 20:32:33 +02:00
b317581e9c Add "FROZEN" mode to Ghost behavior and update logic
Introduced a new "FROZEN" mode to the Ghost state machine, ensuring proper handling in ghost behaviors and animations. Updated `GhostManager` and `Ghost` classes to integrate this mode, including relevant logic for updates and animations. Simplified and refined ghost initialization and strategy handling for better code maintainability.
2025-09-03 20:25:54 +02:00
11a550e997 Implement ghost "eaten" behavior and refine fright mechanics
Added a new "eaten" mode for ghosts, including animations, movement strategy, and logic to reset ghosts after being eaten. Adjusted scoring for frightened ghosts using a fright multiplier and introduced enhancements like streamlined direction prioritization and position alignment. Also temporarily disabled non-essential ghosts for testing purposes.
2025-09-03 12:53:26 +02:00
d4b980f522 Moved around 2025-09-03 00:42:48 +02:00
9c0a613e4d Minor changes 2025-09-03 00:38:28 +02:00
c05398201f Minot pacman-changes 2025-09-03 00:27:44 +02:00
4638484b97 Refactor CollisionChecker for cleaner boundary handling
Streamlined collision logic by introducing helper methods for boundary calculations and movement validation. Replaced commented-out code with efficient implementations, improving readability and maintainability. Added clear documentation for new methods to better convey their purpose and usage.
2025-09-03 00:18:27 +02:00
328098bbe7 Refactor PacMan to utilize SpriteSheetManager.
Replaced hardcoded sprite loading logic with centralized SpriteSheetManager to improve maintainability and reduce duplication. Adjusted related methods to use the new Sprites record for streamlined sprite management.
2025-09-02 21:47:50 +02:00
5ba16402e4 Deathanimation working 2025-09-02 21:30:08 +02:00
96c89b6598 Inlcuded deathanimation in spritesheet 2025-09-02 12:26:18 +02:00
2d5c4c18f5 HighScoreManager 2025-09-01 21:53:10 +02:00
72 changed files with 1150 additions and 444 deletions

View File

@ -2,105 +2,93 @@ package se.urmo.game.collision;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import se.urmo.game.util.MyPoint;
import se.urmo.game.main.GamePanel; import se.urmo.game.main.GamePanel;
import se.urmo.game.util.Direction;
import se.urmo.game.map.GameMap; import se.urmo.game.map.GameMap;
import se.urmo.game.util.Direction;
import se.urmo.game.util.MyPoint;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@Slf4j @Slf4j
public class CollisionChecker { public class CollisionChecker {
private GameMap map; private final GameMap map;
public CollisionChecker(GameMap map) { public CollisionChecker(GameMap map) {
this.map = map; this.map = map;
} }
// public Point getValidDestination(Direction direction, Point position, int agent_width, int agent_height) { public MyPoint getValidDestination(Direction direction, MyPoint position, int agentWidth, int agentHeight) {
// List<Point> boundaries = switch (direction) { List<MyPoint> boundaries = getBoundariesForDirection(direction, position, agentWidth / 2, agentHeight / 2);
// case NONE -> Collections.emptyList();
// case RIGHT, LEFT -> List.of(
// new Point(position.x + (direction.dx * agent_width/2), position.y - agent_height/2),
// new Point(position.x + (direction.dx * agent_width/2), position.y + agent_height/2)
// );
// case UP, DOWN -> List.of(
// new Point(position.x - agent_width/2, position.y + (direction.dy * agent_height/2)),
// new Point(position.x + agent_width/2, position.y + (direction.dy * agent_height/2))
// );
// };
//
// List<Pair> bs = boundaries.stream().map(p -> new Pair(p.x, p.y, GameMap.screenToRow(p.y), GameMap.screenToCol(p.x))).toList();
// log.debug("{} boundaries for {} are {}", direction, position, bs);
//
// List<Point> normalized = boundaries.stream()
// .map(p -> normalizePosition(direction, p, agent_width, agent_height))
// .toList();
//
// if (map.isPassable(normalized)) {
// return normalizePosition(direction, position, agent_width, agent_height);
// }
// return null; // Blocked
// }
// /** return canMoveInDirection(agentWidth, boundaries)
// * Applies specific rules to movement ? normalizePosition(position, agentWidth)
// * This, for instance, makes sure the tunnel left/right works. : null; // Blocked
// * }
// * @param dir
// * @param pos
// * @param agent_width
// * @param agent_height
// * @return
// */
// public Point normalizePosition(Direction dir, Point pos, int agent_width, int agent_height) {
// int x = pos.x;
// int y = pos.y;
// int width = GamePanel.SCREEN_WIDTH;
// int height = GamePanel.SCREEN_HEIGHT;
//
// // tunnel
// if (x < GameMap.OFFSET_X) x = width - agent_width/2 - GameMap.OFFSET_X; // right
// if (x >= (width - GameMap.OFFSET_X)) x = GameMap.OFFSET_X; // left
//
// return new Point(x, y);
// }
public MyPoint getValidDestination(Direction direction, MyPoint position, int agent_width, int agent_height) { /**
List<MyPoint> boundaries = switch (direction) { * Determines whether boundaries (corners) are in a passible i.e., not blocked, position
*
* @param agentWidth The width of the entity attempting to move.
* @param boundaries The boundary points representing the edges of the area occupied by the entity.
* @return {@code true} if movement in the specified direction is possible; {@code false} otherwise.
*/
private boolean canMoveInDirection(int agentWidth, List<MyPoint> boundaries) {
return boundaries.stream()
.map(boundary -> normalizePosition(boundary, agentWidth))
.allMatch(myPoint -> map.isPassable((int) myPoint.x, (int) myPoint.y));
}
/**
* Calculates the boundary points of a given position based on a specified direction
* and horizontal and vertical offsets. The resulting boundaries represent the edges
* of the area occupied by the entity moving in the specified direction.
*
* @param direction The direction of movement (e.g., UP, DOWN, LEFT, RIGHT, or NONE).
* @param position The current position represented as a {@code MyPoint}.
* @param horizontalOffset The horizontal offset to determine the boundary width.
* @param verticalOffset The vertical offset to determine the boundary height.
* @return A list of boundary points in the form of {@code MyPoint} objects. If the
* direction is {@code NONE}, an empty list is returned.
*/
private static List<MyPoint> getBoundariesForDirection(Direction direction, MyPoint position, int horizontalOffset, int verticalOffset) {
return switch (direction) {
case NONE -> Collections.emptyList(); case NONE -> Collections.emptyList();
case RIGHT, LEFT -> List.of( case RIGHT, LEFT -> List.of(
new MyPoint(position.x + (direction.dx * agent_width/2), position.y - agent_height/2), new MyPoint(position.x + ((double) (direction.dx * horizontalOffset) / 2), position.y - (double) verticalOffset / 2),
new MyPoint(position.x + (direction.dx * agent_width/2), position.y + agent_height/2) new MyPoint(position.x + ((double) (direction.dx * horizontalOffset) / 2), position.y + (double) verticalOffset / 2)
); );
case UP, DOWN -> List.of( case UP, DOWN -> List.of(
new MyPoint(position.x - agent_width/2, position.y + (direction.dy * agent_height/2)), new MyPoint(position.x - (double) horizontalOffset / 2, position.y + ((double) (direction.dy * verticalOffset) / 2)),
new MyPoint(position.x + agent_width/2, position.y + (direction.dy * agent_height/2)) new MyPoint(position.x + (double) horizontalOffset / 2, position.y + ((double) (direction.dy * verticalOffset) / 2))
); );
}; };
}
List<MyPoint> normalized = boundaries.stream() /**
.map(p -> normalizePosition(direction, p.x, p.y, agent_width, agent_height)) * Normalizes the position of an agent when it reaches screen boundaries,
.toList(); * implementing tunnel-like behavior when crossing horizontal boundaries.
*
boolean passable = normalized.stream().allMatch(myPoint -> map.isPassable((int) myPoint.x, (int) myPoint.y)); * @param position The current position to normalize
if (passable) { * @param agentWidth The width of the agent
return normalizePosition(direction, position.x, position.y, agent_width, agent_height); * @return Normalized position as MyPoint
*/
private MyPoint normalizePosition(MyPoint position, int agentWidth) {
if (isLeftBoundary(position.x)) {
return new MyPoint(GamePanel.SCREEN_WIDTH - (double) agentWidth / 2 - GameMap.OFFSET_X, position.y);
} }
return null; // Blocked if (isRightBoundary(position.x)) {
return new MyPoint(GameMap.OFFSET_X, position.y);
}
return position;
} }
private MyPoint normalizePosition(Direction direction, double x, double y, int agent_width, int agent_height) { private boolean isLeftBoundary(double x) {
double x1 = x; return x < GameMap.OFFSET_X;
double y1 = y;
int width = GamePanel.SCREEN_WIDTH;
int height = GamePanel.SCREEN_HEIGHT;
// tunnel
if (x < GameMap.OFFSET_X) x1 = width - agent_width/2 - GameMap.OFFSET_X; // right
if (x>= (width - GameMap.OFFSET_X)) x1 = GameMap.OFFSET_X; // left
return new MyPoint(x1, y1);
} }
private boolean isRightBoundary(double x) {
return x >= (GamePanel.SCREEN_WIDTH - GameMap.OFFSET_X);
}
} }

View File

@ -1,9 +1,10 @@
package se.urmo.game.collision; package se.urmo.game.collision;
import lombok.extern.slf4j.Slf4j; 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.map.GameMap;
import se.urmo.game.util.Direction; import se.urmo.game.util.Direction;
import se.urmo.game.util.MyPoint;
import java.util.List; import java.util.List;
@ -15,19 +16,16 @@ public class GhostCollisionChecker {
this.map = map; this.map = map;
} }
public List<Direction> calculateDirectionAlternatives(MyPoint position) { public List<Direction> calculateDirectionAlternatives(Ghost ghost, MyPoint position) {
List<Direction> intersection = map.directionAlternatives((int) position.x, (int) position.y); List<Direction> intersection = map.directionAlternatives(ghost, (int) position.x, (int) position.y);
log.info("Possible travel directions: {}", intersection); log.info("Possible travel directions: {}", intersection);
return intersection; return intersection;
} }
public MyPoint canMoveTo(Ghost ghost, Direction dir, double x, double y) {
public MyPoint canMoveTo(Direction dir, double x, double y) {
// -1 is because else we endup in next tile // -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(ghost,
return ! map.isSolidXY(
(int) (x) + dir.dx * (GameMap.MAP_TILESIZE/2 - 1), (int) (x) + dir.dx * (GameMap.MAP_TILESIZE/2 - 1),
(int) (y) + dir.dy * (GameMap.MAP_TILESIZE/2 - 1)) ? new MyPoint(x,y) : null; (int) (y) + dir.dy * (GameMap.MAP_TILESIZE/2 - 1)) ? new MyPoint(x,y) : null;
} }

View File

@ -0,0 +1,62 @@
package se.urmo.game.entities;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
public final class ScorePopup {
private final double startX, startY; // world px (not screen)
private final String text;
private final long startNs;
private final long lifeNs; // e.g. 1_000_000_000L (1s)
// simple motion params
private final double risePixels; // e.g. 16 px total rise
public ScorePopup(double x, double y, String text, long lifeNs, double risePixels) {
this.startX = x;
this.startY = y;
this.text = text;
this.lifeNs = lifeNs;
this.risePixels = risePixels;
this.startNs = System.nanoTime();
}
public boolean isAlive() {
return (System.nanoTime() - startNs) < lifeNs;
}
public void draw(Graphics2D g, int offsetX, int offsetY, Font font) {
long dt = System.nanoTime() - startNs;
double t = Math.min(1.0, dt / (double) lifeNs); // 0..1
// motion: ease-out upward (quadratic)
double y = startY - (risePixels * (1 - (1 - t) * (1 - t)));
// fade: alpha 1 → 0
float alpha = (float) (1.0 - t);
var oldComp = g.getComposite();
var oldFont = g.getFont();
g.setFont(font);
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
// draw centered on tile center
var fm = g.getFontMetrics();
int sx = offsetX + (int) Math.round(startX);
int sy = offsetY + (int) Math.round(y);
int x = sx - fm.stringWidth(text) / 2;
int baseline = sy; // tune if you want it a tad above the exact point
// g.setColor(Color.WHITE);
g.setColor(Color.BLACK);
g.drawString(text, x + 1, baseline + 1);
g.setColor(Color.WHITE);
g.drawString(text, x, baseline);
g.drawString(text, x, baseline);
g.setComposite(oldComp);
g.setFont(oldFont);
}
}

View File

@ -4,7 +4,7 @@ import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import se.urmo.game.entities.pacman.PacMan; import se.urmo.game.entities.pacman.PacMan;
import se.urmo.game.map.GameMap; import se.urmo.game.map.GameMap;
import se.urmo.game.state.FruitType; import se.urmo.game.util.FruitType;
import java.awt.Graphics; import java.awt.Graphics;
import java.awt.Point; import java.awt.Point;

View File

@ -1,226 +1,139 @@
package se.urmo.game.entities.ghost; package se.urmo.game.entities.ghost;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import se.urmo.game.collision.GhostCollisionChecker; import se.urmo.game.collision.GhostCollisionChecker;
import se.urmo.game.entities.BaseAnimated; import se.urmo.game.entities.BaseAnimated;
import se.urmo.game.entities.ghost.strategy.FearStrategy; import se.urmo.game.entities.ghost.mode.ChaseGhostMode;
import se.urmo.game.entities.ghost.mode.EatenGhostMode;
import se.urmo.game.entities.ghost.mode.FrightenedGhostMode;
import se.urmo.game.entities.ghost.mode.FrozenGhostMode;
import se.urmo.game.entities.ghost.mode.GhostMode;
import se.urmo.game.entities.ghost.mode.GhostState;
import se.urmo.game.entities.ghost.mode.HouseGhostMode;
import se.urmo.game.entities.ghost.mode.ScatterGhostMode;
import se.urmo.game.entities.ghost.strategy.GhostStrategy; import se.urmo.game.entities.ghost.strategy.GhostStrategy;
import se.urmo.game.entities.pacman.PacMan; import se.urmo.game.entities.pacman.PacMan;
import se.urmo.game.graphics.SpriteLocation; import se.urmo.game.main.GhostManager;
import se.urmo.game.graphics.SpriteSheetManager; import se.urmo.game.main.LevelManager;
import se.urmo.game.main.Game;
import se.urmo.game.map.GameMap; 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.Direction;
import se.urmo.game.util.MiscUtil;
import se.urmo.game.util.MyPoint; import se.urmo.game.util.MyPoint;
import java.awt.Color; import java.awt.Color;
import java.awt.Graphics; import java.awt.Graphics;
import java.awt.Point; import java.util.EnumMap;
import java.awt.image.BufferedImage;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
@Slf4j @Slf4j
public class Ghost extends BaseAnimated { public class Ghost extends BaseAnimated {
private static final double BASE_SPEED = 0.40; public static final double BASE_SPEED = 0.40;
private static final int WARNING_THRESHOLD = 180; // 3 seconds of warning
public static final int GHOST_SIZE = 32; public static final int GHOST_SIZE = 32;
private static final int ANIMATION_UPDATE_FREQUENCY = 25; private static final int ANIMATION_UPDATE_FREQUENCY = 25;
private static final int FRIGHTENED_DURATION_TICKS = 10 * Game.UPS_SET;
@Getter
private final GhostCollisionChecker collisionChecker; private final GhostCollisionChecker collisionChecker;
private final GhostStrategy chaseStrategy; @Getter
private final MyPoint startPos; private final int animation;
private final BufferedImage[] fearAnimation; @Getter
private final BufferedImage[] baseAnimation;
private final LevelManager levelManager; private final LevelManager levelManager;
private MyPoint position;
@Getter
private final GhostStrategy scaterStrategy; private final GhostStrategy scaterStrategy;
private GhostStrategy currentStrategy; private GhostState currentState;
private BufferedImage[] animation;
private Direction direction;
private Direction prevDirection;
private GhostMode mode;
private final GhostStrategy fearStrategy = new FearStrategy();
private int frightenedTimer = 0;
private boolean isBlinking = false;
private final Map<GhostMode, GhostState> states = new EnumMap<>(GhostMode.class);
public Ghost(GhostCollisionChecker collisionChecker, GhostStrategy strategy, GhostStrategy scaterStrategy, int animation, LevelManager levelManager) { // Movement-related state
@Getter
@Setter
protected MyPoint position; // center of sprite
@Getter
@Setter
protected Direction direction;
@Getter
@Setter
protected Direction prevDirection;
@Getter
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); super(ANIMATION_UPDATE_FREQUENCY, GhostManager.MAX_SPRITE_FRAMES);
this.collisionChecker = collisionChecker; this.collisionChecker = collisionChecker;
this.chaseStrategy = strategy;
this.scaterStrategy = scaterStrategy; this.scaterStrategy = scaterStrategy;
this.baseAnimation = SpriteSheetManager.get(SpriteLocation.GHOST).getAnimation(animation); this.animation = animation;
this.fearAnimation = SpriteSheetManager.get(SpriteLocation.GHOST).getAnimation(8);
position = new MyPoint(
13 * GameMap.MAP_TILESIZE + GameMap.OFFSET_X + ((double) GameMap.MAP_TILESIZE / 2),
4 * GameMap.MAP_TILESIZE + GameMap.OFFSET_Y + ((double) GameMap.MAP_TILESIZE / 2));
startPos = position;
this.currentStrategy = chaseStrategy;
this.animation = baseAnimation;
this.levelManager = levelManager; this.levelManager = levelManager;
this.position = startPosition;
states.put(GhostMode.CHASE, new ChaseGhostMode(this, chaseStrategy));
states.put(GhostMode.SCATTER, new ScatterGhostMode(this, scaterStrategy));
states.put(GhostMode.FRIGHTENED, new FrightenedGhostMode(this));
states.put(GhostMode.EATEN, new EatenGhostMode(this));
states.put(GhostMode.FROZEN, new FrozenGhostMode(this));
states.put(GhostMode.HOUSE, new HouseGhostMode(this));
} }
public void draw(Graphics g) { public void draw(Graphics g) {
g.drawImage( g.drawImage(
animation[aniIndex], currentState.getAnimation()[aniIndex],
(int) position.x - GHOST_SIZE / 2, (int) position.x - GHOST_SIZE / 2,
(int) position.y - GHOST_SIZE / 2, (int) position.y - GHOST_SIZE / 2,
GHOST_SIZE, GHOST_SIZE,
GHOST_SIZE, null); 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);
// g.fillRect((int) position.x, (int) position.y, 4, 4);
} }
public void update(PacMan pacman, GameMap map) { public void update(PacMan pacman, GameMap map) {
//updateAnimationTick(); currentState.update(this, pacman, map);
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) {
if (map.isAligned(new Point((int) position.x, (int) position.y))) {
log.info("Evaluating possible directions");
prevDirection = direction;
direction = chooseDirection(
prioritize(collisionChecker.calculateDirectionAlternatives(position)),
currentStrategy.chooseTarget(this, pacman, map));
log.info("selecting direction {}", direction);
}
moveTo(getNewPosition());
}
/**
* Given a position and a direction - calculate the new position
* Moves one pixel in the given direction
*
* @return new position
*/
private MyPoint getNewPosition() {
return new MyPoint(
position.x + direction.dx * getSpeed(),
position.y + direction.dy * getSpeed());
}
private double getSpeed() {
return BASE_SPEED * levelManager.getGhostSpeed();
}
private void moveTo(MyPoint newPosition) {
MyPoint destination = collisionChecker.canMoveTo(direction, newPosition.x, newPosition.y);
if (destination != null) {
position = destination;
}
}
/**
* Creates a map of directions and their associated priority values based on the given list of directions.
* Directions with a value of {@code Direction.NONE} are excluded. If a direction is opposite to the
* previous direction, it is given a higher priority value.
*
* @param directions a list of potential movement directions
* @return a map where keys are valid directions and values are their associated priority
*/
private Map<Direction, Integer> prioritize(List<Direction> directions) {
return directions.stream()
.filter(d -> d != Direction.NONE)
.collect(Collectors.toMap(
d -> d,
d -> (prevDirection != null && d == prevDirection.opposite()) ? 2 : 1
));
}
/**
* Selects the best movement direction for the ghost based on priority and distance to the target.
* The method evaluates the directions with the lowest priority value and chooses the one that
* minimizes the distance to the target point.
*
* @param options a map where keys represent potential movement directions and values
* represent their associated priority levels
* @param target the target point towards which the direction is evaluated
* @return the direction that has the lowest priority and minimizes the distance to the target
*/
private Direction chooseDirection(Map<Direction, Integer> options, Point target) {
// Find the lowest priority
int lowestPriority = options.values().stream()
.mapToInt(Integer::intValue)
.min()
.orElse(Integer.MAX_VALUE);
// Collect all directions that have this priority
List<Direction> directions = options.entrySet().stream()
.filter(entry -> entry.getValue() == lowestPriority)
.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;
for (Direction d : directions) {
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 best;
} }
public void setMode(GhostMode mode) { public void setMode(GhostMode mode) {
this.mode = mode; GhostMode currentMode = currentMode();
switch (mode) { if (currentMode == null || mode.ordinal() < currentMode.ordinal()) { // only if new mode has higher prio
case CHASE -> { log.debug("Mode changed to {}", mode);
animation = baseAnimation; currentState = states.get(mode);
currentStrategy = chaseStrategy; } else {
} log.debug("Mode not changed to {}, current mode is {}", mode, currentMode);
case SCATTER -> currentStrategy = scaterStrategy;
case FRIGHTENED -> {
frightenedTimer = FRIGHTENED_DURATION_TICKS;
isBlinking = false;
animation = fearAnimation;
currentStrategy = fearStrategy;
}
case EATEN -> currentStrategy = null;
} }
} }
/**
* 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) {
log.debug("Requested mode change to {}", mode);
currentState = states.get(mode);
}
public boolean isFrightened() { public boolean isFrightened() {
return mode == GhostMode.FRIGHTENED; return states.get(GhostMode.FRIGHTENED) == currentState;
} }
public void resetPosition() { public boolean isEaten() {
position = startPos; return states.get(GhostMode.EATEN) == currentState;
} }
public Point getPosition() { public GhostMode currentMode() {
return new Point((int) position.x, (int) position.y); return states.entrySet().stream()
.filter(s -> s.getValue() == currentState)
.map(Map.Entry::getKey)
.findFirst()
.orElse(null);
} }
public void resetModes() { public void reset() {
mode = GhostMode.CHASE; currentState = states.get(GhostMode.CHASE);
((ChaseGhostMode) currentState).resetPosition();
} }
} }

View File

@ -1,8 +0,0 @@
package se.urmo.game.entities.ghost;
public enum GhostMode {
CHASE,
SCATTER,
FRIGHTENED,
EATEN
}

View File

@ -0,0 +1,148 @@
package se.urmo.game.entities.ghost.mode;
import lombok.extern.slf4j.Slf4j;
import se.urmo.game.entities.ghost.Ghost;
import se.urmo.game.entities.ghost.strategy.GhostStrategy;
import se.urmo.game.entities.pacman.PacMan;
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.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;
@Slf4j
public abstract class AbstractGhostModeImpl implements GhostState {
private final Ghost ghost;
protected GhostStrategy strategy;
protected final LevelManager levelManager;
protected int animation;
public AbstractGhostModeImpl(Ghost ghost, GhostStrategy strategy, LevelManager levelManager, int animation) {
this.ghost = ghost;
this.strategy = strategy;
this.levelManager = levelManager;
this.animation = animation;
}
/**
* Update method to be implemented by each mode
*/
public abstract void update(Ghost ghost, PacMan pacman, GameMap map);
public double getSpeed() {
return Ghost.BASE_SPEED * levelManager.getGhostSpeed();
}
@Override
public MyPoint getPosition() {
return ghost.getPosition();
}
@Override
public Direction getDirection() {
return ghost.getDirection();
}
@Override
public BufferedImage[] getAnimation() {
return SpriteSheetManager.get(SpriteLocation.GHOST).getAnimation(animation);
}
protected void updatePosition(Ghost ghost, PacMan pacman, GameMap map) {
if (map.isAligned(getPosition().asPoint())) {
ghost.setPrevDirection(getDirection());
ghost.setDirection(chooseDirection(
ghost,
prioritizeDirections(
ghost.getCollisionChecker().calculateDirectionAlternatives(ghost, getPosition())),
getStrategy().chooseTarget(ghost, pacman, map)));
log.debug("Ghost moving to {}", getPosition());
}
moveTo(ghost, getNewPosition(getSpeed()));
}
protected GhostStrategy getStrategy() {
return strategy;
}
private MyPoint getNewPosition(double speed) {
MyPoint position = ghost.getPosition();
Direction direction = ghost.getDirection();
return new MyPoint(
position.x + direction.dx * speed,
position.y + direction.dy * speed);
}
private void moveTo(Ghost ghost, MyPoint newPosition) {
MyPoint destination = ghost.getCollisionChecker().canMoveTo(ghost,
getDirection(), newPosition.x, newPosition.y);
if (destination != null) {
ghost.setPosition(destination);
}
}
private Map<Direction, Integer> prioritizeDirections(List<Direction> directions) {
return directions.stream()
.filter(d -> d != Direction.NONE)
.collect(Collectors.toMap(
d -> d,
d -> (ghost.getPrevDirection() != null &&
d == ghost.getPrevDirection().opposite()) ? 2 : 1
));
}
private Direction chooseDirection(Ghost ghost, Map<Direction, Integer> options, Point target) {
// Find the lowest priority
int lowestPriority = options.values().stream()
.mapToInt(Integer::intValue)
.min()
.orElse(Integer.MAX_VALUE);
// Collect all directions that have this priority
List<Direction> directions = options.entrySet().stream()
.filter(entry -> entry.getValue() == lowestPriority)
.map(Map.Entry::getKey)
.toList();
MyPoint position = getPosition();
// 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);
return new DirectionDistance(d, dist);
})
.toList();
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;
}
public void resetPosition() {
ghost.setPosition(Ghost.getStartPosition());
}
}

View File

@ -0,0 +1,31 @@
package se.urmo.game.entities.ghost.mode;
import lombok.extern.slf4j.Slf4j;
import se.urmo.game.entities.ghost.Ghost;
import se.urmo.game.entities.ghost.strategy.GhostStrategy;
import se.urmo.game.entities.pacman.PacMan;
import se.urmo.game.map.GameMap;
@Slf4j
public class ChaseGhostMode extends AbstractGhostModeImpl {
public ChaseGhostMode(Ghost ghost, GhostStrategy strategy) {
super(ghost, strategy, ghost.getLevelManager(), ghost.getAnimation());
}
@Override
public void update(Ghost ghost, PacMan pacman, GameMap map) {
// Use the common position update method with chase strategy
updatePosition(ghost, pacman, map);
}
// @Override
// public void enter(Ghost ghost) {
// }
@Override
public double getSpeed() {
return Ghost.BASE_SPEED * levelManager.getGhostSpeed();
}
}

View File

@ -0,0 +1,43 @@
package se.urmo.game.entities.ghost.mode;
import lombok.extern.slf4j.Slf4j;
import se.urmo.game.entities.ghost.Ghost;
import se.urmo.game.entities.ghost.strategy.EatenStrategy;
import se.urmo.game.entities.pacman.PacMan;
import se.urmo.game.graphics.SpriteLocation;
import se.urmo.game.graphics.SpriteSheetManager;
import se.urmo.game.map.GameMap;
import java.awt.image.BufferedImage;
@Slf4j
public class EatenGhostMode extends AbstractGhostModeImpl {
// Eaten mode specific constants
private static final BufferedImage[] EATEN_ANIMATION = SpriteSheetManager.get(SpriteLocation.GHOST).getAnimation(9);
private static final double EATEN_SPEED = 1.0; // Faster when eaten
public EatenGhostMode(Ghost ghost) {
super(ghost, new EatenStrategy(), ghost.getLevelManager(), 9);
}
@Override
public void update(Ghost ghost, PacMan pacman, GameMap map) {
if (getPosition().asPoint().distance(Ghost.getHouseEntrance().asPoint()) < 10) {
log.debug("Ghost reached home, returning to chase mode");
ghost.requestModeChange(GhostMode.CHASE);
return;
}
updatePosition(ghost, pacman, map);
}
@Override
public BufferedImage[] getAnimation() {
return EATEN_ANIMATION;
}
@Override
public double getSpeed() {
return EATEN_SPEED;
}
}

View File

@ -0,0 +1,87 @@
package se.urmo.game.entities.ghost.mode;
import lombok.extern.slf4j.Slf4j;
import se.urmo.game.entities.ghost.Ghost;
import se.urmo.game.entities.ghost.strategy.FearStrategy;
import se.urmo.game.entities.pacman.PacMan;
import se.urmo.game.graphics.SpriteLocation;
import se.urmo.game.graphics.SpriteSheetManager;
import se.urmo.game.main.Game;
import se.urmo.game.map.GameMap;
import java.awt.image.BufferedImage;
@Slf4j
public class FrightenedGhostMode extends AbstractGhostModeImpl {
// Frightened mode specific constants
private static final int WARNING_THRESHOLD = 180; // 3 seconds of warning
private static final BufferedImage[] FEAR_ANIMATION = SpriteSheetManager.get(SpriteLocation.GHOST).getAnimation(8);
private static final int FRIGHTENED_DURATION_TICKS = 10 * Game.UPS_SET;
// Frightened mode specific state
private int frightenedTimer = FRIGHTENED_DURATION_TICKS;
private boolean isBlinking = false;
public FrightenedGhostMode(Ghost ghost) {
super(ghost, new FearStrategy(), ghost.getLevelManager(), ghost.getAnimation());
strategy = new FearStrategy();
}
@Override
public void update(Ghost ghost, PacMan pacman, GameMap map) {
updateFrightened(ghost);
updatePosition(ghost, pacman, map);
}
@Override
public BufferedImage[] getAnimation() {
return (!isBlinking ? FEAR_ANIMATION : super.getAnimation());
}
@Override
public double getSpeed() {
return 1.0;
}
/**
* Update the frightened state timer and handle blinking animation
*/
private void updateFrightened(Ghost ghost) {
frightenedTimer--;
// Handle blinking animation when timer is running low
if (frightenedTimer <= WARNING_THRESHOLD) {
isBlinking = (frightenedTimer / 25) % 2 == 0;
}
// Check if frightened mode should end
if (frightenedTimer <= 0) {
log.debug("Frightened mode ended");
ghost.requestModeChange(GhostMode.CHASE);
frightenedTimer = FRIGHTENED_DURATION_TICKS;
isBlinking = false;
}
}
// public void enter(Ghost ghost) {
// // Initialize frightened mode-specific state
// log.debug("Entering frightened mode");
// frightenedTimer = FRIGHTENED_DURATION_TICKS;
// isBlinking = false;
// }
/**
* Get the current time remaining in frightened mode (for external queries)
*/
public int getRemainingTime() {
return frightenedTimer;
}
/**
* Reset the frightened timer (for power pellet extensions)
*/
public void resetTimer() {
frightenedTimer = FRIGHTENED_DURATION_TICKS;
isBlinking = false;
}
}

View File

@ -0,0 +1,23 @@
package se.urmo.game.entities.ghost.mode;
import lombok.extern.slf4j.Slf4j;
import se.urmo.game.entities.ghost.Ghost;
import se.urmo.game.entities.pacman.PacMan;
import se.urmo.game.map.GameMap;
@Slf4j
public class FrozenGhostMode extends AbstractGhostModeImpl {
public FrozenGhostMode(Ghost ghost) {
super(ghost, null, ghost.getLevelManager(), ghost.getAnimation());
}
@Override
public void update(Ghost ghost, PacMan pacman, GameMap map) {
// Do not update position - frozen ghosts don't move
}
// @Override
// public void enter(Ghost ghost) {
// log.debug("Entering frozen mode");
// }
}

View File

@ -0,0 +1,12 @@
package se.urmo.game.entities.ghost.mode;
public enum GhostMode {
// 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
}

View File

@ -0,0 +1,19 @@
package se.urmo.game.entities.ghost.mode;
import se.urmo.game.entities.ghost.Ghost;
import se.urmo.game.entities.pacman.PacMan;
import se.urmo.game.map.GameMap;
import se.urmo.game.util.Direction;
import se.urmo.game.util.MyPoint;
import java.awt.image.BufferedImage;
public interface GhostState {
void update(Ghost ghost, PacMan pacman, GameMap map);
MyPoint getPosition();
Direction getDirection();
BufferedImage[] getAnimation();
}

View File

@ -0,0 +1,23 @@
package se.urmo.game.entities.ghost.mode;
import lombok.extern.slf4j.Slf4j;
import se.urmo.game.entities.ghost.Ghost;
import se.urmo.game.entities.ghost.strategy.HouseStrategy;
import se.urmo.game.entities.pacman.PacMan;
import se.urmo.game.map.GameMap;
@Slf4j
public class HouseGhostMode extends AbstractGhostModeImpl {
public HouseGhostMode(Ghost ghost) {
super(ghost, new HouseStrategy(), ghost.getLevelManager(), ghost.getAnimation());
}
@Override
public void update(Ghost ghost, PacMan pacman, GameMap map) {
if (getPosition().asPoint().distance(Ghost.getHouseEntrance().asPoint()) < 15) {
log.debug("Ghost left home, switching to chase mode");
ghost.requestModeChange(GhostMode.CHASE);
}
updatePosition(ghost, pacman, map);
}
}

View File

@ -0,0 +1,56 @@
package se.urmo.game.entities.ghost.mode;
import lombok.extern.slf4j.Slf4j;
import se.urmo.game.entities.ghost.Ghost;
import se.urmo.game.entities.ghost.strategy.GhostStrategy;
import se.urmo.game.entities.pacman.PacMan;
import se.urmo.game.map.GameMap;
@Slf4j
public class ScatterGhostMode extends AbstractGhostModeImpl {
// Time in scatter mode before returning to chase
private static final int SCATTER_DURATION = 7 * 60; // 7 seconds at 60 ticks/second
private int scatterTimer = 0;
public ScatterGhostMode(Ghost ghost, GhostStrategy scaterStrategy) {
super(ghost, scaterStrategy, ghost.getLevelManager(), ghost.getAnimation());
}
@Override
public void update(Ghost ghost, PacMan pacman, GameMap map) {
// Update scatter timer
updateScatterTimer(ghost);
// Use common position update with scatter strategy
updatePosition(ghost, pacman, map);
}
@Override
public double getSpeed() {
return Ghost.BASE_SPEED * levelManager.getGhostSpeed();
}
private void updateScatterTimer(Ghost ghost) {
scatterTimer--;
if (scatterTimer <= 0) {
log.debug("Scatter mode timed out, returning to chase");
ghost.requestModeChange(GhostMode.CHASE);
scatterTimer = SCATTER_DURATION;
}
}
// @Override
// public void enter(Ghost ghost) {
// log.debug("Entering scatter mode");
//
// // Initialize scatter mode timer
// scatterTimer = SCATTER_DURATION;
// }
/**
* Reset the scatter timer (for extending scatter mode)
*/
public void resetTimer() {
scatterTimer = SCATTER_DURATION;
}
}

View File

@ -1,7 +1,7 @@
package se.urmo.game.entities.ghost.strategy; package se.urmo.game.entities.ghost.strategy;
import se.urmo.game.entities.pacman.PacMan;
import se.urmo.game.entities.ghost.Ghost; import se.urmo.game.entities.ghost.Ghost;
import se.urmo.game.entities.pacman.PacMan;
import se.urmo.game.map.GameMap; import se.urmo.game.map.GameMap;
import java.awt.Point; import java.awt.Point;
@ -10,7 +10,7 @@ public class ClydeStrategy implements GhostStrategy {
@Override @Override
public Point chooseTarget(Ghost clyde, PacMan pacman, GameMap map) { public Point chooseTarget(Ghost clyde, PacMan pacman, GameMap map) {
Point pacTile = pacman.getPosition(); Point pacTile = pacman.getPosition();
Point clydeTile = clyde.getPosition(); // ghosts current tile Point clydeTile = clyde.getPosition().asPoint(); // ghosts current tile
double distance = pacTile.distance(clydeTile); double distance = pacTile.distance(clydeTile);

View File

@ -0,0 +1,14 @@
package se.urmo.game.entities.ghost.strategy;
import se.urmo.game.entities.ghost.Ghost;
import se.urmo.game.entities.pacman.PacMan;
import se.urmo.game.map.GameMap;
import java.awt.Point;
public class EatenStrategy implements GhostStrategy {
@Override
public Point chooseTarget(Ghost ghost, PacMan pacman, GameMap map) {
return Ghost.getHouseEntrance().asPoint();
}
}

View File

@ -1,7 +1,7 @@
package se.urmo.game.entities.ghost.strategy; package se.urmo.game.entities.ghost.strategy;
import se.urmo.game.entities.pacman.PacMan;
import se.urmo.game.entities.ghost.Ghost; import se.urmo.game.entities.ghost.Ghost;
import se.urmo.game.entities.pacman.PacMan;
import se.urmo.game.map.GameMap; import se.urmo.game.map.GameMap;
import se.urmo.game.util.Direction; import se.urmo.game.util.Direction;
@ -16,18 +16,18 @@ public class FearStrategy implements GhostStrategy {
public Point chooseTarget(Ghost ghost, PacMan pacman, GameMap map) { public Point chooseTarget(Ghost ghost, PacMan pacman, GameMap map) {
// Frightened ghosts do not target Pacman. // Frightened ghosts do not target Pacman.
// Instead, they pick a random adjacent valid tile. // Instead, they pick a random adjacent valid tile.
Point ghostPos = ghost.getPosition(); 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()) { if (neighbors.isEmpty()) {
return ghost.getPosition(); // stuck return ghost.getPosition().asPoint(); // stuck
} }
//Transform directions to actual Points //Transform directions to actual Points
List<Point> potentialTargets = neighbors.stream() List<Point> potentialTargets = neighbors.stream()
.map(d -> new Point( .map(d -> new Point(
ghost.getPosition().x + d.dx * GameMap.MAP_TILESIZE, (int) (ghost.getPosition().x + d.dx * GameMap.MAP_TILESIZE),
ghost.getPosition().y + d.dy * GameMap.MAP_TILESIZE)).toList(); (int) (ghost.getPosition().y + d.dy * GameMap.MAP_TILESIZE))).toList();
// Pick a random valid neighbor // Pick a random valid neighbor
return potentialTargets.get(random.nextInt(neighbors.size())); return potentialTargets.get(random.nextInt(neighbors.size()));

View File

@ -0,0 +1,14 @@
package se.urmo.game.entities.ghost.strategy;
import se.urmo.game.entities.ghost.Ghost;
import se.urmo.game.entities.pacman.PacMan;
import se.urmo.game.map.GameMap;
import java.awt.Point;
public class HouseStrategy implements GhostStrategy {
@Override
public Point chooseTarget(Ghost ghost, PacMan pacman, GameMap map) {
return new Point(232, 280);
}
}

View File

@ -1,7 +1,7 @@
package se.urmo.game.entities.ghost.strategy; package se.urmo.game.entities.ghost.strategy;
import se.urmo.game.entities.pacman.PacMan;
import se.urmo.game.entities.ghost.Ghost; import se.urmo.game.entities.ghost.Ghost;
import se.urmo.game.entities.pacman.PacMan;
import se.urmo.game.map.GameMap; import se.urmo.game.map.GameMap;
import se.urmo.game.util.Direction; import se.urmo.game.util.Direction;
@ -27,7 +27,7 @@ public class InkyStrategy implements GhostStrategy {
}; };
// 2. Vector from blinky to that tile // 2. Vector from blinky to that tile
Point blinkyPos = blinky.getPosition(); Point blinkyPos = blinky.getPosition().asPoint();
int vx = ahead.x - blinkyPos.x; int vx = ahead.x - blinkyPos.x;
int vy = ahead.y - blinkyPos.y; int vy = ahead.y - blinkyPos.y;

View File

@ -5,15 +5,21 @@ import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import se.urmo.game.collision.CollisionChecker; import se.urmo.game.collision.CollisionChecker;
import se.urmo.game.entities.BaseAnimated; import se.urmo.game.entities.BaseAnimated;
import se.urmo.game.state.LevelManager; import se.urmo.game.graphics.SpriteLocation;
import se.urmo.game.util.Direction; import se.urmo.game.graphics.SpriteSheetManager;
import se.urmo.game.main.LevelManager;
import se.urmo.game.map.GameMap; import se.urmo.game.map.GameMap;
import se.urmo.game.util.Direction;
import se.urmo.game.util.LoadSave; import se.urmo.game.util.LoadSave;
import se.urmo.game.util.MyPoint; import se.urmo.game.util.MyPoint;
import java.awt.*; import java.awt.Graphics;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.util.Arrays; import java.util.Arrays;
import java.util.stream.Stream;
@Slf4j @Slf4j
@ -22,74 +28,112 @@ public class PacMan extends BaseAnimated {
public static final int PACMAN_OFFSET = PACMAN_SIZE / 2; public static final int PACMAN_OFFSET = PACMAN_SIZE / 2;
private static final int COLLISION_BOX_SIZE = 16; 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 COLLISION_BOX_OFFSET = (PACMAN_SIZE - COLLISION_BOX_SIZE) / 2;
private final MyPoint startPosition;
private static final int ANIMATION_UPDATE_FREQUENCY = 10; private static final int ANIMATION_UPDATE_FREQUENCY = 10;
private static final double BASE_SPEED = 0.40; private static final double BASE_SPEED = 0.40;
private boolean moving = false; private static final long FRAME_NS = 80_000_000L; // 80 ms
private final BufferedImage[][] spriteSheets; public static final int PACMAN_SPRITE_FRAMES = 4;
private MyPoint position; private final MyPoint startPosition;
private final CollisionChecker collisionChecker; private final CollisionChecker collisionChecker;
private final LevelManager levelManager; private final LevelManager levelManager;
private final Sprites sprites;
private boolean moving = false;
private MyPoint position; //
@Setter @Setter
@Getter @Getter
private Direction direction = Direction.NONE; private Direction direction = Direction.NONE;
private double pacmanLevelSpeed; private BufferedImage[] deathFrames; // working copy
private long lastChangeNs;
// animation state
private PacmanState state = PacmanState.ALIVE;
private int deathFrameIdx = 0;
private double speed;
public PacMan(CollisionChecker collisionChecker, LevelManager levelManager) { public PacMan(CollisionChecker collisionChecker, LevelManager levelManager) {
super(ANIMATION_UPDATE_FREQUENCY, 4); super(ANIMATION_UPDATE_FREQUENCY, PACMAN_SPRITE_FRAMES);
this.collisionChecker = collisionChecker; this.collisionChecker = collisionChecker;
this.levelManager = levelManager; this.levelManager = levelManager;
this.position = new MyPoint( this.position = new MyPoint(
26 * GameMap.MAP_TILESIZE + GameMap.OFFSET_X, 26 * GameMap.MAP_TILESIZE + GameMap.OFFSET_X,
13 * GameMap.MAP_TILESIZE + GameMap.OFFSET_Y + ((double) GameMap.MAP_TILESIZE / 2)); 13 * GameMap.MAP_TILESIZE + GameMap.OFFSET_Y + ((double) GameMap.MAP_TILESIZE / 2));
this.startPosition = this.position; this.startPosition = this.position;
this.spriteSheets = loadAnimation(); this.sprites = loadAnimation();
this.pacmanLevelSpeed = this.levelManager.getPacmanLevelSpeed(); this.speed = BASE_SPEED * levelManager.getPacmanLevelSpeed();
} }
private BufferedImage[][] loadAnimation() { private Sprites loadAnimation() {
BufferedImage[][] image = new BufferedImage[3][4]; BufferedImage[][] spriteMap = new BufferedImage[6][PACMAN_SPRITE_FRAMES];
BufferedImage[][] spriteMap = new BufferedImage[4][4];; BufferedImage[] deathFrames;
BufferedImage img = LoadSave.GetSpriteAtlas("sprites/PacManAssets-PacMan.png"); BufferedImage[] animation = SpriteSheetManager.get(SpriteLocation.PACMAN).getAnimation(0);
for (int row = 0; row < 3; row++) { spriteMap[Direction.RIGHT.ordinal()] = animation;
for (int col = 0; col < 4; col++) { spriteMap[Direction.LEFT.ordinal()] = Arrays.stream(animation)
image[row][col] = img.getSubimage(PACMAN_SIZE * col, PACMAN_SIZE * row, PACMAN_SIZE, PACMAN_SIZE); .map(i -> LoadSave.rotate(i, Direction.LEFT.angel))
}
}
spriteMap[Direction.RIGHT.ordinal()] = image[0];
spriteMap[Direction.LEFT.ordinal()] = Arrays.stream(image[0])
.map(i -> LoadSave.rotate(i, 180))
.toArray(BufferedImage[]::new); .toArray(BufferedImage[]::new);
spriteMap[Direction.DOWN.ordinal()] = Arrays.stream(image[0]) spriteMap[Direction.DOWN.ordinal()] = Arrays.stream(animation)
.map(i -> LoadSave.rotate(i, 90)) .map(i -> LoadSave.rotate(i, Direction.DOWN.angel))
.toArray(BufferedImage[]::new); .toArray(BufferedImage[]::new);
spriteMap[Direction.UP.ordinal()] = Arrays.stream(image[0]) spriteMap[Direction.UP.ordinal()] = Arrays.stream(animation)
.map(i -> LoadSave.rotate(i, 270)) .map(i -> LoadSave.rotate(i, Direction.UP.angel))
.toArray(BufferedImage[]::new); .toArray(BufferedImage[]::new);
return spriteMap; 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) { 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( g.drawImage(
spriteSheets[direction==Direction.NONE ? 0 : direction.ordinal()][aniIndex], sprites.spriteSheets[direction == Direction.NONE ? 0 : direction.ordinal()][aniIndex],
(int) position.x - PACMAN_OFFSET, (int) position.x - PACMAN_OFFSET,
(int) position.y - PACMAN_OFFSET, (int) position.y - PACMAN_OFFSET,
PACMAN_SIZE, PACMAN_SIZE,
PACMAN_SIZE, null); PACMAN_SIZE, null);
} }
public void update() { private void drawDead(Graphics g) {
if(moving) { if (state == PacmanState.ALIVE) return; // ignore if not dying/dead
MyPoint mpoint = switch (direction) {
case RIGHT -> new MyPoint(position.x + getSpeed(), position.y);
case LEFT -> new MyPoint(position.x - getSpeed(), position.y);
case UP -> new MyPoint(position.x, position.y - getSpeed());
case DOWN -> new MyPoint(position.x, position.y + getSpeed());
default -> throw new IllegalStateException("Unexpected value: " + direction);
};
MyPoint destination = collisionChecker.getValidDestination(direction, mpoint, COLLISION_BOX_SIZE, COLLISION_BOX_SIZE); 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) { if (destination != null) {
position = destination; position = destination;
@ -97,25 +141,39 @@ public class PacMan extends BaseAnimated {
} }
} }
private MyPoint getNewPosition() {
return new MyPoint(position.x + direction.dx * getSpeed(), position.y + direction.dy * getSpeed());
}
private double getSpeed() { private double getSpeed() {
return BASE_SPEED * pacmanLevelSpeed; return this.speed;
} }
public double distanceTo(Point point) { public double distanceTo(Point point) {
return new Point((int) position.x, (int) position.y).distance(point); return new Point((int) position.x, (int) position.y).distance(point);
} }
public void resetPosition() { public void setState(PacmanState state) {
position = startPosition; this.state = state;
} switch (state) {
case ALIVE -> {
public void reset() { position = startPosition;
resetPosition(); aniIndex = 0; // reset animation to start
aniIndex = 0; // reset animation to start deathFrameIdx = 0;
speed = BASE_SPEED * levelManager.getPacmanLevelSpeed(); // Recalculate
}
case 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);
}
}
} }
public Image getLifeIcon() { public Image getLifeIcon() {
return spriteSheets[0][1]; return sprites.spriteSheets[0][1];
} }
public Rectangle getBounds() { public Rectangle getBounds() {
@ -134,4 +192,11 @@ public class PacMan extends BaseAnimated {
moving = b; moving = b;
paused = !b; paused = !b;
} }
public enum PacmanState {
ALIVE, DYING, DEAD
}
record Sprites(BufferedImage[][] spriteSheets, BufferedImage[] deathFrames) {
}
} }

View File

@ -2,6 +2,7 @@ package se.urmo.game.graphics;
import lombok.Getter; import lombok.Getter;
import se.urmo.game.entities.ghost.Ghost; import se.urmo.game.entities.ghost.Ghost;
import se.urmo.game.entities.pacman.PacMan;
import se.urmo.game.map.GameMap; import se.urmo.game.map.GameMap;
import se.urmo.game.util.LoadSave; import se.urmo.game.util.LoadSave;
@ -12,6 +13,7 @@ public enum SpriteLocation {
MAP("sprites/PacMan-custom-spritemap-0-3.png", 5, 11, GameMap.MAP_TILESIZE), MAP("sprites/PacMan-custom-spritemap-0-3.png", 5, 11, GameMap.MAP_TILESIZE),
ITEM("sprites/PacManAssets-Items.png", 2, 8, GameMap.MAP_TILESIZE), ITEM("sprites/PacManAssets-Items.png", 2, 8, GameMap.MAP_TILESIZE),
GHOST("sprites/PacManAssets-Ghosts.png", 11, 4, Ghost.GHOST_SIZE), GHOST("sprites/PacManAssets-Ghosts.png", 11, 4, Ghost.GHOST_SIZE),
PACMAN("sprites/PacManAssets-PacMan.png", 3, 4, PacMan.PACMAN_SIZE),
NONE("", 0, 0, 0) { // Special case for tiles without sprites NONE("", 0, 0, 0) { // Special case for tiles without sprites
@Override @Override
public BufferedImage[][] loadSprites(int tileSize) { public BufferedImage[][] loadSprites(int tileSize) {

View File

@ -1,21 +1,14 @@
package se.urmo.game.graphics; package se.urmo.game.graphics;
import se.urmo.game.map.GameMap;
import java.util.EnumMap; import java.util.EnumMap;
import java.util.Map; import java.util.Map;
public class SpriteSheetManager { public class SpriteSheetManager {
private static final Map<SpriteLocation, SpriteSheet> spriteSheets = new EnumMap<>(SpriteLocation.class); private static final Map<SpriteLocation, SpriteSheet> spriteSheets = new EnumMap<>(SpriteLocation.class);
public static SpriteSheet get(SpriteLocation location) { public static SpriteSheet get(SpriteLocation location) {
return spriteSheets.computeIfAbsent(location, SpriteSheet::new); return spriteSheets.computeIfAbsent(location, SpriteSheet::new);
} }
// Optionally add methods like:
public static void reloadAll() {
spriteSheets.clear();
}
} }

View File

@ -1,6 +1,6 @@
package se.urmo.game.input; package se.urmo.game.input;
import se.urmo.game.state.GameStateManager; import se.urmo.game.main.GameStateManager;
import java.awt.event.KeyEvent; import java.awt.event.KeyEvent;
import java.awt.event.KeyListener; import java.awt.event.KeyListener;

View File

@ -1,4 +1,4 @@
package se.urmo.game.state; package se.urmo.game.main;
import se.urmo.game.entities.Animated; import se.urmo.game.entities.Animated;

View File

@ -1,8 +1,10 @@
package se.urmo.game.state; package se.urmo.game.main;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import se.urmo.game.entities.collectibles.Fruit; import se.urmo.game.entities.collectibles.Fruit;
import se.urmo.game.entities.pacman.PacMan; import se.urmo.game.entities.pacman.PacMan;
import se.urmo.game.state.PlayingState;
import se.urmo.game.util.FruitType;
import java.awt.Graphics; import java.awt.Graphics;

View File

@ -2,9 +2,8 @@ package se.urmo.game.main;
import lombok.Getter; import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import se.urmo.game.state.GameStateManager;
import javax.swing.*; import javax.swing.JFrame;
@Slf4j @Slf4j
public class Game implements Runnable { public class Game implements Runnable {
@ -22,7 +21,7 @@ public class Game implements Runnable {
private final GamePanel gamePanel; private final GamePanel gamePanel;
public Game() { public Game() {
this.gameStateManager = new GameStateManager(this); this.gameStateManager = new GameStateManager();
this.gamePanel = new GamePanel(this, gameStateManager); this.gamePanel = new GamePanel(this, gameStateManager);
} }

View File

@ -1,10 +1,12 @@
package se.urmo.game.main; package se.urmo.game.main;
import se.urmo.game.input.KeyHandler; import se.urmo.game.input.KeyHandler;
import se.urmo.game.state.GameStateManager;
import javax.swing.*; import javax.swing.JPanel;
import java.awt.*; import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
public class GamePanel extends JPanel { public class GamePanel extends JPanel {
public static final int ORIGINAL_TILE_SIZE = 8; public static final int ORIGINAL_TILE_SIZE = 8;

View File

@ -1,24 +1,25 @@
package se.urmo.game.state; package se.urmo.game.main;
import lombok.Getter; import lombok.Getter;
import se.urmo.game.main.Game; import se.urmo.game.state.GameOverState;
import se.urmo.game.state.GameState;
import se.urmo.game.state.PlayingState;
import se.urmo.game.util.GameStateType;
import java.awt.*; import java.awt.Graphics2D;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
public class GameStateManager { public class GameStateManager {
private final Game game; private final Map<GameStateType, GameState> states = new HashMap<>();
private Map<GameStateType, GameState> states = new HashMap<>();
@Getter @Getter
private GameState currentState; private GameState currentState;
public GameStateManager(Game game) { public GameStateManager() {
this.game = game;
GameOverState gameOverState = new GameOverState(this, new HighScoreManager()); GameOverState gameOverState = new GameOverState(this, new HighScoreManager());
states.put(GameStateType.PLAYING, new PlayingState(game, this, gameOverState)); states.put(GameStateType.PLAYING, new PlayingState(this, gameOverState));
states.put(GameStateType.GAME_OVER, gameOverState); states.put(GameStateType.GAME_OVER, gameOverState);
setState(GameStateType.GAME_OVER); setState(GameStateType.PLAYING);
} }
public void setState(GameStateType type) { public void setState(GameStateType type) {

View File

@ -1,19 +1,19 @@
package se.urmo.game.state; package se.urmo.game.main;
import lombok.Getter; import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import se.urmo.game.collision.GhostCollisionChecker; 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.ghost.strategy.BlinkyStrategy; import se.urmo.game.entities.ghost.strategy.BlinkyStrategy;
import se.urmo.game.entities.ghost.strategy.ClydeStrategy; import se.urmo.game.entities.ghost.strategy.ClydeStrategy;
import se.urmo.game.entities.ghost.Ghost;
import se.urmo.game.entities.ghost.GhostMode;
import se.urmo.game.entities.ghost.strategy.InkyStrategy; import se.urmo.game.entities.ghost.strategy.InkyStrategy;
import se.urmo.game.entities.pacman.PacMan;
import se.urmo.game.entities.ghost.strategy.PinkyStrategy; import se.urmo.game.entities.ghost.strategy.PinkyStrategy;
import se.urmo.game.entities.ghost.strategy.ScatterToBottomLeft; import se.urmo.game.entities.ghost.strategy.ScatterToBottomLeft;
import se.urmo.game.entities.ghost.strategy.ScatterToBottomRight; import se.urmo.game.entities.ghost.strategy.ScatterToBottomRight;
import se.urmo.game.entities.ghost.strategy.ScatterToTopLeft; import se.urmo.game.entities.ghost.strategy.ScatterToTopLeft;
import se.urmo.game.entities.ghost.strategy.ScatterToTopRight; import se.urmo.game.entities.ghost.strategy.ScatterToTopRight;
import se.urmo.game.entities.pacman.PacMan;
import se.urmo.game.map.GameMap; import se.urmo.game.map.GameMap;
import java.awt.Graphics2D; import java.awt.Graphics2D;
@ -29,7 +29,6 @@ public class GhostManager {
public static final int CLYDE_ANIMATION = 3; public static final int CLYDE_ANIMATION = 3;
@Getter @Getter
private final List<Ghost> ghosts = new ArrayList<>(); private final List<Ghost> ghosts = new ArrayList<>();
private final LevelManager levelManager;
private long lastModeSwitchTime; private long lastModeSwitchTime;
private int phaseIndex = 0; private int phaseIndex = 0;
@ -41,23 +40,22 @@ public class GhostManager {
5000, 20000, // scatter 5s, chase 20s 5000, 20000, // scatter 5s, chase 20s
5000, Integer.MAX_VALUE // scatter 5s, then chase forever 5000, Integer.MAX_VALUE // scatter 5s, then chase forever
}; };
private boolean frozen;
public GhostManager(GhostCollisionChecker ghostCollisionChecker, AnimationManager animationManager, LevelManager levelManager) { public GhostManager(GhostCollisionChecker ghostCollisionChecker, AnimationManager animationManager, LevelManager levelManager) {
this.levelManager = levelManager;
// Create ghosts with their strategies // Create ghosts with their strategies
Ghost blinky = new Ghost(ghostCollisionChecker, new BlinkyStrategy(), new ScatterToTopRight(), BLINKY_ANIMATION, levelManager); Ghost blinky = new Ghost(ghostCollisionChecker, new BlinkyStrategy(), new ScatterToTopRight(), BLINKY_ANIMATION, levelManager);
ghosts.add(blinky); ghosts.add(blinky);
ghosts.add(new Ghost(ghostCollisionChecker, new PinkyStrategy(), new ScatterToTopLeft(), PINKY_ANIMATION, levelManager)); ghosts.add(new Ghost(ghostCollisionChecker, new PinkyStrategy(), new ScatterToTopLeft(), PINKY_ANIMATION, levelManager));
ghosts.add(new Ghost(ghostCollisionChecker,new InkyStrategy(blinky), new ScatterToBottomRight(), INKY_ANIMATION, levelManager)); ghosts.add(new Ghost(ghostCollisionChecker, new InkyStrategy(blinky), new ScatterToBottomRight(), INKY_ANIMATION, levelManager));
ghosts.add(new Ghost(ghostCollisionChecker, new ClydeStrategy(), new ScatterToBottomLeft(), CLYDE_ANIMATION, levelManager)); ghosts.add(new Ghost(ghostCollisionChecker, new ClydeStrategy(), new ScatterToBottomLeft(), CLYDE_ANIMATION, levelManager));
ghosts.forEach(animationManager::register); ghosts.forEach(animationManager::register);
setMode(GhostMode.CHASE); setMode(GhostMode.HOUSE);
} }
public void setMode(GhostMode mode) { public void setMode(GhostMode mode) {
log.debug("Mode changed to {}", mode);
for (Ghost g : ghosts) { for (Ghost g : ghosts) {
g.setMode(mode); g.setMode(mode);
} }
@ -95,6 +93,14 @@ public class GhostManager {
public void reset() { public void reset() {
phaseIndex = 0; phaseIndex = 0;
setMode(GhostMode.SCATTER); setMode(GhostMode.SCATTER);
ghosts.forEach(Ghost::resetPosition); ghosts.forEach(Ghost::reset);
}
public void setFrozen(boolean frozen) {
this.ghosts.forEach(ghost -> ghost.setMode(GhostMode.FROZEN));
}
public int isFrightened() {
return (int) ghosts.stream().filter(Ghost::isFrightened).count();
} }
} }

View File

@ -0,0 +1,42 @@
package se.urmo.game.main;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class HighScoreManager {
private final Path file = Path.of("highscores.dat");
private final int maxEntries = 10;
private final List<Integer> scores = new ArrayList<>();
public HighScoreManager() { load(); }
public void submit(int score) {
scores.add(score);
scores.sort(Comparator.reverseOrder());
if (scores.size() > maxEntries) scores.remove(scores.size() - 1);
save();
}
public List<Integer> top() { return List.copyOf(scores); }
private void load() {
try {
if (!Files.exists(file)) return;
for (String line : Files.readAllLines(file)) {
scores.add(Integer.parseInt(line.trim()));
}
scores.sort(Comparator.reverseOrder());
} catch (Exception ignored) {}
}
private void save() {
try {
var lines = scores.stream().map(String::valueOf).toList();
Files.write(file, lines, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
} catch (Exception ignored) {}
}
}

View File

@ -1,6 +1,7 @@
package se.urmo.game.state; package se.urmo.game.main;
import lombok.Getter; import lombok.Getter;
import se.urmo.game.util.FruitType;
public class LevelManager { public class LevelManager {
@ -11,19 +12,19 @@ public class LevelManager {
@Getter @Getter
public enum Level { public enum Level {
LEVEL1(1,6000, 0.75, 0.8, FruitType.CHERRY), LEVEL1(1,6000, 0.75, 0.8, FruitType.CHERRY),
LEVEL2(2,3000, 0.85, 0.85, FruitType.CHERRY), LEVEL2(2, 6000, 0.75, 0.8, FruitType.CHERRY),
LEVEL3(3,1000, 0.95, 0.9, FruitType.CHERRY), LEVEL3(3, 6000, 0.75, 0.8, FruitType.CHERRY),
LEVEL4(4,6000, 0.75, 0.85, FruitType.CHERRY), LEVEL4(4, 6000, 0.75, 0.8, FruitType.CHERRY),
LEVEL5(5,6000, 0.75, 0.85, FruitType.CHERRY), LEVEL5(5, 3000, 0.85, 0.85, FruitType.CHERRY),
LEVEL6(6,6000, 0.75, 0.85, FruitType.CHERRY), LEVEL6(6, 3000, 0.85, 0.85, FruitType.CHERRY),
LEVEL7(7,6000, 0.75, 0.85, FruitType.CHERRY), LEVEL7(7, 3000, 0.85, 0.85, FruitType.CHERRY),
LEVEL8(8,6000, 0.75, 0.85, FruitType.CHERRY), LEVEL8(8, 3000, 0.85, 0.85, FruitType.CHERRY),
LEVEL9(9,6000, 0.75, 0.85, FruitType.CHERRY), LEVEL9(9, 3000, 0.85, 0.85, FruitType.CHERRY),
LEVEL10(10,6000, 0.75, 0.85, FruitType.CHERRY), LEVEL10(10, 1000, 0.95, 0.9, FruitType.CHERRY),
LEVEL11(11,6000, 0.75, 0.85, FruitType.CHERRY), LEVEL11(11, 1000, 0.95, 0.9, FruitType.CHERRY),
LEVEL12(12,6000, 0.75, 0.85, FruitType.CHERRY), LEVEL12(12, 1000, 0.95, 0.9, FruitType.CHERRY),
LEVEL13(13,6000, 0.75, 0.85, FruitType.CHERRY), LEVEL13(13, 1000, 0.95, 0.9, FruitType.CHERRY),
LEVEL14(14,6000, 0.75, 0.85, FruitType.CHERRY), LEVEL14(14, 6000, 0.95, 0.9, FruitType.CHERRY),
LEVEL15(15,6000, 0.75, 0.85, FruitType.CHERRY), LEVEL15(15,6000, 0.75, 0.85, FruitType.CHERRY),
LEVEL16(16,6000, 0.75, 0.85, FruitType.CHERRY), LEVEL16(16,6000, 0.75, 0.85, FruitType.CHERRY),
LEVEL17(17,6000, 0.75, 0.85, FruitType.CHERRY), LEVEL17(17,6000, 0.75, 0.85, FruitType.CHERRY),

View File

@ -0,0 +1,41 @@
package se.urmo.game.main;
import se.urmo.game.entities.ScorePopup;
import java.awt.Font;
import java.awt.Graphics2D;
import java.util.ArrayList;
import java.util.List;
public final class ScorePopupManager {
private final List<ScorePopup> popups = new ArrayList<>();
// convenience defaults
private static final long LIFE_NS = 1_000_000_000L; // 1s
private static final double RISE_PX = 16.0;
private static final double worldX = GamePanel.SCREEN_WIDTH / 2.0 - 15;
private static final double worldY = 230;
public void spawn(String text) {
popups.add(new ScorePopup(worldX, worldY, text, LIFE_NS, RISE_PX));
}
public void spawn(int score) {
spawn(String.valueOf(score));
}
public void update() {
// time-based; no per-tick math needed, just prune dead ones
popups.removeIf(p -> !p.isAlive());
}
public void draw(Graphics2D g, int offsetX, int offsetY, Font font) {
for (ScorePopup p : popups) {
p.draw(g, offsetX, offsetY, font);
}
}
public void clear() {
popups.clear();
}
}

View File

@ -1,11 +1,18 @@
package se.urmo.game.map; package se.urmo.game.map;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import se.urmo.game.entities.BaseAnimated;
import se.urmo.game.entities.ghost.Ghost;
import se.urmo.game.entities.ghost.mode.GhostMode;
import se.urmo.game.util.Direction; import se.urmo.game.util.Direction;
import java.awt.*; import java.awt.Graphics;
import java.awt.Point;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.*; import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
@ -20,7 +27,7 @@ public class GameMap {
public static final int OFFSET_X = MAP_TILESIZE; // 16px from left public static final int OFFSET_X = MAP_TILESIZE; // 16px from left
private MapTile[][] mapData; private MapTile[][] mapData;
private final int[][] csvData; private final int[][] csvData;
private long numberOfDots; private final long numberOfDots;
public GameMap(String mapFilePath) { public GameMap(String mapFilePath) {
@ -67,11 +74,6 @@ public class GameMap {
.map(String::trim) .map(String::trim)
.mapToInt(Integer::parseInt) .mapToInt(Integer::parseInt)
.toArray(); .toArray();
// for (int col = 0; col < mapColSize; col++) {
// int value = Integer.parseInt(tokens[col].trim());
// data[row][col] = MapTile.byType(TileType.fromValue(value));
// }
} }
if (row != mapRowSize) { if (row != mapRowSize) {
@ -107,26 +109,43 @@ public class GameMap {
int col = screenToCol(screenX); int col = screenToCol(screenX);
int tileY = (screenY - OFFSET_Y) % MAP_TILESIZE; int tileY = (screenY - OFFSET_Y) % MAP_TILESIZE;
int tileX = (screenX - OFFSET_X) % MAP_TILESIZE; int tileX = (screenX - OFFSET_X) % MAP_TILESIZE;
log.trace("Point[x={},y={}] is row={}, col={} with reminder x={},y={}", screenX, screenY, row, col, tileX, tileY); //log.trace("Point[x={},y={}] is row={}, col={} with reminder x={},y={}", screenX, screenY, row, col, tileX, tileY);
boolean[][] mask = mapData[row][col].getCollisionMask(); boolean[][] mask = mapData[row][col].getCollisionMask();
boolean b = mask == null || !mask[tileY][tileX]; boolean b = mask == null || !mask[tileY][tileX];
log.trace(b ? " - passable" : " - not passable"); //log.trace(b ? " - passable" : " - not passable");
return b; return b;
} }
public boolean isSolidXY(int screenX, int screenY) { public boolean isSolidXY(Ghost ghost, int screenX, int screenY) {
return isSolid(screenToRow(screenY), screenToCol(screenX)); 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 (col >= columns() || col < 0) return true;
if (row >= rows() || row < 0) return true; if (row >= rows() || row < 0) return true;
// Get the tile information
MapTile mapTile = mapData[row][col]; MapTile mapTile = mapData[row][col];
boolean solid = mapTile.isSolid(); boolean solid = mapTile.isSolid();
log.debug("[{}][{}] {}", row, col, mapTile.getTileType());
// 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 in house mode
if (mapTile.getTileType() == TileType.DOOR && ((Ghost) entity).currentMode() == GhostMode.HOUSE) {
return false; // Not solid for ghosts
}
}
//log.debug("[{}][{}] {}", row, col, mapTile.getTileType());
return solid; return solid;
} }
public boolean removeTileImage(Point screen) { public boolean removeTileImage(Point screen) {
@ -145,7 +164,7 @@ public class GameMap {
TileType type = tile.getTileType(); TileType type = tile.getTileType();
if (type.isRemovable() && tile.getImage() != null) { if (type.isRemovable() && tile.getImage() != null) {
log.debug("Removing tile {}", tile); // log.debug("Removing tile {}", tile);
tile.setImage(null); tile.setImage(null);
return true; return true;
} }
@ -221,13 +240,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 row = (screenY - GameMap.OFFSET_Y) / GameMap.MAP_TILESIZE;
int col = (screenX - GameMap.OFFSET_X) / GameMap.MAP_TILESIZE; int col = (screenX - GameMap.OFFSET_X) / GameMap.MAP_TILESIZE;
record DirectionCheck(int rowOffset, int colOffset, Direction direction) { record DirectionCheck(int rowOffset, int colOffset, Direction direction) {
} }
log.debug("At [{}][{}]", row, col); log.debug("At ({},{}), [{}][{}]", screenX, screenY, row, col);
return Stream.of( return Stream.of(
new DirectionCheck(0, 1, Direction.RIGHT), new DirectionCheck(0, 1, Direction.RIGHT),
new DirectionCheck(0, -1, Direction.LEFT), new DirectionCheck(0, -1, Direction.LEFT),
@ -237,7 +256,7 @@ public class GameMap {
.filter(dc -> { .filter(dc -> {
int r = row + dc.rowOffset; int r = row + dc.rowOffset;
int c = col + dc.colOffset; 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"); log.debug("[{}][{}] {} is {}", r, c, dc.direction, solid ? "solid" : " not solid");
return !solid; return !solid;
}) })
@ -252,8 +271,8 @@ public class GameMap {
} }
public long numberOfDots() { public long numberOfDots() {
//return this.numberOfDots; return this.numberOfDots;
return 50; //return 50;
} }
public void reset() { public void reset() {

View File

@ -43,7 +43,7 @@ public enum TileType {
TILE_35(35, true, SpriteLocation.MAP, 3, 1, false ,0), TILE_35(35, true, SpriteLocation.MAP, 3, 1, false ,0),
TILE_36(36, true, SpriteLocation.MAP, 3, 2, false ,0), TILE_36(36, true, SpriteLocation.MAP, 3, 2, false ,0),
TILE_37(37, true, SpriteLocation.MAP, 3, 3, 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_39(39, true, SpriteLocation.MAP, 3, 5, false ,0),
TILE_40(40, true, SpriteLocation.MAP, 3, 6, false ,0), TILE_40(40, true, SpriteLocation.MAP, 3, 6, false ,0),
TILE_41(41, true, SpriteLocation.MAP, 3, 7, false ,0), TILE_41(41, true, SpriteLocation.MAP, 3, 7, false ,0),
@ -62,6 +62,7 @@ public enum TileType {
TILE_54(54, true, SpriteLocation.MAP, 4, 9, false ,0), TILE_54(54, true, SpriteLocation.MAP, 4, 9, false ,0),
TILE_55(55, true, SpriteLocation.MAP, 4, 10, false ,0), TILE_55(55, true, SpriteLocation.MAP, 4, 10, false ,0),
LARGE_PELLET(56, false, SpriteLocation.ITEM, 1 ,1,true, 50), LARGE_PELLET(56, false, SpriteLocation.ITEM, 1 ,1,true, 50),
TUNNEL(57, false, SpriteLocation.ITEM, 1, 0, true, 10),
EMPTY(99, false, SpriteLocation.NONE, 0, 0, false, 0); // No sprite associated with empty tiles EMPTY(99, false, SpriteLocation.NONE, 0, 0, false, 0); // No sprite associated with empty tiles
private final int value; private final int value;

View File

@ -0,0 +1,5 @@
package se.urmo.game.sound;
public enum SoundEffect {
START, SIREN, MUNCH1, MUNCH2, FRUIT, GHOST_EATEN, EXTRA_LIFE, DEATH
}

View File

@ -0,0 +1,62 @@
package se.urmo.game.sound;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import java.util.EnumMap;
import java.util.Map;
public class SoundManager {
private final Map<SoundEffect, Clip> clips = new EnumMap<>(SoundEffect.class);
private SoundEffect lastMunch = SoundEffect.MUNCH1;
public SoundManager() {
load(SoundEffect.START, "/sounds/start.wav");
load(SoundEffect.SIREN, "/sounds/siren0.wav");
load(SoundEffect.MUNCH1, "/sounds/eat_dot_0_fast.wav");
load(SoundEffect.MUNCH2, "/sounds/eat_dot_1_fast.wav");
load(SoundEffect.FRUIT, "/sounds/eat_fruit.wav");
load(SoundEffect.GHOST_EATEN, "/sounds/eat_ghost.wav");
load(SoundEffect.EXTRA_LIFE, "/sounds/extend.wav");
load(SoundEffect.DEATH, "/sounds/death_0.wav");
}
private void load(SoundEffect id, String path) {
try (AudioInputStream ais = AudioSystem.getAudioInputStream(getClass().getResource(path))) {
Clip clip = AudioSystem.getClip();
clip.open(ais);
clips.put(id, clip);
} catch (Exception e) {
throw new RuntimeException("Failed to load sound: " + path, e);
}
}
public void play(SoundEffect id) {
Clip clip = clips.get(id);
if (clip == null) return;
if (clip.isRunning()) clip.stop();
clip.setFramePosition(0);
clip.start();
}
public void loop(SoundEffect id) {
Clip clip = clips.get(id);
if (clip == null) return;
if (!clip.isRunning()) {
clip.setFramePosition(0);
clip.loop(Clip.LOOP_CONTINUOUSLY);
}
}
public void stop(SoundEffect id) {
Clip clip = clips.get(id);
if (clip != null && clip.isRunning()) clip.stop();
}
// For dot munch alternation
public void playMunch() {
lastMunch = (lastMunch == SoundEffect.MUNCH1 ? SoundEffect.MUNCH2 : SoundEffect.MUNCH1);
play(lastMunch);
}
}

View File

@ -1,7 +1,11 @@
package se.urmo.game.state; package se.urmo.game.state;
import lombok.Getter;
import se.urmo.game.main.GamePanel; import se.urmo.game.main.GamePanel;
import se.urmo.game.main.GameStateManager;
import se.urmo.game.main.HighScoreManager;
import se.urmo.game.util.GameFonts; import se.urmo.game.util.GameFonts;
import se.urmo.game.util.GameStateType;
import java.awt.Color; import java.awt.Color;
import java.awt.Graphics2D; import java.awt.Graphics2D;
@ -11,6 +15,7 @@ public class GameOverState implements GameState {
private final GameStateManager gsm; private final GameStateManager gsm;
private int finalScore = 0; private int finalScore = 0;
private int finalLevel = 0; private int finalLevel = 0;
@Getter
private final HighScoreManager highScores; // optional private final HighScoreManager highScores; // optional
private boolean saved = false; private boolean saved = false;

View File

@ -1,7 +0,0 @@
package se.urmo.game.state;
public class HighScoreManager {
public void submit(int finalScore) {
}
}

View File

@ -5,22 +5,31 @@ import lombok.extern.slf4j.Slf4j;
import se.urmo.game.collision.CollisionChecker; import se.urmo.game.collision.CollisionChecker;
import se.urmo.game.collision.GhostCollisionChecker; import se.urmo.game.collision.GhostCollisionChecker;
import se.urmo.game.entities.ghost.Ghost; import se.urmo.game.entities.ghost.Ghost;
import se.urmo.game.entities.ghost.GhostMode; import se.urmo.game.entities.ghost.mode.GhostMode;
import se.urmo.game.entities.pacman.PacMan; import se.urmo.game.entities.pacman.PacMan;
import se.urmo.game.main.Game; 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.main.ScorePopupManager;
import se.urmo.game.map.GameMap; import se.urmo.game.map.GameMap;
import se.urmo.game.map.MapTile; import se.urmo.game.map.MapTile;
import se.urmo.game.map.TileType; import se.urmo.game.map.TileType;
import se.urmo.game.sound.SoundEffect;
import se.urmo.game.sound.SoundManager;
import se.urmo.game.util.Direction; import se.urmo.game.util.Direction;
import se.urmo.game.util.GameFonts; import se.urmo.game.util.GameFonts;
import se.urmo.game.util.GameStateType;
import java.awt.*; import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.KeyEvent; import java.awt.event.KeyEvent;
@Slf4j @Slf4j
public class PlayingState implements GameState { public class PlayingState implements GameState {
public static final int REMAINING_LIVES = 0;
private final Game game;
private final GameStateManager gameStateManager; private final GameStateManager gameStateManager;
private final GameOverState gameOverState; private final GameOverState gameOverState;
@ -29,12 +38,12 @@ public class PlayingState implements GameState {
private final FruitManager fruitManager; private final FruitManager fruitManager;
private final LevelManager levelManager; private final LevelManager levelManager;
private final AnimationManager animationManager; private final AnimationManager animationManager;
private PacMan pacman; private final PacMan pacman;
@Getter @Getter
private GameMap map; private final GameMap map;
// Durations (tune to taste) // Durations (tune to taste)
private static final int READY_MS = 1500; private static final int READY_MS = 3000;
private static final int LEVEL_COMPLETE_MS = 1500; private static final int LEVEL_COMPLETE_MS = 1500;
private static final int LIFE_LOST_MS = 2000; private static final int LIFE_LOST_MS = 2000;
@ -44,12 +53,16 @@ public class PlayingState implements GameState {
private int dotsEaten = 0; private int dotsEaten = 0;
// Phase + timers // Phase + timers
private RoundPhase phase = RoundPhase.PLAYING; private RoundPhase phase = RoundPhase.READY;
private long phaseStartMs = System.currentTimeMillis(); private long phaseStartMs = System.currentTimeMillis();
private boolean deathInProgress; private boolean deathInProgress;
public PlayingState(Game game, GameStateManager gameStateManager, GameOverState gameOverState) { private final ScorePopupManager scorePopups = new ScorePopupManager();
this.game = game; private final Font scorePopupFont = GameFonts.arcade(16F);
private final SoundManager sound = new SoundManager();
public PlayingState(GameStateManager gameStateManager, GameOverState gameOverState) {
this.gameStateManager = gameStateManager; this.gameStateManager = gameStateManager;
this.gameOverState = gameOverState; this.gameOverState = gameOverState;
this.map = new GameMap("maps/map1.csv"); this.map = new GameMap("maps/map1.csv");
@ -59,6 +72,7 @@ public class PlayingState implements GameState {
this.animationManager.register(pacman); this.animationManager.register(pacman);
this.ghostManager = new GhostManager(new GhostCollisionChecker(map), animationManager, levelManager); this.ghostManager = new GhostManager(new GhostCollisionChecker(map), animationManager, levelManager);
this.fruitManager = new FruitManager(levelManager); this.fruitManager = new FruitManager(levelManager);
sound.play(SoundEffect.START);
} }
@Override @Override
@ -67,10 +81,12 @@ public class PlayingState implements GameState {
case READY -> { case READY -> {
// Freeze everything during READY // Freeze everything during READY
if (phaseElapsed() >= READY_MS) { if (phaseElapsed() >= READY_MS) {
ghostManager.setMode(GhostMode.CHASE);
setPhase(RoundPhase.PLAYING); setPhase(RoundPhase.PLAYING);
} }
} }
case PLAYING -> { case PLAYING -> {
sound.loop(SoundEffect.SIREN);
animationManager.updateAll(); animationManager.updateAll();
pacman.update(); pacman.update();
ghostManager.update(pacman, map); ghostManager.update(pacman, map);
@ -88,20 +104,18 @@ public class PlayingState implements GameState {
case LIFE_LOST -> { case LIFE_LOST -> {
// Freeze, then reset round (keep dot state) // Freeze, then reset round (keep dot state)
if (phaseElapsed() >= LIFE_LOST_MS) { if (phaseElapsed() >= LIFE_LOST_MS) {
pacman.setState(PacMan.PacmanState.ALIVE);
deathInProgress = false; deathInProgress = false;
resetAfterLifeLost(); ghostManager.getGhosts().forEach(ghost -> ghost.requestModeChange(GhostMode.CHASE));
setPhase(RoundPhase.READY); setPhase(RoundPhase.READY);
if (lives <= 0) { if (lives <= 0) {
endGame(); endGame();
} }
} }
pacman.update();
} }
} }
} scorePopups.update();
private void resetAfterLifeLost() {
pacman.reset(); // to start tile, direction stopped
ghostManager.reset(); // to house
} }
private void advanceLevel() { private void advanceLevel() {
@ -109,7 +123,7 @@ public class PlayingState implements GameState {
map.reset(); map.reset();
ghostManager.reset(); ghostManager.reset();
fruitManager.reset(); fruitManager.reset();
pacman.reset(); pacman.setState(PacMan.PacmanState.ALIVE);
dotsEaten = 0; dotsEaten = 0;
} }
@ -130,6 +144,7 @@ public class PlayingState implements GameState {
ghostManager.setFrightMode(); ghostManager.setFrightMode();
} }
if (wasRemoved) { if (wasRemoved) {
sound.playMunch();
dotsEaten++; dotsEaten++;
fruitManager.dotEaten(dotsEaten); fruitManager.dotEaten(dotsEaten);
score += tile.getTileType().getScore(); score += tile.getTileType().getScore();
@ -142,15 +157,17 @@ public class PlayingState implements GameState {
@Override @Override
public void render(Graphics2D g) { public void render(Graphics2D g) {
map.draw(g); map.draw(g);
pacman.draw(g);
ghostManager.draw(g); ghostManager.draw(g);
pacman.draw(g);
fruitManager.draw(g); fruitManager.draw(g);
drawUI(g); drawUI(g);
scorePopups.draw(g, GameMap.OFFSET_X, GameMap.OFFSET_Y, scorePopupFont);
// Phase overlays // Phase overlays
switch (phase) { switch (phase) {
case READY -> drawCenterText(g, "READY!"); case READY -> drawCenterText(g, "READY!");
case LEVEL_COMPLETE -> drawCenterText(g, "LEVEL COMPLETE!"); case LEVEL_COMPLETE ->
drawCenterText(g, "LEVEL " + levelManager.getCurrentLevel().getLevel() + " COMPLETE!");
case LIFE_LOST -> drawCenterText(g, "LIFE LOST"); case LIFE_LOST -> drawCenterText(g, "LIFE LOST");
default -> { /* no overlay */ } default -> { /* no overlay */ }
} }
@ -173,6 +190,10 @@ public class PlayingState implements GameState {
g.drawString("Your Score", 48, 48); g.drawString("Your Score", 48, 48);
g.drawString("" + score, 48, 72); g.drawString("" + score, 48, 72);
// High Score (above map, right)
g.drawString("High Score", 248, 48);
g.drawString("" + gameOverState.getHighScores().top().stream().findFirst().orElse(0), 248, 72);
// Lives (below map, left) // Lives (below map, left)
for (int i = 1; i < lives; i++) { for (int i = 1; i < lives; i++) {
g.drawImage(pacman.getLifeIcon(), g.drawImage(pacman.getLifeIcon(),
@ -206,15 +227,22 @@ public class PlayingState implements GameState {
private void checkCollisions() { private void checkCollisions() {
for (Ghost ghost : ghostManager.getGhosts()) { for (Ghost ghost : ghostManager.getGhosts()) {
if (deathInProgress) return; // guard if (deathInProgress) return; // guard
//if(overlap(pacman, ghost) if (overlaps(pacman, ghost)) {
double dist = pacman.distanceTo(ghost.getPosition()); //double dist = pacman.distanceTo(ghost.getPosition().asPoint());
if (dist < GameMap.MAP_TILESIZE / 2.0) { //if (dist < GameMap.MAP_TILESIZE / 2.0) {
if (ghost.isEaten()) return;
if (ghost.isFrightened()) { if (ghost.isFrightened()) {
// Pac-Man eats ghost log.debug("Pacman eats ghost");
score += 200; sound.play(SoundEffect.GHOST_EATEN);
ghost.resetPosition(); int pts = 200 * (1 << (ghostManager.getGhosts().size() - ghostManager.isFrightened()));
ghost.setMode(GhostMode.CHASE); // end frightend score += pts;
scorePopups.spawn(pts);
ghost.setMode(GhostMode.EATEN);
} else { } else {
log.debug("Pacman loses a life");
sound.play(SoundEffect.DEATH);
ghostManager.setFrozen(true);
pacman.setState(PacMan.PacmanState.DYING);
deathInProgress = true; deathInProgress = true;
// Pac-Man loses a life // Pac-Man loses a life
lives--; lives--;
@ -224,13 +252,13 @@ public class PlayingState implements GameState {
} }
} }
// private boolean overlaps(PacMan p, Ghost g) { private boolean overlaps(PacMan p, Ghost g) {
// // center-distance or AABB; center distance keeps the arcade feel // center-distance or AABB; center distance keeps the arcade feel
// double dx = p.getCenterX() - g.getCenterX(); double dx = p.getPosition().x - g.getPosition().x;
// double dy = p.getCenterY() - g.getCenterY(); double dy = p.getPosition().y - g.getPosition().y;
// double r = map.getTileSize() * 0.45; // tune threshold double r = GameMap.MAP_TILESIZE * 1.0; // tune threshold
// return (dx*dx + dy*dy) <= r*r; return (dx * dx + dy * dy) <= r * r;
// } }
private void endGame() { private void endGame() {
gameOverState.setScore(score); gameOverState.setScore(score);
@ -239,6 +267,7 @@ public class PlayingState implements GameState {
} }
public void setScore(int score) { public void setScore(int score) {
scorePopups.spawn(score);
this.score += score; this.score += score;
} }
} }

View File

@ -1,18 +1,20 @@
package se.urmo.game.util; package se.urmo.game.util;
public enum Direction { public enum Direction {
RIGHT(1, 0), RIGHT(1, 0, 0),
LEFT(-1, 0), LEFT(-1, 0 , 180),
DOWN(0, 1), DOWN(0, 1, 90),
UP(0, -1), UP(0, -1, 270),
NONE(0, 0); NONE(0, 0, 0);
public final int dx; public final int dx;
public final int dy; public final int dy;
public final int angel;
Direction(int dx, int dy) { Direction(int dx, int dy, int angel) {
this.dx = dx; this.dx = dx;
this.dy = dy; this.dy = dy;
this.angel = angel;
} }
public Direction opposite() { public Direction opposite() {

View File

@ -1,4 +1,4 @@
package se.urmo.game.state; package se.urmo.game.util;
import lombok.Getter; import lombok.Getter;
import se.urmo.game.graphics.SpriteLocation; import se.urmo.game.graphics.SpriteLocation;

View File

@ -1,4 +1,4 @@
package se.urmo.game.state; package se.urmo.game.util;
public enum GameStateType { public enum GameStateType {
PLAYING, GAME_OVER, PLAYING, GAME_OVER,

View File

@ -1,5 +1,7 @@
package se.urmo.game.util; package se.urmo.game.util;
import java.awt.Point;
public class MyPoint { public class MyPoint {
public final double x; public final double x;
public final double y; public final double y;
@ -9,6 +11,10 @@ public class MyPoint {
this.y = y; this.y = y;
} }
public Point asPoint() {
return new Point((int) x, (int) y);
}
@Override @Override
public String toString() { public String toString() {
return "MyPoint{" + return "MyPoint{" +

View File

@ -5,7 +5,14 @@
</encoder> </encoder>
</appender> </appender>
<root level="DEBUG"> <!--root level="DEBUG">
<appender-ref ref="STDOUT" /> <appender-ref ref="STDOUT" />
</root> </root -->
<root level="OFF"/>
<!-- Enable entire package -->
<logger name="se.urmo.myapp" level="DEBUG" additivity="false">
<appender-ref ref="STDOUT"/>
</logger>
</configuration> </configuration>

View File

@ -11,7 +11,7 @@
99,99,99,99,99,22, 0,15,17,99,99,99,99,99,99,99,99,99,99,15,17, 0,20,99,99,99,99,99 99,99,99,99,99,22, 0,15,17,99,99,99,99,99,99,99,99,99,99,15,17, 0,20,99,99,99,99,99
99,99,99,99,99,22, 0,15,17,99,34,35,36,37,38,35,35,39,99,15,17, 0,20,99,99,99,99,99 99,99,99,99,99,22, 0,15,17,99,34,35,36,37,38,35,35,39,99,15,17, 0,20,99,99,99,99,99
32,32,32,32,32,33, 0,26,28,99,40,99,99,99,99,99,99,41,99,26,28, 0,31,32,32,32,32,32 32,32,32,32,32,33, 0,26,28,99,40,99,99,99,99,99,99,41,99,26,28, 0,31,32,32,32,32,32
0, 0, 0, 0, 0, 0, 0, 0, 0,99,40,99,99,99,99,99,99,41,99, 0, 0, 0, 0, 0, 0, 0, 0, 0 57,57,57,57,57,57, 0, 0, 0,99,40,99,99,99,99,99,99,41,99, 0, 0,57,57,57,57,57,57,57
10,10,10,10,10,11, 0, 4, 6,99,40,99,99,99,99,99,99,41,99, 4, 6, 0, 9,10,10,10,10,10 10,10,10,10,10,11, 0, 4, 6,99,40,99,99,99,99,99,99,41,99, 4, 6, 0, 9,10,10,10,10,10
99,99,99,99,99,22, 0,15,17,99,42,43,43,43,43,43,43,44,99,15,17, 0,20,99,99,99,99,99 99,99,99,99,99,22, 0,15,17,99,42,43,43,43,43,43,43,44,99,15,17, 0,20,99,99,99,99,99
99,99,99,99,99,22, 0,15,17,99,99,99,99,99,99,99,99,99,99,15,17, 0,20,99,99,99,99,99 99,99,99,99,99,22, 0,15,17,99,99,99,99,99,99,99,99,99,99,15,17, 0,20,99,99,99,99,99
@ -25,6 +25,6 @@
49,27,28, 0,26,28, 0,15,17, 0,26,27,27, 7, 8,27,27,28, 0,15,17, 0,26,28, 0,26,27,50 49,27,28, 0,26,28, 0,15,17, 0,26,27,27, 7, 8,27,27,28, 0,15,17, 0,26,28, 0,26,27,50
12, 0, 0, 0, 0, 0, 0,15,17, 0, 0, 0, 0,15,17, 0, 0, 0, 0,15,17, 0, 0, 0, 0, 0, 0,14 12, 0, 0, 0, 0, 0, 0,15,17, 0, 0, 0, 0,15,17, 0, 0, 0, 0,15,17, 0, 0, 0, 0, 0, 0,14
12, 0, 4, 5, 5, 5, 5,18,19, 5, 5, 6, 0,15,17, 0, 4, 5, 5,18,19, 5, 5, 5, 5, 6, 0,14 12, 0, 4, 5, 5, 5, 5,18,19, 5, 5, 6, 0,15,17, 0, 4, 5, 5,18,19, 5, 5, 5, 5, 6, 0,14
12, 0,26,27,27,27,27,27,27,27,27,28, 0,26,28, 0,26,27,27,27,27,27,27,27,27,28, 0,14 12,56,26,27,27,27,27,27,27,27,27,28, 0,26,28, 0,26,27,27,27,27,27,27,27,27,28,56,14
12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,14 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,14
23,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,25 23,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,25
1 1 2 2 2 2 2 2 2 2 2 2 2 2 45 46 2 2 2 2 2 2 2 2 2 2 2 2 3
11 99 99 99 99 99 22 0 15 17 99 99 99 99 99 99 99 99 99 99 15 17 0 20 99 99 99 99 99
12 99 99 99 99 99 22 0 15 17 99 34 35 36 37 38 35 35 39 99 15 17 0 20 99 99 99 99 99
13 32 32 32 32 32 33 0 26 28 99 40 99 99 99 99 99 99 41 99 26 28 0 31 32 32 32 32 32
14 0 57 0 57 0 57 0 57 0 57 0 57 0 0 0 99 40 99 99 99 99 99 99 41 99 0 0 0 57 0 57 0 57 0 57 0 57 0 57 0 57
15 10 10 10 10 10 11 0 4 6 99 40 99 99 99 99 99 99 41 99 4 6 0 9 10 10 10 10 10
16 99 99 99 99 99 22 0 15 17 99 42 43 43 43 43 43 43 44 99 15 17 0 20 99 99 99 99 99
17 99 99 99 99 99 22 0 15 17 99 99 99 99 99 99 99 99 99 99 15 17 0 20 99 99 99 99 99
25 49 27 28 0 26 28 0 15 17 0 26 27 27 7 8 27 27 28 0 15 17 0 26 28 0 26 27 50
26 12 0 0 0 0 0 0 15 17 0 0 0 0 15 17 0 0 0 0 15 17 0 0 0 0 0 0 14
27 12 0 4 5 5 5 5 18 19 5 5 6 0 15 17 0 4 5 5 18 19 5 5 5 5 6 0 14
28 12 0 56 26 27 27 27 27 27 27 27 27 28 0 26 28 0 26 27 27 27 27 27 27 27 27 28 0 56 14
29 12 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 14
30 23 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 25

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.