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
This commit is contained in:
commit
6457a8904e
50 files changed
+14486
No files matched your search
@@ -0,0 +1,204 @@
|
||||
// Garden Roguelike System: Seed Drafting, Artifact Relics, and Progressive Stage Escalation
|
||||
window.RoguelikeSystem = class RoguelikeSystem {
|
||||
constructor(gameEngine) {
|
||||
this.engine = gameEngine;
|
||||
this.activeRelics = [];
|
||||
this.currentFloor = 1;
|
||||
this.draftChoices = [];
|
||||
|
||||
// All available relics in Roguelike mode
|
||||
this.relicPool = [
|
||||
{
|
||||
id: 'relic_lightning',
|
||||
name: 'Громоотвод',
|
||||
icon: '⚡',
|
||||
desc: 'Каждые 7 секунд молния бьет в передового зомби, нанося 160 урона.',
|
||||
timer: 7
|
||||
},
|
||||
{
|
||||
id: 'relic_toxic_soil',
|
||||
name: 'Токсичный перегной',
|
||||
icon: '🧪',
|
||||
desc: 'Вся почва выделяет споры: все зомби замедлены на 15% и получают 6 ед. яда в секунду.'
|
||||
},
|
||||
{
|
||||
id: 'relic_sun_resonance',
|
||||
name: 'Солнечный резонанс',
|
||||
icon: '☀️',
|
||||
desc: 'Все подсолнухи производят на +15 больше солнца за каждую вспышку.'
|
||||
},
|
||||
{
|
||||
id: 'relic_iron_bark',
|
||||
name: 'Титановая кора',
|
||||
icon: '🛡️',
|
||||
desc: 'Все высаживаемые Стеноорехи моментально получают щит прочности +350 HP.'
|
||||
},
|
||||
{
|
||||
id: 'relic_vampire_sap',
|
||||
name: 'Вампирский сок',
|
||||
icon: '💖',
|
||||
desc: 'Каждое уничтожение зомби восстанавливает +40 HP самому раненому растению.'
|
||||
},
|
||||
{
|
||||
id: 'relic_fertilizer_boost',
|
||||
name: 'Гормон роста',
|
||||
icon: '🌿',
|
||||
desc: 'Все стрелковые растения атакуют на 20% быстрее.'
|
||||
}
|
||||
];
|
||||
|
||||
// Seed draft candidates
|
||||
this.plantPool = [
|
||||
'snowpea',
|
||||
'repeater',
|
||||
'wallnut',
|
||||
'potatomine',
|
||||
'chomper',
|
||||
'cherrybomb',
|
||||
'jalapeno',
|
||||
'fumeshroom'
|
||||
];
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.activeRelics = [];
|
||||
this.currentFloor = 1;
|
||||
this.draftChoices = [];
|
||||
}
|
||||
|
||||
hasRelic(id) {
|
||||
return this.activeRelics.some(r => r.id === id);
|
||||
}
|
||||
|
||||
addRelic(relic) {
|
||||
if (!this.hasRelic(relic.id)) {
|
||||
this.activeRelics.push({ ...relic, timer: relic.timer || 0 });
|
||||
if (this.engine && this.engine.achievements) {
|
||||
this.engine.achievements.trigger('roguelike_relic');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate 3 choices: seeds or relics
|
||||
generateDraftChoices() {
|
||||
const choices = [];
|
||||
const availableRelics = this.relicPool.filter(r => !this.hasRelic(r.id));
|
||||
|
||||
// 1-2 Relic choices
|
||||
if (availableRelics.length > 0) {
|
||||
const shuffledRelics = [...availableRelics].sort(() => 0.5 - Math.random());
|
||||
const r = shuffledRelics[0];
|
||||
choices.push({
|
||||
type: 'relic',
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
icon: r.icon,
|
||||
desc: r.desc,
|
||||
raw: r
|
||||
});
|
||||
}
|
||||
|
||||
// Seed choices: plants not currently in economy deck
|
||||
const currentCards = this.engine && this.engine.economy ? this.engine.economy.cards.map(c => c.id) : [];
|
||||
const availablePlants = this.plantPool.filter(p => !currentCards.includes(p));
|
||||
const shuffledPlants = [...availablePlants].sort(() => 0.5 - Math.random());
|
||||
|
||||
for (const pId of shuffledPlants.slice(0, 3 - choices.length)) {
|
||||
const def = this.engine.workshop ? this.engine.workshop.plantDefs[pId] : null;
|
||||
choices.push({
|
||||
type: 'seed',
|
||||
id: pId,
|
||||
name: def ? def.name : pId,
|
||||
icon: def ? def.icon : '🌱',
|
||||
desc: `Добавляет семя ${def ? def.name : pId} в колоду для текущего забега!`,
|
||||
raw: pId
|
||||
});
|
||||
}
|
||||
|
||||
// Fill remaining if needed
|
||||
while (choices.length < 3 && this.plantPool.length > 0) {
|
||||
const pId = this.plantPool[choices.length % this.plantPool.length];
|
||||
choices.push({
|
||||
type: 'seed',
|
||||
id: pId,
|
||||
name: pId,
|
||||
icon: '🌱',
|
||||
desc: `Дополнительное семя для текущего забега.`,
|
||||
raw: pId
|
||||
});
|
||||
}
|
||||
|
||||
this.draftChoices = choices;
|
||||
return choices;
|
||||
}
|
||||
|
||||
selectChoice(index) {
|
||||
const choice = this.draftChoices[index];
|
||||
if (!choice) return false;
|
||||
|
||||
if (choice.type === 'relic') {
|
||||
this.addRelic(choice.raw);
|
||||
} else if (choice.type === 'seed' && this.engine && this.engine.economy) {
|
||||
// Add seed to deck
|
||||
const newCard = this.engine.economy.allCards.find(c => c.id === choice.raw);
|
||||
if (newCard && !this.engine.economy.cards.some(c => c.id === newCard.id)) {
|
||||
this.engine.economy.cards.push({ ...newCard, timer: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
this.currentFloor++;
|
||||
return true;
|
||||
}
|
||||
|
||||
update(dt, gameState) {
|
||||
// 1. Relic: Lightning strike
|
||||
const lightning = this.activeRelics.find(r => r.id === 'relic_lightning');
|
||||
if (lightning) {
|
||||
lightning.timer = (lightning.timer || 7) - dt;
|
||||
if (lightning.timer <= 0) {
|
||||
lightning.timer = 7;
|
||||
const target = gameState.zombies.find(z => z.health > 0 && !z.toRemove);
|
||||
if (target) {
|
||||
target.takeDamage(160);
|
||||
gameState.combat.createExplosionParticles(target.x, target.y);
|
||||
if (window.PvZAudio) window.PvZAudio.playStrike();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Relic: Toxic soil poison damage
|
||||
if (this.hasRelic('relic_toxic_soil')) {
|
||||
for (const z of gameState.zombies) {
|
||||
if (z.health > 0 && !z.toRemove) {
|
||||
z.takeDamage(6 * dt);
|
||||
z.speedModifier = Math.min(z.speedModifier, 0.85);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw active relic badges in HUD
|
||||
draw(ctx) {
|
||||
if (this.activeRelics.length === 0) return;
|
||||
|
||||
ctx.save();
|
||||
let rx = 580;
|
||||
const ry = 52;
|
||||
for (const relic of this.activeRelics) {
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.55)';
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(rx, ry, 26, 26, 4);
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = '#ffd700';
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.stroke();
|
||||
|
||||
ctx.font = '14px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(relic.icon, rx + 13, ry + 13);
|
||||
rx += 30;
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
};
|
||||
Reference in new issue
Block a user