// 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); } // 2.5 Area / Range Preview Highlight (Cherry Bomb 3x3, Jalapeno row, Potato Mine, Doom-shroom 5x5) if (hoverCell && selectedPlantType) { this.drawAreaPreview(ctx, hoverCell, selectedPlantType); } // 3. Hover placement indicator 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(); } drawAreaPreview(ctx, hoverCell, selectedPlantType) { if (!hoverCell || !selectedPlantType) return; ctx.save(); // 1. Cherry Bomb / Bowling Cherry: 3x3 explosion area if (selectedPlantType === 'cherrybomb' || selectedPlantType === 'bowling_cherry') { const minCol = Math.max(0, hoverCell.col - 1); const maxCol = Math.min(this.cols - 1, hoverCell.col + 1); const minRow = Math.max(0, hoverCell.row - 1); const maxRow = Math.min(this.rows - 1, hoverCell.row + 1); // Fill affected tiles for (let r = minRow; r <= maxRow; r++) { for (let c = minCol; c <= maxCol; c++) { const b = this.getCellBounds(c, r); ctx.fillStyle = 'rgba(255, 87, 34, 0.28)'; ctx.fillRect(b.x, b.y, b.w, b.h); ctx.strokeStyle = 'rgba(255, 112, 67, 0.45)'; ctx.lineWidth = 1; ctx.strokeRect(b.x + 1, b.y + 1, b.w - 2, b.h - 2); } } // Enclosing blast perimeter const tl = this.getCellBounds(minCol, minRow); const br = this.getCellBounds(maxCol, maxRow); const areaW = (br.x + br.w) - tl.x; const areaH = (br.y + br.h) - tl.y; ctx.strokeStyle = '#ff3d00'; ctx.lineWidth = 2.5; ctx.setLineDash([8, 4]); ctx.strokeRect(tl.x, tl.y, areaW, areaH); ctx.setLineDash([]); ctx.fillStyle = '#ff1744'; ctx.font = 'bold 11px sans-serif'; ctx.textAlign = 'left'; ctx.shadowColor = '#000000'; ctx.shadowBlur = 4; ctx.fillText('💥 ЗОНА ВЗРЫВА 3х3', tl.x + 6, tl.y - 4); } // 2. Jalapeno: Entire Row Lane else if (selectedPlantType === 'jalapeno') { const r = hoverCell.row; const startX = this.startX; const totalW = this.cols * this.cellW; const y = this.startY + r * this.cellH; const h = this.cellH; ctx.fillStyle = 'rgba(244, 67, 54, 0.28)'; ctx.fillRect(startX, y, totalW, h); // Fiery boundary lines ctx.strokeStyle = '#ff5722'; ctx.lineWidth = 2.5; ctx.beginPath(); ctx.moveTo(startX, y); ctx.lineTo(startX + totalW, y); ctx.moveTo(startX, y + h); ctx.lineTo(startX + totalW, y + h); ctx.stroke(); ctx.fillStyle = '#ff3d00'; ctx.font = 'bold 11px sans-serif'; ctx.textAlign = 'left'; ctx.shadowColor = '#000000'; ctx.shadowBlur = 4; ctx.fillText('🔥 ОГНЕННАЯ ЛИНИЯ ПО ВСЕЙ ПОЛОСЕ', startX + 6, y - 4); } // 3. Potato Mine: Single tile armed trigger zone else if (selectedPlantType === 'potatomine') { const b = this.getCellBounds(hoverCell.col, hoverCell.row); ctx.fillStyle = 'rgba(255, 193, 7, 0.25)'; ctx.fillRect(b.x, b.y, b.w, b.h); ctx.strokeStyle = '#ffb300'; ctx.lineWidth = 2; ctx.setLineDash([4, 4]); ctx.strokeRect(b.x + 3, b.y + 3, b.w - 6, b.h - 6); ctx.setLineDash([]); // Crosshair / target marker in tile center ctx.strokeStyle = 'rgba(255, 179, 0, 0.6)'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(b.centerX, b.centerY, 18, 0, Math.PI * 2); ctx.stroke(); ctx.fillStyle = '#ffa000'; ctx.font = 'bold 10px sans-serif'; ctx.textAlign = 'center'; ctx.shadowColor = '#000000'; ctx.shadowBlur = 3; ctx.fillText('💣 МИНА', b.centerX, b.y - 3); } // 4. Doom-shroom: Large 5x5 blast area else if (selectedPlantType === 'doomshroom') { const minCol = Math.max(0, hoverCell.col - 2); const maxCol = Math.min(this.cols - 1, hoverCell.col + 2); const minRow = Math.max(0, hoverCell.row - 2); const maxRow = Math.min(this.rows - 1, hoverCell.row + 2); for (let r = minRow; r <= maxRow; r++) { for (let c = minCol; c <= maxCol; c++) { const b = this.getCellBounds(c, r); ctx.fillStyle = 'rgba(156, 39, 176, 0.28)'; ctx.fillRect(b.x, b.y, b.w, b.h); } } const tl = this.getCellBounds(minCol, minRow); const br = this.getCellBounds(maxCol, maxRow); const areaW = (br.x + br.w) - tl.x; const areaH = (br.y + br.h) - tl.y; ctx.strokeStyle = '#ab47bc'; ctx.lineWidth = 2.5; ctx.setLineDash([8, 4]); ctx.strokeRect(tl.x, tl.y, areaW, areaH); ctx.setLineDash([]); ctx.fillStyle = '#ba68c8'; ctx.font = 'bold 11px sans-serif'; ctx.textAlign = 'left'; ctx.shadowColor = '#000000'; ctx.shadowBlur = 4; ctx.fillText('☢️ ЯДЕРНЫЙ ВЗРЫВ 5х5', tl.x + 6, tl.y - 4); } ctx.restore(); } };