feat(barn-defense): upgrade to retina/DPR template patterns

Upgrade the barn-defense example to use the new high-DPI rendering
architecture from the Phaser 2D template, ensuring crisp rendering on
retina displays and proper responsive scaling across all screen sizes.

Key changes:
- Constants.js: Add DPR/PX canvas sizing, scale all spatial values
  (sizes, speeds, ranges, radii) by PX factor while preserving game
  logic values (HP, damage, costs, durations)
- GameConfig.js: Add Scale.FIT, CENTER_BOTH, zoom: 1/DPR, roundPixels,
  antialias, preserveDrawingBuffer
- index.html: Responsive viewport (max-scale=1, no user-scale),
  fullscreen game container
- main.js: Add render_game_to_text() and advanceTime(ms) test hooks
- PixelRenderer.js: Round canvas dimensions for fractional PX scales,
  use floor/ceil pixel rendering to avoid sub-pixel gaps
- All sprites (tiles, enemies, towers, projectiles): Scale by PX
- All scenes: PX-scale hardcoded offsets for text, buttons, particles
- All UI components: PX-scale button sizes, spacing, fonts, info panels
- All systems: PX-scale particle effects, path markers, barn drawing
- Grid math preserved: TILE_SIZE = 40*PX, GRID_COLS/ROWS unchanged

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
rshtirmer
2026-02-23 16:52:16 -05:00
parent 3142f9ddcd
commit 695a6ebe84
19 changed files with 357 additions and 252 deletions
+3 -3
View File
@@ -2,12 +2,12 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>Barn Defense</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #000; display: flex; justify-content: center; align-items: center; min-height: 100vh; overflow: hidden; }
#game-container { width: 800px; height: 600px; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; }
#game-container { width: 100%; height: 100%; }
</style>
</head>
<body>
+134 -94
View File
@@ -2,15 +2,48 @@
// Barn Defense - Constants
// All magic numbers, colors, timing, and configuration values.
// Zero hardcoded values in game logic.
//
// DPR / PX scaling: canvas is sized to the device-pixel area so the game
// renders at native resolution. PX = canvasWidth / designWidth is the
// universal scale factor for all spatial values (sizes, speeds, physics).
// =============================================================================
// --- DPR / Canvas sizing (matches phaser-2d template) ---
export const DPR = Math.min(window.devicePixelRatio || 1, 2);
const _designW = 800;
const _designH = 600;
const _designAspect = _designW / _designH;
const _deviceW = window.innerWidth * DPR;
const _deviceH = window.innerHeight * DPR;
let _canvasW, _canvasH;
if (_deviceW / _deviceH > _designAspect) {
// Device is wider than design -- pillarbox
_canvasH = _deviceH;
_canvasW = Math.round(_deviceH * _designAspect);
} else {
// Device is taller than design -- letterbox
_canvasW = _deviceW;
_canvasH = Math.round(_deviceW / _designAspect);
}
export const PX = _canvasW / _designW;
// --- Game dimensions ---
export const GAME = {
WIDTH: 800,
HEIGHT: 600,
TILE_SIZE: 40,
WIDTH: _canvasW,
HEIGHT: _canvasH,
TILE_SIZE: Math.round(40 * PX),
GRID_COLS: 20,
GRID_ROWS: 15,
DELTA_CAP: 33.33, // cap at ~30fps worth of delta
DELTA_CAP: 33.33, // cap at ~30fps worth of delta (time value, not spatial)
};
// --- Safe zone ---
export const SAFE_ZONE = {
TOP: GAME.HEIGHT * 0.08,
};
// Tile types for the map grid
@@ -96,70 +129,75 @@ export const COLORS = {
};
// Enemy configuration
// hp, reward, jumpDistance are game-logic values -- DO NOT scale.
// speed and size are spatial -- scale by PX.
export const ENEMIES = {
CHICKEN: {
key: 'chicken',
name: 'Chicken',
hp: 30,
speed: 100,
speed: 100 * PX,
reward: 5,
size: 10,
size: 10 * PX,
color: 0xffdd00,
},
PIG: {
key: 'pig',
name: 'Pig',
hp: 80,
speed: 60,
speed: 60 * PX,
reward: 15,
size: 14,
size: 14 * PX,
color: 0xffaacc,
},
COW: {
key: 'cow',
name: 'Cow',
hp: 200,
speed: 35,
speed: 35 * PX,
reward: 30,
size: 18,
size: 18 * PX,
color: 0xffffff,
},
GOAT: {
key: 'goat',
name: 'Goat',
hp: 90,
speed: 65,
speed: 65 * PX,
reward: 20,
size: 13,
size: 13 * PX,
color: 0xaaaaaa,
canJump: true,
jumpChance: 0.15,
jumpDistance: 2, // skips 2 waypoints
jumpDistance: 2, // game-logic, not spatial
},
BULL: {
key: 'bull',
name: 'Bull',
hp: 600,
speed: 25,
speed: 25 * PX,
reward: 100,
size: 22,
size: 22 * PX,
color: 0x882222,
},
};
// Tower configuration
// cost, damage, fireRate, slowDuration, slowAmount, upgrade multipliers, maxLevel
// are game-logic values -- DO NOT scale.
// range, projectileSpeed, projectileSize, splashRadius are spatial -- scale by PX.
export const TOWERS = {
SCARECROW: {
key: 'scarecrow',
name: 'Scarecrow',
cost: 50,
damage: 8,
range: 100,
fireRate: 800, // ms between shots
projectileSpeed: 250,
range: 100 * PX,
fireRate: 800,
projectileSpeed: 250 * PX,
color: 0x8b6914,
projectileColor: 0xccaa44,
projectileSize: 4,
projectileSize: 4 * PX,
upgradeCostMultiplier: 1.5,
upgradeDamageMultiplier: 1.4,
upgradeRangeMultiplier: 1.15,
@@ -171,12 +209,12 @@ export const TOWERS = {
name: 'Pitchfork',
cost: 100,
damage: 25,
range: 150,
range: 150 * PX,
fireRate: 1400,
projectileSpeed: 350,
projectileSpeed: 350 * PX,
color: 0x888888,
projectileColor: 0xaaaaaa,
projectileSize: 5,
projectileSize: 5 * PX,
upgradeCostMultiplier: 1.5,
upgradeDamageMultiplier: 1.4,
upgradeRangeMultiplier: 1.1,
@@ -188,14 +226,14 @@ export const TOWERS = {
name: 'Corn Cannon',
cost: 150,
damage: 40,
range: 90,
range: 90 * PX,
fireRate: 1800,
projectileSpeed: 200,
projectileSpeed: 200 * PX,
color: 0xddcc00,
projectileColor: 0xffee44,
projectileSize: 6,
projectileSize: 6 * PX,
splash: true,
splashRadius: 50,
splashRadius: 50 * PX,
upgradeCostMultiplier: 1.6,
upgradeDamageMultiplier: 1.5,
upgradeRangeMultiplier: 1.1,
@@ -207,15 +245,15 @@ export const TOWERS = {
name: 'Sprinkler',
cost: 75,
damage: 4,
range: 110,
range: 110 * PX,
fireRate: 600,
projectileSpeed: 200,
projectileSpeed: 200 * PX,
color: 0x4488ff,
projectileColor: 0x66aaff,
projectileSize: 3,
projectileSize: 3 * PX,
slowEffect: true,
slowAmount: 0.5, // multiplier on enemy speed
slowDuration: 2000, // ms
slowAmount: 0.5,
slowDuration: 2000,
upgradeCostMultiplier: 1.4,
upgradeDamageMultiplier: 1.3,
upgradeRangeMultiplier: 1.15,
@@ -227,12 +265,12 @@ export const TOWERS = {
name: 'Tractor',
cost: 300,
damage: 35,
range: 130,
range: 130 * PX,
fireRate: 500,
projectileSpeed: 400,
projectileSpeed: 400 * PX,
color: 0x44aa44,
projectileColor: 0x88cc88,
projectileSize: 5,
projectileSize: 5 * PX,
upgradeCostMultiplier: 1.8,
upgradeDamageMultiplier: 1.5,
upgradeRangeMultiplier: 1.1,
@@ -244,57 +282,57 @@ export const TOWERS = {
// Tower types in order for the UI panel
export const TOWER_ORDER = ['SCARECROW', 'PITCHFORK', 'CORN_CANNON', 'SPRINKLER', 'TRACTOR'];
// Health bar configuration
// Health bar configuration (spatial values scale by PX)
export const HEALTH_BAR = {
WIDTH: 24,
HEIGHT: 3,
OFFSET_Y: -16,
WIDTH: 24 * PX,
HEIGHT: 3 * PX,
OFFSET_Y: -16 * PX,
HIGH_THRESHOLD: 0.6,
MED_THRESHOLD: 0.3,
};
// Transition / animation config
// Transition / animation config (durations, not spatial)
export const TRANSITION = {
FADE_DURATION: 400,
SCORE_POP_SCALE: 1.3,
SCORE_POP_DURATION: 150,
};
// Starting resources per level
// Starting resources per level (game-logic, not spatial)
export const LEVEL_STARTING_CORN = [200, 250, 300, 350, 400];
export const LEVEL_STARTING_LIVES = [20, 20, 25, 25, 30];
// Wave timing
// Wave timing (durations, not spatial)
export const WAVE = {
SPAWN_INTERVAL: 600, // ms between enemies in a group
GROUP_DELAY: 1500, // ms between groups in a wave
COUNTDOWN_DURATION: 3, // seconds before first wave if auto (not used - manual start)
SPAWN_INTERVAL: 600,
GROUP_DELAY: 1500,
COUNTDOWN_DURATION: 3,
};
// Projectile physics
export const PROJECTILE = {
LIFETIME: 5000, // ms before auto-destroy
HIT_DISTANCE: 15, // pixels to count as hit
LIFETIME: 5000, // ms, not spatial
HIT_DISTANCE: 15 * PX, // spatial
};
// UI Layout
// UI Layout (spatial values scale by PX, font sizes use PX-scaled values)
export const UI = {
TOP_BAR_HEIGHT: 36,
TOP_BAR_HEIGHT: Math.round(36 * PX),
TOP_BAR_BG: 0x2d1b0e,
TOP_BAR_ALPHA: 0.9,
PANEL_WIDTH: 800,
PANEL_HEIGHT: 70,
PANEL_Y: 530, // bottom of screen
TOWER_ICON_SIZE: 36,
TOWER_ICON_SPACING: 10,
PANEL_WIDTH: GAME.WIDTH,
PANEL_HEIGHT: Math.round(70 * PX),
PANEL_Y: GAME.HEIGHT - Math.round(70 * PX), // computed from canvas
TOWER_ICON_SIZE: Math.round(36 * PX),
TOWER_ICON_SPACING: Math.round(10 * PX),
FONT_FAMILY: 'monospace',
FONT_SIZE_SMALL: '12px',
FONT_SIZE_MEDIUM: '16px',
FONT_SIZE_LARGE: '24px',
FONT_SIZE_TITLE: '48px',
FONT_SIZE_SMALL: Math.round(12 * PX) + 'px',
FONT_SIZE_MEDIUM: Math.round(16 * PX) + 'px',
FONT_SIZE_LARGE: Math.round(24 * PX) + 'px',
FONT_SIZE_TITLE: Math.round(48 * PX) + 'px',
};
// Game speed multipliers
// Game speed multipliers (game-logic, not spatial)
export const SPEED = {
NORMAL: 1,
FAST: 2,
@@ -305,66 +343,68 @@ export const SPEED = {
// =============================================================================
// Particle effects configuration
// Speed and size are spatial -- scale by PX.
// Duration, count, colors are not spatial.
export const PARTICLES = {
// Enemy death burst
ENEMY_DEATH: {
COUNT: 12,
MIN_SPEED: 40,
MAX_SPEED: 120,
MIN_SIZE: 2,
MAX_SIZE: 5,
MIN_SPEED: 40 * PX,
MAX_SPEED: 120 * PX,
MIN_SIZE: 2 * PX,
MAX_SIZE: 5 * PX,
DURATION: 500,
GRAVITY: 80,
GRAVITY: 80 * PX,
},
// Corn earned floating text
CORN_EARNED: {
FLOAT_DISTANCE: 40,
FLOAT_DISTANCE: 40 * PX,
DURATION: 800,
FONT_SIZE: '14px',
FONT_SIZE: Math.round(14 * PX) + 'px',
COLOR: '#ffdd44',
},
// Projectile splash ring
PROJECTILE_SPLASH: {
RING_COUNT: 10,
MIN_SPEED: 30,
MAX_SPEED: 80,
SIZE: 3,
MIN_SPEED: 30 * PX,
MAX_SPEED: 80 * PX,
SIZE: 3 * PX,
DURATION: 350,
COLOR: 0xffee44,
},
// Tower placed dust puff
TOWER_PLACED: {
COUNT: 8,
MIN_SPEED: 20,
MAX_SPEED: 60,
SIZE: 3,
MIN_SPEED: 20 * PX,
MAX_SPEED: 60 * PX,
SIZE: 3 * PX,
DURATION: 400,
COLOR: 0xc4a35a,
},
// Barn hit red flash particles
BARN_HIT: {
COUNT: 10,
MIN_SPEED: 30,
MAX_SPEED: 90,
SIZE: 4,
MIN_SPEED: 30 * PX,
MAX_SPEED: 90 * PX,
SIZE: 4 * PX,
DURATION: 500,
COLOR: 0xff3333,
},
// Menu fireflies
MENU_FIREFLIES: {
COUNT: 20,
MIN_SIZE: 1,
MAX_SIZE: 3,
MIN_SIZE: 1 * PX,
MAX_SIZE: 3 * PX,
COLOR: 0xffee88,
MIN_DURATION: 2000,
MAX_DURATION: 4000,
DRIFT: 60,
DRIFT: 60 * PX,
},
// Game over embers
GAMEOVER_EMBERS: {
COUNT: 25,
MIN_SIZE: 2,
MAX_SIZE: 4,
MIN_SIZE: 2 * PX,
MAX_SIZE: 4 * PX,
COLORS: [0xff4422, 0xff6633, 0xcc3311, 0xff8844],
MIN_DURATION: 3000,
MAX_DURATION: 5000,
@@ -373,25 +413,25 @@ export const PARTICLES = {
LEVELCOMPLETE_CONFETTI: {
COUNT: 40,
COLORS: [0xffdd00, 0x44ff44, 0xff44ff, 0x44ddff, 0xff8844, 0xff4444],
MIN_SIZE: 3,
MAX_SIZE: 6,
MIN_SIZE: 3 * PX,
MAX_SIZE: 6 * PX,
MIN_DURATION: 2000,
MAX_DURATION: 4000,
},
// Level complete star burst
LEVELCOMPLETE_STARBURST: {
COUNT: 16,
MIN_SPEED: 80,
MAX_SPEED: 200,
SIZE: 5,
MIN_SPEED: 80 * PX,
MAX_SPEED: 200 * PX,
SIZE: 5 * PX,
DURATION: 700,
COLOR: 0xffdd00,
},
// Projectile trail
TRAIL: {
INTERVAL: 50, // ms between trail particles
SIZE: 2,
DURATION: 200,
INTERVAL: 50, // ms, not spatial
SIZE: 2 * PX,
DURATION: 200, // ms, not spatial
ALPHA: 0.5,
},
};
@@ -401,7 +441,7 @@ export const EFFECTS = {
// Camera shake on barn hit
BARN_HIT_SHAKE: {
DURATION: 200,
INTENSITY: 0.008,
INTENSITY: 0.008, // ratio, not spatial
},
BARN_HIT_FLASH: {
DURATION: 250,
@@ -451,7 +491,7 @@ export const EFFECTS = {
},
// Game over title shake
GAMEOVER_TITLE_SHAKE: {
OFFSET: 4,
OFFSET: 4 * PX,
DURATION: 60,
REPEATS: 4,
DELAY: 200,
@@ -463,7 +503,7 @@ export const EFFECTS = {
},
// Stats slide in (level complete)
STATS_SLIDE_IN: {
OFFSET_X: -200,
OFFSET_X: -200 * PX,
DURATION: 400,
STAGGER_DELAY: 150,
EASE: 'Back.easeOut',
@@ -473,7 +513,7 @@ export const EFFECTS = {
CORN_FLASH_DURATION: 100,
// Lives counter shake
LIVES_SHAKE: {
OFFSET: 4,
OFFSET: 4 * PX,
DURATION: 50,
REPEATS: 3,
},
@@ -487,12 +527,12 @@ export const EFFECTS = {
},
// Typography
TEXT_SHADOW_COLOR: '#000000',
TEXT_SHADOW_BLUR: 4,
TEXT_STROKE_THICKNESS: 2,
TEXT_SHADOW_BLUR: Math.round(4 * PX),
TEXT_STROKE_THICKNESS: Math.round(2 * PX),
TEXT_STROKE_COLOR: '#000000',
};
// Enemy type to particle color mapping
// Enemy type to particle color mapping (colors, not spatial)
export const ENEMY_DEATH_COLORS = {
chicken: [0xffdd00, 0xffee44, 0xeecc00],
pig: [0xffaacc, 0xff88aa, 0xffccdd],
+12 -1
View File
@@ -1,10 +1,11 @@
// =============================================================================
// Barn Defense - GameConfig
// Phaser configuration. Registers all scenes. No physics gravity needed.
// Uses DPR-aware scaling for crisp retina rendering.
// =============================================================================
import Phaser from 'phaser';
import { GAME, COLORS } from './Constants.js';
import { GAME, COLORS, DPR } from './Constants.js';
import { BootScene } from '../scenes/BootScene.js';
import { GameScene } from '../scenes/GameScene.js';
import { UIScene } from '../scenes/UIScene.js';
@@ -17,5 +18,15 @@ export const GameConfig = {
height: GAME.HEIGHT,
parent: 'game-container',
backgroundColor: 0x1a3a0e,
roundPixels: true,
antialias: true,
render: {
preserveDrawingBuffer: true,
},
scale: {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH,
zoom: 1 / DPR,
},
scene: [BootScene, GameScene, UIScene, GameOverScene, LevelCompleteScene],
};
@@ -1,6 +1,7 @@
// =============================================================================
// Barn Defense - PixelRenderer
// Renders 2D pixel matrices to Phaser textures.
// Canvas dimensions are rounded to integers for proper rendering.
// =============================================================================
/**
@@ -12,10 +13,14 @@ export function renderPixelArt(scene, pixels, palette, key, scale = 2) {
const h = pixels.length;
const w = pixels[0].length;
const canvas = document.createElement('canvas');
canvas.width = w * scale;
canvas.height = h * scale;
canvas.width = Math.round(w * scale);
canvas.height = Math.round(h * scale);
const ctx = canvas.getContext('2d');
// Compute per-pixel size to fill canvas exactly
const pxW = canvas.width / w;
const pxH = canvas.height / h;
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const idx = pixels[y][x];
@@ -25,7 +30,12 @@ export function renderPixelArt(scene, pixels, palette, key, scale = 2) {
const g = (color >> 8) & 0xff;
const b = color & 0xff;
ctx.fillStyle = `rgb(${r},${g},${b})`;
ctx.fillRect(x * scale, y * scale, scale, scale);
// Use floor/ceil to avoid sub-pixel gaps
const rx = Math.floor(x * pxW);
const ry = Math.floor(y * pxH);
const rw = Math.ceil((x + 1) * pxW) - rx;
const rh = Math.ceil((y + 1) * pxH) - ry;
ctx.fillRect(rx, ry, rw, rh);
}
}
@@ -40,13 +50,17 @@ export function renderSpriteSheet(scene, frames, palette, key, scale = 2) {
const h = frames[0].length;
const w = frames[0][0].length;
const frameW = w * scale;
const frameH = h * scale;
const frameW = Math.round(w * scale);
const frameH = Math.round(h * scale);
const canvas = document.createElement('canvas');
canvas.width = frameW * frames.length;
canvas.height = frameH;
const ctx = canvas.getContext('2d');
// Compute per-pixel size to fill frame exactly
const pxW = frameW / w;
const pxH = frameH / h;
frames.forEach((pixels, fi) => {
const offsetX = fi * frameW;
for (let y = 0; y < h; y++) {
@@ -58,7 +72,11 @@ export function renderSpriteSheet(scene, frames, palette, key, scale = 2) {
const g = (color >> 8) & 0xff;
const b = color & 0xff;
ctx.fillStyle = `rgb(${r},${g},${b})`;
ctx.fillRect(offsetX + x * scale, y * scale, scale, scale);
const rx = Math.floor(x * pxW);
const ry = Math.floor(y * pxH);
const rw = Math.ceil((x + 1) * pxW) - rx;
const rh = Math.ceil((y + 1) * pxH) - ry;
ctx.fillRect(offsetX + rx, ry, rw, rh);
}
}
});
+2 -2
View File
@@ -5,7 +5,7 @@
// Uses pixel art sprite sheets for visuals.
// =============================================================================
import { HEALTH_BAR, COLORS, GAME } from '../core/Constants.js';
import { HEALTH_BAR, COLORS, GAME, PX } from '../core/Constants.js';
import { eventBus, Events } from '../core/EventBus.js';
import { gameState } from '../core/GameState.js';
@@ -89,7 +89,7 @@ export class Enemy {
const dy = target.y - this.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 2) {
if (dist < 2 * PX) {
// Reached waypoint
this.x = target.x;
this.y = target.y;
+4 -4
View File
@@ -5,7 +5,7 @@
// Uses pixel art textures for visuals.
// =============================================================================
import { GAME, TOWERS, COLORS, EFFECTS } from '../core/Constants.js';
import { GAME, TOWERS, COLORS, EFFECTS, PX } from '../core/Constants.js';
import { eventBus, Events } from '../core/EventBus.js';
import { gameState } from '../core/GameState.js';
@@ -41,10 +41,10 @@ export class Tower {
// Level indicator (with text stroke for readability)
this.levelText = scene.add.text(
GAME.TILE_SIZE / 2 - 6, -GAME.TILE_SIZE / 2 + 1,
GAME.TILE_SIZE / 2 - 6 * PX, -GAME.TILE_SIZE / 2 + 1 * PX,
'1',
{
fontSize: '10px',
fontSize: Math.round(10 * PX) + 'px',
fontFamily: 'monospace',
color: '#ffffff',
stroke: EFFECTS.TEXT_STROKE_COLOR,
@@ -55,7 +55,7 @@ export class Tower {
// Range indicator (hidden by default)
this.rangeCircle = scene.add.circle(0, 0, this.range, COLORS.RANGE_FILL, COLORS.RANGE_ALPHA);
this.rangeCircle.setStrokeStyle(1, COLORS.RANGE_STROKE, COLORS.RANGE_STROKE_ALPHA);
this.rangeCircle.setStrokeStyle(1 * PX, COLORS.RANGE_STROKE, COLORS.RANGE_STROKE_ALPHA);
this.rangeCircle.setVisible(false);
this.container.add(this.rangeCircle);
this.container.sendToBack(this.rangeCircle);
+27
View File
@@ -7,6 +7,7 @@ import Phaser from 'phaser';
import { GameConfig } from './core/GameConfig.js';
import { eventBus, Events } from './core/EventBus.js';
import { gameState } from './core/GameState.js';
import { GAME } from './core/Constants.js';
import { initAudioBridge } from './audio/AudioBridge.js';
// Initialize audio bridge before Phaser game creation
@@ -19,3 +20,29 @@ window.__GAME__ = game;
window.__GAME_STATE__ = gameState;
window.__EVENT_BUS__ = eventBus;
window.__EVENTS__ = Events;
// ---------------------------------------------------------------------------
// render_game_to_text() -- AI-readable snapshot of current game state
// ---------------------------------------------------------------------------
window.render_game_to_text = function () {
const state = {
note: `Coordinate system: ${GAME.WIDTH}x${GAME.HEIGHT}, origin top-left, grid ${GAME.GRID_COLS}x${GAME.GRID_ROWS}`,
mode: gameState.gameOver ? 'game_over' : gameState.levelComplete ? 'level_complete' : 'playing',
level: gameState.currentLevel + 1,
wave: `${gameState.currentWave}/${gameState.totalWaves}`,
waveInProgress: gameState.waveInProgress,
gold: gameState.corn,
lives: gameState.lives,
towersPlaced: gameState.towersPlaced.length,
enemiesAlive: gameState.enemiesAlive,
gameSpeed: gameState.gameSpeed,
};
return JSON.stringify(state);
};
// ---------------------------------------------------------------------------
// advanceTime(ms) -- resolves after ms real-time milliseconds
// ---------------------------------------------------------------------------
window.advanceTime = function (ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
};
@@ -4,7 +4,7 @@
// =============================================================================
import Phaser from 'phaser';
import { GAME, COLORS, UI, TRANSITION, PARTICLES, EFFECTS } from '../core/Constants.js';
import { GAME, COLORS, UI, TRANSITION, PARTICLES, EFFECTS, PX } from '../core/Constants.js';
import { eventBus, Events } from '../core/EventBus.js';
import { gameState } from '../core/GameState.js';
import { LEVELS } from '../systems/MapSystem.js';
@@ -32,16 +32,16 @@ export class GameOverScene extends Phaser.Scene {
this.createEmbers();
// Game Over title with shake effect
const title = this.add.text(cx, cy - 120, 'GAME OVER', {
const title = this.add.text(cx, cy - 120 * PX, 'GAME OVER', {
fontSize: UI.FONT_SIZE_TITLE,
fontFamily: UI.FONT_FAMILY,
color: COLORS.UI_RED_TEXT,
fontStyle: 'bold',
stroke: EFFECTS.TEXT_STROKE_COLOR,
strokeThickness: 4,
strokeThickness: Math.round(4 * PX),
shadow: {
offsetX: 2,
offsetY: 2,
offsetX: Math.round(2 * PX),
offsetY: Math.round(2 * PX),
color: EFFECTS.TEXT_SHADOW_COLOR,
blur: EFFECTS.TEXT_SHADOW_BLUR,
fill: true,
@@ -66,12 +66,12 @@ export class GameOverScene extends Phaser.Scene {
const fadeCfg = EFFECTS.STATS_FADE_IN;
const stats = [];
const msgText = this.add.text(cx, cy - 60, 'The barn has been overrun!', {
const msgText = this.add.text(cx, cy - 60 * PX, 'The barn has been overrun!', {
fontSize: UI.FONT_SIZE_MEDIUM,
fontFamily: UI.FONT_FAMILY,
color: COLORS.UI_TEXT,
stroke: EFFECTS.TEXT_STROKE_COLOR,
strokeThickness: 1,
strokeThickness: Math.round(1 * PX),
}).setOrigin(0.5).setAlpha(0);
stats.push(msgText);
@@ -80,30 +80,30 @@ export class GameOverScene extends Phaser.Scene {
? LEVELS[gameState.currentLevel].name
: 'Unknown';
const stat1 = this.add.text(cx, cy - 20, `Level: ${gameState.currentLevel + 1} - ${levelName}`, {
const stat1 = this.add.text(cx, cy - 20 * PX, `Level: ${gameState.currentLevel + 1} - ${levelName}`, {
fontSize: UI.FONT_SIZE_MEDIUM,
fontFamily: UI.FONT_FAMILY,
color: COLORS.UI_GOLD_TEXT,
stroke: EFFECTS.TEXT_STROKE_COLOR,
strokeThickness: 1,
strokeThickness: Math.round(1 * PX),
}).setOrigin(0.5).setAlpha(0);
stats.push(stat1);
const stat2 = this.add.text(cx, cy + 10, `Wave reached: ${gameState.currentWave}/${gameState.totalWaves}`, {
const stat2 = this.add.text(cx, cy + 10 * PX, `Wave reached: ${gameState.currentWave}/${gameState.totalWaves}`, {
fontSize: UI.FONT_SIZE_MEDIUM,
fontFamily: UI.FONT_FAMILY,
color: COLORS.UI_TEXT,
stroke: EFFECTS.TEXT_STROKE_COLOR,
strokeThickness: 1,
strokeThickness: Math.round(1 * PX),
}).setOrigin(0.5).setAlpha(0);
stats.push(stat2);
const stat3 = this.add.text(cx, cy + 40, `Towers placed: ${gameState.towersPlaced.length}`, {
const stat3 = this.add.text(cx, cy + 40 * PX, `Towers placed: ${gameState.towersPlaced.length}`, {
fontSize: UI.FONT_SIZE_MEDIUM,
fontFamily: UI.FONT_FAMILY,
color: COLORS.UI_TEXT,
stroke: EFFECTS.TEXT_STROKE_COLOR,
strokeThickness: 1,
strokeThickness: Math.round(1 * PX),
}).setOrigin(0.5).setAlpha(0);
stats.push(stat3);
@@ -119,11 +119,11 @@ export class GameOverScene extends Phaser.Scene {
});
// Retry button with hover scale
const retryBtn = this.add.rectangle(cx, cy + 110, 140, 44, COLORS.BUTTON, 0.9);
retryBtn.setStrokeStyle(2, COLORS.BUTTON_HOVER);
const retryBtn = this.add.rectangle(cx, cy + 110 * PX, 140 * PX, 44 * PX, COLORS.BUTTON, 0.9);
retryBtn.setStrokeStyle(2 * PX, COLORS.BUTTON_HOVER);
retryBtn.setInteractive({ useHandCursor: true });
const retryText = this.add.text(cx, cy + 110, 'RETRY', {
const retryText = this.add.text(cx, cy + 110 * PX, 'RETRY', {
fontSize: UI.FONT_SIZE_LARGE,
fontFamily: UI.FONT_FAMILY,
color: COLORS.UI_TEXT,
@@ -164,7 +164,7 @@ export class GameOverScene extends Phaser.Scene {
for (let i = 0; i < cfg.COUNT; i++) {
const x = Math.random() * GAME.WIDTH;
const startY = -20 - Math.random() * 100;
const startY = -20 * PX - Math.random() * 100 * PX;
const size = cfg.MIN_SIZE + Math.random() * (cfg.MAX_SIZE - cfg.MIN_SIZE);
const color = cfg.COLORS[Math.floor(Math.random() * cfg.COLORS.length)];
const duration = cfg.MIN_DURATION + Math.random() * (cfg.MAX_DURATION - cfg.MIN_DURATION);
@@ -174,14 +174,14 @@ export class GameOverScene extends Phaser.Scene {
this.tweens.add({
targets: ember,
y: GAME.HEIGHT + 20,
x: x + (Math.random() - 0.5) * 100,
y: GAME.HEIGHT + 20 * PX,
x: x + (Math.random() - 0.5) * 100 * PX,
alpha: 0,
duration: duration,
delay: Math.random() * 2000,
repeat: -1,
onRepeat: () => {
ember.setPosition(Math.random() * GAME.WIDTH, -20);
ember.setPosition(Math.random() * GAME.WIDTH, -20 * PX);
ember.setAlpha(0.6);
},
ease: 'Sine.easeIn',
@@ -4,7 +4,7 @@
// =============================================================================
import Phaser from 'phaser';
import { GAME, COLORS, UI, TRANSITION, PARTICLES, EFFECTS } from '../core/Constants.js';
import { GAME, COLORS, UI, TRANSITION, PARTICLES, EFFECTS, PX } from '../core/Constants.js';
import { eventBus, Events } from '../core/EventBus.js';
import { gameState } from '../core/GameState.js';
import { LEVELS } from '../systems/MapSystem.js';
@@ -40,16 +40,16 @@ export class LevelCompleteScene extends Phaser.Scene {
);
// Victory title with pulse
const title = this.add.text(cx, cy - 120, 'LEVEL COMPLETE!', {
const title = this.add.text(cx, cy - 120 * PX, 'LEVEL COMPLETE!', {
fontSize: UI.FONT_SIZE_TITLE,
fontFamily: UI.FONT_FAMILY,
color: COLORS.UI_GREEN_TEXT,
fontStyle: 'bold',
stroke: EFFECTS.TEXT_STROKE_COLOR,
strokeThickness: 4,
strokeThickness: Math.round(4 * PX),
shadow: {
offsetX: 2,
offsetY: 2,
offsetX: Math.round(2 * PX),
offsetY: Math.round(2 * PX),
color: EFFECTS.TEXT_SHADOW_COLOR,
blur: EFFECTS.TEXT_SHADOW_BLUR,
fill: true,
@@ -68,7 +68,7 @@ export class LevelCompleteScene extends Phaser.Scene {
});
// Star burst at center
this.createStarBurst(cx, cy - 120);
this.createStarBurst(cx, cy - 120 * PX);
// Level info -- slides in from left
const levelName = LEVELS[gameState.currentLevel]
@@ -78,40 +78,40 @@ export class LevelCompleteScene extends Phaser.Scene {
const slideCfg = EFFECTS.STATS_SLIDE_IN;
const statsItems = [];
const levelInfo = this.add.text(cx, cy - 60, `${levelName} - Defended!`, {
const levelInfo = this.add.text(cx, cy - 60 * PX, `${levelName} - Defended!`, {
fontSize: UI.FONT_SIZE_LARGE,
fontFamily: UI.FONT_FAMILY,
color: COLORS.UI_GOLD_TEXT,
stroke: EFFECTS.TEXT_STROKE_COLOR,
strokeThickness: 1,
strokeThickness: Math.round(1 * PX),
}).setOrigin(0.5);
statsItems.push(levelInfo);
// Stats
const stat1 = this.add.text(cx, cy - 20, `Waves survived: ${gameState.currentWave}`, {
const stat1 = this.add.text(cx, cy - 20 * PX, `Waves survived: ${gameState.currentWave}`, {
fontSize: UI.FONT_SIZE_MEDIUM,
fontFamily: UI.FONT_FAMILY,
color: COLORS.UI_TEXT,
stroke: EFFECTS.TEXT_STROKE_COLOR,
strokeThickness: 1,
strokeThickness: Math.round(1 * PX),
}).setOrigin(0.5);
statsItems.push(stat1);
const stat2 = this.add.text(cx, cy + 10, `Corn remaining: ${gameState.corn}`, {
const stat2 = this.add.text(cx, cy + 10 * PX, `Corn remaining: ${gameState.corn}`, {
fontSize: UI.FONT_SIZE_MEDIUM,
fontFamily: UI.FONT_FAMILY,
color: COLORS.UI_GOLD_TEXT,
stroke: EFFECTS.TEXT_STROKE_COLOR,
strokeThickness: 1,
strokeThickness: Math.round(1 * PX),
}).setOrigin(0.5);
statsItems.push(stat2);
const stat3 = this.add.text(cx, cy + 40, `Lives remaining: ${gameState.lives}`, {
const stat3 = this.add.text(cx, cy + 40 * PX, `Lives remaining: ${gameState.lives}`, {
fontSize: UI.FONT_SIZE_MEDIUM,
fontFamily: UI.FONT_FAMILY,
color: COLORS.UI_TEXT,
stroke: EFFECTS.TEXT_STROKE_COLOR,
strokeThickness: 1,
strokeThickness: Math.round(1 * PX),
}).setOrigin(0.5);
statsItems.push(stat3);
@@ -134,11 +134,11 @@ export class LevelCompleteScene extends Phaser.Scene {
const hasNextLevel = gameState.currentLevel + 1 < LEVELS.length;
if (hasNextLevel) {
const nextBtn = this.add.rectangle(cx, cy + 110, 150, 44, COLORS.BUTTON, 0.9);
nextBtn.setStrokeStyle(2, COLORS.BUTTON_HOVER);
const nextBtn = this.add.rectangle(cx, cy + 110 * PX, 150 * PX, 44 * PX, COLORS.BUTTON, 0.9);
nextBtn.setStrokeStyle(2 * PX, COLORS.BUTTON_HOVER);
nextBtn.setInteractive({ useHandCursor: true });
const nextText = this.add.text(cx, cy + 110, 'NEXT LEVEL', {
const nextText = this.add.text(cx, cy + 110 * PX, 'NEXT LEVEL', {
fontSize: UI.FONT_SIZE_MEDIUM,
fontFamily: UI.FONT_FAMILY,
color: COLORS.UI_TEXT,
@@ -169,11 +169,11 @@ export class LevelCompleteScene extends Phaser.Scene {
});
nextBtn.on('pointerdown', () => this.nextLevel());
} else {
const againBtn = this.add.rectangle(cx, cy + 110, 150, 44, COLORS.BUTTON, 0.9);
againBtn.setStrokeStyle(2, COLORS.BUTTON_HOVER);
const againBtn = this.add.rectangle(cx, cy + 110 * PX, 150 * PX, 44 * PX, COLORS.BUTTON, 0.9);
againBtn.setStrokeStyle(2 * PX, COLORS.BUTTON_HOVER);
againBtn.setInteractive({ useHandCursor: true });
const againText = this.add.text(cx, cy + 110, 'PLAY AGAIN', {
const againText = this.add.text(cx, cy + 110 * PX, 'PLAY AGAIN', {
fontSize: UI.FONT_SIZE_MEDIUM,
fontFamily: UI.FONT_FAMILY,
color: COLORS.UI_TEXT,
@@ -244,7 +244,7 @@ export class LevelCompleteScene extends Phaser.Scene {
for (let i = 0; i < cfg.COUNT; i++) {
const x = Math.random() * GAME.WIDTH;
const y = -20 - Math.random() * 200;
const y = -20 * PX - Math.random() * 200 * PX;
const color = cfg.COLORS[Math.floor(Math.random() * cfg.COLORS.length)];
const size = cfg.MIN_SIZE + Math.random() * (cfg.MAX_SIZE - cfg.MIN_SIZE);
const duration = cfg.MIN_DURATION + Math.random() * (cfg.MAX_DURATION - cfg.MIN_DURATION);
@@ -257,15 +257,15 @@ export class LevelCompleteScene extends Phaser.Scene {
this.tweens.add({
targets: confetti,
y: GAME.HEIGHT + 20,
x: x + (Math.random() - 0.5) * 200,
y: GAME.HEIGHT + 20 * PX,
x: x + (Math.random() - 0.5) * 200 * PX,
angle: (Math.random() - 0.5) * 720,
alpha: 0,
duration: duration,
delay: Math.random() * 1500,
repeat: -1,
onRepeat: () => {
confetti.setPosition(Math.random() * GAME.WIDTH, -20);
confetti.setPosition(Math.random() * GAME.WIDTH, -20 * PX);
confetti.setAlpha(1);
confetti.setAngle(0);
},
+10 -10
View File
@@ -5,7 +5,7 @@
// =============================================================================
import Phaser from 'phaser';
import { GAME, COLORS, UI, SPEED, TRANSITION, EFFECTS } from '../core/Constants.js';
import { GAME, COLORS, UI, SPEED, TRANSITION, EFFECTS, PX } from '../core/Constants.js';
import { eventBus, Events } from '../core/EventBus.js';
import { gameState } from '../core/GameState.js';
import { TowerPanel } from '../ui/TowerPanel.js';
@@ -30,7 +30,7 @@ export class UIScene extends Phaser.Scene {
? LEVELS[gameState.currentLevel].name
: 'Unknown';
this.levelText = this.add.text(10, 10, `Level ${gameState.currentLevel + 1}: ${levelName}`, {
this.levelText = this.add.text(10 * PX, 10 * PX, `Level ${gameState.currentLevel + 1}: ${levelName}`, {
fontSize: UI.FONT_SIZE_MEDIUM,
fontFamily: UI.FONT_FAMILY,
color: COLORS.UI_GOLD_TEXT,
@@ -40,7 +40,7 @@ export class UIScene extends Phaser.Scene {
});
// Corn display (with text shadow)
this.cornText = this.add.text(GAME.WIDTH - 200, 10, `Corn: ${gameState.corn}`, {
this.cornText = this.add.text(GAME.WIDTH - 200 * PX, 10 * PX, `Corn: ${gameState.corn}`, {
fontSize: UI.FONT_SIZE_MEDIUM,
fontFamily: UI.FONT_FAMILY,
color: COLORS.UI_GOLD_TEXT,
@@ -48,10 +48,10 @@ export class UIScene extends Phaser.Scene {
stroke: EFFECTS.TEXT_STROKE_COLOR,
strokeThickness: EFFECTS.TEXT_STROKE_THICKNESS,
});
this.cornBaseX = GAME.WIDTH - 200;
this.cornBaseX = GAME.WIDTH - 200 * PX;
// Lives display (with text shadow)
this.livesText = this.add.text(GAME.WIDTH - 80, 10, `Lives: ${gameState.lives}`, {
this.livesText = this.add.text(GAME.WIDTH - 80 * PX, 10 * PX, `Lives: ${gameState.lives}`, {
fontSize: UI.FONT_SIZE_MEDIUM,
fontFamily: UI.FONT_FAMILY,
color: COLORS.UI_TEXT,
@@ -59,17 +59,17 @@ export class UIScene extends Phaser.Scene {
stroke: EFFECTS.TEXT_STROKE_COLOR,
strokeThickness: EFFECTS.TEXT_STROKE_THICKNESS,
});
this.livesBaseX = GAME.WIDTH - 80;
this.livesBaseX = GAME.WIDTH - 80 * PX;
// Speed button
this.speedBtn = this.add.rectangle(
GAME.WIDTH - 40, UI.PANEL_Y - 22,
60, 24,
GAME.WIDTH - 40 * PX, UI.PANEL_Y - 22 * PX,
60 * PX, 24 * PX,
COLORS.UI_BUTTON, 0.8
);
this.speedBtn.setInteractive({ useHandCursor: true });
this.speedBtnText = this.add.text(
GAME.WIDTH - 40, UI.PANEL_Y - 22,
GAME.WIDTH - 40 * PX, UI.PANEL_Y - 22 * PX,
'1x',
{
fontSize: UI.FONT_SIZE_SMALL,
@@ -77,7 +77,7 @@ export class UIScene extends Phaser.Scene {
color: COLORS.UI_TEXT,
fontStyle: 'bold',
stroke: EFFECTS.TEXT_STROKE_COLOR,
strokeThickness: 1,
strokeThickness: Math.round(1 * PX),
}
).setOrigin(0.5);
+6 -5
View File
@@ -5,6 +5,7 @@
// =============================================================================
import { FARM_PALETTE } from './palette.js';
import { PX } from '../core/Constants.js';
// ---- CHICKEN (12x12) ----
// Small round yellow body, orange beak, tiny red comb, black eye
@@ -214,9 +215,9 @@ const BULL_WALK2 = [
// ---- Export all enemy sprites ----
export const ENEMY_SPRITES = {
chicken: { frames: [CHICKEN_WALK1, CHICKEN_WALK2], palette: FARM_PALETTE, scale: 2, animRate: 6 },
pig: { frames: [PIG_WALK1, PIG_WALK2], palette: FARM_PALETTE, scale: 2, animRate: 5 },
cow: { frames: [COW_WALK1, COW_WALK2], palette: FARM_PALETTE, scale: 2, animRate: 4 },
goat: { frames: [GOAT_WALK1, GOAT_WALK2], palette: FARM_PALETTE, scale: 2, animRate: 5 },
bull: { frames: [BULL_WALK1, BULL_WALK2], palette: FARM_PALETTE, scale: 2, animRate: 3 },
chicken: { frames: [CHICKEN_WALK1, CHICKEN_WALK2], palette: FARM_PALETTE, scale: 2 * PX, animRate: 6 },
pig: { frames: [PIG_WALK1, PIG_WALK2], palette: FARM_PALETTE, scale: 2 * PX, animRate: 5 },
cow: { frames: [COW_WALK1, COW_WALK2], palette: FARM_PALETTE, scale: 2 * PX, animRate: 4 },
goat: { frames: [GOAT_WALK1, GOAT_WALK2], palette: FARM_PALETTE, scale: 2 * PX, animRate: 5 },
bull: { frames: [BULL_WALK1, BULL_WALK2], palette: FARM_PALETTE, scale: 2 * PX, animRate: 3 },
};
@@ -4,6 +4,7 @@
// =============================================================================
import { FARM_PALETTE } from './palette.js';
import { PX } from '../core/Constants.js';
// ---- HAY BALE (8x8) ----
// Golden square shape, darker cross pattern
@@ -78,9 +79,9 @@ const TRACTOR_BOLT = [
// ---- Export all projectile sprites ----
export const PROJECTILE_SPRITES = {
scarecrow: { pixels: HAY_BALE, palette: FARM_PALETTE, scale: 2 },
pitchfork: { pixels: PITCHFORK_PROJ, palette: FARM_PALETTE, scale: 2 },
corn_cannon: { pixels: CORN_COB, palette: FARM_PALETTE, scale: 2 },
sprinkler: { pixels: WATER_DROP, palette: FARM_PALETTE, scale: 2 },
tractor: { pixels: TRACTOR_BOLT, palette: FARM_PALETTE, scale: 2 },
scarecrow: { pixels: HAY_BALE, palette: FARM_PALETTE, scale: 2 * PX },
pitchfork: { pixels: PITCHFORK_PROJ, palette: FARM_PALETTE, scale: 2 * PX },
corn_cannon: { pixels: CORN_COB, palette: FARM_PALETTE, scale: 2 * PX },
sprinkler: { pixels: WATER_DROP, palette: FARM_PALETTE, scale: 2 * PX },
tractor: { pixels: TRACTOR_BOLT, palette: FARM_PALETTE, scale: 2 * PX },
};
+18 -12
View File
@@ -1,10 +1,14 @@
// =============================================================================
// Barn Defense - Tile Sprites
// Background tiles (16x16 grid at scale 2.5 = 40px to match TILE_SIZE).
// Background tiles (16x16 grid at scale 2.5*PX to match TILE_SIZE).
// Also includes decorative elements.
//
// Original: 16px * 2.5 = 40px = TILE_SIZE
// Scaled: 16px * 2.5 * PX = 40 * PX = GAME.TILE_SIZE
// =============================================================================
import { FARM_PALETTE } from './palette.js';
import { PX } from '../core/Constants.js';
// ---- GRASS TILE 1 (16x16) ----
// Base green with subtle darker speckles
@@ -215,20 +219,22 @@ const ROCK = [
];
// ---- Export tile data ----
// Tile scale: 16px * 2.5 * PX => produces TILE_SIZE-pixel textures
// Decoration scale: original * PX
export const TILE_SPRITES = {
grass1: { pixels: GRASS_TILE_1, palette: FARM_PALETTE, scale: 2.5 },
grass2: { pixels: GRASS_TILE_2, palette: FARM_PALETTE, scale: 2.5 },
grass3: { pixels: GRASS_TILE_3, palette: FARM_PALETTE, scale: 2.5 },
path1: { pixels: PATH_TILE_1, palette: FARM_PALETTE, scale: 2.5 },
path2: { pixels: PATH_TILE_2, palette: FARM_PALETTE, scale: 2.5 },
water: { pixels: WATER_TILE, palette: FARM_PALETTE, scale: 2.5 },
entry: { pixels: ENTRY_TILE, palette: FARM_PALETTE, scale: 2.5 },
grass1: { pixels: GRASS_TILE_1, palette: FARM_PALETTE, scale: 2.5 * PX },
grass2: { pixels: GRASS_TILE_2, palette: FARM_PALETTE, scale: 2.5 * PX },
grass3: { pixels: GRASS_TILE_3, palette: FARM_PALETTE, scale: 2.5 * PX },
path1: { pixels: PATH_TILE_1, palette: FARM_PALETTE, scale: 2.5 * PX },
path2: { pixels: PATH_TILE_2, palette: FARM_PALETTE, scale: 2.5 * PX },
water: { pixels: WATER_TILE, palette: FARM_PALETTE, scale: 2.5 * PX },
entry: { pixels: ENTRY_TILE, palette: FARM_PALETTE, scale: 2.5 * PX },
};
export const DECORATION_SPRITES = {
hayStack: { pixels: HAY_STACK, palette: FARM_PALETTE, scale: 2 },
fencePost: { pixels: FENCE_POST, palette: FARM_PALETTE, scale: 2 },
flowerPatch: { pixels: FLOWER_PATCH, palette: FARM_PALETTE, scale: 2 },
rock: { pixels: ROCK, palette: FARM_PALETTE, scale: 2 },
hayStack: { pixels: HAY_STACK, palette: FARM_PALETTE, scale: 2 * PX },
fencePost: { pixels: FENCE_POST, palette: FARM_PALETTE, scale: 2 * PX },
flowerPatch: { pixels: FLOWER_PATCH, palette: FARM_PALETTE, scale: 2 * PX },
rock: { pixels: ROCK, palette: FARM_PALETTE, scale: 2 * PX },
};
+6 -5
View File
@@ -4,6 +4,7 @@
// =============================================================================
import { FARM_PALETTE } from './palette.js';
import { PX } from '../core/Constants.js';
// ---- SCARECROW (16x16) ----
// Brown body, straw hat brim, stick arms, button eyes, stuffed chest
@@ -118,9 +119,9 @@ const TRACTOR = [
// ---- Export all tower sprites ----
export const TOWER_SPRITES = {
scarecrow: { pixels: SCARECROW, palette: FARM_PALETTE, scale: 2 },
pitchfork: { pixels: PITCHFORK_TOWER, palette: FARM_PALETTE, scale: 2 },
corn_cannon: { pixels: CORN_CANNON, palette: FARM_PALETTE, scale: 2 },
sprinkler: { pixels: SPRINKLER, palette: FARM_PALETTE, scale: 2 },
tractor: { pixels: TRACTOR, palette: FARM_PALETTE, scale: 2 },
scarecrow: { pixels: SCARECROW, palette: FARM_PALETTE, scale: 2 * PX },
pitchfork: { pixels: PITCHFORK_TOWER, palette: FARM_PALETTE, scale: 2 * PX },
corn_cannon: { pixels: CORN_CANNON, palette: FARM_PALETTE, scale: 2 * PX },
sprinkler: { pixels: SPRINKLER, palette: FARM_PALETTE, scale: 2 * PX },
tractor: { pixels: TRACTOR, palette: FARM_PALETTE, scale: 2 * PX },
};
@@ -5,7 +5,7 @@
// from entities, everything flows through EventBus.
// =============================================================================
import { GAME, PARTICLES, EFFECTS, ENEMY_DEATH_COLORS } from '../core/Constants.js';
import { GAME, PARTICLES, EFFECTS, ENEMY_DEATH_COLORS, PX } from '../core/Constants.js';
import { eventBus, Events } from '../core/EventBus.js';
export class ParticleSystem {
@@ -40,7 +40,7 @@ export class ParticleSystem {
this.onBarnHit = () => {
// Barn hit particles -- find barn position from map center-right area
// We use a fixed barn area approximation
this.emitBarnHit(GAME.WIDTH - 80, GAME.HEIGHT / 2);
this.emitBarnHit(GAME.WIDTH - 80 * PX, GAME.HEIGHT / 2);
};
eventBus.on(Events.BARN_HIT, this.onBarnHit);
@@ -102,7 +102,7 @@ export class ParticleSystem {
// =========================================================================
emitCornEarned(x, y, amount) {
const cfg = PARTICLES.CORN_EARNED;
const text = this.scene.add.text(x, y - 10, `+${amount}`, {
const text = this.scene.add.text(x, y - 10 * PX, `+${amount}`, {
fontSize: cfg.FONT_SIZE,
fontFamily: 'monospace',
color: cfg.COLOR,
@@ -113,7 +113,7 @@ export class ParticleSystem {
this.scene.tweens.add({
targets: text,
y: y - 10 - cfg.FLOAT_DISTANCE,
y: y - 10 * PX - cfg.FLOAT_DISTANCE,
alpha: 0,
duration: cfg.DURATION,
ease: 'Quad.easeOut',
@@ -128,14 +128,14 @@ export class ParticleSystem {
const cfg = PARTICLES.PROJECTILE_SPLASH;
// Expanding ring
const ring = this.scene.add.circle(x, y, 5, cfg.COLOR, 0.4);
ring.setStrokeStyle(2, cfg.COLOR, 0.6);
const ring = this.scene.add.circle(x, y, 5 * PX, cfg.COLOR, 0.4);
ring.setStrokeStyle(2 * PX, cfg.COLOR, 0.6);
ring.setDepth(90);
this.scene.tweens.add({
targets: ring,
scaleX: radius / 5,
scaleY: radius / 5,
scaleX: radius / (5 * PX),
scaleY: radius / (5 * PX),
alpha: 0,
duration: cfg.DURATION,
ease: 'Quad.easeOut',
@@ -172,8 +172,8 @@ export class ParticleSystem {
emitHitSpark(x, y) {
for (let i = 0; i < 4; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = 30 + Math.random() * 50;
const p = this.scene.add.circle(x, y, 2, 0xffffff);
const speed = (30 + Math.random() * 50) * PX;
const p = this.scene.add.circle(x, y, 2 * PX, 0xffffff);
p.setDepth(90);
this.scene.tweens.add({
@@ -200,13 +200,13 @@ export class ParticleSystem {
const dx = Math.cos(angle) * speed;
const dy = Math.sin(angle) * speed;
const p = this.scene.add.circle(x, y + 8, cfg.SIZE, cfg.COLOR, 0.7);
const p = this.scene.add.circle(x, y + 8 * PX, cfg.SIZE, cfg.COLOR, 0.7);
p.setDepth(80);
this.scene.tweens.add({
targets: p,
x: x + dx * (cfg.DURATION / 1000),
y: y + 8 + dy * (cfg.DURATION / 1000) - 15,
y: y + 8 * PX + dy * (cfg.DURATION / 1000) - 15 * PX,
alpha: 0,
scaleX: 1.5,
scaleY: 1.5,
@@ -226,8 +226,8 @@ export class ParticleSystem {
for (let i = 0; i < cfg.COUNT; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = cfg.MIN_SPEED + Math.random() * (cfg.MAX_SPEED - cfg.MIN_SPEED);
const px = x + (Math.random() - 0.5) * 40;
const py = y + (Math.random() - 0.5) * 40;
const px = x + (Math.random() - 0.5) * 40 * PX;
const py = y + (Math.random() - 0.5) * 40 * PX;
const p = this.scene.add.circle(px, py, cfg.SIZE, cfg.COLOR, 0.8);
p.setDepth(100);
@@ -5,7 +5,7 @@
// Uses pixel art tiles and decorations.
// =============================================================================
import { GAME, TILE, COLORS } from '../core/Constants.js';
import { GAME, TILE, COLORS, PX } from '../core/Constants.js';
// Seeded random for deterministic tile/decoration placement
function mulberry32(seed) {
@@ -127,24 +127,24 @@ export class PathSystem {
// Barn body
g.fillStyle(COLORS.BARN_COLOR, 1);
g.fillRect(x + 2, y + S * 0.3, S - 4, S * 0.7 - 2);
g.fillRect(x + 2 * PX, y + S * 0.3, S - 4 * PX, S * 0.7 - 2 * PX);
// Barn roof
g.fillStyle(COLORS.BARN_ROOF, 1);
g.fillTriangle(
x, y + S * 0.3,
x + S / 2, y + 2,
x + S / 2, y + 2 * PX,
x + S, y + S * 0.3
);
// Door
g.fillStyle(0x663322, 1);
g.fillRect(x + S / 2 - 5, y + S * 0.5, 10, S * 0.5 - 2);
g.fillRect(x + S / 2 - 5 * PX, y + S * 0.5, 10 * PX, S * 0.5 - 2 * PX);
// X on door
g.lineStyle(1, 0x442211, 1);
g.lineBetween(x + S / 2 - 4, y + S * 0.52, x + S / 2 + 4, y + S - 4);
g.lineBetween(x + S / 2 + 4, y + S * 0.52, x + S / 2 - 4, y + S - 4);
g.lineStyle(1 * PX, 0x442211, 1);
g.lineBetween(x + S / 2 - 4 * PX, y + S * 0.52, x + S / 2 + 4 * PX, y + S - 4 * PX);
g.lineBetween(x + S / 2 + 4 * PX, y + S * 0.52, x + S / 2 - 4 * PX, y + S - 4 * PX);
}
drawPathMarkers() {
@@ -156,7 +156,7 @@ export class PathSystem {
g.fillStyle(0x887744, 0.3);
for (const path of paths) {
for (let i = 0; i < path.length; i += 3) {
g.fillCircle(path[i].x, path[i].y, 2);
g.fillCircle(path[i].x, path[i].y, 2 * PX);
}
}
}
@@ -3,7 +3,7 @@
// Manages tower placement, targeting, firing, and upgrades.
// =============================================================================
import { GAME, TOWERS, TOWER_ORDER, COLORS } from '../core/Constants.js';
import { GAME, TOWERS, TOWER_ORDER, COLORS, PX } from '../core/Constants.js';
import { eventBus, Events } from '../core/EventBus.js';
import { gameState } from '../core/GameState.js';
import { Tower } from '../entities/Tower.js';
@@ -103,7 +103,7 @@ export class TowerSystem {
// Create ghost graphic
if (!this.ghostGraphic) {
this.ghostGraphic = this.scene.add.rectangle(
0, 0, GAME.TILE_SIZE - 2, GAME.TILE_SIZE - 2,
0, 0, GAME.TILE_SIZE - 2 * PX, GAME.TILE_SIZE - 2 * PX,
COLORS.VALID_PLACEMENT, COLORS.PLACEMENT_ALPHA
);
}
@@ -113,7 +113,7 @@ export class TowerSystem {
0, 0, config.range,
COLORS.RANGE_FILL, COLORS.RANGE_ALPHA
);
this.ghostRange.setStrokeStyle(1, COLORS.RANGE_STROKE, COLORS.RANGE_STROKE_ALPHA);
this.ghostRange.setStrokeStyle(1 * PX, COLORS.RANGE_STROKE, COLORS.RANGE_STROKE_ALPHA);
} else {
this.ghostRange.setRadius(config.range);
}
+30 -30
View File
@@ -4,7 +4,7 @@
// Shows tower icons with costs and names.
// =============================================================================
import { GAME, TOWERS, TOWER_ORDER, COLORS, UI, EFFECTS } from '../core/Constants.js';
import { GAME, TOWERS, TOWER_ORDER, COLORS, UI, EFFECTS, PX } from '../core/Constants.js';
import { eventBus, Events } from '../core/EventBus.js';
import { gameState } from '../core/GameState.js';
@@ -51,11 +51,11 @@ export class TowerPanel {
UI.PANEL_WIDTH, UI.PANEL_HEIGHT,
UI.TOP_BAR_BG, UI.TOP_BAR_ALPHA
);
this.panelBg.setStrokeStyle(2, COLORS.UI_PANEL_BORDER);
this.panelBg.setStrokeStyle(2 * PX, COLORS.UI_PANEL_BORDER);
// Tower buttons
const startX = 60;
const spacing = 140;
const startX = 60 * PX;
const spacing = 140 * PX;
TOWER_ORDER.forEach((typeKey, index) => {
const config = TOWERS[typeKey];
@@ -64,33 +64,33 @@ export class TowerPanel {
// Affordable glow behind button (subtle pulsing)
const glowCfg = EFFECTS.TOWER_GLOW;
const glow = this.scene.add.rectangle(x, y, 134, 56, glowCfg.COLOR, 0);
const glow = this.scene.add.rectangle(x, y, 134 * PX, 56 * PX, glowCfg.COLOR, 0);
glow.setDepth(0);
// Button background
const btn = this.scene.add.rectangle(x, y, 130, 52, COLORS.UI_BUTTON, 0.8);
btn.setStrokeStyle(1, COLORS.UI_PANEL_BORDER);
const btn = this.scene.add.rectangle(x, y, 130 * PX, 52 * PX, COLORS.UI_BUTTON, 0.8);
btn.setStrokeStyle(1 * PX, COLORS.UI_PANEL_BORDER);
btn.setInteractive({ useHandCursor: true });
// Tower icon (small colored square)
const icon = this.scene.add.rectangle(x - 45, y, 24, 24, config.color);
const icon = this.scene.add.rectangle(x - 45 * PX, y, 24 * PX, 24 * PX, config.color);
// Tower name (with text stroke)
const nameText = this.scene.add.text(x - 28, y - 14, config.name, {
const nameText = this.scene.add.text(x - 28 * PX, y - 14 * PX, config.name, {
fontSize: UI.FONT_SIZE_SMALL,
fontFamily: UI.FONT_FAMILY,
color: COLORS.UI_TEXT,
stroke: EFFECTS.TEXT_STROKE_COLOR,
strokeThickness: 1,
strokeThickness: Math.round(1 * PX),
});
// Cost text (with text stroke)
const costText = this.scene.add.text(x - 28, y + 2, `${config.cost} corn`, {
const costText = this.scene.add.text(x - 28 * PX, y + 2 * PX, `${config.cost} corn`, {
fontSize: UI.FONT_SIZE_SMALL,
fontFamily: UI.FONT_FAMILY,
color: COLORS.UI_GOLD_TEXT,
stroke: EFFECTS.TEXT_STROKE_COLOR,
strokeThickness: 1,
strokeThickness: Math.round(1 * PX),
});
// Hover effects
@@ -142,43 +142,43 @@ export class TowerPanel {
this.infoContainer.setVisible(false);
// Background
const bg = this.scene.add.rectangle(0, 0, 220, 140, UI.TOP_BAR_BG, 0.95);
bg.setStrokeStyle(2, COLORS.UI_PANEL_BORDER);
const bg = this.scene.add.rectangle(0, 0, 220 * PX, 140 * PX, UI.TOP_BAR_BG, 0.95);
bg.setStrokeStyle(2 * PX, COLORS.UI_PANEL_BORDER);
this.infoContainer.add(bg);
// Title
this.infoTitle = this.scene.add.text(-100, -60, '', {
this.infoTitle = this.scene.add.text(-100 * PX, -60 * PX, '', {
fontSize: UI.FONT_SIZE_MEDIUM,
fontFamily: UI.FONT_FAMILY,
color: COLORS.UI_TEXT,
fontStyle: 'bold',
stroke: EFFECTS.TEXT_STROKE_COLOR,
strokeThickness: 1,
strokeThickness: Math.round(1 * PX),
});
this.infoContainer.add(this.infoTitle);
// Stats
this.infoStats = this.scene.add.text(-100, -35, '', {
this.infoStats = this.scene.add.text(-100 * PX, -35 * PX, '', {
fontSize: UI.FONT_SIZE_SMALL,
fontFamily: UI.FONT_FAMILY,
color: COLORS.UI_TEXT,
lineSpacing: 4,
lineSpacing: 4 * PX,
stroke: EFFECTS.TEXT_STROKE_COLOR,
strokeThickness: 1,
strokeThickness: Math.round(1 * PX),
});
this.infoContainer.add(this.infoStats);
// Upgrade button
this.upgradeBtn = this.scene.add.rectangle(-30, 45, 100, 28, COLORS.UI_BUTTON, 0.9);
this.upgradeBtn = this.scene.add.rectangle(-30 * PX, 45 * PX, 100 * PX, 28 * PX, COLORS.UI_BUTTON, 0.9);
this.upgradeBtn.setInteractive({ useHandCursor: true });
this.infoContainer.add(this.upgradeBtn);
this.upgradeBtnText = this.scene.add.text(-30, 45, 'Upgrade', {
this.upgradeBtnText = this.scene.add.text(-30 * PX, 45 * PX, 'Upgrade', {
fontSize: UI.FONT_SIZE_SMALL,
fontFamily: UI.FONT_FAMILY,
color: COLORS.UI_TEXT,
stroke: EFFECTS.TEXT_STROKE_COLOR,
strokeThickness: 1,
strokeThickness: Math.round(1 * PX),
}).setOrigin(0.5);
this.infoContainer.add(this.upgradeBtnText);
@@ -190,16 +190,16 @@ export class TowerPanel {
});
// Sell button
this.sellBtn = this.scene.add.rectangle(70, 45, 70, 28, 0xaa3333, 0.9);
this.sellBtn = this.scene.add.rectangle(70 * PX, 45 * PX, 70 * PX, 28 * PX, 0xaa3333, 0.9);
this.sellBtn.setInteractive({ useHandCursor: true });
this.infoContainer.add(this.sellBtn);
this.sellBtnText = this.scene.add.text(70, 45, 'Sell', {
this.sellBtnText = this.scene.add.text(70 * PX, 45 * PX, 'Sell', {
fontSize: UI.FONT_SIZE_SMALL,
fontFamily: UI.FONT_FAMILY,
color: COLORS.UI_TEXT,
stroke: EFFECTS.TEXT_STROKE_COLOR,
strokeThickness: 1,
strokeThickness: Math.round(1 * PX),
}).setOrigin(0.5);
this.infoContainer.add(this.sellBtnText);
@@ -238,10 +238,10 @@ export class TowerPanel {
for (const button of this.buttons) {
if (button.typeKey === this.selectedType) {
button.btn.setFillStyle(COLORS.UI_BUTTON_HOVER, 0.9);
button.btn.setStrokeStyle(2, 0xffff00);
button.btn.setStrokeStyle(2 * PX, 0xffff00);
} else {
button.btn.setFillStyle(COLORS.UI_BUTTON, 0.8);
button.btn.setStrokeStyle(1, COLORS.UI_PANEL_BORDER);
button.btn.setStrokeStyle(1 * PX, COLORS.UI_PANEL_BORDER);
}
}
}
@@ -271,18 +271,18 @@ export class TowerPanel {
this.selectedType = null;
this.updateButtonStates();
this.infoContainer.setPosition(tower.x, Math.max(tower.y - 100, 100));
this.infoContainer.setPosition(tower.x, Math.max(tower.y - 100 * PX, 100 * PX));
this.infoContainer.setVisible(true);
this.infoTitle.setText(`${tower.config.name} Lv.${tower.level}`);
const stats = [
`Damage: ${tower.damage}`,
`Range: ${tower.range}`,
`Range: ${Math.round(tower.range / PX)}`,
`Fire Rate: ${tower.fireRate}ms`,
];
if (tower.config.splash) {
stats.push(`Splash: ${tower.config.splashRadius}px`);
stats.push(`Splash: ${Math.round(tower.config.splashRadius / PX)}px`);
}
if (tower.config.slowEffect) {
stats.push(`Slow: ${Math.round((1 - tower.config.slowAmount) * 100)}%`);
@@ -3,7 +3,7 @@
// Shows current wave progress and provides "Start Wave" button.
// =============================================================================
import { GAME, COLORS, UI, EFFECTS } from '../core/Constants.js';
import { GAME, COLORS, UI, EFFECTS, PX } from '../core/Constants.js';
import { eventBus, Events } from '../core/EventBus.js';
import { gameState } from '../core/GameState.js';
@@ -55,7 +55,7 @@ export class WaveIndicator {
create() {
// Wave text - positioned in top bar area (with text stroke)
this.waveText = this.scene.add.text(GAME.WIDTH / 2, 10, `Wave 0/${gameState.totalWaves}`, {
this.waveText = this.scene.add.text(GAME.WIDTH / 2, 10 * PX, `Wave 0/${gameState.totalWaves}`, {
fontSize: UI.FONT_SIZE_MEDIUM,
fontFamily: UI.FONT_FAMILY,
color: COLORS.UI_TEXT,
@@ -66,15 +66,15 @@ export class WaveIndicator {
// Start Wave button
this.startBtn = this.scene.add.rectangle(
GAME.WIDTH / 2, UI.PANEL_Y - 22,
140, 28,
GAME.WIDTH / 2, UI.PANEL_Y - 22 * PX,
140 * PX, 28 * PX,
COLORS.BUTTON, 0.9
);
this.startBtn.setStrokeStyle(1, COLORS.BUTTON_HOVER);
this.startBtn.setStrokeStyle(1 * PX, COLORS.BUTTON_HOVER);
this.startBtn.setInteractive({ useHandCursor: true });
this.startBtnText = this.scene.add.text(
GAME.WIDTH / 2, UI.PANEL_Y - 22,
GAME.WIDTH / 2, UI.PANEL_Y - 22 * PX,
'Start Wave',
{
fontSize: UI.FONT_SIZE_MEDIUM,