diff --git a/src/main/java/se/urmo/game/entities/ScorePopup.java b/src/main/java/se/urmo/game/entities/ScorePopup.java new file mode 100644 index 0000000..375e521 --- /dev/null +++ b/src/main/java/se/urmo/game/entities/ScorePopup.java @@ -0,0 +1,58 @@ +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.drawString(text, x, baseline); + + g.setComposite(oldComp); + g.setFont(oldFont); + } +} diff --git a/src/main/java/se/urmo/game/main/ScorePopupManager.java b/src/main/java/se/urmo/game/main/ScorePopupManager.java new file mode 100644 index 0000000..1678681 --- /dev/null +++ b/src/main/java/se/urmo/game/main/ScorePopupManager.java @@ -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 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(); + } +} diff --git a/src/main/java/se/urmo/game/state/PlayingState.java b/src/main/java/se/urmo/game/state/PlayingState.java index 775b580..72babeb 100644 --- a/src/main/java/se/urmo/game/state/PlayingState.java +++ b/src/main/java/se/urmo/game/state/PlayingState.java @@ -12,6 +12,7 @@ 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.MapTile; import se.urmo.game.map.TileType; @@ -20,6 +21,7 @@ import se.urmo.game.util.GameFonts; import se.urmo.game.util.GameStateType; import java.awt.Color; +import java.awt.Font; import java.awt.Graphics2D; import java.awt.Point; import java.awt.event.KeyEvent; @@ -54,6 +56,9 @@ public class PlayingState implements GameState { private boolean deathInProgress; private int frightMultiplier; + private final ScorePopupManager scorePopups = new ScorePopupManager(); + private final Font scorePopupFont = GameFonts.arcade(16F); // or your arcade font + public PlayingState(GameStateManager gameStateManager, GameOverState gameOverState) { this.gameStateManager = gameStateManager; this.gameOverState = gameOverState; @@ -105,6 +110,7 @@ public class PlayingState implements GameState { pacman.update(); } } + scorePopups.update(); } private void advanceLevel() { @@ -150,6 +156,7 @@ public class PlayingState implements GameState { pacman.draw(g); fruitManager.draw(g); drawUI(g); + scorePopups.draw(g, GameMap.OFFSET_X, GameMap.OFFSET_Y, scorePopupFont); // Phase overlays switch (phase) { @@ -221,7 +228,9 @@ public class PlayingState implements GameState { if (ghost.isEaten()) return; if (ghost.isFrightened()) { log.debug("Pacman eats ghost"); - score += 200 * (1 << (ghostManager.getGhosts().size() - ghostManager.isFrightened())); + int pts = 200 * (1 << (ghostManager.getGhosts().size() - ghostManager.isFrightened())); + score += pts; + scorePopups.spawn(pts); ghost.setMode(GhostMode.EATEN); } else { log.debug("Pacman loses a life"); @@ -251,6 +260,7 @@ public class PlayingState implements GameState { } public void setScore(int score) { + scorePopups.spawn(score); this.score += score; } }