import { System } from '../core/System.js'; import { SkillRegistry } from '../skills/SkillRegistry.js'; import { Events } from '../core/EventBus.js'; import { PixelFont } from '../core/PixelFont.js'; import { Palette } from '../core/Palette.js'; export class UISystem extends System { constructor(engine) { super('UISystem'); this.requiredComponents = []; // No required components - renders UI this.priority = 200; // Render after everything else this.engine = engine; this.ctx = engine.ctx; this.damageNumbers = []; this.notifications = []; // Subscribe to events engine.on(Events.DAMAGE_DEALT, (data) => this.addDamageNumber(data)); engine.on(Events.MUTATION_GAINED, (data) => this.addNotification(`Mutation Gained: ${data.name} `)); } addDamageNumber(data) { this.damageNumbers.push({ x: data.x, y: data.y, value: Math.floor(data.value), color: data.color || '#ffffff', lifetime: 1.0, vy: -50 }); } addNotification(text) { this.notifications.push({ text, lifetime: 3.0, alpha: 1.0 }); } process(deltaTime, _entities) { // Update damage numbers this.updateDamageNumbers(deltaTime); this.updateNotifications(deltaTime); const menuSystem = this.engine.systems.find(s => s.name === 'MenuSystem'); const gameState = menuSystem ? menuSystem.getGameState() : 'playing'; // Only draw menu overlay if in start, paused, or gameOver state if (gameState === 'start' || gameState === 'paused' || gameState === 'gameOver') { if (menuSystem) { menuSystem.drawMenu(); } // Don't draw game UI when menu is showing return; } const playerController = this.engine.systems.find(s => s.name === 'PlayerControllerSystem'); const player = playerController ? playerController.getPlayerEntity() : null; if (!player) return; // Draw UI this.drawHUD(player); this.drawSkills(player); // REMOVED drawStats and drawSkillProgress from active gameplay this.drawDamageNumbers(); this.drawNotifications(); this.drawAbsorptionEffects(); } drawHUD(player) { const health = player.getComponent('Health'); const stats = player.getComponent('Stats'); const evolution = player.getComponent('Evolution'); if (!health || !stats || !evolution) return; const ctx = this.ctx; // Health bar const barWidth = 64; const barHeight = 6; const barX = 4; const barY = 4; // Outer border ctx.fillStyle = Palette.DARK_BLUE; ctx.fillRect(barX - 1, barY - 1, barWidth + 2, barHeight + 2); // Background ctx.fillStyle = Palette.VOID; ctx.fillRect(barX, barY, barWidth, barHeight); const healthPercent = health.hp / health.maxHp; ctx.fillStyle = healthPercent > 0.5 ? Palette.CYAN : healthPercent > 0.25 ? Palette.SKY_BLUE : Palette.WHITE; ctx.fillRect(barX, barY, Math.floor(barWidth * healthPercent), barHeight); // HP Text PixelFont.drawText(ctx, `${Math.ceil(health.hp)}/${health.maxHp}`, barX, barY + barHeight + 3, Palette.WHITE, 1); // Evolution display const form = evolution.getDominantForm(); const formY = barY + barHeight + 14; PixelFont.drawText(ctx, form.toUpperCase(), barX, formY, Palette.SKY_BLUE, 1); } drawSkills(player) { const skills = player.getComponent('Skills'); if (!skills) return; const ctx = this.ctx; const width = this.engine.canvas.width; const startX = width - 80; const startY = 4; PixelFont.drawText(ctx, 'SKILLS', startX, startY, Palette.WHITE, 1); skills.activeSkills.forEach((skillId, index) => { const y = startY + 10 + index * 9; const onCooldown = skills.isOnCooldown(skillId); const cooldown = skills.getCooldown(skillId); const skill = SkillRegistry.get(skillId); let skillName = skill ? skill.name : skillId.replace('_', ' '); if (skillName.length > 10) skillName = skillName.substring(0, 10); const color = onCooldown ? Palette.ROYAL_BLUE : Palette.CYAN; const text = `${index + 1} ${skillName}${onCooldown ? ` ${cooldown.toFixed(0)}` : ''}`; PixelFont.drawText(ctx, text, startX, y, color, 1); }); } drawStats(player, x, y) { const stats = player.getComponent('Stats'); const evolution = player.getComponent('Evolution'); if (!stats || !evolution) return; const ctx = this.ctx; PixelFont.drawText(ctx, 'STATISTICS', x, y, Palette.WHITE, 1); PixelFont.drawText(ctx, `STR ${stats.strength}`, x, y + 10, Palette.ROYAL_BLUE, 1); PixelFont.drawText(ctx, `AGI ${stats.agility}`, x, y + 20, Palette.ROYAL_BLUE, 1); PixelFont.drawText(ctx, `INT ${stats.intelligence}`, x, y + 30, Palette.ROYAL_BLUE, 1); PixelFont.drawText(ctx, `CON ${stats.constitution}`, x, y + 40, Palette.ROYAL_BLUE, 1); PixelFont.drawText(ctx, 'EVOLUTION', x, y + 60, Palette.WHITE, 1); PixelFont.drawText(ctx, `HUMAN: ${Math.floor(evolution.human)}`, x, y + 70, Palette.ROYAL_BLUE, 1); PixelFont.drawText(ctx, `BEAST: ${Math.floor(evolution.beast)}`, x, y + 80, Palette.ROYAL_BLUE, 1); PixelFont.drawText(ctx, `SLIME: ${Math.floor(evolution.slime)}`, x, y + 90, Palette.ROYAL_BLUE, 1); } drawSkillProgress(player, x, y) { const skillProgress = player.getComponent('SkillProgress'); if (!skillProgress) return; const ctx = this.ctx; const progress = skillProgress.getAllProgress(); PixelFont.drawText(ctx, 'KNOWLEDGE', x, y, Palette.CYAN, 1); if (progress.size === 0) { PixelFont.drawText(ctx, 'NONE YET', x, y + 10, Palette.DARK_BLUE, 1); return; } let idx = 0; progress.forEach((count, skillId) => { const required = skillProgress.requiredAbsorptions; const skill = SkillRegistry.get(skillId); let name = skill ? skill.name : skillId; if (name.length > 10) name = name.substring(0, 10); const py = y + 10 + idx * 9; PixelFont.drawText(ctx, `${name}: ${count}/${required}`, x, py, Palette.SKY_BLUE, 1); idx++; }); } updateDamageNumbers(deltaTime) { for (let i = this.damageNumbers.length - 1; i >= 0; i--) { const num = this.damageNumbers[i]; num.lifetime -= deltaTime; num.y += num.vy * deltaTime; num.vy *= 0.95; if (num.lifetime <= 0) this.damageNumbers.splice(i, 1); } } updateNotifications(deltaTime) { for (let i = this.notifications.length - 1; i >= 0; i--) { const note = this.notifications[i]; note.lifetime -= deltaTime; if (note.lifetime < 0.5) note.alpha = note.lifetime * 2; if (note.lifetime <= 0) this.notifications.splice(i, 1); } } drawDamageNumbers() { const ctx = this.ctx; this.damageNumbers.forEach(num => { const color = num.color.startsWith('rgba') ? num.color : Palette.WHITE; PixelFont.drawText(ctx, num.value.toString(), Math.floor(num.x), Math.floor(num.y), color, 1); }); } hexToRgb(hex) { const r = parseInt(hex.slice(1, 3), 16); const g = parseInt(hex.slice(3, 5), 16); const b = parseInt(hex.slice(5, 7), 16); return `${r}, ${g}, ${b}`; } drawNotifications() { const ctx = this.ctx; const width = this.engine.canvas.width; this.notifications.forEach((note, index) => { const textWidth = PixelFont.getTextWidth(note.text, 1); const x = Math.floor((width - textWidth) / 2); const y = 40 + index * 10; PixelFont.drawText(ctx, note.text, x, y, Palette.WHITE, 1); }); } drawAbsorptionEffects() { const absorptionSystem = this.engine.systems.find(s => s.name === 'AbsorptionSystem'); if (!absorptionSystem || !absorptionSystem.getEffects) return; const effects = absorptionSystem.getEffects(); const ctx = this.ctx; effects.forEach(effect => { const alpha = Math.min(1, effect.lifetime * 2); // Glow effect ctx.shadowBlur = 10; ctx.shadowColor = effect.color; ctx.fillStyle = effect.color; ctx.globalAlpha = alpha; ctx.beginPath(); ctx.arc(effect.x, effect.y, effect.size, 0, Math.PI * 2); ctx.fill(); // Inner bright core ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'; ctx.beginPath(); ctx.arc(effect.x, effect.y, effect.size * 0.5, 0, Math.PI * 2); ctx.fill(); ctx.globalAlpha = 1.0; ctx.shadowBlur = 0; }); } }