diff --git a/index.html b/index.html index fce0816..2f0e048 100644 --- a/index.html +++ b/index.html @@ -14,8 +14,8 @@ } #app { position: absolute; - width: 640px; - height: 480px; + width: 800px; + height: 600px; } diff --git a/src/global.d.ts b/src/global.d.ts index b52cb8a..b4e43a3 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -52,12 +52,22 @@ interface SoundPreset { volume?: number; } -interface Texture { +interface AnimationSpritePreset { id: string; - imageData: ImageData; + frames: string[] +} + +interface TextureBitmap { width: number; height: number; colors: Color[][]; } +interface Color { + r: number; + g: number; + b: number; + a: number; +} + type Sprite = Texture \ No newline at end of file diff --git a/src/levels/base.ts b/src/levels/base.ts index 316b3bf..93f2a3c 100644 --- a/src/levels/base.ts +++ b/src/levels/base.ts @@ -1,25 +1,23 @@ -import { Color } from "src/managers/TextureManager"; - const random = (from: number, to: number) => { return from + Math.random() * (to - from); }; const generateEnemies = (limit: number) => { return new Array(limit).fill(0).map(() => ({ - x: random(2, 4), - y: random(3, 8), - angle: 45, - health: 100, - sprite: "soldier", - radius: 0.4, - })) -} + x: random(2, 4), + y: random(3, 8), + angle: 45, + health: 100, + sprite: "zombie", + radius: 0.4, + })); +}; const level: Level = { world: { colors: { - top: new Color(0, 0, 0, 255), - bottom: new Color(84, 98, 92, 255), + top: { r: 0, g: 0, b: 0, a: 255 }, + bottom: { r: 84, g: 98, b: 92, a: 255 }, }, }, map: [ @@ -45,8 +43,8 @@ const level: Level = { angle: 0, health: 100, }, - enemies: generateEnemies(10) - /* + enemies: generateEnemies(10), + /* [ { x: 3, @@ -57,7 +55,6 @@ const level: Level = { radius: 0.4, }, ]*/ - , exit: { x: 4, y: 5, diff --git a/src/lib/ecs/components/AIComponent.ts b/src/lib/ecs/components/AIComponent.ts index 939080d..09a3412 100644 --- a/src/lib/ecs/components/AIComponent.ts +++ b/src/lib/ecs/components/AIComponent.ts @@ -2,6 +2,8 @@ import { Component } from "src/lib/ecs/Component"; export default class AIComponent implements Component { distance: number; + lastAttackTime: number = 0; + damagePerSecond: number = 5; constructor(distance: number) { this.distance = distance; diff --git a/src/lib/ecs/components/AnimatedSpriteComponent.ts b/src/lib/ecs/components/AnimatedSpriteComponent.ts deleted file mode 100644 index 2c8f069..0000000 --- a/src/lib/ecs/components/AnimatedSpriteComponent.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Component } from "src/lib/ecs/Component"; - -type Sprite = Texture; - -export default class SpriteComponent implements Component { - sprite: Sprite; - - constructor(sprite: Sprite) { - this.sprite = sprite; - } -} diff --git a/src/lib/ecs/components/AnimationComponent.ts b/src/lib/ecs/components/AnimationComponent.ts new file mode 100644 index 0000000..2eacb14 --- /dev/null +++ b/src/lib/ecs/components/AnimationComponent.ts @@ -0,0 +1,36 @@ +import { Component } from "src/lib/ecs/Component"; + +export default class AnimatedSpriteComponent implements Component { + + states: Record = {}; + currentState: string; + currentFrame: number; + sprite: TextureBitmap; + animationSpeed: number = 0.2; + timeSinceLastFrame: number = 0; + + constructor(initialState: string, states: Record) { + this.states = states; + this.currentFrame = 0; + this.currentState = initialState; + this.sprite = states[initialState][0]; + } + + // @TODO: improve + update(dt: number) { + const frames = this.states[this.currentState]; + this.timeSinceLastFrame += dt; + + if (this.timeSinceLastFrame > this.animationSpeed) { + this.currentFrame = (this.currentFrame + 1) % frames.length; + this.sprite = frames[this.currentFrame]; + this.timeSinceLastFrame = 0; + } + } + + switchState(stateName: string) { + if (stateName in this.states) { + this.currentState = stateName; + } + } +} diff --git a/src/lib/ecs/systems/AISystem.ts b/src/lib/ecs/systems/AISystem.ts index 484e843..1c948b0 100644 --- a/src/lib/ecs/systems/AISystem.ts +++ b/src/lib/ecs/systems/AISystem.ts @@ -4,14 +4,17 @@ import PositionComponent from "src/lib/ecs/components/PositionComponent"; import QuerySystem from "../lib/QuerySystem"; import AIComponent from "../components/AIComponent"; import CameraComponent from "../components/CameraComponent"; +import CircleComponent from "../components/CircleComponent"; +import HealthComponent from "../components/HealthComponent"; +import AnimatedSpriteComponent from "../components/AnimationComponent"; export default class AISystem extends System { - requiredComponents = [AIComponent, PositionComponent]; + requiredComponents = [AIComponent, CircleComponent, PositionComponent]; camera: Entity; constructor(querySystem: QuerySystem) { super(querySystem); - const [camera] = this.querySystem.query([CameraComponent]); + const [camera] = this.querySystem.query([HealthComponent, CircleComponent, CameraComponent]); this.camera = camera; } @@ -20,26 +23,42 @@ export default class AISystem extends System { update(dt: number, entities: Entity[]) { const cameraPosition = this.camera.getComponent(PositionComponent); + const cameraCircle = this.camera.getComponent(CircleComponent); + const cameraHealth = this.camera.getComponent(HealthComponent); entities.forEach((entity: Entity) => { const entityAI = entity.getComponent(AIComponent); const entityPosition = entity.getComponent(PositionComponent); + const entityCircle = entity.getComponent(CircleComponent); + const entityAnimation = entity.getComponent(AnimatedSpriteComponent); const dx = cameraPosition.x - entityPosition.x; const dy = cameraPosition.y - entityPosition.y; - const distance = Math.sqrt(dx**2 + dy**2); + const distance = Math.sqrt(dx**2 + dy**2) - cameraCircle.radius - entityCircle.radius; + // @TODO: move to move system if (entityAI.distance > distance) { - this.moveAI(dt, dx, dy, entity); + entityAnimation.switchState('walk'); + entityPosition.x += dx * dt + entityPosition.y += dy * dt + } else { + entityAnimation.switchState('idle'); + } + + if (distance < 0) { + entityAI.lastAttackTime += dt; + } else { + entityAI.lastAttackTime = 0; } - }); - } - moveAI(dt:number, dx: number, dy: number, entity: Entity) { - entity.getComponent(PositionComponent).x += dx * dt - entity.getComponent(PositionComponent).y += dy * dt + if (entityAI.lastAttackTime >= 1) { + cameraHealth.current -= entityAI.damagePerSecond; + entityAI.lastAttackTime = 0; + } + }); } destroy(){} -} + +} \ No newline at end of file diff --git a/src/lib/ecs/systems/AnimationSystem.ts b/src/lib/ecs/systems/AnimationSystem.ts new file mode 100644 index 0000000..379d6d3 --- /dev/null +++ b/src/lib/ecs/systems/AnimationSystem.ts @@ -0,0 +1,18 @@ +import Entity from "src/lib/ecs/Entity"; +import System from "src/lib/ecs/System"; +import AnimatedSpriteComponent from "../components/AnimationComponent"; + + +export default class AnimationSystem extends System { + requiredComponents = [AnimatedSpriteComponent]; + + start(): void {} + + update(dt: number, entities: Entity[]) { + entities.forEach((entity) => { + entity.getComponent(AnimatedSpriteComponent).update(dt); + }); + } + + destroy(): void {} +} diff --git a/src/lib/ecs/systems/MinimapSystem.ts b/src/lib/ecs/systems/MinimapSystem.ts index 6d4aee7..2b49436 100644 --- a/src/lib/ecs/systems/MinimapSystem.ts +++ b/src/lib/ecs/systems/MinimapSystem.ts @@ -28,7 +28,7 @@ export default class MinimapSystem extends System { this.canvas = new Canvas({ height: rows * this.scale, width: cols * this.scale, - }); + }); } start() { @@ -37,6 +37,7 @@ export default class MinimapSystem extends System { update(_: number, entities: Entity[]) { this.canvas.clear(); + this.canvas.drawBackground('green'); entities.forEach((entity) => { const { x, y } = entity.getComponent(PositionComponent); diff --git a/src/lib/ecs/systems/MoveSystem.ts b/src/lib/ecs/systems/MoveSystem.ts index 6ea9771..def0ba5 100644 --- a/src/lib/ecs/systems/MoveSystem.ts +++ b/src/lib/ecs/systems/MoveSystem.ts @@ -101,11 +101,11 @@ export default class MoveSystem extends System { const newX = positionComponent.x + k * playerCos * moveComponent.moveSpeed * dt; const newY = positionComponent.y + k * playerSin * moveComponent.moveSpeed * dt; - if (newX < 0 || newX > this.cols) { + if (newX <= 0 || newX > this.cols) { return } - if (newY < 0 || newY > this.rows) { + if (newY <= 0 || newY > this.rows) { return } diff --git a/src/lib/ecs/systems/CameraSystem.ts b/src/lib/ecs/systems/RenderSystem.ts similarity index 92% rename from src/lib/ecs/systems/CameraSystem.ts rename to src/lib/ecs/systems/RenderSystem.ts index 4e5bfe2..fb96abc 100644 --- a/src/lib/ecs/systems/CameraSystem.ts +++ b/src/lib/ecs/systems/RenderSystem.ts @@ -11,8 +11,9 @@ import QuerySystem from "../lib/QuerySystem"; import SpriteComponent from "../components/SpriteComponent"; import TextureManager from "src/managers/TextureManager"; import PolarMap, { PolarPosition } from "../lib/PolarMap"; +import AnimatedSpriteComponent from "../components/AnimationComponent"; -export default class CameraSystem extends System { +export default class RenderSystem extends System { requiredComponents = [CameraComponent, PositionComponent]; protected walls: PositionMap; @@ -107,12 +108,12 @@ export default class CameraSystem extends System { rayX += incrementRayX; rayY += incrementRayY; - if (rayX < 0 || rayX > this.walls.cols) { + if (rayX < 0 || rayX > this.walls.rows) { isPropogating = false; continue; } - if (rayY < 0 || rayY > this.walls.rows) { + if (rayY < 0 || rayY > this.walls.cols) { isPropogating = false; continue; } @@ -205,24 +206,27 @@ export default class CameraSystem extends System { } _drawSpriteLine(screenX: number, rayAngle: number, polarEntity: PolarPosition){ + const animateComponent = polarEntity.entity.getComponent(AnimatedSpriteComponent).sprite; const spriteComponent = polarEntity.entity.getComponent(SpriteComponent).sprite; const projectionHeight = Math.floor(this.height / 2 / polarEntity.distance); + const sprite = animateComponent || spriteComponent; + const a1 = normalizeAngle(rayAngle - polarEntity.angleFrom); const a2 = normalizeAngle(polarEntity.angleTo - polarEntity.angleFrom); - const xTexture = Math.floor(a1 / a2 * spriteComponent.width) + const xTexture = Math.floor(a1 / a2 * sprite.width) - const yIncrementer = (projectionHeight * 2) / spriteComponent.height; + const yIncrementer = (projectionHeight * 2) / sprite.height; let y = this.height / 2 - projectionHeight; - for (let i = 0; i < spriteComponent.height; i++) { + for (let i = 0; i < sprite.height; i++) { if (y > -yIncrementer && y < this.height) { this.canvas.drawVerticalLine({ x: screenX, y1: y, y2: Math.floor(y + yIncrementer), - color: spriteComponent.colors[i][xTexture], + color: sprite.colors[i][xTexture], }); } y += yIncrementer; diff --git a/src/lib/image.ts b/src/lib/image.ts new file mode 100644 index 0000000..52b6f2c --- /dev/null +++ b/src/lib/image.ts @@ -0,0 +1,55 @@ +export async function extractTextureBitmap(url: string) { + const image = await loadImage(url); + const imageData = await extractImageData(image); + const colors = await extractColors(image.height, image.width, imageData.data); + + return { + height: image.height, + width: image.width, + colors, + } as TextureBitmap; +} + +async function loadImage(url: string): Promise { + return new Promise((resolve, reject) => { + const element = document.createElement("img"); + + element.src = url; + element.onerror = () => reject(); + element.onload = () => resolve(element); + }); +} + +async function extractImageData(image: HTMLImageElement) { + const canvas = document.createElement("canvas"); + canvas.width = image.width; + canvas.height = image.height; + + const context = canvas.getContext("2d")!; + + context.drawImage(image, 0, 0, image.width, image.height); + + return context.getImageData(0, 0, image.width, image.height); +} + +async function extractColors( + height: number, + width: number, + imageData: Uint8ClampedArray +) { + const colors: Color[][] = []; + for (let y = 0; y < height; y++) { + const row: Color[] = []; + for (let x = 0; x < width; x++) { + const i = x * 4 + y * width * 4; + row.push({ + r: imageData[i], + g: imageData[i + 1], + b: imageData[i + 2], + a: imageData[i + 3], + }); + } + colors.push(row); + } + return colors; +} diff --git a/src/lib/world.ts b/src/lib/world.ts index 82ad7f3..21557d9 100644 --- a/src/lib/world.ts +++ b/src/lib/world.ts @@ -12,9 +12,12 @@ import CollisionComponent from "./ecs/components/CollisionComponent"; import HealthComponent from "./ecs/components/HealthComponent"; import SpriteComponent from "./ecs/components/SpriteComponent"; import CircleComponent from "./ecs/components/CircleComponent"; +import AIComponent from "./ecs/components/AIComponent"; +import AnimatedSpriteComponent from "./ecs/components/AnimationComponent"; +import AnimationManager from "src/managers/AnimationManager"; // import AIComponent from "./ecs/components/AIComponent"; -export function createEntities(level: Level, textureManager: TextureManager) { +export function createEntities(level: Level, textureManager: TextureManager, animationManager: AnimationManager,) { // player const player = new Entity(); @@ -33,7 +36,13 @@ export function createEntities(level: Level, textureManager: TextureManager) { const entity = new Entity(); const texture = textureManager.get(enemy.sprite); - // entity.addComponent(new AIComponent(2)); + entity.addComponent(new AIComponent(2)); + entity.addComponent(new AnimatedSpriteComponent('idle', { + 'idle': animationManager.get('zombieIdle'), + 'damage': animationManager.get('zombieDamage'), + 'death': animationManager.get('zombieDeath'), + 'walk': animationManager.get('zombieWalk'), + })), entity.addComponent(new CircleComponent(enemy.radius)); entity.addComponent(new PositionComponent(enemy.x, enemy.y)); entity.addComponent(new HealthComponent(enemy.health, enemy.health)); diff --git a/src/main.ts b/src/main.ts index a62460b..cd5a93f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,16 +5,19 @@ import SoundManager from "./managers/SoundManager.ts"; import TextureManager from "./managers/TextureManager.ts"; import LevelScene from "./scenes/LevelScene.ts"; import TitleScene from "./scenes/TitleScene.ts"; +import AnimationManager from "./managers/AnimationManager.ts"; const container = document.getElementById('app')!; const soundManager = new SoundManager(); const textureManager = new TextureManager(); +const animationManager = new AnimationManager(); window.onload = async () => { try { await Promise.all([ await soundManager.load(presets.sounds), await textureManager.load([...presets.textures, ...presets.sprites]), + await animationManager.load(presets.animation), ]); const introScene = new TitleScene(container, [ @@ -26,7 +29,8 @@ window.onload = async () => { container, level, soundManager, - textureManager + textureManager, + animationManager, }); const winScene = new TitleScene(container, [ diff --git a/src/managers/AnimationManager.ts b/src/managers/AnimationManager.ts new file mode 100644 index 0000000..13e25ed --- /dev/null +++ b/src/managers/AnimationManager.ts @@ -0,0 +1,17 @@ +import { extractTextureBitmap } from "src/lib/image"; + +export default class AnimationManager { + private animations: Record = {}; + + async load(presets: AnimationSpritePreset[]) { + for (const preset of presets) { + this.animations[preset.id] = await Promise.all( + preset.frames.map(async url => await extractTextureBitmap(url)) + ); + } + } + + get(id: string) { + return this.animations[id]; + } +} diff --git a/src/managers/TextureManager.ts b/src/managers/TextureManager.ts index d63aaf8..228f32d 100644 --- a/src/managers/TextureManager.ts +++ b/src/managers/TextureManager.ts @@ -1,77 +1,15 @@ -export class Color { - r: number; - g: number; - b: number; - a: number; - - constructor(r: number, g: number, b: number, a: number) { - this.r = r; - this.g = g; - this.b = b; - this.a = a; - } -} +import { extractTextureBitmap } from "src/lib/image"; export default class TextureManager { - private textures: Record = {}; + private textures: Record = {}; async load(presets: TexturePreset[]) { - await Promise.all( - presets.map(async (preset) => { - const { id, url } = preset; - const image = await loadPresetImage(url); - const imageData = await extractImageData(image); - const colors = await extractColors(image.height, image.width, imageData.data); - - this.textures[id] = { - id, - imageData, - height: image.height, - width: image.width, - colors, - }; - }) - ); + for (const preset of presets) { + this.textures[preset.id] = await extractTextureBitmap(preset.url); + } } get(id: string) { return this.textures[id]; } } - -async function loadPresetImage( - url: string -): Promise { - return new Promise((resolve, reject) => { - const element = document.createElement("img"); - - element.src = url; - element.onerror = () => reject(); - element.onload = () => resolve(element); - }); -} - -async function extractImageData(image: HTMLImageElement) { - const canvas = document.createElement("canvas"); - canvas.width = image.width; - canvas.height = image.height; - - const context = canvas.getContext("2d")!; - - context.drawImage(image, 0, 0, image.width, image.height); - - return context.getImageData(0, 0, image.width, image.height); -} - -async function extractColors(height: number, width: number, imageData: Uint8ClampedArray) { - const colors: Color[][] = []; - for (let y = 0; y < height; y++) { - const row: Color[] = []; - for (let x = 0; x < width; x++) { - const i = x * 4 + y * width * 4; - row.push(new Color(imageData[i], imageData[i + 1], imageData[i + 2], imageData[i + 3])); - } - colors.push(row); - } - return colors; -} diff --git a/src/presets.ts b/src/presets.ts index 0774754..56102bd 100644 --- a/src/presets.ts +++ b/src/presets.ts @@ -27,6 +27,41 @@ export const sprites: SpritePreset[] = [ url: "./assets/characters/ZombieIdle.png" } ]; + +export const animation: AnimationSpritePreset[] = [ + { + id: "zombieIdle", + frames: [ + "./assets/characters/ZombieIdle.png", + ], + }, + { + id: "zombieWalk", + frames: [ + "./assets/characters/ZombieWalk1.png", + "./assets/characters/ZombieWalk2.png", + "./assets/characters/ZombieWalk3.png", + "./assets/characters/ZombieWalk4.png", + ], + }, + { + id: "zombieDamage", + frames: [ + "./assets/characters/ZombieDamage1.png", + "./assets/characters/ZombieDamage2.png", + ], + }, + { + id: "zombieDeath", + frames: [ + "./assets/characters/ZombieDeath1.png", + "./assets/characters/ZombieDeath2.png", + "./assets/characters/ZombieDeath3.png", + "./assets/characters/ZombieDeath4.png", + ], + }, +]; + export const sounds: SoundPreset[] = [ { diff --git a/src/scenes/LevelScene.ts b/src/scenes/LevelScene.ts index 9c00aec..52763c0 100644 --- a/src/scenes/LevelScene.ts +++ b/src/scenes/LevelScene.ts @@ -6,19 +6,22 @@ import System from "src/lib/ecs/System"; import MinimapSystem from "src/lib/ecs/systems/MinimapSystem"; import ControlSystem from "src/lib/ecs/systems/ControlSystem"; import MoveSystem from "src/lib/ecs/systems/MoveSystem"; -import CameraSystem from "src/lib/ecs/systems/CameraSystem"; +import RenderSystem from "src/lib/ecs/systems/RenderSystem"; import { createEntities } from "src/lib/world"; import QuerySystem from "src/lib/ecs/lib/QuerySystem"; import CameraComponent from "src/lib/ecs/components/CameraComponent"; import PositionComponent from "src/lib/ecs/components/PositionComponent"; import BaseScene from "./BaseScene"; import AISystem from "src/lib/ecs/systems/AISystem"; +import AnimationManager from "src/managers/AnimationManager"; +import AnimationSystem from "src/lib/ecs/systems/AnimationSystem"; interface LevelSceneProps { container: HTMLElement; level: Level; soundManager: SoundManager; textureManager: TextureManager; + animationManager: AnimationManager; } export default class LevelScene implements BaseScene { @@ -30,17 +33,18 @@ export default class LevelScene implements BaseScene { protected readonly systems: System[]; protected readonly entities: Entity[]; - constructor({ container, level, textureManager }: LevelSceneProps) { + constructor({ container, level, textureManager, animationManager }: LevelSceneProps) { this.level = level; - const entities = createEntities(level, textureManager); + const entities = createEntities(level, textureManager, animationManager); const querySystem = new QuerySystem(entities); this.systems = [ new ControlSystem(querySystem), new AISystem(querySystem), new MoveSystem(querySystem, level), - new CameraSystem(querySystem, container, level, textureManager), + new AnimationSystem(querySystem), + new RenderSystem(querySystem, container, level, textureManager), new MinimapSystem(querySystem, container, level), ];