882 lines
31 KiB
JavaScript
882 lines
31 KiB
JavaScript
// 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);
|
||
|
||
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();
|
||
}
|
||
};
|