feat(gameplay): add crazy dave casino and bank, and zombie mutation lab

This commit is contained in:
Antigravity committed 2026-09-22 09:25:08 +00:00
1 parent d8be8f4366
commit b8db5a123d
16 files changed
+2365 -135

No files matched your search

+714 -82
View File
@@ -47,6 +47,12 @@ window.GameEngine = class GameEngine {
this.feedback = window.PvZFeedback;
this.feedback.init();
}
if (window.CasinoSystem) {
this.casino = new window.CasinoSystem(this.economy);
}
if (window.ZombieLabSystem) {
this.zombieLab = new window.ZombieLabSystem(this.economy, this.workshop);
}
this.wave = new window.WaveSystem();
this.combat = new window.CombatSystem();
if (window.ProjectilePool) {
@@ -97,6 +103,14 @@ window.GameEngine = class GameEngine {
}
window._pvzEngine = this; // allow auth.js to trigger profile reload
// Dynamic profile sync event listener
window.addEventListener('pvz:profile-updated', () => {
this.loadUserData();
this.updateProfileUI();
this.updateRecordDisplay();
this.updateShopUI();
});
// Bind events
this.bindEvents();
this.bindUI();
@@ -131,6 +145,8 @@ window.GameEngine = class GameEngine {
if (this.achievements) this.achievements.loadFromAuth();
if (this.tree) this.tree.loadFromAuth();
if (this.leaderboards) this.leaderboards.loadFromAuth();
if (this.casino) this.casino.loadFromAuth();
if (this.zombieLab) this.zombieLab.loadFromAuth();
} else {
this.currentLevel = parseInt(localStorage.getItem('pvz_level') || '1', 10);
this.maxUnlockedLevel = parseInt(localStorage.getItem('pvz_max_level') || this.currentLevel.toString(), 10);
@@ -162,9 +178,12 @@ window.GameEngine = class GameEngine {
saveUserData() {
if (window.PvZAuth) {
if (this.workshop) this.workshop.saveToAuth();
if (this.achievements) this.achievements.saveToAuth();
if (this.tree) this.tree.saveToAuth();
if (this.leaderboards) this.leaderboards.saveToAuth();
if (this.casino) this.casino.saveToAuth();
if (this.zombieLab) this.zombieLab.saveToAuth();
window.PvZAuth.saveUserData({
coins: this.economy ? this.economy.totalCoins : undefined,
level: this.currentLevel,
@@ -319,11 +338,107 @@ window.GameEngine = class GameEngine {
if (statPlants) statPlants.textContent = (stats.plantsPlanted || 0).toLocaleString('ru-RU');
if (statLevelsWon) statLevelsWon.textContent = (stats.levelsCompleted || 0).toLocaleString('ru-RU');
if (statCoinsEarned) statCoinsEarned.textContent = (stats.coinsEarned || 0).toLocaleString('ru-RU');
this.renderUserSessionsUI();
}
}
}
}
async renderUserSessionsUI() {
const listEl = document.getElementById('auth-sessions-list');
if (!listEl) return;
if (!window.PvZAuth || window.PvZAuth.isGuest()) {
listEl.innerHTML = '<div style="font-size: 12px; color: #b0bec5; text-align: center; padding: 8px;">Войдите в аккаунт для просмотра сессий</div>';
return;
}
const sessions = await window.PvZAuth.fetchSessions();
if (!sessions || sessions.length === 0) {
listEl.innerHTML = '<div style="font-size: 12px; color: #b0bec5; text-align: center; padding: 8px;">Нет активных сессий</div>';
return;
}
listEl.innerHTML = '';
sessions.forEach(s => {
const card = document.createElement('div');
card.className = `session-card ${s.isCurrent ? 'is-current' : ''}`;
// Choose icon based on device name
let icon = '🖥️';
const dName = (s.deviceName || '').toLowerCase();
if (dName.includes('iphone') || dName.includes('android') || dName.includes('phone') || dName.includes('mobile')) {
icon = '📱';
} else if (dName.includes('ipad') || dName.includes('tablet')) {
icon = '📟';
} else if (dName.includes('mac') || dName.includes('apple')) {
icon = '🍎';
} else if (dName.includes('windows') || dName.includes('linux') || dName.includes('chrome') || dName.includes('pc')) {
icon = '💻';
}
const iconDiv = document.createElement('div');
iconDiv.className = 'session-icon';
iconDiv.textContent = icon;
const detailsDiv = document.createElement('div');
detailsDiv.className = 'session-details';
const devNameDiv = document.createElement('div');
devNameDiv.className = 'session-dev-name';
devNameDiv.textContent = s.deviceName || 'Браузер / Компьютер';
const metaDiv = document.createElement('div');
metaDiv.className = 'session-meta';
const ipSpan = document.createElement('span');
ipSpan.textContent = `IP: ${s.ipAddress || '127.0.0.1'}`;
const activeSpan = document.createElement('span');
activeSpan.textContent = `Активность: ${s.lastActive || s.createdAt || 'Только что'}`;
metaDiv.appendChild(ipSpan);
metaDiv.appendChild(activeSpan);
detailsDiv.appendChild(devNameDiv);
detailsDiv.appendChild(metaDiv);
card.appendChild(iconDiv);
card.appendChild(detailsDiv);
if (s.isCurrent) {
const badge = document.createElement('span');
badge.className = 'session-current-badge';
badge.textContent = '🟢 Текущее устройство';
card.appendChild(badge);
} else {
const btnTerm = document.createElement('button');
btnTerm.type = 'button';
btnTerm.className = 'btn btn-danger btn-terminate-session';
btnTerm.textContent = 'Завершить';
btnTerm.addEventListener('click', async (e) => {
e.stopPropagation();
btnTerm.disabled = true;
btnTerm.textContent = '⏳';
const res = await window.PvZAuth.terminateSession(s.token);
if (res && res.success) {
if (window.PvZAuth && window.PvZAuth.showNotification) {
window.PvZAuth.showNotification('Сессия устройства завершена', s.deviceName, '🔒');
}
} else {
if (window.PvZAuth && window.PvZAuth.showNotification) {
window.PvZAuth.showNotification('Ошибка завершения сессии', res?.message || 'Сбой', '❌');
}
}
await this.renderUserSessionsUI();
});
card.appendChild(btnTerm);
}
listEl.appendChild(card);
});
}
openMainMenu() {
this.isRunning = false;
if (window.PvZAudio) window.PvZAudio.stopMusic();
@@ -333,7 +448,7 @@ window.GameEngine = class GameEngine {
// Close any game-phase modals that might have been left open
['game-over-modal', 'victory-modal', 'draft-modal', 'zengarden-modal',
'workshop-modal', 'leaderboard-modal', 'achievement-modal', 'almanac-modal',
'bugreport-modal', 'campaign-modal', 'tree-modal'].forEach(id => {
'bugreport-modal', 'campaign-modal', 'tree-modal', 'casino-modal'].forEach(id => {
const el = document.getElementById(id);
if (el) el.classList.add('hidden');
});
@@ -417,75 +532,133 @@ window.GameEngine = class GameEngine {
this.rakeCol = 7;
}
if (this.wave && this.wave.startingSun !== null && this.wave.startingSun !== undefined && this.wave.startingSun >= 0) {
this.economy.sun = this.wave.startingSun;
}
this.isRunning = true;
this.lastTime = performance.now();
const musicTheme = (mode === 'boss' || mode === 'bossrush') ? 'boss' : this.wave.biome;
if (window.PvZAudio) window.PvZAudio.startMusic(musicTheme);
}
setupIZombieLawn() {
setupIZombieLawn(stage) {
this.plants = [];
this.izombieSetupDone = true;
const currentStage = stage || (this.zombieLab ? (this.zombieLab.selectedStage || this.zombieLab.stage || 1) : 1);
// Col 0: Sunflowers in rows 0..4 (5 trophy targets)
for (let r = 0; r < 5; r++) {
const b0 = this.grid.getCellBounds(0, r);
const sf = new window.Plant('sunflower', 0, r, b0.x, b0.y, b0.w, b0.h);
this.grid.setPlant(0, r, sf);
this.plants.push(sf);
const addPlant = (type, col, row, options = {}) => {
const b = this.grid.getCellBounds(col, row);
const p = new window.Plant(type, col, row, b.x, b.y, b.w, b.h);
if (options.isArmed) {
p.isArmed = true;
p.armTimer = 15;
}
this.grid.setPlant(col, row, p);
this.plants.push(p);
return p;
};
if (currentStage === 5) {
// Stage 5: RAID BOSS - Mega Sunflower (5000 HP) with Royal Defense!
for (let r = 0; r < 5; r++) {
if (r === 2) {
addPlant('mega_sunflower', 0, r); // Center lane Raid Boss!
} else {
addPlant('sunflower', 0, r);
}
}
addPlant('snowpea', 2, 0);
addPlant('tallnut', 4, 0);
addPlant('repeater', 1, 1);
addPlant('repeater', 3, 1);
addPlant('tallnut', 4, 1);
// Lane 2: The Boss Lane
addPlant('repeater', 1, 2);
addPlant('snowpea', 2, 2);
addPlant('tallnut', 4, 2);
addPlant('repeater', 1, 3);
addPlant('repeater', 3, 3);
addPlant('tallnut', 4, 3);
addPlant('snowpea', 2, 4);
addPlant('tallnut', 4, 4);
} else if (currentStage === 4) {
// Stage 4: The Great Wall (Tallnuts & repeaters)
for (let r = 0; r < 5; r++) {
addPlant('sunflower', 0, r);
addPlant('repeater', 1, r);
addPlant('tallnut', 3, r);
}
addPlant('potatomine', 4, 1, { isArmed: true });
addPlant('potatomine', 4, 3, { isArmed: true });
} else if (currentStage === 3) {
// Stage 3: Frost & Thorns (Snowpeas & armed mines)
for (let r = 0; r < 5; r++) {
addPlant('sunflower', 0, r);
}
addPlant('snowpea', 2, 0);
addPlant('wallnut', 4, 0);
addPlant('repeater', 1, 1);
addPlant('snowpea', 3, 1);
addPlant('potatomine', 4, 1, { isArmed: true });
addPlant('snowpea', 2, 2);
addPlant('tallnut', 4, 2);
addPlant('repeater', 1, 3);
addPlant('potatomine', 3, 3, { isArmed: true });
addPlant('wallnut', 4, 3);
addPlant('snowpea', 2, 4);
addPlant('chomper', 4, 4);
} else if (currentStage === 2) {
// Stage 2: Night Guard (Mushrooms & Peashooters)
for (let r = 0; r < 5; r++) {
addPlant('sunflower', 0, r);
}
addPlant('peashooter', 1, 0);
addPlant('fumeshroom', 3, 0);
addPlant('snowpea', 2, 1);
addPlant('wallnut', 4, 1);
addPlant('repeater', 1, 2);
addPlant('repeater', 3, 2);
addPlant('wallnut', 5, 2);
addPlant('fumeshroom', 2, 3);
addPlant('chomper', 4, 3);
addPlant('peashooter', 1, 4);
addPlant('potatomine', 3, 4, { isArmed: true });
} else {
// Stage 1: Classic Lawn
for (let r = 0; r < 5; r++) {
addPlant('sunflower', 0, r);
}
addPlant('peashooter', 2, 0);
addPlant('repeater', 2, 1);
addPlant('wallnut', 4, 1);
addPlant('snowpea', 1, 2);
addPlant('peashooter', 3, 2);
addPlant('peashooter', 2, 3);
addPlant('wallnut', 4, 3);
addPlant('repeater', 1, 4);
addPlant('potatomine', 3, 4, { isArmed: true });
}
// Row 0 defense: Peashooter at col 2
const b02 = this.grid.getCellBounds(2, 0);
const p02 = new window.Plant('peashooter', 2, 0, b02.x, b02.y, b02.w, b02.h);
this.grid.setPlant(2, 0, p02);
this.plants.push(p02);
// Row 1 defense: Repeater at col 2, Wallnut at col 4
const b12 = this.grid.getCellBounds(2, 1);
const p12 = new window.Plant('repeater', 2, 1, b12.x, b12.y, b12.w, b12.h);
this.grid.setPlant(2, 1, p12);
this.plants.push(p12);
const b14 = this.grid.getCellBounds(4, 1);
const p14 = new window.Plant('wallnut', 4, 1, b14.x, b14.y, b14.w, b14.h);
this.grid.setPlant(4, 1, p14);
this.plants.push(p14);
// Row 2 defense: Snowpea at col 1, Peashooter at col 3
const b21 = this.grid.getCellBounds(1, 2);
const p21 = new window.Plant('snowpea', 1, 2, b21.x, b21.y, b21.w, b21.h);
this.grid.setPlant(1, 2, p21);
this.plants.push(p21);
const b23 = this.grid.getCellBounds(3, 2);
const p23 = new window.Plant('peashooter', 3, 2, b23.x, b23.y, b23.w, b23.h);
this.grid.setPlant(3, 2, p23);
this.plants.push(p23);
// Row 3 defense: Peashooter at col 2, Wallnut at col 4
const b32 = this.grid.getCellBounds(2, 3);
const p32 = new window.Plant('peashooter', 2, 3, b32.x, b32.y, b32.w, b32.h);
this.grid.setPlant(2, 3, p32);
this.plants.push(p32);
const b34 = this.grid.getCellBounds(4, 3);
const p34 = new window.Plant('wallnut', 4, 3, b34.x, b34.y, b34.w, b34.h);
this.grid.setPlant(4, 3, p34);
this.plants.push(p34);
// Row 4 defense: Repeater at col 1, Potato Mine at col 3 (armed)
const b41 = this.grid.getCellBounds(1, 4);
const p41 = new window.Plant('repeater', 1, 4, b41.x, b41.y, b41.w, b41.h);
this.grid.setPlant(1, 4, p41);
this.plants.push(p41);
const b43 = this.grid.getCellBounds(3, 4);
const p43 = new window.Plant('potatomine', 3, 4, b43.x, b43.y, b43.w, b43.h);
p43.isArmed = true;
p43.armTimer = 15;
this.grid.setPlant(3, 4, p43);
this.plants.push(p43);
}
showDaveDialogue(level, mode, onContinue) {
@@ -593,6 +766,15 @@ window.GameEngine = class GameEngine {
const zombie = new window.Zombie(row, type);
const bounds = this.grid.getCellBounds(0, row);
zombie.y = bounds.centerY - 5;
if (this.wave) {
if (this.wave.hpMultiplier && this.wave.hpMultiplier !== 1.0) {
zombie.maxHp = Math.round(zombie.maxHp * this.wave.hpMultiplier);
zombie.hp = Math.round(zombie.hp * this.wave.hpMultiplier);
}
if (this.wave.speedMultiplier && this.wave.speedMultiplier !== 1.0) {
zombie.speed *= this.wave.speedMultiplier;
}
}
this.zombies.push(zombie);
}
@@ -661,7 +843,7 @@ window.GameEngine = class GameEngine {
this.economy.addCoins(100); // 100 coin bonus for level completion!
// Check achievement: clean_lawn (all lawnmowers intact)
if (this.grid.lawnmowers && this.grid.lawnmowers.length > 0 && this.grid.lawnmowers.every(lm => lm && lm.active)) {
if (this.grid.lawnmowers && this.grid.lawnmowers.length > 0 && this.grid.lawnmowers.every(lm => lm && !lm.used && !lm.active)) {
if (this.achievements) this.achievements.trigger('clean_lawn');
}
@@ -690,6 +872,27 @@ window.GameEngine = class GameEngine {
// Save level progression
let levelReward = 100;
let izombieNextUnlocked = false;
let izombieCurrentStage = 1;
let izombieRewardCoins = 150;
if (this.currentMode === 'izombie' && this.zombieLab) {
izombieCurrentStage = this.zombieLab.selectedStage || 1;
izombieNextUnlocked = this.zombieLab.unlockNextStage(izombieCurrentStage);
const stDef = this.zombieLab.stages.find(s => s.id === izombieCurrentStage);
if (stDef) {
izombieRewardCoins = stDef.reward || 150;
if (stDef.crystals && this.workshop) {
this.workshop.addMaterial('crystals', stDef.crystals);
}
}
if (this.workshop) {
this.workshop.addMaterial('scrapMetal', 6);
this.workshop.addMaterial('bioHumus', 4);
}
this.economy.addCoins(Math.max(0, izombieRewardCoins - 100));
}
if (this.currentMode === 'adventure') {
const campLevel = window.CampaignSystem ? window.CampaignSystem.getLevel(this.currentLevel) : null;
if (campLevel && campLevel.reward) {
@@ -726,7 +929,7 @@ window.GameEngine = class GameEngine {
const statsEl = document.getElementById('victory-stats');
if (statsEl) {
if (this.currentMode === 'izombie') {
statsEl.innerHTML = `Победа! Все подсолнухи съедены! Мозги захвачены!<br>Награда: <strong>+100 💰 монет</strong> (Всего: ${this.economy.totalCoins})`;
statsEl.innerHTML = `Победа! Все цели уничтожены! Мозги захвачены!<br>Этап «Я, Зомби»: <strong>Этап ${izombieCurrentStage} из 5</strong>${izombieNextUnlocked ? ' (🎉 Открыт следующий этап!)' : ''}<br>Награда: <strong>+${izombieRewardCoins} 💰 монет</strong>, <strong>+6 ⚙️ деталей</strong>, <strong>+4 🧪 биогумуса</strong> (Всего: ${this.economy.totalCoins})`;
} else if (this.currentMode === 'bossrush') {
statsEl.innerHTML = `Триумф! Режим Boss Rush успешно завершён!<br>Награда: <strong>+500 💰 монет</strong> &bull; <strong>+2 💎 кристалла</strong> (Всего: ${this.economy.totalCoins})`;
} else if (this.currentMode === 'adventure' && window.CampaignSystem) {
@@ -1174,6 +1377,11 @@ window.GameEngine = class GameEngine {
authModal.classList.remove('hidden');
if (authUser && window.PvZAuth && window.PvZAuth.isGuest()) {
setTimeout(() => authUser.focus(), 50);
} else if (window.PvZAuth && !window.PvZAuth.isGuest()) {
window.PvZAuth.fetchProfile().then(() => {
this.loadUserData();
this.updateProfileUI();
});
}
});
}
@@ -1368,6 +1576,49 @@ window.GameEngine = class GameEngine {
});
}
// Active Sessions event listeners
const btnRefreshSessions = document.getElementById('btn-refresh-sessions');
if (btnRefreshSessions) {
btnRefreshSessions.addEventListener('click', async () => {
btnRefreshSessions.classList.add('rotating');
await this.renderUserSessionsUI();
setTimeout(() => btnRefreshSessions.classList.remove('rotating'), 500);
});
}
const btnTermOthers = document.getElementById('btn-terminate-other-sessions');
if (btnTermOthers) {
btnTermOthers.addEventListener('click', async () => {
if (confirm('Завершить сессии на всех других устройствах?')) {
btnTermOthers.disabled = true;
btnTermOthers.textContent = '⏳ Завершение...';
const res = await window.PvZAuth.terminateOtherSessions();
btnTermOthers.disabled = false;
btnTermOthers.textContent = '🔒 Завершить сессии на всех других устройствах';
if (res && res.success) {
if (window.PvZAuth && window.PvZAuth.showNotification) {
window.PvZAuth.showNotification('Все другие сессии завершены!', 'Осталось только текущее устройство', '🔒');
}
} else {
if (window.PvZAuth && window.PvZAuth.showNotification) {
window.PvZAuth.showNotification('Ошибка', res?.message || 'Не удалось завершить сессии', '❌');
}
}
await this.renderUserSessionsUI();
}
});
}
// Real-time Profile change event subscription
if (window.PvZAuth && typeof window.PvZAuth.onProfileChange === 'function') {
window.PvZAuth.onProfileChange(() => {
this.loadUserData();
this.updateProfileUI();
this.updateRecordDisplay();
this.updateShopUI();
});
}
// Auto-save on page hide / tab switch / page unload
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
@@ -1523,8 +1774,10 @@ window.GameEngine = class GameEngine {
const btnCloseWorkshop = document.getElementById('btn-close-workshop');
const tabPlants = document.getElementById('tab-plants');
const tabPerks = document.getElementById('tab-perks');
const tabZombies = document.getElementById('tab-zombies');
const contentPlants = document.getElementById('ws-content-plants');
const contentPerks = document.getElementById('ws-content-perks');
const contentZombies = document.getElementById('ws-content-zombies');
const openWorkshop = () => {
this.updateWorkshopUI();
@@ -1540,18 +1793,194 @@ window.GameEngine = class GameEngine {
});
}
if (tabPlants && tabPerks && contentPlants && contentPerks) {
tabPlants.addEventListener('click', () => {
tabPlants.classList.add('active');
tabPerks.classList.remove('active');
contentPlants.classList.remove('hidden');
contentPerks.classList.add('hidden');
const switchWorkshopTab = (tab) => {
if (tabPlants) tabPlants.classList.toggle('active', tab === 'plants');
if (tabPerks) tabPerks.classList.toggle('active', tab === 'perks');
if (tabZombies) tabZombies.classList.toggle('active', tab === 'zombies');
if (contentPlants) contentPlants.classList.toggle('hidden', tab !== 'plants');
if (contentPerks) contentPerks.classList.toggle('hidden', tab !== 'perks');
if (contentZombies) contentZombies.classList.toggle('hidden', tab !== 'zombies');
};
if (tabPlants) tabPlants.addEventListener('click', () => switchWorkshopTab('plants'));
if (tabPerks) tabPerks.addEventListener('click', () => switchWorkshopTab('perks'));
if (tabZombies) tabZombies.addEventListener('click', () => switchWorkshopTab('zombies'));
// Casino Modal Handlers
const casinoModal = document.getElementById('casino-modal');
const btnCasinoStart = document.getElementById('btn-casino-start');
const btnCasinoDrawer = document.getElementById('btn-drawer-casino');
const btnCloseCasino = document.getElementById('btn-close-casino');
const tabCasinoSlots = document.getElementById('tab-casino-slots');
const tabCasinoBank = document.getElementById('tab-casino-bank');
const contentCasinoSlots = document.getElementById('casino-content-slots');
const contentCasinoBank = document.getElementById('casino-content-bank');
const openCasino = () => {
this.updateCasinoUI();
if (casinoModal) casinoModal.classList.remove('hidden');
};
if (btnCasinoStart) btnCasinoStart.addEventListener('click', openCasino);
if (btnCasinoDrawer) btnCasinoDrawer.addEventListener('click', () => {
openCasino();
const drawer = document.getElementById('mobile-drawer');
if (drawer) drawer.classList.add('hidden');
});
if (btnCloseCasino && casinoModal) {
btnCloseCasino.addEventListener('click', () => {
casinoModal.classList.add('hidden');
this.updateRecordDisplay();
});
tabPerks.addEventListener('click', () => {
tabPerks.classList.add('active');
tabPlants.classList.remove('active');
contentPerks.classList.remove('hidden');
contentPlants.classList.add('hidden');
}
if (tabCasinoSlots && tabCasinoBank) {
tabCasinoSlots.addEventListener('click', () => {
tabCasinoSlots.classList.add('active');
tabCasinoBank.classList.remove('active');
if (contentCasinoSlots) contentCasinoSlots.classList.remove('hidden');
if (contentCasinoBank) contentCasinoBank.classList.add('hidden');
});
tabCasinoBank.addEventListener('click', () => {
tabCasinoBank.classList.add('active');
tabCasinoSlots.classList.remove('active');
if (contentCasinoBank) contentCasinoBank.classList.remove('hidden');
if (contentCasinoSlots) contentCasinoSlots.classList.add('hidden');
this.updateCasinoUI();
});
}
// Casino Bet Buttons
const betBtns = document.querySelectorAll('.btn-bet');
betBtns.forEach(btn => {
btn.addEventListener('click', () => {
betBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const betVal = parseInt(btn.getAttribute('data-bet') || '50', 10);
if (this.casino) this.casino.currentBet = betVal;
});
});
// Casino Spin Button
const btnSpin = document.getElementById('btn-spin-slots');
const reel1 = document.getElementById('reel-1');
const reel2 = document.getElementById('reel-2');
const reel3 = document.getElementById('reel-3');
const resultBanner = document.getElementById('slot-result-banner');
if (btnSpin) {
btnSpin.addEventListener('click', () => {
if (!this.casino) return;
if (this.casino.isSpinning) return;
const bet = this.casino.currentBet || 50;
if (this.economy.totalCoins < bet) {
if (resultBanner) {
resultBanner.textContent = '❌ Недостаточно монет для ставки! Возьмите займ в банке.';
resultBanner.style.color = '#ef5350';
}
return;
}
// Animate spin
[reel1, reel2, reel3].forEach(r => { if (r) r.classList.add('spinning'); });
if (btnSpin) btnSpin.disabled = true;
if (resultBanner) {
resultBanner.textContent = '🎰 Барабаны вращаются...';
resultBanner.style.color = '#ffca28';
}
setTimeout(() => {
if (this.casino) {
this.casino.isSpinning = false;
const res = this.casino.spin(bet);
[reel1, reel2, reel3].forEach(r => { if (r) r.classList.remove('spinning'); });
if (btnSpin) btnSpin.disabled = false;
if (res.icons) {
if (reel1) reel1.innerHTML = `<span class="reel-symbol">${res.icons[0]}</span>`;
if (reel2) reel2.innerHTML = `<span class="reel-symbol">${res.icons[1]}</span>`;
if (reel3) reel3.innerHTML = `<span class="reel-symbol">${res.icons[2]}</span>`;
}
if (resultBanner) {
resultBanner.textContent = res.message || '';
if (res.isJackpot) resultBanner.style.color = '#ffd700';
else if (res.payout > 0) resultBanner.style.color = '#69f0ae';
else resultBanner.style.color = '#b0bec5';
}
this.updateCasinoUI();
this.updateRecordDisplay();
}
}, 500);
});
}
// Bank Deposit
const btnDeposit = document.getElementById('btn-bank-deposit');
const inputDeposit = document.getElementById('input-deposit-amount');
if (btnDeposit) {
btnDeposit.addEventListener('click', () => {
if (!this.casino) return;
const amt = parseInt(inputDeposit ? inputDeposit.value : '0', 10);
const res = this.casino.depositCoins(amt);
if (resultBanner) {
resultBanner.textContent = res.message;
resultBanner.style.color = res.success ? '#69f0ae' : '#ef5350';
}
if (inputDeposit && res.success) inputDeposit.value = '';
this.updateCasinoUI();
this.updateRecordDisplay();
});
}
// Bank Withdraw
const btnWithdraw = document.getElementById('btn-bank-withdraw');
if (btnWithdraw) {
btnWithdraw.addEventListener('click', () => {
if (!this.casino) return;
const res = this.casino.withdrawDeposit();
if (resultBanner) {
resultBanner.textContent = res.message;
resultBanner.style.color = res.success ? '#69f0ae' : '#ef5350';
}
this.updateCasinoUI();
this.updateRecordDisplay();
});
}
// Bank Take Loan
const btnTakeLoan = document.getElementById('btn-bank-take-loan');
const inputLoan = document.getElementById('input-loan-amount');
if (btnTakeLoan) {
btnTakeLoan.addEventListener('click', () => {
if (!this.casino) return;
const amt = parseInt(inputLoan ? inputLoan.value : '0', 10);
const res = this.casino.takeLoan(amt);
if (resultBanner) {
resultBanner.textContent = res.message;
resultBanner.style.color = res.success ? '#69f0ae' : '#ef5350';
}
this.updateCasinoUI();
this.updateRecordDisplay();
});
}
// Bank Repay Loan
const btnRepayLoan = document.getElementById('btn-bank-repay-loan');
if (btnRepayLoan) {
btnRepayLoan.addEventListener('click', () => {
if (!this.casino) return;
const amt = parseInt(inputLoan && inputLoan.value ? inputLoan.value : String(this.casino.loan), 10);
const res = this.casino.repayLoan(amt);
if (resultBanner) {
resultBanner.textContent = res.message;
resultBanner.style.color = res.success ? '#69f0ae' : '#ef5350';
}
if (inputLoan && res.success) inputLoan.value = '';
this.updateCasinoUI();
this.updateRecordDisplay();
});
}
@@ -2085,7 +2514,9 @@ window.GameEngine = class GameEngine {
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 price = window.PvZConfig ? window.PvZConfig.getPlantShopPrice(p.id) : (p.price || 0);
const sunCost = window.PvZConfig ? window.PvZConfig.getPlantSunCost(p.id, p.cost) : p.cost;
const canAfford = this.economy.totalCoins >= price;
const card = document.createElement('div');
card.className = 'shop-plant-card';
@@ -2096,14 +2527,14 @@ window.GameEngine = class GameEngine {
} else if (isUnlocked) {
actionHtml = '<span class="badge-purchased">Куплено ✅</span>';
} else {
actionHtml = `<button type="button" class="btn-buy" data-plant-id="${p.id}" ${canAfford ? '' : 'disabled'}>Купить (${p.price} 💰)</button>`;
actionHtml = `<button type="button" class="btn-buy" data-plant-id="${p.id}" ${canAfford ? '' : 'disabled'}>Купить (${price} 💰)</button>`;
}
card.innerHTML = `
<div class="shop-plant-icon">${p.icon}</div>
<div class="shop-plant-info">
<h4>${p.name}</h4>
<div class="shop-plant-cost">☀️ ${p.cost} солнца</div>
<div class="shop-plant-cost">☀️ ${sunCost} солнца</div>
<p>${p.desc}</p>
</div>
<div>${actionHtml}</div>
@@ -2310,6 +2741,193 @@ window.GameEngine = class GameEngine {
});
});
}
// 4. Zombie Lab mutations & campaign stages
this.renderZombieLabUI();
}
renderZombieLabUI() {
const container = document.getElementById('zombies-upgrade-list');
if (!container || !this.zombieLab) return;
container.innerHTML = '';
// Campaign Stage Selector Banner
const stageBanner = document.createElement('div');
stageBanner.className = 'upgrade-card';
stageBanner.style.gridColumn = '1 / -1';
stageBanner.style.background = 'linear-gradient(135deg, rgba(76, 175, 80, 0.15), rgba(38, 50, 56, 0.95))';
stageBanner.style.border = '1px solid rgba(76, 175, 80, 0.5)';
const selStage = this.zombieLab.selectedStage || 1;
const unlStage = this.zombieLab.unlockedStage || 1;
let stageBtnsHtml = '';
this.zombieLab.stages.forEach(st => {
const isUnlocked = st.id <= unlStage;
const isSelected = st.id === selStage;
const activeStyle = isSelected ? 'border: 2px solid #ffeb3b; background: rgba(255, 235, 59, 0.25); color: #fff;' : '';
const lockIcon = isUnlocked ? (st.isBoss ? '👑' : '🧠') : '🔒';
stageBtnsHtml += `
<button class="btn btn-small btn-stage-select ${isSelected ? 'active' : ''}" data-stage-id="${st.id}" ${isUnlocked ? '' : 'disabled'} style="margin: 3px; font-size: 11px; padding: 5px 9px; ${activeStyle}">
${lockIcon} Этап ${st.id} ${st.isBoss ? '(БОСС)' : ''}
</button>
`;
});
const curStageDef = this.zombieLab.stages.find(s => s.id === selStage) || this.zombieLab.stages[0];
stageBanner.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px;">
<div>
<h3 style="margin: 0; color: #a5d6a7; font-size: 15px;">🗺️ Кампания «Я, Зомби» (Этап ${selStage} из 5)</h3>
<p style="margin: 3px 0 0 0; font-size: 12px; color: #cfd8dc;">${curStageDef.title}: ${curStageDef.desc}</p>
</div>
<div style="display: flex; flex-wrap: wrap; gap: 4px;">
${stageBtnsHtml}
</div>
</div>
`;
container.appendChild(stageBanner);
// Bind stage select buttons
stageBanner.querySelectorAll('.btn-stage-select').forEach(btn => {
btn.addEventListener('click', (e) => {
const stId = parseInt(e.currentTarget.getAttribute('data-stage-id'), 10);
this.zombieLab.selectedStage = stId;
this.renderZombieLabUI();
});
});
// Zombie Mutation Cards
for (const [key, zDef] of Object.entries(this.zombieLab.zombies)) {
const curTier = this.zombieLab.getZombieLevel(key);
const isMax = curTier >= 3;
const curData = zDef.tiers[curTier] || zDef.tiers[1];
const nextData = isMax ? null : zDef.tiers[curTier + 1];
const card = document.createElement('div');
card.className = 'upgrade-card zombie-tier-card';
let costHtml = '';
if (nextData && nextData.cost) {
const parts = [];
if (nextData.cost.coins) parts.push(`💰 ${nextData.cost.coins}`);
if (nextData.cost.scrapMetal) parts.push(`⚙️ ${nextData.cost.scrapMetal}`);
if (nextData.cost.bioHumus) parts.push(`🧪 ${nextData.cost.bioHumus}`);
if (nextData.cost.crystals) parts.push(`💎 ${nextData.cost.crystals}`);
costHtml = parts.join(' &bull; ');
}
const stars = '★'.repeat(curTier) + '☆'.repeat(3 - curTier);
const canAfford = this.zombieLab.canUpgrade(key);
card.innerHTML = `
<div class="upgrade-card-header">
<div class="upgrade-plant-icon">${zDef.icon}</div>
<div class="upgrade-title-box">
<h4>${zDef.name}</h4>
<div class="upgrade-stars" style="color: #69f0ae;">${stars} (Ур. ${curTier}/3)</div>
</div>
<div class="zombie-tier-badge">Ранг ${curTier}</div>
</div>
<div class="upgrade-stats-current" style="font-size: 12px; color: #cfd8dc; margin-bottom: 6px;">
<strong>Текущий бонус:</strong> ${curData.title} &mdash; ${curData.desc}
</div>
${!isMax ? `
<div class="upgrade-stats-next" style="font-size: 12px; color: #a5d6a7; margin-bottom: 8px; border-left-color: #69f0ae;">
<strong>Мутация до Ур. ${curTier + 1}:</strong> ${nextData.title} &mdash; ${nextData.desc}
</div>
<button class="btn btn-upgrade" data-upgrade-zombie="${key}" ${canAfford ? '' : 'disabled'} style="width: 100%; font-size: 12px; padding: 7px;">
Мутировать (${costHtml})
</button>
` : `
<div class="upgrade-stats-next" style="border-left-color: #ffd700; color: #ffe082; font-size: 12px;">
🧬 Максимальная мутация достигнута!
</div>
`}
`;
container.appendChild(card);
}
// Bind upgrade buttons for zombies
container.querySelectorAll('[data-upgrade-zombie]').forEach(btn => {
btn.addEventListener('click', (e) => {
const zType = e.currentTarget.getAttribute('data-upgrade-zombie');
const res = this.zombieLab.upgradeZombie(zType);
if (res.success) {
this.saveUserData();
this.updateWorkshopUI();
}
});
});
}
updateCasinoUI() {
if (!this.casino) return;
const coinsEl = document.getElementById('casino-coins');
if (coinsEl) coinsEl.textContent = (this.economy ? this.economy.totalCoins : 0).toLocaleString();
const depStat = document.getElementById('bank-deposit-stat');
if (depStat) depStat.textContent = this.casino.deposit.toLocaleString();
const intStat = document.getElementById('bank-interest-stat');
if (intStat) intStat.textContent = `+${this.casino.getAccruedInterest().toLocaleString()}`;
const debtStat = document.getElementById('bank-debt-stat');
if (debtStat) debtStat.textContent = this.casino.loan.toLocaleString();
const depVal = document.getElementById('bank-deposit-val');
if (depVal) depVal.textContent = this.casino.deposit.toLocaleString();
const intVal = document.getElementById('bank-interest-val');
if (intVal) intVal.textContent = this.casino.getAccruedInterest().toLocaleString();
const debtVal = document.getElementById('bank-debt-val');
if (debtVal) debtVal.textContent = this.casino.loan.toLocaleString();
}
triggerShrekEasterEgg() {
if (window.PvZAudio && window.PvZAudio.playExplosion) {
window.PvZAudio.playExplosion();
}
const canvas = document.getElementById('game-canvas');
if (canvas) {
canvas.classList.add('shake');
setTimeout(() => canvas.classList.remove('shake'), 1200);
}
let affected = 0;
if (this.zombies && this.zombies.length > 0) {
for (const z of this.zombies) {
if (z && z.health > 0) {
z.takeDamage(2000);
affected++;
if (this.combat && this.combat.createSplash) {
this.combat.createSplash(z.x, z.y, '#7cb342', 12);
}
}
}
}
let toast = document.getElementById('achievement-toast');
if (!toast) {
toast = document.createElement('div');
toast.id = 'achievement-toast';
toast.className = 'achievement-toast';
document.body.appendChild(toast);
}
toast.innerHTML = `
<div class="ach-toast-icon">🧅</div>
<div class="ach-toast-text">
<div class="ach-toast-subtitle">«THIS IS MY SWAMP!»</div>
<div class="ach-toast-title">ШРЕК СОТРЯС ЗЕМЛЮ! 2000 урона всем зомби! (${affected} поражено)</div>
</div>
`;
toast.classList.add('visible');
setTimeout(() => toast.classList.remove('visible'), 4000);
}
initZenGarden() {
@@ -2756,6 +3374,16 @@ window.GameEngine = class GameEngine {
return;
}
function escapeHtml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
let rowsHtml = '';
entries.forEach((item, index) => {
let medal = `#${index + 1}`;
@@ -2763,12 +3391,16 @@ window.GameEngine = class GameEngine {
else if (index === 1) medal = '🥈 2';
else if (index === 2) medal = '🥉 3';
const safeName = escapeHtml(item.name || 'Игрок');
const safeDetail = escapeHtml(item.detail || item.score || '');
const safeDate = escapeHtml(item.date || '');
rowsHtml += `
<tr class="${index < 3 ? 'top-rank' : ''}">
<td style="font-weight: bold; width: 60px; text-align: center;">${medal}</td>
<td style="font-weight: bold; color: #fff;">${item.name}</td>
<td style="color: #ffd54f; font-weight: bold;">${item.detail || item.score}</td>
<td style="color: #b0bec5; font-size: 11px; text-align: right;">${item.date || ''}</td>
<td style="font-weight: bold; color: #fff;">${safeName}</td>
<td style="color: #ffd54f; font-weight: bold;">${safeDetail}</td>
<td style="color: #b0bec5; font-size: 11px; text-align: right;">${safeDate}</td>
</tr>
`;
});
@@ -3117,14 +3749,14 @@ window.GameEngine = class GameEngine {
// 9. I, Zombie Win & Defeat Check
if (this.currentMode === 'izombie' && this.izombieSetupDone && this.isRunning) {
const sunflowers = this.plants.filter(p => p.type === 'sunflower');
if (sunflowers.length === 0) {
const targets = this.plants.filter(p => p.type === 'sunflower' || p.type === 'mega_sunflower');
if (targets.length === 0) {
this.triggerVictory();
} else {
const minZombieCost = 50;
const hasAliveZombies = this.zombies.some(z => z.health > 0 && !z.toRemove);
if (!hasAliveZombies && this.economy.sun < minZombieCost) {
this.triggerGameOver('Закончились солнце и зомби! Подсолнухи уцелели.');
this.triggerGameOver('Закончились солнце и зомби! Растения уцелели.');
}
}
}