// Plant Catalogue for Purchase & Inventory Selection window.PLANT_CATALOGUE = [ { id: 'sunflower', name: 'Подсолнух', icon: '🌻', cost: 50, cooldown: 7.5, price: 0, defaultUnlocked: true, desc: 'Производит дополнительное солнце для посадки растений.' }, { id: 'peashooter', name: 'Горохострел', icon: '🌱', cost: 100, cooldown: 7.5, price: 0, defaultUnlocked: true, desc: 'Базовый защитник, стреляет горошинами по зомби.' }, { id: 'wallnut', name: 'Стеноорех', icon: '🥜', cost: 50, cooldown: 20.0, price: 0, defaultUnlocked: true, desc: 'Прочная преграда, задерживает натиск зомби.' }, { id: 'potatomine', name: 'Картофельная мина', icon: '🥔', cost: 25, cooldown: 18.0, price: 150, defaultUnlocked: false, desc: 'Взрывает первого наступившего зомби (взводится 14с).' }, { id: 'snowpea', name: 'Ледяной горох', icon: '❄️', cost: 175, cooldown: 10.0, price: 350, defaultUnlocked: false, desc: 'Стреляет ледяным горохом, замедляет зомби на 50%.' }, { id: 'repeater', name: 'Повторитель', icon: '🌿', cost: 200, cooldown: 7.5, price: 500, defaultUnlocked: false, desc: 'Выпускает по 2 горошины за каждый выстрел.' }, { id: 'chomper', name: 'Зубастик', icon: '🐊', cost: 150, cooldown: 10.0, price: 450, defaultUnlocked: false, desc: 'Заглатывает зомби целиком, затем долго пережевывает.' }, { id: 'cherrybomb', name: 'Вишневая бомба', icon: '🍒', cost: 150, cooldown: 25.0, price: 400, defaultUnlocked: false, desc: 'Взрывает всех врагов в области 3х3 клетки.' }, { id: 'jalapeno', name: 'Халапеньо', icon: '🌶️', cost: 125, cooldown: 30.0, price: 450, defaultUnlocked: false, desc: 'Испепеляет всех зомби на целой линии газона.' }, { id: 'sunshroom', name: 'Солнцегриб', icon: '☀️', cost: 25, cooldown: 7.5, price: 250, defaultUnlocked: false, desc: 'Дает маленькое солнце, а вырастая — полноценное.' }, { id: 'fumeshroom', name: 'Дымогриб', icon: '🍄', cost: 75, cooldown: 7.5, price: 300, defaultUnlocked: false, desc: 'Стреляет пробивающими спорами через броню и щиты.' }, { id: 'lilypad', name: 'Кувшинка', icon: '🍃', cost: 25, cooldown: 7.5, price: 200, defaultUnlocked: false, desc: 'Позволяет сажать наземные растения на водной линии.' }, { id: 'gravebuster', name: 'Могилоед', icon: '🪦', cost: 75, cooldown: 15.0, price: 250, defaultUnlocked: false, desc: 'Пожирает могилы на ночном газоне.' }, { id: 'flowerpot', name: 'Цветочный горшок', icon: '🪴', cost: 25, cooldown: 7.5, price: 200, defaultUnlocked: false, desc: 'Необходим для высадки растений на скатной крыше.' }, { id: 'cabbagepult', name: 'Капустопульта', icon: '🥬', cost: 100, cooldown: 7.5, price: 400, defaultUnlocked: false, desc: 'Перебрасывает капусту через препятствия и щиты.' }, { id: 'melonpult', name: 'Арбузопульта', icon: '🍉', cost: 300, cooldown: 15.0, price: 750, defaultUnlocked: false, desc: 'Наносит мощнейший сплэш-урон тяжелыми арбузами.' } ]; // Economy, Sun, Coin Drops & Crazy Dave's Shop System window.EconomySystem = class EconomySystem { constructor() { this.sun = window.PvZConfig ? window.PvZConfig.getStartingSun() : 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; // Default unlocked starter plants (Sunflower, Peashooter, Wallnut) this.defaultUnlocked = ['sunflower', 'peashooter', 'wallnut']; this.unlockedPlants = [...this.defaultUnlocked]; this.selectedDeck = [...this.defaultUnlocked]; // Total coins & upgrades persistent storage this.reloadProfileData(); // Full Card Registry this.allCards = window.PLANT_CATALOGUE.map(p => ({ id: p.id, name: p.name, cost: window.PvZConfig ? window.PvZConfig.getPlantSunCost(p.id, p.cost) : p.cost, cooldown: p.cooldown, timer: 0 })); this.selectedCard = null; this.hoveredCard = null; this.hoveredCardIndex = -1; this.hoveredCardPos = null; this.animTime = 0; this.setDeckForLevel(1, 'day'); } getMaxSlots() { return this.upgrades && this.upgrades.extraSlot ? 6 : 5; } isPlantUnlocked(plantId) { if (this.defaultUnlocked.includes(plantId)) return true; return this.unlockedPlants && this.unlockedPlants.includes(plantId); } buyPlant(plantId) { const p = window.PLANT_CATALOGUE.find(item => item.id === plantId); if (!p) return { success: false, message: 'Растение не найдено' }; if (this.isPlantUnlocked(plantId)) return { success: false, message: 'Растение уже куплено' }; const price = window.PvZConfig ? window.PvZConfig.getPlantShopPrice(plantId) : (p.price || 0); if (this.totalCoins < price) { return { success: false, message: `Недостаточно монет! Требуется ${price} 💰 (у вас: ${this.totalCoins} 💰)` }; } this.addCoins(-price); if (!this.unlockedPlants.includes(plantId)) { this.unlockedPlants.push(plantId); } if (this.selectedDeck.length < this.getMaxSlots() && !this.selectedDeck.includes(plantId)) { this.selectedDeck.push(plantId); } this.saveProfileData(); return { success: true, plant: p }; } addToDeck(plantId) { if (!this.isPlantUnlocked(plantId)) { return { success: false, message: 'Сначала купите это растение в магазине!' }; } if (this.selectedDeck.includes(plantId)) { return { success: false, message: 'Растение уже добавлено в колоду' }; } if (this.selectedDeck.length >= this.getMaxSlots()) { return { success: false, message: `Колода заполнена (максимум ${this.getMaxSlots()} слотов)` }; } this.selectedDeck.push(plantId); this.saveProfileData(); return { success: true }; } removeFromDeck(plantId) { const idx = this.selectedDeck.indexOf(plantId); if (idx !== -1) { this.selectedDeck.splice(idx, 1); this.saveProfileData(); return { success: true }; } return { success: false, message: 'Растение не найдено в колоде' }; } autoFillDeck() { const priority = [ 'sunflower', 'peashooter', 'wallnut', 'potatomine', 'repeater', 'snowpea', 'chomper', 'cherrybomb', 'jalapeno', 'cabbagepult', 'melonpult', 'fumeshroom', 'sunshroom', 'lilypad', 'flowerpot', 'gravebuster' ]; for (const id of priority) { if (this.selectedDeck.length >= this.getMaxSlots()) break; if (this.isPlantUnlocked(id) && !this.selectedDeck.includes(id)) { this.selectedDeck.push(id); } } this.saveProfileData(); } setDeckFromSelection(deckIds = null) { if (Array.isArray(deckIds) && deckIds.length > 0) { this.selectedDeck = deckIds.filter(id => this.isPlantUnlocked(id)).slice(0, this.getMaxSlots()); this.saveProfileData(); } const chosen = (this.selectedDeck && this.selectedDeck.length > 0) ? this.selectedDeck : this.defaultUnlocked; this.cards = chosen.map(id => { const found = this.allCards.find(c => c.id === id); return found ? { ...found, timer: 0 } : null; }).filter(Boolean); // Guaranteed fallback: If this.cards is empty, restore default starter cards so top menu never disappears if (!this.cards || this.cards.length === 0) { this.selectedDeck = [...this.defaultUnlocked]; this.cards = this.defaultUnlocked.map(id => { const found = this.allCards.find(c => c.id === id); return found ? { ...found, timer: 0 } : null; }).filter(Boolean); this.saveProfileData(); } } 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 }; if (Array.isArray(uData.unlockedPlants) && uData.unlockedPlants.length > 0) { this.unlockedPlants = Array.from(new Set([...this.defaultUnlocked, ...uData.unlockedPlants])); } else { // Check localStorage fallback try { const raw = localStorage.getItem('pvz_unlocked_plants'); this.unlockedPlants = raw ? Array.from(new Set([...this.defaultUnlocked, ...JSON.parse(raw)])) : [...this.defaultUnlocked]; } catch (_) { this.unlockedPlants = [...this.defaultUnlocked]; } } if (Array.isArray(uData.selectedDeck) && uData.selectedDeck.length > 0) { this.selectedDeck = uData.selectedDeck.filter(id => this.isPlantUnlocked(id)).slice(0, this.getMaxSlots()); } else { try { const rawDeck = localStorage.getItem('pvz_selected_deck'); this.selectedDeck = rawDeck ? JSON.parse(rawDeck).slice(0, this.getMaxSlots()) : [...this.defaultUnlocked]; } catch (_) { this.selectedDeck = [...this.defaultUnlocked]; } } } 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' }; try { const raw = localStorage.getItem('pvz_unlocked_plants'); this.unlockedPlants = raw ? Array.from(new Set([...this.defaultUnlocked, ...JSON.parse(raw)])) : [...this.defaultUnlocked]; } catch (_) { this.unlockedPlants = [...this.defaultUnlocked]; } try { const rawDeck = localStorage.getItem('pvz_selected_deck'); this.selectedDeck = rawDeck ? JSON.parse(rawDeck).slice(0, this.getMaxSlots()) : [...this.defaultUnlocked]; } catch (_) { this.selectedDeck = [...this.defaultUnlocked]; } } } 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'); localStorage.setItem('pvz_unlocked_plants', JSON.stringify(this.unlockedPlants)); localStorage.setItem('pvz_selected_deck', JSON.stringify(this.selectedDeck)); if (window.PvZAuth && !window.PvZAuth.isGuest()) { window.PvZAuth.saveUserData({ coins: this.totalCoins, upgrades: this.upgrades, unlockedPlants: this.unlockedPlants, selectedDeck: this.selectedDeck }); } } setDeckForLevel(level = 1, biome = 'day') { this.isNight = biome === 'night'; this.isConveyor = false; this.isIZombie = false; // Use player selected deck if configured and valid if (this.selectedDeck && this.selectedDeck.length > 0) { this.setDeckFromSelection(); return; } // Fallback: choose relevant deck based on unlocked plants and biome const slotCount = this.getMaxSlots(); 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']; } else if (biome === 'pool') { availableIds = ['sunflower', 'peashooter', 'lilypad', 'wallnut', 'repeater', 'snowpea']; } else { availableIds = ['sunflower', 'peashooter', 'wallnut', 'potatomine', 'snowpea', 'repeater', 'chomper', 'cherrybomb']; } const filtered = availableIds.filter(id => this.isPlantUnlocked(id)); const deckIds = filtered.length > 0 ? filtered.slice(0, slotCount) : this.defaultUnlocked; this.setDeckFromSelection(deckIds); } 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 : (window.PvZConfig ? window.PvZConfig.getStartingSun() : 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.hoveredCard = null; this.hoveredCardIndex = -1; this.hoveredCardPos = 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; if (amount > 0 && this.engine && typeof this.engine.recordStat === 'function') { this.engine.recordStat('coinsEarned', 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) { const sunMult = window.PvZConfig ? window.PvZConfig.getSunMultiplier() : 1.0; const finalVal = Math.max(5, Math.round(value * sunMult)); this.suns.push({ x, y, targetY: targetY !== null ? targetY : y, value: finalVal, isFalling, fallSpeed: 90, radius: 26, life: 13.0, collecting: false, toRemove: false, pulse: Math.random() * 5 }); } spawnCoin(x, y, type = 'silver') { const dropMult = window.PvZConfig ? window.PvZConfig.getCoinDropRate() : 1.0; const baseVal = type === 'diamond' ? 200 : (type === 'gold' ? 50 : 10); const finalVal = Math.max(1, Math.round(baseVal * dropMult)); this.coins.push({ x, y, type, value: finalVal, radius: 16, life: 12.0, collecting: false, toRemove: false, pulse: Math.random() * 5 }); } update(dt) { this.animTime += dt; // Cooldown timers with fastCool perk bonus and PvZConfig const fastCoolLvl = this.workshop ? this.workshop.getPerkLevel('fastCool') : 0; const cfgCooldownMult = window.PvZConfig ? (1 / Math.max(0.1, window.PvZConfig.getPlantCooldownMult())) : 1.0; const cooldownMultiplier = (1 + fastCoolLvl * 0.15) * cfgCooldownMult; 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; const sunDropMult = window.PvZConfig ? window.PvZConfig.getSunDropRate() : 1.0; const effectiveInterval = this.skySunInterval / Math.max(0.2, sunDropMult); if (this.skySunTimer >= effectiveInterval) { 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 }; } handleMouseMove(mouseX, mouseY) { this.hoveredCard = null; this.hoveredCardIndex = -1; this.hoveredCardPos = null; if (mouseX < 0 || mouseY < 0) return; 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) { this.hoveredCard = card; this.hoveredCardIndex = i; this.hoveredCardPos = { x: cx, y: cardY, w: cardW, h: cardH }; return; } } // Shovel hover const shovelX = cardStartX + this.cards.length * spacing + 12; if (mouseX >= shovelX && mouseX <= shovelX + 54 && mouseY >= cardY && mouseY <= cardY + cardH) { this.hoveredCard = { id: 'shovel', name: 'Лопата', cost: 0, cooldown: 0 }; this.hoveredCardIndex = -2; this.hoveredCardPos = { x: shovelX, y: cardY, w: 54, h: cardH }; } } getCardInfo(cardId) { const p = window.PLANT_CATALOGUE ? window.PLANT_CATALOGUE.find(item => item.id === cardId) : null; if (p) { return { name: p.name, icon: p.icon || '🌱', cost: p.cost, cooldown: p.cooldown, desc: p.desc || 'Защитник газона.' }; } const specialMap = { shovel: { name: 'Лопата', icon: '⛏️', cost: 0, cooldown: 0, desc: 'Выкапывает любое растение с газона, освобождая клетку для новой посадки.' }, bowling_nut: { name: 'Орех-шар', icon: '🎳', cost: 0, cooldown: 4.0, desc: 'Катится по дорожке на огромной скорости и сбивает всех зомби на своём пути.' }, bowling_cherry: { name: 'Взрывной орех', icon: '💥', cost: 0, cooldown: 12.0, desc: 'Взрывается при первом контакте с зомби в радиусе 3х3 клетки.' }, z_normal: { name: 'Обычный зомби', icon: '🧟', cost: 50, cooldown: 2.0, desc: 'Базовый зомби-пехотинец. Идёт вперёд и пожирает растения.' }, z_conehead: { name: 'Зомби с конусом', icon: '⚠️', cost: 75, cooldown: 3.0, desc: 'Дорожный конус удваивает запас здоровья зомби.' }, z_polevaulter: { name: 'Зомби с шестом', icon: '🏃', cost: 75, cooldown: 4.0, desc: 'Быстро бежит и перепрыгивает первое встреченное растение.' }, z_buckethead: { name: 'Зомби с ведром', icon: '🪣', cost: 125, cooldown: 5.0, desc: 'Прочное железное ведро выдерживает массивный урон.' }, z_football: { name: 'Зомби-футболист', icon: '🏈', cost: 175, cooldown: 6.0, desc: 'Сверхбыстрый и тяжело бронированный штурмовик.' } }; return specialMap[cardId] || { name: cardId, icon: '🌱', cost: 0, cooldown: 5.0, desc: 'Защитник сада.' }; } getRechargeSpeedText(cd) { if (cd <= 0) return 'Мгновенно'; if (cd <= 7.5) return `Быстрая (${cd}с)`; if (cd <= 15) return `Средняя (${cd}с)`; if (cd <= 25) return `Медленная (${cd}с)`; return `Очень медленная (${cd}с)`; } wrapTooltipText(ctx, text, maxWidth) { if (!text) return []; const words = text.split(' '); const lines = []; let currentLine = ''; for (const word of words) { const testLine = currentLine ? `${currentLine} ${word}` : word; const width = ctx.measureText(testLine).width; if (width > maxWidth && currentLine) { lines.push(currentLine); currentLine = word; } else { currentLine = testLine; } } if (currentLine) lines.push(currentLine); return lines; } drawCardTooltip(ctx, card) { if (!card || !this.hoveredCardPos) return; const info = this.getCardInfo(card.id); const boxW = 240; ctx.save(); ctx.font = '11px sans-serif'; const descLines = this.wrapTooltipText(ctx, info.desc, boxW - 24); const boxH = 58 + descLines.length * 15; let boxX = this.hoveredCardPos.x - 10; boxX = Math.max(10, Math.min(boxX, 900 - boxW - 10)); const boxY = 88; // Stylish dark-glass shadow & container ctx.shadowColor = 'rgba(0, 0, 0, 0.7)'; ctx.shadowBlur = 14; ctx.shadowOffsetY = 4; const bgGrad = ctx.createLinearGradient(boxX, boxY, boxX, boxY + boxH); bgGrad.addColorStop(0, 'rgba(15, 23, 42, 0.96)'); bgGrad.addColorStop(1, 'rgba(30, 41, 59, 0.96)'); ctx.fillStyle = bgGrad; ctx.beginPath(); ctx.roundRect(boxX, boxY, boxW, boxH, 8); ctx.fill(); // Border glow ctx.shadowColor = 'transparent'; ctx.strokeStyle = 'rgba(105, 240, 174, 0.75)'; ctx.lineWidth = 1.5; ctx.stroke(); // Top subtle neon accent line const accentGrad = ctx.createLinearGradient(boxX + 10, boxY, boxX + boxW - 10, boxY); accentGrad.addColorStop(0, '#69f0ae'); accentGrad.addColorStop(0.5, '#ffd54f'); accentGrad.addColorStop(1, '#81d4fa'); ctx.fillStyle = accentGrad; ctx.fillRect(boxX + 8, boxY + 1.5, boxW - 16, 2); // Title & Icon ctx.fillStyle = '#ffeb3b'; ctx.font = 'bold 13px sans-serif'; ctx.textAlign = 'left'; let titleStr = `${info.icon} ${info.name}`; if (this.workshop && card.id !== 'shovel') { const lvl = this.workshop.getPlantLevel(card.id); if (lvl > 1) { titleStr += ` (${lvl === 3 ? '★★★' : '★★'})`; } } ctx.fillText(titleStr, boxX + 12, boxY + 20); // Subtitle (Cost + Recharge) ctx.fillStyle = '#81d4fa'; ctx.font = 'bold 10.5px sans-serif'; const costText = this.isConveyor ? 'Бесплатно' : `☀️ ${info.cost}`; const cdText = this.getRechargeSpeedText(info.cooldown); ctx.fillText(`Стоимость: ${costText} ⏱️ ${cdText}`, boxX + 12, boxY + 36); // Divider ctx.strokeStyle = 'rgba(255, 255, 255, 0.15)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(boxX + 10, boxY + 44); ctx.lineTo(boxX + boxW - 10, boxY + 44); ctx.stroke(); // Description lines ctx.fillStyle = '#cfd8dc'; ctx.font = '11px sans-serif'; for (let i = 0; i < descLines.length; i++) { ctx.fillText(descLines[i], boxX + 12, boxY + 58 + i * 15); } ctx.restore(); } 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 if (!this.isConveyor && !this.isIZombie && (!this.cards || this.cards.length === 0)) { this.setDeckFromSelection(); } 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); // Slot Hotkey Number Indicator (1..6) if (i < 9) { ctx.fillStyle = 'rgba(0, 0, 0, 0.45)'; ctx.beginPath(); ctx.roundRect(cx + 3, cardY + 3, 13, 13, 3); ctx.fill(); ctx.fillStyle = '#ffffff'; ctx.font = 'bold 9px sans-serif'; ctx.textAlign = 'center'; ctx.fillText((i + 1).toString(), cx + 9.5, cardY + 13); } 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(); // Hotkey [S] badge on shovel ctx.fillStyle = 'rgba(0, 0, 0, 0.45)'; ctx.beginPath(); ctx.roundRect(shovelX + 3, cardY + 3, 14, 13, 3); ctx.fill(); ctx.fillStyle = '#ffffff'; ctx.font = 'bold 9px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('S', shovelX + 10, cardY + 13); 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); } // 7. Dynamic Hover Tooltip on Deck Card if (this.hoveredCard) { this.drawCardTooltip(ctx, this.hoveredCard); } } 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(); } };