feat: migrate JavaScript files to TypeScript, enhancing type safety and maintainability across the codebase
This commit is contained in:
parent
3db2bb9160
commit
c582f2004e
107 changed files with 5876 additions and 3588 deletions
344
src/systems/UISystem.ts
Normal file
344
src/systems/UISystem.ts
Normal file
|
|
@ -0,0 +1,344 @@
|
|||
import { System } from '../core/System.ts';
|
||||
import { SkillRegistry } from '../skills/SkillRegistry.ts';
|
||||
import { Events, DamageDealtEvent, MutationGainedEvent } from '../core/EventBus.ts';
|
||||
import { PixelFont } from '../core/PixelFont.ts';
|
||||
import { Palette } from '../core/Palette.ts';
|
||||
import { GameState, ComponentType, SystemName } from '../core/Constants.ts';
|
||||
import type { Entity } from '../core/Entity.ts';
|
||||
import type { Engine } from '../core/Engine.ts';
|
||||
import type { Health } from '../components/Health.ts';
|
||||
import type { Stats } from '../components/Stats.ts';
|
||||
import type { Evolution } from '../components/Evolution.ts';
|
||||
import type { Skills } from '../components/Skills.ts';
|
||||
import type { SkillProgress } from '../components/SkillProgress.ts';
|
||||
import type { MenuSystem } from './MenuSystem.ts';
|
||||
import type { PlayerControllerSystem } from './PlayerControllerSystem.ts';
|
||||
|
||||
interface DamageNumber {
|
||||
x: number;
|
||||
y: number;
|
||||
value: number;
|
||||
color: string;
|
||||
lifetime: number;
|
||||
vy: number;
|
||||
}
|
||||
|
||||
interface Notification {
|
||||
text: string;
|
||||
lifetime: number;
|
||||
alpha: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* System responsible for rendering all UI elements, including HUD, skill bars, notifications, and damage numbers.
|
||||
*/
|
||||
export class UISystem extends System {
|
||||
ctx: CanvasRenderingContext2D;
|
||||
damageNumbers: DamageNumber[];
|
||||
notifications: Notification[];
|
||||
|
||||
/**
|
||||
* @param engine - The game engine instance
|
||||
*/
|
||||
constructor(engine: Engine) {
|
||||
super(SystemName.UI);
|
||||
this.requiredComponents = [];
|
||||
this.priority = 200;
|
||||
this.engine = engine;
|
||||
this.ctx = engine.ctx;
|
||||
|
||||
this.damageNumbers = [];
|
||||
this.notifications = [];
|
||||
|
||||
engine.on(Events.DAMAGE_DEALT, (data: unknown) => {
|
||||
const damageData = data as DamageDealtEvent;
|
||||
this.addDamageNumber(damageData);
|
||||
});
|
||||
engine.on(Events.MUTATION_GAINED, (data: unknown) => {
|
||||
const mutationData = data as MutationGainedEvent;
|
||||
this.addNotification(`Mutation Gained: ${mutationData.name} `);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a floating damage number effect.
|
||||
* @param data - Object containing x, y, value, and optional color
|
||||
*/
|
||||
addDamageNumber(data: DamageDealtEvent): void {
|
||||
this.damageNumbers.push({
|
||||
x: data.x,
|
||||
y: data.y,
|
||||
value: Math.floor(data.value),
|
||||
color: data.color || '#ffffff',
|
||||
lifetime: 1.0,
|
||||
vy: -50,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a screen notification message.
|
||||
* @param text - The message text
|
||||
*/
|
||||
addNotification(text: string): void {
|
||||
this.notifications.push({
|
||||
text,
|
||||
lifetime: 3.0,
|
||||
alpha: 1.0,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update UI states and execute rendering of all UI components.
|
||||
* @param deltaTime - Time elapsed since last frame in seconds
|
||||
* @param _entities - Filtered entities
|
||||
*/
|
||||
process(deltaTime: number, _entities: Entity[]): void {
|
||||
this.updateDamageNumbers(deltaTime);
|
||||
this.updateNotifications(deltaTime);
|
||||
|
||||
const menuSystem = this.engine.systems.find((s) => s.name === SystemName.MENU) as
|
||||
| MenuSystem
|
||||
| undefined;
|
||||
const gameState = menuSystem ? menuSystem.getGameState() : GameState.PLAYING;
|
||||
|
||||
if (
|
||||
gameState === GameState.START ||
|
||||
gameState === GameState.PAUSED ||
|
||||
gameState === GameState.GAME_OVER
|
||||
) {
|
||||
if (menuSystem) {
|
||||
menuSystem.drawMenu();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const playerController = this.engine.systems.find(
|
||||
(s) => s.name === SystemName.PLAYER_CONTROLLER
|
||||
) as PlayerControllerSystem | undefined;
|
||||
const player = playerController ? playerController.getPlayerEntity() : null;
|
||||
|
||||
if (!player) return;
|
||||
|
||||
this.drawHUD(player);
|
||||
this.drawSkills(player);
|
||||
this.drawDamageNumbers();
|
||||
this.drawNotifications();
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the main player heads-up display (health, form).
|
||||
*/
|
||||
drawHUD(player: Entity): void {
|
||||
const health = player.getComponent<Health>(ComponentType.HEALTH);
|
||||
const stats = player.getComponent<Stats>(ComponentType.STATS);
|
||||
const evolution = player.getComponent<Evolution>(ComponentType.EVOLUTION);
|
||||
|
||||
if (!health || !stats || !evolution) return;
|
||||
|
||||
const ctx = this.ctx;
|
||||
const barWidth = 64;
|
||||
const barHeight = 6;
|
||||
const barX = 4;
|
||||
const barY = 4;
|
||||
|
||||
ctx.fillStyle = Palette.DARK_BLUE;
|
||||
ctx.fillRect(barX - 1, barY - 1, barWidth + 2, barHeight + 2);
|
||||
|
||||
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);
|
||||
|
||||
PixelFont.drawText(
|
||||
ctx,
|
||||
`${Math.ceil(health.hp)}/${health.maxHp}`,
|
||||
barX,
|
||||
barY + barHeight + 3,
|
||||
Palette.WHITE,
|
||||
1
|
||||
);
|
||||
|
||||
const form = evolution.getDominantForm();
|
||||
const formY = barY + barHeight + 14;
|
||||
PixelFont.drawText(ctx, form.toUpperCase(), barX, formY, Palette.SKY_BLUE, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the skill slots and their active cooldowns.
|
||||
*/
|
||||
drawSkills(player: Entity): void {
|
||||
const skills = player.getComponent<Skills>(ComponentType.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);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw detailed player statistics.
|
||||
*/
|
||||
drawStats(player: Entity, x: number, y: number): void {
|
||||
const stats = player.getComponent<Stats>(ComponentType.STATS);
|
||||
const evolution = player.getComponent<Evolution>(ComponentType.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
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the progress towards learning new skills from absorption.
|
||||
*/
|
||||
drawSkillProgress(player: Entity, x: number, y: number): void {
|
||||
const skillProgress = player.getComponent<SkillProgress>(ComponentType.SKILL_PROGRESS);
|
||||
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++;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the list of currently active mutations.
|
||||
*/
|
||||
drawMutations(player: Entity, x: number, y: number): void {
|
||||
const evolution = player.getComponent<Evolution>(ComponentType.EVOLUTION);
|
||||
if (!evolution) return;
|
||||
|
||||
const ctx = this.ctx;
|
||||
PixelFont.drawText(ctx, 'MUTATIONS', x, y, Palette.CYAN, 1);
|
||||
|
||||
if (evolution.mutations.size === 0) {
|
||||
PixelFont.drawText(ctx, 'NONE', x, y + 10, Palette.DARK_BLUE, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
let idx = 0;
|
||||
evolution.mutations.forEach((mutation) => {
|
||||
const py = y + 10 + idx * 9;
|
||||
PixelFont.drawText(ctx, `> ${mutation.toUpperCase()}`, x, py, Palette.SKY_BLUE, 1);
|
||||
idx++;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update active damage numbers position and lifetimes.
|
||||
*/
|
||||
updateDamageNumbers(deltaTime: number): void {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update notification messages lifetimes and transparency.
|
||||
*/
|
||||
updateNotifications(deltaTime: number): void {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw floating damage numbers on screen.
|
||||
*/
|
||||
drawDamageNumbers(): void {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw active notification messages at the center of the screen.
|
||||
*/
|
||||
drawNotifications(): void {
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue