258 lines
8.2 KiB
JavaScript
258 lines
8.2 KiB
JavaScript
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;
|
|
});
|
|
}
|
|
}
|
|
|