Files
ai-qpid-pvz/game/js/systems/grid.js
T

564 lines
16 KiB
JavaScript

// Grid System (Day, Night with Graves, and Pool with Water Lanes)
window.GridSystem = class GridSystem {
constructor() {
this.biome = 'day'; // 'day', 'night', 'pool'
this.cols = 9;
this.startX = 120;
this.startY = 108; // Shifted down so zombie HP bars in top row are fully visible under HUD
this.cellW = 80;
this.graves = [];
this.animTime = 0;
this.bgCanvas = null;
this.bgCtx = null;
this.bgDirty = true;
this.initBiome('day');
}
initBackgroundCache() {
if (!this.bgCanvas) {
if (typeof document !== 'undefined') {
this.bgCanvas = document.createElement('canvas');
this.bgCanvas.width = 900;
this.bgCanvas.height = 600;
this.bgCtx = this.bgCanvas.getContext('2d');
}
}
}
invalidateBackground() {
this.bgDirty = true;
}
initBiome(biome = 'day') {
this.biome = biome;
this.rows = (biome === 'pool' || biome === 'fog') ? 6 : 5;
this.cellH = (biome === 'pool' || biome === 'fog') ? 80 : 96;
this.invalidateBackground();
this.grid = [];
for (let r = 0; r < this.rows; r++) {
this.grid[r] = [];
for (let c = 0; c < this.cols; c++) {
this.grid[r][c] = null;
}
}
this.graves = [];
if (biome === 'night') {
this.graves = [
{ col: 7, row: 1 },
{ col: 8, row: 3 },
{ col: 6, row: 4 }
];
}
this.initLawnmowers();
}
initLawnmowers() {
this.lawnmowers = [];
for (let r = 0; r < this.rows; r++) {
const isWater = (this.biome === 'pool' || this.biome === 'fog') && (r === 2 || r === 3);
this.lawnmowers.push({
row: r,
x: 65,
y: this.startY + r * this.cellH + this.cellH / 2,
active: false,
used: false,
speed: 650,
isCleaner: isWater
});
}
}
reset() {
this.initBiome(this.biome);
}
isWaterCell(row) {
return (this.biome === 'pool' || this.biome === 'fog') && (row === 2 || row === 3);
}
isRoof() {
return this.biome === 'roof';
}
isFog() {
return this.biome === 'fog';
}
hasGrave(col, row) {
return this.graves.some(g => g.col === col && g.row === row);
}
removeGrave(col, row) {
this.graves = this.graves.filter(g => !(g.col === col && g.row === row));
}
getCellFromCoords(x, y) {
if (x < this.startX || x >= this.startX + this.cols * this.cellW) return null;
if (y < this.startY || y >= this.startY + this.rows * this.cellH) return null;
const col = Math.floor((x - this.startX) / this.cellW);
const row = Math.floor((y - this.startY) / this.cellH);
if (col >= 0 && col < this.cols && row >= 0 && row < this.rows) {
return { col, row };
}
return null;
}
getCellBounds(col, row) {
return {
x: this.startX + col * this.cellW,
y: this.startY + row * this.cellH,
w: this.cellW,
h: this.cellH,
centerX: this.startX + col * this.cellW + this.cellW / 2,
centerY: this.startY + row * this.cellH + this.cellH / 2
};
}
getPlant(col, row) {
if (row >= 0 && row < this.rows && col >= 0 && col < this.cols) {
return this.grid[row][col];
}
return null;
}
setPlant(col, row, plant) {
if (row >= 0 && row < this.rows && col >= 0 && col < this.cols) {
this.grid[row][col] = plant;
}
}
removePlant(col, row) {
if (row >= 0 && row < this.rows && col >= 0 && col < this.cols) {
this.grid[row][col] = null;
}
}
updateLawnmowers(dt, zombies) {
this.animTime += dt;
for (const mower of this.lawnmowers) {
if (mower.used) continue;
if (mower.active) {
mower.x += mower.speed * dt;
for (const z of zombies) {
if (z.row === mower.row && Math.abs(z.x - mower.x) < 45 && z.health > 0) {
z.takeDamage(9999);
}
}
if (mower.x > 960) {
mower.used = true;
mower.active = false;
}
} else {
for (const z of zombies) {
if (z.row === mower.row && z.x <= 115 && z.health > 0) {
mower.active = true;
if (window.PvZAudio) window.PvZAudio.playMower();
break;
}
}
}
}
}
get theme() {
return this._theme || 'classic';
}
set theme(val) {
if (this._theme !== val) {
this._theme = val;
this.invalidateBackground();
}
}
renderBackgroundCache() {
this.initBackgroundCache();
if (!this.bgCtx) return;
const ctx = this.bgCtx;
ctx.clearRect(0, 0, 900, 600);
// 1. Draw Static Lawn Tiles or Pool Base
for (let r = 0; r < this.rows; r++) {
const isWater = this.isWaterCell(r);
for (let c = 0; c < this.cols; c++) {
const x = this.startX + c * this.cellW;
const y = this.startY + r * this.cellH;
const isEven = (r + c) % 2 === 0;
if (this.biome === 'roof') {
// Terracotta Roof Shingles
ctx.fillStyle = isEven ? '#bf360c' : '#d84315';
ctx.fillRect(x, y, this.cellW, this.cellH);
// Slanted shingle pattern lines
ctx.strokeStyle = 'rgba(0, 0, 0, 0.22)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(x, y + this.cellH * 0.5);
ctx.lineTo(x + this.cellW, y + this.cellH * 0.5);
ctx.stroke();
ctx.strokeStyle = 'rgba(255, 255, 255, 0.12)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(x + 10, y);
ctx.lineTo(x + 20, y + this.cellH);
ctx.moveTo(x + 50, y);
ctx.lineTo(x + 60, y + this.cellH);
ctx.stroke();
} else if (isWater) {
// Pool Water Base
ctx.fillStyle = isEven ? '#0288d1' : '#039be5';
ctx.fillRect(x, y, this.cellW, this.cellH);
} else if (this.theme === 'cyberpunk') {
// Cyberpunk Grid Tiles
ctx.fillStyle = isEven ? '#120326' : '#1d063d';
ctx.fillRect(x, y, this.cellW, this.cellH);
ctx.strokeStyle = 'rgba(0, 229, 255, 0.25)';
ctx.lineWidth = 1;
ctx.strokeRect(x, y, this.cellW, this.cellH);
ctx.fillStyle = 'rgba(233, 30, 99, 0.4)';
ctx.fillRect(x + this.cellW - 3, y + this.cellH - 3, 3, 3);
} else if (this.theme === 'winter') {
// Winter Frost Lawn
ctx.fillStyle = isEven ? '#e0f7fa' : '#b2ebf2';
ctx.fillRect(x, y, this.cellW, this.cellH);
ctx.strokeStyle = 'rgba(255, 255, 255, 0.6)';
ctx.lineWidth = 1;
ctx.strokeRect(x, y, this.cellW, this.cellH);
} else if (this.theme === 'retro8bit') {
// Retro 8-bit Pixel Lawn
ctx.fillStyle = isEven ? '#15803d' : '#166534';
ctx.fillRect(x, y, this.cellW, this.cellH);
ctx.strokeStyle = '#10b981';
ctx.lineWidth = 1.5;
ctx.strokeRect(x, y, this.cellW, this.cellH);
} else if (this.biome === 'night' || this.biome === 'fog') {
// Dark Night Lawn
ctx.fillStyle = isEven ? '#1b5e20' : '#144618';
ctx.fillRect(x, y, this.cellW, this.cellH);
} else {
// Day Lawn
ctx.fillStyle = isEven ? '#4caf50' : '#43a047';
ctx.fillRect(x, y, this.cellW, this.cellH);
}
ctx.strokeStyle = 'rgba(0, 0, 0, 0.08)';
ctx.lineWidth = 1;
ctx.strokeRect(x, y, this.cellW, this.cellH);
}
}
// 2. Left Porch Deck / Roof Chimney
if (this.biome === 'roof') {
this.drawRoofBackground(ctx);
} else {
ctx.fillStyle = (this.biome === 'night' || this.biome === 'fog') ? '#4e342e' : '#795548';
ctx.fillRect(0, this.startY, this.startX, this.rows * this.cellH);
ctx.fillStyle = '#3e2723';
ctx.fillRect(this.startX - 6, this.startY, 6, this.rows * this.cellH);
// 3. Right Grass Edge
const rightEdge = this.startX + this.cols * this.cellW;
ctx.fillStyle = (this.biome === 'night' || this.biome === 'fog') ? '#0d280f' : '#388e3c';
ctx.fillRect(rightEdge, this.startY, 900 - rightEdge, this.rows * this.cellH);
}
// 2b. Decorative Neighbour Fence along top edge (y: 86..108)
this.drawNeighbourFence(ctx);
this.bgDirty = false;
}
draw(ctx, hoverCell, selectedPlantType) {
// 1. Offscreen Canvas Cached Static Background (Lawn, Porch/Chimney, Fence, Water Base)
if (this.bgDirty || !this.bgCanvas) {
this.renderBackgroundCache();
}
if (this.bgCanvas) {
ctx.drawImage(this.bgCanvas, 0, 0);
}
// 1b. Dynamic Animated Water Ripples (Pool & Fog)
if (this.biome === 'pool' || this.biome === 'fog') {
ctx.save();
ctx.strokeStyle = 'rgba(255, 255, 255, 0.25)';
ctx.lineWidth = 1.5;
for (let r = 2; r <= 3; r++) {
for (let c = 0; c < this.cols; c++) {
const x = this.startX + c * this.cellW;
const y = this.startY + r * this.cellH;
const waveOff = Math.sin(this.animTime * 3 + c * 0.8 + r) * 3;
ctx.beginPath();
ctx.moveTo(x + 10, y + 20 + waveOff);
ctx.quadraticCurveTo(x + 40, y + 15 + waveOff, x + 70, y + 20 + waveOff);
ctx.stroke();
}
}
ctx.restore();
}
// 2. Draw Gravestones in Night Mode
for (const grave of this.graves) {
const bounds = this.getCellBounds(grave.col, grave.row);
this.drawGravestone(ctx, bounds.centerX, bounds.centerY);
}
// 3. Hover effect
if (hoverCell && selectedPlantType) {
const bounds = this.getCellBounds(hoverCell.col, hoverCell.row);
const isWater = this.isWaterCell(hoverCell.row);
const plant = this.getPlant(hoverCell.col, hoverCell.row);
const isGrave = this.hasGrave(hoverCell.col, hoverCell.row);
let canPlace = false;
if (selectedPlantType === 'shovel') {
canPlace = !!plant;
} else if (this.isRoof()) {
if (selectedPlantType === 'flowerpot') {
canPlace = !plant;
} else {
canPlace = plant && plant.type === 'flowerpot';
}
} else if (selectedPlantType === 'gravebuster') {
canPlace = isGrave && !plant;
} else if (isGrave) {
canPlace = false;
} else if (isWater) {
canPlace = selectedPlantType === 'lilypad' || (plant && plant.type === 'lilypad');
} else {
canPlace = !plant;
}
ctx.fillStyle = canPlace ? 'rgba(255, 255, 255, 0.35)' : 'rgba(244, 67, 54, 0.35)';
ctx.fillRect(bounds.x, bounds.y, bounds.w, bounds.h);
ctx.strokeStyle = canPlace ? '#ffffff' : '#f44336';
ctx.lineWidth = 2;
ctx.strokeRect(bounds.x, bounds.y, bounds.w, bounds.h);
}
// 4. Draw Lawnmowers & Pool Cleaners
for (const mower of this.lawnmowers) {
if (mower.used) continue;
if (mower.isCleaner) {
this.drawPoolCleaner(ctx, mower.x, mower.y);
} else {
this.drawLawnmower(ctx, mower.x, mower.y);
}
}
// 5. Volumetric Fog Layer
if (this.biome === 'fog') {
this.drawFogOverlay(ctx);
}
}
drawRoofBackground(ctx) {
// Porch and terracotta roof chimney
ctx.fillStyle = '#4e342e';
ctx.fillRect(0, this.startY, this.startX, this.rows * this.cellH);
ctx.fillStyle = '#8d6e63';
ctx.fillRect(25, this.startY + 40, 65, 120);
ctx.strokeStyle = '#3e2723';
ctx.lineWidth = 2;
ctx.strokeRect(25, this.startY + 40, 65, 120);
// Right roof edge
const rightEdge = this.startX + this.cols * this.cellW;
ctx.fillStyle = '#a02700';
ctx.fillRect(rightEdge, this.startY, 900 - rightEdge, this.rows * this.cellH);
}
drawFogOverlay(ctx) {
ctx.save();
for (let c = 4; c < this.cols; c++) {
const fx = this.startX + c * this.cellW;
const drift = Math.sin(this.animTime * 1.2 + c) * 8;
ctx.fillStyle = 'rgba(38, 50, 56, 0.78)';
ctx.fillRect(fx + drift, this.startY, this.cellW + 12, this.rows * this.cellH);
}
for (let i = 0; i < 7; i++) {
const cx = 500 + i * 55 + Math.sin(this.animTime * 0.9 + i) * 18;
const cy = this.startY + (i * 70) % (this.rows * this.cellH);
ctx.fillStyle = 'rgba(69, 90, 100, 0.42)';
ctx.beginPath();
ctx.arc(cx, cy, 46, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
drawGravestone(ctx, x, y) {
ctx.save();
ctx.translate(x, y + 10);
// Stone base shadow
ctx.fillStyle = 'rgba(0, 0, 0, 0.3)';
ctx.beginPath();
ctx.ellipse(0, 24, 26, 8, 0, 0, Math.PI * 2);
ctx.fill();
// Tombstone body
ctx.fillStyle = '#757575';
ctx.beginPath();
ctx.roundRect(-22, -26, 44, 48, [16, 16, 2, 2]);
ctx.fill();
ctx.strokeStyle = '#424242';
ctx.lineWidth = 2;
ctx.stroke();
// RIP text
ctx.fillStyle = '#212121';
ctx.font = 'bold 12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('R.I.P.', 0, -2);
// Crack
ctx.strokeStyle = '#212121';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(-10, 8);
ctx.lineTo(-4, 14);
ctx.lineTo(-8, 20);
ctx.stroke();
ctx.restore();
}
drawLawnmower(ctx, x, y) {
ctx.save();
ctx.translate(x, y);
ctx.fillStyle = '#212121';
ctx.beginPath();
ctx.arc(-14, 16, 7, 0, Math.PI * 2);
ctx.arc(14, 16, 7, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#9e9e9e';
ctx.beginPath();
ctx.arc(-14, 16, 3, 0, Math.PI * 2);
ctx.arc(14, 16, 3, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#d32f2f';
ctx.beginPath();
ctx.roundRect(-22, -6, 44, 20, [6, 6, 2, 2]);
ctx.fill();
ctx.fillStyle = '#424242';
ctx.fillRect(-10, -18, 20, 14);
ctx.strokeStyle = '#bdbdbd';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(-18, 0);
ctx.lineTo(-32, -26);
ctx.stroke();
ctx.restore();
}
drawPoolCleaner(ctx, x, y) {
ctx.save();
ctx.translate(x, y);
// Blue floating cleaner
ctx.fillStyle = '#0288d1';
ctx.beginPath();
ctx.roundRect(-20, -10, 40, 24, 8);
ctx.fill();
// Yellow propeller
ctx.fillStyle = '#ffeb3b';
ctx.beginPath();
ctx.arc(0, 2, 8, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
drawNeighbourFence(ctx) {
if (this.biome === 'roof') return; // Roof has shingles/gutters instead
const fenceY = 88;
const fenceH = 20;
ctx.save();
// 1. Shadow under fence on the lawn
ctx.fillStyle = 'rgba(0, 0, 0, 0.28)';
ctx.fillRect(0, fenceY + fenceH - 2, 900, 4);
// 2. Dirt/Base Strip
ctx.fillStyle = (this.biome === 'night' || this.biome === 'fog') ? '#1a0f07' : '#3e2723';
ctx.fillRect(0, fenceY + fenceH - 4, 900, 4);
// 3. Horizontal Support Rails
const isNight = this.biome === 'night' || this.biome === 'fog';
const isWinter = this.theme === 'winter';
const isCyber = this.theme === 'cyberpunk';
let railColor = isNight ? '#4e342e' : '#8d6e63';
let picketColor = isNight ? '#5d4037' : '#d7ccc8';
let picketTopColor = isNight ? '#795548' : '#efebe9';
if (isWinter) {
picketColor = '#eceff1';
picketTopColor = '#ffffff';
railColor = '#b0bec5';
} else if (isCyber) {
picketColor = '#21083a';
picketTopColor = '#00e5ff';
railColor = '#e91e63';
}
// Top & Bottom Horizontal Rails
ctx.fillStyle = railColor;
ctx.fillRect(0, fenceY + 4, 900, 3);
ctx.fillRect(0, fenceY + 12, 900, 3);
// 4. Vertical Pickets (Wooden Fence Slats)
const picketWidth = 8;
const picketGap = 16;
for (let x = 4; x < 900; x += picketGap) {
// Picket Body
ctx.fillStyle = picketColor;
ctx.fillRect(x, fenceY + 2, picketWidth, fenceH - 4);
// Pointy Picket Cap
ctx.fillStyle = picketTopColor;
ctx.beginPath();
ctx.moveTo(x, fenceY + 2);
ctx.lineTo(x + picketWidth / 2, fenceY - 2);
ctx.lineTo(x + picketWidth, fenceY + 2);
ctx.closePath();
ctx.fill();
// Nail / Wood detail
ctx.fillStyle = 'rgba(0, 0, 0, 0.4)';
ctx.fillRect(x + 3, fenceY + 5, 2, 2);
ctx.fillRect(x + 3, fenceY + 13, 2, 2);
// Winter snow cap
if (isWinter) {
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(x + picketWidth / 2, fenceY - 2, 3, 0, Math.PI * 2);
ctx.fill();
}
}
ctx.restore();
}
};