Files

541 lines
16 KiB
JavaScript

// 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();
}
};