feat(polish & stats): add wallnut degradation states, hit flash, decapitation particle, frost crystals, dave dialogues and lifetime player stats

This commit is contained in:
2026-09-17 13:28:33 +00:00
parent 23918dab1f
commit 643b0287c1
10 changed files with 1260 additions and 65 deletions
+183
View File
@@ -48,6 +48,9 @@ window.EconomySystem = class EconomySystem {
}));
this.selectedCard = null;
this.hoveredCard = null;
this.hoveredCardIndex = -1;
this.hoveredCardPos = null;
this.animTime = 0;
this.setDeckForLevel(1, 'day');
}
@@ -276,6 +279,9 @@ window.EconomySystem = class EconomySystem {
this.coins = [];
this.skySunTimer = 0;
this.selectedCard = null;
this.hoveredCard = null;
this.hoveredCardIndex = -1;
this.hoveredCardPos = null;
this.cards.forEach(c => c.timer = 0);
}
@@ -301,6 +307,9 @@ window.EconomySystem = class EconomySystem {
addCoins(amount) {
this.totalCoins += amount;
if (amount > 0 && this.engine && typeof this.engine.recordStat === 'function') {
this.engine.recordStat('coinsEarned', amount);
}
this.saveProfileData();
}
@@ -493,6 +502,175 @@ window.EconomySystem = class EconomySystem {
return { handled: false };
}
handleMouseMove(mouseX, mouseY) {
this.hoveredCard = null;
this.hoveredCardIndex = -1;
this.hoveredCardPos = null;
if (mouseX < 0 || mouseY < 0) return;
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) {
this.hoveredCard = card;
this.hoveredCardIndex = i;
this.hoveredCardPos = { x: cx, y: cardY, w: cardW, h: cardH };
return;
}
}
// Shovel hover
const shovelX = cardStartX + this.cards.length * spacing + 12;
if (mouseX >= shovelX && mouseX <= shovelX + 54 && mouseY >= cardY && mouseY <= cardY + cardH) {
this.hoveredCard = { id: 'shovel', name: 'Лопата', cost: 0, cooldown: 0 };
this.hoveredCardIndex = -2;
this.hoveredCardPos = { x: shovelX, y: cardY, w: 54, h: cardH };
}
}
getCardInfo(cardId) {
const p = window.PLANT_CATALOGUE ? window.PLANT_CATALOGUE.find(item => item.id === cardId) : null;
if (p) {
return {
name: p.name,
icon: p.icon || '🌱',
cost: p.cost,
cooldown: p.cooldown,
desc: p.desc || 'Защитник газона.'
};
}
const specialMap = {
shovel: { name: 'Лопата', icon: '⛏️', cost: 0, cooldown: 0, desc: 'Выкапывает любое растение с газона, освобождая клетку для новой посадки.' },
bowling_nut: { name: 'Орех-шар', icon: '🎳', cost: 0, cooldown: 4.0, desc: 'Катится по дорожке на огромной скорости и сбивает всех зомби на своём пути.' },
bowling_cherry: { name: 'Взрывной орех', icon: '💥', cost: 0, cooldown: 12.0, desc: 'Взрывается при первом контакте с зомби в радиусе 3х3 клетки.' },
z_normal: { name: 'Обычный зомби', icon: '🧟', cost: 50, cooldown: 2.0, desc: 'Базовый зомби-пехотинец. Идёт вперёд и пожирает растения.' },
z_conehead: { name: 'Зомби с конусом', icon: '⚠️', cost: 75, cooldown: 3.0, desc: 'Дорожный конус удваивает запас здоровья зомби.' },
z_polevaulter: { name: 'Зомби с шестом', icon: '🏃', cost: 75, cooldown: 4.0, desc: 'Быстро бежит и перепрыгивает первое встреченное растение.' },
z_buckethead: { name: 'Зомби с ведром', icon: '🪣', cost: 125, cooldown: 5.0, desc: 'Прочное железное ведро выдерживает массивный урон.' },
z_football: { name: 'Зомби-футболист', icon: '🏈', cost: 175, cooldown: 6.0, desc: 'Сверхбыстрый и тяжело бронированный штурмовик.' }
};
return specialMap[cardId] || {
name: cardId,
icon: '🌱',
cost: 0,
cooldown: 5.0,
desc: 'Защитник сада.'
};
}
getRechargeSpeedText(cd) {
if (cd <= 0) return 'Мгновенно';
if (cd <= 7.5) return `Быстрая (${cd}с)`;
if (cd <= 15) return `Средняя (${cd}с)`;
if (cd <= 25) return `Медленная (${cd}с)`;
return `Очень медленная (${cd}с)`;
}
wrapTooltipText(ctx, text, maxWidth) {
if (!text) return [];
const words = text.split(' ');
const lines = [];
let currentLine = '';
for (const word of words) {
const testLine = currentLine ? `${currentLine} ${word}` : word;
const width = ctx.measureText(testLine).width;
if (width > maxWidth && currentLine) {
lines.push(currentLine);
currentLine = word;
} else {
currentLine = testLine;
}
}
if (currentLine) lines.push(currentLine);
return lines;
}
drawCardTooltip(ctx, card) {
if (!card || !this.hoveredCardPos) return;
const info = this.getCardInfo(card.id);
const boxW = 240;
ctx.save();
ctx.font = '11px sans-serif';
const descLines = this.wrapTooltipText(ctx, info.desc, boxW - 24);
const boxH = 58 + descLines.length * 15;
let boxX = this.hoveredCardPos.x - 10;
boxX = Math.max(10, Math.min(boxX, 900 - boxW - 10));
const boxY = 88;
// Stylish dark-glass shadow & container
ctx.shadowColor = 'rgba(0, 0, 0, 0.7)';
ctx.shadowBlur = 14;
ctx.shadowOffsetY = 4;
const bgGrad = ctx.createLinearGradient(boxX, boxY, boxX, boxY + boxH);
bgGrad.addColorStop(0, 'rgba(15, 23, 42, 0.96)');
bgGrad.addColorStop(1, 'rgba(30, 41, 59, 0.96)');
ctx.fillStyle = bgGrad;
ctx.beginPath();
ctx.roundRect(boxX, boxY, boxW, boxH, 8);
ctx.fill();
// Border glow
ctx.shadowColor = 'transparent';
ctx.strokeStyle = 'rgba(105, 240, 174, 0.75)';
ctx.lineWidth = 1.5;
ctx.stroke();
// Top subtle neon accent line
const accentGrad = ctx.createLinearGradient(boxX + 10, boxY, boxX + boxW - 10, boxY);
accentGrad.addColorStop(0, '#69f0ae');
accentGrad.addColorStop(0.5, '#ffd54f');
accentGrad.addColorStop(1, '#81d4fa');
ctx.fillStyle = accentGrad;
ctx.fillRect(boxX + 8, boxY + 1.5, boxW - 16, 2);
// Title & Icon
ctx.fillStyle = '#ffeb3b';
ctx.font = 'bold 13px sans-serif';
ctx.textAlign = 'left';
let titleStr = `${info.icon} ${info.name}`;
if (this.workshop && card.id !== 'shovel') {
const lvl = this.workshop.getPlantLevel(card.id);
if (lvl > 1) {
titleStr += ` (${lvl === 3 ? '★★★' : '★★'})`;
}
}
ctx.fillText(titleStr, boxX + 12, boxY + 20);
// Subtitle (Cost + Recharge)
ctx.fillStyle = '#81d4fa';
ctx.font = 'bold 10.5px sans-serif';
const costText = this.isConveyor ? 'Бесплатно' : `☀️ ${info.cost}`;
const cdText = this.getRechargeSpeedText(info.cooldown);
ctx.fillText(`Стоимость: ${costText} ⏱️ ${cdText}`, boxX + 12, boxY + 36);
// Divider
ctx.strokeStyle = 'rgba(255, 255, 255, 0.15)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(boxX + 10, boxY + 44);
ctx.lineTo(boxX + boxW - 10, boxY + 44);
ctx.stroke();
// Description lines
ctx.fillStyle = '#cfd8dc';
ctx.font = '11px sans-serif';
for (let i = 0; i < descLines.length; i++) {
ctx.fillText(descLines[i], boxX + 12, boxY + 58 + i * 15);
}
ctx.restore();
}
startCooldown(cardId) {
const card = this.cards.find(c => c.id === cardId);
if (card) {
@@ -666,6 +844,11 @@ window.EconomySystem = class EconomySystem {
for (const c of this.coins) {
this.drawCoinGraphic(ctx, c.x, c.y, c.radius, c.type, c.pulse);
}
// 7. Dynamic Hover Tooltip on Deck Card
if (this.hoveredCard) {
this.drawCardTooltip(ctx, this.hoveredCard);
}
}
drawSunGraphic(ctx, x, y, radius, pulseAngle, alpha = 1) {