feat: Implement pixel-art rendering with new level loading, tile maps, palettes, and pixel fonts, alongside a game over screen.
This commit is contained in:
parent
5b15e63ac3
commit
cf04677511
41 changed files with 793 additions and 331 deletions
|
|
@ -1,6 +1,8 @@
|
|||
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) {
|
||||
|
|
@ -15,7 +17,7 @@ export class UISystem extends System {
|
|||
|
||||
// Subscribe to events
|
||||
engine.on(Events.DAMAGE_DEALT, (data) => this.addDamageNumber(data));
|
||||
engine.on(Events.MUTATION_GAINED, (data) => this.addNotification(`Mutation Gained: ${data.name}`));
|
||||
engine.on(Events.MUTATION_GAINED, (data) => this.addNotification(`Mutation Gained: ${data.name} `));
|
||||
}
|
||||
|
||||
addDamageNumber(data) {
|
||||
|
|
@ -45,8 +47,8 @@ export class UISystem extends System {
|
|||
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') {
|
||||
// Only draw menu overlay if in start, paused, or gameOver state
|
||||
if (gameState === 'start' || gameState === 'paused' || gameState === 'gameOver') {
|
||||
if (menuSystem) {
|
||||
menuSystem.drawMenu();
|
||||
}
|
||||
|
|
@ -73,62 +75,43 @@ export class UISystem extends System {
|
|||
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;
|
||||
const barWidth = 64;
|
||||
const barHeight = 6;
|
||||
const barX = 4;
|
||||
const barY = 4;
|
||||
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
|
||||
// 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 ? '#00ff00' : healthPercent > 0.25 ? '#ffff00' : '#ff0000';
|
||||
ctx.fillRect(barX, barY, barWidth * healthPercent, barHeight);
|
||||
ctx.fillStyle = healthPercent > 0.5 ? Palette.CYAN : healthPercent > 0.25 ? Palette.SKY_BLUE : Palette.WHITE;
|
||||
ctx.fillRect(barX, barY, Math.floor(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);
|
||||
// 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 + 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);
|
||||
const formY = barY + barHeight + 14;
|
||||
PixelFont.drawText(ctx, form.toUpperCase(), barX, formY, Palette.SKY_BLUE, 1);
|
||||
|
||||
// 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);
|
||||
// Tiny evolution details
|
||||
const evoDetails = `H${Math.floor(evolution.human)} B${Math.floor(evolution.beast)} S${Math.floor(evolution.slime)}`;
|
||||
PixelFont.drawText(ctx, evoDetails, barX, formY + 9, Palette.ROYAL_BLUE, 1);
|
||||
|
||||
// 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);
|
||||
// Small Instructions
|
||||
PixelFont.drawText(ctx, 'WASD CLICK', barX, this.engine.canvas.height - 10, Palette.DARK_BLUE, 1);
|
||||
}
|
||||
|
||||
drawSkills(player) {
|
||||
|
|
@ -137,28 +120,23 @@ export class UISystem extends System {
|
|||
|
||||
const ctx = this.ctx;
|
||||
const width = this.engine.canvas.width;
|
||||
const startX = width - 250;
|
||||
const startY = 20;
|
||||
const startX = width - 80;
|
||||
const startY = 4;
|
||||
|
||||
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);
|
||||
PixelFont.drawText(ctx, 'SKILLS', startX, startY, Palette.WHITE, 1);
|
||||
|
||||
skills.activeSkills.forEach((skillId, index) => {
|
||||
const y = startY + 40 + index * 30;
|
||||
const key = (index + 1).toString();
|
||||
const y = startY + 10 + index * 9;
|
||||
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('_', ' ');
|
||||
let skillName = skill ? skill.name : skillId.replace('_', ' ');
|
||||
if (skillName.length > 10) skillName = skillName.substring(0, 10);
|
||||
|
||||
ctx.fillStyle = onCooldown ? '#888888' : '#00ff96';
|
||||
ctx.fillText(`${key}. ${skillName}${onCooldown ? ` (${cooldown.toFixed(1)}s)` : ''}`, startX + 10, y);
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -168,26 +146,12 @@ export class UISystem extends System {
|
|||
|
||||
const ctx = this.ctx;
|
||||
const width = this.engine.canvas.width;
|
||||
const startX = width - 250;
|
||||
const startY = 200;
|
||||
const startX = width - 80;
|
||||
const startY = 60;
|
||||
|
||||
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);
|
||||
PixelFont.drawText(ctx, 'STATS', startX, startY, Palette.WHITE, 1);
|
||||
PixelFont.drawText(ctx, `STR ${stats.strength} AGI ${stats.agility}`, startX, startY + 9, Palette.ROYAL_BLUE, 1);
|
||||
PixelFont.drawText(ctx, `INT ${stats.intelligence} CON ${stats.constitution}`, startX, startY + 18, Palette.ROYAL_BLUE, 1);
|
||||
}
|
||||
|
||||
drawSkillProgress(player) {
|
||||
|
|
@ -196,29 +160,24 @@ export class UISystem extends System {
|
|||
|
||||
const ctx = this.ctx;
|
||||
const width = this.engine.canvas.width;
|
||||
const startX = width - 250;
|
||||
const startY = 360;
|
||||
const startX = width - 80;
|
||||
const startY = 100;
|
||||
|
||||
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);
|
||||
PixelFont.drawText(ctx, 'LRN', startX, startY, Palette.CYAN, 1);
|
||||
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.font = '12px Courier New';
|
||||
ctx.fillText('Skill Progress:', startX + 10, startY + 20);
|
||||
|
||||
let y = startY + 35;
|
||||
let idx = 0;
|
||||
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('_', ' ');
|
||||
let name = skill ? skill.name : skillId;
|
||||
if (name.length > 4) name = name.substring(0, 4);
|
||||
|
||||
ctx.fillStyle = count >= required ? '#00ff00' : '#ffff00';
|
||||
ctx.fillText(`${skillName}: ${count}/${required}`, startX + 10, y);
|
||||
y += 20;
|
||||
const y = startY + 9 + idx * 8;
|
||||
PixelFont.drawText(ctx, `${name} ${count}/${required}`, startX, y, Palette.SKY_BLUE, 1);
|
||||
idx++;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -244,17 +203,9 @@ export class UISystem extends System {
|
|||
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);
|
||||
const color = num.color.startsWith('rgba') ? num.color : Palette.WHITE;
|
||||
|
||||
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);
|
||||
PixelFont.drawText(ctx, num.value.toString(), Math.floor(num.x), Math.floor(num.y), color, 1);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -270,11 +221,11 @@ export class UISystem extends System {
|
|||
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';
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue