Merge pull request 'Feature/Pixel-Rework' (#4) from Feature/Pixel-Rework into main
Reviewed-on: #4
This commit is contained in:
commit
294e2dcf1f
41 changed files with 812 additions and 341 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -4,3 +4,4 @@ dist/
|
||||||
*.log
|
*.log
|
||||||
.vite/
|
.vite/
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
32
index.html
32
index.html
|
|
@ -1,31 +1,57 @@
|
||||||
<!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);
|
||||||
|
min-width: 640px;
|
||||||
|
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>
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,3 +52,4 @@ export class Absorbable extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,10 @@ 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;
|
||||||
|
|
||||||
// Combat state
|
// Combat state
|
||||||
this.isAttacking = false;
|
this.isAttacking = false;
|
||||||
this.attackDirection = 0; // Angle in radians
|
this.attackDirection = 0; // Angle in radians
|
||||||
|
|
@ -28,12 +28,12 @@ export class Combat extends Component {
|
||||||
*/
|
*/
|
||||||
attack(currentTime, direction) {
|
attack(currentTime, direction) {
|
||||||
if (!this.canAttack(currentTime)) return false;
|
if (!this.canAttack(currentTime)) return false;
|
||||||
|
|
||||||
this.lastAttackTime = currentTime;
|
this.lastAttackTime = currentTime;
|
||||||
this.isAttacking = true;
|
this.isAttacking = true;
|
||||||
this.attackDirection = direction;
|
this.attackDirection = direction;
|
||||||
this.attackCooldown = 0.3; // Attack animation duration
|
this.attackCooldown = 0.3; // Attack animation duration
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,13 @@ export class Sprite extends Component {
|
||||||
this.shape = shape; // 'circle', 'rect', 'slime'
|
this.shape = shape; // 'circle', 'rect', 'slime'
|
||||||
this.alpha = 1.0;
|
this.alpha = 1.0;
|
||||||
this.scale = 1.0;
|
this.scale = 1.0;
|
||||||
|
|
||||||
// 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 { 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,17 +16,27 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a system to the engine
|
* Add a system to the engine
|
||||||
*/
|
*/
|
||||||
addSystem(system) {
|
addSystem(system) {
|
||||||
if (system instanceof System) {
|
if (system instanceof System) {
|
||||||
system.init(this);
|
system.init(this);
|
||||||
|
|
@ -37,22 +48,22 @@ export class Engine {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emit an event locally
|
* Emit an event locally
|
||||||
*/
|
*/
|
||||||
emit(event, data) {
|
emit(event, data) {
|
||||||
this.events.emit(event, data);
|
this.events.emit(event, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscribe to an event
|
* Subscribe to an event
|
||||||
*/
|
*/
|
||||||
on(event, callback) {
|
on(event, callback) {
|
||||||
return this.events.on(event, callback);
|
return this.events.on(event, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create and add an entity
|
* Create and add an entity
|
||||||
*/
|
*/
|
||||||
createEntity() {
|
createEntity() {
|
||||||
const entity = new Entity();
|
const entity = new Entity();
|
||||||
this.entities.push(entity);
|
this.entities.push(entity);
|
||||||
|
|
@ -60,8 +71,8 @@ export class Engine {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove an entity
|
* Remove an entity
|
||||||
*/
|
*/
|
||||||
removeEntity(entity) {
|
removeEntity(entity) {
|
||||||
const index = this.entities.indexOf(entity);
|
const index = this.entities.indexOf(entity);
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
|
|
@ -70,15 +81,15 @@ export class Engine {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all entities
|
* Get all entities
|
||||||
*/
|
*/
|
||||||
getEntities() {
|
getEntities() {
|
||||||
return this.entities.filter(e => e.active);
|
return this.entities.filter(e => e.active);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main game loop
|
* Main game loop
|
||||||
*/
|
*/
|
||||||
start() {
|
start() {
|
||||||
if (this.running) return;
|
if (this.running) return;
|
||||||
this.running = true;
|
this.running = true;
|
||||||
|
|
@ -87,15 +98,15 @@ export class Engine {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop the game loop
|
* Stop the game loop
|
||||||
*/
|
*/
|
||||||
stop() {
|
stop() {
|
||||||
this.running = false;
|
this.running = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Game loop using requestAnimationFrame
|
* Game loop using requestAnimationFrame
|
||||||
*/
|
*/
|
||||||
gameLoop = (currentTime = 0) => {
|
gameLoop = (currentTime = 0) => {
|
||||||
if (!this.running) return;
|
if (!this.running) return;
|
||||||
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -130,8 +141,8 @@ export class Engine {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear the canvas
|
* Clear the canvas
|
||||||
*/
|
*/
|
||||||
clear() {
|
clear() {
|
||||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
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
|
// 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);
|
||||||
|
|
|
||||||
|
|
@ -30,3 +30,4 @@ export class Skill {
|
||||||
return !skills.isOnCooldown(this.id);
|
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);
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,7 +52,7 @@ export class SlimeGun extends Skill {
|
||||||
const startX = position.x;
|
const startX = position.x;
|
||||||
const startY = position.y;
|
const startY = position.y;
|
||||||
projectile.addComponent(new Position(startX, startY));
|
projectile.addComponent(new Position(startX, startY));
|
||||||
|
|
||||||
// Create velocity with high maxSpeed for projectiles
|
// Create velocity with high maxSpeed for projectiles
|
||||||
const projectileVelocity = new Velocity(
|
const projectileVelocity = new Velocity(
|
||||||
Math.cos(shootAngle) * this.speed,
|
Math.cos(shootAngle) * this.speed,
|
||||||
|
|
@ -61,8 +61,8 @@ 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);
|
||||||
projectileHealth.isProjectile = true;
|
projectileHealth.isProjectile = true;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -25,3 +25,4 @@ export class HealthRegenerationSystem extends System {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -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() {
|
updatePreviousStates() {
|
||||||
// Deep copy current states to previous for next frame
|
// Deep copy current states to previous for next frame
|
||||||
this.keysPrevious = {};
|
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) {
|
isKeyPressed(key) {
|
||||||
return this.keys[key.toLowerCase()] === true;
|
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) {
|
isKeyJustPressed(key) {
|
||||||
const keyLower = key.toLowerCase();
|
const keyLower = key.toLowerCase();
|
||||||
const isPressed = this.keys[keyLower] === true;
|
const isPressed = this.keys[keyLower] === true;
|
||||||
|
|
@ -128,22 +132,22 @@ export class InputSystem extends System {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get mouse position
|
* Get mouse position
|
||||||
*/
|
*/
|
||||||
getMousePosition() {
|
getMousePosition() {
|
||||||
return { x: this.mouse.x, y: this.mouse.y };
|
return { x: this.mouse.x, y: this.mouse.y };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if mouse button is pressed
|
* Check if mouse button is pressed
|
||||||
*/
|
*/
|
||||||
isMouseButtonPressed(button = 0) {
|
isMouseButtonPressed(button = 0) {
|
||||||
return this.mouse.buttons[button] === true;
|
return this.mouse.buttons[button] === true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if mouse button was just pressed
|
* Check if mouse button was just pressed
|
||||||
*/
|
*/
|
||||||
isMouseButtonJustPressed(button = 0) {
|
isMouseButtonJustPressed(button = 0) {
|
||||||
const isPressed = this.mouse.buttons[button] === true;
|
const isPressed = this.mouse.buttons[button] === true;
|
||||||
const wasPressed = this.mouse.buttonsPrevious[button] === true;
|
const wasPressed = this.mouse.buttonsPrevious[button] === true;
|
||||||
|
|
|
||||||
|
|
@ -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,62 @@ 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, 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, 45, Palette.WHITE, 1);
|
||||||
|
|
||||||
|
// Draw Stats and Knowledge (Moved from HUD)
|
||||||
|
const player = this.engine.getEntities().find(e => e.hasComponent('Evolution'));
|
||||||
|
const uiSystem = this.engine.systems.find(s => s.name === 'UISystem');
|
||||||
|
|
||||||
|
if (player && uiSystem) {
|
||||||
|
// Draw Stats on the left
|
||||||
|
uiSystem.drawStats(player, 20, 80);
|
||||||
|
|
||||||
|
// Draw Learning Progress on the right
|
||||||
|
uiSystem.drawSkillProgress(player, width - 110, 80);
|
||||||
|
}
|
||||||
|
} else if (this.gameState === 'gameOver') {
|
||||||
|
const dead = 'YOU PERISHED';
|
||||||
|
const deadW = PixelFont.getTextWidth(dead, 2);
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ export class MovementSystem extends System {
|
||||||
const position = entity.getComponent('Position');
|
const position = entity.getComponent('Position');
|
||||||
const velocity = entity.getComponent('Velocity');
|
const velocity = entity.getComponent('Velocity');
|
||||||
const health = entity.getComponent('Health');
|
const health = entity.getComponent('Health');
|
||||||
|
|
||||||
if (!position || !velocity) return;
|
if (!position || !velocity) return;
|
||||||
|
|
||||||
// Check if this is a projectile
|
// Check if this is a projectile
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,3 +39,4 @@ export class SkillEffectSystem extends System {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
@ -62,8 +64,7 @@ export class UISystem extends System {
|
||||||
// Draw UI
|
// Draw UI
|
||||||
this.drawHUD(player);
|
this.drawHUD(player);
|
||||||
this.drawSkills(player);
|
this.drawSkills(player);
|
||||||
this.drawStats(player);
|
// REMOVED drawStats and drawSkillProgress from active gameplay
|
||||||
this.drawSkillProgress(player);
|
|
||||||
this.drawDamageNumbers();
|
this.drawDamageNumbers();
|
||||||
this.drawNotifications();
|
this.drawNotifications();
|
||||||
this.drawAbsorptionEffects();
|
this.drawAbsorptionEffects();
|
||||||
|
|
@ -73,62 +74,36 @@ 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
|
|
||||||
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);
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
drawSkills(player) {
|
drawSkills(player) {
|
||||||
|
|
@ -137,88 +112,68 @@ 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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
drawStats(player) {
|
drawStats(player, x, y) {
|
||||||
const stats = player.getComponent('Stats');
|
const stats = player.getComponent('Stats');
|
||||||
if (!stats) return;
|
const evolution = player.getComponent('Evolution');
|
||||||
|
if (!stats || !evolution) return;
|
||||||
|
|
||||||
const ctx = this.ctx;
|
const ctx = this.ctx;
|
||||||
const width = this.engine.canvas.width;
|
PixelFont.drawText(ctx, 'STATISTICS', x, y, Palette.WHITE, 1);
|
||||||
const startX = width - 250;
|
PixelFont.drawText(ctx, `STR ${stats.strength}`, x, y + 10, Palette.ROYAL_BLUE, 1);
|
||||||
const startY = 200;
|
PixelFont.drawText(ctx, `AGI ${stats.agility}`, x, y + 20, Palette.ROYAL_BLUE, 1);
|
||||||
|
PixelFont.drawText(ctx, `INT ${stats.intelligence}`, x, y + 30, Palette.ROYAL_BLUE, 1);
|
||||||
|
PixelFont.drawText(ctx, `CON ${stats.constitution}`, x, y + 40, Palette.ROYAL_BLUE, 1);
|
||||||
|
|
||||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
|
PixelFont.drawText(ctx, 'EVOLUTION', x, y + 60, Palette.WHITE, 1);
|
||||||
ctx.fillRect(startX, startY, 230, 150);
|
PixelFont.drawText(ctx, `HUMAN: ${Math.floor(evolution.human)}`, x, y + 70, Palette.ROYAL_BLUE, 1);
|
||||||
|
PixelFont.drawText(ctx, `BEAST: ${Math.floor(evolution.beast)}`, x, y + 80, Palette.ROYAL_BLUE, 1);
|
||||||
ctx.fillStyle = '#ffffff';
|
PixelFont.drawText(ctx, `SLIME: ${Math.floor(evolution.slime)}`, x, y + 90, Palette.ROYAL_BLUE, 1);
|
||||||
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, x, y) {
|
||||||
const skillProgress = player.getComponent('SkillProgress');
|
const skillProgress = player.getComponent('SkillProgress');
|
||||||
if (!skillProgress) return;
|
if (!skillProgress) return;
|
||||||
|
|
||||||
const ctx = this.ctx;
|
const ctx = this.ctx;
|
||||||
const width = this.engine.canvas.width;
|
|
||||||
const startX = width - 250;
|
|
||||||
const startY = 360;
|
|
||||||
|
|
||||||
const progress = skillProgress.getAllProgress();
|
const progress = skillProgress.getAllProgress();
|
||||||
if (progress.size === 0) return;
|
|
||||||
|
|
||||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
|
PixelFont.drawText(ctx, 'KNOWLEDGE', x, y, Palette.CYAN, 1);
|
||||||
ctx.fillRect(startX, startY, 230, 30 + progress.size * 25);
|
|
||||||
|
|
||||||
ctx.fillStyle = '#ffffff';
|
if (progress.size === 0) {
|
||||||
ctx.font = '12px Courier New';
|
PixelFont.drawText(ctx, 'NONE YET', x, y + 10, Palette.DARK_BLUE, 1);
|
||||||
ctx.fillText('Skill Progress:', startX + 10, startY + 20);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let y = startY + 35;
|
let idx = 0;
|
||||||
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 > 10) name = name.substring(0, 10);
|
||||||
|
|
||||||
ctx.fillStyle = count >= required ? '#00ff00' : '#ffff00';
|
const py = y + 10 + idx * 9;
|
||||||
ctx.fillText(`${skillName}: ${count}/${required}`, startX + 10, y);
|
PixelFont.drawText(ctx, `${name}: ${count}/${required}`, x, py, Palette.SKY_BLUE, 1);
|
||||||
y += 20;
|
idx++;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -244,17 +199,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 +217,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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,3 +25,4 @@ export class World {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue