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,741 @@
|
||||
// Economy, Sun, Coin Drops & Crazy Dave's Shop System
|
||||
window.EconomySystem = class EconomySystem {
|
||||
constructor() {
|
||||
this.sun = 50;
|
||||
this.suns = [];
|
||||
this.coins = [];
|
||||
this.skySunTimer = 0;
|
||||
this.skySunInterval = 8.5;
|
||||
this.isNight = false;
|
||||
this.isConveyor = false; // For Wall-nut bowling mini-game
|
||||
this.isIZombie = false;
|
||||
|
||||
// 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.selectedCard = null;
|
||||
this.animTime = 0;
|
||||
this.setDeckForLevel(1, 'day');
|
||||
}
|
||||
|
||||
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 };
|
||||
} 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'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
saveProfileData() {
|
||||
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');
|
||||
if (window.PvZAuth && !window.PvZAuth.isGuest()) {
|
||||
window.PvZAuth.saveUserData({
|
||||
coins: this.totalCoins,
|
||||
upgrades: this.upgrades
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setDeckForLevel(level = 1, biome = 'day') {
|
||||
this.isNight = biome === 'night';
|
||||
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;
|
||||
|
||||
// Choose relevant deck based on biome and progression
|
||||
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'];
|
||||
} 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'];
|
||||
}
|
||||
|
||||
this.cards = this.allCards.filter(c => availableIds.includes(c.id)).slice(0, slotCount);
|
||||
}
|
||||
|
||||
setBossDeck() {
|
||||
this.isConveyor = true;
|
||||
this.isIZombie = false;
|
||||
this.cards = [
|
||||
{ id: 'flowerpot', name: 'Горшок', cost: 0, cooldown: 3.5, timer: 0 },
|
||||
{ id: 'cabbagepult', name: 'Капуста', cost: 0, cooldown: 5.0, timer: 0 },
|
||||
{ id: 'melonpult', name: 'Арбуз', cost: 0, cooldown: 10.0, timer: 0 },
|
||||
{ id: 'jalapeno', name: 'Халапеньо', cost: 0, cooldown: 14.0, timer: 0 },
|
||||
{ id: 'snowpea', name: 'Ледяной', cost: 0, cooldown: 8.0, timer: 0 }
|
||||
];
|
||||
}
|
||||
|
||||
setConveyorDeck() {
|
||||
this.isConveyor = true;
|
||||
this.isIZombie = false;
|
||||
this.cards = [
|
||||
{ id: 'bowling_nut', name: 'Орех-шар', cost: 0, cooldown: 4.0, timer: 0 },
|
||||
{ id: 'bowling_cherry', name: 'Взрывной', cost: 0, cooldown: 12.0, timer: 0 }
|
||||
];
|
||||
}
|
||||
|
||||
setIZombieDeck() {
|
||||
this.isConveyor = false;
|
||||
this.isIZombie = true;
|
||||
this.isNight = true;
|
||||
this.sun = 200;
|
||||
this.cards = [
|
||||
{ id: 'z_normal', name: 'Обычный', cost: 50, cooldown: 2.0, timer: 0 },
|
||||
{ id: 'z_conehead', name: 'Конус', cost: 75, cooldown: 3.0, timer: 0 },
|
||||
{ id: 'z_polevaulter', name: 'С шестом', cost: 75, cooldown: 4.0, timer: 0 },
|
||||
{ id: 'z_buckethead', name: 'Ведро', cost: 125, cooldown: 5.0, timer: 0 },
|
||||
{ id: 'z_football', name: 'Футболист', cost: 175, cooldown: 6.0, timer: 0 }
|
||||
];
|
||||
}
|
||||
|
||||
reset() {
|
||||
let baseSun = this.isConveyor ? 0 : (this.isIZombie ? 200 : 50);
|
||||
if (!this.isConveyor && !this.isIZombie && this.workshop) {
|
||||
baseSun += this.workshop.getPerkLevel('startSun') * 50;
|
||||
}
|
||||
this.sun = baseSun;
|
||||
this.suns = [];
|
||||
this.coins = [];
|
||||
this.skySunTimer = 0;
|
||||
this.selectedCard = null;
|
||||
this.cards.forEach(c => c.timer = 0);
|
||||
}
|
||||
|
||||
addSun(amount) {
|
||||
this.sun += amount;
|
||||
if (this.engine && this.engine.achievements) {
|
||||
this.engine.achievements.setProgress('sun_tycoon', this.sun);
|
||||
}
|
||||
}
|
||||
|
||||
canAfford(cost) {
|
||||
return this.isConveyor || this.sun >= cost;
|
||||
}
|
||||
|
||||
spendSun(cost) {
|
||||
if (this.isConveyor) return true;
|
||||
if (this.sun >= cost) {
|
||||
this.sun -= cost;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
addCoins(amount) {
|
||||
this.totalCoins += amount;
|
||||
this.saveProfileData();
|
||||
}
|
||||
|
||||
buyUpgrade(type) {
|
||||
if (type === 'extraSlot' && this.totalCoins >= 500 && !this.upgrades.extraSlot) {
|
||||
this.addCoins(-500);
|
||||
this.upgrades.extraSlot = true;
|
||||
this.saveProfileData();
|
||||
this.setDeckForLevel(1, this.isNight ? 'night' : 'day');
|
||||
return true;
|
||||
} else if (type === 'rake' && this.totalCoins >= 150 && !this.upgrades.hasRake) {
|
||||
this.addCoins(-150);
|
||||
this.upgrades.hasRake = true;
|
||||
this.saveProfileData();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
spawnSun(x, y, value = 25, isFalling = false, targetY = null) {
|
||||
this.suns.push({
|
||||
x,
|
||||
y,
|
||||
targetY: targetY !== null ? targetY : y,
|
||||
value,
|
||||
isFalling,
|
||||
fallSpeed: 90,
|
||||
radius: 26,
|
||||
life: 13.0,
|
||||
collecting: false,
|
||||
toRemove: false,
|
||||
pulse: Math.random() * 5
|
||||
});
|
||||
}
|
||||
|
||||
spawnCoin(x, y, type = 'silver') {
|
||||
const val = type === 'diamond' ? 200 : (type === 'gold' ? 50 : 10);
|
||||
this.coins.push({
|
||||
x,
|
||||
y,
|
||||
type,
|
||||
value: val,
|
||||
radius: 16,
|
||||
life: 12.0,
|
||||
collecting: false,
|
||||
toRemove: false,
|
||||
pulse: Math.random() * 5
|
||||
});
|
||||
}
|
||||
|
||||
update(dt) {
|
||||
this.animTime += dt;
|
||||
|
||||
// Cooldown timers with fastCool perk bonus
|
||||
const fastCoolLvl = this.workshop ? this.workshop.getPerkLevel('fastCool') : 0;
|
||||
const cooldownMultiplier = 1 + fastCoolLvl * 0.15;
|
||||
for (const card of this.cards) {
|
||||
if (card.timer > 0) {
|
||||
card.timer = Math.max(0, card.timer - dt * cooldownMultiplier);
|
||||
}
|
||||
}
|
||||
|
||||
// Sky sun spawner (disabled at night and in conveyor mode)
|
||||
if (!this.isNight && !this.isConveyor) {
|
||||
this.skySunTimer += dt;
|
||||
if (this.skySunTimer >= this.skySunInterval) {
|
||||
this.skySunTimer = 0;
|
||||
const spawnX = 140 + Math.random() * 640;
|
||||
const targetY = 140 + Math.random() * 380;
|
||||
this.spawnSun(spawnX, 40, 25, true, targetY);
|
||||
}
|
||||
}
|
||||
|
||||
// Active suns
|
||||
for (const s of this.suns) {
|
||||
s.pulse += dt * 4;
|
||||
if (s.collecting) {
|
||||
const targetX = 50;
|
||||
const targetY = 45;
|
||||
const dx = targetX - s.x;
|
||||
const dy = targetY - s.y;
|
||||
const dist = Math.hypot(dx, dy);
|
||||
|
||||
if (dist < 15) {
|
||||
this.addSun(s.value);
|
||||
s.toRemove = true;
|
||||
} else {
|
||||
s.x += (dx / dist) * 750 * dt;
|
||||
s.y += (dy / dist) * 750 * dt;
|
||||
}
|
||||
} else {
|
||||
if (s.isFalling && s.y < s.targetY) {
|
||||
s.y += s.fallSpeed * dt;
|
||||
}
|
||||
s.life -= dt;
|
||||
if (s.life <= 0) s.toRemove = true;
|
||||
}
|
||||
}
|
||||
this.suns = this.suns.filter(s => !s.toRemove);
|
||||
|
||||
// Active coins
|
||||
for (const c of this.coins) {
|
||||
c.pulse += dt * 5;
|
||||
if (c.collecting) {
|
||||
const targetX = 850;
|
||||
const targetY = 45;
|
||||
const dx = targetX - c.x;
|
||||
const dy = targetY - c.y;
|
||||
const dist = Math.hypot(dx, dy);
|
||||
|
||||
if (dist < 15) {
|
||||
this.addCoins(c.value);
|
||||
c.toRemove = true;
|
||||
} else {
|
||||
c.x += (dx / dist) * 750 * dt;
|
||||
c.y += (dy / dist) * 750 * dt;
|
||||
}
|
||||
} else {
|
||||
c.life -= dt;
|
||||
if (c.life <= 0) c.toRemove = true;
|
||||
}
|
||||
}
|
||||
this.coins = this.coins.filter(c => !c.toRemove);
|
||||
}
|
||||
|
||||
handleClick(mouseX, mouseY) {
|
||||
// 0. Workshop Floating Loot
|
||||
if (this.workshop) {
|
||||
for (let i = this.workshop.floatingLoot.length - 1; i >= 0; i--) {
|
||||
const loot = this.workshop.floatingLoot[i];
|
||||
if (!loot.collected && Math.hypot(mouseX - loot.x, mouseY - loot.y) <= 30) {
|
||||
this.workshop.collectLoot(loot);
|
||||
return { handled: true, action: 'loot_collected' };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Suns
|
||||
for (let i = this.suns.length - 1; i >= 0; i--) {
|
||||
const s = this.suns[i];
|
||||
if (!s.collecting && Math.hypot(mouseX - s.x, mouseY - s.y) <= s.radius + 14) {
|
||||
s.collecting = true;
|
||||
if (window.PvZAudio) window.PvZAudio.playSun();
|
||||
if (this.workshop) {
|
||||
this.sunCount = (this.sunCount || 0) + 1;
|
||||
if (this.sunCount >= 8) {
|
||||
this.sunCount = 0;
|
||||
this.workshop.addMaterial('sunEssence', 1);
|
||||
}
|
||||
}
|
||||
return { handled: true, action: 'sun_collected' };
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Coins
|
||||
for (let i = this.coins.length - 1; i >= 0; i--) {
|
||||
const c = this.coins[i];
|
||||
if (!c.collecting && Math.hypot(mouseX - c.x, mouseY - c.y) <= c.radius + 14) {
|
||||
c.collecting = true;
|
||||
if (window.PvZAudio) window.PvZAudio.playCoin();
|
||||
return { handled: true, action: 'coin_collected' };
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Seed Cards
|
||||
const cardStartX = 105;
|
||||
const cardY = 8;
|
||||
const cardW = 66;
|
||||
const cardH = 72;
|
||||
const spacing = 72;
|
||||
|
||||
for (let i = 0; i < this.cards.length; i++) {
|
||||
const card = this.cards[i];
|
||||
const cx = cardStartX + i * spacing;
|
||||
if (mouseX >= cx && mouseX <= cx + cardW && mouseY >= cardY && mouseY <= cardY + cardH) {
|
||||
if (this.canAfford(card.cost) && card.timer <= 0) {
|
||||
this.selectedCard = (this.selectedCard === card.id) ? null : card.id;
|
||||
}
|
||||
return { handled: true, action: 'card_selected' };
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Shovel
|
||||
const shovelX = cardStartX + this.cards.length * spacing + 12;
|
||||
if (mouseX >= shovelX && mouseX <= shovelX + 54 && mouseY >= cardY && mouseY <= cardY + cardH) {
|
||||
this.selectedCard = (this.selectedCard === 'shovel') ? null : 'shovel';
|
||||
return { handled: true, action: 'shovel_selected' };
|
||||
}
|
||||
|
||||
return { handled: false };
|
||||
}
|
||||
|
||||
startCooldown(cardId) {
|
||||
const card = this.cards.find(c => c.id === cardId);
|
||||
if (card) {
|
||||
card.timer = card.cooldown;
|
||||
}
|
||||
}
|
||||
|
||||
draw(ctx) {
|
||||
// HUD Bar Background
|
||||
ctx.fillStyle = this.isNight ? '#1a100c' : '#2c1810';
|
||||
ctx.fillRect(0, 0, 900, 90);
|
||||
ctx.fillStyle = this.isNight ? '#2a1810' : '#422817';
|
||||
ctx.fillRect(0, 0, 900, 86);
|
||||
|
||||
// 1. Sun Bank Box (or Conveyor indicator)
|
||||
if (!this.isConveyor) {
|
||||
ctx.fillStyle = '#e0c080';
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(10, 8, 85, 72, 8);
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = '#6d4c41';
|
||||
ctx.lineWidth = 3;
|
||||
ctx.stroke();
|
||||
|
||||
this.drawSunGraphic(ctx, 36, 34, 16, 0, 1);
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.font = 'bold 18px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(this.sun, 52, 66);
|
||||
} else {
|
||||
// Conveyor belt indicator
|
||||
ctx.fillStyle = '#616161';
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(10, 8, 85, 72, 8);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.font = 'bold 12px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('КОНВЕЙЕР', 52, 48);
|
||||
}
|
||||
|
||||
// 2. Draw Cards
|
||||
const cardStartX = 105;
|
||||
const cardY = 8;
|
||||
const cardW = 66;
|
||||
const cardH = 72;
|
||||
const spacing = 72;
|
||||
|
||||
for (let i = 0; i < this.cards.length; i++) {
|
||||
const card = this.cards[i];
|
||||
const cx = cardStartX + i * spacing;
|
||||
const isSelected = this.selectedCard === card.id;
|
||||
const affordable = this.canAfford(card.cost);
|
||||
const ready = card.timer <= 0;
|
||||
|
||||
ctx.fillStyle = isSelected ? '#fff9c4' : (affordable && ready ? '#fff8e1' : '#b0bec5');
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(cx, cardY, cardW, cardH, 6);
|
||||
ctx.fill();
|
||||
|
||||
ctx.strokeStyle = isSelected ? '#ffeb3b' : (affordable && ready ? '#8d6e63' : '#546e7a');
|
||||
ctx.lineWidth = isSelected ? 3 : 2;
|
||||
ctx.stroke();
|
||||
|
||||
this.drawCardThumbnail(ctx, card.id, cx + cardW / 2, cardY + 28);
|
||||
|
||||
if (this.workshop) {
|
||||
const lvl = this.workshop.getPlantLevel(card.id);
|
||||
if (lvl > 1) {
|
||||
ctx.fillStyle = lvl === 3 ? '#e65100' : '#f57f17';
|
||||
ctx.font = 'bold 9px sans-serif';
|
||||
ctx.textAlign = 'right';
|
||||
ctx.fillText(lvl === 3 ? '★★★' : '★★', cx + cardW - 4, cardY + 11);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.fillStyle = '#263238';
|
||||
ctx.font = 'bold 9px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(card.name, cx + cardW / 2, cardY + 54);
|
||||
|
||||
if (!this.isConveyor) {
|
||||
ctx.fillStyle = affordable ? '#1b5e20' : '#b71c1c';
|
||||
ctx.font = 'bold 11px sans-serif';
|
||||
ctx.fillText(card.cost, cx + cardW / 2, cardY + 68);
|
||||
} else {
|
||||
ctx.fillStyle = '#1b5e20';
|
||||
ctx.font = 'bold 10px sans-serif';
|
||||
ctx.fillText('БЕСПЛАТНО', cx + cardW / 2, cardY + 68);
|
||||
}
|
||||
|
||||
if (card.timer > 0) {
|
||||
const cdRatio = card.timer / card.cooldown;
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.55)';
|
||||
ctx.beginPath();
|
||||
ctx.rect(cx, cardY, cardW, cardH * cdRatio);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.font = 'bold 13px sans-serif';
|
||||
ctx.fillText(Math.ceil(card.timer), cx + cardW / 2, cardY + cardH / 2 + 5);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Shovel
|
||||
const shovelX = cardStartX + this.cards.length * spacing + 12;
|
||||
const isShovelSelected = this.selectedCard === 'shovel';
|
||||
ctx.fillStyle = isShovelSelected ? '#ffcdd2' : '#efebe9';
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(shovelX, cardY, 54, cardH, 6);
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = isShovelSelected ? '#d32f2f' : '#8d6e63';
|
||||
ctx.lineWidth = isShovelSelected ? 3 : 2;
|
||||
ctx.stroke();
|
||||
|
||||
this.drawShovelGraphic(ctx, shovelX + 27, cardY + 36);
|
||||
ctx.fillStyle = '#37474f';
|
||||
ctx.font = 'bold 9px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('Лопата', shovelX + 27, cardY + 66);
|
||||
|
||||
// 4. Coin Bag / Wallet in Top Right
|
||||
ctx.fillStyle = '#ffe082';
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(595, 20, 80, 26, 4);
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = '#ff8f00';
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.stroke();
|
||||
|
||||
// Coin icon
|
||||
ctx.fillStyle = '#ffd700';
|
||||
ctx.beginPath();
|
||||
ctx.arc(608, 33, 7, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = '#3e2723';
|
||||
ctx.font = 'bold 12px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(`💰 ${this.totalCoins}`, 645, 38);
|
||||
|
||||
// 5. Active Suns
|
||||
for (const s of this.suns) {
|
||||
const alpha = Math.min(1, s.life / 1.5);
|
||||
this.drawSunGraphic(ctx, s.x, s.y, s.radius, s.pulse, alpha);
|
||||
}
|
||||
|
||||
// 6. Active Coins
|
||||
for (const c of this.coins) {
|
||||
this.drawCoinGraphic(ctx, c.x, c.y, c.radius, c.type, c.pulse);
|
||||
}
|
||||
}
|
||||
|
||||
drawSunGraphic(ctx, x, y, radius, pulseAngle, alpha = 1) {
|
||||
ctx.save();
|
||||
ctx.globalAlpha = alpha;
|
||||
ctx.translate(x, y);
|
||||
|
||||
const numRays = 12;
|
||||
ctx.fillStyle = '#fbc02d';
|
||||
for (let i = 0; i < numRays; i++) {
|
||||
ctx.save();
|
||||
ctx.rotate(pulseAngle + (i * Math.PI * 2) / numRays);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-radius * 0.25, -radius * 0.8);
|
||||
ctx.lineTo(0, -radius * 1.35);
|
||||
ctx.lineTo(radius * 0.25, -radius * 0.8);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
const grad = ctx.createRadialGradient(0, 0, radius * 0.3, 0, 0, radius);
|
||||
grad.addColorStop(0, '#fffde7');
|
||||
grad.addColorStop(0.5, '#fff176');
|
||||
grad.addColorStop(1, '#fbc02d');
|
||||
ctx.fillStyle = grad;
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, radius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
drawCoinGraphic(ctx, x, y, radius, type, pulse) {
|
||||
ctx.save();
|
||||
ctx.translate(x, y);
|
||||
const spin = Math.sin(pulse * 3);
|
||||
ctx.scale(spin, 1);
|
||||
|
||||
if (type === 'gold') {
|
||||
ctx.fillStyle = '#ffd700';
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, radius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = '#b78103';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = '#b78103';
|
||||
ctx.font = 'bold 12px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('$', 0, 4);
|
||||
} else if (type === 'diamond') {
|
||||
ctx.fillStyle = '#80deea';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, -radius);
|
||||
ctx.lineTo(radius, 0);
|
||||
ctx.lineTo(0, radius);
|
||||
ctx.lineTo(-radius, 0);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
} else {
|
||||
// Silver
|
||||
ctx.fillStyle = '#e0e0e0';
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, radius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = '#757575';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
drawCardThumbnail(ctx, type, x, y) {
|
||||
ctx.save();
|
||||
ctx.translate(x, y);
|
||||
ctx.scale(0.52, 0.52);
|
||||
|
||||
if (type === 'sunflower') {
|
||||
ctx.fillStyle = '#fbc02d';
|
||||
for (let i = 0; i < 8; i++) {
|
||||
ctx.save();
|
||||
ctx.rotate((i * Math.PI * 2) / 8);
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, -18, 6, 12, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
ctx.fillStyle = '#5d4037';
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, 14, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
} else if (type === 'peashooter' || type === 'snowpea' || type === 'repeater') {
|
||||
const col = type === 'snowpea' ? '#29b6f6' : (type === 'repeater' ? '#2e7d32' : '#4caf50');
|
||||
ctx.fillStyle = col;
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, 16, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(14, 0, 8, 10, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
} else if (type === 'wallnut' || type === 'bowling_nut') {
|
||||
ctx.fillStyle = '#8d6e63';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, 0, 16, 22, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
} else if (type === 'cherrybomb' || type === 'bowling_cherry') {
|
||||
ctx.fillStyle = '#d32f2f';
|
||||
ctx.beginPath();
|
||||
ctx.arc(-9, 4, 12, 0, Math.PI * 2);
|
||||
ctx.arc(9, 4, 12, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
} else if (type === 'potatomine') {
|
||||
ctx.fillStyle = '#a1887f';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, 6, 16, 12, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#f44336';
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, -6, 4, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
} else if (type === 'chomper') {
|
||||
ctx.fillStyle = '#7b1fa2';
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, 18, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
} else if (type === 'jalapeno') {
|
||||
ctx.fillStyle = '#d50000';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, 0, 10, 20, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
} else if (type === 'sunshroom') {
|
||||
ctx.fillStyle = '#fbc02d';
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, 14, Math.PI, 0);
|
||||
ctx.fill();
|
||||
} else if (type === 'fumeshroom') {
|
||||
ctx.fillStyle = '#8e24aa';
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, 16, Math.PI, 0);
|
||||
ctx.fill();
|
||||
} else if (type === 'lilypad') {
|
||||
ctx.fillStyle = '#2e7d32';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, 0, 20, 10, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
} else if (type === 'gravebuster') {
|
||||
ctx.strokeStyle = '#33691e';
|
||||
ctx.lineWidth = 4;
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, 14, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
} else if (type === 'flowerpot') {
|
||||
ctx.fillStyle = '#bf360c';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-14, 4);
|
||||
ctx.lineTo(14, 4);
|
||||
ctx.lineTo(10, 16);
|
||||
ctx.lineTo(-10, 16);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#d84315';
|
||||
ctx.fillRect(-16, 0, 32, 5);
|
||||
} else if (type === 'cabbagepult') {
|
||||
ctx.fillStyle = '#8d6e63';
|
||||
ctx.fillRect(-12, 6, 24, 6);
|
||||
ctx.fillStyle = '#81c784';
|
||||
ctx.beginPath();
|
||||
ctx.arc(4, -4, 11, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
} else if (type === 'melonpult') {
|
||||
ctx.fillStyle = '#5d4037';
|
||||
ctx.fillRect(-14, 6, 28, 8);
|
||||
ctx.fillStyle = '#2e7d32';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(4, -4, 14, 11, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
} else if (type && type.startsWith('z_')) {
|
||||
ctx.fillStyle = '#809973';
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(-12, -14, 24, 26, [8, 8, 4, 4]);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#fff9c4';
|
||||
ctx.beginPath();
|
||||
ctx.arc(-5, -4, 4, 0, Math.PI * 2);
|
||||
ctx.arc(5, -4, 4, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.beginPath();
|
||||
ctx.arc(-5, -4, 1.5, 0, Math.PI * 2);
|
||||
ctx.arc(5, -4, 1.5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
if (type === 'z_conehead') {
|
||||
ctx.fillStyle = '#ff6d00';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-10, -12); ctx.lineTo(10, -12); ctx.lineTo(0, -30); ctx.closePath();
|
||||
ctx.fill();
|
||||
} else if (type === 'z_buckethead') {
|
||||
ctx.fillStyle = '#90a4ae';
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(-12, -26, 24, 16, 4);
|
||||
ctx.fill();
|
||||
} else if (type === 'z_football') {
|
||||
ctx.fillStyle = '#b71c1c';
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, -10, 15, Math.PI, 0);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
drawShovelGraphic(ctx, x, y) {
|
||||
ctx.save();
|
||||
ctx.translate(x, y);
|
||||
ctx.rotate(0.35);
|
||||
ctx.strokeStyle = '#8d6e63';
|
||||
ctx.lineWidth = 4;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, -16);
|
||||
ctx.lineTo(0, 8);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.fillStyle = '#90a4ae';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-9, 8);
|
||||
ctx.lineTo(9, 8);
|
||||
ctx.lineTo(6, 20);
|
||||
ctx.lineTo(0, 24);
|
||||
ctx.lineTo(-6, 20);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
};
|
||||
Reference in new issue
Block a user