feat: Implement pixel-art rendering with new level loading, tile maps, palettes, and pixel fonts, alongside a game over screen.

This commit is contained in:
Juan Sebastián Montoya 2026-01-06 17:21:15 -05:00
parent 5b15e63ac3
commit cf04677511
41 changed files with 793 additions and 331 deletions

1
.gitignore vendored
View file

@ -4,3 +4,4 @@ dist/
*.log *.log
.vite/ .vite/

View file

@ -1,31 +1,56 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Slime Genesis - PoC</title> <title>Slime Genesis - PoC</title>
<style> <style>
* {
box-sizing: border-box;
}
body { body {
margin: 0; margin: 0;
padding: 0; padding: 0;
width: 100vw;
height: 100vh;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
min-height: 100vh;
background: #1a1a1a; background: #1a1a1a;
font-family: 'Courier New', monospace; font-family: 'Courier New', monospace;
overflow: hidden;
} }
#game-container { #game-container {
border: 2px solid #4a4a4a; width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background: #000;
}
#game-canvas {
display: block;
image-rendering: pixelated;
box-shadow: 0 0 20px rgba(0, 255, 150, 0.3); box-shadow: 0 0 20px rgba(0, 255, 150, 0.3);
border: 2px solid #4a4a4a;
/* Simplified scaling: Aspect ratio determines size, strictly contained */
width: calc(100% - 500px);
height: auto;
aspect-ratio: 4 / 3;
} }
</style> </style>
</head> </head>
<body> <body>
<div id="game-container"> <div id="game-container">
<canvas id="game-canvas" tabindex="0"></canvas> <canvas id="game-canvas" tabindex="0"></canvas>
</div> </div>
<script type="module" src="/src/main.js"></script> <script type="module" src="/src/main.js"></script>
</body> </body>
</html>
</html>

View file

@ -13,7 +13,7 @@ export const GameConfig = {
}, },
Absorption: { Absorption: {
range: 80, range: 30, // Scaled down
healPercentMin: 0.1, healPercentMin: 0.1,
healPercentMax: 0.2, healPercentMax: 0.2,
skillAbsorptionChance: 0.3, skillAbsorptionChance: 0.3,

View file

@ -7,12 +7,12 @@ export class AI extends Component {
this.state = 'idle'; // 'idle', 'moving', 'attacking', 'fleeing' this.state = 'idle'; // 'idle', 'moving', 'attacking', 'fleeing'
this.target = null; // Entity ID to target this.target = null; // Entity ID to target
this.awareness = 0; // 0-1, how aware of player this.awareness = 0; // 0-1, how aware of player
this.alertRadius = 150; this.alertRadius = 60; // Scaled for 320x240
this.chaseRadius = 300; this.chaseRadius = 120;
this.fleeRadius = 100; this.fleeRadius = 40;
// Behavior parameters // Behavior parameters
this.wanderSpeed = 50; this.wanderSpeed = 20; // Slower wander
this.wanderDirection = Math.random() * Math.PI * 2; this.wanderDirection = Math.random() * Math.PI * 2;
this.wanderChangeTime = 0; this.wanderChangeTime = 0;
this.wanderChangeInterval = 2.0; // seconds this.wanderChangeInterval = 2.0; // seconds
@ -50,3 +50,4 @@ export class AI extends Component {
} }
} }

View file

@ -52,3 +52,4 @@ export class Absorbable extends Component {
} }
} }

View file

@ -6,7 +6,7 @@ export class Combat extends Component {
this.attackDamage = 10; this.attackDamage = 10;
this.defense = 5; this.defense = 5;
this.attackSpeed = 1.0; // Attacks per second this.attackSpeed = 1.0; // Attacks per second
this.attackRange = 50; this.attackRange = 15; // Melee range for pixel art
this.lastAttackTime = 0; this.lastAttackTime = 0;
this.attackCooldown = 0; this.attackCooldown = 0;
@ -50,3 +50,4 @@ export class Combat extends Component {
} }
} }

View file

@ -103,3 +103,4 @@ export class Evolution extends Component {
} }
} }

View file

@ -69,3 +69,4 @@ export class Inventory extends Component {
} }
} }

View file

@ -9,3 +9,4 @@ export class Position extends Component {
} }
} }

View file

@ -43,3 +43,4 @@ export class SkillProgress extends Component {
} }
} }

View file

@ -67,3 +67,4 @@ export class Skills extends Component {
} }
} }

View file

@ -12,7 +12,10 @@ export class Sprite extends Component {
// Animation properties // Animation properties
this.animationTime = 0; 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
} }
} }

View file

@ -53,3 +53,4 @@ export class Stats extends Component {
} }
} }

View file

@ -46,3 +46,4 @@ export class Stealth extends Component {
} }
} }

View file

@ -9,3 +9,4 @@ export class Velocity extends Component {
} }
} }

View file

@ -12,3 +12,4 @@ export class Component {
} }
} }

View file

@ -1,6 +1,7 @@
import { System } from './System.js'; import { System } from './System.js';
import { Entity } from './Entity.js'; import { Entity } from './Entity.js';
import { EventBus } from './EventBus.js'; import { EventBus } from './EventBus.js';
import { LevelLoader } from './LevelLoader.js';
/** /**
* Main game engine - manages ECS, game loop, and systems * Main game engine - manages ECS, game loop, and systems
@ -15,12 +16,22 @@ export class Engine {
this.running = false; this.running = false;
this.lastTime = 0; this.lastTime = 0;
// Set canvas size // Set internal resolution (low-res for pixel art)
this.canvas.width = 1024; this.canvas.width = 320;
this.canvas.height = 768; 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 // Game state
this.deltaTime = 0; this.deltaTime = 0;
// Initialize standard map (320x240 / 16px tiles = 20x15)
this.tileMap = LevelLoader.loadSimpleLevel(20, 15, 16);
} }
/** /**
@ -109,7 +120,7 @@ export class Engine {
// Update all systems // Update all systems
const menuSystem = this.systems.find(s => s.name === 'MenuSystem'); const menuSystem = this.systems.find(s => s.name === 'MenuSystem');
const gameState = menuSystem ? menuSystem.getGameState() : 'playing'; const gameState = menuSystem ? menuSystem.getGameState() : 'playing';
const isPaused = gameState === 'paused' || gameState === 'start'; const isPaused = gameState === 'paused' || gameState === 'start' || gameState === 'gameOver';
this.systems.forEach(system => { this.systems.forEach(system => {
// Skip game systems if paused/start menu (but allow MenuSystem, UISystem, and RenderSystem) // Skip game systems if paused/start menu (but allow MenuSystem, UISystem, and RenderSystem)

View file

@ -56,3 +56,4 @@ export class Entity {
} }
} }

22
src/core/LevelLoader.js Normal file
View file

@ -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;
}
}

27
src/core/Palette.js Normal file
View file

@ -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
];
}
};

80
src/core/PixelFont.js Normal file
View file

@ -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;
}
};

167
src/core/SpriteLibrary.js Normal file
View file

@ -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]
]
]
}
};

View file

@ -43,3 +43,4 @@ export class System {
} }
} }

34
src/core/TileMap.js Normal file
View file

@ -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;
}
}

View file

@ -11,3 +11,4 @@ export class Item {
} }
} }

View file

@ -51,3 +51,4 @@ export class ItemRegistry {
} }
} }

View file

@ -56,9 +56,9 @@ if (!canvas) {
// Create player entity // Create player entity
const player = engine.createEntity(); const player = engine.createEntity();
player.addComponent(new Position(512, 384)); player.addComponent(new Position(160, 120)); // Center of 320x240
player.addComponent(new Velocity(0, 0, 200)); player.addComponent(new Velocity(0, 0, 100)); // Slower speed for small resolution
player.addComponent(new Sprite('#00ff96', 40, 40, 'slime')); player.addComponent(new Sprite('#00ff96', 14, 14, 'slime')); // 14x14 pixel sprite
player.addComponent(new Health(100)); player.addComponent(new Health(100));
player.addComponent(new Stats()); player.addComponent(new Stats());
player.addComponent(new Evolution()); player.addComponent(new Evolution());
@ -77,7 +77,7 @@ if (!canvas) {
function createCreature(engine, x, y, type) { function createCreature(engine, x, y, type) {
const creature = engine.createEntity(); const creature = engine.createEntity();
creature.addComponent(new Position(x, y)); 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; let color, evolutionData, skills;
@ -103,8 +103,8 @@ if (!canvas) {
skills = []; skills = [];
} }
creature.addComponent(new Sprite(color, 25, 25, 'circle')); creature.addComponent(new Sprite(color, 10, 10, type));
creature.addComponent(new Health(50 + Math.random() * 30)); creature.addComponent(new Health(15 + Math.random() * 10)); // Adjusted health for smaller enemies
creature.addComponent(new Stats()); creature.addComponent(new Stats());
creature.addComponent(new Combat()); creature.addComponent(new Combat());
creature.addComponent(new AI('wander')); creature.addComponent(new AI('wander'));
@ -119,8 +119,8 @@ if (!canvas) {
// Spawn initial creatures // Spawn initial creatures
for (let i = 0; i < 8; i++) { for (let i = 0; i < 8; i++) {
const x = 100 + Math.random() * 824; const x = 20 + Math.random() * 280; // Fit in 320 width
const y = 100 + Math.random() * 568; const y = 20 + Math.random() * 200; // Fit in 240 height
const types = ['humanoid', 'beast', 'elemental']; const types = ['humanoid', 'beast', 'elemental'];
const type = types[Math.floor(Math.random() * types.length)]; const type = types[Math.floor(Math.random() * types.length)];
createCreature(engine, x, y, type); createCreature(engine, x, y, type);
@ -133,8 +133,8 @@ if (!canvas) {
); );
if (existingCreatures.length < 10) { if (existingCreatures.length < 10) {
const x = 100 + Math.random() * 824; const x = 20 + Math.random() * 280;
const y = 100 + Math.random() * 568; const y = 20 + Math.random() * 200;
const types = ['humanoid', 'beast', 'elemental']; const types = ['humanoid', 'beast', 'elemental'];
const type = types[Math.floor(Math.random() * types.length)]; const type = types[Math.floor(Math.random() * types.length)];
createCreature(engine, x, y, type); createCreature(engine, x, y, type);

View file

@ -30,3 +30,4 @@ export class Skill {
return !skills.isOnCooldown(this.id); return !skills.isOnCooldown(this.id);
} }
} }

View file

@ -33,3 +33,4 @@ export class StealthMode extends Skill {
} }
} }

View file

@ -9,8 +9,8 @@ export class SlimeGun extends Skill {
super('slime_gun', 'Slime Gun', 1.0); super('slime_gun', 'Slime Gun', 1.0);
this.description = 'Shoot a blob of slime at enemies (costs 1 HP)'; this.description = 'Shoot a blob of slime at enemies (costs 1 HP)';
this.damage = 15; this.damage = 15;
this.range = 800; // Long range for a gun this.range = 250; // Screen width approx
this.speed = 600; // Faster projectile this.speed = 250; // Faster than player but readable
this.hpCost = 1; this.hpCost = 1;
} }
@ -61,7 +61,7 @@ export class SlimeGun extends Skill {
projectileVelocity.maxSpeed = this.speed * 2; // Allow projectiles to move fast projectileVelocity.maxSpeed = this.speed * 2; // Allow projectiles to move fast
projectile.addComponent(projectileVelocity); projectile.addComponent(projectileVelocity);
// Slime-colored projectile // 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 // Projectile has temporary health for collision detection
const projectileHealth = new Health(1); const projectileHealth = new Health(1);

View file

@ -15,13 +15,20 @@ export class AISystem extends System {
const config = GameConfig.AI; const config = GameConfig.AI;
entities.forEach(entity => { entities.forEach(entity => {
const health = entity.getComponent('Health');
const ai = entity.getComponent('AI'); const ai = entity.getComponent('AI');
const position = entity.getComponent('Position'); const position = entity.getComponent('Position');
const velocity = entity.getComponent('Velocity'); const velocity = entity.getComponent('Velocity');
const _stealth = entity.getComponent('Stealth');
if (!ai || !position || !velocity) return; 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 // Update wander timer
ai.wanderChangeTime += deltaTime; ai.wanderChangeTime += deltaTime;

View file

@ -24,9 +24,15 @@ export class DeathSystem extends System {
// Check if entity is dead // Check if entity is dead
if (health.isDead()) { if (health.isDead()) {
// Don't remove player // Check if player died
const evolution = entity.getComponent('Evolution'); 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 // Mark as inactive immediately so it stops being processed by other systems
if (entity.active) { if (entity.active) {

View file

@ -25,3 +25,4 @@ export class HealthRegenerationSystem extends System {
} }
} }

View file

@ -76,8 +76,12 @@ export class InputSystem extends System {
if (this.engine && this.engine.canvas) { if (this.engine && this.engine.canvas) {
const canvas = this.engine.canvas; const canvas = this.engine.canvas;
const rect = canvas.getBoundingClientRect(); const rect = canvas.getBoundingClientRect();
this.mouse.x = e.clientX - rect.left; // Calculate scale factors between displayed size and internal resolution
this.mouse.y = e.clientY - rect.top; 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;
} }
}); });

View file

@ -1,4 +1,6 @@
import { System } from '../core/System.js'; 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) * System to handle game menus (start, pause)
@ -31,11 +33,22 @@ export class MenuSystem extends System {
this.startGame(); this.startGame();
} else if (this.gameState === 'paused') { } else if (this.gameState === 'paused') {
this.resumeGame(); 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() { startGame() {
this.gameState = 'playing'; this.gameState = 'playing';
this.paused = false; this.paused = false;
@ -75,27 +88,50 @@ export class MenuSystem extends System {
const width = this.engine.canvas.width; const width = this.engine.canvas.width;
const height = this.engine.canvas.height; const height = this.engine.canvas.height;
// Dark overlay // Darker overlay matching palette
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; ctx.fillStyle = 'rgba(32, 21, 51, 0.8)'; // Semi-transparent VOID
ctx.fillRect(0, 0, width, height); 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') { if (this.gameState === 'start') {
ctx.fillText('SLIME GENESIS', width / 2, height / 2 - 100); const title = 'SLIME GENESIS';
ctx.font = '24px Courier New'; const titleW = PixelFont.getTextWidth(title, 2);
ctx.fillText('Press ENTER or SPACE to Start', width / 2, height / 2); PixelFont.drawText(ctx, title, (width - titleW) / 2, height / 2 - 40, Palette.CYAN, 2);
ctx.font = '16px Courier New';
ctx.fillText('WASD: Move | Mouse: Aim | Click/Space: Attack', width / 2, height / 2 + 50); const start = 'PRESS ENTER TO START';
ctx.fillText('Shift: Stealth | 1-9: Skills | ESC: Pause', width / 2, height / 2 + 80); 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') { } else if (this.gameState === 'paused') {
ctx.fillText('PAUSED', width / 2, height / 2 - 50); const paused = 'PAUSED';
ctx.font = '24px Courier New'; const pausedW = PixelFont.getTextWidth(paused, 2);
ctx.fillText('Press ENTER or SPACE to Resume', width / 2, height / 2); PixelFont.drawText(ctx, paused, (width - pausedW) / 2, height / 2 - 20, Palette.SKY_BLUE, 2);
ctx.fillText('Press ESC to Pause/Unpause', width / 2, height / 2 + 40);
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);
} }
} }

View file

@ -28,9 +28,24 @@ export class MovementSystem extends System {
} }
} }
// Update position // Update position with collision detection
position.x += velocity.vx * deltaTime; const tileMap = this.engine.tileMap;
position.y += velocity.vy * deltaTime;
// 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) // Apply friction (skip for projectiles - they should maintain speed)
if (!isProjectile) { if (!isProjectile) {

View file

@ -26,7 +26,7 @@ export class PlayerControllerSystem extends System {
// Movement input // Movement input
let moveX = 0; let moveX = 0;
let moveY = 0; let moveY = 0;
const moveSpeed = 200; const moveSpeed = 100; // Scaled down for 320x240
if (inputSystem.isKeyPressed('w') || inputSystem.isKeyPressed('arrowup')) { if (inputSystem.isKeyPressed('w') || inputSystem.isKeyPressed('arrowup')) {
moveY -= 1; moveY -= 1;
@ -67,3 +67,4 @@ export class PlayerControllerSystem extends System {
} }
} }

View file

@ -1,4 +1,6 @@
import { System } from '../core/System.js'; import { System } from '../core/System.js';
import { Palette } from '../core/Palette.js';
import { SpriteLibrary } from '../core/SpriteLibrary.js';
export class RenderSystem extends System { export class RenderSystem extends System {
constructor(engine) { constructor(engine) {
@ -16,6 +18,9 @@ export class RenderSystem extends System {
// Draw background // Draw background
this.drawBackground(); this.drawBackground();
// Draw map
this.drawMap();
// Draw entities // Draw entities
// Get all entities including inactive ones for rendering dead absorbable entities // Get all entities including inactive ones for rendering dead absorbable entities
const allEntities = this.engine.entities; const allEntities = this.engine.entities;
@ -54,35 +59,48 @@ export class RenderSystem extends System {
const width = this.engine.canvas.width; const width = this.engine.canvas.width;
const height = this.engine.canvas.height; const height = this.engine.canvas.height;
// Cave background with gradient // Solid background
const gradient = ctx.createLinearGradient(0, 0, 0, height); ctx.fillStyle = Palette.VOID;
gradient.addColorStop(0, '#0f0f1f');
gradient.addColorStop(1, '#1a1a2e');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, width, height); ctx.fillRect(0, 0, width, height);
// Add cave features with better visuals // Dithered pattern or simple shapes for cave features
ctx.fillStyle = '#2a2a3e'; ctx.fillStyle = Palette.DARKER_BLUE;
for (let i = 0; i < 20; i++) { for (let i = 0; i < 20; i++) {
const x = (i * 70 + Math.sin(i) * 30) % width; // Snap to grid for pixel art look
const y = (i * 50 + Math.cos(i) * 40) % height; const x = Math.floor((i * 70 + Math.sin(i) * 30) % width);
const size = 25 + (i % 4) * 15; 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.beginPath();
ctx.arc(x, y, size, 0, Math.PI * 2); // Draw as rectangles or pixelated circles? Let's use Rects for now to match the style better or keep arcs but accept anti-aliasing
ctx.fill(); // Use integer coordinates strictly.
ctx.shadowBlur = 0; // Pixel Art style: use small squares instead of circles
ctx.fillRect(x, y, size, size);
}
} }
// Add some ambient lighting drawMap() {
const lightGradient = ctx.createRadialGradient(width / 2, height / 2, 0, width / 2, height / 2, 400); const tileMap = this.engine.tileMap;
lightGradient.addColorStop(0, 'rgba(100, 150, 200, 0.1)'); if (!tileMap) return;
lightGradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
ctx.fillStyle = lightGradient; const ctx = this.ctx;
ctx.fillRect(0, 0, width, height); 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) { drawEntity(entity, deltaTime, isDeadFade = false) {
@ -93,6 +111,11 @@ export class RenderSystem extends System {
if (!position || !sprite) return; if (!position || !sprite) return;
this.ctx.save(); this.ctx.save();
// Pixel snapping
const drawX = Math.floor(position.x);
const drawY = Math.floor(position.y);
// Fade out dead entities // Fade out dead entities
let alpha = sprite.alpha; let alpha = sprite.alpha;
if (isDeadFade && health && health.isDead()) { if (isDeadFade && health && health.isDead()) {
@ -106,8 +129,8 @@ export class RenderSystem extends System {
} }
} }
this.ctx.globalAlpha = alpha; this.ctx.globalAlpha = alpha;
this.ctx.translate(position.x, position.y); this.ctx.translate(drawX, drawY);
this.ctx.rotate(position.rotation); // REMOVED GLOBAL ROTATION: this.ctx.rotate(position.rotation);
this.ctx.scale(sprite.scale, sprite.scale); this.ctx.scale(sprite.scale, sprite.scale);
// Update animation time for slime morphing // 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; sprite.morphAmount = Math.sin(sprite.animationTime * 3) * 0.2 + 0.8;
} }
// Draw based on shape // Map legacy colors to new Palette if necessary
this.ctx.fillStyle = sprite.color; 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.ctx.fillStyle = drawColor;
this.drawSlime(sprite);
} else if (sprite.shape === 'rect') { // Select appropriate animation state based on velocity
this.ctx.fillRect(-sprite.width / 2, -sprite.height / 2, sprite.width, sprite.height); 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 // Lookup animation data
if (health && health.maxHp > 0) { 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); this.drawHealthBar(health, sprite);
} }
// Draw combat indicator if attacking // Draw combat indicator if attacking (This DOES rotate)
const combat = entity.getComponent('Combat'); const combat = entity.getComponent('Combat');
if (combat && combat.isAttacking) { if (combat && combat.isAttacking) {
// Draw attack indicator relative to entity's current rotation this.ctx.save();
// Since we're already rotated, we need to draw relative to 0,0 forward this.ctx.rotate(position.rotation);
this.drawAttackIndicator(combat, position); this.drawAttackIndicator(combat, position);
this.ctx.restore();
} }
// Draw stealth indicator // Draw stealth indicator
@ -144,124 +251,68 @@ export class RenderSystem extends System {
this.drawStealthIndicator(stealth, sprite); this.drawStealthIndicator(stealth, sprite);
} }
// Mutation Visual Effects // Mutation Visual Effects - Simplified for pixel art
const evolution = entity.getComponent('Evolution'); const evolution = entity.getComponent('Evolution');
if (evolution) { if (evolution) {
if (evolution.mutationEffects.glowingBody) { if (evolution.mutationEffects.glowingBody) {
// Draw light aura // Simple outline (square)
const auraGradient = this.ctx.createRadialGradient(0, 0, 0, 0, 0, sprite.width * 2); this.ctx.strokeStyle = Palette.WHITE;
auraGradient.addColorStop(0, 'rgba(255, 255, 200, 0.2)'); this.ctx.lineWidth = 1;
auraGradient.addColorStop(1, 'rgba(255, 255, 200, 0)'); this.ctx.strokeRect(-sprite.width / 2 - 2, -sprite.height / 2 - 2, sprite.width + 4, sprite.height + 4);
this.ctx.fillStyle = auraGradient;
this.ctx.beginPath();
this.ctx.arc(0, 0, sprite.width * 2, 0, Math.PI * 2);
this.ctx.fill();
} }
if (evolution.mutationEffects.electricSkin) { if (evolution.mutationEffects.electricSkin) {
// Add tiny sparks // Sparks
if (Math.random() < 0.2) { if (Math.random() < 0.2) {
this.ctx.strokeStyle = '#00ffff'; this.ctx.fillStyle = Palette.CYAN;
this.ctx.lineWidth = 2; const sparkX = Math.floor((Math.random() - 0.5) * sprite.width);
this.ctx.beginPath(); const sparkY = Math.floor((Math.random() - 0.5) * sprite.height);
const sparkX = (Math.random() - 0.5) * sprite.width; this.ctx.fillRect(sparkX, sparkY, 2, 2);
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();
} }
} }
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(); 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) { drawHealthBar(health, sprite) {
// Pixel art health bar
const ctx = this.ctx; const ctx = this.ctx;
const barWidth = sprite.width * 1.5; // Width relative to sprite, snapped to even number
const barHeight = 4; const barWidth = Math.floor(sprite.width * 1.2);
const yOffset = sprite.height / 2 + 10; const barHeight = 2;
const yOffset = Math.floor(sprite.height / 2 + 3);
// Background const startX = -Math.floor(barWidth / 2);
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; const startY = -yOffset;
ctx.fillRect(-barWidth / 2, -yOffset, barWidth, barHeight);
// Health fill // Background (Dark Blue)
const healthPercent = health.hp / health.maxHp; ctx.fillStyle = Palette.DARK_BLUE;
ctx.fillStyle = healthPercent > 0.5 ? '#00ff00' : healthPercent > 0.25 ? '#ffff00' : '#ff0000'; ctx.fillRect(startX, startY, barWidth, barHeight);
ctx.fillRect(-barWidth / 2, -yOffset, barWidth * healthPercent, barHeight);
// Border // Fill
ctx.strokeStyle = '#ffffff'; const healthPercent = Math.max(0, health.hp / health.maxHp);
ctx.lineWidth = 1; const fillWidth = Math.floor(barWidth * healthPercent);
ctx.strokeRect(-barWidth / 2, -yOffset, barWidth, barHeight);
// 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) { drawAttackIndicator(combat, _position) {
const ctx = this.ctx; 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 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), // 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 // Draw slime tentacle/extension
ctx.strokeStyle = `rgba(0, 255, 150, ${0.8 * attackProgress})`; ctx.strokeStyle = `rgba(0, 255, 150, ${0.8 * attackProgress})`;
ctx.fillStyle = `rgba(0, 255, 150, ${0.6 * attackProgress})`; ctx.fillStyle = `rgba(0, 255, 150, ${0.6 * attackProgress})`;
ctx.lineWidth = 8; ctx.lineWidth = 4; // Scaled down
ctx.lineCap = 'round'; ctx.lineCap = 'round';
// Tentacle extends outward during attack (forward from entity) // Tentacle extends outward during attack (forward from entity)
@ -286,15 +337,15 @@ export class RenderSystem extends System {
// Add slight curve to tentacle // Add slight curve to tentacle
const midX = Math.cos(angle) * tentacleLength * 0.5; const midX = Math.cos(angle) * tentacleLength * 0.5;
const midY = Math.sin(angle) * tentacleLength * 0.5; const midY = Math.sin(angle) * tentacleLength * 0.5;
const perpX = -Math.sin(angle) * 5 * attackProgress; const perpX = -Math.sin(angle) * 3 * attackProgress;
const perpY = Math.cos(angle) * 5 * attackProgress; const perpY = Math.cos(angle) * 3 * attackProgress;
ctx.quadraticCurveTo(midX + perpX, midY + perpY, tentacleEndX, tentacleEndY); ctx.quadraticCurveTo(midX + perpX, midY + perpY, tentacleEndX, tentacleEndY);
ctx.stroke(); ctx.stroke();
// Draw impact point // Draw impact point
if (attackProgress > 0.5) { if (attackProgress > 0.5) {
ctx.beginPath(); ctx.beginPath();
ctx.arc(tentacleEndX, tentacleEndY, 6 * attackProgress, 0, Math.PI * 2); ctx.arc(tentacleEndX, tentacleEndY, 3 * attackProgress, 0, Math.PI * 2);
ctx.fill(); ctx.fill();
} }
} }

View file

@ -39,3 +39,4 @@ export class SkillEffectSystem extends System {
} }
} }

View file

@ -1,6 +1,8 @@
import { System } from '../core/System.js'; import { System } from '../core/System.js';
import { SkillRegistry } from '../skills/SkillRegistry.js'; import { SkillRegistry } from '../skills/SkillRegistry.js';
import { Events } from '../core/EventBus.js'; import { Events } from '../core/EventBus.js';
import { PixelFont } from '../core/PixelFont.js';
import { Palette } from '../core/Palette.js';
export class UISystem extends System { export class UISystem extends System {
constructor(engine) { constructor(engine) {
@ -15,7 +17,7 @@ export class UISystem extends System {
// Subscribe to events // Subscribe to events
engine.on(Events.DAMAGE_DEALT, (data) => this.addDamageNumber(data)); 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) { addDamageNumber(data) {
@ -45,8 +47,8 @@ export class UISystem extends System {
const menuSystem = this.engine.systems.find(s => s.name === 'MenuSystem'); const menuSystem = this.engine.systems.find(s => s.name === 'MenuSystem');
const gameState = menuSystem ? menuSystem.getGameState() : 'playing'; const gameState = menuSystem ? menuSystem.getGameState() : 'playing';
// Only draw menu overlay if in start or paused state // Only draw menu overlay if in start, paused, or gameOver state
if (gameState === 'start' || gameState === 'paused') { if (gameState === 'start' || gameState === 'paused' || gameState === 'gameOver') {
if (menuSystem) { if (menuSystem) {
menuSystem.drawMenu(); menuSystem.drawMenu();
} }
@ -73,62 +75,43 @@ export class UISystem extends System {
const health = player.getComponent('Health'); const health = player.getComponent('Health');
const stats = player.getComponent('Stats'); const stats = player.getComponent('Stats');
const evolution = player.getComponent('Evolution'); const evolution = player.getComponent('Evolution');
const skills = player.getComponent('Skills');
if (!health || !stats || !evolution) return; if (!health || !stats || !evolution) return;
const ctx = this.ctx; const ctx = this.ctx;
const _width = this.engine.canvas.width;
const _height = this.engine.canvas.height;
// Health bar // Health bar
const barWidth = 200; const barWidth = 64;
const barHeight = 20; const barHeight = 6;
const barX = 20; const barX = 4;
const barY = 20; 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); ctx.fillRect(barX, barY, barWidth, barHeight);
const healthPercent = health.hp / health.maxHp; const healthPercent = health.hp / health.maxHp;
ctx.fillStyle = healthPercent > 0.5 ? '#00ff00' : healthPercent > 0.25 ? '#ffff00' : '#ff0000'; ctx.fillStyle = healthPercent > 0.5 ? Palette.CYAN : healthPercent > 0.25 ? Palette.SKY_BLUE : Palette.WHITE;
ctx.fillRect(barX, barY, barWidth * healthPercent, barHeight); ctx.fillRect(barX, barY, Math.floor(barWidth * healthPercent), barHeight);
ctx.strokeStyle = '#ffffff'; // HP Text
ctx.lineWidth = 2; PixelFont.drawText(ctx, `${Math.ceil(health.hp)}/${health.maxHp}`, barX, barY + barHeight + 3, Palette.WHITE, 1);
ctx.strokeRect(barX, barY, barWidth, barHeight);
ctx.fillStyle = '#ffffff';
ctx.font = '14px Courier New';
ctx.fillText(`HP: ${Math.ceil(health.hp)}/${health.maxHp}`, barX + 5, barY + 15);
// Evolution display // Evolution display
const form = evolution.getDominantForm(); const form = evolution.getDominantForm();
const formY = barY + barHeight + 10; const formY = barY + barHeight + 14;
ctx.fillStyle = '#ffffff'; PixelFont.drawText(ctx, form.toUpperCase(), barX, formY, Palette.SKY_BLUE, 1);
ctx.font = '12px Courier New';
ctx.fillText(`Form: ${form.toUpperCase()}`, barX, formY);
ctx.fillText(`Human: ${evolution.human.toFixed(1)} | Beast: ${evolution.beast.toFixed(1)} | Slime: ${evolution.slime.toFixed(1)}`, barX, formY + 15);
// Instructions // Tiny evolution details
const instructionsY = formY + 40; const evoDetails = `H${Math.floor(evolution.human)} B${Math.floor(evolution.beast)} S${Math.floor(evolution.slime)}`;
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'; PixelFont.drawText(ctx, evoDetails, barX, formY + 9, Palette.ROYAL_BLUE, 1);
ctx.font = '11px Courier New';
ctx.fillText('WASD: Move | Mouse: Aim | Click/Space: Attack', barX, instructionsY);
ctx.fillText('Shift: Stealth | 1-9: Skills (Press 1 for Slime Gun)', barX, instructionsY + 15);
// Show skill hint if player has skills // Small Instructions
if (skills && skills.activeSkills.length > 0) { PixelFont.drawText(ctx, 'WASD CLICK', barX, this.engine.canvas.height - 10, Palette.DARK_BLUE, 1);
ctx.fillStyle = '#00ff96';
ctx.fillText(`You have ${skills.activeSkills.length} skill(s)! Press 1-${skills.activeSkills.length} to use them.`, barX, instructionsY + 30);
} else {
ctx.fillStyle = '#ffaa00';
ctx.fillText('Defeat and absorb creatures 5 times to learn their skills!', barX, instructionsY + 30);
}
// Health regeneration hint
ctx.fillStyle = '#00aaff';
ctx.fillText('Health regenerates when not in combat', barX, instructionsY + 45);
} }
drawSkills(player) { drawSkills(player) {
@ -137,28 +120,23 @@ export class UISystem extends System {
const ctx = this.ctx; const ctx = this.ctx;
const width = this.engine.canvas.width; const width = this.engine.canvas.width;
const startX = width - 250; const startX = width - 80;
const startY = 20; const startY = 4;
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; PixelFont.drawText(ctx, 'SKILLS', startX, startY, Palette.WHITE, 1);
ctx.fillRect(startX, startY, 230, 30 + skills.activeSkills.length * 30);
ctx.fillStyle = '#ffffff';
ctx.font = '14px Courier New';
ctx.fillText('Skills:', startX + 10, startY + 20);
skills.activeSkills.forEach((skillId, index) => { skills.activeSkills.forEach((skillId, index) => {
const y = startY + 40 + index * 30; const y = startY + 10 + index * 9;
const key = (index + 1).toString();
const onCooldown = skills.isOnCooldown(skillId); const onCooldown = skills.isOnCooldown(skillId);
const cooldown = skills.getCooldown(skillId); const cooldown = skills.getCooldown(skillId);
// Get skill name from registry for display
const skill = SkillRegistry.get(skillId); 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'; const color = onCooldown ? Palette.ROYAL_BLUE : Palette.CYAN;
ctx.fillText(`${key}. ${skillName}${onCooldown ? ` (${cooldown.toFixed(1)}s)` : ''}`, startX + 10, y); 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 ctx = this.ctx;
const width = this.engine.canvas.width; const width = this.engine.canvas.width;
const startX = width - 250; const startX = width - 80;
const startY = 200; const startY = 60;
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; PixelFont.drawText(ctx, 'STATS', startX, startY, Palette.WHITE, 1);
ctx.fillRect(startX, startY, 230, 150); 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);
ctx.fillStyle = '#ffffff';
ctx.font = '12px Courier New';
let y = startY + 20;
ctx.fillText('Stats:', startX + 10, y);
y += 20;
ctx.fillText(`STR: ${stats.strength}`, startX + 10, y);
y += 15;
ctx.fillText(`AGI: ${stats.agility}`, startX + 10, y);
y += 15;
ctx.fillText(`INT: ${stats.intelligence}`, startX + 10, y);
y += 15;
ctx.fillText(`CON: ${stats.constitution}`, startX + 10, y);
y += 15;
ctx.fillText(`PER: ${stats.perception}`, startX + 10, y);
} }
drawSkillProgress(player) { drawSkillProgress(player) {
@ -196,29 +160,24 @@ export class UISystem extends System {
const ctx = this.ctx; const ctx = this.ctx;
const width = this.engine.canvas.width; const width = this.engine.canvas.width;
const startX = width - 250; const startX = width - 80;
const startY = 360; const startY = 100;
const progress = skillProgress.getAllProgress(); const progress = skillProgress.getAllProgress();
if (progress.size === 0) return; if (progress.size === 0) return;
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; PixelFont.drawText(ctx, 'LRN', startX, startY, Palette.CYAN, 1);
ctx.fillRect(startX, startY, 230, 30 + progress.size * 25);
ctx.fillStyle = '#ffffff'; let idx = 0;
ctx.font = '12px Courier New';
ctx.fillText('Skill Progress:', startX + 10, startY + 20);
let y = startY + 35;
progress.forEach((count, skillId) => { progress.forEach((count, skillId) => {
const required = skillProgress.requiredAbsorptions; const required = skillProgress.requiredAbsorptions;
const _percent = Math.min(100, (count / required) * 100);
const skill = SkillRegistry.get(skillId); 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'; const y = startY + 9 + idx * 8;
ctx.fillText(`${skillName}: ${count}/${required}`, startX + 10, y); PixelFont.drawText(ctx, `${name} ${count}/${required}`, startX, y, Palette.SKY_BLUE, 1);
y += 20; idx++;
}); });
} }
@ -244,17 +203,9 @@ export class UISystem extends System {
drawDamageNumbers() { drawDamageNumbers() {
const ctx = this.ctx; const ctx = this.ctx;
this.damageNumbers.forEach(num => { this.damageNumbers.forEach(num => {
const alpha = Math.min(1, num.lifetime); const color = num.color.startsWith('rgba') ? num.color : Palette.WHITE;
const size = 14 + Math.min(num.value / 2, 10);
ctx.font = `bold ${size}px Courier New`; PixelFont.drawText(ctx, num.value.toString(), Math.floor(num.x), Math.floor(num.y), color, 1);
// 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);
}); });
} }
@ -270,11 +221,11 @@ export class UISystem extends System {
const width = this.engine.canvas.width; const width = this.engine.canvas.width;
this.notifications.forEach((note, index) => { this.notifications.forEach((note, index) => {
ctx.fillStyle = `rgba(255, 255, 0, ${note.alpha})`; const textWidth = PixelFont.getTextWidth(note.text, 1);
ctx.font = 'bold 20px Courier New'; const x = Math.floor((width - textWidth) / 2);
ctx.textAlign = 'center'; const y = 40 + index * 10;
ctx.fillText(note.text, width / 2, 100 + index * 30);
ctx.textAlign = 'left'; PixelFont.drawText(ctx, note.text, x, y, Palette.WHITE, 1);
}); });
} }

View file

@ -25,3 +25,4 @@ export class World {
} }
} }