Compare commits

..

2 Commits

Author SHA1 Message Date
63b2b7ae68 Adding fruits 2025-08-30 08:45:43 +02:00
23fce8dded Adding Ghost to SpriteLocation 2025-08-29 15:24:30 +02:00
14 changed files with 206 additions and 58 deletions

View File

@ -0,0 +1,45 @@
package se.urmo.game.entities;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import se.urmo.game.map.GameMap;
import se.urmo.game.state.FruitType;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
@Slf4j
public class Fruit {
private final Point position;
private final BufferedImage sprite;
@Getter
private final int score;
private final long spawnTime;
private final long lifetimeMs = 9000; // ~9 seconds
public Fruit(FruitType type) {
this.position = new Point(GameMap.colToScreen(13), GameMap.rowToScreen(16)); ;
this.sprite = type.getSprite();
this.score = type.getScore();
this.spawnTime = System.currentTimeMillis();
}
public void draw(Graphics g) {
g.drawImage(sprite, position.x + GameMap.MAP_TILESIZE / 2, position.y, null);
}
public boolean isExpired() {
return System.currentTimeMillis() - spawnTime > lifetimeMs;
}
public boolean collidesWith(PacMan pacman) {
//return pacman.distanceTo(position) < GameMap.MAP_TILESIZE / 2.0;
Rectangle pacmanBounds = pacman.getBounds();
Rectangle fruitBounds = new Rectangle(position.x, position.y, sprite.getWidth(), sprite.getHeight());
return pacmanBounds.intersects(fruitBounds);
}
}

View File

@ -3,6 +3,8 @@ package se.urmo.game.entities;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import se.urmo.game.collision.GhostCollisionChecker;
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 se.urmo.game.state.GhostManager;
@ -52,12 +54,13 @@ public class Ghost {
public Ghost(GhostCollisionChecker collisionChecker, GhostStrategy strategy, GhostStrategy scaterStrategy, BufferedImage[] animation, BufferedImage[] fearAnimation) {
public Ghost(GhostCollisionChecker collisionChecker, GhostStrategy strategy, GhostStrategy scaterStrategy, int animation) {
this.collisionChecker = collisionChecker;
this.chaseStrategy = strategy;
this.scaterStrategy = scaterStrategy;
this.baseAnimation = animation;
this.fearAnimation = fearAnimation;
this.baseAnimation = SpriteSheetManager.get(SpriteLocation.GHOST).getAnimation(animation);;
this.fearAnimation = SpriteSheetManager.get(SpriteLocation.GHOST).getAnimation(8);;
position = new Point(
13 * GameMap.MAP_TILESIZE + GameMap.OFFSET_X + (GameMap.MAP_TILESIZE / 2),
4 * GameMap.MAP_TILESIZE + GameMap.OFFSET_Y + (GameMap.MAP_TILESIZE / 2) );

View File

@ -76,7 +76,7 @@ public class PacMan {
position.y - PACMAN_OFFSET,
PACMAN_SIZE,
PACMAN_SIZE, null);
g.drawImage(COLLISION_BOX, position.x - COLLISION_BOX_OFFSET, position.y - COLLISION_BOX_OFFSET, COLLISION_BOX_SIZE, COLLISION_BOX_SIZE, null);
//g.drawImage(COLLISION_BOX, position.x - COLLISION_BOX_OFFSET, position.y - COLLISION_BOX_OFFSET, COLLISION_BOX_SIZE, COLLISION_BOX_SIZE, null);
//g.setColor(Color.BLUE);
//g.fillRect(position.x-1, position.y-1, 3, 3);
}
@ -129,4 +129,8 @@ public class PacMan {
public Image getLifeIcon() {
return movmentImages[0][1];
}
public Rectangle getBounds() {
return new Rectangle(position.x - COLLISION_BOX_OFFSET, position.y - COLLISION_BOX_OFFSET, COLLISION_BOX_SIZE, COLLISION_BOX_SIZE);
}
}

View File

@ -1,15 +1,18 @@
package se.urmo.game.map;
package se.urmo.game.graphics;
import lombok.Getter;
import se.urmo.game.entities.Ghost;
import se.urmo.game.map.GameMap;
import se.urmo.game.util.LoadSave;
import java.awt.image.BufferedImage;
@Getter
public enum SpriteLocation {
MAP("sprites/PacMan-custom-spritemap-0-3.png", 5, 11),
ITEM("sprites/PacManAssets-Items.png", 2, 8),
NONE("", 0, 0) { // Special case for tiles without sprites
MAP("sprites/PacMan-custom-spritemap-0-3.png", 5, 11, GameMap.MAP_TILESIZE),
ITEM("sprites/PacManAssets-Items.png", 2, 8, GameMap.MAP_TILESIZE),
GHOST("sprites/PacManAssets-Ghosts.png", 11, 4, Ghost.GHOST_SIZE),
NONE("", 0, 0, 0) { // Special case for tiles without sprites
@Override
public BufferedImage[][] loadSprites(int tileSize) {
return new BufferedImage[][] {{ null }}; // Single null sprite
@ -19,11 +22,13 @@ public enum SpriteLocation {
private final String path;
private final int rows;
private final int cols;
private final int size;
SpriteLocation(String path, int rows, int cols) {
SpriteLocation(String path, int rows, int cols, int size) {
this.path = path;
this.rows = rows;
this.cols = cols;
this.size = size;
}
public BufferedImage[][] loadSprites(int tileSize) {

View File

@ -1,6 +1,7 @@
package se.urmo.game.map;
package se.urmo.game.graphics;
import lombok.Getter;
import java.awt.image.BufferedImage;
@ -11,15 +12,7 @@ public class SpriteSheet {
public SpriteSheet(SpriteLocation location) {
this.location = location;
this.spriteSheet = location.loadSprites(GameMap.MAP_TILESIZE);
}
public BufferedImage getSprite(TileType tileType) {
if (tileType.getSpriteSheet() != location) {
throw new IllegalArgumentException("Tile type belongs to different sprite sheet");
}
// NONE will always return null without additional checks
return getSprite(tileType.getRow(), tileType.getCol());
this.spriteSheet = location.loadSprites(location.getSize());
}
public BufferedImage getSprite(int row, int col) {
@ -31,4 +24,12 @@ public class SpriteSheet {
}
return spriteSheet[row][col];
}
public BufferedImage[] getAnimation(int row) {
if(location == SpriteLocation.NONE) return null;
if(row>= spriteSheet.length) return null;
return spriteSheet[row];
}
}

View File

@ -1,4 +1,6 @@
package se.urmo.game.map;
package se.urmo.game.graphics;
import se.urmo.game.map.GameMap;
import java.util.EnumMap;
import java.util.Map;

View File

@ -1,6 +1,7 @@
package se.urmo.game.map;
import lombok.extern.slf4j.Slf4j;
import se.urmo.game.graphics.SpriteSheetManager;
import se.urmo.game.util.Direction;
import java.awt.*;
@ -31,28 +32,26 @@ public class GameMap {
BufferedReader br = new BufferedReader(new InputStreamReader(Objects.requireNonNull(is)))) {
String line;
int rowIndex = 0;
while ((line = br.readLine()) != null && rowIndex < mapRowSize) {
int row = 0;
while ((line = br.readLine()) != null && row < mapRowSize) {
String[] tokens = line.split(",");
if (tokens.length != mapColSize) {
throw new IllegalStateException(
"Invalid map format: row " + rowIndex + " has " + tokens.length +
"Invalid map format: row " + row + " has " + tokens.length +
" columns, expected " + mapColSize
);
}
for (int col = 0; col < mapColSize; col++) {
int value = Integer.parseInt(tokens[col].trim());
TileType type = TileType.fromValue(value);
BufferedImage sprite = SpriteSheetManager.get(type.getSpriteSheet()).getSprite(type);
data[rowIndex][col] = new MapTile(sprite, type);
data[row][col] = MapTile.byType(TileType.fromValue(value));
}
rowIndex++;
row++;
}
if (rowIndex != mapRowSize) {
if (row != mapRowSize) {
throw new IllegalStateException(
"Invalid map format: found " + rowIndex + " rows, expected " + mapRowSize
"Invalid map format: found " + row + " rows, expected " + mapRowSize
);
}
@ -78,11 +77,6 @@ public class GameMap {
}
}
private BufferedImage getSprite(int value) {
TileType type = TileType.fromValue(value);
return SpriteSheetManager.get(type.getSpriteSheet()).getSprite(type);
}
public boolean isPassable(List<Point> list){
return list.stream().allMatch(p -> isPassable(p.x, p.y));
}
@ -189,6 +183,13 @@ public class GameMap {
return (screenY - OFFSET_Y) / MAP_TILESIZE;
}
public static int colToScreen(int col) {
return col * MAP_TILESIZE + OFFSET_X;
}
public static int rowToScreen(int row) {
return row * MAP_TILESIZE + OFFSET_Y;
}
public MapTile getTile(Point screenPos) {
int r = screenToRow(screenPos);
int c = screenToCol(screenPos);

View File

@ -2,6 +2,7 @@ package se.urmo.game.map;
import lombok.Getter;
import lombok.Setter;
import se.urmo.game.graphics.SpriteSheetManager;
import java.awt.image.BufferedImage;
@ -12,12 +13,19 @@ public class MapTile {
private BufferedImage image;
private final boolean[][] collisionMask;
public MapTile(BufferedImage image, TileType tileType) {
this.tileType = tileType;
public MapTile(BufferedImage image, TileType type) {
this.tileType = type;
this.image = image;
this.collisionMask = tileType.isSolid() ? createCollisionMask(image) : null;
}
public static MapTile byType(TileType type) {
BufferedImage img = SpriteSheetManager
.get(type.getSpriteSheet())
.getSprite(type.getRow(), type.getCol()); // row and col in sprite - not in map!
return new MapTile(img, type) ;
}
private boolean[][] createCollisionMask(BufferedImage img) {
if(img == null) return null;
int w = img.getWidth();

View File

@ -1,6 +1,7 @@
package se.urmo.game.map;
import lombok.Getter;
import se.urmo.game.graphics.SpriteLocation;
@Getter
public enum TileType {

View File

@ -0,0 +1,46 @@
package se.urmo.game.state;
import lombok.extern.slf4j.Slf4j;
import se.urmo.game.entities.Fruit;
import se.urmo.game.entities.PacMan;
import java.awt.Graphics;
@Slf4j
public class FruitManager {
private Fruit activeFruit;
private int dotsEaten = 0;
public void dotEaten() {
dotsEaten++;
if (dotsEaten == 10 || dotsEaten == 170) {
spawnFruit(1);
}
}
private void spawnFruit(int level) {
FruitType type = FruitType.forLevel(level);
activeFruit = new Fruit(type);
}
public void update(PacMan pacman, PlayingState playingState) {
if (activeFruit != null) {
if (activeFruit.isExpired()) {
activeFruit = null;
return;
}
if (activeFruit.collidesWith(pacman)) {
playingState.setScore(activeFruit.getScore());
activeFruit = null;
}
}
}
public void draw(Graphics g) {
if (activeFruit != null) {
activeFruit.draw(g);
}
}
}

View File

@ -0,0 +1,29 @@
package se.urmo.game.state;
import lombok.Getter;
import se.urmo.game.graphics.SpriteLocation;
import se.urmo.game.graphics.SpriteSheetManager;
import java.awt.image.BufferedImage;
import java.util.Arrays;
@Getter
public enum FruitType {
CHERRY(1, SpriteSheetManager.get(SpriteLocation.ITEM).getSprite(0,0), 100);
private final int level;
private final BufferedImage sprite;
private final int score;
FruitType(int level, BufferedImage sprite, int score) {
this.level = level;
this.sprite = sprite;
this.score = score;
}
public static FruitType forLevel(int i) {
return Arrays.stream(values())
.filter(fruitType -> fruitType.level ==i)
.findFirst().orElse(null);
}
}

View File

@ -26,10 +26,12 @@ import java.util.List;
public class GhostManager {
public static final int SPRITE_SHEET_ROWS = 10;
public static final int MAX_SPRITE_FRAMES = 4;
public static final int BLINKY_ANIMATION = 0;
public static final int PINKY_ANIMATION = 2;
public static final int INKY_ANIMATION = 1;
public static final int CLYDE_ANIMATION = 3;
@Getter
private final List<Ghost> ghosts = new ArrayList<>();
private BufferedImage[][] image;
private GhostMode globalMode;
private long lastModeSwitchTime;
private int phaseIndex = 0;
@ -43,31 +45,17 @@ public class GhostManager {
};
public GhostManager(GhostCollisionChecker ghostCollisionChecker) {
loadAnimation();
// Create ghosts with their strategies
Ghost blinky = new Ghost(ghostCollisionChecker, new BlinkyStrategy(), new ScatterToTopRight(), image[0], image[9]);
Ghost blinky = new Ghost(ghostCollisionChecker, new BlinkyStrategy(), new ScatterToTopRight(), BLINKY_ANIMATION);
ghosts.add(blinky);
//ghosts.add(new Ghost(ghostCollisionChecker, new PinkyStrategy(),new ScatterToTopLeft(), image[2], image[9]));
//ghosts.add(new Ghost(ghostCollisionChecker,new InkyStrategy(blinky), new ScatterToBottomRight(), image[1], image[9]));
//ghosts.add(new Ghost(ghostCollisionChecker, new ClydeStrategy(), new ScatterToBottomLeft(), image[3], image[8]));
// ghosts.add(new Ghost(ghostCollisionChecker, new PinkyStrategy(), new ScatterToTopLeft(), PINKY_ANIMATION));
// ghosts.add(new Ghost(ghostCollisionChecker,new InkyStrategy(blinky), new ScatterToBottomRight(), INKY_ANIMATION));
// ghosts.add(new Ghost(ghostCollisionChecker, new ClydeStrategy(), new ScatterToBottomLeft(), CLYDE_ANIMATION));
setMode(GhostMode.CHASE);
}
private void loadAnimation() {
image = new BufferedImage[SPRITE_SHEET_ROWS][MAX_SPRITE_FRAMES];
BufferedImage img = LoadSave.GetSpriteAtlas("sprites/PacManAssets-Ghosts.png");
for (int row = 0; row < SPRITE_SHEET_ROWS; row++) {
for (int col = 0; col < MAX_SPRITE_FRAMES; col++) {
image[row][col] = img.getSubimage(Ghost.GHOST_SIZE * col, Ghost.GHOST_SIZE * row, Ghost.GHOST_SIZE, Ghost.GHOST_SIZE);
}
}
}
public void setMode(GhostMode mode) {
this.globalMode = mode;
log.debug("Mode changed to {}", globalMode);
log.debug("Mode changed to {}", mode);
for (Ghost g : ghosts) {
g.setMode(mode);
}

View File

@ -0,0 +1,4 @@
package se.urmo.game.state;
public class LevelManager {
}

View File

@ -23,6 +23,8 @@ public class PlayingState implements GameState {
private final GameStateManager gameStateManager;
private final GhostManager ghostManager;
private final Font arcadeFont;
private final FruitManager fruitManager;
private final LevelManager levelManager;
private PacMan pacman;
@Getter
private GameMap map;
@ -35,6 +37,8 @@ public class PlayingState implements GameState {
this.map = new GameMap("maps/map1.csv");
this.pacman = new PacMan(game, new CollisionChecker(map));
this.ghostManager = new GhostManager(new GhostCollisionChecker(map));
this.fruitManager = new FruitManager();
this.levelManager = new LevelManager();
this.arcadeFont = loadArcadeFont();
}
@ -42,6 +46,7 @@ public class PlayingState implements GameState {
public void update() {
pacman.update();
ghostManager.update(pacman, map);
fruitManager.update(pacman, this);
checkCollisions();
handleDots();
}
@ -54,6 +59,7 @@ public class PlayingState implements GameState {
ghostManager.setFrightMode();
}
if(wasRemoved){
fruitManager.dotEaten();
score+=tile.getTileType().getScore();
}
}
@ -63,6 +69,7 @@ public class PlayingState implements GameState {
map.draw(g);
pacman.draw(g);
ghostManager.draw(g);
fruitManager.draw(g);
drawUI(g);
}
@ -129,4 +136,8 @@ public class PlayingState implements GameState {
return new Font("Monospaced", Font.BOLD, 16);
}
}
public void setScore(int score) {
this.score += score;
}
}