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:
2026-09-17 09:30:52 +00:00
commit 6457a8904e
50 changed files with 14486 additions and 0 deletions
+741
View File
@@ -0,0 +1,741 @@
// Economy, Sun, Coin Drops & Crazy Dave's Shop System
window.EconomySystem = class EconomySystem {
constructor() {
this.sun = 50;
this.suns = [];
this.coins = [];
this.skySunTimer = 0;
this.skySunInterval = 8.5;
this.isNight = false;
this.isConveyor = false; // For Wall-nut bowling mini-game
this.isIZombie = false;
// Total coins & upgrades persistent storage
this.reloadProfileData();
// Full Card Registry
this.allCards = [
{ id: 'sunflower', name: 'Подсолнух', cost: 50, cooldown: 7.5, timer: 0 },
{ id: 'peashooter', name: 'Горохострел', cost: 100, cooldown: 7.5, timer: 0 },
{ id: 'potatomine', name: 'Мина', cost: 25, cooldown: 18.0, timer: 0 },
{ id: 'wallnut', name: 'Стеноорех', cost: 50, cooldown: 20.0, timer: 0 },
{ id: 'repeater', name: 'Повторитель', cost: 200, cooldown: 7.5, timer: 0 },
{ id: 'snowpea', name: 'Ледяной', cost: 175, cooldown: 10.0, timer: 0 },
{ id: 'chomper', name: 'Зубастик', cost: 150, cooldown: 10.0, timer: 0 },
{ id: 'cherrybomb', name: 'Вишня', cost: 150, cooldown: 25.0, timer: 0 },
{ id: 'jalapeno', name: 'Халапеньо', cost: 125, cooldown: 30.0, timer: 0 },
{ id: 'sunshroom', name: 'Солнцегриб', cost: 25, cooldown: 7.5, timer: 0 },
{ id: 'fumeshroom', name: 'Дымогриб', cost: 75, cooldown: 7.5, timer: 0 },
{ id: 'lilypad', name: 'Кувшинка', cost: 25, cooldown: 7.5, timer: 0 },
{ id: 'gravebuster', name: 'Могилоед', cost: 75, cooldown: 15.0, timer: 0 },
{ id: 'flowerpot', name: 'Горшок', cost: 25, cooldown: 7.5, timer: 0 },
{ id: 'cabbagepult', name: 'Капуста', cost: 100, cooldown: 7.5, timer: 0 },
{ id: 'melonpult', name: 'Арбуз', cost: 300, cooldown: 15.0, timer: 0 }
];
this.selectedCard = null;
this.animTime = 0;
this.setDeckForLevel(1, 'day');
}
reloadProfileData() {
if (window.PvZAuth) {
const uData = window.PvZAuth.getUserData();
this.totalCoins = uData.coins !== undefined ? uData.coins : 250;
this.upgrades = uData.upgrades || { extraSlot: false, hasRake: false };
} else {
this.totalCoins = parseInt(localStorage.getItem('pvz_coins') || '250', 10);
this.upgrades = {
extraSlot: localStorage.getItem('pvz_upg_slot') === 'true',
hasRake: localStorage.getItem('pvz_upg_rake') === 'true'
};
}
}
saveProfileData() {
localStorage.setItem('pvz_coins', this.totalCoins.toString());
localStorage.setItem('pvz_upg_slot', this.upgrades.extraSlot ? 'true' : 'false');
localStorage.setItem('pvz_upg_rake', this.upgrades.hasRake ? 'true' : 'false');
if (window.PvZAuth && !window.PvZAuth.isGuest()) {
window.PvZAuth.saveUserData({
coins: this.totalCoins,
upgrades: this.upgrades
});
}
}
setDeckForLevel(level = 1, biome = 'day') {
this.isNight = biome === 'night';
this.isConveyor = false;
this.isIZombie = false;
// Card slots count (5 default, 6 with Dave's extra slot upgrade)
const slotCount = this.upgrades.extraSlot ? 6 : 5;
// Choose relevant deck based on biome and progression
let availableIds = [];
if (biome === 'roof') {
availableIds = ['flowerpot', 'cabbagepult', 'melonpult', 'wallnut', 'jalapeno', 'potatomine'];
} else if (biome === 'fog') {
availableIds = ['sunshroom', 'fumeshroom', 'lilypad', 'cabbagepult', 'wallnut', 'snowpea'];
} else if (biome === 'night') {
availableIds = ['sunshroom', 'fumeshroom', 'potatomine', 'wallnut', 'gravebuster', 'hypnoshroom'];
} else if (biome === 'pool') {
availableIds = ['sunflower', 'peashooter', 'lilypad', 'wallnut', 'repeater', 'snowpea'];
} else {
if (level === 1) availableIds = ['sunflower', 'peashooter', 'potatomine', 'wallnut', 'cherrybomb'];
else if (level === 2) availableIds = ['sunflower', 'peashooter', 'repeater', 'wallnut', 'jalapeno'];
else availableIds = ['sunflower', 'peashooter', 'snowpea', 'chomper', 'wallnut', 'jalapeno'];
}
this.cards = this.allCards.filter(c => availableIds.includes(c.id)).slice(0, slotCount);
}
setBossDeck() {
this.isConveyor = true;
this.isIZombie = false;
this.cards = [
{ id: 'flowerpot', name: 'Горшок', cost: 0, cooldown: 3.5, timer: 0 },
{ id: 'cabbagepult', name: 'Капуста', cost: 0, cooldown: 5.0, timer: 0 },
{ id: 'melonpult', name: 'Арбуз', cost: 0, cooldown: 10.0, timer: 0 },
{ id: 'jalapeno', name: 'Халапеньо', cost: 0, cooldown: 14.0, timer: 0 },
{ id: 'snowpea', name: 'Ледяной', cost: 0, cooldown: 8.0, timer: 0 }
];
}
setConveyorDeck() {
this.isConveyor = true;
this.isIZombie = false;
this.cards = [
{ id: 'bowling_nut', name: 'Орех-шар', cost: 0, cooldown: 4.0, timer: 0 },
{ id: 'bowling_cherry', name: 'Взрывной', cost: 0, cooldown: 12.0, timer: 0 }
];
}
setIZombieDeck() {
this.isConveyor = false;
this.isIZombie = true;
this.isNight = true;
this.sun = 200;
this.cards = [
{ id: 'z_normal', name: 'Обычный', cost: 50, cooldown: 2.0, timer: 0 },
{ id: 'z_conehead', name: 'Конус', cost: 75, cooldown: 3.0, timer: 0 },
{ id: 'z_polevaulter', name: 'С шестом', cost: 75, cooldown: 4.0, timer: 0 },
{ id: 'z_buckethead', name: 'Ведро', cost: 125, cooldown: 5.0, timer: 0 },
{ id: 'z_football', name: 'Футболист', cost: 175, cooldown: 6.0, timer: 0 }
];
}
reset() {
let baseSun = this.isConveyor ? 0 : (this.isIZombie ? 200 : 50);
if (!this.isConveyor && !this.isIZombie && this.workshop) {
baseSun += this.workshop.getPerkLevel('startSun') * 50;
}
this.sun = baseSun;
this.suns = [];
this.coins = [];
this.skySunTimer = 0;
this.selectedCard = null;
this.cards.forEach(c => c.timer = 0);
}
addSun(amount) {
this.sun += amount;
if (this.engine && this.engine.achievements) {
this.engine.achievements.setProgress('sun_tycoon', this.sun);
}
}
canAfford(cost) {
return this.isConveyor || this.sun >= cost;
}
spendSun(cost) {
if (this.isConveyor) return true;
if (this.sun >= cost) {
this.sun -= cost;
return true;
}
return false;
}
addCoins(amount) {
this.totalCoins += amount;
this.saveProfileData();
}
buyUpgrade(type) {
if (type === 'extraSlot' && this.totalCoins >= 500 && !this.upgrades.extraSlot) {
this.addCoins(-500);
this.upgrades.extraSlot = true;
this.saveProfileData();
this.setDeckForLevel(1, this.isNight ? 'night' : 'day');
return true;
} else if (type === 'rake' && this.totalCoins >= 150 && !this.upgrades.hasRake) {
this.addCoins(-150);
this.upgrades.hasRake = true;
this.saveProfileData();
return true;
}
return false;
}
spawnSun(x, y, value = 25, isFalling = false, targetY = null) {
this.suns.push({
x,
y,
targetY: targetY !== null ? targetY : y,
value,
isFalling,
fallSpeed: 90,
radius: 26,
life: 13.0,
collecting: false,
toRemove: false,
pulse: Math.random() * 5
});
}
spawnCoin(x, y, type = 'silver') {
const val = type === 'diamond' ? 200 : (type === 'gold' ? 50 : 10);
this.coins.push({
x,
y,
type,
value: val,
radius: 16,
life: 12.0,
collecting: false,
toRemove: false,
pulse: Math.random() * 5
});
}
update(dt) {
this.animTime += dt;
// Cooldown timers with fastCool perk bonus
const fastCoolLvl = this.workshop ? this.workshop.getPerkLevel('fastCool') : 0;
const cooldownMultiplier = 1 + fastCoolLvl * 0.15;
for (const card of this.cards) {
if (card.timer > 0) {
card.timer = Math.max(0, card.timer - dt * cooldownMultiplier);
}
}
// Sky sun spawner (disabled at night and in conveyor mode)
if (!this.isNight && !this.isConveyor) {
this.skySunTimer += dt;
if (this.skySunTimer >= this.skySunInterval) {
this.skySunTimer = 0;
const spawnX = 140 + Math.random() * 640;
const targetY = 140 + Math.random() * 380;
this.spawnSun(spawnX, 40, 25, true, targetY);
}
}
// Active suns
for (const s of this.suns) {
s.pulse += dt * 4;
if (s.collecting) {
const targetX = 50;
const targetY = 45;
const dx = targetX - s.x;
const dy = targetY - s.y;
const dist = Math.hypot(dx, dy);
if (dist < 15) {
this.addSun(s.value);
s.toRemove = true;
} else {
s.x += (dx / dist) * 750 * dt;
s.y += (dy / dist) * 750 * dt;
}
} else {
if (s.isFalling && s.y < s.targetY) {
s.y += s.fallSpeed * dt;
}
s.life -= dt;
if (s.life <= 0) s.toRemove = true;
}
}
this.suns = this.suns.filter(s => !s.toRemove);
// Active coins
for (const c of this.coins) {
c.pulse += dt * 5;
if (c.collecting) {
const targetX = 850;
const targetY = 45;
const dx = targetX - c.x;
const dy = targetY - c.y;
const dist = Math.hypot(dx, dy);
if (dist < 15) {
this.addCoins(c.value);
c.toRemove = true;
} else {
c.x += (dx / dist) * 750 * dt;
c.y += (dy / dist) * 750 * dt;
}
} else {
c.life -= dt;
if (c.life <= 0) c.toRemove = true;
}
}
this.coins = this.coins.filter(c => !c.toRemove);
}
handleClick(mouseX, mouseY) {
// 0. Workshop Floating Loot
if (this.workshop) {
for (let i = this.workshop.floatingLoot.length - 1; i >= 0; i--) {
const loot = this.workshop.floatingLoot[i];
if (!loot.collected && Math.hypot(mouseX - loot.x, mouseY - loot.y) <= 30) {
this.workshop.collectLoot(loot);
return { handled: true, action: 'loot_collected' };
}
}
}
// 1. Suns
for (let i = this.suns.length - 1; i >= 0; i--) {
const s = this.suns[i];
if (!s.collecting && Math.hypot(mouseX - s.x, mouseY - s.y) <= s.radius + 14) {
s.collecting = true;
if (window.PvZAudio) window.PvZAudio.playSun();
if (this.workshop) {
this.sunCount = (this.sunCount || 0) + 1;
if (this.sunCount >= 8) {
this.sunCount = 0;
this.workshop.addMaterial('sunEssence', 1);
}
}
return { handled: true, action: 'sun_collected' };
}
}
// 2. Coins
for (let i = this.coins.length - 1; i >= 0; i--) {
const c = this.coins[i];
if (!c.collecting && Math.hypot(mouseX - c.x, mouseY - c.y) <= c.radius + 14) {
c.collecting = true;
if (window.PvZAudio) window.PvZAudio.playCoin();
return { handled: true, action: 'coin_collected' };
}
}
// 3. Seed Cards
const cardStartX = 105;
const cardY = 8;
const cardW = 66;
const cardH = 72;
const spacing = 72;
for (let i = 0; i < this.cards.length; i++) {
const card = this.cards[i];
const cx = cardStartX + i * spacing;
if (mouseX >= cx && mouseX <= cx + cardW && mouseY >= cardY && mouseY <= cardY + cardH) {
if (this.canAfford(card.cost) && card.timer <= 0) {
this.selectedCard = (this.selectedCard === card.id) ? null : card.id;
}
return { handled: true, action: 'card_selected' };
}
}
// 4. Shovel
const shovelX = cardStartX + this.cards.length * spacing + 12;
if (mouseX >= shovelX && mouseX <= shovelX + 54 && mouseY >= cardY && mouseY <= cardY + cardH) {
this.selectedCard = (this.selectedCard === 'shovel') ? null : 'shovel';
return { handled: true, action: 'shovel_selected' };
}
return { handled: false };
}
startCooldown(cardId) {
const card = this.cards.find(c => c.id === cardId);
if (card) {
card.timer = card.cooldown;
}
}
draw(ctx) {
// HUD Bar Background
ctx.fillStyle = this.isNight ? '#1a100c' : '#2c1810';
ctx.fillRect(0, 0, 900, 90);
ctx.fillStyle = this.isNight ? '#2a1810' : '#422817';
ctx.fillRect(0, 0, 900, 86);
// 1. Sun Bank Box (or Conveyor indicator)
if (!this.isConveyor) {
ctx.fillStyle = '#e0c080';
ctx.beginPath();
ctx.roundRect(10, 8, 85, 72, 8);
ctx.fill();
ctx.strokeStyle = '#6d4c41';
ctx.lineWidth = 3;
ctx.stroke();
this.drawSunGraphic(ctx, 36, 34, 16, 0, 1);
ctx.fillStyle = '#000';
ctx.font = 'bold 18px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(this.sun, 52, 66);
} else {
// Conveyor belt indicator
ctx.fillStyle = '#616161';
ctx.beginPath();
ctx.roundRect(10, 8, 85, 72, 8);
ctx.fill();
ctx.fillStyle = '#fff';
ctx.font = 'bold 12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('КОНВЕЙЕР', 52, 48);
}
// 2. Draw Cards
const cardStartX = 105;
const cardY = 8;
const cardW = 66;
const cardH = 72;
const spacing = 72;
for (let i = 0; i < this.cards.length; i++) {
const card = this.cards[i];
const cx = cardStartX + i * spacing;
const isSelected = this.selectedCard === card.id;
const affordable = this.canAfford(card.cost);
const ready = card.timer <= 0;
ctx.fillStyle = isSelected ? '#fff9c4' : (affordable && ready ? '#fff8e1' : '#b0bec5');
ctx.beginPath();
ctx.roundRect(cx, cardY, cardW, cardH, 6);
ctx.fill();
ctx.strokeStyle = isSelected ? '#ffeb3b' : (affordable && ready ? '#8d6e63' : '#546e7a');
ctx.lineWidth = isSelected ? 3 : 2;
ctx.stroke();
this.drawCardThumbnail(ctx, card.id, cx + cardW / 2, cardY + 28);
if (this.workshop) {
const lvl = this.workshop.getPlantLevel(card.id);
if (lvl > 1) {
ctx.fillStyle = lvl === 3 ? '#e65100' : '#f57f17';
ctx.font = 'bold 9px sans-serif';
ctx.textAlign = 'right';
ctx.fillText(lvl === 3 ? '★★★' : '★★', cx + cardW - 4, cardY + 11);
}
}
ctx.fillStyle = '#263238';
ctx.font = 'bold 9px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(card.name, cx + cardW / 2, cardY + 54);
if (!this.isConveyor) {
ctx.fillStyle = affordable ? '#1b5e20' : '#b71c1c';
ctx.font = 'bold 11px sans-serif';
ctx.fillText(card.cost, cx + cardW / 2, cardY + 68);
} else {
ctx.fillStyle = '#1b5e20';
ctx.font = 'bold 10px sans-serif';
ctx.fillText('БЕСПЛАТНО', cx + cardW / 2, cardY + 68);
}
if (card.timer > 0) {
const cdRatio = card.timer / card.cooldown;
ctx.fillStyle = 'rgba(0, 0, 0, 0.55)';
ctx.beginPath();
ctx.rect(cx, cardY, cardW, cardH * cdRatio);
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 13px sans-serif';
ctx.fillText(Math.ceil(card.timer), cx + cardW / 2, cardY + cardH / 2 + 5);
}
}
// 3. Shovel
const shovelX = cardStartX + this.cards.length * spacing + 12;
const isShovelSelected = this.selectedCard === 'shovel';
ctx.fillStyle = isShovelSelected ? '#ffcdd2' : '#efebe9';
ctx.beginPath();
ctx.roundRect(shovelX, cardY, 54, cardH, 6);
ctx.fill();
ctx.strokeStyle = isShovelSelected ? '#d32f2f' : '#8d6e63';
ctx.lineWidth = isShovelSelected ? 3 : 2;
ctx.stroke();
this.drawShovelGraphic(ctx, shovelX + 27, cardY + 36);
ctx.fillStyle = '#37474f';
ctx.font = 'bold 9px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('Лопата', shovelX + 27, cardY + 66);
// 4. Coin Bag / Wallet in Top Right
ctx.fillStyle = '#ffe082';
ctx.beginPath();
ctx.roundRect(595, 20, 80, 26, 4);
ctx.fill();
ctx.strokeStyle = '#ff8f00';
ctx.lineWidth = 1.5;
ctx.stroke();
// Coin icon
ctx.fillStyle = '#ffd700';
ctx.beginPath();
ctx.arc(608, 33, 7, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#3e2723';
ctx.font = 'bold 12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(`💰 ${this.totalCoins}`, 645, 38);
// 5. Active Suns
for (const s of this.suns) {
const alpha = Math.min(1, s.life / 1.5);
this.drawSunGraphic(ctx, s.x, s.y, s.radius, s.pulse, alpha);
}
// 6. Active Coins
for (const c of this.coins) {
this.drawCoinGraphic(ctx, c.x, c.y, c.radius, c.type, c.pulse);
}
}
drawSunGraphic(ctx, x, y, radius, pulseAngle, alpha = 1) {
ctx.save();
ctx.globalAlpha = alpha;
ctx.translate(x, y);
const numRays = 12;
ctx.fillStyle = '#fbc02d';
for (let i = 0; i < numRays; i++) {
ctx.save();
ctx.rotate(pulseAngle + (i * Math.PI * 2) / numRays);
ctx.beginPath();
ctx.moveTo(-radius * 0.25, -radius * 0.8);
ctx.lineTo(0, -radius * 1.35);
ctx.lineTo(radius * 0.25, -radius * 0.8);
ctx.closePath();
ctx.fill();
ctx.restore();
}
const grad = ctx.createRadialGradient(0, 0, radius * 0.3, 0, 0, radius);
grad.addColorStop(0, '#fffde7');
grad.addColorStop(0.5, '#fff176');
grad.addColorStop(1, '#fbc02d');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(0, 0, radius, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
drawCoinGraphic(ctx, x, y, radius, type, pulse) {
ctx.save();
ctx.translate(x, y);
const spin = Math.sin(pulse * 3);
ctx.scale(spin, 1);
if (type === 'gold') {
ctx.fillStyle = '#ffd700';
ctx.beginPath();
ctx.arc(0, 0, radius, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#b78103';
ctx.lineWidth = 2;
ctx.stroke();
ctx.fillStyle = '#b78103';
ctx.font = 'bold 12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('$', 0, 4);
} else if (type === 'diamond') {
ctx.fillStyle = '#80deea';
ctx.beginPath();
ctx.moveTo(0, -radius);
ctx.lineTo(radius, 0);
ctx.lineTo(0, radius);
ctx.lineTo(-radius, 0);
ctx.closePath();
ctx.fill();
} else {
// Silver
ctx.fillStyle = '#e0e0e0';
ctx.beginPath();
ctx.arc(0, 0, radius, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#757575';
ctx.lineWidth = 2;
ctx.stroke();
}
ctx.restore();
}
drawCardThumbnail(ctx, type, x, y) {
ctx.save();
ctx.translate(x, y);
ctx.scale(0.52, 0.52);
if (type === 'sunflower') {
ctx.fillStyle = '#fbc02d';
for (let i = 0; i < 8; i++) {
ctx.save();
ctx.rotate((i * Math.PI * 2) / 8);
ctx.beginPath();
ctx.ellipse(0, -18, 6, 12, 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
ctx.fillStyle = '#5d4037';
ctx.beginPath();
ctx.arc(0, 0, 14, 0, Math.PI * 2);
ctx.fill();
} else if (type === 'peashooter' || type === 'snowpea' || type === 'repeater') {
const col = type === 'snowpea' ? '#29b6f6' : (type === 'repeater' ? '#2e7d32' : '#4caf50');
ctx.fillStyle = col;
ctx.beginPath();
ctx.arc(0, 0, 16, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(14, 0, 8, 10, 0, 0, Math.PI * 2);
ctx.fill();
} else if (type === 'wallnut' || type === 'bowling_nut') {
ctx.fillStyle = '#8d6e63';
ctx.beginPath();
ctx.ellipse(0, 0, 16, 22, 0, 0, Math.PI * 2);
ctx.fill();
} else if (type === 'cherrybomb' || type === 'bowling_cherry') {
ctx.fillStyle = '#d32f2f';
ctx.beginPath();
ctx.arc(-9, 4, 12, 0, Math.PI * 2);
ctx.arc(9, 4, 12, 0, Math.PI * 2);
ctx.fill();
} else if (type === 'potatomine') {
ctx.fillStyle = '#a1887f';
ctx.beginPath();
ctx.ellipse(0, 6, 16, 12, 0, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#f44336';
ctx.beginPath();
ctx.arc(0, -6, 4, 0, Math.PI * 2);
ctx.fill();
} else if (type === 'chomper') {
ctx.fillStyle = '#7b1fa2';
ctx.beginPath();
ctx.arc(0, 0, 18, 0, Math.PI * 2);
ctx.fill();
} else if (type === 'jalapeno') {
ctx.fillStyle = '#d50000';
ctx.beginPath();
ctx.ellipse(0, 0, 10, 20, 0, 0, Math.PI * 2);
ctx.fill();
} else if (type === 'sunshroom') {
ctx.fillStyle = '#fbc02d';
ctx.beginPath();
ctx.arc(0, 0, 14, Math.PI, 0);
ctx.fill();
} else if (type === 'fumeshroom') {
ctx.fillStyle = '#8e24aa';
ctx.beginPath();
ctx.arc(0, 0, 16, Math.PI, 0);
ctx.fill();
} else if (type === 'lilypad') {
ctx.fillStyle = '#2e7d32';
ctx.beginPath();
ctx.ellipse(0, 0, 20, 10, 0, 0, Math.PI * 2);
ctx.fill();
} else if (type === 'gravebuster') {
ctx.strokeStyle = '#33691e';
ctx.lineWidth = 4;
ctx.beginPath();
ctx.arc(0, 0, 14, 0, Math.PI * 2);
ctx.stroke();
} else if (type === 'flowerpot') {
ctx.fillStyle = '#bf360c';
ctx.beginPath();
ctx.moveTo(-14, 4);
ctx.lineTo(14, 4);
ctx.lineTo(10, 16);
ctx.lineTo(-10, 16);
ctx.closePath();
ctx.fill();
ctx.fillStyle = '#d84315';
ctx.fillRect(-16, 0, 32, 5);
} else if (type === 'cabbagepult') {
ctx.fillStyle = '#8d6e63';
ctx.fillRect(-12, 6, 24, 6);
ctx.fillStyle = '#81c784';
ctx.beginPath();
ctx.arc(4, -4, 11, 0, Math.PI * 2);
ctx.fill();
} else if (type === 'melonpult') {
ctx.fillStyle = '#5d4037';
ctx.fillRect(-14, 6, 28, 8);
ctx.fillStyle = '#2e7d32';
ctx.beginPath();
ctx.ellipse(4, -4, 14, 11, 0, 0, Math.PI * 2);
ctx.fill();
} else if (type && type.startsWith('z_')) {
ctx.fillStyle = '#809973';
ctx.beginPath();
ctx.roundRect(-12, -14, 24, 26, [8, 8, 4, 4]);
ctx.fill();
ctx.fillStyle = '#fff9c4';
ctx.beginPath();
ctx.arc(-5, -4, 4, 0, Math.PI * 2);
ctx.arc(5, -4, 4, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#000';
ctx.beginPath();
ctx.arc(-5, -4, 1.5, 0, Math.PI * 2);
ctx.arc(5, -4, 1.5, 0, Math.PI * 2);
ctx.fill();
if (type === 'z_conehead') {
ctx.fillStyle = '#ff6d00';
ctx.beginPath();
ctx.moveTo(-10, -12); ctx.lineTo(10, -12); ctx.lineTo(0, -30); ctx.closePath();
ctx.fill();
} else if (type === 'z_buckethead') {
ctx.fillStyle = '#90a4ae';
ctx.beginPath();
ctx.roundRect(-12, -26, 24, 16, 4);
ctx.fill();
} else if (type === 'z_football') {
ctx.fillStyle = '#b71c1c';
ctx.beginPath();
ctx.arc(0, -10, 15, Math.PI, 0);
ctx.fill();
}
}
ctx.restore();
}
drawShovelGraphic(ctx, x, y) {
ctx.save();
ctx.translate(x, y);
ctx.rotate(0.35);
ctx.strokeStyle = '#8d6e63';
ctx.lineWidth = 4;
ctx.beginPath();
ctx.moveTo(0, -16);
ctx.lineTo(0, 8);
ctx.stroke();
ctx.fillStyle = '#90a4ae';
ctx.beginPath();
ctx.moveTo(-9, 8);
ctx.lineTo(9, 8);
ctx.lineTo(6, 20);
ctx.lineTo(0, 24);
ctx.lineTo(-6, 20);
ctx.closePath();
ctx.fill();
ctx.restore();
}
};