// Zen Garden System: Idle Potted Plant Farming, AFK Coins, Care & Stinky the Snail window.ZenGardenSystem = class ZenGardenSystem { constructor(economy) { this.economy = economy; // Default Zen Garden state this.pots = [ { id: 0, plantType: 'sunflower', stage: 2, thirstTimer: 0, needsFertilizer: false, happyTimer: 60, coinTimer: 5 }, { id: 1, plantType: 'peashooter', stage: 1, thirstTimer: 15, needsFertilizer: false, happyTimer: 45, coinTimer: 8 }, { id: 2, plantType: 'wallnut', stage: 0, thirstTimer: 0, needsFertilizer: true, happyTimer: 0, coinTimer: 0 }, // Sprout { id: 3, plantType: null, stage: 0, thirstTimer: 0, needsFertilizer: false, happyTimer: 0, coinTimer: 0 }, { id: 4, plantType: null, stage: 0, thirstTimer: 0, needsFertilizer: false, happyTimer: 0, coinTimer: 0 }, { id: 5, plantType: null, stage: 0, thirstTimer: 0, needsFertilizer: false, happyTimer: 0, coinTimer: 0 }, { id: 6, plantType: null, stage: 0, thirstTimer: 0, needsFertilizer: false, happyTimer: 0, coinTimer: 0 }, { id: 7, plantType: null, stage: 0, thirstTimer: 0, needsFertilizer: false, happyTimer: 0, coinTimer: 0 } ]; this.fertilizerCount = 5; this.chocolateCount = 3; // Snail state this.snail = { x: 100, targetX: 300, dir: 1, isAwake: true, sleepTimer: 120, // seconds until sleep boostTimer: 0 // chocolate boost }; // Dropped coins in Zen Garden this.gardenCoins = []; // Offline progression tracking this.lastVisit = Date.now(); this.offlineEarned = 0; this.loadFromAuth(); this.calculateOfflineEarnings(); } loadFromAuth() { const defaultPots = [ { id: 1, plantType: 'sunflower', stage: 2, watered: true, fertilized: true, needWaterTime: 0, matureTime: 0, dropsCollected: 0 }, { id: 2, plantType: 'peashooter', stage: 1, watered: true, fertilized: false, needWaterTime: 0, matureTime: 0, dropsCollected: 0 }, { id: 3, plantType: null, stage: 0, watered: false, fertilized: false, needWaterTime: 0, matureTime: 0, dropsCollected: 0 }, { id: 4, plantType: null, stage: 0, watered: false, fertilized: false, needWaterTime: 0, matureTime: 0, dropsCollected: 0 }, { id: 5, plantType: null, stage: 0, watered: false, fertilized: false, needWaterTime: 0, matureTime: 0, dropsCollected: 0 }, { id: 6, plantType: null, stage: 0, watered: false, fertilized: false, needWaterTime: 0, matureTime: 0, dropsCollected: 0 }, { id: 7, plantType: null, stage: 0, watered: false, fertilized: false, needWaterTime: 0, matureTime: 0, dropsCollected: 0 }, { id: 8, plantType: null, stage: 0, watered: false, fertilized: false, needWaterTime: 0, matureTime: 0, dropsCollected: 0 } ]; this.pots = JSON.parse(JSON.stringify(defaultPots)); this.fertilizerCount = 5; this.chocolateCount = 2; this.snail = { name: 'Стинки', active: false, speed: 60, boostTimer: 0, x: 50, direction: 1 }; this.lastVisit = Date.now(); if (window.PvZAuth) { const uData = window.PvZAuth.getUserData(); if (uData.zenGarden) { if (uData.zenGarden.pots && Array.isArray(uData.zenGarden.pots) && uData.zenGarden.pots.length > 0) { this.pots = JSON.parse(JSON.stringify(uData.zenGarden.pots)); } if (uData.zenGarden.fertilizerCount !== undefined) this.fertilizerCount = uData.zenGarden.fertilizerCount; if (uData.zenGarden.chocolateCount !== undefined) this.chocolateCount = uData.zenGarden.chocolateCount; if (uData.zenGarden.snail) this.snail = { ...this.snail, ...uData.zenGarden.snail }; if (uData.zenGarden.lastVisit) this.lastVisit = uData.zenGarden.lastVisit; } } } saveToAuth() { if (window.PvZAuth && !window.PvZAuth.isGuest()) { window.PvZAuth.saveUserData({ zenGarden: { pots: this.pots, fertilizerCount: this.fertilizerCount, chocolateCount: this.chocolateCount, snail: this.snail, lastVisit: Date.now() } }); } } calculateOfflineEarnings() { const now = Date.now(); const elapsedSec = Math.max(0, (now - this.lastVisit) / 1000); this.lastVisit = now; // Count mature/blooming plants let earningRatePerMinute = 0; for (const pot of this.pots) { if (pot.plantType && pot.stage >= 2) { earningRatePerMinute += 15; // 15 coins / minute per mature plant } else if (pot.plantType && pot.stage === 1) { earningRatePerMinute += 5; } } if (earningRatePerMinute > 0 && elapsedSec > 15) { // Cap offline earnings to 8 hours max const cappedMinutes = Math.min(elapsedSec / 60, 480); const earned = Math.floor(cappedMinutes * earningRatePerMinute); if (earned > 0) { this.offlineEarned = earned; if (this.economy) { this.economy.addCoins(earned); } this.saveToAuth(); } } } // Plant a new sprout in first empty pot plantSprout(plantType = 'sunflower') { const emptyPot = this.pots.find(p => !p.plantType); if (emptyPot) { emptyPot.plantType = plantType; emptyPot.stage = 0; // Sprout emptyPot.thirstTimer = 10; emptyPot.needsFertilizer = false; emptyPot.happyTimer = 30; emptyPot.coinTimer = 0; this.saveToAuth(); return true; } return false; } waterPlant(potId) { const pot = this.pots.find(p => p.id === potId); if (!pot || !pot.plantType) return false; if (pot.thirstTimer > 0) { pot.thirstTimer = 0; if (pot.stage < 2 && Math.random() < 0.6) { pot.needsFertilizer = true; } pot.happyTimer += 60; if (window.PvZAudio) window.PvZAudio.playPlant(); this.spawnGardenCoin(potId, 'silver'); this.saveToAuth(); return true; } return false; } fertilizePlant(potId) { const pot = this.pots.find(p => p.id === potId); if (!pot || !pot.plantType) return false; if (pot.needsFertilizer && this.fertilizerCount > 0) { this.fertilizerCount--; pot.needsFertilizer = false; if (pot.stage < 2) { pot.stage++; if (pot.stage === 2 && this.economy && this.economy.engine && this.economy.engine.achievements) { this.economy.engine.achievements.trigger('zen_blooming'); } } pot.happyTimer += 120; if (window.PvZAudio) window.PvZAudio.playWin(); this.spawnGardenCoin(potId, 'gold'); this.saveToAuth(); return true; } return false; } playMusicToPlant(potId) { const pot = this.pots.find(p => p.id === potId); if (!pot || !pot.plantType) return false; pot.happyTimer = Math.max(pot.happyTimer, 180); this.spawnGardenCoin(potId, 'silver'); if (window.PvZAudio) window.PvZAudio.playCoin(); this.saveToAuth(); return true; } feedSnail() { if (this.chocolateCount > 0) { this.chocolateCount--; this.snail.isAwake = true; this.snail.sleepTimer = 300; // 5 min this.snail.boostTimer = 120; // 2 min super fast speed if (this.economy && this.economy.engine && this.economy.engine.achievements) { this.economy.engine.achievements.trigger('snail_feed'); } if (window.PvZAudio) window.PvZAudio.playWin(); this.saveToAuth(); return true; } return false; } spawnGardenCoin(potId, coinType = 'silver') { const pot = this.pots.find(p => p.id === potId); if (!pot) return; const potCol = potId % 4; const potRow = Math.floor(potId / 4); const baseX = 120 + potCol * 150; const baseY = 95 + potRow * 125; const dropMult = window.PvZConfig ? window.PvZConfig.getZenDropRateMult() : 1.0; const baseVal = coinType === 'diamond' ? 200 : (coinType === 'gold' ? 50 : 10); this.gardenCoins.push({ x: baseX + (Math.random() * 30 - 15), y: baseY + 30, vy: -140, type: coinType, value: Math.max(1, Math.round(baseVal * dropMult)), collected: false, alpha: 1.0, anim: 0 }); } update(dt) { // 1. Plant timers for (const pot of this.pots) { if (!pot.plantType) continue; if (pot.happyTimer > 0) pot.happyTimer -= dt; // Mature plants drop coins always (slower interval when unhappy) if (pot.stage >= 2) { pot.coinTimer -= dt; if (pot.coinTimer <= 0) { const happy = pot.happyTimer > 0; pot.coinTimer = happy ? (15 + Math.random() * 10) : (40 + Math.random() * 20); const isGold = happy && Math.random() < 0.4; this.spawnGardenCoin(pot.id, isGold ? 'gold' : 'silver'); } } // Thirst accumulation if (pot.thirstTimer <= 0 && Math.random() < 0.005) { pot.thirstTimer = 1; } } // 2. Snail update if (this.snail.isAwake) { this.snail.sleepTimer -= dt; if (this.snail.boostTimer > 0) this.snail.boostTimer -= dt; if (this.snail.sleepTimer <= 0) { this.snail.isAwake = false; } else { const speed = this.snail.boostTimer > 0 ? 110 : 45; // Move towards nearest uncollected coin let targetCoin = null; let minDist = Infinity; for (const c of this.gardenCoins) { if (!c.collected) { const d = Math.abs(c.x - this.snail.x); if (d < minDist) { minDist = d; targetCoin = c; } } } if (targetCoin) { this.snail.targetX = targetCoin.x; } else if (Math.abs(this.snail.x - this.snail.targetX) < 15) { // Pick a new random target at least 120px away to avoid spinning in place let newTarget; let tries = 0; do { newTarget = 80 + Math.random() * 560; tries++; } while (Math.abs(newTarget - this.snail.x) < 120 && tries < 10); this.snail.targetX = newTarget; } const distToTarget = this.snail.targetX - this.snail.x; const moveStep = speed * dt; if (Math.abs(distToTarget) <= moveStep) { this.snail.x = this.snail.targetX; } else if (distToTarget > 0) { this.snail.dir = 1; this.snail.x += moveStep; } else { this.snail.dir = -1; this.snail.x -= moveStep; } } } // 3. Garden coins update & Snail collision const FLOOR_Y = 280; // garden floor where coins land and snail walks for (let i = this.gardenCoins.length - 1; i >= 0; i--) { const c = this.gardenCoins[i]; c.anim += dt; if (!c.collected) { // Coin physics: arc up then fall to garden floor if (c.y < FLOOR_Y) { c.y += c.vy * dt; c.vy += 350 * dt; if (c.y >= FLOOR_Y) { c.y = FLOOR_Y; c.vy = 0; } } else { c.vy = 0; // already on floor } // Snail collects coins on the floor if (this.snail.isAwake && Math.abs(this.snail.x - c.x) < 45 && c.y >= FLOOR_Y - 40) { this.collectCoin(c); } } else { c.y -= 90 * dt; c.alpha -= 2.2 * dt; if (c.alpha <= 0) { this.gardenCoins.splice(i, 1); } } } } collectCoin(c) { if (c.collected) return; c.collected = true; if (this.economy) { this.economy.addCoins(c.value); } if (window.PvZAudio) window.PvZAudio.playCoin(); this.saveToAuth(); } // Draw Zen Garden Canvas (720x330) draw(ctx) { // 1. Greenhouse Background const bgGrad = ctx.createLinearGradient(0, 0, 0, 330); bgGrad.addColorStop(0, '#1b3b22'); bgGrad.addColorStop(0.6, '#2e5b38'); bgGrad.addColorStop(1, '#3e2723'); ctx.fillStyle = bgGrad; ctx.fillRect(0, 0, 720, 330); // Glass arches ctx.strokeStyle = 'rgba(255, 255, 255, 0.12)'; ctx.lineWidth = 2; for (let x = 60; x < 720; x += 120) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, 260); ctx.stroke(); } // Wooden Shelves ctx.fillStyle = '#4e342e'; ctx.fillRect(40, 150, 640, 16); ctx.fillRect(40, 275, 640, 16); ctx.fillStyle = '#6d4c41'; ctx.fillRect(40, 150, 640, 4); ctx.fillRect(40, 275, 640, 4); // 2. Pots & Plants for (const pot of this.pots) { const col = pot.id % 4; const row = Math.floor(pot.id / 4); const px = 120 + col * 150; const py = (row === 0 ? 150 : 275); this.drawPot(ctx, px, py, pot); } // 3. Stinky the Snail this.drawSnail(ctx, this.snail.x, 290, this.snail); // 4. Floating Coins for (const c of this.gardenCoins) { ctx.save(); ctx.globalAlpha = Math.max(0, c.alpha); ctx.translate(c.x, c.y); ctx.fillStyle = c.type === 'diamond' ? '#00e5ff' : (c.type === 'gold' ? '#ffd700' : '#cfd8dc'); ctx.beginPath(); ctx.arc(0, 0, 10, 0, Math.PI * 2); ctx.fill(); ctx.strokeStyle = '#3e2723'; ctx.lineWidth = 1.5; ctx.stroke(); ctx.fillStyle = '#212121'; ctx.font = 'bold 9px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(c.type === 'diamond' ? '💎' : '$', 0, 1); ctx.restore(); } } drawPot(ctx, x, y, pot) { ctx.save(); ctx.translate(x, y); // Ceramic clay pot ctx.fillStyle = '#bf360c'; ctx.beginPath(); ctx.moveTo(-24, 0); ctx.lineTo(24, 0); ctx.lineTo(18, 22); ctx.lineTo(-18, 22); ctx.closePath(); ctx.fill(); // Pot rim ctx.fillStyle = '#d84315'; ctx.beginPath(); ctx.roundRect(-28, -6, 56, 7, 3); ctx.fill(); // If pot has plant if (pot.plantType) { if (pot.stage === 0) { // Sprout ctx.fillStyle = '#8bc34a'; ctx.beginPath(); ctx.arc(0, -12, 6, 0, Math.PI * 2); ctx.fill(); ctx.strokeStyle = '#558b2f'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(0, -6); ctx.lineTo(0, -12); ctx.stroke(); } else { // Plant icon representation ctx.font = pot.stage === 1 ? '22px sans-serif' : '30px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'bottom'; const icons = { sunflower: '🌻', peashooter: '🌱', wallnut: '🌰', snowpea: '❄️', repeater: '🌿', chomper: '🌺', potatomine: '🥔' }; ctx.fillText(icons[pot.plantType] || '🌱', 0, -4); } // Needs bubble indicator if (pot.thirstTimer > 0) { ctx.fillStyle = 'rgba(33, 150, 243, 0.9)'; ctx.beginPath(); ctx.arc(20, -32, 10, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#fff'; ctx.font = '11px sans-serif'; ctx.fillText('💧', 20, -28); } else if (pot.needsFertilizer) { ctx.fillStyle = 'rgba(76, 175, 80, 0.9)'; ctx.beginPath(); ctx.arc(20, -32, 10, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#fff'; ctx.font = '11px sans-serif'; ctx.fillText('🧪', 20, -28); } else if (pot.happyTimer > 0 && pot.stage >= 2) { ctx.fillStyle = '#ffd54f'; ctx.font = '10px sans-serif'; ctx.fillText('✨', 18, -34); } } else { // Empty pot text ctx.fillStyle = 'rgba(255, 255, 255, 0.35)'; ctx.font = '10px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('+ Росток', 0, -8); } ctx.restore(); } drawSnail(ctx, x, y, snail) { ctx.save(); ctx.translate(x, y); ctx.scale(snail.dir, 1); // Shell ctx.fillStyle = snail.boostTimer > 0 ? '#ffb300' : '#8d6e63'; ctx.beginPath(); ctx.arc(-6, -10, 14, 0, Math.PI * 2); ctx.fill(); ctx.strokeStyle = '#4e342e'; ctx.lineWidth = 2; ctx.stroke(); // Body ctx.fillStyle = '#ffecb3'; ctx.beginPath(); ctx.ellipse(4, -4, 18, 7, 0, 0, Math.PI * 2); ctx.fill(); // Eye stalks ctx.strokeStyle = '#ffecb3'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(14, -6); ctx.lineTo(18, -14); ctx.moveTo(10, -6); ctx.lineTo(13, -15); ctx.stroke(); // Eyes ctx.fillStyle = '#212121'; ctx.beginPath(); ctx.arc(18, -14, 1.5, 0, Math.PI * 2); ctx.arc(13, -15, 1.5, 0, Math.PI * 2); ctx.fill(); // Zzz if asleep if (!snail.isAwake) { ctx.fillStyle = '#90caf9'; ctx.font = 'bold 12px sans-serif'; ctx.fillText('Zzz...', -10, -26); } else if (snail.boostTimer > 0) { ctx.fillStyle = '#ff3d00'; ctx.font = 'bold 10px sans-serif'; ctx.fillText('⚡ 2x', -8, -26); } ctx.restore(); } };