-
МАГАЗИН БЕЗУМНОГО ДЭЙВА
-
💰 Монеты: 0
+
МАГАЗИН БЕЗУМНОГО ДЭЙВА
+
💰 Монеты: 0
-
-
-
-
+1 Слот под семена
-
Разблокирует 6-й слот для пакета растений в колоде.
-
-
-
+
+
+
+
+
-
-
-
Садовые грабли
-
Автоматически уничтожают первого зомби на газоне в каждом раунде.
-
-
+
+
-
+
+
+
+
+
+
+1 Слот под семена
+
Разблокирует 6-й слот для пакета растений в колоде.
+
+
+
+
+
+
+
Садовые грабли
+
Автоматически уничтожают первого зомби на газоне в каждом раунде.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
🌱 ВЫБОР РАСТЕНИЙ ДЛЯ БОЯ
+
+ Соберите свою колоду для защиты газона. Нажмите на растение для добавления или удаления из слота.
+
+
+
+
+ ВЫБРАННАЯ КОЛОДА (0 / 5)
+
+
+
+
+
+
+
+
+ ИНВЕНТАРЬ РАСТЕНИЙ
+
+
+
+
+
+
+
+
+
+
+
diff --git a/game/js/game.js b/game/js/game.js
index b1fdf0b..b81a09c 100644
--- a/game/js/game.js
+++ b/game/js/game.js
@@ -422,7 +422,14 @@ window.GameEngine = class GameEngine {
} else if (this.currentMode === 'roguelike') {
startLvl = this.roguelike ? this.roguelike.currentFloor : 1;
}
- this.startLevel(startLvl, this.currentMode);
+
+ // Fixed-conveyor mini-games launch immediately, standard modes open deck builder
+ if (this.currentMode === 'bowling' || this.currentMode === 'izombie' || this.currentMode === 'boss' || this.currentMode === 'bossrush') {
+ this.startLevel(startLvl, this.currentMode);
+ } else {
+ this.openSeedSelectionModal(startLvl, this.currentMode);
+ }
+
if (!this.loopStarted) {
this.loopStarted = true;
requestAnimationFrame(this.loop.bind(this));
@@ -1102,10 +1109,32 @@ window.GameEngine = class GameEngine {
}
- // Shop Button
+ // Inventory / Deck Builder Buttons
+ const btnInvStart = document.getElementById('btn-inventory-start');
+ const btnDrawerInv = document.getElementById('btn-drawer-inventory');
+ if (btnInvStart) {
+ btnInvStart.addEventListener('click', () => {
+ this.openSeedSelectionModal(this.currentLevel || 1, this.currentMode || 'adventure');
+ });
+ }
+ if (btnDrawerInv) {
+ btnDrawerInv.addEventListener('click', () => {
+ const drawer = document.getElementById('mobile-drawer');
+ const backdrop = document.getElementById('drawer-backdrop');
+ if (drawer) drawer.classList.add('hidden');
+ if (backdrop) backdrop.classList.add('hidden');
+ this.openSeedSelectionModal(this.currentLevel || 1, this.currentMode || 'adventure');
+ });
+ }
+
+ // Shop Button & Tabs
const btnShop = document.getElementById('btn-shop');
const shopModal = document.getElementById('shop-modal');
const btnCloseShop = document.getElementById('btn-close-shop');
+ const tabShopPlants = document.getElementById('tab-shop-plants');
+ const tabShopUpgrades = document.getElementById('tab-shop-upgrades');
+ const panelShopPlants = document.getElementById('shop-plants-panel');
+ const panelShopUpgrades = document.getElementById('shop-upgrades-panel');
if (btnShop && shopModal) {
btnShop.addEventListener('click', () => {
@@ -1120,6 +1149,73 @@ window.GameEngine = class GameEngine {
});
}
+ if (tabShopPlants && tabShopUpgrades && panelShopPlants && panelShopUpgrades) {
+ tabShopPlants.addEventListener('click', () => {
+ tabShopPlants.classList.add('active');
+ tabShopUpgrades.classList.remove('active');
+ panelShopPlants.classList.remove('hidden');
+ panelShopUpgrades.classList.add('hidden');
+ });
+ tabShopUpgrades.addEventListener('click', () => {
+ tabShopUpgrades.classList.add('active');
+ tabShopPlants.classList.remove('active');
+ panelShopUpgrades.classList.remove('hidden');
+ panelShopPlants.classList.add('hidden');
+ });
+ }
+
+ // Pre-Battle Seed Selection & Inventory Modal Handlers
+ const seedSelectModal = document.getElementById('seed-select-modal');
+ const btnSeedStart = document.getElementById('btn-seed-start-battle');
+ const btnSeedAuto = document.getElementById('btn-seed-auto-fill');
+ const btnSeedShop = document.getElementById('btn-seed-open-shop');
+ const btnSeedCancel = document.getElementById('btn-seed-cancel');
+
+ if (btnSeedStart) {
+ btnSeedStart.addEventListener('click', () => {
+ if (!this.economy.selectedDeck || this.economy.selectedDeck.length === 0) {
+ this.economy.autoFillDeck();
+ }
+ if (seedSelectModal) seedSelectModal.classList.add('hidden');
+ const startModal = document.getElementById('start-modal');
+ if (startModal) startModal.classList.add('hidden');
+
+ if (window.PvZAudio) window.PvZAudio.init();
+ const lvl = this.pendingLevel || this.currentLevel || 1;
+ const mode = this.pendingMode || this.currentMode || 'adventure';
+
+ this.startLevel(lvl, mode);
+ if (!this.loopStarted) {
+ this.loopStarted = true;
+ requestAnimationFrame(this.loop.bind(this));
+ }
+ });
+ }
+
+ if (btnSeedAuto) {
+ btnSeedAuto.addEventListener('click', () => {
+ this.economy.autoFillDeck();
+ this.renderSeedSelectionUI();
+ if (window.PvZAudio) window.PvZAudio.playButtonClick();
+ });
+ }
+
+ if (btnSeedShop) {
+ btnSeedShop.addEventListener('click', () => {
+ if (seedSelectModal) seedSelectModal.classList.add('hidden');
+ this.updateShopUI();
+ if (shopModal) shopModal.classList.remove('hidden');
+ });
+ }
+
+ if (btnSeedCancel) {
+ btnSeedCancel.addEventListener('click', () => {
+ if (seedSelectModal) seedSelectModal.classList.add('hidden');
+ const startModal = document.getElementById('start-modal');
+ if (startModal) startModal.classList.remove('hidden');
+ });
+ }
+
// Shop Item Purchases
const btnBuySlot = document.getElementById('btn-buy-slot');
if (btnBuySlot) {
@@ -1128,6 +1224,7 @@ window.GameEngine = class GameEngine {
if (window.PvZAudio) window.PvZAudio.playWin();
this.saveUserData();
this.updateShopUI();
+ this.renderSeedSelectionUI();
}
});
}
@@ -1544,7 +1641,11 @@ window.GameEngine = class GameEngine {
if (startModal) startModal.classList.add('hidden');
if (window.PvZAudio) window.PvZAudio.init();
- this.startLevel(targetLevel, 'adventure');
+ if (targetLevel === 25) {
+ this.startLevel(targetLevel, 'adventure');
+ } else {
+ this.openSeedSelectionModal(targetLevel, 'adventure');
+ }
if (!this.loopStarted) {
this.loopStarted = true;
requestAnimationFrame(this.loop.bind(this));
@@ -1566,9 +1667,126 @@ window.GameEngine = class GameEngine {
});
}
+ openSeedSelectionModal(targetLevel = null, targetMode = null) {
+ this.pendingLevel = targetLevel !== null ? targetLevel : (this.currentLevel || 1);
+ this.pendingMode = targetMode !== null ? targetMode : (this.currentMode || 'adventure');
+
+ this.renderSeedSelectionUI();
+
+ const modal = document.getElementById('seed-select-modal');
+ if (modal) modal.classList.remove('hidden');
+
+ const startModal = document.getElementById('start-modal');
+ if (startModal) startModal.classList.add('hidden');
+ const campaignModal = document.getElementById('campaign-modal');
+ if (campaignModal) campaignModal.classList.add('hidden');
+ }
+
+ renderSeedSelectionUI() {
+ const activeDeckContainer = document.getElementById('seed-active-deck');
+ const inventoryContainer = document.getElementById('seed-inventory-grid');
+ const deckCountEl = document.getElementById('seed-deck-count');
+ const deckMaxEl = document.getElementById('seed-deck-max');
+
+ const maxSlots = this.economy.getMaxSlots();
+ const selectedDeck = this.economy.selectedDeck || [];
+
+ if (deckCountEl) deckCountEl.textContent = selectedDeck.length;
+ if (deckMaxEl) deckMaxEl.textContent = maxSlots;
+
+ // 1. Render Active Deck Slots
+ if (activeDeckContainer) {
+ activeDeckContainer.innerHTML = '';
+ selectedDeck.forEach(plantId => {
+ const p = window.PLANT_CATALOGUE.find(item => item.id === plantId);
+ if (!p) return;
+
+ const slot = document.createElement('div');
+ slot.className = 'seed-slot-card';
+ slot.title = `Нажмите, чтобы убрать ${p.name} из колоды`;
+ slot.innerHTML = `
+
✕
+
${p.icon}
+
☀️ ${p.cost}
+ `;
+ slot.addEventListener('click', () => {
+ this.economy.removeFromDeck(plantId);
+ this.renderSeedSelectionUI();
+ if (window.PvZAudio) window.PvZAudio.playButtonClick();
+ });
+ activeDeckContainer.appendChild(slot);
+ });
+
+ // Render Empty Slots up to maxSlots
+ for (let i = selectedDeck.length; i < maxSlots; i++) {
+ const emptySlot = document.createElement('div');
+ emptySlot.className = 'seed-slot-empty';
+ emptySlot.textContent = `+ Слот ${i + 1}`;
+ activeDeckContainer.appendChild(emptySlot);
+ }
+ }
+
+ // 2. Render Inventory Grid
+ if (inventoryContainer && window.PLANT_CATALOGUE) {
+ inventoryContainer.innerHTML = '';
+ window.PLANT_CATALOGUE.forEach(p => {
+ const isUnlocked = this.economy.isPlantUnlocked(p.id);
+ const isInDeck = selectedDeck.includes(p.id);
+ const canAfford = this.economy.totalCoins >= (p.price || 0);
+
+ const item = document.createElement('div');
+ item.className = `seed-inv-item ${isUnlocked ? (isInDeck ? 'in-deck' : 'unlocked') : 'locked'}`;
+
+ let statusBadge = '';
+ if (isInDeck) {
+ statusBadge = '
✓ В колоде';
+ } else if (isUnlocked) {
+ statusBadge = '
+ Выбрать';
+ } else {
+ statusBadge = `
`;
+ }
+
+ item.innerHTML = `
+
${p.icon}
+
${p.name}
+
☀️ ${p.cost}
+ ${statusBadge}
+ `;
+
+ if (isUnlocked) {
+ item.addEventListener('click', () => {
+ if (isInDeck) {
+ this.economy.removeFromDeck(p.id);
+ } else {
+ this.economy.addToDeck(p.id);
+ }
+ this.renderSeedSelectionUI();
+ if (window.PvZAudio) window.PvZAudio.playButtonClick();
+ });
+ } else {
+ const buyBtn = item.querySelector('button');
+ if (buyBtn) {
+ buyBtn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ const res = this.economy.buyPlant(p.id);
+ if (res.success) {
+ if (window.PvZAudio) window.PvZAudio.playCoin();
+ this.saveUserData();
+ this.updateShopUI();
+ this.renderSeedSelectionUI();
+ }
+ });
+ }
+ }
+
+ inventoryContainer.appendChild(item);
+ });
+ }
+ }
+
updateShopUI() {
const elCoins = document.getElementById('shop-coins');
- if (elCoins) elCoins.textContent = `💰 Монеты: ${this.economy.totalCoins}`;
+ if (elCoins) elCoins.textContent = `💰 Монеты: ${this.economy.totalCoins.toLocaleString('ru-RU')}`;
const btnBuySlot = document.getElementById('btn-buy-slot');
if (btnBuySlot) {
@@ -1581,6 +1799,54 @@ window.GameEngine = class GameEngine {
btnBuyRake.textContent = this.economy.upgrades.hasRake ? 'Куплено ✔️' : 'Купить (150 💰)';
btnBuyRake.disabled = this.economy.upgrades.hasRake || this.economy.totalCoins < 150;
}
+
+ // Populate Shop Plants Grid
+ const shopPlantsGrid = document.getElementById('shop-plants-grid');
+ if (shopPlantsGrid && window.PLANT_CATALOGUE) {
+ shopPlantsGrid.innerHTML = '';
+ window.PLANT_CATALOGUE.forEach(p => {
+ const isBasic = this.economy.defaultUnlocked.includes(p.id);
+ const isUnlocked = this.economy.isPlantUnlocked(p.id);
+ const canAfford = this.economy.totalCoins >= (p.price || 0);
+
+ const card = document.createElement('div');
+ card.className = 'shop-plant-card';
+
+ let actionHtml = '';
+ if (isBasic) {
+ actionHtml = '
Базовое ✅';
+ } else if (isUnlocked) {
+ actionHtml = '
Куплено ✅';
+ } else {
+ actionHtml = `
`;
+ }
+
+ card.innerHTML = `
+
${p.icon}
+
+
${p.name}
+
☀️ ${p.cost} солнца
+
${p.desc}
+
+
${actionHtml}
+ `;
+
+ const buyBtn = card.querySelector('button.btn-buy');
+ if (buyBtn) {
+ buyBtn.addEventListener('click', () => {
+ const res = this.economy.buyPlant(p.id);
+ if (res.success) {
+ if (window.PvZAudio) window.PvZAudio.playCoin();
+ this.saveUserData();
+ this.updateShopUI();
+ this.renderSeedSelectionUI();
+ }
+ });
+ }
+
+ shopPlantsGrid.appendChild(card);
+ });
+ }
}
updateWorkshopUI() {
diff --git a/game/js/systems/auth.js b/game/js/systems/auth.js
index 11603dd..1aef9d7 100644
--- a/game/js/systems/auth.js
+++ b/game/js/systems/auth.js
@@ -48,6 +48,8 @@ window.PvZAuth = (function () {
highScore: 0,
theme: 'classic',
upgrades: { extraSlot: false, hasRake: false },
+ unlockedPlants: ['sunflower', 'peashooter', 'wallnut'],
+ selectedDeck: ['sunflower', 'peashooter', 'wallnut'],
materials: { bioHumus: 10, scrapMetal: 5, sunEssence: 20, crystals: 0 },
plantLevels: { peashooter: 1, sunflower: 1, wallnut: 1, potatomine: 1,
repeater: 1, snowpea: 1, chomper: 1 },
@@ -113,10 +115,12 @@ window.PvZAuth = (function () {
return {
...def,
...serverData,
- upgrades: { ...def.upgrades, ...(serverData.upgrades || {}) },
- materials: { ...def.materials, ...(serverData.materials || {}) },
- plantLevels: { ...def.plantLevels, ...(serverData.plantLevels || {}) },
- perks: { ...def.perks, ...(serverData.perks || {}) },
+ upgrades: { ...def.upgrades, ...(serverData.upgrades || {}) },
+ unlockedPlants: serverData.unlockedPlants || def.unlockedPlants,
+ selectedDeck: serverData.selectedDeck || def.selectedDeck,
+ materials: { ...def.materials, ...(serverData.materials || {}) },
+ plantLevels: { ...def.plantLevels, ...(serverData.plantLevels || {}) },
+ perks: { ...def.perks, ...(serverData.perks || {}) },
zenGarden: serverData.zenGarden
? {
...def.zenGarden,
diff --git a/game/js/systems/economy.js b/game/js/systems/economy.js
index f40def0..30555a5 100644
--- a/game/js/systems/economy.js
+++ b/game/js/systems/economy.js
@@ -1,3 +1,23 @@
+// 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() {
@@ -10,45 +30,156 @@ window.EconomySystem = class EconomySystem {
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 = [
- { id: 'sunflower', name: 'Подсолнух', cost: 50, cooldown: 7.5, timer: 0 },
- { id: 'peashooter', name: 'Горохострел', cost: 100, cooldown: 7.5, timer: 0 },
- { id: 'potatomine', name: 'Мина', cost: 25, cooldown: 18.0, timer: 0 },
- { id: 'wallnut', name: 'Стеноорех', cost: 50, cooldown: 20.0, timer: 0 },
- { id: 'repeater', name: 'Повторитель', cost: 200, cooldown: 7.5, timer: 0 },
- { id: 'snowpea', name: 'Ледяной', cost: 175, cooldown: 10.0, timer: 0 },
- { id: 'chomper', name: 'Зубастик', cost: 150, cooldown: 10.0, timer: 0 },
- { id: 'cherrybomb', name: 'Вишня', cost: 150, cooldown: 25.0, timer: 0 },
- { id: 'jalapeno', name: 'Халапеньо', cost: 125, cooldown: 30.0, timer: 0 },
- { id: 'sunshroom', name: 'Солнцегриб', cost: 25, cooldown: 7.5, timer: 0 },
- { id: 'fumeshroom', name: 'Дымогриб', cost: 75, cooldown: 7.5, timer: 0 },
- { id: 'lilypad', name: 'Кувшинка', cost: 25, cooldown: 7.5, timer: 0 },
- { id: 'gravebuster', name: 'Могилоед', cost: 75, cooldown: 15.0, timer: 0 },
- { id: 'flowerpot', name: 'Горшок', cost: 25, cooldown: 7.5, timer: 0 },
- { id: 'cabbagepult', name: 'Капуста', cost: 100, cooldown: 7.5, timer: 0 },
- { id: 'melonpult', name: 'Арбуз', cost: 300, cooldown: 15.0, timer: 0 }
- ];
+ 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];
+ }
}
}
@@ -56,10 +187,15 @@ window.EconomySystem = class EconomySystem {
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
+ upgrades: this.upgrades,
+ unlockedPlants: this.unlockedPlants,
+ selectedDeck: this.selectedDeck
});
}
}
@@ -69,26 +205,30 @@ window.EconomySystem = class EconomySystem {
this.isConveyor = false;
this.isIZombie = false;
- // Card slots count (5 default, 6 with Dave's extra slot upgrade)
- const slotCount = this.upgrades.extraSlot ? 6 : 5;
+ // Use player selected deck if configured and valid
+ if (this.selectedDeck && this.selectedDeck.length > 0) {
+ this.setDeckFromSelection();
+ return;
+ }
- // Choose relevant deck based on biome and progression
+ // 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', 'hypnoshroom'];
+ availableIds = ['sunshroom', 'fumeshroom', 'potatomine', 'wallnut', 'gravebuster'];
} else if (biome === 'pool') {
availableIds = ['sunflower', 'peashooter', 'lilypad', 'wallnut', 'repeater', 'snowpea'];
} else {
- if (level === 1) availableIds = ['sunflower', 'peashooter', 'potatomine', 'wallnut', 'cherrybomb'];
- else if (level === 2) availableIds = ['sunflower', 'peashooter', 'repeater', 'wallnut', 'jalapeno'];
- else availableIds = ['sunflower', 'peashooter', 'snowpea', 'chomper', 'wallnut', 'jalapeno'];
+ availableIds = ['sunflower', 'peashooter', 'wallnut', 'potatomine', 'snowpea', 'repeater', 'chomper', 'cherrybomb'];
}
- this.cards = this.allCards.filter(c => availableIds.includes(c.id)).slice(0, slotCount);
+ const filtered = availableIds.filter(id => this.isPlantUnlocked(id));
+ const deckIds = filtered.length > 0 ? filtered.slice(0, slotCount) : this.defaultUnlocked;
+ this.setDeckFromSelection(deckIds);
}
setBossDeck() {
diff --git a/scripts/db.py b/scripts/db.py
index 5ea1c58..445cc3f 100644
--- a/scripts/db.py
+++ b/scripts/db.py
@@ -88,7 +88,14 @@ def get_default_profile_dict():
"maxUnlockedLevel": 1,
"highScore": 0,
"theme": "classic",
- "upgrades": {"extraSlot": False, "hasRake": False},
+ "upgrades": {
+ "extraSlot": False,
+ "hasRake": False,
+ "unlockedPlants": ["sunflower", "peashooter", "wallnut"],
+ "selectedDeck": ["sunflower", "peashooter", "wallnut"]
+ },
+ "unlockedPlants": ["sunflower", "peashooter", "wallnut"],
+ "selectedDeck": ["sunflower", "peashooter", "wallnut"],
"materials": {"bioHumus": 10, "scrapMetal": 5, "sunEssence": 20, "crystals": 0},
"plantLevels": {"peashooter": 1, "sunflower": 1, "wallnut": 1, "potatomine": 1, "repeater": 1, "snowpea": 1, "chomper": 1},
"perks": {"magnetism": 0, "startSun": 0, "fastCool": 0, "hardyRoots": 0, "luckyDrop": 0},
@@ -199,13 +206,19 @@ def authenticate_user(username, password_hash):
if not p:
profile_dict = get_default_profile_dict()
else:
+ upg_data = json.loads(p["upgrades"] or "{}")
+ unlocked_plants = upg_data.get("unlockedPlants", ["sunflower", "peashooter", "wallnut"])
+ selected_deck = upg_data.get("selectedDeck", ["sunflower", "peashooter", "wallnut"])
+
profile_dict = {
"coins": p["coins"],
"level": p["level"],
"maxUnlockedLevel": p["max_unlocked_level"],
"highScore": p["high_score"],
"theme": p["theme"],
- "upgrades": json.loads(p["upgrades"] or "{}"),
+ "upgrades": upg_data,
+ "unlockedPlants": unlocked_plants,
+ "selectedDeck": selected_deck,
"materials": json.loads(p["materials"] or "{}"),
"plantLevels": json.loads(p["plant_levels"] or "{}"),
"perks": json.loads(p["perks"] or "{}"),
@@ -247,7 +260,15 @@ def save_user_profile(username, data):
cur_obj.update(new_val)
return json.dumps(cur_obj)
- upgrades_json = merge_json("upgrades", {})
+ upg_obj = json.loads(current["upgrades"] or "{}") if current and current["upgrades"] else {}
+ if "upgrades" in data and isinstance(data["upgrades"], dict):
+ upg_obj.update(data["upgrades"])
+ if "unlockedPlants" in data and isinstance(data["unlockedPlants"], list):
+ upg_obj["unlockedPlants"] = data["unlockedPlants"]
+ if "selectedDeck" in data and isinstance(data["selectedDeck"], list):
+ upg_obj["selectedDeck"] = data["selectedDeck"]
+ upgrades_json = json.dumps(upg_obj)
+
materials_json = merge_json("materials", {})
plant_levels_json = merge_json("plant_levels", {}) if "plant_levels" in data else (json.dumps(data["plantLevels"]) if "plantLevels" in data else (current["plant_levels"] if current else "{}"))
perks_json = merge_json("perks", {})
diff --git a/scripts/test_game.py b/scripts/test_game.py
index a108118..e677857 100644
--- a/scripts/test_game.py
+++ b/scripts/test_game.py
@@ -736,6 +736,54 @@ def test_http_endpoint():
except Exception as e:
record(f"HTTP {name} ({url})", "FAIL", str(e))
+def test_plant_shop_and_deck_selection():
+ print("\n--- 11b. Plant Shop, Locked Plants & Pre-Battle Deck Selection ---")
+ econ_path = os.path.join(GAME_DIR, "js/systems/economy.js")
+ with open(econ_path, "r", encoding="utf-8") as f:
+ econ_code = f.read()
+
+ record("PLANT_CATALOGUE defined in economy.js", "PASS" if "window.PLANT_CATALOGUE" in econ_code else "FAIL")
+ record("Sunflower default unlocked in PLANT_CATALOGUE", "PASS" if "id: 'sunflower'" in econ_code and "defaultUnlocked: true" in econ_code else "FAIL")
+ record("Peashooter default unlocked in PLANT_CATALOGUE", "PASS" if "id: 'peashooter'" in econ_code and "defaultUnlocked: true" in econ_code else "FAIL")
+ record("Wallnut default unlocked in PLANT_CATALOGUE", "PASS" if "id: 'wallnut'" in econ_code and "defaultUnlocked: true" in econ_code else "FAIL")
+ record("PotatoMine locked by default with price", "PASS" if "id: 'potatomine'" in econ_code and "price: 150" in econ_code and "defaultUnlocked: false" in econ_code else "FAIL")
+ record("Melonpult locked by default with price", "PASS" if "id: 'melonpult'" in econ_code and "price: 750" in econ_code and "defaultUnlocked: false" in econ_code else "FAIL")
+
+ econ_methods = [
+ "isPlantUnlocked",
+ "buyPlant",
+ "addToDeck",
+ "removeFromDeck",
+ "autoFillDeck",
+ "setDeckFromSelection",
+ "getMaxSlots"
+ ]
+ for m in econ_methods:
+ if m in econ_code:
+ record(f"EconomySystem.{m} method implemented", "PASS")
+ else:
+ record(f"EconomySystem.{m} method implemented", "FAIL", f"Missing {m}")
+
+ index_html_path = os.path.join(GAME_DIR, "index.html")
+ with open(index_html_path, "r", encoding="utf-8") as f:
+ html_code = f.read()
+
+ record("Shop plants grid present in HTML", "PASS" if 'id="shop-plants-grid"' in html_code else "FAIL")
+ record("Pre-battle seed select modal present in HTML", "PASS" if 'id="seed-select-modal"' in html_code else "FAIL")
+ record("Seed active deck container present in HTML", "PASS" if 'id="seed-active-deck"' in html_code else "FAIL")
+ record("Seed inventory grid container present in HTML", "PASS" if 'id="seed-inventory-grid"' in html_code else "FAIL")
+ record("Start battle button in seed select modal present", "PASS" if 'id="btn-seed-start-battle"' in html_code else "FAIL")
+ record("Auto-fill button in seed select modal present", "PASS" if 'id="btn-seed-auto-fill"' in html_code else "FAIL")
+ record("Inventory start button on main menu present", "PASS" if 'id="btn-inventory-start"' in html_code else "FAIL")
+
+ game_js_path = os.path.join(GAME_DIR, "js/game.js")
+ with open(game_js_path, "r", encoding="utf-8") as f:
+ game_code = f.read()
+
+ record("GameEngine.openSeedSelectionModal implemented", "PASS" if "openSeedSelectionModal(" in game_code else "FAIL")
+ record("GameEngine.renderSeedSelectionUI implemented", "PASS" if "renderSeedSelectionUI(" in game_code else "FAIL")
+ record("Shop plants nursery population implemented in updateShopUI", "PASS" if "shop-plants-grid" in game_code and "PLANT_CATALOGUE" in game_code else "FAIL")
+
def main():
print("==================================================")
print("🌱 PLANTS VS. ZOMBIES — AUTONOMOUS TEST SUITE 🌱")
@@ -752,6 +800,7 @@ def main():
test_v2_1_features()
test_campaign_system()
test_feedback_and_layout_system()
+ test_plant_shop_and_deck_selection()
test_http_endpoint()
passed = sum(1 for r in TEST_RESULTS if r["status"] == "PASS")