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),
];