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
|
|
@ -1,15 +0,0 @@
|
|||
/**
|
||||
* Base Component class for ECS architecture
|
||||
* Components are pure data containers
|
||||
*/
|
||||
export class Component {
|
||||
constructor(type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
static getType() {
|
||||
return this.name;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
25
src/core/Component.ts
Normal file
25
src/core/Component.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* Base Component class for ECS architecture.
|
||||
* Components are pure data containers.
|
||||
*/
|
||||
export class Component {
|
||||
/**
|
||||
* The unique type identifier for this component
|
||||
*/
|
||||
readonly type: string;
|
||||
|
||||
/**
|
||||
* @param type - The unique type identifier for this component
|
||||
*/
|
||||
constructor(type: string) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the type name of the component class.
|
||||
* @returns The name of the class
|
||||
*/
|
||||
static getType(): string {
|
||||
return this.name;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
/**
|
||||
* Centralized Constants and Enums
|
||||
*/
|
||||
|
||||
export const GameState = {
|
||||
START: 'start',
|
||||
PLAYING: 'playing',
|
||||
PAUSED: 'paused',
|
||||
GAME_OVER: 'gameOver'
|
||||
};
|
||||
|
||||
export const ComponentType = {
|
||||
POSITION: 'Position',
|
||||
VELOCITY: 'Velocity',
|
||||
SPRITE: 'Sprite',
|
||||
HEALTH: 'Health',
|
||||
COMBAT: 'Combat',
|
||||
AI: 'AI',
|
||||
EVOLUTION: 'Evolution',
|
||||
STATS: 'Stats',
|
||||
SKILLS: 'Skills',
|
||||
SKILL_PROGRESS: 'SkillProgress',
|
||||
ABSORBABLE: 'Absorbable',
|
||||
STEALTH: 'Stealth'
|
||||
};
|
||||
|
||||
export const EntityType = {
|
||||
SLIME: 'slime',
|
||||
HUMANOID: 'humanoid',
|
||||
BEAST: 'beast',
|
||||
ELEMENTAL: 'elemental',
|
||||
PROJECTILE: 'projectile'
|
||||
};
|
||||
|
||||
export const AnimationState = {
|
||||
IDLE: 'idle',
|
||||
WALK: 'walk'
|
||||
};
|
||||
|
||||
export const VFXType = {
|
||||
IMPACT: 'impact',
|
||||
ABSORPTION: 'absorption'
|
||||
};
|
||||
|
||||
export const SystemName = {
|
||||
MENU: 'MenuSystem',
|
||||
UI: 'UISystem',
|
||||
PLAYER_CONTROLLER: 'PlayerControllerSystem',
|
||||
ABSORPTION: 'AbsorptionSystem',
|
||||
COMBAT: 'CombatSystem',
|
||||
PROJECTILE: 'ProjectileSystem',
|
||||
VFX: 'VFXSystem',
|
||||
MOVEMENT: 'MovementSystem',
|
||||
AI: 'AISystem',
|
||||
DEATH: 'DeathSystem',
|
||||
RENDER: 'RenderSystem',
|
||||
INPUT: 'InputSystem',
|
||||
SKILL_EFFECT: 'SkillEffectSystem',
|
||||
SKILL: 'SkillSystem',
|
||||
STEALTH: 'StealthSystem',
|
||||
HEALTH_REGEN: 'HealthRegenerationSystem'
|
||||
};
|
||||
82
src/core/Constants.ts
Normal file
82
src/core/Constants.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
/**
|
||||
* Game state enumeration.
|
||||
*/
|
||||
export enum GameState {
|
||||
/** Initial start screen */
|
||||
START = 'start',
|
||||
/** Active gameplay */
|
||||
PLAYING = 'playing',
|
||||
/** Game paused */
|
||||
PAUSED = 'paused',
|
||||
/** Player death screen */
|
||||
GAME_OVER = 'gameOver',
|
||||
}
|
||||
|
||||
/**
|
||||
* Component type identifiers.
|
||||
*/
|
||||
export enum ComponentType {
|
||||
POSITION = 'Position',
|
||||
VELOCITY = 'Velocity',
|
||||
SPRITE = 'Sprite',
|
||||
HEALTH = 'Health',
|
||||
COMBAT = 'Combat',
|
||||
AI = 'AI',
|
||||
EVOLUTION = 'Evolution',
|
||||
STATS = 'Stats',
|
||||
SKILLS = 'Skills',
|
||||
SKILL_PROGRESS = 'SkillProgress',
|
||||
ABSORBABLE = 'Absorbable',
|
||||
STEALTH = 'Stealth',
|
||||
INTENT = 'Intent',
|
||||
INVENTORY = 'Inventory',
|
||||
}
|
||||
|
||||
/**
|
||||
* Entity type identifiers for sprites and behaviors.
|
||||
*/
|
||||
export enum EntityType {
|
||||
SLIME = 'slime',
|
||||
HUMANOID = 'humanoid',
|
||||
BEAST = 'beast',
|
||||
ELEMENTAL = 'elemental',
|
||||
PROJECTILE = 'projectile',
|
||||
}
|
||||
|
||||
/**
|
||||
* Animation states for sprites.
|
||||
*/
|
||||
export enum AnimationState {
|
||||
IDLE = 'idle',
|
||||
WALK = 'walk',
|
||||
}
|
||||
|
||||
/**
|
||||
* Visual effect types.
|
||||
*/
|
||||
export enum VFXType {
|
||||
IMPACT = 'impact',
|
||||
ABSORPTION = 'absorption',
|
||||
}
|
||||
|
||||
/**
|
||||
* System name identifiers.
|
||||
*/
|
||||
export enum SystemName {
|
||||
MENU = 'MenuSystem',
|
||||
UI = 'UISystem',
|
||||
PLAYER_CONTROLLER = 'PlayerControllerSystem',
|
||||
ABSORPTION = 'AbsorptionSystem',
|
||||
COMBAT = 'CombatSystem',
|
||||
PROJECTILE = 'ProjectileSystem',
|
||||
VFX = 'VFXSystem',
|
||||
MOVEMENT = 'MovementSystem',
|
||||
AI = 'AISystem',
|
||||
DEATH = 'DeathSystem',
|
||||
RENDER = 'RenderSystem',
|
||||
INPUT = 'InputSystem',
|
||||
SKILL_EFFECT = 'SkillEffectSystem',
|
||||
SKILL = 'SkillSystem',
|
||||
STEALTH = 'StealthSystem',
|
||||
HEALTH_REGEN = 'HealthRegenerationSystem',
|
||||
}
|
||||
|
|
@ -1,152 +0,0 @@
|
|||
import { System } from './System.js';
|
||||
import { Entity } from './Entity.js';
|
||||
import { EventBus } from './EventBus.js';
|
||||
import { LevelLoader } from './LevelLoader.js';
|
||||
import { GameState, SystemName } from './Constants.js';
|
||||
|
||||
/**
|
||||
* Main game engine - manages ECS, game loop, and systems
|
||||
*/
|
||||
export class Engine {
|
||||
constructor(canvas) {
|
||||
this.canvas = canvas;
|
||||
this.ctx = canvas.getContext('2d');
|
||||
this.entities = [];
|
||||
this.systems = [];
|
||||
this.events = new EventBus();
|
||||
this.running = false;
|
||||
this.lastTime = 0;
|
||||
|
||||
// Set internal resolution (low-res for pixel art)
|
||||
this.canvas.width = 320;
|
||||
this.canvas.height = 240;
|
||||
|
||||
// Apply CSS for sharp pixel scaling
|
||||
this.canvas.style.imageRendering = 'pixelated'; // Standard
|
||||
// Fallbacks for other browsers if needed (mostly covered by modern standards, but good to be safe)
|
||||
this.canvas.style.imageRendering = '-moz-crisp-edges';
|
||||
this.canvas.style.imageRendering = 'crisp-edges';
|
||||
this.ctx.imageSmoothingEnabled = false;
|
||||
|
||||
// Game state
|
||||
this.deltaTime = 0;
|
||||
|
||||
// Initialize standard map (320x240 / 16px tiles = 20x15)
|
||||
this.tileMap = LevelLoader.loadSimpleLevel(20, 15, 16);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a system to the engine
|
||||
*/
|
||||
addSystem(system) {
|
||||
if (system instanceof System) {
|
||||
system.init(this);
|
||||
this.systems.push(system);
|
||||
// Sort by priority (lower priority runs first)
|
||||
this.systems.sort((a, b) => a.priority - b.priority);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit an event locally
|
||||
*/
|
||||
emit(event, data) {
|
||||
this.events.emit(event, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to an event
|
||||
*/
|
||||
on(event, callback) {
|
||||
return this.events.on(event, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and add an entity
|
||||
*/
|
||||
createEntity() {
|
||||
const entity = new Entity();
|
||||
this.entities.push(entity);
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an entity
|
||||
*/
|
||||
removeEntity(entity) {
|
||||
const index = this.entities.indexOf(entity);
|
||||
if (index > -1) {
|
||||
this.entities.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all entities
|
||||
*/
|
||||
getEntities() {
|
||||
return this.entities.filter(e => e.active);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main game loop
|
||||
*/
|
||||
start() {
|
||||
if (this.running) return;
|
||||
this.running = true;
|
||||
this.lastTime = performance.now();
|
||||
this.gameLoop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the game loop
|
||||
*/
|
||||
stop() {
|
||||
this.running = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Game loop using requestAnimationFrame
|
||||
*/
|
||||
gameLoop = (currentTime = 0) => {
|
||||
if (!this.running) return;
|
||||
|
||||
// Calculate delta time
|
||||
this.deltaTime = (currentTime - this.lastTime) / 1000; // Convert to seconds
|
||||
this.lastTime = currentTime;
|
||||
|
||||
// Clamp delta time to prevent large jumps
|
||||
this.deltaTime = Math.min(this.deltaTime, 0.1);
|
||||
|
||||
// Update all systems
|
||||
const menuSystem = this.systems.find(s => s.name === SystemName.MENU);
|
||||
const gameState = menuSystem ? menuSystem.getGameState() : GameState.PLAYING;
|
||||
const isPaused = [GameState.PAUSED, GameState.START, GameState.GAME_OVER].includes(gameState);
|
||||
const unskippedSystems = [SystemName.MENU, SystemName.UI, SystemName.RENDER];
|
||||
|
||||
this.systems.forEach(system => {
|
||||
// Skip game systems if paused/start menu (but allow MenuSystem, UISystem, and RenderSystem)
|
||||
if (isPaused && !unskippedSystems.includes(system.name)) {
|
||||
return;
|
||||
}
|
||||
system.update(this.deltaTime, this.entities);
|
||||
});
|
||||
|
||||
// Update input system's previous states at end of frame
|
||||
const inputSystem = this.systems.find(s => s.name === SystemName.INPUT);
|
||||
if (inputSystem && inputSystem.updatePreviousStates) {
|
||||
inputSystem.updatePreviousStates();
|
||||
}
|
||||
|
||||
// Continue loop
|
||||
requestAnimationFrame(this.gameLoop);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the canvas
|
||||
*/
|
||||
clear() {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
}
|
||||
}
|
||||
|
||||
171
src/core/Engine.ts
Normal file
171
src/core/Engine.ts
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
import { System } from './System.ts';
|
||||
import { Entity } from './Entity.ts';
|
||||
import { EventBus } from './EventBus.ts';
|
||||
import { LevelLoader } from './LevelLoader.ts';
|
||||
import { GameState, SystemName } from './Constants.ts';
|
||||
import type { TileMap } from './TileMap.ts';
|
||||
import type { MenuSystem } from '../systems/MenuSystem.ts';
|
||||
import type { InputSystem } from '../systems/InputSystem.ts';
|
||||
|
||||
/**
|
||||
* Main game engine responsible for managing the ECS lifecycle, game loop, and system execution.
|
||||
*/
|
||||
export class Engine {
|
||||
canvas: HTMLCanvasElement;
|
||||
ctx: CanvasRenderingContext2D;
|
||||
entities: Entity[];
|
||||
systems: System[];
|
||||
events: EventBus;
|
||||
running: boolean;
|
||||
lastTime: number;
|
||||
deltaTime: number;
|
||||
tileMap: TileMap | null;
|
||||
|
||||
/**
|
||||
* @param canvas - The canvas element to render to
|
||||
*/
|
||||
constructor(canvas: HTMLCanvasElement) {
|
||||
this.canvas = canvas;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
throw new Error('Failed to get 2d rendering context from canvas');
|
||||
}
|
||||
this.ctx = ctx;
|
||||
this.entities = [];
|
||||
this.systems = [];
|
||||
this.events = new EventBus();
|
||||
this.running = false;
|
||||
this.lastTime = 0;
|
||||
|
||||
this.canvas.width = 320;
|
||||
this.canvas.height = 240;
|
||||
|
||||
this.canvas.style.imageRendering = 'pixelated';
|
||||
this.ctx.imageSmoothingEnabled = false;
|
||||
|
||||
this.deltaTime = 0;
|
||||
this.tileMap = LevelLoader.loadSimpleLevel(20, 15, 16);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a system and sort systems by priority.
|
||||
* @param system - The system to add
|
||||
* @returns This engine instance
|
||||
*/
|
||||
addSystem(system: System): Engine {
|
||||
if (system instanceof System) {
|
||||
system.init(this);
|
||||
this.systems.push(system);
|
||||
this.systems.sort((a, b) => a.priority - b.priority);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a global event.
|
||||
* @param event - Unique event identifier
|
||||
* @param data - Optional event payload
|
||||
*/
|
||||
emit(event: string, data?: unknown): void {
|
||||
this.events.emit(event, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to a global event.
|
||||
* @param event - Unique event identifier
|
||||
* @param callback - Function to execute when event is emitted
|
||||
* @returns Unsubscribe function
|
||||
*/
|
||||
on(event: string, callback: (data?: unknown) => void): () => void {
|
||||
return this.events.on(event, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new entity and track it.
|
||||
* @returns The newly created entity
|
||||
*/
|
||||
createEntity(): Entity {
|
||||
const entity = new Entity();
|
||||
this.entities.push(entity);
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Permanently remove an entity from the engine.
|
||||
* @param entity - The entity to remove
|
||||
*/
|
||||
removeEntity(entity: Entity): void {
|
||||
const index = this.entities.indexOf(entity);
|
||||
if (index > -1) {
|
||||
this.entities.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all currently active entities.
|
||||
* @returns List of active entities
|
||||
*/
|
||||
getEntities(): Entity[] {
|
||||
return this.entities.filter((e) => e.active);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the game loop.
|
||||
*/
|
||||
start(): void {
|
||||
if (this.running) return;
|
||||
this.running = true;
|
||||
this.lastTime = performance.now();
|
||||
this.gameLoop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminate the game loop.
|
||||
*/
|
||||
stop(): void {
|
||||
this.running = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Core game loop executing system updates.
|
||||
* @param currentTime - High-resolution timestamp
|
||||
*/
|
||||
gameLoop = (currentTime = 0): void => {
|
||||
if (!this.running) return;
|
||||
|
||||
this.deltaTime = (currentTime - this.lastTime) / 1000;
|
||||
this.lastTime = currentTime;
|
||||
|
||||
this.deltaTime = Math.min(this.deltaTime, 0.1);
|
||||
|
||||
const menuSystem = this.systems.find((s) => s.name === SystemName.MENU) as
|
||||
| MenuSystem
|
||||
| undefined;
|
||||
const gameState = menuSystem ? menuSystem.getGameState() : GameState.PLAYING;
|
||||
const isPaused = [GameState.PAUSED, GameState.START, GameState.GAME_OVER].includes(gameState);
|
||||
const unskippedSystems = [SystemName.MENU, SystemName.UI, SystemName.RENDER];
|
||||
|
||||
this.systems.forEach((system) => {
|
||||
if (isPaused && !unskippedSystems.includes(system.name as SystemName)) {
|
||||
return;
|
||||
}
|
||||
system.update(this.deltaTime, this.entities);
|
||||
});
|
||||
|
||||
const inputSystem = this.systems.find((s) => s.name === SystemName.INPUT) as
|
||||
| InputSystem
|
||||
| undefined;
|
||||
if (inputSystem && inputSystem.updatePreviousStates) {
|
||||
inputSystem.updatePreviousStates();
|
||||
}
|
||||
|
||||
requestAnimationFrame(this.gameLoop);
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear the rendering surface.
|
||||
*/
|
||||
clear(): void {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
/**
|
||||
* Entity class - represents a game object with a unique ID
|
||||
* Entities are just containers for components
|
||||
*/
|
||||
export class Entity {
|
||||
static nextId = 0;
|
||||
|
||||
constructor() {
|
||||
this.id = Entity.nextId++;
|
||||
this.components = new Map();
|
||||
this.active = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a component to this entity
|
||||
*/
|
||||
addComponent(component) {
|
||||
this.components.set(component.type, component);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a component by type
|
||||
*/
|
||||
getComponent(type) {
|
||||
return this.components.get(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if entity has a component
|
||||
*/
|
||||
hasComponent(type) {
|
||||
return this.components.has(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if entity has all specified components
|
||||
*/
|
||||
hasComponents(...types) {
|
||||
return types.every(type => this.components.has(type));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a component
|
||||
*/
|
||||
removeComponent(type) {
|
||||
this.components.delete(type);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all components
|
||||
*/
|
||||
getAllComponents() {
|
||||
return Array.from(this.components.values());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
83
src/core/Entity.ts
Normal file
83
src/core/Entity.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import { Component } from './Component.ts';
|
||||
|
||||
/**
|
||||
* Entity class - represents a game object with a unique ID.
|
||||
* Entities are containers for components.
|
||||
*/
|
||||
export class Entity {
|
||||
private static nextId = 0;
|
||||
|
||||
readonly id: number;
|
||||
private components: Map<string, Component>;
|
||||
active: boolean;
|
||||
|
||||
// Optional dynamic properties for specific entity types
|
||||
owner?: number;
|
||||
startX?: number;
|
||||
startY?: number;
|
||||
maxRange?: number;
|
||||
lifetime?: number;
|
||||
damage?: number;
|
||||
deathTime?: number;
|
||||
|
||||
constructor() {
|
||||
this.id = Entity.nextId++;
|
||||
this.components = new Map();
|
||||
this.active = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a component to this entity.
|
||||
* @param component - The component to add
|
||||
* @returns This entity for chaining
|
||||
*/
|
||||
addComponent(component: Component): Entity {
|
||||
this.components.set(component.type, component);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a component by type.
|
||||
* @param type - The component type name
|
||||
* @returns The component instance if found
|
||||
*/
|
||||
getComponent<T extends Component>(type: string): T | undefined {
|
||||
return this.components.get(type) as T | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if entity has a component.
|
||||
* @param type - The component type name
|
||||
* @returns True if the component exists
|
||||
*/
|
||||
hasComponent(type: string): boolean {
|
||||
return this.components.has(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if entity has all specified components.
|
||||
* @param types - List of component type names
|
||||
* @returns True if all components exist
|
||||
*/
|
||||
hasComponents(...types: string[]): boolean {
|
||||
return types.every((type) => this.components.has(type));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a component.
|
||||
* @param type - The component type name
|
||||
* @returns This entity for chaining
|
||||
*/
|
||||
removeComponent(type: string): Entity {
|
||||
this.components.delete(type);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all components attached to this entity.
|
||||
* @returns Array of components
|
||||
*/
|
||||
getAllComponents(): Component[] {
|
||||
return Array.from(this.components.values());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
/**
|
||||
* Lightweight EventBus for pub/sub communication between systems
|
||||
*/
|
||||
export const Events = {
|
||||
// Combat Events
|
||||
DAMAGE_DEALT: 'combat:damage_dealt',
|
||||
ENTITY_DIED: 'combat:entity_died',
|
||||
|
||||
// Evolution Events
|
||||
EVOLVED: 'evolution:evolved',
|
||||
MUTATION_GAINED: 'evolution:mutation_gained',
|
||||
|
||||
// Leveling Events
|
||||
EXP_GAINED: 'stats:exp_gained',
|
||||
LEVEL_UP: 'stats:level_up',
|
||||
|
||||
// Skill Events
|
||||
SKILL_LEARNED: 'skills:learned',
|
||||
SKILL_COOLDOWN_STARTED: 'skills:cooldown_started'
|
||||
};
|
||||
|
||||
export class EventBus {
|
||||
constructor() {
|
||||
this.listeners = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to an event
|
||||
*/
|
||||
on(event, callback) {
|
||||
if (!this.listeners.has(event)) {
|
||||
this.listeners.set(event, []);
|
||||
}
|
||||
this.listeners.get(event).push(callback);
|
||||
return () => this.off(event, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from an event
|
||||
*/
|
||||
off(event, callback) {
|
||||
if (!this.listeners.has(event)) return;
|
||||
const callbacks = this.listeners.get(event);
|
||||
const index = callbacks.indexOf(callback);
|
||||
if (index > -1) {
|
||||
callbacks.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit an event
|
||||
*/
|
||||
emit(event, data) {
|
||||
if (!this.listeners.has(event)) return;
|
||||
this.listeners.get(event).forEach(callback => callback(data));
|
||||
}
|
||||
}
|
||||
101
src/core/EventBus.ts
Normal file
101
src/core/EventBus.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
/**
|
||||
* Enum for game-wide event types.
|
||||
*/
|
||||
export enum Events {
|
||||
DAMAGE_DEALT = 'combat:damage_dealt',
|
||||
ENTITY_DIED = 'combat:entity_died',
|
||||
EVOLVED = 'evolution:evolved',
|
||||
MUTATION_GAINED = 'evolution:mutation_gained',
|
||||
EXP_GAINED = 'stats:exp_gained',
|
||||
LEVEL_UP = 'stats:level_up',
|
||||
SKILL_LEARNED = 'skills:learned',
|
||||
ATTACK_PERFORMED = 'combat:attack_performed',
|
||||
SKILL_COOLDOWN_STARTED = 'skills:cooldown_started',
|
||||
}
|
||||
|
||||
/**
|
||||
* Event data types
|
||||
*/
|
||||
export interface DamageDealtEvent {
|
||||
x: number;
|
||||
y: number;
|
||||
value: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export interface MutationGainedEvent {
|
||||
name: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface SkillLearnedEvent {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface EntityDiedEvent {
|
||||
entity: unknown;
|
||||
}
|
||||
|
||||
export interface AttackPerformedEvent {
|
||||
entity: unknown;
|
||||
angle: number;
|
||||
}
|
||||
|
||||
type EventCallback = (data?: unknown) => void;
|
||||
|
||||
/**
|
||||
* Lightweight EventBus for pub/sub communication between systems.
|
||||
*/
|
||||
export class EventBus {
|
||||
private listeners: Map<string, EventCallback[]>;
|
||||
|
||||
constructor() {
|
||||
this.listeners = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to an event with a callback.
|
||||
* @param event - The event name from the Events enum
|
||||
* @param callback - The function to call when the event is emitted
|
||||
* @returns An unsubscribe function
|
||||
*/
|
||||
on(event: string, callback: EventCallback): () => void {
|
||||
if (!this.listeners.has(event)) {
|
||||
this.listeners.set(event, []);
|
||||
}
|
||||
const callbacks = this.listeners.get(event);
|
||||
if (callbacks) {
|
||||
callbacks.push(callback);
|
||||
}
|
||||
return () => this.off(event, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe a specific callback from an event.
|
||||
* @param event - The event name
|
||||
* @param callback - The original callback function to remove
|
||||
*/
|
||||
off(event: string, callback: EventCallback): void {
|
||||
if (!this.listeners.has(event)) return;
|
||||
const callbacks = this.listeners.get(event);
|
||||
if (callbacks) {
|
||||
const index = callbacks.indexOf(callback);
|
||||
if (index > -1) {
|
||||
callbacks.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit an event to all subscribers.
|
||||
* @param event - The event name
|
||||
* @param data - Data to pass to the callbacks
|
||||
*/
|
||||
emit(event: string, data?: unknown): void {
|
||||
if (!this.listeners.has(event)) return;
|
||||
const callbacks = this.listeners.get(event);
|
||||
if (callbacks) {
|
||||
callbacks.forEach((callback) => callback(data));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
import { TileMap } from './TileMap.js';
|
||||
|
||||
export class LevelLoader {
|
||||
static loadSimpleLevel(cols, rows, tileSize) {
|
||||
const map = new TileMap(cols, rows, tileSize);
|
||||
|
||||
// Create a box arena for testing
|
||||
for (let r = 0; r < rows; r++) {
|
||||
for (let c = 0; c < cols; c++) {
|
||||
if (r === 0 || r === rows - 1 || c === 0 || c === cols - 1) {
|
||||
map.setTile(c, r, 1); // Wall
|
||||
} else {
|
||||
// Random obstacles
|
||||
if (Math.random() < 0.1) {
|
||||
map.setTile(c, r, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
30
src/core/LevelLoader.ts
Normal file
30
src/core/LevelLoader.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { TileMap } from './TileMap.ts';
|
||||
|
||||
/**
|
||||
* Utility class responsible for generating or loading level tile maps.
|
||||
*/
|
||||
export class LevelLoader {
|
||||
/**
|
||||
* Generates a simple arena level with walls at the boundaries and random obstacles.
|
||||
* @param cols - Map width in tiles
|
||||
* @param rows - Map height in tiles
|
||||
* @param tileSize - Tile size in pixels
|
||||
* @returns The generated tile map
|
||||
*/
|
||||
static loadSimpleLevel(cols: number, rows: number, tileSize: number): TileMap {
|
||||
const map = new TileMap(cols, rows, tileSize);
|
||||
|
||||
for (let r = 0; r < rows; r++) {
|
||||
for (let c = 0; c < cols; c++) {
|
||||
if (r === 0 || r === rows - 1 || c === 0 || c === cols - 1) {
|
||||
map.setTile(c, r, 1);
|
||||
} else {
|
||||
if (Math.random() < 0.1) {
|
||||
map.setTile(c, r, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
/**
|
||||
* Limited 7-color palette for the game
|
||||
*/
|
||||
export const Palette = {
|
||||
WHITE: '#ffffff', // Highlights, UI Text
|
||||
CYAN: '#0ce6f2', // Energy, Slime core
|
||||
SKY_BLUE: '#0098db', // Water, Friendly elements
|
||||
ROYAL_BLUE: '#1e579c', // Shadows, Depth
|
||||
DARK_BLUE: '#203562', // Walls, Obstacles
|
||||
DARKER_BLUE: '#252446', // Background details
|
||||
VOID: '#201533', // Void, Deep Background
|
||||
|
||||
/**
|
||||
* Get all colors as an array
|
||||
*/
|
||||
getAll() {
|
||||
return [
|
||||
this.WHITE,
|
||||
this.CYAN,
|
||||
this.SKY_BLUE,
|
||||
this.ROYAL_BLUE,
|
||||
this.DARK_BLUE,
|
||||
this.DARKER_BLUE,
|
||||
this.VOID
|
||||
];
|
||||
}
|
||||
};
|
||||
35
src/core/Palette.ts
Normal file
35
src/core/Palette.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
/**
|
||||
* Limited color palette used throughout the game to ensure a consistent aesthetic.
|
||||
*/
|
||||
export const Palette = {
|
||||
/** Highlights and UI Text */
|
||||
WHITE: '#ffffff',
|
||||
/** Energy and Slime core */
|
||||
CYAN: '#0ce6f2',
|
||||
/** Water and friendly elements */
|
||||
SKY_BLUE: '#0098db',
|
||||
/** Shadows and visual depth */
|
||||
ROYAL_BLUE: '#1e579c',
|
||||
/** Walls and obstacles */
|
||||
DARK_BLUE: '#203562',
|
||||
/** Background details */
|
||||
DARKER_BLUE: '#252446',
|
||||
/** Deep background and void */
|
||||
VOID: '#201533',
|
||||
|
||||
/**
|
||||
* Get all colors in the palette as an array.
|
||||
* @returns Array of hex color strings
|
||||
*/
|
||||
getAll(): string[] {
|
||||
return [
|
||||
this.WHITE,
|
||||
this.CYAN,
|
||||
this.SKY_BLUE,
|
||||
this.ROYAL_BLUE,
|
||||
this.DARK_BLUE,
|
||||
this.DARKER_BLUE,
|
||||
this.VOID,
|
||||
];
|
||||
},
|
||||
};
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
/**
|
||||
* Simple 5x7 Matrix Pixel Font
|
||||
* Each character is a 5x7 bitmask
|
||||
*/
|
||||
const FONT_DATA = {
|
||||
'A': [0x0E, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11],
|
||||
'B': [0x1E, 0x11, 0x11, 0x1E, 0x11, 0x11, 0x1E],
|
||||
'C': [0x0E, 0x11, 0x11, 0x10, 0x11, 0x11, 0x0E],
|
||||
'D': [0x1E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x1E],
|
||||
'E': [0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x1F],
|
||||
'F': [0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x10],
|
||||
'G': [0x0F, 0x10, 0x10, 0x17, 0x11, 0x11, 0x0F],
|
||||
'H': [0x11, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11],
|
||||
'I': [0x0E, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0E],
|
||||
'J': [0x07, 0x02, 0x02, 0x02, 0x02, 0x12, 0x0C],
|
||||
'K': [0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11],
|
||||
'L': [0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1F],
|
||||
'M': [0x11, 0x1B, 0x15, 0x15, 0x11, 0x11, 0x11],
|
||||
'N': [0x11, 0x11, 0x19, 0x15, 0x13, 0x11, 0x11],
|
||||
'O': [0x0E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E],
|
||||
'P': [0x1E, 0x11, 0x11, 0x1E, 0x10, 0x10, 0x10],
|
||||
'Q': [0x0E, 0x11, 0x11, 0x11, 0x15, 0x12, 0x0D],
|
||||
'R': [0x1E, 0x11, 0x11, 0x1E, 0x14, 0x12, 0x11],
|
||||
'S': [0x0E, 0x11, 0x10, 0x0E, 0x01, 0x11, 0x0E],
|
||||
'T': [0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04],
|
||||
'U': [0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E],
|
||||
'V': [0x11, 0x11, 0x11, 0x11, 0x11, 0x0A, 0x04],
|
||||
'W': [0x11, 0x11, 0x11, 0x15, 0x15, 0x1B, 0x11],
|
||||
'X': [0x11, 0x11, 0x0A, 0x04, 0x0A, 0x11, 0x11],
|
||||
'Y': [0x11, 0x11, 0x0A, 0x04, 0x04, 0x04, 0x04],
|
||||
'Z': [0x1F, 0x01, 0x02, 0x04, 0x08, 0x10, 0x1F],
|
||||
'0': [0x0E, 0x11, 0x13, 0x15, 0x19, 0x11, 0x0E],
|
||||
'1': [0x04, 0x0C, 0x04, 0x04, 0x04, 0x04, 0x0E],
|
||||
'2': [0x0E, 0x11, 0x01, 0x02, 0x04, 0x08, 0x1F],
|
||||
'3': [0x1F, 0x02, 0x04, 0x02, 0x01, 0x11, 0x0E],
|
||||
'4': [0x02, 0x06, 0x0A, 0x12, 0x1F, 0x02, 0x02],
|
||||
'5': [0x1F, 0x10, 0x1E, 0x01, 0x01, 0x11, 0x0E],
|
||||
'6': [0x06, 0x08, 0x10, 0x1E, 0x11, 0x11, 0x0E],
|
||||
'7': [0x1F, 0x01, 0x02, 0x04, 0x08, 0x08, 0x08],
|
||||
'8': [0x0E, 0x11, 0x11, 0x0E, 0x11, 0x11, 0x0E],
|
||||
'9': [0x0E, 0x11, 0x11, 0x0F, 0x01, 0x02, 0x0C],
|
||||
':': [0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00],
|
||||
'.': [0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00],
|
||||
',': [0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x08],
|
||||
'!': [0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x04],
|
||||
'?': [0x0E, 0x11, 0x01, 0x02, 0x04, 0x00, 0x04],
|
||||
'+': [0x00, 0x04, 0x04, 0x1F, 0x04, 0x04, 0x00],
|
||||
'-': [0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00],
|
||||
'/': [0x01, 0x02, 0x04, 0x08, 0x10, 0x10, 0x10],
|
||||
'(': [0x02, 0x04, 0x04, 0x04, 0x04, 0x04, 0x02],
|
||||
')': [0x08, 0x04, 0x04, 0x04, 0x04, 0x04, 0x08],
|
||||
' ': [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
|
||||
'|': [0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04]
|
||||
};
|
||||
|
||||
export const PixelFont = {
|
||||
drawText(ctx, text, x, y, color = '#ffffff', scale = 1) {
|
||||
ctx.save();
|
||||
ctx.fillStyle = color;
|
||||
let cursorX = x;
|
||||
|
||||
const chars = text.toUpperCase().split('');
|
||||
chars.forEach(char => {
|
||||
const glyph = FONT_DATA[char] || FONT_DATA['?'];
|
||||
for (let row = 0; row < 7; row++) {
|
||||
for (let col = 0; col < 5; col++) {
|
||||
if ((glyph[row] >> (4 - col)) & 1) {
|
||||
ctx.fillRect(cursorX + col * scale, y + row * scale, scale, scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
cursorX += 6 * scale; // 5 width + 1 spacing
|
||||
});
|
||||
ctx.restore();
|
||||
},
|
||||
|
||||
getTextWidth(text, scale = 1) {
|
||||
return text.length * 6 * scale;
|
||||
}
|
||||
};
|
||||
105
src/core/PixelFont.ts
Normal file
105
src/core/PixelFont.ts
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
/**
|
||||
* Simple 5x7 Matrix Pixel Font data.
|
||||
* Each character is represented by an array of 7 integers, where each integer is a 5-bit mask.
|
||||
*/
|
||||
const FONT_DATA: Record<string, number[]> = {
|
||||
A: [0x0e, 0x11, 0x11, 0x1f, 0x11, 0x11, 0x11],
|
||||
B: [0x1e, 0x11, 0x11, 0x1e, 0x11, 0x11, 0x1e],
|
||||
C: [0x0e, 0x11, 0x11, 0x10, 0x11, 0x11, 0x0e],
|
||||
D: [0x1e, 0x11, 0x11, 0x11, 0x11, 0x11, 0x1e],
|
||||
E: [0x1f, 0x10, 0x10, 0x1e, 0x10, 0x10, 0x1f],
|
||||
F: [0x1f, 0x10, 0x10, 0x1e, 0x10, 0x10, 0x10],
|
||||
G: [0x0f, 0x10, 0x10, 0x17, 0x11, 0x11, 0x0f],
|
||||
H: [0x11, 0x11, 0x11, 0x1f, 0x11, 0x11, 0x11],
|
||||
I: [0x0e, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0e],
|
||||
J: [0x07, 0x02, 0x02, 0x02, 0x02, 0x12, 0x0c],
|
||||
K: [0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11],
|
||||
L: [0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1f],
|
||||
M: [0x11, 0x1b, 0x15, 0x15, 0x11, 0x11, 0x11],
|
||||
N: [0x11, 0x11, 0x19, 0x15, 0x13, 0x11, 0x11],
|
||||
O: [0x0e, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0e],
|
||||
P: [0x1e, 0x11, 0x11, 0x1e, 0x10, 0x10, 0x10],
|
||||
Q: [0x0e, 0x11, 0x11, 0x11, 0x15, 0x12, 0x0d],
|
||||
R: [0x1e, 0x11, 0x11, 0x1e, 0x14, 0x12, 0x11],
|
||||
S: [0x0e, 0x11, 0x10, 0x0e, 0x01, 0x11, 0x0e],
|
||||
T: [0x1f, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04],
|
||||
U: [0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0e],
|
||||
V: [0x11, 0x11, 0x11, 0x11, 0x11, 0x0a, 0x04],
|
||||
W: [0x11, 0x11, 0x11, 0x15, 0x15, 0x1b, 0x11],
|
||||
X: [0x11, 0x11, 0x0a, 0x04, 0x0a, 0x11, 0x11],
|
||||
Y: [0x11, 0x11, 0x0a, 0x04, 0x04, 0x04, 0x04],
|
||||
Z: [0x1f, 0x01, 0x02, 0x04, 0x08, 0x10, 0x1f],
|
||||
'0': [0x0e, 0x11, 0x13, 0x15, 0x19, 0x11, 0x0e],
|
||||
'1': [0x04, 0x0c, 0x04, 0x04, 0x04, 0x04, 0x0e],
|
||||
'2': [0x0e, 0x11, 0x01, 0x02, 0x04, 0x08, 0x1f],
|
||||
'3': [0x1f, 0x02, 0x04, 0x02, 0x01, 0x11, 0x0e],
|
||||
'4': [0x02, 0x06, 0x0a, 0x12, 0x1f, 0x02, 0x02],
|
||||
'5': [0x1f, 0x10, 0x1e, 0x01, 0x01, 0x11, 0x0e],
|
||||
'6': [0x06, 0x08, 0x10, 0x1e, 0x11, 0x11, 0x0e],
|
||||
'7': [0x1f, 0x01, 0x02, 0x04, 0x08, 0x08, 0x08],
|
||||
'8': [0x0e, 0x11, 0x11, 0x0e, 0x11, 0x11, 0x0e],
|
||||
'9': [0x0e, 0x11, 0x11, 0x0f, 0x01, 0x02, 0x0c],
|
||||
':': [0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00],
|
||||
'.': [0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00],
|
||||
',': [0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x08],
|
||||
'!': [0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x04],
|
||||
'?': [0x0e, 0x11, 0x01, 0x02, 0x04, 0x00, 0x04],
|
||||
'+': [0x00, 0x04, 0x04, 0x1f, 0x04, 0x04, 0x00],
|
||||
'-': [0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00],
|
||||
'/': [0x01, 0x02, 0x04, 0x08, 0x10, 0x10, 0x10],
|
||||
'(': [0x02, 0x04, 0x04, 0x04, 0x04, 0x04, 0x02],
|
||||
')': [0x08, 0x04, 0x04, 0x04, 0x04, 0x04, 0x08],
|
||||
' ': [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
|
||||
'|': [0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04],
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility class for rendering text using a custom pixel font.
|
||||
*/
|
||||
export const PixelFont = {
|
||||
/**
|
||||
* Render a string of text to the canvas.
|
||||
* @param ctx - The rendering context
|
||||
* @param text - The text to draw
|
||||
* @param x - Horizontal start position
|
||||
* @param y - Vertical start position
|
||||
* @param color - The color of the text
|
||||
* @param scale - Pixel scale factor
|
||||
*/
|
||||
drawText(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
text: string,
|
||||
x: number,
|
||||
y: number,
|
||||
color = '#ffffff',
|
||||
scale = 1
|
||||
): void {
|
||||
ctx.save();
|
||||
ctx.fillStyle = color;
|
||||
let cursorX = x;
|
||||
|
||||
const chars = text.toUpperCase().split('');
|
||||
chars.forEach((char) => {
|
||||
const glyph = FONT_DATA[char] || FONT_DATA['?'];
|
||||
for (let row = 0; row < 7; row++) {
|
||||
for (let col = 0; col < 5; col++) {
|
||||
if ((glyph[row] >> (4 - col)) & 1) {
|
||||
ctx.fillRect(cursorX + col * scale, y + row * scale, scale, scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
cursorX += 6 * scale;
|
||||
});
|
||||
ctx.restore();
|
||||
},
|
||||
|
||||
/**
|
||||
* Calculate the total width of a string of text when rendered.
|
||||
* @param text - The text string
|
||||
* @param scale - Pixel scale factor
|
||||
* @returns Width in pixels
|
||||
*/
|
||||
getTextWidth(text: string, scale = 1): number {
|
||||
return text.length * 6 * scale;
|
||||
},
|
||||
};
|
||||
|
|
@ -1,25 +1,41 @@
|
|||
import { EntityType, AnimationState } from './Constants.js';
|
||||
import { EntityType, AnimationState } from './Constants.ts';
|
||||
|
||||
/**
|
||||
* A 2D grid of pixel values (0-3)
|
||||
*/
|
||||
export type SpriteFrame = number[][];
|
||||
|
||||
/**
|
||||
* An array of frames forming an animation
|
||||
*/
|
||||
export type SpriteAnimation = SpriteFrame[];
|
||||
|
||||
/**
|
||||
* A map of animation states to animations
|
||||
*/
|
||||
export type EntitySpriteData = Record<string, SpriteAnimation>;
|
||||
|
||||
/**
|
||||
* Sprite Library defining pixel art grids as 2D arrays.
|
||||
*
|
||||
* Pixel Values:
|
||||
* 0: Transparent
|
||||
* 1: Primary Color (Entity Color)
|
||||
* 2: Highlight (White / Shine)
|
||||
* 3: Detail/Shade (Darker Blue / Eyes)
|
||||
*/
|
||||
export const SpriteLibrary = {
|
||||
// 8x8 Slime - Bottom-heavy blob
|
||||
export const SpriteLibrary: Record<string, EntitySpriteData> = {
|
||||
[EntityType.SLIME]: {
|
||||
[AnimationState.IDLE]: [
|
||||
[
|
||||
[0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 1, 1, 1, 1, 0, 0], // Top
|
||||
[0, 0, 1, 1, 1, 1, 0, 0],
|
||||
[0, 1, 1, 1, 1, 1, 1, 0],
|
||||
[1, 1, 2, 1, 1, 2, 1, 1], // Highlights
|
||||
[1, 1, 3, 1, 1, 3, 1, 1], // Eyes
|
||||
[1, 1, 2, 1, 1, 2, 1, 1],
|
||||
[1, 1, 3, 1, 1, 3, 1, 1],
|
||||
[1, 1, 1, 1, 1, 1, 1, 1],
|
||||
[1, 1, 1, 1, 1, 1, 1, 1],
|
||||
[0, 1, 1, 1, 1, 1, 1, 0] // Flat-ish base
|
||||
[0, 1, 1, 1, 1, 1, 1, 0],
|
||||
],
|
||||
[
|
||||
[0, 0, 0, 0, 0, 0, 0, 0],
|
||||
|
|
@ -29,8 +45,8 @@ export const SpriteLibrary = {
|
|||
[1, 1, 2, 1, 1, 2, 1, 1],
|
||||
[1, 1, 3, 1, 1, 3, 1, 1],
|
||||
[1, 1, 1, 1, 1, 1, 1, 1],
|
||||
[1, 1, 1, 1, 1, 1, 1, 1] // Squashed base
|
||||
]
|
||||
[1, 1, 1, 1, 1, 1, 1, 1],
|
||||
],
|
||||
],
|
||||
[AnimationState.WALK]: [
|
||||
[
|
||||
|
|
@ -41,7 +57,7 @@ export const SpriteLibrary = {
|
|||
[1, 1, 1, 1, 1, 1, 1, 1],
|
||||
[1, 1, 1, 1, 1, 1, 1, 1],
|
||||
[1, 3, 1, 1, 1, 1, 3, 1],
|
||||
[0, 1, 1, 1, 1, 1, 1, 0]
|
||||
[0, 1, 1, 1, 1, 1, 1, 0],
|
||||
],
|
||||
[
|
||||
[0, 0, 0, 0, 0, 0, 0, 0],
|
||||
|
|
@ -51,12 +67,11 @@ export const SpriteLibrary = {
|
|||
[1, 1, 3, 1, 1, 3, 1, 1],
|
||||
[1, 1, 1, 1, 1, 1, 1, 1],
|
||||
[1, 1, 1, 1, 1, 1, 1, 1],
|
||||
[1, 1, 1, 1, 1, 1, 1, 1]
|
||||
]
|
||||
]
|
||||
[1, 1, 1, 1, 1, 1, 1, 1],
|
||||
],
|
||||
],
|
||||
},
|
||||
|
||||
// 8x8 Humanoid - Simple Walk Cycle
|
||||
[EntityType.HUMANOID]: {
|
||||
[AnimationState.IDLE]: [
|
||||
[
|
||||
|
|
@ -67,8 +82,8 @@ export const SpriteLibrary = {
|
|||
[1, 0, 2, 1, 1, 2, 0, 1],
|
||||
[1, 0, 1, 1, 1, 1, 0, 1],
|
||||
[0, 0, 1, 0, 0, 1, 0, 0],
|
||||
[0, 0, 1, 0, 0, 1, 0, 0]
|
||||
]
|
||||
[0, 0, 1, 0, 0, 1, 0, 0],
|
||||
],
|
||||
],
|
||||
[AnimationState.WALK]: [
|
||||
[
|
||||
|
|
@ -79,7 +94,7 @@ export const SpriteLibrary = {
|
|||
[0, 0, 2, 1, 1, 2, 0, 1],
|
||||
[0, 0, 1, 1, 1, 1, 0, 1],
|
||||
[0, 0, 1, 0, 0, 0, 0, 0],
|
||||
[0, 1, 1, 0, 0, 0, 0, 0]
|
||||
[0, 1, 1, 0, 0, 0, 0, 0],
|
||||
],
|
||||
[
|
||||
[0, 0, 0, 1, 1, 0, 0, 0],
|
||||
|
|
@ -89,12 +104,11 @@ export const SpriteLibrary = {
|
|||
[1, 0, 2, 1, 1, 2, 0, 0],
|
||||
[1, 0, 1, 1, 1, 1, 0, 0],
|
||||
[0, 0, 0, 0, 0, 1, 0, 0],
|
||||
[0, 0, 0, 0, 0, 1, 1, 0]
|
||||
]
|
||||
]
|
||||
[0, 0, 0, 0, 0, 1, 1, 0],
|
||||
],
|
||||
],
|
||||
},
|
||||
|
||||
// 8x8 Beast - Bounding Cycle
|
||||
[EntityType.BEAST]: {
|
||||
[AnimationState.IDLE]: [
|
||||
[
|
||||
|
|
@ -105,8 +119,8 @@ export const SpriteLibrary = {
|
|||
[1, 1, 1, 1, 1, 1, 1, 1],
|
||||
[0, 1, 1, 2, 2, 1, 1, 0],
|
||||
[0, 1, 0, 0, 0, 0, 1, 0],
|
||||
[0, 1, 0, 0, 0, 0, 1, 0]
|
||||
]
|
||||
[0, 1, 0, 0, 0, 0, 1, 0],
|
||||
],
|
||||
],
|
||||
[AnimationState.WALK]: [
|
||||
[
|
||||
|
|
@ -117,7 +131,7 @@ export const SpriteLibrary = {
|
|||
[0, 1, 1, 2, 2, 1, 1, 0],
|
||||
[0, 1, 0, 0, 0, 0, 1, 0],
|
||||
[1, 1, 0, 0, 0, 0, 1, 1],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0]
|
||||
[0, 0, 0, 0, 0, 0, 0, 0],
|
||||
],
|
||||
[
|
||||
[0, 0, 0, 0, 0, 0, 0, 0],
|
||||
|
|
@ -127,12 +141,11 @@ export const SpriteLibrary = {
|
|||
[1, 1, 1, 1, 1, 1, 1, 1],
|
||||
[0, 1, 1, 2, 2, 1, 1, 0],
|
||||
[0, 0, 1, 0, 0, 1, 0, 0],
|
||||
[0, 0, 1, 0, 0, 1, 0, 0]
|
||||
]
|
||||
]
|
||||
[0, 0, 1, 0, 0, 1, 0, 0],
|
||||
],
|
||||
],
|
||||
},
|
||||
|
||||
// 8x8 Elemental - Floating Pulse
|
||||
[EntityType.ELEMENTAL]: {
|
||||
[AnimationState.IDLE]: [
|
||||
[
|
||||
|
|
@ -143,7 +156,7 @@ export const SpriteLibrary = {
|
|||
[1, 1, 1, 3, 3, 1, 1, 1],
|
||||
[0, 1, 1, 1, 1, 1, 1, 0],
|
||||
[0, 0, 1, 1, 1, 1, 0, 0],
|
||||
[0, 0, 0, 1, 1, 0, 0, 0]
|
||||
[0, 0, 0, 1, 1, 0, 0, 0],
|
||||
],
|
||||
[
|
||||
[0, 0, 0, 0, 0, 0, 0, 0],
|
||||
|
|
@ -153,17 +166,17 @@ export const SpriteLibrary = {
|
|||
[1, 1, 1, 3, 3, 1, 1, 1],
|
||||
[0, 1, 1, 1, 1, 1, 1, 0],
|
||||
[0, 0, 1, 1, 1, 1, 0, 0],
|
||||
[0, 0, 0, 1, 1, 0, 0, 0]
|
||||
]
|
||||
]
|
||||
[0, 0, 0, 1, 1, 0, 0, 0],
|
||||
],
|
||||
],
|
||||
},
|
||||
|
||||
[EntityType.PROJECTILE]: {
|
||||
[AnimationState.IDLE]: [
|
||||
[
|
||||
[1, 1],
|
||||
[1, 1]
|
||||
]
|
||||
]
|
||||
}
|
||||
[1, 1],
|
||||
],
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
/**
|
||||
* Base System class for ECS architecture
|
||||
* Systems contain logic that operates on entities with specific components
|
||||
*/
|
||||
export class System {
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
this.requiredComponents = [];
|
||||
this.priority = 0; // Lower priority runs first
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an entity matches this system's requirements
|
||||
*/
|
||||
matches(entity) {
|
||||
if (!entity.active) return false;
|
||||
return this.requiredComponents.every(componentType =>
|
||||
entity.hasComponent(componentType)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update method - override in subclasses
|
||||
*/
|
||||
update(deltaTime, entities) {
|
||||
// Filter entities that match this system's requirements
|
||||
const matchingEntities = entities.filter(entity => this.matches(entity));
|
||||
this.process(deltaTime, matchingEntities);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process matching entities - override in subclasses
|
||||
*/
|
||||
process(_deltaTime, _entities) {
|
||||
// Override in subclasses
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when system is added to engine
|
||||
*/
|
||||
init(engine) {
|
||||
this.engine = engine;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
64
src/core/System.ts
Normal file
64
src/core/System.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { Entity } from './Entity.ts';
|
||||
import type { Engine } from './Engine.ts';
|
||||
|
||||
/**
|
||||
* Base System class for ECS architecture.
|
||||
* Systems contain logic that operates on entities with specific components.
|
||||
*/
|
||||
export class System {
|
||||
/** Unique identifier for the system */
|
||||
readonly name: string;
|
||||
|
||||
/** List of component types required by this system */
|
||||
requiredComponents: string[];
|
||||
|
||||
/** Execution priority (lower runs first) */
|
||||
priority: number;
|
||||
|
||||
/** Reference to the game engine */
|
||||
protected engine!: Engine;
|
||||
|
||||
/**
|
||||
* @param name - The unique name of the system
|
||||
*/
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
this.requiredComponents = [];
|
||||
this.priority = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an entity matches this system's requirements.
|
||||
* @param entity - The entity to check
|
||||
* @returns True if the entity is active and has all required components
|
||||
*/
|
||||
matches(entity: Entity): boolean {
|
||||
if (!entity.active) return false;
|
||||
return this.requiredComponents.every((componentType) => entity.hasComponent(componentType));
|
||||
}
|
||||
|
||||
/**
|
||||
* Main update entry point called every frame.
|
||||
* @param deltaTime - Time elapsed since last frame in seconds
|
||||
* @param entities - All entities in the engine
|
||||
*/
|
||||
update(deltaTime: number, entities: Entity[]): void {
|
||||
const matchingEntities = entities.filter((entity) => this.matches(entity));
|
||||
this.process(deltaTime, matchingEntities);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process matching entities. To be implemented by subclasses.
|
||||
* @param _deltaTime - Time elapsed since last frame in seconds
|
||||
* @param _entities - Filtered entities that match this system
|
||||
*/
|
||||
process(_deltaTime: number, _entities: Entity[]): void {}
|
||||
|
||||
/**
|
||||
* Called when system is added to engine.
|
||||
* @param engine - The game engine instance
|
||||
*/
|
||||
init(engine: Engine): void {
|
||||
this.engine = engine;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
export class TileMap {
|
||||
constructor(cols, rows, tileSize) {
|
||||
this.cols = cols;
|
||||
this.rows = rows;
|
||||
this.tileSize = tileSize;
|
||||
this.tiles = new Array(cols * rows).fill(0);
|
||||
}
|
||||
|
||||
setTile(col, row, value) {
|
||||
if (this.isValid(col, row)) {
|
||||
this.tiles[row * this.cols + col] = value;
|
||||
}
|
||||
}
|
||||
|
||||
getTile(col, row) {
|
||||
if (this.isValid(col, row)) {
|
||||
return this.tiles[row * this.cols + col];
|
||||
}
|
||||
return 1; // Treat out of bounds as solid wall
|
||||
}
|
||||
|
||||
isValid(col, row) {
|
||||
return col >= 0 && col < this.cols && row >= 0 && row < this.rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a world position collides with a solid tile
|
||||
*/
|
||||
isSolid(x, y) {
|
||||
const col = Math.floor(x / this.tileSize);
|
||||
const row = Math.floor(y / this.tileSize);
|
||||
return this.getTile(col, row) !== 0;
|
||||
}
|
||||
}
|
||||
68
src/core/TileMap.ts
Normal file
68
src/core/TileMap.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
/**
|
||||
* Class representing a grid-based tile map for the game level.
|
||||
*/
|
||||
export class TileMap {
|
||||
cols: number;
|
||||
rows: number;
|
||||
tileSize: number;
|
||||
tiles: number[];
|
||||
|
||||
/**
|
||||
* @param cols - Number of columns
|
||||
* @param rows - Number of rows
|
||||
* @param tileSize - Size of each tile in pixels
|
||||
*/
|
||||
constructor(cols: number, rows: number, tileSize: number) {
|
||||
this.cols = cols;
|
||||
this.rows = rows;
|
||||
this.tileSize = tileSize;
|
||||
this.tiles = new Array(cols * rows).fill(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value of a specific tile.
|
||||
* @param col - Tile column
|
||||
* @param row - Tile row
|
||||
* @param value - Tile type value
|
||||
*/
|
||||
setTile(col: number, row: number, value: number): void {
|
||||
if (this.isValid(col, row)) {
|
||||
this.tiles[row * this.cols + col] = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the type value of a tile. Treats out-of-bounds as solid.
|
||||
* @param col - Tile column
|
||||
* @param row - Tile row
|
||||
* @returns The tile type value
|
||||
*/
|
||||
getTile(col: number, row: number): number {
|
||||
if (this.isValid(col, row)) {
|
||||
return this.tiles[row * this.cols + col];
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tile coordinate is within the map boundaries.
|
||||
* @param col - Tile column
|
||||
* @param row - Tile row
|
||||
* @returns True if coordinates are valid
|
||||
*/
|
||||
isValid(col: number, row: number): boolean {
|
||||
return col >= 0 && col < this.cols && row >= 0 && row < this.rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a world position (x, y) collides with a solid tile.
|
||||
* @param x - World X coordinate
|
||||
* @param y - World Y coordinate
|
||||
* @returns True if the position is solid
|
||||
*/
|
||||
isSolid(x: number, y: number): boolean {
|
||||
const col = Math.floor(x / this.tileSize);
|
||||
const row = Math.floor(y / this.tileSize);
|
||||
return this.getTile(col, row) !== 0;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue