diff --git a/packages/assets/package.json b/packages/assets/package.json index e0ceea7..7e8a424 100644 --- a/packages/assets/package.json +++ b/packages/assets/package.json @@ -1,13 +1,9 @@ { "name": "@serbanghita-gamedev/assets", - "description": "Asset related utils (JSON, Images, etc)", - "version": "0.0.1", + "description": "Opinionated asset (JSON, Images, etc) related utility library", + "version": "0.0.2", "author": "Serban Ghita (https://ghita.org)", "license": "MIT", - "publishConfig": { - "access": "public", - "registry": "https://registry.npmjs.org/" - }, "main": "src/index.ts", "type": "module", "scripts": { diff --git a/packages/assets/src/index.ts b/packages/assets/src/index.ts index 0f9bc57..159b579 100644 --- a/packages/assets/src/index.ts +++ b/packages/assets/src/index.ts @@ -1,15 +1,39 @@ +import { SpriteSheetAnimation } from "@serbanghita-gamedev/component"; +import { EntityDeclaration } from "@serbanghita-gamedev/ecs"; +import { TiledMapFile } from "@serbanghita-gamedev/tiled"; +import { PhysicsBodyPropsDeclaration } from "@serbanghita-gamedev/component/PhysicsBody"; +import { SpriteSheetPropsDeclaration } from "@serbanghita-gamedev/component/SpriteSheet"; + export async function loadLocalImage(data: string): Promise { - const img = new Image(); - const test1 = data.match(/([a-z0-9-_]+).(png|gif|jpg)$/i); - const test2 = data.match(/^data:image\//i); - if (!test1 && !test2) { - throw new Error(`Trying to an load an invalid image ${data}.`); - } - - return new Promise((resolve) => { - img.src = data; - img.onload = function() { - resolve(this as HTMLImageElement); - } - }); -} \ No newline at end of file + const img = new Image(); + const test1 = data.match(/([a-z0-9-_]+).(png|gif|jpg)$/i); + const test2 = data.match(/^data:image\//i); + if (!test1 && !test2) { + throw new Error(`Trying to an load an invalid image ${data}.`); + } + + return new Promise((resolve) => { + img.src = data; + img.onload = function () { + resolve(this as HTMLImageElement); + }; + }); +} + +export type Assets = { + "entities/images": { [key: string]: HTMLImageElement }; + "entities/animations": { [key: string]: { [key: string]: SpriteSheetAnimation } }; + "entities/declarations": EntityDeclaration[]; + "maps/images": { [key: string]: HTMLImageElement }; + "maps/declarations": { [key: string]: TiledMapFile }; +}; + +export type EntityDeclaration = { + id: string; + components: { + [componentName: string]: object; + + PhysicsBody: PhysicsBodyPropsDeclaration; + SpriteSheet: SpriteSheetPropsDeclaration; + }; +}; diff --git a/packages/assets/tsconfig.json b/packages/assets/tsconfig.json new file mode 100644 index 0000000..743f875 --- /dev/null +++ b/packages/assets/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "module": "commonjs", /* Specify what module code is generated. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + "strict": true, /* Enable all strict type-checking options. */ + "skipLibCheck": true, /* Skip type checking all .d.ts files. */ + "rootDirs": ["src"], + "baseUrl": "src", + "sourceMap": true, + "paths": { + "@serbanghita-gamedev/component/*": ["../../component/src/*"], + "@serbanghita-gamedev/input/*": ["../../input/src/*"], + "@serbanghita-gamedev/renderer/*": ["../../renderer/src/*"], + "@serbanghita-gamedev/ecs/*": ["../../ecs/src/*"], + "@serbanghita-gamedev/tiled/*": ["../../tiled/src/*"], + } + }, + "include": [ + "src/index.ts" + ], + "exclude": [ + "node_modules", + "dist" + ] +} diff --git a/packages/component/src/PhysicsBody.ts b/packages/component/src/PhysicsBody.ts new file mode 100644 index 0000000..ee9c103 --- /dev/null +++ b/packages/component/src/PhysicsBody.ts @@ -0,0 +1,19 @@ +import { Component } from "@serbanghita-gamedev/ecs"; +import { Rectangle, Point } from "@serbanghita-gamedev/geometry"; + +export interface PhysicsBodyProps { + width: number; + height: number; + x: number; + y: number; + rectangle: Rectangle; + point: Point; +} + +export type PhysicsBodyPropsDeclaration = Exclude; + +export default class PhysicsBody extends Component { + constructor(public properties: PhysicsBodyProps) { + super(properties); + } +} diff --git a/packages/component/src/SpriteSheet.ts b/packages/component/src/SpriteSheet.ts index d13a995..6091b03 100644 --- a/packages/component/src/SpriteSheet.ts +++ b/packages/component/src/SpriteSheet.ts @@ -1,52 +1,54 @@ import Component from "../../ecs/src/Component"; -import {loadLocalImage} from "@serbanghita-gamedev/assets"; +import { loadLocalImage } from "@serbanghita-gamedev/assets"; export type Animation = { - frames: AnimationFrame[]; - speed: number; - hitboxOffset: {x: number, y: number}; -} + frames: AnimationFrame[]; + speed: number; + hitboxOffset: { x: number; y: number }; +}; export type AnimationFrame = { - width: number; - height: number; + width: number; + height: number; + x: number; + y: number; +}; + +export type SpriteSheetAnimation = { + defaultAnimation?: boolean; + parent?: string; // An animation based on a previous animation (doesn't count as offset on Y). + width: number; + height: number; + frames: number[]; + speedTicks: number; + hitboxOffset: { x: number; y: number; -} + }; +}; -export type SpriteSheetAnimation = { - defaultAnimation?: boolean; - parent?: string; // An animation based on a previous animation (doesn't count as offset on Y). - width: number; - height: number; - frames: number[]; - speedTicks: number; - hitboxOffset: { - x: number; - y: number; - }; -} +export type SpriteSheetProps = { + name: string; + offset_x: number; + offset_y: number; + animations: Map; // This is added and computed in PreRenderSystem. + animationCurrentFrame: string; + animationDefaultFrame: string; + spriteSheetImgPath: string; + spriteSheetAnimationsPath: string; +}; -export type SpriteSheetProperties = { - name: string; - offset_x: number; - offset_y: number; - animations: Map; // This is added and computed in PreRenderSystem. - animationCurrentFrame: string; - animationDefaultFrame: string; - spriteSheetImgPath: string; - spriteSheetAnimationsPath: string; -} +export type SpriteSheetPropsDeclaration = Pick; export default class SpriteSheet extends Component { - constructor(public properties: SpriteSheetProperties) { - super(properties); + constructor(public properties: SpriteSheetProps) { + super(properties); - // this.properties.img = loadLocalImage( - // // eslint-disable-next-line @typescript-eslint/no-var-requires - // require(this.properties.spriteSheetImgPath) - // ); - // // eslint-disable-next-line @typescript-eslint/no-var-requires - // this.properties.animationsDeclaration = require(this.properties.spriteSheetAnimationsPath) as ISpriteSheetAnimation[]; - } -} \ No newline at end of file + // this.properties.img = loadLocalImage( + // // eslint-disable-next-line @typescript-eslint/no-var-requires + // require(this.properties.spriteSheetImgPath) + // ); + // // eslint-disable-next-line @typescript-eslint/no-var-requires + // this.properties.animationsDeclaration = require(this.properties.spriteSheetAnimationsPath) as ISpriteSheetAnimation[]; + } +} diff --git a/packages/component/src/index.ts b/packages/component/src/index.ts index 5dc9a5c..803a3c4 100644 --- a/packages/component/src/index.ts +++ b/packages/component/src/index.ts @@ -1,10 +1,10 @@ -export {default as MatrixConfig} from './MatrixConfig'; -export {default as IsOnMatrix} from './IsOnMatrix'; -export {default as Body} from './Body'; -export {default as Direction, Directions} from './Direction'; -export {default as Keyboard} from './Keyboard'; -export {default as Position} from './Position'; -export {default as Renderable} from './Renderable'; -export {default as SpriteSheet, SpriteSheetAnimation, SpriteSheetProperties, Animation, AnimationFrame} from './SpriteSheet'; -export {default as State} from './State'; -export {default as IsTiledMap} from './IsTiledMap'; \ No newline at end of file +export { default as MatrixConfig } from "./MatrixConfig"; +export { default as IsOnMatrix } from "./IsOnMatrix"; +export { default as Body } from "./Body"; +export { default as Direction, Directions } from "./Direction"; +export { default as Keyboard } from "./Keyboard"; +export { default as Position } from "./Position"; +export { default as Renderable } from "./Renderable"; +export { default as SpriteSheet, SpriteSheetAnimation, SpriteSheetProps, Animation, AnimationFrame, SpriteSheetPropsDeclaration } from "./SpriteSheet"; +export { default as State } from "./State"; +export { default as IsTiledMap } from "./IsTiledMap"; diff --git a/packages/demos/game-playground/.editorconfig b/packages/demos/game-playground/.editorconfig index b71ccb1..8c23ebf 100644 --- a/packages/demos/game-playground/.editorconfig +++ b/packages/demos/game-playground/.editorconfig @@ -3,4 +3,4 @@ root = true [*.{js,json,ts}] indent_style = space indent_size = 2 -max_line_length = 140 \ No newline at end of file +max_line_length = 160 \ No newline at end of file diff --git a/packages/demos/game-playground/.prettierrc b/packages/demos/game-playground/.prettierrc index 3037118..91a3517 100644 --- a/packages/demos/game-playground/.prettierrc +++ b/packages/demos/game-playground/.prettierrc @@ -1,3 +1,3 @@ { - "printWidth": 140 + "printWidth": 160 } diff --git a/packages/demos/game-playground/src/AttackingWithClubSystem.ts b/packages/demos/game-playground/src/AttackingWithClubSystem.ts new file mode 100644 index 0000000..bb15af7 --- /dev/null +++ b/packages/demos/game-playground/src/AttackingWithClubSystem.ts @@ -0,0 +1,65 @@ +import { Direction, Directions } from "@serbanghita-gamedev/component"; +import { Entity, System } from "@serbanghita-gamedev/ecs"; +import { StateStatus } from "./state"; + +import IsAttackingWithClub from "./IsAttackingWithClub"; + +export default class AttackingWithClubSystem extends System { + private onEnter(entity: Entity, component: IsAttackingWithClub) { + component.properties.tick = 0; + component.properties.animationTick = 0; + component.properties.status = StateStatus.STARTED; + } + + private onUpdate(entity: Entity, component: IsAttackingWithClub) { + const direction = entity.getComponent(Direction); + + if (component.properties.animationTick === 5) { + this.onExit(entity, component); + } + + if (direction.properties.y === Directions.UP) { + component.properties.animationStateName = "club_attack_one_up"; + } else if (direction.properties.y === Directions.DOWN) { + component.properties.animationStateName = "club_attack_one_down"; + } + + if (direction.properties.x === Directions.LEFT) { + component.properties.animationStateName = "club_attack_one_left"; + } else if (direction.properties.x === Directions.RIGHT) { + component.properties.animationStateName = "club_attack_one_right"; + } + + component.properties.tick++; + if (component.properties.tick % 15 === 0) { + component.properties.animationTick += 1; + } + // console.log(component.properties.tick); + // console.log(component.properties.animationTick); + } + + private onExit(entity: Entity, component: IsAttackingWithClub) { + component.properties.status = StateStatus.FINISHED; + } + + public update(now: number): void { + this.query.execute().forEach((entity) => { + const component = entity.getComponent(IsAttackingWithClub); + + console.log("IsAttackingWithClub", entity.id); + + if (component.properties.status === StateStatus.FINISHED) { + entity.removeComponent(IsAttackingWithClub); + return; + } + + if (component.properties.status === StateStatus.NOT_STARTED) { + this.onEnter(entity, component); + } + + this.onUpdate(entity, component); + + return true; + }); + } +} diff --git a/packages/demos/game-playground/src/IdleSystem.ts b/packages/demos/game-playground/src/IdleSystem.ts new file mode 100644 index 0000000..44db7ae --- /dev/null +++ b/packages/demos/game-playground/src/IdleSystem.ts @@ -0,0 +1,58 @@ +import { Direction } from "@serbanghita-gamedev/component"; +import { System, Entity } from "@serbanghita-gamedev/ecs"; +import IsIdle from "./IsIdle"; +import { StateStatus } from "./state"; + +export default class IdleSystem extends System { + private onEnter(entity: Entity, component: IsIdle) { + component.properties.tick = 0; + component.properties.animationTick = 0; + component.properties.status = StateStatus.STARTED; + } + + private onUpdate(entity: Entity, component: IsIdle) { + // Loop. @todo: move logic + // if (component.properties.tick === 10) { + // this.onEnter(entity, component); + // } + + const direction = entity.getComponent(Direction); + const directionLiteral = direction.properties.literal || ""; + + // console.log(`idle_${directionLiteral}`); + + component.properties.animationStateName = directionLiteral ? `idle_${directionLiteral}` : "idle"; + component.properties.tick++; + if (component.properties.tick % 15 === 0) { + component.properties.animationTick += 1; + } + + // console.log(component.properties.animationTick); + } + + private onExit(entity: Entity, component: IsIdle) { + component.properties.status = StateStatus.FINISHED; + } + + public update(now: number): void { + this.query.execute().forEach((entity: Entity) => { + const component = entity.getComponent(IsIdle); + + // console.log('IsIdle', entity.id); + + if (component.properties.status === StateStatus.FINISHED) { + console.log("IsIdle finished and removed"); + entity.removeComponent(IsIdle); + return; + } + + if (component.properties.status === StateStatus.NOT_STARTED) { + this.onEnter(entity, component); + } + + this.onUpdate(entity, component); + + return true; + }); + } +} diff --git a/packages/demos/game-playground/src/IsAttackingWithClub.ts b/packages/demos/game-playground/src/IsAttackingWithClub.ts new file mode 100644 index 0000000..739f35b --- /dev/null +++ b/packages/demos/game-playground/src/IsAttackingWithClub.ts @@ -0,0 +1,32 @@ +import { Component } from "@serbanghita-gamedev/ecs"; +import { StateStatus } from "./state"; +import { extend } from "./utils"; + +interface IsAttackingWithClubProps { + stateName: string; + animationStateName: string; + animationTick: number; + tick: number; + status: StateStatus; + [key: string]: any; +} + +export default class IsAttackingWithClub extends Component { + constructor(public properties: IsAttackingWithClubProps) { + super(properties); + + this.init(properties); + } + + public init(properties: IsAttackingWithClubProps) { + const defaultProps = { + stateName: "club_attack_one", + animationStateName: "club_attack_one_down", + animationTick: 0, + tick: 0, + status: StateStatus.NOT_STARTED, + }; + + this.properties = extend(defaultProps, properties); + } +} diff --git a/packages/demos/game-playground/src/IsIdle.ts b/packages/demos/game-playground/src/IsIdle.ts new file mode 100644 index 0000000..150acca --- /dev/null +++ b/packages/demos/game-playground/src/IsIdle.ts @@ -0,0 +1,32 @@ +import { Component } from "@serbanghita-gamedev/ecs"; +import { StateStatus } from "./state"; +import { extend } from "./utils"; + +interface IsIdleProps { + stateName: string; + animationStateName: string; + animationTick: number; + tick: number; + status: StateStatus; + [key: string]: any; +} + +export default class IsIdle extends Component { + constructor(public properties: IsIdleProps) { + super(properties); + + this.init(properties); + } + + public init(properties: IsIdleProps) { + const defaultProps = { + stateName: "idle", + animationStateName: "idle_down", + animationTick: 0, + tick: 0, + status: StateStatus.NOT_STARTED, + }; + + this.properties = extend(defaultProps, properties); + } +} diff --git a/packages/demos/game-playground/src/component/IsWalking.ts b/packages/demos/game-playground/src/IsWalking.ts similarity index 100% rename from packages/demos/game-playground/src/component/IsWalking.ts rename to packages/demos/game-playground/src/IsWalking.ts diff --git a/packages/demos/game-playground/src/system/MatrixSystem.ts b/packages/demos/game-playground/src/MatrixSystem.ts similarity index 100% rename from packages/demos/game-playground/src/system/MatrixSystem.ts rename to packages/demos/game-playground/src/MatrixSystem.ts diff --git a/packages/demos/game-playground/src/system/PlayerKeyboardSystem.ts b/packages/demos/game-playground/src/PlayerKeyboardSystem.ts similarity index 100% rename from packages/demos/game-playground/src/system/PlayerKeyboardSystem.ts rename to packages/demos/game-playground/src/PlayerKeyboardSystem.ts diff --git a/packages/demos/game-playground/src/system/RenderSystem.ts b/packages/demos/game-playground/src/RenderSystem.ts similarity index 60% rename from packages/demos/game-playground/src/system/RenderSystem.ts rename to packages/demos/game-playground/src/RenderSystem.ts index e252a19..b5ca46a 100644 --- a/packages/demos/game-playground/src/system/RenderSystem.ts +++ b/packages/demos/game-playground/src/RenderSystem.ts @@ -1,28 +1,28 @@ -import { System, Query, World } from "@serbanghita-gamedev/ecs"; -import { clearCtx, getCtx, image, rectangle } from "@serbanghita-gamedev/renderer"; +import { System, Query, World, Entity } from "@serbanghita-gamedev/ecs"; +import { clearCtx, image, rectangle, AnimationRegistry, AnimationRegistryItem } from "@serbanghita-gamedev/renderer"; import { Position, SpriteSheet } from "@serbanghita-gamedev/component"; -import IsWalking from "../component/IsWalking"; -import IsIdle from "../component/IsIdle"; -import IsAttackingWithClub from "../component/IsAttackingWithClub"; -import { ANIMATIONS_REGISTRY } from "../assets"; +import IsWalking from "./IsWalking"; +import IsIdle from "./IsIdle"; +import IsAttackingWithClub from "./IsAttackingWithClub"; export default class RenderSystem extends System { public constructor( public world: World, public query: Query, - protected CANVAS: HTMLCanvasElement, - protected SPRITES: { [key: string]: HTMLImageElement }, + + protected animationRegistry: AnimationRegistry, + protected ctx: CanvasRenderingContext2D, ) { super(world, query); } public update(now: number): void { - clearCtx(getCtx(this.CANVAS), 0, 0, 640, 480); + clearCtx(this.ctx, 0, 0, 640, 480); this.query.execute().forEach((entity) => { const position = entity.getComponent(Position); const spriteSheet = entity.getComponent(SpriteSheet); - const spriteSheetImg = this.SPRITES[spriteSheet.properties.spriteSheetImgPath]; + const spriteSheetImg = this.animationRegistry.assets["entities/images"][spriteSheet.properties.spriteSheetImgPath]; if (!spriteSheetImg) { throw new Error(`SpriteSheet image file ${spriteSheet.properties.spriteSheetImgPath} is missing.`); @@ -40,9 +40,11 @@ export default class RenderSystem extends System { throw new Error(`Entity ${entity.id} has no default state to render.`); } - const animation = ANIMATIONS_REGISTRY[spriteSheet.properties.spriteSheetAnimationsPath].animations.get( - component.properties.animationStateName, - ); + const animationItem = this.animationRegistry.getAnimationsFor(spriteSheet.properties.spriteSheetAnimationsPath); + if (!animationItem) { + throw new Error(`Animations were not loaded for ${spriteSheet.properties.spriteSheetAnimationsPath}.`); + } + const animation = animationItem.animations.get(component.properties.animationStateName); if (!animation) { throw new Error( @@ -61,13 +63,11 @@ export default class RenderSystem extends System { const destPositionY = hitboxOffset?.y ? position.properties.y - hitboxOffset.y : position.properties.y; if (!animationFrame) { - throw new Error( - `Cannot find animation frame ${component.properties.animationTick} for "${component.properties.animationStateName}".`, - ); + throw new Error(`Cannot find animation frame ${component.properties.animationTick} for "${component.properties.animationStateName}".`); } image( - getCtx(this.CANVAS), + this.ctx, spriteSheetImg, // source animationFrame.x, @@ -81,23 +81,9 @@ export default class RenderSystem extends System { animationFrame.height, ); - rectangle( - getCtx(this.CANVAS) as CanvasRenderingContext2D, - destPositionX, - destPositionY, - animationFrame.width, - animationFrame.height, - "#cccccc", - ); + rectangle(this.ctx, destPositionX, destPositionY, animationFrame.width, animationFrame.height, "#cccccc"); - rectangle( - getCtx(this.CANVAS) as CanvasRenderingContext2D, - destPositionX + hitboxOffset.x, - destPositionY + hitboxOffset.y, - 16, - 16, - "red", - ); + rectangle(this.ctx, destPositionX + hitboxOffset.x, destPositionY + hitboxOffset.y, 16, 16, "red"); }); } } diff --git a/packages/demos/game-playground/src/system/WalkingSystem.ts b/packages/demos/game-playground/src/WalkingSystem.ts similarity index 100% rename from packages/demos/game-playground/src/system/WalkingSystem.ts rename to packages/demos/game-playground/src/WalkingSystem.ts diff --git a/packages/demos/game-playground/src/assets.ts b/packages/demos/game-playground/src/assets.ts index c2bc343..f0bdf0f 100644 --- a/packages/demos/game-playground/src/assets.ts +++ b/packages/demos/game-playground/src/assets.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ /** * This file statically defines all the assets needed for the game to run: * - entities & components declarations @@ -12,52 +11,45 @@ // Added manually because esbuild doesn't support dynamic imports import { loadLocalImage } from "@serbanghita-gamedev/assets"; -import { Animation, SpriteSheetAnimation } from "@serbanghita-gamedev/component"; +import { SpriteSheetAnimation } from "@serbanghita-gamedev/component"; import { TiledMapFile } from "@serbanghita-gamedev/tiled"; +import { Assets, EntityDeclaration } from "@serbanghita-gamedev/assets"; // @see https://esbuild.github.io/content-types/#data-url -export async function loadSprites() { - const SPRITES: { [key: string]: HTMLImageElement } = { - "./assets/sprites/kil.png": await loadLocalImage(require("./assets/sprites/kil.png")), - "./assets/sprites/dino-boss.png": await loadLocalImage(require("./assets/sprites/dino-boss.png")), - "./assets/sprites/dino-minion.png": await loadLocalImage(require("./assets/sprites/dino-minion.png")), - "./assets/sprites/anky-boss.png": await loadLocalImage(require("./assets/sprites/anky-boss.png")), - "./assets/sprites/anky-minion.png": await loadLocalImage(require("./assets/sprites/anky-minion.png")), - "./assets/sprites/ptery-boss.png": await loadLocalImage(require("./assets/sprites/ptery-boss.png")), - "./assets/sprites/ptery-minion.png": await loadLocalImage(require("./assets/sprites/ptery-minion.png")), - "./assets/sprites/terrain.png": await loadLocalImage(require("./assets/sprites/terrain.png")), - }; - - return SPRITES; -} - -export const MAPS: { [key: string]: TiledMapFile } = { - "./assets/maps/E1MM2.json": require("./assets/maps/E1MM2.json"), -}; - // @see https://esbuild.github.io/content-types/#json -export const ANIMATIONS_DECLARATIONS: { [animationDeclarationPath: string]: SpriteSheetAnimation[] } = { - "./assets/sprites/kil.animations.json": require("./assets/sprites/kil.animations.json"), - "./assets/sprites/dino-boss.animations.json": require("./assets/sprites/dino-boss.animations.json"), - "./assets/sprites/dino-minion.animations.json": require("./assets/sprites/dino-minion.animations.json"), - "./assets/sprites/anky-boss.animations.json": require("./assets/sprites/anky-boss.animations.json"), - "./assets/sprites/anky-minion.animations.json": require("./assets/sprites/anky-minion.animations.json"), - "./assets/sprites/ptery-boss.animations.json": require("./assets/sprites/ptery-boss.animations.json"), - "./assets/sprites/ptery-minion.animations.json": require("./assets/sprites/ptery-minion.animations.json"), -}; -export type AnimationsRegistry = { - [animationDeclarationPath: string]: { - animationDefaultFrame: string; - animations: Map; +export async function loadAssets(): Promise { + return { + "entities/images": { + "./assets/sprites/kil.png": await loadLocalImage(require("./assets/sprites/kil.png")), + "./assets/sprites/dino-boss.png": await loadLocalImage(require("./assets/sprites/dino-boss.png")), + "./assets/sprites/dino-minion.png": await loadLocalImage(require("./assets/sprites/dino-minion.png")), + "./assets/sprites/anky-boss.png": await loadLocalImage(require("./assets/sprites/anky-boss.png")), + "./assets/sprites/anky-minion.png": await loadLocalImage(require("./assets/sprites/anky-minion.png")), + "./assets/sprites/ptery-boss.png": await loadLocalImage(require("./assets/sprites/ptery-boss.png")), + "./assets/sprites/ptery-minion.png": await loadLocalImage(require("./assets/sprites/ptery-minion.png")), + }, + "entities/animations": { + "./assets/sprites/kil.animations.json": require("./assets/sprites/kil.animations.json") as { [key: string]: SpriteSheetAnimation }, + "./assets/sprites/dino-boss.animations.json": require("./assets/sprites/dino-boss.animations.json") as { [key: string]: SpriteSheetAnimation }, + "./assets/sprites/dino-minion.animations.json": require("./assets/sprites/dino-minion.animations.json") as { + [key: string]: SpriteSheetAnimation; + }, + "./assets/sprites/anky-boss.animations.json": require("./assets/sprites/anky-boss.animations.json") as { [key: string]: SpriteSheetAnimation }, + "./assets/sprites/anky-minion.animations.json": require("./assets/sprites/anky-minion.animations.json") as { + [key: string]: SpriteSheetAnimation; + }, + "./assets/sprites/ptery-boss.animations.json": require("./assets/sprites/ptery-boss.animations.json") as { [key: string]: SpriteSheetAnimation }, + "./assets/sprites/ptery-minion.animations.json": require("./assets/sprites/ptery-minion.animations.json") as { + [key: string]: SpriteSheetAnimation; + }, + }, + "entities/declarations": require("./assets/entities.json") as EntityDeclaration[], + "maps/images": { + "./assets/sprites/terrain.png": await loadLocalImage(require("./assets/sprites/terrain.png")), + }, + "maps/declarations": { + "./assets/maps/E1MM2.json": require("./assets/maps/E1MM2.json") as TiledMapFile, + }, }; -}; - -export const ANIMATIONS_REGISTRY: AnimationsRegistry = {}; - -export interface EntityDeclaration { - id: string; - components: { [componentName: string]: object }; } - -export const ENTITIES_DECLARATIONS = require("./assets/entities.json") as EntityDeclaration[]; diff --git a/packages/demos/game-playground/src/assets/entities.json b/packages/demos/game-playground/src/assets/entities.json index f85302b..779ddd3 100644 --- a/packages/demos/game-playground/src/assets/entities.json +++ b/packages/demos/game-playground/src/assets/entities.json @@ -1,11 +1,4 @@ [ - { - "id": "map", - "components": { - "IsTiledMap": {"mapFilePath": "./assets/maps/E1MM2.json" }, - "MatrixConfig": { "width": 40, "height": 30, "tileSize": 16 } - } - }, { "id": "player", "components": { @@ -13,7 +6,7 @@ "animationStateName": "idle" }, "Body": { "width": 16, "height": 16 }, - "Position": { "x": 0, "y": 200 }, + "Position": { "x": 40, "y": 90 }, "IsOnMatrix": { "tile": 0 }, "Direction": { "x": 0, "y": 0 }, "Keyboard": { "up": "w", "down": "s", "left": "a", "right": "d" }, @@ -34,7 +27,7 @@ "animationStateName": "idle" }, "Body": { "width": 16, "height": 16 }, - "Position": { "x": 64, "y": 200 }, + "Position": { "x": 124, "y": 200 }, "IsOnMatrix": { "tile": 0 }, "Direction": { "x": 0, "y": 0 }, "Keyboard": { "up": "w", "down": "s", "left": "a", "right": "d" }, @@ -55,7 +48,7 @@ "animationStateName": "idle" }, "Body": { "width": 16, "height": 16 }, - "Position": { "x": 64, "y": 300 }, + "Position": { "x": 40, "y": 300 }, "IsOnMatrix": { "tile": 0 }, "Direction": { "x": 0, "y": 0 }, "Keyboard": { "up": "w", "down": "s", "left": "a", "right": "d" }, @@ -76,7 +69,7 @@ "animationStateName": "idle" }, "Body": { "width": 16, "height": 16 }, - "Position": { "x": 160, "y": 200 }, + "Position": { "x": 210, "y": 140 }, "IsOnMatrix": { "tile": 0 }, "Direction": { "x": 0, "y": 0 }, "Keyboard": { "up": "w", "down": "s", "left": "a", "right": "d" }, @@ -97,7 +90,7 @@ "animationStateName": "idle" }, "Body": { "width": 16, "height": 16 }, - "Position": { "x": 160, "y": 300 }, + "Position": { "x": 265, "y": 280 }, "IsOnMatrix": { "tile": 0 }, "Direction": { "x": 0, "y": 0 }, "Keyboard": { "up": "w", "down": "s", "left": "a", "right": "d" }, @@ -139,7 +132,7 @@ "animationStateName": "idle" }, "Body": { "width": 16, "height": 16 }, - "Position": { "x": 350, "y": 300 }, + "Position": { "x": 400, "y": 290 }, "IsOnMatrix": { "tile": 0 }, "Direction": { "x": 0, "y": 0 }, "Keyboard": { "up": "w", "down": "s", "left": "a", "right": "d" }, diff --git a/packages/demos/game-playground/src/component/CurrentState.ts b/packages/demos/game-playground/src/component/CurrentState.ts deleted file mode 100644 index 8d37745..0000000 --- a/packages/demos/game-playground/src/component/CurrentState.ts +++ /dev/null @@ -1,12 +0,0 @@ -import {Component} from "@serbanghita-gamedev/ecs"; - -interface CurrentStateProps { - stateName: string; - defaultStateName: string; -} - -export default class CurrentState extends Component { - constructor(public properties: CurrentStateProps) { - super(properties); - } -} \ No newline at end of file diff --git a/packages/demos/game-playground/src/component/IsAttackingWithClub.ts b/packages/demos/game-playground/src/component/IsAttackingWithClub.ts deleted file mode 100644 index b2423ff..0000000 --- a/packages/demos/game-playground/src/component/IsAttackingWithClub.ts +++ /dev/null @@ -1,32 +0,0 @@ -import {Component} from "@serbanghita-gamedev/ecs"; -import {StateStatus} from "../state/state-status"; -import {extend} from "../utils"; - -interface IsAttackingWithClubProps { - stateName: string; - animationStateName: string; - animationTick: number; - tick: number; - status: StateStatus; - [key: string]: any; -} - -export default class IsAttackingWithClub extends Component { - constructor(public properties: IsAttackingWithClubProps) { - super(properties); - - this.init(properties); - } - - public init(properties: IsAttackingWithClubProps) { - const defaultProps = { - stateName: 'club_attack_one', - animationStateName: 'club_attack_one_down', - animationTick: 0, - tick: 0, - status: StateStatus.NOT_STARTED - }; - - this.properties = extend(defaultProps, properties); - } -} \ No newline at end of file diff --git a/packages/demos/game-playground/src/component/IsIdle.ts b/packages/demos/game-playground/src/component/IsIdle.ts deleted file mode 100644 index a69af27..0000000 --- a/packages/demos/game-playground/src/component/IsIdle.ts +++ /dev/null @@ -1,32 +0,0 @@ -import {Component} from "@serbanghita-gamedev/ecs"; -import {StateStatus} from "../state/state-status"; -import {extend} from "../utils"; - -interface IsIdleProps { - stateName: string; - animationStateName: string; - animationTick: number; - tick: number; - status: StateStatus; - [key: string]: any; -} - -export default class IsIdle extends Component { - constructor(public properties: IsIdleProps) { - super(properties); - - this.init(properties); - } - - public init(properties: IsIdleProps) { - const defaultProps = { - stateName: 'idle', - animationStateName: 'idle_down', - animationTick: 0, - tick: 0, - status: StateStatus.NOT_STARTED - }; - - this.properties = extend(defaultProps, properties); - } -} \ No newline at end of file diff --git a/packages/demos/game-playground/src/index.ts b/packages/demos/game-playground/src/index.ts index d7853a1..f34462b 100644 --- a/packages/demos/game-playground/src/index.ts +++ b/packages/demos/game-playground/src/index.ts @@ -1,39 +1,45 @@ -import { ENTITIES_DECLARATIONS, EntityDeclaration, loadSprites, MAPS } from "./assets"; -import World from "../../../ecs/src/World"; -import { - Body, - Position, - Direction, - Keyboard, - Renderable, - SpriteSheet, - IsOnMatrix, - MatrixConfig, - IsTiledMap, -} from "@serbanghita-gamedev/component"; +import { loadAssets } from "./assets"; +import { World } from "@serbanghita-gamedev/ecs"; +import { Body, Position, Direction, Keyboard, Renderable, SpriteSheet, IsOnMatrix, MatrixConfig, IsTiledMap } from "@serbanghita-gamedev/component"; import { Keyboard as KeyboardInput, InputActions } from "@serbanghita-gamedev/input"; -import PlayerKeyboardSystem from "./system/PlayerKeyboardSystem"; -import RenderSystem from "./system/RenderSystem"; -import PreRenderSystem from "./system/PreRenderSystem"; -import IsIdle from "./component/IsIdle"; -import IsWalking from "./component/IsWalking"; -import IdleSystem from "./system/IdleSystem"; -import WalkingSystem from "./system/WalkingSystem"; -import CurrentState from "./component/CurrentState"; -import IsAttackingWithClub from "./component/IsAttackingWithClub"; -import AttackingWithClubSystem from "./system/AttackingWithClubSystem"; -import MatrixSystem from "./system/MatrixSystem"; -import { createHtmlUiElements, PreRenderTiledMapSystem } from "@serbanghita-gamedev/renderer"; +import PlayerKeyboardSystem from "./PlayerKeyboardSystem"; +import RenderSystem from "./RenderSystem"; +import IsIdle from "./IsIdle"; +import IsWalking from "./IsWalking"; +import IdleSystem from "./IdleSystem"; +import WalkingSystem from "./WalkingSystem"; +import IsAttackingWithClub from "./IsAttackingWithClub"; +import AttackingWithClubSystem from "./AttackingWithClubSystem"; +import MatrixSystem from "./MatrixSystem"; +import { createHtmlUiElements, RenderTiledMapTerrainSystem, AnimationRegistry, loadAnimationRegistry } from "@serbanghita-gamedev/renderer"; +import { TiledMap } from "@serbanghita-gamedev/tiled"; async function setup() { - // 0. Create the UI and canvas. - const [, CANVAS_BACKGROUND, CANVAS_FOREGROUND] = createHtmlUiElements(); + /************************************************************ + * Create the UI and canvas. + ************************************************************/ - // 1. Load sprite sheets IMGs. - const SPRITES = await loadSprites(); - // 2. Load JSON animations for sprite sheets. - // 3. Load JSON map declarations (Tiled). - // 4. Create the grid. + const [$htmlWrapper, $canvasBackground, $ctxBackground, $canvasForeground, $ctxForeground] = createHtmlUiElements(); + + /************************************************************* + * Preload assets (IMGs, JSONs) + * + * entities/images - Sprite sheets as IMGs for entities animations. + * entities/animations - Animation frames as JSON + * entities/declarations - Entities + Components as JSON + * maps/images - Terrain as sprite sheets IMGs. + * maps/declarations - Maps as JSON exported from Tiled. + * + ************************************************************/ + + const assets = await loadAssets(); + + /************************************************************ + * Registry for animations. Stores Entity's animation frames. + * Used by the RenderSystem to pre-compute animation frames data. + ************************************************************/ + + const animationRegistry = loadAnimationRegistry(assets); // Load input Component. const input = new KeyboardInput(); @@ -48,38 +54,16 @@ async function setup() { const world = new World(); // Register "Components". - world.declarations.components.registerComponents([ - Body, - Position, - Direction, - Keyboard, - Renderable, - SpriteSheet, - IsIdle, - IsWalking, - IsAttackingWithClub, - CurrentState, - MatrixConfig, - IsOnMatrix, - IsTiledMap, - ]); - - function createEntityFromDeclaration(entityDeclaration: EntityDeclaration) { - // Create the entity and assign it to the world. - const entity = world.createEntity(entityDeclaration.id); - - // Add Component(s) to the Entity. - for (const name in entityDeclaration.components) { - const componentDeclaration = world.declarations.components.getComponent(name); - const props = entityDeclaration.components[name]; - - entity.addComponent(componentDeclaration, props); - } - } - - ENTITIES_DECLARATIONS.forEach((entityDeclaration) => createEntityFromDeclaration(entityDeclaration)); - - const MatrixQuery = world.createQuery("MatrixQuery", { all: [IsOnMatrix] }); + world.registerComponents([Body, Direction, Keyboard, Renderable, SpriteSheet, IsIdle, IsWalking, IsAttackingWithClub, IsTiledMap]); + + // Create entities automatically from "entities.json" declaration file. + assets["entities/declarations"].forEach((entityDeclaration) => world.createEntityFromDeclaration(entityDeclaration)); + + const map = world.createEntity("map"); + map.addComponent(IsTiledMap, { mapFile: assets["maps/declarations"]["./assets/maps/E1MM2.json"], mapFilePath: "./assets/maps/E1MM2.json" }); + // Load the "TiledMap" class wrapper over the json file declaration. + const tiledMap = new TiledMap(map.getComponent(IsTiledMap).properties.mapFile); + const KeyboardQuery = world.createQuery("KeyboardQuery", { all: [Keyboard] }); const IdleQuery = world.createQuery("IdleQuery", { all: [IsIdle] }); const WalkingQuery = world.createQuery("WalkingQuery", { all: [IsWalking] }); @@ -87,15 +71,12 @@ async function setup() { const RenderableQuery = world.createQuery("RenderableQuery", { all: [Renderable, SpriteSheet, Position] }); const TiledMapQuery = world.createQuery("TiledMapQuery", { all: [IsTiledMap] }); - world - .createSystem(PreRenderTiledMapSystem, TiledMapQuery, CANVAS_BACKGROUND, SPRITES["./assets/sprites/terrain.png"], MAPS) - .runOnlyOnce(); - world.createSystem(PreRenderSystem, RenderableQuery).runOnlyOnce(); + world.createSystem(RenderTiledMapTerrainSystem, TiledMapQuery, $ctxBackground, assets["entities/images"]["./assets/sprites/terrain.png"]).runOnlyOnce(); world.createSystem(PlayerKeyboardSystem, KeyboardQuery, input); world.createSystem(IdleSystem, IdleQuery); world.createSystem(WalkingSystem, WalkingQuery); world.createSystem(AttackingWithClubSystem, AttackingWithClubQuery); - world.createSystem(RenderSystem, RenderableQuery, CANVAS_FOREGROUND, SPRITES); + world.createSystem(RenderSystem, RenderableQuery, animationRegistry, $ctxForeground); world.createSystem(MatrixSystem, MatrixQuery); world.start(); diff --git a/packages/demos/game-playground/src/state.ts b/packages/demos/game-playground/src/state.ts new file mode 100644 index 0000000..b10d514 --- /dev/null +++ b/packages/demos/game-playground/src/state.ts @@ -0,0 +1,58 @@ +export enum StateStatus { + NOT_STARTED = 0, + STARTED = 1, + FINISHED = 2, +} + +export const enum PlayerAnimationState { + "idle" = "idle", + "walk_left" = "walk_left", + "idle_left" = "idle_left", + "walk_right" = "walk_right", + "idle_right" = "idle_right", + "walk_up" = "walk_up", + "idle_up" = "idle_up", + "throw_boomerang_up" = "throw_boomerang_up", + "walk_down" = "walk_down", + "idle_down" = "idle_down", + "throw_boomerang_down" = "throw_boomerang_down", + "eat_left" = "eat_left", + "eat_right" = "eat_right", + "get_hurt_left" = "get_hurt_left", + "get_hurt_right" = "get_hurt_right", + "draw_guns" = "draw_guns", + "play_with_boomerang" = "play_with_boomerang", + "play_with_bomb" = "play_with_bomb", + "thumbs_up" = "thumbs_up", + "wave" = "wave", + "teleport_in" = "teleport_in", + "teleport_out" = "teleport_out", + "throw_boomerang_left" = "throw_boomerang_left", + "throw_boomerang_right" = "throw_boomerang_right", + "lay_bomb_left" = "lay_bomb_left", + "lay_bomb_right" = "lay_bomb_right", + "club_attack_one_up" = "club_attack_one_up", + "club_attack_two_up" = "club_attack_two_up", + "club_attack_one_down" = "club_attack_one_down", + "club_attack_two_down" = "club_attack_two_down", + "club_attack_one_right" = "club_attack_one_right", + "club_attack_two_right" = "club_attack_two_right", + "club_attack_one_left" = "club_attack_one_left", + "club_attack_two_left" = "club_attack_two_left", + "get_hurt_down" = "get_hurt_down", + "get_hurt_up" = "get_hurt_up", + "club_swipe_down_from_left" = "club_swipe_down_from_left", + "club_swipe_down_from_right" = "club_swipe_down_from_right", + "club_swipe_right_from_top" = "club_swipe_right_from_top", + "club_swipe_right_from_bottom" = "club_swipe_right_from_bottom", + "club_swipe_left_from_top" = "club_swipe_left_from_top", + "club_swipe_left_from_bottom" = "club_swipe_left_from_bottom", + "club_swipe_up_from_left" = "club_swipe_up_from_left", + "club_swipe_up_from_right" = "club_swipe_up_from_right", +} + +export const enum PlayerState { + idle = "idle", + walk = "walk", + club_attack = "club_attack", +} diff --git a/packages/demos/game-playground/src/state/player-animation-states.ts b/packages/demos/game-playground/src/state/player-animation-states.ts deleted file mode 100644 index 3a5dbc3..0000000 --- a/packages/demos/game-playground/src/state/player-animation-states.ts +++ /dev/null @@ -1,46 +0,0 @@ -export const enum PlayerAnimationState { - "idle" = "idle", - "walk_left" = "walk_left", - "idle_left" = "idle_left", - "walk_right" = "walk_right", - "idle_right" = "idle_right", - "walk_up" = "walk_up", - "idle_up" = "idle_up", - "throw_boomerang_up" = "throw_boomerang_up", - "walk_down" = "walk_down", - "idle_down" = "idle_down", - "throw_boomerang_down" = "throw_boomerang_down", - "eat_left" = "eat_left", - "eat_right" = "eat_right", - "get_hurt_left" = "get_hurt_left", - "get_hurt_right" = "get_hurt_right", - "draw_guns" = "draw_guns", - "play_with_boomerang" = "play_with_boomerang", - "play_with_bomb" = "play_with_bomb", - "thumbs_up" = "thumbs_up", - "wave" = "wave", - "teleport_in" = "teleport_in", - "teleport_out" = "teleport_out", - "throw_boomerang_left" = "throw_boomerang_left", - "throw_boomerang_right" = "throw_boomerang_right", - "lay_bomb_left" = "lay_bomb_left", - "lay_bomb_right" = "lay_bomb_right", - "club_attack_one_up" = "club_attack_one_up", - "club_attack_two_up" = "club_attack_two_up", - "club_attack_one_down" = "club_attack_one_down", - "club_attack_two_down" = "club_attack_two_down", - "club_attack_one_right" = "club_attack_one_right", - "club_attack_two_right" = "club_attack_two_right", - "club_attack_one_left" = "club_attack_one_left", - "club_attack_two_left" = "club_attack_two_left", - "get_hurt_down" = "get_hurt_down", - "get_hurt_up" = "get_hurt_up", - "club_swipe_down_from_left" = "club_swipe_down_from_left", - "club_swipe_down_from_right" = "club_swipe_down_from_right", - "club_swipe_right_from_top" = "club_swipe_right_from_top", - "club_swipe_right_from_bottom" = "club_swipe_right_from_bottom", - "club_swipe_left_from_top" = "club_swipe_left_from_top", - "club_swipe_left_from_bottom" = "club_swipe_left_from_bottom", - "club_swipe_up_from_left" = "club_swipe_up_from_left", - "club_swipe_up_from_right" = "club_swipe_up_from_right", -} \ No newline at end of file diff --git a/packages/demos/game-playground/src/state/player-states.ts b/packages/demos/game-playground/src/state/player-states.ts deleted file mode 100644 index edb976f..0000000 --- a/packages/demos/game-playground/src/state/player-states.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const enum PlayerState { - idle = 'idle', - walk = 'walk', - club_attack = 'club_attack' -} - diff --git a/packages/demos/game-playground/src/state/state-status.ts b/packages/demos/game-playground/src/state/state-status.ts deleted file mode 100644 index a283a80..0000000 --- a/packages/demos/game-playground/src/state/state-status.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum StateStatus { - NOT_STARTED = 0, - STARTED = 1, - FINISHED = 2 -} \ No newline at end of file diff --git a/packages/demos/game-playground/src/system/AttackingWithClubSystem.ts b/packages/demos/game-playground/src/system/AttackingWithClubSystem.ts deleted file mode 100644 index c42be1d..0000000 --- a/packages/demos/game-playground/src/system/AttackingWithClubSystem.ts +++ /dev/null @@ -1,69 +0,0 @@ -import {Direction, Directions } from "@serbanghita-gamedev/component"; -import {Entity, System} from "@serbanghita-gamedev/ecs"; -import {StateStatus} from "../state/state-status"; - -import IsAttackingWithClub from "../component/IsAttackingWithClub"; - -export default class AttackingWithClubSystem extends System { - - private onEnter(entity: Entity, component: IsAttackingWithClub) - { - component.properties.tick = 0; - component.properties.animationTick = 0; - component.properties.status = StateStatus.STARTED; - } - - private onUpdate(entity: Entity, component: IsAttackingWithClub) - { - const direction = entity.getComponent(Direction); - - if (component.properties.animationTick === 5) { - this.onExit(entity, component); - } - - if (direction.properties.y === Directions.UP) { - component.properties.animationStateName = 'club_attack_one_up'; - } else if (direction.properties.y === Directions.DOWN) { - component.properties.animationStateName = 'club_attack_one_down'; - } - - if (direction.properties.x === Directions.LEFT) { - component.properties.animationStateName = 'club_attack_one_left'; - } else if (direction.properties.x === Directions.RIGHT) { - component.properties.animationStateName = 'club_attack_one_right'; - } - - component.properties.tick++; - if (component.properties.tick % 15 === 0) { - component.properties.animationTick += 1; - } - // console.log(component.properties.tick); - // console.log(component.properties.animationTick); - } - - private onExit(entity: Entity, component: IsAttackingWithClub) { - component.properties.status = StateStatus.FINISHED; - } - - public update(now: number): void { - this.query.execute().forEach(entity => { - const component = entity.getComponent(IsAttackingWithClub); - - console.log('IsAttackingWithClub', entity.id); - - if (component.properties.status === StateStatus.FINISHED) { - entity.removeComponent(IsAttackingWithClub); - return; - } - - if (component.properties.status === StateStatus.NOT_STARTED) { - this.onEnter(entity, component); - } - - this.onUpdate(entity, component); - - return true; - }); - } - -} \ No newline at end of file diff --git a/packages/demos/game-playground/src/system/IdleSystem.ts b/packages/demos/game-playground/src/system/IdleSystem.ts deleted file mode 100644 index 85a25f1..0000000 --- a/packages/demos/game-playground/src/system/IdleSystem.ts +++ /dev/null @@ -1,63 +0,0 @@ -import {Direction} from "@serbanghita-gamedev/component"; -import {System, Entity} from "@serbanghita-gamedev/ecs"; -import IsIdle from "../component/IsIdle"; -import {StateStatus} from "../state/state-status"; - -export default class IdleSystem extends System { - - private onEnter(entity: Entity, component: IsIdle) - { - component.properties.tick = 0; - component.properties.animationTick = 0; - component.properties.status = StateStatus.STARTED; - } - - private onUpdate(entity: Entity, component: IsIdle) - { - // Loop. @todo: move logic - // if (component.properties.tick === 10) { - // this.onEnter(entity, component); - // } - - const direction = entity.getComponent(Direction); - const directionLiteral = direction.properties.literal || ''; - - // console.log(`idle_${directionLiteral}`); - - component.properties.animationStateName = directionLiteral ? `idle_${directionLiteral}` : 'idle'; - component.properties.tick++; - if (component.properties.tick % 15 === 0) { - component.properties.animationTick += 1; - } - - // console.log(component.properties.animationTick); - } - - private onExit(entity: Entity, component: IsIdle) { - component.properties.status = StateStatus.FINISHED; - } - - public update(now: number): void { - this.query.execute().forEach((entity: Entity) => { - - const component = entity.getComponent(IsIdle); - - // console.log('IsIdle', entity.id); - - if (component.properties.status === StateStatus.FINISHED) { - console.log('IsIdle finished and removed'); - entity.removeComponent(IsIdle); - return; - } - - if (component.properties.status === StateStatus.NOT_STARTED) { - this.onEnter(entity, component); - } - - this.onUpdate(entity, component); - - return true; - }); - } - -} \ No newline at end of file diff --git a/packages/demos/tiled-rendering/src/index.ts b/packages/demos/tiled-rendering/src/index.ts index 13aad0d..091df4f 100644 --- a/packages/demos/tiled-rendering/src/index.ts +++ b/packages/demos/tiled-rendering/src/index.ts @@ -1,7 +1,6 @@ -// 0. Create the UI and canvas. -import { createWrapperElement, createCanvas, run, dot, rectangle } from "@serbanghita-gamedev/renderer"; +import { createHtmlUiElements } from "@serbanghita-gamedev/renderer"; import { World } from "@serbanghita-gamedev/ecs"; -import { PreRenderTiledMapSystem } from "@serbanghita-gamedev/renderer"; +import { RenderTiledMapTerrainSystem } from "@serbanghita-gamedev/renderer"; import { IsTiledMap, Renderable } from "@serbanghita-gamedev/component"; import { loadSprites } from "./assets"; import { Point, Rectangle } from "@serbanghita-gamedev/geometry"; @@ -19,26 +18,16 @@ import IsPlayer from "./IsPlayer"; import MoveSystem from "./MoveSystem"; async function setup() { - const HTML_WRAPPER = createWrapperElement("game-wrapper", 640, 480); - const CANVAS_FOREGROUND = createCanvas("foreground", 640, 480, "2"); - const CTX_FOREGROUND = CANVAS_FOREGROUND.getContext("2d") as CanvasRenderingContext2D; - const CANVAS_BACKGROUND = createCanvas("background", 640, 480, "1"); - const CTX_BACKGROUND = CANVAS_BACKGROUND.getContext("2d") as CanvasRenderingContext2D; - HTML_WRAPPER.appendChild(CANVAS_FOREGROUND); - HTML_WRAPPER.appendChild(CANVAS_BACKGROUND); - document.body.appendChild(HTML_WRAPPER); + // 0. Create the UI and canvas. + const [, , $ctxBackground, $canvasForeground, $ctxForeground] = createHtmlUiElements(); + // 1. Load sprite sheets IMGs. const SPRITES = await loadSprites(); // Create the current "World" (scene). const world = new World(); - world.registerComponent(IsTiledMap); - world.registerComponent(IsMatrix); - world.registerComponent(IsCollisionTile); - world.registerComponent(IsPreRendered); - world.registerComponent(IsRenderedInForeground); - world.registerComponent(IsPlayer); + world.registerComponents([IsTiledMap, IsMatrix, IsCollisionTile, IsPreRendered, IsRenderedInForeground, IsPlayer]); // Load the map from Tiled json file declaration. // Create a dedicated map entity. @@ -64,22 +53,22 @@ async function setup() { }); const TiledMapQuery = world.createQuery("TiledMapQuery", { all: [IsTiledMap] }); - world.createSystem(PreRenderTiledMapSystem, TiledMapQuery, CANVAS_BACKGROUND, SPRITES["./assets/sprites/terrain.png"]).runOnlyOnce(); + world.createSystem(RenderTiledMapTerrainSystem, TiledMapQuery, $ctxBackground, SPRITES["./assets/sprites/terrain.png"]).runOnlyOnce(); // Quadtree const area = new Rectangle(640, 480, new Point(640 / 2, 480 / 2)); const quadtree = new QuadTree(area, 5, 5); const CollisionTilesQuery = world.createQuery("CollisionTilesQuery", { all: [IsCollisionTile] }); - world.createSystem(PreRenderCollisionTilesSystem, CollisionTilesQuery, CTX_BACKGROUND).runOnlyOnce(); + world.createSystem(PreRenderCollisionTilesSystem, CollisionTilesQuery, $ctxBackground).runOnlyOnce(); const RenderingQuery = world.createQuery("RenderingQuery", { all: [IsRenderedInForeground] }); - world.createSystem(RenderingSystem, RenderingQuery, CTX_FOREGROUND, quadtree); + world.createSystem(RenderingSystem, RenderingQuery, $ctxForeground, quadtree); const IsPlayerQuery = world.createQuery("IsPlayerQuery", { all: [IsPlayer] }); world.createSystem(MoveSystem, IsPlayerQuery); world.createSystem(QuadTreeSystem, IsPlayerQuery, quadtree); - CANVAS_FOREGROUND.addEventListener("dblclick", (event) => { + $canvasForeground.addEventListener("dblclick", (event) => { const x = event.clientX | 0; const y = event.clientY | 0; diff --git a/packages/ecs/src/Entity.ts b/packages/ecs/src/Entity.ts index deb7245..cd05df0 100644 --- a/packages/ecs/src/Entity.ts +++ b/packages/ecs/src/Entity.ts @@ -1,82 +1,80 @@ import Component from "./Component"; -import {addBit, hasBit, removeBit} from "@serbanghita-gamedev/bitmask"; +import { addBit, hasBit, removeBit } from "@serbanghita-gamedev/bitmask"; import World from "./World"; export default class Entity { - // Bitmask for storing Entity's components. - public componentsBitmask = 0n; - // Cache of Component instances. - public components = new Map(); + // Bitmask for storing Entity's components. + public componentsBitmask = 0n; + // Cache of Component instances. + public components = new Map(); + + constructor( + public world: World, + public id: string, + ) {} + + public addComponent(declaration: T, properties: object = {}): Entity { + let instance = this.components.get(declaration.name); + // If the Component's instance is already in our cache, just re-use the instance and lazy init it. + if (instance) { + instance.init(properties); + } else { + instance = new declaration(properties); + } - constructor(public world: World, public id: string) {} + this.components.set(instance.constructor.name, instance); - public addComponent(declaration: T, properties: object = {}): Entity - { - let instance = this.components.get(declaration.name); - // If the Component's instance is already in our cache, just re-use the instance and lazy init it. - if (instance) { - instance.init(properties); - } else { - instance = new declaration(properties); - } + if (typeof instance.bitmask === "undefined") { + throw new Error(`Please register the component ${instance.constructor.name} in the ComponentRegistry.`); + } - this.components.set(instance.constructor.name, instance); + this.componentsBitmask = addBit(this.componentsBitmask, instance.bitmask); - if (typeof instance.bitmask === "undefined") { - throw new Error(`Please register the component ${instance.constructor.name} in the ComponentRegistry.`); - } + this.onAddComponent(instance); - this.componentsBitmask = addBit(this.componentsBitmask, instance.bitmask); + return this; + } - this.onAddComponent(instance); + public getComponent(declaration: T): InstanceType { + const instance = this.components.get(declaration.name) as InstanceType; - return this; + if (!instance) { + throw new Error(`Component requested ${declaration.name} is non-existent.`); } - public getComponent(declaration: T): InstanceType - { - const instance = this.components.get(declaration.name) as InstanceType; + return instance; + } - if (!instance) { - throw new Error(`Component requested ${declaration.name} is non-existent.`); - } + public getComponentByName(name: string): Component { + const instance = this.components.get(name); - return instance; + if (!instance) { + throw new Error(`Component requested ${name} is non-existent.`); } - public getComponentByName(name: string): Component { - const instance = this.components.get(name); + return instance; + } - if (!instance) { - throw new Error(`Component requested ${name} is non-existent.`); - } + public removeComponent(declaration: T): Entity { + const component = this.getComponent(declaration); - return instance; - } + this.componentsBitmask = removeBit(this.componentsBitmask, component.bitmask); - public removeComponent(declaration: T): Entity - { - const component = this.getComponent(declaration); + this.onRemoveComponent(component); - this.componentsBitmask = removeBit(this.componentsBitmask, component.bitmask); + return this; + } - this.onRemoveComponent(component); + public hasComponent(declaration: typeof Component): boolean { + return hasBit(this.componentsBitmask, declaration.prototype.bitmask); + } - return this; - } + private onAddComponent(newComponent: Component) { + this.world.notifyQueriesOfEntityComponentAddition(this, newComponent); + return this; + } - public hasComponent(declaration: typeof Component): boolean { - return hasBit(this.componentsBitmask, declaration.prototype.bitmask); - } - - private onAddComponent(newComponent: Component) - { - this.world.notifyQueriesOfEntityComponentAddition(this, newComponent); - return this; - } - - private onRemoveComponent(oldComponent: Component) - { - this.world.notifyQueriesOfEntityComponentRemoval(this, oldComponent); - } -} \ No newline at end of file + private onRemoveComponent(oldComponent: Component) { + this.world.notifyQueriesOfEntityComponentRemoval(this, oldComponent); + } +} diff --git a/packages/ecs/src/World.ts b/packages/ecs/src/World.ts index 7efdd24..89f6880 100644 --- a/packages/ecs/src/World.ts +++ b/packages/ecs/src/World.ts @@ -1,4 +1,4 @@ -import Entity from "./Entity"; +import Entity, { EntityDeclaration } from "./Entity"; import System, { SystemSettings } from "./System"; import Query, { IQueryFilters } from "./Query"; import Component from "./Component"; @@ -15,11 +15,14 @@ export default class World { public systems = new Map(); public fps: number = 0; - public registerComponent(declaration: typeof Component) - { + public registerComponent(declaration: typeof Component) { this.declarations.components.registerComponent(declaration); } + public registerComponents(declarations: (typeof Component)[]) { + this.declarations.components.registerComponents(declarations); + } + public createQuery(id: string, filters: IQueryFilters): Query { const query = new Query(this, id, filters); @@ -46,6 +49,19 @@ export default class World { return query; } + public createEntityFromDeclaration(entityDeclaration: EntityDeclaration) { + // Create the entity and assign it to the world. + const entity = this.createEntity(entityDeclaration.id); + + // Add Component(s) to the Entity. + for (const name in entityDeclaration.components) { + const componentDeclaration = this.declarations.components.getComponent(name); + const props = entityDeclaration.components[name]; + + entity.addComponent(componentDeclaration, props); + } + } + public createEntity(id: string): Entity { if (this.entities.has(id)) { throw new Error(`Entity with the id "${id}" already exists.`); diff --git a/packages/ecs/src/index.ts b/packages/ecs/src/index.ts index 7a841e5..c7d058f 100644 --- a/packages/ecs/src/index.ts +++ b/packages/ecs/src/index.ts @@ -5,13 +5,13 @@ import Component from "./Component"; import ComponentRegistry from "./ComponentRegistry"; -import Entity from "./Entity"; +import Entity, {EntityDeclaration} from "./Entity"; import Query from "./Query"; import System from "./System"; import World from "./World"; export { Component, ComponentRegistry, - Entity, Query, + Entity, EntityDeclaration, Query, System, World -} \ No newline at end of file +} diff --git a/packages/renderer/package.json b/packages/renderer/package.json index 01c55ba..3d7f1bf 100644 --- a/packages/renderer/package.json +++ b/packages/renderer/package.json @@ -2,8 +2,6 @@ "name": "@serbanghita-gamedev/renderer", "version": "1.0.0", "scripts": { - "build": "webpack --mode development", - "dev": "webpack --mode development --watch", "test": "echo \"Error: no test specified for renderer yet.\"" }, "author": "Serban Ghita", diff --git a/packages/demos/game-playground/src/system/PreRenderSystem.ts b/packages/renderer/src/AnimationRegistry.ts similarity index 52% rename from packages/demos/game-playground/src/system/PreRenderSystem.ts rename to packages/renderer/src/AnimationRegistry.ts index b5e3b1a..02e7737 100644 --- a/packages/demos/game-playground/src/system/PreRenderSystem.ts +++ b/packages/renderer/src/AnimationRegistry.ts @@ -1,11 +1,36 @@ -import { Entity, System } from "@serbanghita-gamedev/ecs"; import { Animation, SpriteSheet } from "@serbanghita-gamedev/component"; -import { ANIMATIONS_DECLARATIONS, ANIMATIONS_REGISTRY } from "../assets"; +import { Assets, EntityDeclaration } from "@serbanghita-gamedev/assets"; -export default class PreRenderSystem extends System { - private setAnimationFramesForSpriteSheet(entity: Entity) { - const spriteSheet = entity.getComponent(SpriteSheet); - const spriteSheetAnimations = ANIMATIONS_DECLARATIONS[spriteSheet.properties.spriteSheetAnimationsPath]; +export type AnimationRegistryItem = { + animationDefaultFrame: string; + animations: Map; +}; + +export function loadAnimationRegistry(assets: Assets): AnimationRegistry { + const instance = new AnimationRegistry(assets); + instance.load(); + + return instance; +} + +export default class AnimationRegistry { + // Store pre-computed animation frames from the sprite sheet asset. + private animations: Map = new Map(); + + public constructor(public assets: Assets) {} + + public load() { + this.assets["entities/declarations"].forEach((entityDeclaration) => this.setAnimationFramesForSpriteSheet(entityDeclaration)); + } + + public getAnimationsFor(spriteSheetAnimationsPath: string) + { + return this.animations.get(spriteSheetAnimationsPath); + } + + public setAnimationFramesForSpriteSheet(entityDeclaration: EntityDeclaration) { + const spriteSheet = entityDeclaration.components.SpriteSheet; + const spriteSheetAnimations = this.assets["entities/animations"][spriteSheet.properties.spriteSheetAnimationsPath]; if (!spriteSheetAnimations) { throw new Error(`Animations JSON file ${spriteSheet.properties.spriteSheetAnimationsPath} is missing.`); @@ -48,12 +73,6 @@ export default class PreRenderSystem extends System { animationIndex++; } - ANIMATIONS_REGISTRY[spriteSheet.properties.spriteSheetAnimationsPath] = { animationDefaultFrame, animations }; - } - - public update(now: number): void { - this.query.execute().forEach((entity) => { - this.setAnimationFramesForSpriteSheet(entity); - }); + this.animations.set(spriteSheet.properties.spriteSheetAnimationsPath, { animationDefaultFrame, animations }); } } diff --git a/packages/renderer/src/PreRenderTiledMapSystem.ts b/packages/renderer/src/RenderTiledMapTerrainSystem.ts similarity index 72% rename from packages/renderer/src/PreRenderTiledMapSystem.ts rename to packages/renderer/src/RenderTiledMapTerrainSystem.ts index 6e7e772..bb6b7dc 100644 --- a/packages/renderer/src/PreRenderTiledMapSystem.ts +++ b/packages/renderer/src/RenderTiledMapTerrainSystem.ts @@ -1,23 +1,22 @@ import { System, Query, World } from "@serbanghita-gamedev/ecs"; import IsTiledMap from "../../component/src/IsTiledMap"; import { renderTile } from "@serbanghita-gamedev/renderer"; -import { TiledMap, TiledMapFile } from "@serbanghita-gamedev/tiled"; +import { TiledMap } from "@serbanghita-gamedev/tiled"; import { getTileCoordinates } from "@serbanghita-gamedev/matrix"; -import { getCtx } from "@serbanghita-gamedev/renderer"; import { rectangle } from "./canvas"; -export default class PreRenderTiledMapSystem extends System { +export default class RenderTiledMapTerrainSystem extends System { public constructor( public world: World, public query: Query, - protected CANVAS_BACKGROUND: HTMLCanvasElement, - protected TERRAIN_SPRITE: HTMLImageElement, + protected ctx: CanvasRenderingContext2D, + protected terrainSprite: HTMLImageElement, ) { super(world, query); } private renderToBackgroundLayer(tiledMap: TiledMap) { - if (!this.CANVAS_BACKGROUND) { + if (!this.ctx) { throw new Error(`Background canvas ($background) was not created or passed.`); } @@ -31,9 +30,8 @@ export default class PreRenderTiledMapSystem extends System { } renderTile( - // @ts-expect-error Not sure why TS sees this as potentially null. - getCtx(this.CANVAS_BACKGROUND), // @todo: Use buffer/off canvas. - this.TERRAIN_SPRITE, + this.ctx, // @todo: Use buffer/off canvas. + this.terrainSprite, tiledMap.getTileWidth(), tiledMap.getTileHeight(), j, @@ -53,16 +51,7 @@ export default class PreRenderTiledMapSystem extends System { const tileCoordinates = getTileCoordinates(j, { width: tiledMap.getWidthInTiles(), height: tiledMap.getHeightInTiles(), tileSize: tiledMap.getTileWidth() }); - rectangle( - // @ts-expect-error Not sure why TS sees this as potentially null. - getCtx(this.CANVAS_BACKGROUND), - tileCoordinates.x, - tileCoordinates.y, - tiledMap.getTileWidth(), - tiledMap.getTileHeight(), - "rgb(125,0,0)", - "rgba(255,0,0,0.1)", - ); + rectangle(this.ctx, tileCoordinates.x, tileCoordinates.y, tiledMap.getTileWidth(), tiledMap.getTileHeight(), "rgb(125,0,0)", "rgba(255,0,0,0.1)"); // j, // layer.data[j], } diff --git a/packages/renderer/src/animation.ts b/packages/renderer/src/animation.ts index ba20f44..6de33f5 100644 --- a/packages/renderer/src/animation.ts +++ b/packages/renderer/src/animation.ts @@ -1,10 +1,11 @@ -import {ISpriteSheetAnimation} from "../../component/src"; +import { Animation, SpriteSheet, SpriteSheetAnimation } from "@serbanghita-gamedev/component"; +import { Entity } from "@serbanghita-gamedev/ecs"; -export function getDefaultAnimationName(animations: ISpriteSheetAnimation[]): string -{ - try { - return (animations.find((animationFrame) => animationFrame.defaultAnimation) as ISpriteSheetAnimation)['name']; - } catch (e) { - throw Error('Your animations file does not contain a "default" animation. Please set "defaultAnimation": true property on one of the animations.'); - } -} \ No newline at end of file +// export function getDefaultAnimationName(animations: ISpriteSheetAnimation[]): string +// { +// try { +// return (animations.find((animationFrame) => animationFrame.defaultAnimation) as ISpriteSheetAnimation)['name']; +// } catch (e) { +// throw Error('Your animations file does not contain a "default" animation. Please set "defaultAnimation": true property on one of the animations.'); +// } +// } diff --git a/packages/renderer/src/index.ts b/packages/renderer/src/index.ts index 3e76d91..aa2e397 100644 --- a/packages/renderer/src/index.ts +++ b/packages/renderer/src/index.ts @@ -1,4 +1,5 @@ export * from "./animation"; export * from "./canvas"; export * from "./ui"; -export {default as PreRenderTiledMapSystem} from "./PreRenderTiledMapSystem"; \ No newline at end of file +export { default as RenderTiledMapTerrainSystem } from "./RenderTiledMapTerrainSystem"; +export { default as AnimationRegistry, AnimationRegistryItem, loadAnimationRegistry } from "./AnimationRegistry"; diff --git a/packages/renderer/src/ui.ts b/packages/renderer/src/ui.ts index 11b95b5..02eacff 100644 --- a/packages/renderer/src/ui.ts +++ b/packages/renderer/src/ui.ts @@ -9,15 +9,20 @@ export function createWrapperElement(layerId: string, width?: number, height?: n } return $elem; } -export function createHtmlUiElements() { - const HTML_WRAPPER = createWrapperElement("game-wrapper", 640, 480); - const CANVAS_BACKGROUND = createCanvas("background", 640, 480, "1"); - const CANVAS_FOREGROUND = createCanvas("foreground", 640, 480, "2"); - HTML_WRAPPER.appendChild(CANVAS_BACKGROUND); - HTML_WRAPPER.appendChild(CANVAS_FOREGROUND); +type UiElements = [HTMLDivElement, HTMLCanvasElement, CanvasRenderingContext2D, HTMLCanvasElement, CanvasRenderingContext2D]; - document.body.appendChild(HTML_WRAPPER); +export function createHtmlUiElements(): UiElements { + const $htmlWrapper = createWrapperElement("game-wrapper", 640, 480); + const $canvasBackground = createCanvas("background", 640, 480, "1"); + const $ctxBackground = $canvasBackground.getContext('2d') as CanvasRenderingContext2D; + const $canvasForeground = createCanvas("foreground", 640, 480, "2"); + const $ctxForeground = $canvasForeground.getContext('2d') as CanvasRenderingContext2D; - return [HTML_WRAPPER, CANVAS_BACKGROUND, CANVAS_FOREGROUND]; -} \ No newline at end of file + $htmlWrapper.appendChild($canvasBackground); + $htmlWrapper.appendChild($canvasForeground); + + document.body.appendChild($htmlWrapper); + + return [$htmlWrapper, $canvasBackground, $ctxBackground, $canvasForeground, $ctxForeground]; +} diff --git a/packages/renderer/tsconfig.json b/packages/renderer/tsconfig.json index 15efea2..2d74f2b 100644 --- a/packages/renderer/tsconfig.json +++ b/packages/renderer/tsconfig.json @@ -10,7 +10,12 @@ "baseUrl": "src", "paths": { "@serbanghita-gamedev/bitmask": ["../../bitmask"], - "@serbanghita-gamedev/tiled": ["../../tiled"] + "@serbanghita-gamedev/tiled": ["../../tiled"], + "@serbanghita-gamedev/component": ["../../component"], + "@serbanghita-gamedev/ecs": ["../../ecs"], + "@serbanghita-gamedev/assets": ["../../assets"], + "@serbanghita-gamedev/renderer": ["../../renderer"], + "@serbanghita-gamedev/matrix": ["../../matrix"] } }, "include": [