feat(polish & stats): add wallnut degradation states, hit flash, decapitation particle, frost crystals, dave dialogues and lifetime player stats
This commit is contained in:
1 parent
23918dab1f
commit
643b0287c1
10 files changed
+1260
-65
No files matched your search
@@ -88,6 +88,13 @@ window.PvZAuth = (function () {
|
||||
maxUnlockedLevel: 1,
|
||||
highScore: 0,
|
||||
theme: 'classic',
|
||||
stats: {
|
||||
zombiesKilled: 0,
|
||||
sunProduced: 0,
|
||||
plantsPlanted: 0,
|
||||
levelsCompleted: 0,
|
||||
coinsEarned: 0
|
||||
},
|
||||
upgrades: { extraSlot: false, hasRake: false },
|
||||
unlockedPlants: ['sunflower', 'peashooter', 'wallnut'],
|
||||
selectedDeck: ['sunflower', 'peashooter', 'wallnut'],
|
||||
@@ -180,6 +187,10 @@ window.PvZAuth = (function () {
|
||||
return {
|
||||
...def,
|
||||
...serverData,
|
||||
stats: {
|
||||
...def.stats,
|
||||
...(serverData.stats || {})
|
||||
},
|
||||
upgrades: { ...def.upgrades, ...(serverData.upgrades || {}) },
|
||||
unlockedPlants: serverData.unlockedPlants || def.unlockedPlants,
|
||||
selectedDeck: serverData.selectedDeck || def.selectedDeck,
|
||||
|
||||
@@ -8,6 +8,34 @@ window.CampaignSystem = (function() {
|
||||
{ id: 5, name: 'Крыша Дома', icon: '🏠', biome: 'roof', range: '5-1 .. 5-5' }
|
||||
];
|
||||
|
||||
const DAVE_DIALOGUES = {
|
||||
'2-1': {
|
||||
title: '🌙 Безумный Дэйв предупреждает:',
|
||||
text: 'Эй, сосед! Ночью солнце с неба не падает! Обязательно возьми Солнцегриб!',
|
||||
btnText: 'ДАЛЕЕ / В БОЙ ⚔️'
|
||||
},
|
||||
'3-1': {
|
||||
title: '🏊 Безумный Дэйв спешит на помощь:',
|
||||
text: 'Там бассейн! На воде растения тонут — сначала сажай Кувшинки (Lily Pad)!',
|
||||
btnText: 'ДАЛЕЕ / В БОЙ ⚔️'
|
||||
},
|
||||
'4-1': {
|
||||
title: '🌫️ Безумный Дэйв щурится во мгле:',
|
||||
text: 'Ого, какой густой туман! Зомби крадутся в темноте, используй Плантерн и Вздуватель!',
|
||||
btnText: 'ДАЛЕЕ / В БОЙ ⚔️'
|
||||
},
|
||||
'5-1': {
|
||||
title: '🏠 Безумный Дэйв лезет на скат:',
|
||||
text: 'Мы на крыше! Скаты мешают прямой стрельбе — используй Горшки и Катапульты!',
|
||||
btnText: 'ДАЛЕЕ / В БОЙ ⚔️'
|
||||
},
|
||||
'5-5': {
|
||||
title: '🤖 БЕЗУМИЕ ПОЛНОГО МАСШТАБА!',
|
||||
text: 'БЕЗУМИЕ! Доктор Зомбосс выкатил своего гигантского робота! Держи оборону!',
|
||||
btnText: 'ДАЛЕЕ / В БОЙ ⚔️'
|
||||
}
|
||||
};
|
||||
|
||||
const LEVELS = [
|
||||
// World 1: Day (1-1 .. 1-5)
|
||||
{ level: 1, world: 1, stage: 1, code: '1-1', title: 'Первые шаги', desc: 'Знакомство с садом и простыми зомби', waves: 3, biome: 'day', reward: 100 },
|
||||
@@ -48,6 +76,18 @@ window.CampaignSystem = (function() {
|
||||
return {
|
||||
WORLDS,
|
||||
LEVELS,
|
||||
DAVE_DIALOGUES,
|
||||
getDaveDialogue(levelNum, mode = 'adventure') {
|
||||
if (mode === 'boss' || mode === 'bossrush') {
|
||||
return DAVE_DIALOGUES['5-5'];
|
||||
}
|
||||
const num = parseInt(levelNum, 10) || 1;
|
||||
const levelObj = LEVELS.find(l => l.level === num);
|
||||
if (levelObj && DAVE_DIALOGUES[levelObj.code]) {
|
||||
return DAVE_DIALOGUES[levelObj.code];
|
||||
}
|
||||
return null;
|
||||
},
|
||||
getLevel(levelNum) {
|
||||
const num = parseInt(levelNum, 10) || 1;
|
||||
return LEVELS.find(l => l.level === num) || LEVELS[0];
|
||||
|
||||
@@ -47,11 +47,175 @@ window.ParticlePool = class ParticlePool {
|
||||
}
|
||||
};
|
||||
|
||||
// Zombie Decapitation Flying Head Particle
|
||||
window.ZombieHeadParticle = class ZombieHeadParticle {
|
||||
constructor(x, y, options = {}) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.vx = (Math.random() * 50 + 40) * (Math.random() < 0.5 ? 1 : -0.7);
|
||||
this.vy = -(Math.random() * 60 + 130);
|
||||
this.gravity = 400;
|
||||
this.rotation = 0;
|
||||
this.rotSpeed = (Math.random() * 4 + 4) * (this.vx >= 0 ? 1 : -1);
|
||||
this.groundY = y + 62;
|
||||
this.bounces = 2;
|
||||
this.life = 2.2;
|
||||
this.maxLife = 2.2;
|
||||
this.type = options.type || 'normal';
|
||||
this.isAngry = !!options.isAngry;
|
||||
this.isSlow = !!options.isSlow;
|
||||
this.cheats = options.cheats || null;
|
||||
}
|
||||
|
||||
update(dt) {
|
||||
this.life -= dt;
|
||||
if (this.life <= 0) return;
|
||||
|
||||
this.vy += this.gravity * dt;
|
||||
this.x += this.vx * dt;
|
||||
this.y += this.vy * dt;
|
||||
this.rotation += this.rotSpeed * dt;
|
||||
|
||||
if (this.y >= this.groundY) {
|
||||
this.y = this.groundY;
|
||||
if (this.bounces > 0) {
|
||||
this.bounces--;
|
||||
this.vy = -this.vy * 0.42;
|
||||
this.vx *= 0.6;
|
||||
this.rotSpeed *= 0.5;
|
||||
} else {
|
||||
this.vy = 0;
|
||||
this.vx = 0;
|
||||
this.rotSpeed = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
draw(ctx) {
|
||||
if (this.life <= 0) return;
|
||||
ctx.save();
|
||||
|
||||
const alpha = this.life < 0.6 ? Math.max(0, this.life / 0.6) : 1;
|
||||
|
||||
// Ground shadow
|
||||
const distToGround = Math.max(0, this.groundY - this.y);
|
||||
if (distToGround < 65) {
|
||||
ctx.save();
|
||||
ctx.translate(this.x, this.groundY + 12);
|
||||
ctx.fillStyle = `rgba(0,0,0,${0.25 * (1 - distToGround / 65) * alpha})`;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, 0, 14, 5, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
ctx.translate(this.x, this.y);
|
||||
ctx.rotate(this.rotation);
|
||||
ctx.globalAlpha = alpha;
|
||||
|
||||
let skinColor = this.isAngry ? '#b71c1c' : '#809973';
|
||||
if (this.isSlow) skinColor = '#81d4fa';
|
||||
|
||||
// Head base
|
||||
ctx.fillStyle = skinColor;
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(-14, -16, 28, 30, [10, 10, 6, 6]);
|
||||
ctx.fill();
|
||||
|
||||
// Eyes
|
||||
ctx.fillStyle = this.isAngry ? '#ff5252' : '#fff9c4';
|
||||
ctx.beginPath();
|
||||
ctx.arc(-8, -4, 4.5, 0, Math.PI * 2);
|
||||
ctx.arc(3, -4, 5.5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.beginPath();
|
||||
ctx.arc(-9, -4, 2, 0, Math.PI * 2);
|
||||
ctx.arc(2, -4, 2, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Open mouth / teeth
|
||||
ctx.fillStyle = '#212121';
|
||||
ctx.fillRect(-10, 6, 18, 5);
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.fillRect(-9, 6, 3, 3);
|
||||
ctx.fillRect(-4, 6, 3, 3);
|
||||
ctx.fillRect(1, 8, 3, 3);
|
||||
|
||||
// Cheats
|
||||
if (this.cheats) {
|
||||
if (this.cheats.future) {
|
||||
ctx.fillStyle = '#00e5ff';
|
||||
ctx.fillRect(-12, -7, 22, 6);
|
||||
}
|
||||
if (this.cheats.mustache) {
|
||||
ctx.fillStyle = '#3e2723';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-11, 4);
|
||||
ctx.quadraticCurveTo(-4, 0, 0, 3);
|
||||
ctx.quadraticCurveTo(4, 0, 9, 4);
|
||||
ctx.quadraticCurveTo(0, 8, -11, 4);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
// Hat / Helmet
|
||||
if (this.type === 'conehead') {
|
||||
ctx.fillStyle = '#ff6d00';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-14, -14);
|
||||
ctx.lineTo(14, -14);
|
||||
ctx.lineTo(3, -48);
|
||||
ctx.lineTo(-3, -48);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-8, -26);
|
||||
ctx.lineTo(8, -26);
|
||||
ctx.lineTo(6, -34);
|
||||
ctx.lineTo(-6, -34);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
} else if (this.type === 'buckethead') {
|
||||
ctx.fillStyle = '#90a4ae';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-16, -14);
|
||||
ctx.lineTo(16, -14);
|
||||
ctx.lineTo(13, -44);
|
||||
ctx.lineTo(-13, -44);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = '#546e7a';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
} else if (this.type === 'football') {
|
||||
ctx.fillStyle = '#b71c1c';
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, -10, 18, Math.PI, 0);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillRect(-3, -28, 6, 18);
|
||||
ctx.strokeStyle = '#ffffff';
|
||||
ctx.lineWidth = 2.5;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-12, -4);
|
||||
ctx.lineTo(-2, 4);
|
||||
ctx.lineTo(8, -4);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
};
|
||||
|
||||
// Combat & Collision Resolution System (with Bowling Ricochet & Coin Drops)
|
||||
window.CombatSystem = class CombatSystem {
|
||||
constructor() {
|
||||
this.particlePool = new window.ParticlePool(150);
|
||||
this.particles = [];
|
||||
this.heads = [];
|
||||
}
|
||||
|
||||
reset() {
|
||||
@@ -59,6 +223,12 @@ window.CombatSystem = class CombatSystem {
|
||||
this.particlePool.releaseAll(this.particles);
|
||||
}
|
||||
this.particles = [];
|
||||
this.heads = [];
|
||||
}
|
||||
|
||||
spawnDecapitatedHead(x, y, type = 'normal', isAngry = false, isSlow = false, cheats = null) {
|
||||
this.heads.push(new window.ZombieHeadParticle(x, y, { type, isAngry, isSlow, cheats }));
|
||||
this.createSplash(x, y + 10, '#689f38', 5);
|
||||
}
|
||||
|
||||
createSplash(x, y, color = '#8bc34a', count = 7) {
|
||||
@@ -110,6 +280,17 @@ window.CombatSystem = class CombatSystem {
|
||||
}
|
||||
this.particles.length = writeIdx;
|
||||
|
||||
// 1.5 Update Decapitated Heads
|
||||
let headWriteIdx = 0;
|
||||
for (let i = 0; i < this.heads.length; i++) {
|
||||
const h = this.heads[i];
|
||||
h.update(dt);
|
||||
if (h.life > 0) {
|
||||
this.heads[headWriteIdx++] = h;
|
||||
}
|
||||
}
|
||||
this.heads.length = headWriteIdx;
|
||||
|
||||
// 2. Projectiles vs Zombies
|
||||
for (const proj of gameState.projectiles) {
|
||||
if (proj.toRemove) continue;
|
||||
@@ -279,9 +460,24 @@ window.CombatSystem = class CombatSystem {
|
||||
}
|
||||
|
||||
handleZombieDeath(zombie, gameState) {
|
||||
if (!zombie.headSpawned) {
|
||||
zombie.headSpawned = true;
|
||||
this.spawnDecapitatedHead(
|
||||
zombie.x,
|
||||
zombie.y - 24,
|
||||
zombie.type,
|
||||
zombie.isAngry,
|
||||
zombie.slowTimer > 0 || (zombie.freezeTimer && zombie.freezeTimer > 0) || zombie.isSlowed,
|
||||
gameState && gameState.tree ? gameState.tree.cheats : null
|
||||
);
|
||||
}
|
||||
|
||||
const pts = zombie.type === 'buckethead' || zombie.type === 'football' ? 300 : (zombie.type === 'conehead' ? 200 : 100);
|
||||
gameState.addScore(pts);
|
||||
gameState.zombiesKilled++;
|
||||
if (typeof gameState.recordStat === 'function') {
|
||||
gameState.recordStat('zombiesKilled', 1);
|
||||
}
|
||||
|
||||
// Achievement: zombie_slayer
|
||||
if (gameState.achievements) {
|
||||
@@ -363,5 +559,8 @@ window.CombatSystem = class CombatSystem {
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
for (const h of this.heads) {
|
||||
h.draw(ctx);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user