From cf0467751197603cad38dd09254eb0e6ab74640f Mon Sep 17 00:00:00 2001 From: Juan Sebastian Montoya Date: Tue, 6 Jan 2026 17:21:15 -0500 Subject: [PATCH 1/3] feat: Implement pixel-art rendering with new level loading, tile maps, palettes, and pixel fonts, alongside a game over screen. --- .gitignore | 1 + index.html | 31 ++- src/GameConfig.js | 2 +- src/components/AI.js | 11 +- src/components/Absorbable.js | 1 + src/components/Combat.js | 9 +- src/components/Evolution.js | 1 + src/components/Inventory.js | 1 + src/components/Position.js | 1 + src/components/SkillProgress.js | 1 + src/components/Skills.js | 1 + src/components/Sprite.js | 7 +- src/components/Stats.js | 1 + src/components/Stealth.js | 1 + src/components/Velocity.js | 1 + src/core/Component.js | 1 + src/core/Engine.js | 59 +++-- src/core/Entity.js | 1 + src/core/LevelLoader.js | 22 ++ src/core/Palette.js | 27 ++ src/core/PixelFont.js | 80 ++++++ src/core/SpriteLibrary.js | 167 +++++++++++++ src/core/System.js | 1 + src/core/TileMap.js | 34 +++ src/items/Item.js | 1 + src/items/ItemRegistry.js | 1 + src/main.js | 20 +- src/skills/Skill.js | 1 + src/skills/skills/StealthMode.js | 1 + src/skills/skills/WaterGun.js | 10 +- src/systems/AISystem.js | 9 +- src/systems/DeathSystem.js | 10 +- src/systems/HealthRegenerationSystem.js | 1 + src/systems/InputSystem.js | 32 +-- src/systems/MenuSystem.js | 70 ++++-- src/systems/MovementSystem.js | 23 +- src/systems/PlayerControllerSystem.js | 3 +- src/systems/RenderSystem.js | 317 ++++++++++++++---------- src/systems/SkillEffectSystem.js | 1 + src/systems/UISystem.js | 161 +++++------- src/world/World.js | 1 + 41 files changed, 793 insertions(+), 331 deletions(-) create mode 100644 src/core/LevelLoader.js create mode 100644 src/core/Palette.js create mode 100644 src/core/PixelFont.js create mode 100644 src/core/SpriteLibrary.js create mode 100644 src/core/TileMap.js diff --git a/.gitignore b/.gitignore index a9bf588..15655b1 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ dist/ *.log .vite/ + diff --git a/index.html b/index.html index 21eb741..5a04747 100644 --- a/index.html +++ b/index.html @@ -1,31 +1,56 @@ + Slime Genesis - PoC +
- + \ No newline at end of file diff --git a/src/GameConfig.js b/src/GameConfig.js index 4995f4f..b80c407 100644 --- a/src/GameConfig.js +++ b/src/GameConfig.js @@ -13,7 +13,7 @@ export const GameConfig = { }, Absorption: { - range: 80, + range: 30, // Scaled down healPercentMin: 0.1, healPercentMax: 0.2, skillAbsorptionChance: 0.3, diff --git a/src/components/AI.js b/src/components/AI.js index dbe67f1..845570b 100644 --- a/src/components/AI.js +++ b/src/components/AI.js @@ -7,12 +7,12 @@ export class AI extends Component { this.state = 'idle'; // 'idle', 'moving', 'attacking', 'fleeing' this.target = null; // Entity ID to target this.awareness = 0; // 0-1, how aware of player - this.alertRadius = 150; - this.chaseRadius = 300; - this.fleeRadius = 100; - + this.alertRadius = 60; // Scaled for 320x240 + this.chaseRadius = 120; + this.fleeRadius = 40; + // Behavior parameters - this.wanderSpeed = 50; + this.wanderSpeed = 20; // Slower wander this.wanderDirection = Math.random() * Math.PI * 2; this.wanderChangeTime = 0; this.wanderChangeInterval = 2.0; // seconds @@ -50,3 +50,4 @@ export class AI extends Component { } } + diff --git a/src/components/Absorbable.js b/src/components/Absorbable.js index ef4b6f0..018f578 100644 --- a/src/components/Absorbable.js +++ b/src/components/Absorbable.js @@ -52,3 +52,4 @@ export class Absorbable extends Component { } } + diff --git a/src/components/Combat.js b/src/components/Combat.js index 516caa0..c518a89 100644 --- a/src/components/Combat.js +++ b/src/components/Combat.js @@ -6,10 +6,10 @@ export class Combat extends Component { this.attackDamage = 10; this.defense = 5; this.attackSpeed = 1.0; // Attacks per second - this.attackRange = 50; + this.attackRange = 15; // Melee range for pixel art this.lastAttackTime = 0; this.attackCooldown = 0; - + // Combat state this.isAttacking = false; this.attackDirection = 0; // Angle in radians @@ -28,12 +28,12 @@ export class Combat extends Component { */ attack(currentTime, direction) { if (!this.canAttack(currentTime)) return false; - + this.lastAttackTime = currentTime; this.isAttacking = true; this.attackDirection = direction; this.attackCooldown = 0.3; // Attack animation duration - + return true; } @@ -50,3 +50,4 @@ export class Combat extends Component { } } + diff --git a/src/components/Evolution.js b/src/components/Evolution.js index e3a9c63..83445db 100644 --- a/src/components/Evolution.js +++ b/src/components/Evolution.js @@ -103,3 +103,4 @@ export class Evolution extends Component { } } + diff --git a/src/components/Inventory.js b/src/components/Inventory.js index 7b619d7..3e67352 100644 --- a/src/components/Inventory.js +++ b/src/components/Inventory.js @@ -69,3 +69,4 @@ export class Inventory extends Component { } } + diff --git a/src/components/Position.js b/src/components/Position.js index 5713b4b..278e144 100644 --- a/src/components/Position.js +++ b/src/components/Position.js @@ -9,3 +9,4 @@ export class Position extends Component { } } + diff --git a/src/components/SkillProgress.js b/src/components/SkillProgress.js index cb27042..5da00e8 100644 --- a/src/components/SkillProgress.js +++ b/src/components/SkillProgress.js @@ -43,3 +43,4 @@ export class SkillProgress extends Component { } } + diff --git a/src/components/Skills.js b/src/components/Skills.js index 6c1869f..f50fc14 100644 --- a/src/components/Skills.js +++ b/src/components/Skills.js @@ -67,3 +67,4 @@ export class Skills extends Component { } } + diff --git a/src/components/Sprite.js b/src/components/Sprite.js index 94ab18c..65306f0 100644 --- a/src/components/Sprite.js +++ b/src/components/Sprite.js @@ -9,10 +9,13 @@ export class Sprite extends Component { this.shape = shape; // 'circle', 'rect', 'slime' this.alpha = 1.0; this.scale = 1.0; - + // Animation properties this.animationTime = 0; - this.morphAmount = 0; // For slime morphing + this.animationState = 'idle'; // 'idle', 'walk' + this.animationSpeed = 4; // frames per second + this.morphAmount = 0; // Legacy slime morphing } } + diff --git a/src/components/Stats.js b/src/components/Stats.js index 9c22ee0..2ece52a 100644 --- a/src/components/Stats.js +++ b/src/components/Stats.js @@ -53,3 +53,4 @@ export class Stats extends Component { } } + diff --git a/src/components/Stealth.js b/src/components/Stealth.js index f1de5ad..43533c2 100644 --- a/src/components/Stealth.js +++ b/src/components/Stealth.js @@ -46,3 +46,4 @@ export class Stealth extends Component { } } + diff --git a/src/components/Velocity.js b/src/components/Velocity.js index d6ebd46..67a367a 100644 --- a/src/components/Velocity.js +++ b/src/components/Velocity.js @@ -9,3 +9,4 @@ export class Velocity extends Component { } } + diff --git a/src/core/Component.js b/src/core/Component.js index b44eac9..88ba0d7 100644 --- a/src/core/Component.js +++ b/src/core/Component.js @@ -12,3 +12,4 @@ export class Component { } } + diff --git a/src/core/Engine.js b/src/core/Engine.js index f9720cc..d5a68a3 100644 --- a/src/core/Engine.js +++ b/src/core/Engine.js @@ -1,6 +1,7 @@ import { System } from './System.js'; import { Entity } from './Entity.js'; import { EventBus } from './EventBus.js'; +import { LevelLoader } from './LevelLoader.js'; /** * Main game engine - manages ECS, game loop, and systems @@ -15,17 +16,27 @@ export class Engine { this.running = false; this.lastTime = 0; - // Set canvas size - this.canvas.width = 1024; - this.canvas.height = 768; + // 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 - */ + * Add a system to the engine + */ addSystem(system) { if (system instanceof System) { system.init(this); @@ -37,22 +48,22 @@ export class Engine { } /** - * Emit an event locally - */ + * Emit an event locally + */ emit(event, data) { this.events.emit(event, data); } /** - * Subscribe to an event - */ + * Subscribe to an event + */ on(event, callback) { return this.events.on(event, callback); } /** - * Create and add an entity - */ + * Create and add an entity + */ createEntity() { const entity = new Entity(); this.entities.push(entity); @@ -60,8 +71,8 @@ export class Engine { } /** - * Remove an entity - */ + * Remove an entity + */ removeEntity(entity) { const index = this.entities.indexOf(entity); if (index > -1) { @@ -70,15 +81,15 @@ export class Engine { } /** - * Get all entities - */ + * Get all entities + */ getEntities() { return this.entities.filter(e => e.active); } /** - * Main game loop - */ + * Main game loop + */ start() { if (this.running) return; this.running = true; @@ -87,15 +98,15 @@ export class Engine { } /** - * Stop the game loop - */ + * Stop the game loop + */ stop() { this.running = false; } /** - * Game loop using requestAnimationFrame - */ + * Game loop using requestAnimationFrame + */ gameLoop = (currentTime = 0) => { if (!this.running) return; @@ -109,7 +120,7 @@ export class Engine { // Update all systems const menuSystem = this.systems.find(s => s.name === 'MenuSystem'); const gameState = menuSystem ? menuSystem.getGameState() : 'playing'; - const isPaused = gameState === 'paused' || gameState === 'start'; + const isPaused = gameState === 'paused' || gameState === 'start' || gameState === 'gameOver'; this.systems.forEach(system => { // Skip game systems if paused/start menu (but allow MenuSystem, UISystem, and RenderSystem) @@ -130,8 +141,8 @@ export class Engine { } /** - * Clear the canvas - */ + * Clear the canvas + */ clear() { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); } diff --git a/src/core/Entity.js b/src/core/Entity.js index 0671938..e0edf56 100644 --- a/src/core/Entity.js +++ b/src/core/Entity.js @@ -56,3 +56,4 @@ export class Entity { } } + diff --git a/src/core/LevelLoader.js b/src/core/LevelLoader.js new file mode 100644 index 0000000..26fed56 --- /dev/null +++ b/src/core/LevelLoader.js @@ -0,0 +1,22 @@ +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; + } +} diff --git a/src/core/Palette.js b/src/core/Palette.js new file mode 100644 index 0000000..84aa67a --- /dev/null +++ b/src/core/Palette.js @@ -0,0 +1,27 @@ +/** + * 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 + ]; + } +}; diff --git a/src/core/PixelFont.js b/src/core/PixelFont.js new file mode 100644 index 0000000..f8d5e88 --- /dev/null +++ b/src/core/PixelFont.js @@ -0,0 +1,80 @@ +/** + * 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; + } +}; diff --git a/src/core/SpriteLibrary.js b/src/core/SpriteLibrary.js new file mode 100644 index 0000000..748b788 --- /dev/null +++ b/src/core/SpriteLibrary.js @@ -0,0 +1,167 @@ +/** + * Sprite Library defining pixel art grids as 2D arrays. + * 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 + slime: { + idle: [ + [ + [0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 0, 0], // Top + [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, 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, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 0], + [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 + ] + ], + walk: [ + [ + [0, 0, 1, 1, 1, 1, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 0], + [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], + [1, 3, 1, 1, 1, 1, 3, 1], + [0, 1, 1, 1, 1, 1, 1, 0] + ], + [ + [0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 0], + [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], + [1, 1, 1, 1, 1, 1, 1, 1] + ] + ] + }, + + // 8x8 Humanoid - Simple Walk Cycle + humanoid: { + idle: [ + [ + [0, 0, 0, 1, 1, 0, 0, 0], + [0, 0, 2, 1, 1, 2, 0, 0], + [0, 0, 0, 1, 1, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 0], + [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] + ] + ], + walk: [ + [ + [0, 0, 0, 1, 1, 0, 0, 0], + [0, 0, 2, 1, 1, 2, 0, 0], + [0, 0, 0, 1, 1, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 0], + [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, 0, 0, 1, 1, 0, 0, 0], + [0, 0, 2, 1, 1, 2, 0, 0], + [0, 0, 0, 1, 1, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 0], + [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] + ] + ] + }, + + // 8x8 Beast - Bounding Cycle + beast: { + idle: [ + [ + [0, 0, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 0, 1], + [0, 1, 1, 1, 1, 1, 1, 0], + [1, 3, 1, 1, 1, 1, 3, 1], + [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] + ] + ], + walk: [ + [ + [1, 0, 0, 0, 0, 0, 0, 1], + [0, 1, 1, 1, 1, 1, 1, 0], + [1, 3, 1, 1, 1, 1, 3, 1], + [1, 1, 1, 1, 1, 1, 1, 1], + [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], + [1, 0, 0, 0, 0, 0, 0, 1], + [0, 1, 1, 1, 1, 1, 1, 0], + [1, 3, 1, 1, 1, 1, 3, 1], + [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] + ] + ] + }, + + // 8x8 Elemental - Floating Pulse + elemental: { + idle: [ + [ + [0, 0, 2, 1, 1, 2, 0, 0], + [0, 1, 1, 2, 2, 1, 1, 0], + [1, 2, 1, 1, 1, 1, 2, 1], + [1, 1, 3, 3, 3, 3, 1, 1], + [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, 0, 0, 0, 0, 0], + [0, 0, 2, 1, 1, 2, 0, 0], + [0, 1, 1, 2, 2, 1, 1, 0], + [1, 2, 3, 3, 3, 3, 2, 1], + [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] + ] + ] + }, + + projectile: { + idle: [ + [ + [1, 1], + [1, 1] + ] + ] + } +}; diff --git a/src/core/System.js b/src/core/System.js index 6b18131..17429bd 100644 --- a/src/core/System.js +++ b/src/core/System.js @@ -43,3 +43,4 @@ export class System { } } + diff --git a/src/core/TileMap.js b/src/core/TileMap.js new file mode 100644 index 0000000..74d0ec2 --- /dev/null +++ b/src/core/TileMap.js @@ -0,0 +1,34 @@ +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; + } +} diff --git a/src/items/Item.js b/src/items/Item.js index bbf314b..e320c62 100644 --- a/src/items/Item.js +++ b/src/items/Item.js @@ -11,3 +11,4 @@ export class Item { } } + diff --git a/src/items/ItemRegistry.js b/src/items/ItemRegistry.js index 91e44cb..b09bced 100644 --- a/src/items/ItemRegistry.js +++ b/src/items/ItemRegistry.js @@ -51,3 +51,4 @@ export class ItemRegistry { } } + diff --git a/src/main.js b/src/main.js index 7c6f278..8d2dd8c 100644 --- a/src/main.js +++ b/src/main.js @@ -56,9 +56,9 @@ if (!canvas) { // Create player entity const player = engine.createEntity(); - player.addComponent(new Position(512, 384)); - player.addComponent(new Velocity(0, 0, 200)); - player.addComponent(new Sprite('#00ff96', 40, 40, 'slime')); + player.addComponent(new Position(160, 120)); // Center of 320x240 + player.addComponent(new Velocity(0, 0, 100)); // Slower speed for small resolution + player.addComponent(new Sprite('#00ff96', 14, 14, 'slime')); // 14x14 pixel sprite player.addComponent(new Health(100)); player.addComponent(new Stats()); player.addComponent(new Evolution()); @@ -77,7 +77,7 @@ if (!canvas) { function createCreature(engine, x, y, type) { const creature = engine.createEntity(); creature.addComponent(new Position(x, y)); - creature.addComponent(new Velocity(0, 0, 100)); + creature.addComponent(new Velocity(0, 0, 50)); // Slower speed let color, evolutionData, skills; @@ -103,8 +103,8 @@ if (!canvas) { skills = []; } - creature.addComponent(new Sprite(color, 25, 25, 'circle')); - creature.addComponent(new Health(50 + Math.random() * 30)); + creature.addComponent(new Sprite(color, 10, 10, type)); + creature.addComponent(new Health(15 + Math.random() * 10)); // Adjusted health for smaller enemies creature.addComponent(new Stats()); creature.addComponent(new Combat()); creature.addComponent(new AI('wander')); @@ -119,8 +119,8 @@ if (!canvas) { // Spawn initial creatures for (let i = 0; i < 8; i++) { - const x = 100 + Math.random() * 824; - const y = 100 + Math.random() * 568; + const x = 20 + Math.random() * 280; // Fit in 320 width + const y = 20 + Math.random() * 200; // Fit in 240 height const types = ['humanoid', 'beast', 'elemental']; const type = types[Math.floor(Math.random() * types.length)]; createCreature(engine, x, y, type); @@ -133,8 +133,8 @@ if (!canvas) { ); if (existingCreatures.length < 10) { - const x = 100 + Math.random() * 824; - const y = 100 + Math.random() * 568; + const x = 20 + Math.random() * 280; + const y = 20 + Math.random() * 200; const types = ['humanoid', 'beast', 'elemental']; const type = types[Math.floor(Math.random() * types.length)]; createCreature(engine, x, y, type); diff --git a/src/skills/Skill.js b/src/skills/Skill.js index 2ed22ee..f0abfe4 100644 --- a/src/skills/Skill.js +++ b/src/skills/Skill.js @@ -30,3 +30,4 @@ export class Skill { return !skills.isOnCooldown(this.id); } } + diff --git a/src/skills/skills/StealthMode.js b/src/skills/skills/StealthMode.js index 368104b..f0971fd 100644 --- a/src/skills/skills/StealthMode.js +++ b/src/skills/skills/StealthMode.js @@ -33,3 +33,4 @@ export class StealthMode extends Skill { } } + diff --git a/src/skills/skills/WaterGun.js b/src/skills/skills/WaterGun.js index e5341e3..9826b6f 100644 --- a/src/skills/skills/WaterGun.js +++ b/src/skills/skills/WaterGun.js @@ -9,8 +9,8 @@ export class SlimeGun extends Skill { super('slime_gun', 'Slime Gun', 1.0); this.description = 'Shoot a blob of slime at enemies (costs 1 HP)'; this.damage = 15; - this.range = 800; // Long range for a gun - this.speed = 600; // Faster projectile + this.range = 250; // Screen width approx + this.speed = 250; // Faster than player but readable this.hpCost = 1; } @@ -52,7 +52,7 @@ export class SlimeGun extends Skill { const startX = position.x; const startY = position.y; projectile.addComponent(new Position(startX, startY)); - + // Create velocity with high maxSpeed for projectiles const projectileVelocity = new Velocity( Math.cos(shootAngle) * this.speed, @@ -61,8 +61,8 @@ export class SlimeGun extends Skill { projectileVelocity.maxSpeed = this.speed * 2; // Allow projectiles to move fast projectile.addComponent(projectileVelocity); // Slime-colored projectile - projectile.addComponent(new Sprite('#00ff96', 10, 10, 'slime')); - + projectile.addComponent(new Sprite('#00ff96', 4, 4, 'projectile')); + // Projectile has temporary health for collision detection const projectileHealth = new Health(1); projectileHealth.isProjectile = true; diff --git a/src/systems/AISystem.js b/src/systems/AISystem.js index c156861..6e49588 100644 --- a/src/systems/AISystem.js +++ b/src/systems/AISystem.js @@ -15,13 +15,20 @@ export class AISystem extends System { const config = GameConfig.AI; entities.forEach(entity => { + const health = entity.getComponent('Health'); const ai = entity.getComponent('AI'); const position = entity.getComponent('Position'); const velocity = entity.getComponent('Velocity'); - const _stealth = entity.getComponent('Stealth'); if (!ai || !position || !velocity) return; + // Stop movement for dead entities + if (health && health.isDead() && !health.isProjectile) { + velocity.vx = 0; + velocity.vy = 0; + return; + } + // Update wander timer ai.wanderChangeTime += deltaTime; diff --git a/src/systems/DeathSystem.js b/src/systems/DeathSystem.js index b5908b7..1bffa95 100644 --- a/src/systems/DeathSystem.js +++ b/src/systems/DeathSystem.js @@ -24,9 +24,15 @@ export class DeathSystem extends System { // Check if entity is dead if (health.isDead()) { - // Don't remove player + // Check if player died const evolution = entity.getComponent('Evolution'); - if (evolution) return; // Player has Evolution component + if (evolution) { + const menuSystem = this.engine.systems.find(s => s.name === 'MenuSystem'); + if (menuSystem) { + menuSystem.showGameOver(); + } + return; + } // Mark as inactive immediately so it stops being processed by other systems if (entity.active) { diff --git a/src/systems/HealthRegenerationSystem.js b/src/systems/HealthRegenerationSystem.js index 47468be..c8d3d0e 100644 --- a/src/systems/HealthRegenerationSystem.js +++ b/src/systems/HealthRegenerationSystem.js @@ -25,3 +25,4 @@ export class HealthRegenerationSystem extends System { } } + diff --git a/src/systems/InputSystem.js b/src/systems/InputSystem.js index 9a45fc6..b5b0d77 100644 --- a/src/systems/InputSystem.js +++ b/src/systems/InputSystem.js @@ -76,8 +76,12 @@ export class InputSystem extends System { if (this.engine && this.engine.canvas) { const canvas = this.engine.canvas; const rect = canvas.getBoundingClientRect(); - this.mouse.x = e.clientX - rect.left; - this.mouse.y = e.clientY - rect.top; + // Calculate scale factors between displayed size and internal resolution + const scaleX = canvas.width / rect.width; + const scaleY = canvas.height / rect.height; + + this.mouse.x = (e.clientX - rect.left) * scaleX; + this.mouse.y = (e.clientY - rect.top) * scaleY; } }); @@ -96,8 +100,8 @@ export class InputSystem extends System { } /** - * Update previous states - called at end of frame - */ + * Update previous states - called at end of frame + */ updatePreviousStates() { // Deep copy current states to previous for next frame this.keysPrevious = {}; @@ -111,15 +115,15 @@ export class InputSystem extends System { } /** - * Check if a key is currently pressed - */ + * Check if a key is currently pressed + */ isKeyPressed(key) { return this.keys[key.toLowerCase()] === true; } /** - * Check if a key was just pressed (not held from previous frame) - */ + * Check if a key was just pressed (not held from previous frame) + */ isKeyJustPressed(key) { const keyLower = key.toLowerCase(); const isPressed = this.keys[keyLower] === true; @@ -128,22 +132,22 @@ export class InputSystem extends System { } /** - * Get mouse position - */ + * Get mouse position + */ getMousePosition() { return { x: this.mouse.x, y: this.mouse.y }; } /** - * Check if mouse button is pressed - */ + * Check if mouse button is pressed + */ isMouseButtonPressed(button = 0) { return this.mouse.buttons[button] === true; } /** - * Check if mouse button was just pressed - */ + * Check if mouse button was just pressed + */ isMouseButtonJustPressed(button = 0) { const isPressed = this.mouse.buttons[button] === true; const wasPressed = this.mouse.buttonsPrevious[button] === true; diff --git a/src/systems/MenuSystem.js b/src/systems/MenuSystem.js index 4c44103..ca90856 100644 --- a/src/systems/MenuSystem.js +++ b/src/systems/MenuSystem.js @@ -1,4 +1,6 @@ import { System } from '../core/System.js'; +import { PixelFont } from '../core/PixelFont.js'; +import { Palette } from '../core/Palette.js'; /** * System to handle game menus (start, pause) @@ -31,11 +33,22 @@ export class MenuSystem extends System { this.startGame(); } else if (this.gameState === 'paused') { this.resumeGame(); + } else if (this.gameState === 'gameOver') { + this.restartGame(); } } }); } + showGameOver() { + this.gameState = 'gameOver'; + this.paused = true; + } + + restartGame() { + window.location.reload(); // Simple and effective for this project + } + startGame() { this.gameState = 'playing'; this.paused = false; @@ -75,27 +88,50 @@ export class MenuSystem extends System { const width = this.engine.canvas.width; const height = this.engine.canvas.height; - // Dark overlay - ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; + // Darker overlay matching palette + ctx.fillStyle = 'rgba(32, 21, 51, 0.8)'; // Semi-transparent VOID ctx.fillRect(0, 0, width, height); - ctx.fillStyle = '#ffffff'; - ctx.font = 'bold 48px Courier New'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - if (this.gameState === 'start') { - ctx.fillText('SLIME GENESIS', width / 2, height / 2 - 100); - ctx.font = '24px Courier New'; - ctx.fillText('Press ENTER or SPACE to Start', width / 2, height / 2); - ctx.font = '16px Courier New'; - ctx.fillText('WASD: Move | Mouse: Aim | Click/Space: Attack', width / 2, height / 2 + 50); - ctx.fillText('Shift: Stealth | 1-9: Skills | ESC: Pause', width / 2, height / 2 + 80); + const title = 'SLIME GENESIS'; + const titleW = PixelFont.getTextWidth(title, 2); + PixelFont.drawText(ctx, title, (width - titleW) / 2, height / 2 - 40, Palette.CYAN, 2); + + const start = 'PRESS ENTER TO START'; + const startW = PixelFont.getTextWidth(start, 1); + PixelFont.drawText(ctx, start, (width - startW) / 2, height / 2, Palette.WHITE, 1); + + const instructions = [ + 'WASD: MOVE | CLICK: ATTACK', + 'NUMS: SKILLS | ESC: PAUSE', + 'COLLECT DNA TO EVOLVE' + ]; + + instructions.forEach((line, i) => { + const lineW = PixelFont.getTextWidth(line, 1); + PixelFont.drawText(ctx, line, (width - lineW) / 2, height / 2 + 25 + i * 10, Palette.ROYAL_BLUE, 1); + }); + } else if (this.gameState === 'paused') { - ctx.fillText('PAUSED', width / 2, height / 2 - 50); - ctx.font = '24px Courier New'; - ctx.fillText('Press ENTER or SPACE to Resume', width / 2, height / 2); - ctx.fillText('Press ESC to Pause/Unpause', width / 2, height / 2 + 40); + const paused = 'PAUSED'; + const pausedW = PixelFont.getTextWidth(paused, 2); + PixelFont.drawText(ctx, paused, (width - pausedW) / 2, height / 2 - 20, Palette.SKY_BLUE, 2); + + const resume = 'PRESS ENTER TO RESUME'; + const resumeW = PixelFont.getTextWidth(resume, 1); + PixelFont.drawText(ctx, resume, (width - resumeW) / 2, height / 2 + 10, Palette.WHITE, 1); + } else if (this.gameState === 'gameOver') { + const dead = 'YOU PERISHED'; + const deadW = PixelFont.getTextWidth(dead, 2); + PixelFont.drawText(ctx, dead, (width - deadW) / 2, height / 2 - 30, Palette.WHITE, 2); + + const sub = 'YOUR DNA SUSTAINS THE CYCLE'; + const subW = PixelFont.getTextWidth(sub, 1); + PixelFont.drawText(ctx, sub, (width - subW) / 2, height / 2 - 5, Palette.ROYAL_BLUE, 1); + + const restart = 'PRESS ENTER TO REBORN'; + const restartW = PixelFont.getTextWidth(restart, 1); + PixelFont.drawText(ctx, restart, (width - restartW) / 2, height / 2 + 30, Palette.CYAN, 1); } } diff --git a/src/systems/MovementSystem.js b/src/systems/MovementSystem.js index b705139..abca20c 100644 --- a/src/systems/MovementSystem.js +++ b/src/systems/MovementSystem.js @@ -12,7 +12,7 @@ export class MovementSystem extends System { const position = entity.getComponent('Position'); const velocity = entity.getComponent('Velocity'); const health = entity.getComponent('Health'); - + if (!position || !velocity) return; // Check if this is a projectile @@ -28,9 +28,24 @@ export class MovementSystem extends System { } } - // Update position - position.x += velocity.vx * deltaTime; - position.y += velocity.vy * deltaTime; + // Update position with collision detection + const tileMap = this.engine.tileMap; + + // X Axis + const nextX = position.x + velocity.vx * deltaTime; + if (tileMap && tileMap.isSolid(nextX, position.y)) { + velocity.vx = 0; + } else { + position.x = nextX; + } + + // Y Axis + const nextY = position.y + velocity.vy * deltaTime; + if (tileMap && tileMap.isSolid(position.x, nextY)) { + velocity.vy = 0; + } else { + position.y = nextY; + } // Apply friction (skip for projectiles - they should maintain speed) if (!isProjectile) { diff --git a/src/systems/PlayerControllerSystem.js b/src/systems/PlayerControllerSystem.js index 397dfe0..7cd3a5d 100644 --- a/src/systems/PlayerControllerSystem.js +++ b/src/systems/PlayerControllerSystem.js @@ -26,7 +26,7 @@ export class PlayerControllerSystem extends System { // Movement input let moveX = 0; let moveY = 0; - const moveSpeed = 200; + const moveSpeed = 100; // Scaled down for 320x240 if (inputSystem.isKeyPressed('w') || inputSystem.isKeyPressed('arrowup')) { moveY -= 1; @@ -67,3 +67,4 @@ export class PlayerControllerSystem extends System { } } + diff --git a/src/systems/RenderSystem.js b/src/systems/RenderSystem.js index dcedd95..1cd8362 100644 --- a/src/systems/RenderSystem.js +++ b/src/systems/RenderSystem.js @@ -1,4 +1,6 @@ import { System } from '../core/System.js'; +import { Palette } from '../core/Palette.js'; +import { SpriteLibrary } from '../core/SpriteLibrary.js'; export class RenderSystem extends System { constructor(engine) { @@ -16,6 +18,9 @@ export class RenderSystem extends System { // Draw background this.drawBackground(); + // Draw map + this.drawMap(); + // Draw entities // Get all entities including inactive ones for rendering dead absorbable entities const allEntities = this.engine.entities; @@ -54,35 +59,48 @@ export class RenderSystem extends System { const width = this.engine.canvas.width; const height = this.engine.canvas.height; - // Cave background with gradient - const gradient = ctx.createLinearGradient(0, 0, 0, height); - gradient.addColorStop(0, '#0f0f1f'); - gradient.addColorStop(1, '#1a1a2e'); - ctx.fillStyle = gradient; + // Solid background + ctx.fillStyle = Palette.VOID; ctx.fillRect(0, 0, width, height); - // Add cave features with better visuals - ctx.fillStyle = '#2a2a3e'; + // Dithered pattern or simple shapes for cave features + ctx.fillStyle = Palette.DARKER_BLUE; for (let i = 0; i < 20; i++) { - const x = (i * 70 + Math.sin(i) * 30) % width; - const y = (i * 50 + Math.cos(i) * 40) % height; - const size = 25 + (i % 4) * 15; + // Snap to grid for pixel art look + const x = Math.floor((i * 70 + Math.sin(i) * 30) % width); + const y = Math.floor((i * 50 + Math.cos(i) * 40) % height); + const size = Math.floor(25 + (i % 4) * 15); - // Add shadow - ctx.shadowBlur = 20; - ctx.shadowColor = 'rgba(0, 0, 0, 0.5)'; ctx.beginPath(); - ctx.arc(x, y, size, 0, Math.PI * 2); - ctx.fill(); - ctx.shadowBlur = 0; + // Draw as rectangles or pixelated circles? Let's use Rects for now to match the style better or keep arcs but accept anti-aliasing + // Use integer coordinates strictly. + // Pixel Art style: use small squares instead of circles + ctx.fillRect(x, y, size, size); } + } - // Add some ambient lighting - const lightGradient = ctx.createRadialGradient(width / 2, height / 2, 0, width / 2, height / 2, 400); - lightGradient.addColorStop(0, 'rgba(100, 150, 200, 0.1)'); - lightGradient.addColorStop(1, 'rgba(0, 0, 0, 0)'); - ctx.fillStyle = lightGradient; - ctx.fillRect(0, 0, width, height); + drawMap() { + const tileMap = this.engine.tileMap; + if (!tileMap) return; + + const ctx = this.ctx; + const tileSize = tileMap.tileSize; + + // Draw walls + ctx.fillStyle = Palette.DARK_BLUE; + + for (let r = 0; r < tileMap.rows; r++) { + for (let c = 0; c < tileMap.cols; c++) { + if (tileMap.getTile(c, r) === 1) { // 1 is wall + ctx.fillRect(c * tileSize, r * tileSize, tileSize, tileSize); + + // Highlight top for 3D feel + ctx.fillStyle = Palette.ROYAL_BLUE; + ctx.fillRect(c * tileSize, r * tileSize, tileSize, 2); + ctx.fillStyle = Palette.DARK_BLUE; + } + } + } } drawEntity(entity, deltaTime, isDeadFade = false) { @@ -93,6 +111,11 @@ export class RenderSystem extends System { if (!position || !sprite) return; this.ctx.save(); + + // Pixel snapping + const drawX = Math.floor(position.x); + const drawY = Math.floor(position.y); + // Fade out dead entities let alpha = sprite.alpha; if (isDeadFade && health && health.isDead()) { @@ -106,8 +129,8 @@ export class RenderSystem extends System { } } this.ctx.globalAlpha = alpha; - this.ctx.translate(position.x, position.y); - this.ctx.rotate(position.rotation); + this.ctx.translate(drawX, drawY); + // REMOVED GLOBAL ROTATION: this.ctx.rotate(position.rotation); this.ctx.scale(sprite.scale, sprite.scale); // Update animation time for slime morphing @@ -116,26 +139,110 @@ export class RenderSystem extends System { sprite.morphAmount = Math.sin(sprite.animationTime * 3) * 0.2 + 0.8; } - // Draw based on shape - this.ctx.fillStyle = sprite.color; + // Map legacy colors to new Palette if necessary + let drawColor = sprite.color; + if (sprite.shape === 'slime') drawColor = Palette.CYAN; + // Map other colors? For now keep them if they match, but we should enforce palette eventually. + // The previous code had specific hardcoded colors. - if (sprite.shape === 'circle' || sprite.shape === 'slime') { - this.drawSlime(sprite); - } else if (sprite.shape === 'rect') { - this.ctx.fillRect(-sprite.width / 2, -sprite.height / 2, sprite.width, sprite.height); + this.ctx.fillStyle = drawColor; + + // Select appropriate animation state based on velocity + const velocity = entity.getComponent('Velocity'); + if (velocity) { + const isMoving = Math.abs(velocity.vx) > 1 || Math.abs(velocity.vy) > 1; + sprite.animationState = isMoving ? 'walk' : 'idle'; } - // Draw health bar if entity has health - if (health && health.maxHp > 0) { + // Lookup animation data + let spriteData = SpriteLibrary[sprite.shape]; + if (!spriteData) { + spriteData = SpriteLibrary.slime; // Hard fallback + } + + // Get animation frames for the current state + let frames = spriteData[sprite.animationState] || spriteData['idle']; + + // If frames is still not an array (fallback for simple grids or missing states) + if (!frames || !Array.isArray(frames)) { + // If it's a 2D array (legacy/simple), wrap it + if (Array.isArray(spriteData) || Array.isArray(spriteData[0])) { + frames = [spriteData]; + } else if (spriteData.idle) { + frames = spriteData.idle; + } else { + frames = SpriteLibrary.slime.idle; + } + } + + // Update animation timing + if (!health || !health.isDead()) { + sprite.animationTime += deltaTime; + } + const currentFrameIdx = Math.floor(sprite.animationTime * sprite.animationSpeed) % frames.length; + const grid = frames[currentFrameIdx]; + + if (!grid || !grid.length) { + this.ctx.restore(); + return; + } + + const rows = grid.length; + const cols = grid[0].length; + + // Calculate pixel size to fit the defined sprite dimensions + const pixelW = sprite.width / cols; + const pixelH = sprite.height / rows; + + // Draw from center + const offsetX = -sprite.width / 2; + const offsetY = -sprite.height / 2; + + // Horizontal Flipping based on rotation (facing left/right) + const isFlipped = Math.cos(position.rotation) < 0; + + this.ctx.save(); + if (isFlipped) { + this.ctx.scale(-1, 1); + } + + for (let r = 0; r < rows; r++) { + for (let c = 0; c < cols; c++) { + const value = grid[r][c]; + if (value === 0) continue; + + // Determine color + if (value === 1) { + this.ctx.fillStyle = drawColor; + } else if (value === 2) { + this.ctx.fillStyle = Palette.WHITE; + } else if (value === 3) { + this.ctx.fillStyle = Palette.DARKER_BLUE; + } + + // Draw pixel (snapped to nearest integer for crisp look) + this.ctx.fillRect( + offsetX + c * pixelW, + offsetY + r * pixelH, + Math.ceil(pixelW), + Math.ceil(pixelH) + ); + } + } + this.ctx.restore(); + + // Draw health bar if entity has health (stays horizontal) + if (health && health.maxHp > 0 && !health.isProjectile) { this.drawHealthBar(health, sprite); } - // Draw combat indicator if attacking + // Draw combat indicator if attacking (This DOES rotate) const combat = entity.getComponent('Combat'); if (combat && combat.isAttacking) { - // Draw attack indicator relative to entity's current rotation - // Since we're already rotated, we need to draw relative to 0,0 forward + this.ctx.save(); + this.ctx.rotate(position.rotation); this.drawAttackIndicator(combat, position); + this.ctx.restore(); } // Draw stealth indicator @@ -144,124 +251,68 @@ export class RenderSystem extends System { this.drawStealthIndicator(stealth, sprite); } - // Mutation Visual Effects + // Mutation Visual Effects - Simplified for pixel art const evolution = entity.getComponent('Evolution'); if (evolution) { if (evolution.mutationEffects.glowingBody) { - // Draw light aura - const auraGradient = this.ctx.createRadialGradient(0, 0, 0, 0, 0, sprite.width * 2); - auraGradient.addColorStop(0, 'rgba(255, 255, 200, 0.2)'); - auraGradient.addColorStop(1, 'rgba(255, 255, 200, 0)'); - this.ctx.fillStyle = auraGradient; - this.ctx.beginPath(); - this.ctx.arc(0, 0, sprite.width * 2, 0, Math.PI * 2); - this.ctx.fill(); + // Simple outline (square) + this.ctx.strokeStyle = Palette.WHITE; + this.ctx.lineWidth = 1; + this.ctx.strokeRect(-sprite.width / 2 - 2, -sprite.height / 2 - 2, sprite.width + 4, sprite.height + 4); } if (evolution.mutationEffects.electricSkin) { - // Add tiny sparks + // Sparks if (Math.random() < 0.2) { - this.ctx.strokeStyle = '#00ffff'; - this.ctx.lineWidth = 2; - this.ctx.beginPath(); - const sparkX = (Math.random() - 0.5) * sprite.width; - const sparkY = (Math.random() - 0.5) * sprite.height; - this.ctx.moveTo(sparkX, sparkY); - this.ctx.lineTo(sparkX + (Math.random() - 0.5) * 10, sparkY + (Math.random() - 0.5) * 10); - this.ctx.stroke(); + this.ctx.fillStyle = Palette.CYAN; + const sparkX = Math.floor((Math.random() - 0.5) * sprite.width); + const sparkY = Math.floor((Math.random() - 0.5) * sprite.height); + this.ctx.fillRect(sparkX, sparkY, 2, 2); } } - if (evolution.mutationEffects.hardenedShell) { - // Darker, thicker border - this.ctx.strokeStyle = 'rgba(0, 0, 0, 0.5)'; - this.ctx.lineWidth = 3; - this.ctx.stroke(); - } } this.ctx.restore(); } - drawSlime(sprite) { - const ctx = this.ctx; - const baseRadius = Math.min(sprite.width, sprite.height) / 2; - if (sprite.shape === 'slime') { - // Animated slime blob with morphing and better visuals - ctx.shadowBlur = 15; - ctx.shadowColor = sprite.color; - - // Main body with morphing - ctx.beginPath(); - const points = 16; - for (let i = 0; i < points; i++) { - const angle = (i / points) * Math.PI * 2; - const morph1 = Math.sin(angle * 2 + sprite.animationTime * 2) * 0.15; - const morph2 = Math.cos(angle * 3 + sprite.animationTime * 1.5) * 0.1; - const radius = baseRadius * (sprite.morphAmount + morph1 + morph2); - const x = Math.cos(angle) * radius; - const y = Math.sin(angle) * radius; - - if (i === 0) { - ctx.moveTo(x, y); - } else { - ctx.lineTo(x, y); - } - } - ctx.closePath(); - ctx.fill(); - - // Inner glow - const innerGradient = ctx.createRadialGradient(0, 0, 0, 0, 0, baseRadius * 0.8); - innerGradient.addColorStop(0, 'rgba(255, 255, 255, 0.4)'); - innerGradient.addColorStop(1, 'rgba(255, 255, 255, 0)'); - ctx.fillStyle = innerGradient; - ctx.beginPath(); - ctx.arc(0, 0, baseRadius * 0.8, 0, Math.PI * 2); - ctx.fill(); - - // Highlight - ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'; - ctx.beginPath(); - ctx.arc(-baseRadius * 0.3, -baseRadius * 0.3, baseRadius * 0.35, 0, Math.PI * 2); - ctx.fill(); - - ctx.fillStyle = sprite.color; - ctx.shadowBlur = 0; - } else { - // Simple circle with glow - ctx.shadowBlur = 10; - ctx.shadowColor = sprite.color; - ctx.beginPath(); - ctx.arc(0, 0, baseRadius, 0, Math.PI * 2); - ctx.fill(); - ctx.shadowBlur = 0; - } - } drawHealthBar(health, sprite) { + // Pixel art health bar const ctx = this.ctx; - const barWidth = sprite.width * 1.5; - const barHeight = 4; - const yOffset = sprite.height / 2 + 10; + // Width relative to sprite, snapped to even number + const barWidth = Math.floor(sprite.width * 1.2); + const barHeight = 2; + const yOffset = Math.floor(sprite.height / 2 + 3); - // Background - ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; - ctx.fillRect(-barWidth / 2, -yOffset, barWidth, barHeight); + const startX = -Math.floor(barWidth / 2); + const startY = -yOffset; - // Health fill - const healthPercent = health.hp / health.maxHp; - ctx.fillStyle = healthPercent > 0.5 ? '#00ff00' : healthPercent > 0.25 ? '#ffff00' : '#ff0000'; - ctx.fillRect(-barWidth / 2, -yOffset, barWidth * healthPercent, barHeight); + // Background (Dark Blue) + ctx.fillStyle = Palette.DARK_BLUE; + ctx.fillRect(startX, startY, barWidth, barHeight); - // Border - ctx.strokeStyle = '#ffffff'; - ctx.lineWidth = 1; - ctx.strokeRect(-barWidth / 2, -yOffset, barWidth, barHeight); + // Fill + const healthPercent = Math.max(0, health.hp / health.maxHp); + const fillWidth = Math.floor(barWidth * healthPercent); + + // Color based on Health (Palette only) + // High: CYAN, Mid: SKY_BLUE, Low: WHITE (flashing) + if (healthPercent > 0.5) { + ctx.fillStyle = Palette.CYAN; + } else if (healthPercent > 0.25) { + ctx.fillStyle = Palette.SKY_BLUE; + } else { + // Flash white for low health + ctx.fillStyle = (Math.floor(Date.now() / 200) % 2 === 0) ? Palette.WHITE : Palette.ROYAL_BLUE; + } + + ctx.fillRect(startX, startY, fillWidth, barHeight); } + drawAttackIndicator(combat, _position) { const ctx = this.ctx; - const length = 50; + const length = 25; // Scaled down const attackProgress = 1.0 - (combat.attackCooldown / 0.3); // 0 to 1 during attack animation // Since we're already in entity's rotated coordinate space (ctx.rotate was applied), @@ -272,7 +323,7 @@ export class RenderSystem extends System { // Draw slime tentacle/extension ctx.strokeStyle = `rgba(0, 255, 150, ${0.8 * attackProgress})`; ctx.fillStyle = `rgba(0, 255, 150, ${0.6 * attackProgress})`; - ctx.lineWidth = 8; + ctx.lineWidth = 4; // Scaled down ctx.lineCap = 'round'; // Tentacle extends outward during attack (forward from entity) @@ -286,15 +337,15 @@ export class RenderSystem extends System { // Add slight curve to tentacle const midX = Math.cos(angle) * tentacleLength * 0.5; const midY = Math.sin(angle) * tentacleLength * 0.5; - const perpX = -Math.sin(angle) * 5 * attackProgress; - const perpY = Math.cos(angle) * 5 * attackProgress; + const perpX = -Math.sin(angle) * 3 * attackProgress; + const perpY = Math.cos(angle) * 3 * attackProgress; ctx.quadraticCurveTo(midX + perpX, midY + perpY, tentacleEndX, tentacleEndY); ctx.stroke(); // Draw impact point if (attackProgress > 0.5) { ctx.beginPath(); - ctx.arc(tentacleEndX, tentacleEndY, 6 * attackProgress, 0, Math.PI * 2); + ctx.arc(tentacleEndX, tentacleEndY, 3 * attackProgress, 0, Math.PI * 2); ctx.fill(); } } diff --git a/src/systems/SkillEffectSystem.js b/src/systems/SkillEffectSystem.js index 181a384..c2565a7 100644 --- a/src/systems/SkillEffectSystem.js +++ b/src/systems/SkillEffectSystem.js @@ -39,3 +39,4 @@ export class SkillEffectSystem extends System { } } + diff --git a/src/systems/UISystem.js b/src/systems/UISystem.js index c6ea2d4..fe15c94 100644 --- a/src/systems/UISystem.js +++ b/src/systems/UISystem.js @@ -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); }); } diff --git a/src/world/World.js b/src/world/World.js index 9afa6e8..6caccf7 100644 --- a/src/world/World.js +++ b/src/world/World.js @@ -25,3 +25,4 @@ export class World { } } + From 86c1c3bc59a8bfdb4986c900b3d8849722dd6768 Mon Sep 17 00:00:00 2001 From: Juan Sebastian Montoya Date: Tue, 6 Jan 2026 17:25:34 -0500 Subject: [PATCH 2/3] feat: Move player stats and skill progress display from active HUD to the paused menu, removing minor HUD elements and consolidating evolution details into the stats display. --- src/systems/MenuSystem.js | 16 +++++++++++-- src/systems/UISystem.js | 50 ++++++++++++++++++--------------------- 2 files changed, 37 insertions(+), 29 deletions(-) diff --git a/src/systems/MenuSystem.js b/src/systems/MenuSystem.js index ca90856..9ba7d57 100644 --- a/src/systems/MenuSystem.js +++ b/src/systems/MenuSystem.js @@ -115,11 +115,23 @@ export class MenuSystem extends System { } else if (this.gameState === 'paused') { const paused = 'PAUSED'; const pausedW = PixelFont.getTextWidth(paused, 2); - PixelFont.drawText(ctx, paused, (width - pausedW) / 2, height / 2 - 20, Palette.SKY_BLUE, 2); + PixelFont.drawText(ctx, paused, (width - pausedW) / 2, 20, Palette.SKY_BLUE, 2); const resume = 'PRESS ENTER TO RESUME'; const resumeW = PixelFont.getTextWidth(resume, 1); - PixelFont.drawText(ctx, resume, (width - resumeW) / 2, height / 2 + 10, Palette.WHITE, 1); + PixelFont.drawText(ctx, resume, (width - resumeW) / 2, 45, Palette.WHITE, 1); + + // Draw Stats and Knowledge (Moved from HUD) + const player = this.engine.getEntities().find(e => e.hasComponent('Evolution')); + const uiSystem = this.engine.systems.find(s => s.name === 'UISystem'); + + if (player && uiSystem) { + // Draw Stats on the left + uiSystem.drawStats(player, 20, 80); + + // Draw Learning Progress on the right + uiSystem.drawSkillProgress(player, width - 110, 80); + } } else if (this.gameState === 'gameOver') { const dead = 'YOU PERISHED'; const deadW = PixelFont.getTextWidth(dead, 2); diff --git a/src/systems/UISystem.js b/src/systems/UISystem.js index fe15c94..134e334 100644 --- a/src/systems/UISystem.js +++ b/src/systems/UISystem.js @@ -64,8 +64,7 @@ export class UISystem extends System { // Draw UI this.drawHUD(player); this.drawSkills(player); - this.drawStats(player); - this.drawSkillProgress(player); + // REMOVED drawStats and drawSkillProgress from active gameplay this.drawDamageNumbers(); this.drawNotifications(); this.drawAbsorptionEffects(); @@ -105,13 +104,6 @@ export class UISystem extends System { const form = evolution.getDominantForm(); const formY = barY + barHeight + 14; PixelFont.drawText(ctx, form.toUpperCase(), barX, formY, Palette.SKY_BLUE, 1); - - // 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); - - // Small Instructions - PixelFont.drawText(ctx, 'WASD CLICK', barX, this.engine.canvas.height - 10, Palette.DARK_BLUE, 1); } drawSkills(player) { @@ -140,43 +132,47 @@ export class UISystem extends System { }); } - drawStats(player) { + drawStats(player, x, y) { const stats = player.getComponent('Stats'); - if (!stats) return; + const evolution = player.getComponent('Evolution'); + if (!stats || !evolution) return; const ctx = this.ctx; - const width = this.engine.canvas.width; - const startX = width - 80; - const startY = 60; + 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, '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); + 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) { + drawSkillProgress(player, x, y) { const skillProgress = player.getComponent('SkillProgress'); if (!skillProgress) return; const ctx = this.ctx; - const width = this.engine.canvas.width; - const startX = width - 80; - const startY = 100; - const progress = skillProgress.getAllProgress(); - if (progress.size === 0) return; - PixelFont.drawText(ctx, 'LRN', startX, startY, Palette.CYAN, 1); + 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 > 4) name = name.substring(0, 4); + if (name.length > 10) name = name.substring(0, 10); - const y = startY + 9 + idx * 8; - PixelFont.drawText(ctx, `${name} ${count}/${required}`, startX, y, Palette.SKY_BLUE, 1); + const py = y + 10 + idx * 9; + PixelFont.drawText(ctx, `${name}: ${count}/${required}`, x, py, Palette.SKY_BLUE, 1); idx++; }); } From 4e51a430e8c57500f7bcfe8d246532c909ee08b8 Mon Sep 17 00:00:00 2001 From: Juan Sebastian Montoya Date: Tue, 6 Jan 2026 17:25:59 -0500 Subject: [PATCH 3/3] fix: Add minimum width to a layout element. --- index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/index.html b/index.html index 5a04747..366bda6 100644 --- a/index.html +++ b/index.html @@ -40,6 +40,7 @@ /* Simplified scaling: Aspect ratio determines size, strictly contained */ width: calc(100% - 500px); + min-width: 640px; height: auto; aspect-ratio: 4 / 3; }