mirror of
https://github.com/PlayableIntelligence/game-creator.git
synced 2026-09-19 07:34:10 +08:00
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:
@@ -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>
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 },
|
||||
};
|
||||
|
||||
@@ -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 },
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user