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:
2026-09-17 09:30:52 +00:00
commit 6457a8904e
50 changed files with 14486 additions and 0 deletions
+394
View File
@@ -0,0 +1,394 @@
// Autonomous Game Testing API for Plants vs. Zombies
window.PvZTestAPI = (function() {
let engine = null;
return {
setEngine: function(e) {
engine = e;
},
getEngine: function() {
return engine;
},
getState: function() {
if (!engine) return null;
return {
isRunning: engine.isRunning,
isGameOver: engine.isGameOver,
isVictory: engine.isVictory,
currentMode: engine.currentMode,
currentLevel: engine.currentLevel,
sun: engine.economy.sun,
coins: engine.economy.totalCoins,
plantsCount: engine.plants.length,
zombiesCount: engine.zombies.length,
projectilesCount: engine.projectiles.length,
plants: engine.plants.map(p => ({ type: p.type, col: p.col, row: p.row, health: p.health })),
zombies: engine.zombies.map(z => ({ type: z.type, row: z.row, x: Math.round(z.x), health: z.health, isEating: z.isEating }))
};
},
clearField: function() {
if (!engine) return;
engine.plants = [];
engine.zombies = [];
engine.projectiles = [];
for (let r = 0; r < engine.grid.rows; r++) {
for (let c = 0; c < engine.grid.cols; c++) {
engine.grid.grid[r][c] = null;
}
}
},
spawnZombie: function(row, type = 'normal', x = 850) {
if (!engine) return null;
const z = new window.Zombie(row, type, x);
const b = engine.grid.getCellBounds(0, row);
z.y = b.centerY - 5;
engine.zombies.push(z);
return z;
},
plant: function(col, row, type) {
if (!engine) return null;
const b = engine.grid.getCellBounds(col, row);
const p = new window.Plant(type, col, row, b.x, b.y, b.w, b.h);
engine.grid.setPlant(col, row, p);
engine.plants.push(p);
return p;
},
step: function(dt = 0.05, ticks = 1) {
if (!engine) return;
for (let i = 0; i < ticks; i++) {
engine.update(dt);
}
},
runAllTests: function() {
const results = [];
let passed = 0;
function assert(name, condition, errorMsg) {
if (condition) {
results.push({ name, status: 'PASS' });
passed++;
} else {
results.push({ name, status: 'FAIL', error: errorMsg });
}
}
if (!engine) {
return { total: 0, passed: 0, failed: 1, error: 'Engine not initialized' };
}
const origMode = engine.currentMode;
const origLevel = engine.currentLevel;
try {
// Test 1: Peashooter Combat Test
this.clearField();
engine.startLevel(1, 'adventure');
this.clearField();
const p = this.plant(0, 0, 'peashooter');
const z = this.spawnZombie(0, 'normal', 500);
const startHealth = z.health;
// Step simulation until pea hits
this.step(0.1, 40); // 4 seconds of game time
assert('Peashooter damages zombie', z.health < startHealth, `Zombie health did not decrease: ${z.health}`);
// Test 2: Zombie Eating & Resume Movement Bug Test
this.clearField();
const testNut = this.plant(2, 1, 'wallnut');
testNut.health = 40; // low health so it gets eaten fast
const eatingZombie = this.spawnZombie(1, 'normal', testNut.x + 20);
// Step until zombie touches and eats nut
this.step(0.1, 20);
assert('Zombie begins eating plant', eatingZombie.isEating === true || testNut.health <= 0, 'Zombie did not start eating');
// Step until nut is consumed
this.step(0.1, 40);
const nutGone = engine.plants.length === 0 || testNut.health <= 0;
assert('Plant is consumed', nutGone, 'Plant was not consumed');
assert('Zombie resumes walking after plant death (Bug Fix)', eatingZombie.isEating === false, `isEating remained true!`);
// Test 3: Potato Mine Arming and Spudow Explosion
this.clearField();
const mine = this.plant(1, 2, 'potatomine');
assert('Potato mine starts unarmed', mine.isArmed === false, 'Mine started armed');
mine.armTimer = mine.armTime; // fast-forward arming
this.step(0.1, 2);
assert('Potato mine is armed', mine.isArmed === true, 'Mine did not arm');
const boomZombie = this.spawnZombie(2, 'normal', mine.x + 20);
this.step(0.1, 10);
assert('Potato mine explodes on contact', mine.toRemove === true || boomZombie.health <= 0, 'Mine did not explode');
// Test 4: Lawnmower Defense
this.clearField();
engine.grid.initLawnmowers();
const lawnZombie = this.spawnZombie(0, 'normal', 110); // triggers mower at x<=115
this.step(0.1, 10);
assert('Lawnmower activates on breach', engine.grid.lawnmowers[0].active === true || engine.grid.lawnmowers[0].used === true, 'Mower did not activate');
assert('Lawnmower kills intruder', lawnZombie.health <= 0 || lawnZombie.toRemove === true, 'Mower did not kill zombie');
// Test 5: I, Zombie Mode (No Instant Victory Bug Test)
this.clearField();
engine.startLevel(1, 'izombie');
assert('I, Zombie does NOT instantly trigger victory', engine.isVictory === false, 'Instant victory bug detected!');
assert('I, Zombie spawns 5 trophy sunflowers', engine.plants.filter(pl => pl.type === 'sunflower').length === 5, 'Incorrect sunflowers count');
assert('I, Zombie uses zombie deck', engine.economy.cards.some(c => c.id.startsWith('z_')), 'Zombie deck not equipped');
// Step simulation 10 ticks: game should remain running without instant win
this.step(0.1, 10);
assert('I, Zombie stays active during play', engine.isVictory === false && engine.isRunning === true, 'Unexpected victory or stop');
// Win simulation: destroy all sunflowers
for (const pl of engine.plants) {
if (pl.type === 'sunflower') pl.toRemove = true;
}
this.step(0.1, 2);
assert('I, Zombie triggers victory when all sunflowers are destroyed', engine.isVictory === true, 'Victory did not trigger upon sunflower defeat');
// Test 6: Auth & Profiles
window.PvZAuth.loginAsGuest();
assert('Guest mode detected', window.PvZAuth.isGuest() === true, 'Failed to login as guest');
const userRes = window.PvZAuth.login('autotester', 'testpass123');
assert('User registration & login succeeds', userRes.success === true && window.PvZAuth.getCurrentUser() === 'autotester', 'Login failed');
window.PvZAuth.saveUserData({ coins: 999, level: 3, highScore: 500, upgrades: {} });
const loaded = window.PvZAuth.getUserData();
assert('User profile data persists', loaded.coins === 999 && loaded.level === 3, 'Data did not persist');
window.PvZAuth.loginAsGuest(); // restore guest
// Test 7: Main Menu Navigation
engine.openMainMenu();
assert('Main menu pauses game', engine.isRunning === false, 'Game was not paused on menu open');
const startMod = document.getElementById('start-modal');
assert('Main menu opens start modal', startMod && !startMod.classList.contains('hidden'), 'Start modal was not shown');
// Test 8: Workshop, Materials & Plant Upgrades
if (engine.workshop) {
const ws = engine.workshop;
ws.materials.bioHumus = 50;
ws.materials.scrapMetal = 50;
ws.materials.crystals = 5;
engine.economy.totalCoins = 2000;
const pLevelBefore = ws.getPlantLevel('peashooter');
const upgradedT2 = ws.upgradePlant('peashooter');
assert('Workshop upgrades plant to Tier 2', upgradedT2 === true && ws.getPlantLevel('peashooter') === 2, 'Failed to upgrade to Tier 2');
const upgradedT3 = ws.upgradePlant('peashooter');
assert('Workshop upgrades plant to Tier 3', upgradedT3 === true && ws.getPlantLevel('peashooter') === 3, 'Failed to upgrade to Tier 3');
// Check perk upgrade
const perkUpgraded = ws.upgradePerk('hardyRoots');
assert('Workshop upgrades passive perk', perkUpgraded === true && ws.getPerkLevel('hardyRoots') === 1, 'Failed to upgrade perk');
// Check plant creation with upgraded tier and hardy roots
const cellB = engine.grid.getCellBounds(1, 0);
const t3Plant = engine.createPlant('peashooter', 1, 0, cellB);
assert('Planted plant inherits Tier 3 level', t3Plant.level === 3, `Expected level 3, got ${t3Plant.level}`);
assert('Hardy Roots perk boosts plant max HP', t3Plant.maxHealth > 300, `Expected boosted HP, got ${t3Plant.maxHealth}`);
// Restore peashooter level and perk for clean state
ws.plantLevels['peashooter'] = 1;
ws.perks['hardyRoots'] = 0;
} else {
assert('WorkshopSystem exists on engine', false, 'engine.workshop is undefined');
}
// Test 9: Zen Garden and Suburban Almanac
if (engine.zengarden) {
const zg = engine.zengarden;
zg.chocolateCount = 2;
zg.fertilizerCount = 3;
const feedRes = zg.feedSnail();
assert('Feeding Stinky snail with chocolate wakes and boosts snail', feedRes === true && zg.snail.isAwake === true && zg.snail.boostTimer > 0, 'Snail feed failed');
// Plant sprout in empty pot
const emptyIdx = zg.pots.findIndex(p => !p.plantType);
if (emptyIdx !== -1) {
const planted = zg.plantSprout('chomper');
assert('Zen Garden plants new sprout in empty pot', planted === true && zg.pots[emptyIdx].plantType === 'chomper', 'Sprout planting failed');
}
// Water thirsty plant
const thirstyPot = zg.pots.find(p => p.plantType && p.thirstTimer > 0);
if (thirstyPot) {
const watered = zg.waterPlant(thirstyPot.id);
assert('Watering thirsty plant succeeds', watered === true && thirstyPot.thirstTimer === 0, 'Watering failed');
} else {
zg.pots[0].thirstTimer = 10;
const watered = zg.waterPlant(zg.pots[0].id);
assert('Watering plant clears thirst timer', watered === true && zg.pots[0].thirstTimer === 0, 'Watering failed');
}
} else {
assert('ZenGardenSystem exists on engine', false, 'engine.zengarden is undefined');
}
if (engine.almanac) {
const alm = engine.almanac;
assert('Almanac has 13 plants defined', alm.plants.length >= 13, `Expected >= 13 plants, got ${alm.plants.length}`);
assert('Almanac has 8 zombies defined', alm.zombies.length >= 8, `Expected >= 8 zombies, got ${alm.zombies.length}`);
const yeti = alm.getItem('z_yeti');
assert('Almanac contains secret Zombie Yeti', yeti && yeti.id === 'z_yeti', 'Yeti not found in almanac');
} else {
assert('AlmanacSystem exists on engine', false, 'engine.almanac is undefined');
}
// Test 10: Achievements System
if (engine.achievements) {
const achs = engine.achievements;
assert('Achievements list contains 14+ achievements', achs.achievements.length >= 14, `Expected >= 14, got ${achs.achievements.length}`);
achs.trigger('first_plant');
const ach = achs.achievements.find(a => a.id === 'first_plant');
assert('first_plant achievement triggers and completes', ach && ach.completed === true, 'Achievement not completed');
const coinsBefore = engine.economy.totalCoins;
if (ach && !ach.claimed) {
const claimed = achs.claimReward('first_plant');
assert('Claiming achievement grants coins reward', claimed === true && engine.economy.totalCoins > coinsBefore, 'Reward claiming failed');
} else {
assert('Achievement reward already claimed or validated', true);
}
} else {
assert('AchievementSystem exists on engine', false, 'engine.achievements is undefined');
}
// Test 11: Garden Roguelike System
if (engine.roguelike) {
const rgl = engine.roguelike;
rgl.reset();
assert('Roguelike starts at floor 1 with 0 relics', rgl.currentFloor === 1 && rgl.activeRelics.length === 0, 'Reset failed');
const choices = rgl.generateDraftChoices();
assert('generateDraftChoices generates exactly 3 choices', choices && choices.length === 3, `Expected 3 choices, got ${choices ? choices.length : 0}`);
const selected = rgl.selectChoice(0);
assert('selectChoice successfully picks choice and increments floor', selected === true && rgl.currentFloor === 2, 'Select choice failed');
} else {
assert('RoguelikeSystem exists on engine', false, 'engine.roguelike is undefined');
}
// Test 12: Tree of Wisdom & Secret Cheats
if (engine.tree) {
const tree = engine.tree;
assert('TreeSystem has wisdom milestones defined', tree.wisdomList && tree.wisdomList.length >= 8, 'Wisdom list too small');
const initialHeight = tree.height;
engine.economy.totalCoins += 300;
const fed = tree.feedFertilizer();
assert('feedFertilizer increases height by 1', fed === true && tree.height === initialHeight + 1, 'Tree feeding failed');
const cheatState = tree.toggleCheat('mustache');
assert('toggleCheat toggles cheat state', typeof cheatState === 'boolean', 'Cheat toggle failed');
} else {
assert('TreeSystem exists on engine', false, 'engine.tree is undefined');
}
// Test 13: Dr. Zomboss Boss Fight
if (engine.boss) {
const boss = engine.boss;
boss.reset();
assert('Boss starts with 6000 HP and isActive true', boss.health === 6000 && boss.isActive === true, 'Boss init failed');
boss.takeDamage(500);
assert('Boss takeDamage accurately decreases HP', boss.health === 5500, `Expected 5500, got ${boss.health}`);
engine.economy.setBossDeck();
const hasPots = engine.economy.cards.some(c => c.id === 'flowerpot');
const hasMelon = engine.economy.cards.some(c => c.id === 'melonpult');
assert('Boss conveyor deck contains flowerpots and melon-pults', hasPots && hasMelon, 'Boss deck missing items');
} else {
assert('BossSystem exists on engine', false, 'engine.boss is undefined');
}
// Test 14: Leaderboard System
if (engine.leaderboards) {
const lb = engine.leaderboards;
assert('Leaderboards has 6 modes', Object.keys(lb.records).length >= 6, 'Missing leaderboard modes');
lb.submitScore('survival', 99, '99 Флагов (Тест)');
const entries = lb.getEntries('survival');
const top = entries[0];
assert('Leaderboard records and sorts top score', top && top.score === 99, 'Score submission failed');
} else {
assert('LeaderboardSystem exists on engine', false, 'engine.leaderboards is undefined');
}
// Test 15: Roof & Fog Biomes and Lobbed Projectiles
engine.grid.initBiome('roof');
assert('Roof biome initialized', engine.grid.biome === 'roof' && engine.grid.isRoof() === true, 'Roof biome init failed');
engine.grid.initBiome('fog');
assert('Fog biome initialized', engine.grid.biome === 'fog' && engine.grid.isFog() === true, 'Fog biome init failed');
const lobbedProj = new window.Projectile(100, 200, 1, 'melon', 80, 500, 200);
assert('Lobbed projectile initializes with arc parameters', lobbedProj.isLobbed === true && lobbedProj.targetX === 500, 'Lobbed proj init failed');
lobbedProj.update(0.1);
assert('Lobbed projectile progresses along trajectory', lobbedProj.lobProgress > 0, 'Trajectory update failed');
// Test 16: v2.1 Soundtrack, Themes & Boss Rush Mode
if (window.PvZAudio) {
window.PvZAudio.setTheme('boss');
assert('PvZAudio supports multi-biome themes', window.PvZAudio.getTheme() === 'boss', 'Theme switch failed');
window.PvZAudio.setTheme('day');
}
engine.applyTheme('cyberpunk');
assert('Visual themes switch to cyberpunk', engine.currentTheme === 'cyberpunk' && document.body.classList.contains('theme-cyberpunk'), 'Cyberpunk theme failed');
engine.applyTheme('winter');
assert('Visual themes switch to winter', engine.currentTheme === 'winter' && document.body.classList.contains('theme-winter'), 'Winter theme failed');
engine.applyTheme('classic');
assert('Visual themes reset to classic', engine.currentTheme === 'classic', 'Classic theme reset failed');
engine.wave.setLevel(1, 'bossrush');
assert('Boss Rush mode initializes 3 waves', engine.wave.totalWaves === 3 && engine.wave.biome === 'roof', 'Boss Rush init failed');
if (engine.leaderboards) {
assert('Leaderboard includes bossrush mode records', Array.isArray(engine.leaderboards.getEntries('bossrush')), 'Missing bossrush leaderboard');
}
// Test 17: Campaign System & 25 Levels Multi-Biome Progression
if (window.CampaignSystem) {
const worlds = window.CampaignSystem.WORLDS;
const levels = window.CampaignSystem.LEVELS;
assert('CampaignSystem defines 5 worlds', Array.isArray(worlds) && worlds.length === 5, `Expected 5 worlds, got ${worlds.length}`);
assert('CampaignSystem defines 25 levels', Array.isArray(levels) && levels.length === 25, `Expected 25 levels, got ${levels.length}`);
const lvl1 = window.CampaignSystem.getLevel(1);
const lvl11 = window.CampaignSystem.getLevel(11);
const lvl16 = window.CampaignSystem.getLevel(16);
const lvl25 = window.CampaignSystem.getLevel(25);
assert('Level 1-1 is Day Lawn biome', lvl1.code === '1-1' && lvl1.biome === 'day', 'Level 1 biome mismatch');
assert('Level 3-1 is Pool biome', lvl11.code === '3-1' && lvl11.biome === 'pool', 'Level 11 biome mismatch');
assert('Level 4-1 is Fog biome', lvl16.code === '4-1' && lvl16.biome === 'fog', 'Level 16 biome mismatch');
assert('Level 5-5 is Dr. Zomboss Boss fight', lvl25.code === '5-5' && lvl25.isBoss === true && lvl25.biome === 'roof', 'Level 25 boss mismatch');
engine.wave.setLevel(11, 'adventure');
assert('WaveSystem sets pool biome on level 11', engine.wave.biome === 'pool', 'Wave biome mismatch for lvl 11');
engine.wave.setLevel(16, 'adventure');
assert('WaveSystem sets fog biome on level 16', engine.wave.biome === 'fog', 'Wave biome mismatch for lvl 16');
engine.wave.setLevel(25, 'adventure');
assert('WaveSystem configures boss mode on level 25', engine.wave.mode === 'boss' && engine.wave.biome === 'roof', 'Wave boss mismatch for lvl 25');
const fmt = window.CampaignSystem.formatLevel(11);
assert('formatLevel formats code and world icon', fmt.fullLabel.includes('3-1') && fmt.worldIcon === '🏊', 'Format level mismatch');
} else {
assert('CampaignSystem is globally defined', false, 'CampaignSystem not found');
}
} catch (err) {
results.push({ name: 'Exception during test run', status: 'FAIL', error: err.toString() });
} finally {
// Restore original state
engine.startLevel(origLevel, origMode);
}
return {
total: results.length,
passed,
failed: results.length - passed,
results
};
}
};
})();