feat: add poc

This commit is contained in:
Juan Sebastián Montoya 2026-01-06 14:02:09 -05:00
parent 43d27b04d9
commit 4a4fa05ce4
53 changed files with 6191 additions and 0 deletions

311
src/systems/UISystem.js Normal file
View file

@ -0,0 +1,311 @@
import { System } from '../core/System.js';
import { SkillRegistry } from '../skills/SkillRegistry.js';
import { Events } from '../core/EventBus.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 or paused state
if (gameState === 'start' || gameState === 'paused') {
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);
this.drawStats(player);
this.drawSkillProgress(player);
this.drawDamageNumbers();
this.drawNotifications();
this.drawAbsorptionEffects();
}
drawHUD(player) {
const health = player.getComponent('Health');
const stats = player.getComponent('Stats');
const evolution = player.getComponent('Evolution');
const skills = player.getComponent('Skills');
if (!health || !stats || !evolution) return;
const ctx = this.ctx;
const _width = this.engine.canvas.width;
const _height = this.engine.canvas.height;
// Health bar
const barWidth = 200;
const barHeight = 20;
const barX = 20;
const barY = 20;
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
ctx.fillRect(barX, barY, barWidth, barHeight);
const healthPercent = health.hp / health.maxHp;
ctx.fillStyle = healthPercent > 0.5 ? '#00ff00' : healthPercent > 0.25 ? '#ffff00' : '#ff0000';
ctx.fillRect(barX, barY, barWidth * healthPercent, barHeight);
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 2;
ctx.strokeRect(barX, barY, barWidth, barHeight);
ctx.fillStyle = '#ffffff';
ctx.font = '14px Courier New';
ctx.fillText(`HP: ${Math.ceil(health.hp)}/${health.maxHp}`, barX + 5, barY + 15);
// Evolution display
const form = evolution.getDominantForm();
const formY = barY + barHeight + 10;
ctx.fillStyle = '#ffffff';
ctx.font = '12px Courier New';
ctx.fillText(`Form: ${form.toUpperCase()}`, barX, formY);
ctx.fillText(`Human: ${evolution.human.toFixed(1)} | Beast: ${evolution.beast.toFixed(1)} | Slime: ${evolution.slime.toFixed(1)}`, barX, formY + 15);
// Instructions
const instructionsY = formY + 40;
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
ctx.font = '11px Courier New';
ctx.fillText('WASD: Move | Mouse: Aim | Click/Space: Attack', barX, instructionsY);
ctx.fillText('Shift: Stealth | 1-9: Skills (Press 1 for Slime Gun)', barX, instructionsY + 15);
// Show skill hint if player has skills
if (skills && skills.activeSkills.length > 0) {
ctx.fillStyle = '#00ff96';
ctx.fillText(`You have ${skills.activeSkills.length} skill(s)! Press 1-${skills.activeSkills.length} to use them.`, barX, instructionsY + 30);
} else {
ctx.fillStyle = '#ffaa00';
ctx.fillText('Defeat and absorb creatures 5 times to learn their skills!', barX, instructionsY + 30);
}
// Health regeneration hint
ctx.fillStyle = '#00aaff';
ctx.fillText('Health regenerates when not in combat', barX, instructionsY + 45);
}
drawSkills(player) {
const skills = player.getComponent('Skills');
if (!skills) return;
const ctx = this.ctx;
const width = this.engine.canvas.width;
const startX = width - 250;
const startY = 20;
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
ctx.fillRect(startX, startY, 230, 30 + skills.activeSkills.length * 30);
ctx.fillStyle = '#ffffff';
ctx.font = '14px Courier New';
ctx.fillText('Skills:', startX + 10, startY + 20);
skills.activeSkills.forEach((skillId, index) => {
const y = startY + 40 + index * 30;
const key = (index + 1).toString();
const onCooldown = skills.isOnCooldown(skillId);
const cooldown = skills.getCooldown(skillId);
// Get skill name from registry for display
const skill = SkillRegistry.get(skillId);
const skillName = skill ? skill.name : skillId.replace('_', ' ');
ctx.fillStyle = onCooldown ? '#888888' : '#00ff96';
ctx.fillText(`${key}. ${skillName}${onCooldown ? ` (${cooldown.toFixed(1)}s)` : ''}`, startX + 10, y);
});
}
drawStats(player) {
const stats = player.getComponent('Stats');
if (!stats) return;
const ctx = this.ctx;
const width = this.engine.canvas.width;
const startX = width - 250;
const startY = 200;
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
ctx.fillRect(startX, startY, 230, 150);
ctx.fillStyle = '#ffffff';
ctx.font = '12px Courier New';
let y = startY + 20;
ctx.fillText('Stats:', startX + 10, y);
y += 20;
ctx.fillText(`STR: ${stats.strength}`, startX + 10, y);
y += 15;
ctx.fillText(`AGI: ${stats.agility}`, startX + 10, y);
y += 15;
ctx.fillText(`INT: ${stats.intelligence}`, startX + 10, y);
y += 15;
ctx.fillText(`CON: ${stats.constitution}`, startX + 10, y);
y += 15;
ctx.fillText(`PER: ${stats.perception}`, startX + 10, y);
}
drawSkillProgress(player) {
const skillProgress = player.getComponent('SkillProgress');
if (!skillProgress) return;
const ctx = this.ctx;
const width = this.engine.canvas.width;
const startX = width - 250;
const startY = 360;
const progress = skillProgress.getAllProgress();
if (progress.size === 0) return;
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
ctx.fillRect(startX, startY, 230, 30 + progress.size * 25);
ctx.fillStyle = '#ffffff';
ctx.font = '12px Courier New';
ctx.fillText('Skill Progress:', startX + 10, startY + 20);
let y = startY + 35;
progress.forEach((count, skillId) => {
const required = skillProgress.requiredAbsorptions;
const _percent = Math.min(100, (count / required) * 100);
const skill = SkillRegistry.get(skillId);
const skillName = skill ? skill.name : skillId.replace('_', ' ');
ctx.fillStyle = count >= required ? '#00ff00' : '#ffff00';
ctx.fillText(`${skillName}: ${count}/${required}`, startX + 10, y);
y += 20;
});
}
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 alpha = Math.min(1, num.lifetime);
const size = 14 + Math.min(num.value / 2, 10);
ctx.font = `bold ${size}px Courier New`;
// Shadow
ctx.fillStyle = `rgba(0, 0, 0, ${alpha * 0.5})`;
ctx.fillText(num.value.toString(), num.x + 2, num.y + 2);
// Main text
ctx.fillStyle = num.color.startsWith('rgba') ? num.color : `rgba(${this.hexToRgb(num.color)}, ${alpha})`;
ctx.fillText(num.value.toString(), num.x, num.y);
});
}
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) => {
ctx.fillStyle = `rgba(255, 255, 0, ${note.alpha})`;
ctx.font = 'bold 20px Courier New';
ctx.textAlign = 'center';
ctx.fillText(note.text, width / 2, 100 + index * 30);
ctx.textAlign = 'left';
});
}
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;
});
}
}