Files
ai-qpid-pvz/game/js/systems/workshop.js
T
Antigravity 6457a8904e feat: Initial commit Plants vs Zombies Web Edition v2.4
- 5 Biomes: Day, Night, Pool, Fog, Roof (25 Campaign levels)
- Minigames: Wall-nut Bowling, I Zombie, Roguelike, Boss Rush & Dr. Zomboss
- Systems: Zen Garden, Workshop Crafting, Almanac, Tree of Wisdom, Achievements
- Backend: REST API + SQLite Accounts & Save Synchronization
- Infrastructure: Nginx HTTPS & Docker deployment
2026-09-17 09:30:52 +00:00

351 lines
14 KiB
JavaScript

// Workshop System (Crafting, 3-Tier Plant Upgrades, and Passive Perks)
window.WorkshopSystem = class WorkshopSystem {
constructor(economy) {
this.economy = economy;
// Default Materials
this.materials = {
bioHumus: 10,
scrapMetal: 5,
sunEssence: 20,
crystals: 0
};
// Plant Upgrade Levels (1, 2, 3)
this.plantLevels = {
peashooter: 1,
sunflower: 1,
wallnut: 1,
potatomine: 1,
repeater: 1,
snowpea: 1,
chomper: 1
};
// Passive Perks (0, 1, 2, 3)
this.perks = {
magnetism: 0,
startSun: 0,
fastCool: 0,
hardyRoots: 0,
luckyDrop: 0
};
// Flying loot items on canvas
this.floatingLoot = [];
// Upgrade Definitions
this.plantDefs = {
peashooter: {
name: 'Горохострел',
icon: '🌱',
tiers: {
1: { title: 'Базовый', desc: 'Урон 20, интервал 1.4с' },
2: { title: 'Скорострел', desc: 'Урон 25, атака +20%, крит 15% (x2 урон)', cost: { coins: 150, bioHumus: 8 } },
3: { title: 'Плазменный стрелок', desc: 'Пробивает 1-го зомби насквозь и ранит следующего', cost: { coins: 400, bioHumus: 20, crystals: 1 } }
}
},
sunflower: {
name: 'Подсолнух',
icon: '🌻',
tiers: {
1: { title: 'Базовый', desc: 'Дает 25 ☀️ каждые 12с' },
2: { title: 'Крупный подсолнух', desc: 'Дает 35 ☀️ каждые 10с', cost: { coins: 120, sunEssence: 15 } },
3: { title: 'Благодатный свет', desc: 'При генерации лечит +30 HP соседним растениям', cost: { coins: 350, sunEssence: 35, crystals: 1 } }
}
},
wallnut: {
name: 'Стеноорех',
icon: '🌰',
tiers: {
1: { title: 'Базовый', desc: '400 HP' },
2: { title: 'Каменный монолит', desc: '700 HP + 15% снижения входящего урона', cost: { coins: 150, scrapMetal: 10 } },
3: { title: 'Шипованный бастион', desc: '1000 HP + возврат 30% урона кусающим зомби', cost: { coins: 400, scrapMetal: 25, crystals: 1 } }
}
},
potatomine: {
name: 'Картофельная мина',
icon: '🥔',
tiers: {
1: { title: 'Базовый', desc: 'Взведение 15с, взрыв в клетке' },
2: { title: 'Быстрый фитиль', desc: 'Взведение всего за 6 секунд!', cost: { coins: 120, bioHumus: 8, scrapMetal: 5 } },
3: { title: 'Атомный Spudow', desc: 'Взрыв 3x3 + горящая лужа лавы на 5с', cost: { coins: 350, bioHumus: 18, scrapMetal: 15, crystals: 1 } }
}
},
repeater: {
name: 'Повторитель',
icon: '🌿',
tiers: {
1: { title: 'Базовый', desc: '2 горошины за выстрел' },
2: { title: 'Тройной залп', desc: '2 горошины + 20% шанс выпустить 3-ю', cost: { coins: 250, bioHumus: 15, sunEssence: 10 } },
3: { title: 'Огненный повторитель', desc: 'Горошины пылают и наносят сплэш-урон', cost: { coins: 600, bioHumus: 30, sunEssence: 25, crystals: 2 } }
}
},
snowpea: {
name: 'Ледяной горох',
icon: '❄️',
tiers: {
1: { title: 'Базовый', desc: 'Замедление 4с, урон 20' },
2: { title: 'Вечная мерзлота', desc: 'Холод 6с + замедляет соседей сзади', cost: { coins: 250, bioHumus: 15, crystals: 1 } },
3: { title: 'Ледяная глыба', desc: 'Шанс 15% полностью обратить зомби в лед на 2с', cost: { coins: 600, bioHumus: 30, crystals: 3 } }
}
},
chomper: {
name: 'Зубастик',
icon: '🌺',
tiers: {
1: { title: 'Базовый', desc: 'Заглатывает зомби, жевание 30с' },
2: { title: 'Острые клыки', desc: 'Скорость жевания ускорена до 14с', cost: { coins: 200, bioHumus: 15, scrapMetal: 10 } },
3: { title: 'Двойной укус', desc: 'Способен проглотить сразу двух зомби подряд!', cost: { coins: 500, bioHumus: 30, scrapMetal: 20, crystals: 1 } }
}
}
};
this.perkDefs = {
magnetism: {
name: 'Магнитный газон',
icon: '🧲',
desc: 'Автоматически притягивает монеты, солнце и материалы',
tiers: {
1: { title: 'Ур. 1', desc: 'Радиус сбора +40px', cost: { coins: 150, scrapMetal: 5 } },
2: { title: 'Ур. 2', desc: 'Радиус сбора +90px', cost: { coins: 300, scrapMetal: 12 } },
3: { title: 'Ур. 3', desc: 'Полный автосбор со всего экрана!', cost: { coins: 600, scrapMetal: 25, crystals: 1 } }
}
},
startSun: {
name: 'Стартовый капитал',
icon: '☀️',
desc: 'Дополнительное солнце в начале каждого раунда',
tiers: {
1: { title: 'Ур. 1', desc: '+50 начального солнца', cost: { coins: 150, sunEssence: 15 } },
2: { title: 'Ур. 2', desc: '+100 начального солнца', cost: { coins: 350, sunEssence: 30 } },
3: { title: 'Ур. 3', desc: '+150 начального солнца', cost: { coins: 700, sunEssence: 60, crystals: 1 } }
}
},
fastCool: {
name: 'Ускоренный полив',
icon: '⏳',
desc: 'Сокращает время перезарядки всех пакетов семян',
tiers: {
1: { title: 'Ур. 1', desc: 'Перезарядка -10%', cost: { coins: 200, bioHumus: 10 } },
2: { title: 'Ур. 2', desc: 'Перезарядка -20%', cost: { coins: 450, bioHumus: 25 } },
3: { title: 'Ур. 3', desc: 'Перезарядка -30%', cost: { coins: 900, bioHumus: 50, crystals: 2 } }
}
},
hardyRoots: {
name: 'Упорные корни',
icon: '🛡️',
desc: 'Увеличивает максимальное здоровье всех растений на газоне',
tiers: {
1: { title: 'Ур. 1', desc: '+20% к запасу здоровья', cost: { coins: 180, scrapMetal: 10, bioHumus: 10 } },
2: { title: 'Ур. 2', desc: '+35% к запасу здоровья', cost: { coins: 400, scrapMetal: 20, bioHumus: 20 } },
3: { title: 'Ур. 3', desc: '+50% к запасу здоровья', cost: { coins: 800, scrapMetal: 40, bioHumus: 40, crystals: 1 } }
}
},
luckyDrop: {
name: 'Золотая рука',
icon: '🍀',
desc: 'Повышает шанс выпадения золотых монет и редких материалов',
tiers: {
1: { title: 'Ур. 1', desc: 'Шанс дропа +30%', cost: { coins: 200, crystals: 1 } },
2: { title: 'Ур. 2', desc: 'Шанс дропа +60%', cost: { coins: 500, crystals: 2 } },
3: { title: 'Ур. 3', desc: 'Шанс дропа +100%', cost: { coins: 1000, crystals: 4 } }
}
}
};
this.loadFromAuth();
}
loadFromAuth() {
const defaultMaterials = { bioHumus: 10, scrapMetal: 5, sunEssence: 20, crystals: 0 };
const defaultPlantLevels = { peashooter: 1, sunflower: 1, wallnut: 1, potatomine: 1, repeater: 1, snowpea: 1, chomper: 1 };
const defaultPerks = { magnetism: 0, startSun: 0, fastCool: 0, hardyRoots: 0, luckyDrop: 0 };
if (window.PvZAuth) {
const data = window.PvZAuth.getUserData();
this.materials = { ...defaultMaterials, ...(data.materials || {}) };
this.plantLevels = { ...defaultPlantLevels, ...(data.plantLevels || {}) };
this.perks = { ...defaultPerks, ...(data.perks || {}) };
} else {
this.materials = { ...defaultMaterials };
this.plantLevels = { ...defaultPlantLevels };
this.perks = { ...defaultPerks };
}
}
saveToAuth() {
if (window.PvZAuth && !window.PvZAuth.isGuest()) {
window.PvZAuth.saveUserData({
materials: this.materials,
plantLevels: this.plantLevels,
perks: this.perks,
coins: this.economy ? this.economy.totalCoins : undefined
});
}
}
getPlantLevel(plantType) {
return this.plantLevels[plantType] || 1;
}
getPerkLevel(perkId) {
return this.perks[perkId] || 0;
}
canAffordCost(cost) {
if (!cost) return false;
const coins = this.economy ? this.economy.totalCoins : 0;
if (cost.coins && coins < cost.coins) return false;
if (cost.bioHumus && this.materials.bioHumus < cost.bioHumus) return false;
if (cost.scrapMetal && this.materials.scrapMetal < cost.scrapMetal) return false;
if (cost.sunEssence && this.materials.sunEssence < cost.sunEssence) return false;
if (cost.crystals && this.materials.crystals < cost.crystals) return false;
return true;
}
payCost(cost) {
if (!this.canAffordCost(cost)) return false;
if (cost.coins && this.economy) this.economy.addCoins(-cost.coins);
if (cost.bioHumus) this.materials.bioHumus -= cost.bioHumus;
if (cost.scrapMetal) this.materials.scrapMetal -= cost.scrapMetal;
if (cost.sunEssence) this.materials.sunEssence -= cost.sunEssence;
if (cost.crystals) this.materials.crystals -= cost.crystals;
this.saveToAuth();
return true;
}
upgradePlant(plantId) {
const current = this.getPlantLevel(plantId);
if (current >= 3) return false;
const nextTier = current + 1;
const def = this.plantDefs[plantId];
if (!def || !def.tiers[nextTier]) return false;
const cost = def.tiers[nextTier].cost;
if (this.payCost(cost)) {
this.plantLevels[plantId] = nextTier;
this.saveToAuth();
if (nextTier === 3 && this.economy && this.economy.engine && this.economy.engine.achievements) {
this.economy.engine.achievements.trigger('tier3_unlocked');
}
if (window.PvZAudio) window.PvZAudio.playWin();
return true;
}
return false;
}
upgradePerk(perkId) {
const current = this.getPerkLevel(perkId);
if (current >= 3) return false;
const nextTier = current + 1;
const def = this.perkDefs[perkId];
if (!def || !def.tiers[nextTier]) return false;
const cost = def.tiers[nextTier].cost;
if (this.payCost(cost)) {
this.perks[perkId] = nextTier;
this.saveToAuth();
if (window.PvZAudio) window.PvZAudio.playWin();
return true;
}
return false;
}
addMaterial(type, amount = 1) {
if (this.materials[type] !== undefined) {
this.materials[type] += amount;
this.saveToAuth();
}
}
// Spawn visual drop on field
spawnLoot(x, y, type) {
this.floatingLoot.push({
x,
y,
startY: y,
targetY: Math.min(y + 25, 550),
type,
animTime: 0,
vy: -120,
collected: false,
alpha: 1.0
});
}
update(dt, gameState) {
// Check loot collection and animation
const mousePos = gameState.hoverCell ? gameState.grid.getCellBounds(gameState.hoverCell.col, gameState.hoverCell.row) : null;
const magLevel = this.getPerkLevel('magnetism');
const magnetRadius = magLevel === 3 ? 9999 : (magLevel === 2 ? 140 : (magLevel === 1 ? 80 : 35));
for (let i = this.floatingLoot.length - 1; i >= 0; i--) {
const loot = this.floatingLoot[i];
loot.animTime += dt;
if (!loot.collected) {
if (loot.vy < 0) {
loot.y += loot.vy * dt;
loot.vy += 300 * dt;
}
// Magnetism collection
if (gameState.hoverCell) {
const bounds = gameState.grid.getCellBounds(gameState.hoverCell.col, gameState.hoverCell.row);
const dist = Math.hypot(bounds.centerX - loot.x, bounds.centerY - loot.y);
if (dist <= magnetRadius) {
this.collectLoot(loot);
}
}
} else {
loot.y -= 100 * dt;
loot.alpha -= 2.0 * dt;
if (loot.alpha <= 0) {
this.floatingLoot.splice(i, 1);
}
}
}
}
collectLoot(loot) {
if (loot.collected) return;
loot.collected = true;
this.addMaterial(loot.type, 1);
if (window.PvZAudio) window.PvZAudio.playCoin();
}
draw(ctx) {
for (const loot of this.floatingLoot) {
ctx.save();
ctx.globalAlpha = Math.max(0, loot.alpha);
ctx.translate(loot.x, loot.y);
// Bounce bobbing
const bob = Math.sin(loot.animTime * 6) * 3;
ctx.translate(0, bob);
// Glow circle
ctx.beginPath();
ctx.arc(0, 0, 16, 0, Math.PI * 2);
ctx.fillStyle = loot.type === 'crystals' ? 'rgba(0, 229, 255, 0.4)' :
(loot.type === 'sunEssence' ? 'rgba(255, 235, 59, 0.4)' :
(loot.type === 'scrapMetal' ? 'rgba(158, 158, 158, 0.4)' : 'rgba(139, 195, 74, 0.4)'));
ctx.fill();
// Icon
ctx.font = '16px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const icon = loot.type === 'crystals' ? '💎' :
(loot.type === 'sunEssence' ? '☀️' :
(loot.type === 'scrapMetal' ? '⚙️' : '🧪'));
ctx.fillText(icon, 0, 1);
ctx.restore();
}
}
};