feat: Implement pixel-art rendering with new level loading, tile maps, palettes, and pixel fonts, alongside a game over screen.
This commit is contained in:
parent
5b15e63ac3
commit
cf04677511
41 changed files with 793 additions and 331 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -4,3 +4,4 @@ dist/
|
|||
*.log
|
||||
.vite/
|
||||
|
||||
|
||||
|
|
|
|||
31
index.html
31
index.html
|
|
@ -1,31 +1,56 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Slime Genesis - PoC</title>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
background: #1a1a1a;
|
||||
font-family: 'Courier New', monospace;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#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);
|
||||
border: 2px solid #4a4a4a;
|
||||
|
||||
/* Simplified scaling: Aspect ratio determines size, strictly contained */
|
||||
width: calc(100% - 500px);
|
||||
height: auto;
|
||||
aspect-ratio: 4 / 3;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="game-container">
|
||||
<canvas id="game-canvas" tabindex="0"></canvas>
|
||||
</div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
</html>
|
||||
|
|
@ -13,7 +13,7 @@ export const GameConfig = {
|
|||
},
|
||||
|
||||
Absorption: {
|
||||
range: 80,
|
||||
range: 30, // Scaled down
|
||||
healPercentMin: 0.1,
|
||||
healPercentMax: 0.2,
|
||||
skillAbsorptionChance: 0.3,
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -52,3 +52,4 @@ export class Absorbable extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ 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;
|
||||
|
||||
|
|
@ -50,3 +50,4 @@ export class Combat extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -103,3 +103,4 @@ export class Evolution extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -69,3 +69,4 @@ export class Inventory extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -9,3 +9,4 @@ export class Position extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -43,3 +43,4 @@ export class SkillProgress extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -67,3 +67,4 @@ export class Skills extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,10 @@ export class Sprite extends Component {
|
|||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -53,3 +53,4 @@ export class Stats extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -46,3 +46,4 @@ export class Stealth extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -9,3 +9,4 @@ export class Velocity extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -12,3 +12,4 @@ export class Component {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,3 +56,4 @@ export class Entity {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
22
src/core/LevelLoader.js
Normal file
22
src/core/LevelLoader.js
Normal 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
27
src/core/Palette.js
Normal 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
80
src/core/PixelFont.js
Normal 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
167
src/core/SpriteLibrary.js
Normal 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]
|
||||
]
|
||||
]
|
||||
}
|
||||
};
|
||||
|
|
@ -43,3 +43,4 @@ export class System {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
34
src/core/TileMap.js
Normal file
34
src/core/TileMap.js
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -11,3 +11,4 @@ export class Item {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -51,3 +51,4 @@ export class ItemRegistry {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
20
src/main.js
20
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);
|
||||
|
|
|
|||
|
|
@ -30,3 +30,4 @@ export class Skill {
|
|||
return !skills.isOnCooldown(this.id);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,3 +33,4 @@ export class StealthMode extends Skill {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
@ -61,7 +61,7 @@ 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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -25,3 +25,4 @@ export class HealthRegenerationSystem extends System {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,3 +39,4 @@ export class SkillEffectSystem extends System {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,3 +25,4 @@ export class World {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue