Files
ai-qpid-pvz/game/js/systems/economy.js
T

904 lines
31 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 = 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: p.cost,
cooldown: p.cooldown,
timer: 0
}));
this.selectedCard = 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 = 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.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);
}
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 : 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);
// 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);
}
}
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();
}
};