feat: Initial commit Plants vs Zombies Web Edition v2.4
- 5 Biomes: Day, Night, Pool, Fog, Roof (25 Campaign levels) - Minigames: Wall-nut Bowling, I Zombie, Roguelike, Boss Rush & Dr. Zomboss - Systems: Zen Garden, Workshop Crafting, Almanac, Tree of Wisdom, Achievements - Backend: REST API + SQLite Accounts & Save Synchronization - Infrastructure: Nginx HTTPS & Docker deployment
This commit is contained in:
@@ -0,0 +1,428 @@
|
||||
// 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 = 90;
|
||||
this.cellW = 80;
|
||||
|
||||
this.graves = [];
|
||||
this.animTime = 0;
|
||||
this.initBiome('day');
|
||||
}
|
||||
|
||||
initBiome(biome = 'day') {
|
||||
this.biome = biome;
|
||||
this.rows = (biome === 'pool' || biome === 'fog') ? 6 : 5;
|
||||
this.cellH = (biome === 'pool' || biome === 'fog') ? 83 : 100;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
draw(ctx, hoverCell, selectedPlantType) {
|
||||
// 1. Draw Lawn Tiles or Pool
|
||||
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
|
||||
ctx.fillStyle = isEven ? '#0288d1' : '#039be5';
|
||||
ctx.fillRect(x, y, this.cellW, this.cellH);
|
||||
|
||||
// Subtle animated water ripples
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.25)';
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.beginPath();
|
||||
const waveOff = Math.sin(this.animTime * 3 + c * 0.8 + r) * 3;
|
||||
ctx.moveTo(x + 10, y + 20 + waveOff);
|
||||
ctx.quadraticCurveTo(x + 40, y + 15 + waveOff, x + 70, y + 20 + waveOff);
|
||||
ctx.stroke();
|
||||
} 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);
|
||||
}
|
||||
|
||||
// 4. 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);
|
||||
}
|
||||
|
||||
// 5. 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);
|
||||
}
|
||||
|
||||
// 6. 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);
|
||||
}
|
||||
}
|
||||
|
||||
// 7. 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();
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user