From 264db31ac7e8dddd760a5347eb62dfb11a7aeaff Mon Sep 17 00:00:00 2001 From: Tangent Wantwight Date: Sat, 4 Apr 2020 18:52:25 -0400 Subject: [PATCH] Refactor typings to enforce cloning hygiene, at expense of intrusive Component type --- plan.txt | 5 +- src/Ecs/Components.ts | 168 ++++++++++++++++++++++++------------- src/Ecs/Data.ts | 148 +++++++++++++++++++------------- src/Ecs/Location.ts | 2 +- src/Ecs/test.ts | 148 ++++++++++++++++++++++---------- src/Game/Death.ts | 17 ++-- src/Game/GameComponents.ts | 132 +++++++++++++++++++---------- src/Game/Main.ts | 11 ++- src/Game/Message.ts | 6 +- 9 files changed, 418 insertions(+), 219 deletions(-) diff --git a/plan.txt b/plan.txt index 64616ea..eebd8b1 100644 --- a/plan.txt +++ b/plan.txt @@ -1,10 +1,11 @@ Open: -- Rework State implementation for easier cloning/deserialization -- Insecured websocket server implementation - Refactor input messages for more than one player +- Insecured websocket server implementation - Cloneable RNG that goes in state (use MurmurHash3 finalizer in counter mode?) +- remove all random() calls Done: +- Rework State implementation for easier cloning/deserialization - Test Lockstep/rollback - Smarter typings for Join/Lookup functions - Parcel scripts diff --git a/src/Ecs/Components.ts b/src/Ecs/Components.ts index a2624e0..bdbe794 100644 --- a/src/Ecs/Components.ts +++ b/src/Ecs/Components.ts @@ -1,6 +1,6 @@ import { Layer, SpriteSheet } from "../Applet/Render"; -import { copy, Data as CoreData, SparseStore, Store } from "./Data"; +import { Component, copyDense, copySparse, EntityState, StateForSchema, Store } from "./Data"; export class Box { constructor( @@ -24,64 +24,116 @@ export function Approach(source: number, target: number, speed: number): number /** * pairs of vertex coordinates in ccw winding order */ -export class Polygon { - constructor( - public points: number[] - ) {}; +export interface Polygon { + points: number[]; } - -export class Location { - constructor(init?: Partial) { - init && Object.assign(this, init); +export class PolygonComponent extends Component { + points: number[]; + constructor(from: Partial) { + super(from); + this.points = from.points?.slice() ?? [] }; - X = 0; - Y = 0; - Angle = 0; - VX = 0; - VY = 0; - VAngle = 0; -} - -export class CollisionClass { - constructor( - public name: string - ) {}; -} - -export class RenderBounds { - constructor( - public color = "#f00", - public layer: number - ) {}; -}; - -export class RenderSprite { - constructor( - public sheet: SpriteSheet, - public layer: number, - public index = 0, - public offsetX = 0, - public offsetY = 0 - ) {}; -}; - -export class Data extends CoreData { - location: Store = []; - bounds: Store = []; - renderBounds: SparseStore = {}; - renderSprite: SparseStore = {}; - collisionSourceClass: SparseStore = {}; - collisionTargetClass: SparseStore = {}; - - layers: Layer[] = [new Layer(0)]; - - constructor(source?: Partial) { - super(source); - if(source?.location) this.location = source.location.map(b => ({...b})); - if(source?.bounds) this.bounds = source.bounds.map(b => ({...b})); - if(source?.renderBounds) this.renderBounds = copy(source.renderBounds); - if(source?.renderSprite) this.renderSprite = copy(source.renderSprite); - if(source?.collisionSourceClass) this.collisionSourceClass = copy(source.collisionSourceClass); - if(source?.collisionTargetClass) this.collisionTargetClass = copy(source.collisionTargetClass); + clone() { + return new PolygonComponent(this); + } +} + +export class Location extends Component { + X: number; + Y: number; + Angle: number; + VX: number; + VY: number; + VAngle: number; + constructor(from: Partial) { + super(from); + this.X = from.X ?? 0; + this.Y = from.Y ?? 0; + this.Angle = from.Angle ?? 0; + this.VX = from.VX ?? 0; + this.VY = from.VY ?? 0; + this.VAngle = from.VAngle ?? 0; + }; + clone() { + return new Location(this); + } +} + +export class CollisionClass extends Component { + public name: string; + constructor(from: Partial) { + super(from); + this.name = from.name ?? "unknown"; + }; + clone() { + return new CollisionClass(this); + } +} + +export class RenderBounds extends Component { + public color: string; + public layer: number; + constructor(from: Partial) { + super(from); + this.color = from.color ?? "#f00"; + this.layer = from.layer ?? 1; + }; + clone() { + return new RenderBounds(this); + } +}; + +export class RenderSprite extends Component { + // TODO: make this an id/handle for serializability + public sheet: SpriteSheet; + public layer: number; + public index: number; + public offsetX: number; + public offsetY: number; + constructor(from: Partial & {sheet: SpriteSheet}) { + super(from); + this.sheet = from.sheet; + this.layer = from.layer ?? 1; + this.index = from.index ?? 0; + this.offsetX = from.offsetX ?? 0; + this.offsetY = from.offsetY ?? 0; + }; + clone() { + return new RenderSprite(this); + } +}; + +export interface ComponentSchema { + location: Location; + bounds: PolygonComponent; + renderBounds: RenderBounds; + renderSprite: RenderSprite; + collisionSourceClass: CollisionClass; + collisionTargetClass: CollisionClass; +} + +export class Data implements StateForSchema { + entity: EntityState[]; + + location: Store; + bounds: Store; + renderBounds: Store; + renderSprite: Store; + collisionSourceClass: Store; + collisionTargetClass: Store; + + layers: Layer[] = [new Layer(0), new Layer(1)]; + + constructor(from: Partial) { + this.entity = copyDense(from.entity); + this.location = copyDense(from.location); + this.bounds = copyDense(from.bounds); + this.renderBounds = copySparse(from.renderBounds); + this.renderSprite = copySparse(from.renderSprite); + this.collisionSourceClass = copySparse(from.collisionSourceClass); + this.collisionTargetClass = copySparse(from.collisionTargetClass); + } + clone() { + return new Data(this); } } diff --git a/src/Ecs/Data.ts b/src/Ecs/Data.ts index 8c2e156..40ec0ab 100644 --- a/src/Ecs/Data.ts +++ b/src/Ecs/Data.ts @@ -1,4 +1,33 @@ +export type Store> = Record; +export interface Clone { + clone(): T; +} + +export function copyDense, S extends Store>(storage?: S): T[] { + if(Array.isArray(storage)) { + return storage.map(x => x.clone()); + } else if(storage) { + const result: T[] = []; + for(const key in storage) { + result[key] = storage[key].clone(); + } + return result; + } else { + return []; + } +} +export function copySparse, S extends Store>(storage?: S): Store { + if(storage) { + const result: Store = {}; + for(const key in storage) { + result[key] = storage[key].clone(); + } + return result; + } else { + return {}; + } +} export type Id = [number, number]; @@ -8,41 +37,36 @@ export const enum Liveness { INACTIVE = 2 } -export interface HasGeneration { - generation: number; -} -export interface EntityState { - alive: Liveness; +export abstract class Component implements Clone> { + public generation: number; + public constructor(from: Partial>) { + this.generation = from.generation ?? -1; + } + abstract clone(): Component & T; } +export class EntityState extends Component { + public alive: Liveness; + public constructor(from: Partial) { + super(from); + this.alive = from.alive ?? Liveness.ALIVE; + } -export type Store = (T & HasGeneration)[]; -export type SparseStore = Record; -export class Data { - entity: Store = []; - - constructor(source?: Partial) { - if(source?.entity) this.entity = source.entity.slice(); + clone(): EntityState { + return new EntityState(this); } } -export function copy(source: SparseStore): SparseStore { - return JSON.parse(JSON.stringify(source)); -} +export type StateForSchema = { + [K in keyof T]: Record>; +} & { + entity: EntityState[]; +} & Clone>; // Ergonomic Lookup typings type StoreKeysOf = { - [K in keyof DATA]: DATA[K] extends Record ? K : never; -}; -type StoreKeys = StoreKeysOf[keyof DATA]; -type ItemType = S extends Record ? T : never; -type StoreType = K extends "id" ? Id : K extends keyof DATA ? ItemType : never; -type StoreTypes = { - [I in keyof K]: StoreType; -}; -type MaybeStoreTypes = { - [I in keyof K]: StoreType | null; -}; - + [K in keyof DATA]: DATA[K] extends Record> ? K : never; +}[keyof DATA]; +type StoreTypeOf = K extends keyof DATA ? DATA[K] extends Record> ? T : never : never; /** * Create an entity in the store @@ -51,35 +75,37 @@ type MaybeStoreTypes = { * @param state Liveness state, allows creating an inactive entity * @returns the new entity's ID and generation */ -type Assigner = { - [S in StoreKeys]?: StoreType +type Assigner = { + [K in StoreKeysOf]?: StoreTypeOf; }; -export function Create(data: DATA, assign: Assigner, state = Liveness.ALIVE): Id { +export function Create>(data: DATA, assign: Assigner, state = Liveness.ALIVE): Id { const entities = data.entity; // find free ID let freeId = -1; let generation = -1; - for(let id = 0; id < entities.length; id++) { - if(entities[id].alive == Liveness.DEAD) { + for (let id = 0; id < entities.length; id++) { + if (entities[id].alive == Liveness.DEAD) { freeId = id; generation = entities[id].generation + 1; break; } } - if(freeId == -1) { + if (freeId == -1) { freeId = entities.length; generation = 1; } - entities[freeId] = { + entities[freeId] = new EntityState({ generation, alive: state - }; + }); - for(const key in assign) { - const store = data[key as keyof Data] as Store<{}>|SparseStore<{}>; - store[freeId] = {...(assign as Record)[key] as {}, generation}; + for (const key in assign) { + const store = data[key as keyof DATA] as Store; + const component = (assign[key as keyof Assigner] as Component).clone(); + component.generation = generation; + store[freeId] = component; } return [freeId, generation]; @@ -92,8 +118,8 @@ export function Create(data: DATA, assign: Assigner, st * @param generation entity ID generation * @param state can be set to Liveness.INACTIVE to disable an entity without actually killing it, for later resurrection */ -export function Remove(data: DATA, [id, generation]: Id, state = Liveness.DEAD) { - if(data.entity[id] && data.entity[id].generation == generation) { +export function Remove(data: StateForSchema, [id, generation]: Id, state = Liveness.DEAD) { + if (data.entity[id] && data.entity[id].generation == generation) { data.entity[id].alive = state; } } @@ -105,21 +131,24 @@ export function Remove(data: DATA, [id, generation]: Id, stat * @param components names of components to look for * @returns the cooresponding components, with unfound ones replaced by nulls */ -export function Lookup[]>(data: DATA, [id, generation]: Id, ...components: K): MaybeStoreTypes { +type MaybeStoreTypes = { + [I in keyof Q]: StoreTypeOf | null; +}; +export function Lookup, Q extends StoreKeysOf[]>(data: DATA, [id, generation]: Id, ...components: Q): MaybeStoreTypes { const entity = data.entity[id]; // inactive entities are fine to lookup, but dead ones are not - if(entity && entity.generation == generation && entity.alive != Liveness.DEAD) { + if (entity && entity.generation == generation && entity.alive != Liveness.DEAD) { return components.map(storeName => { - const store = data[storeName] as unknown as Store<{}>|SparseStore<{}>; + const store = data[storeName as unknown as keyof DATA] as Store; const component = store[id]; - if(component && component.generation == generation) { + if (component && component.generation == generation) { return component; } else { return null; } - }) as MaybeStoreTypes; + }) as MaybeStoreTypes; } else { - return components.map(() => null) as MaybeStoreTypes; + return components.map(() => null) as MaybeStoreTypes; } } @@ -128,36 +157,39 @@ export function Lookup[]>(data: DAT * "id" can be used as a pseudo-component to get the Ids for a match * @returns an array of tuples containing the matching entity Components */ -export function Join | "id")[]>(data: DATA, ...components: K): StoreTypes[] { +type ResultTypes = { + [I in keyof Q]: Q[I] extends "id" ? Id : StoreTypeOf; +}; +export function Join, Q extends (StoreKeysOf | "id")[]>(data: DATA, ...components: Q): ResultTypes[] { const entities = data.entity; const stores = components.map(name => { - if(name == "id") { + if (name == "id") { return "id"; } else { - return data[name] as unknown as (Store<{}>|SparseStore<{}>) + return data[name as unknown as keyof DATA] as Store; }; }); - const results: StoreTypes[] = []; - const firstStore = stores.filter(store => store !== "id")[0] as Store<{}>|SparseStore<{}>; - if(Array.isArray(firstStore)) { - for(let id = 0; id < firstStore.length; id++) { + const results: ResultTypes[] = []; + const firstStore = stores.filter(store => store !== "id")[0] as Store; + if (Array.isArray(firstStore)) { + for (let id = 0; id < firstStore.length; id++) { JoinLoop(id, entities, stores, results); } } else { - for(const id in firstStore) { + for (const id in firstStore) { JoinLoop(Number(id), entities, stores, results); } } return results; } -function JoinLoop | "id")[]>(id: number, entities: Store, stores: (Store<{}> | SparseStore<{}> | "id")[], results: StoreTypes[]) { +function JoinLoop, Q extends (StoreKeysOf | "id")[]>(id: number, entities: EntityState[], stores: (Record> | "id")[], results: ResultTypes[]) { const fullId: Id = [id, -1]; - const result: (HasGeneration | Id)[] = []; + const result: (Component<{}> | Id)[] = []; let generation = -1; for (const store of stores) { - if(store === "id") { + if (store === "id") { result.push(fullId); continue; } @@ -177,5 +209,5 @@ function JoinLoop | "id")[]>(id: n // backpatch generation now that it's known fullId[1] = generation; - results.push(result as StoreTypes); + results.push(result as ResultTypes); } diff --git a/src/Ecs/Location.ts b/src/Ecs/Location.ts index 2bea3d9..9189867 100644 --- a/src/Ecs/Location.ts +++ b/src/Ecs/Location.ts @@ -9,7 +9,7 @@ export function TransformCx(cx: CanvasRenderingContext2D, location: Location, dt export function TfPolygon({points}: Polygon, {X, Y, Angle}: Location): Polygon { const sin = Math.sin(Angle); const cos = Math.cos(Angle); - const result = new Polygon(new Array(points.length)); + const result = {points: new Array(points.length)}; for(let i = 0; i < points.length; i += 2) { const x = points[i]; const y = points[i+1]; diff --git a/src/Ecs/test.ts b/src/Ecs/test.ts index 3cae37b..ca39f7d 100644 --- a/src/Ecs/test.ts +++ b/src/Ecs/test.ts @@ -3,11 +3,42 @@ import { KeyControl } from "../Applet/Keyboard"; import { Loop } from "../Applet/Loop"; import { DrawSet, Layer } from "../Applet/Render"; import { FindCollisions } from "./Collision"; -import { CollisionClass, Data, Location, Polygon, RenderBounds } from "./Components"; -import { Create, Join, Liveness, Lookup, Remove, SparseStore, Store } from "./Data"; +import { + CollisionClass, + ComponentSchema, + Data, + Location, + PolygonComponent, + RenderBounds, +} from "./Components"; +import { + Component, + copySparse, + Create, + EntityState, + Join, + Liveness, + Lookup, + Remove, + StateForSchema, + Store, +} from "./Data"; import { DumbMotion } from "./Location"; import { RunRenderBounds } from "./Renderers"; +class Generic extends Component { + constructor(from: T) { + super(from); + Object.assign(this, from); + } + clone(): Generic & T { + return new Generic(this as unknown as T) as Generic & T; + } +} +function generic(from: T): Component & T { + return new Generic(from) as Component & T; +} + interface Apple {} interface Banana { peeled: boolean @@ -16,38 +47,63 @@ interface Carrot { cronch: number } -class TestData extends Data { - entity = [ - {generation: 5, alive: Liveness.ALIVE}, - {generation: 5, alive: Liveness.DEAD}, - {generation: 5, alive: Liveness.ALIVE}, - {generation: 5, alive: Liveness.ALIVE}, - {generation: 5, alive: Liveness.INACTIVE}, - {generation: 5, alive: Liveness.ALIVE}, - ]; - apple: Store = [ - {generation: 5}, - {generation: 5}, - {generation: -1}, - {generation: -1}, - {generation: 5}, - {generation: 5}, - ]; - banana: SparseStore = { - 3: {generation: 5, peeled: false}, - 4: {generation: 5, peeled: true}, - }; - carrot: SparseStore = { - 0: {generation: 5, cronch: 1}, - 1: {generation: 5, cronch: 1}, - 2: {generation: 4, cronch: 10}, - 3: {generation: 5, cronch: 1}, - }; +interface TestSchema extends ComponentSchema { + apple: Apple; + banana: Banana; + carrot: Carrot; +} + +class TestData extends Data implements StateForSchema { + apple: Store>; + banana: Store>; + carrot: Store>; + constructor(from: Partial) { + super(from); + this.apple = copySparse(from.apple); + this.banana = copySparse(from.banana); + this.carrot = copySparse(from.carrot); + } + clone(): TestData { + return new TestData(this); + } +} + +function makeTestData(): TestData { + return new TestData({ + entity: [ + new EntityState({generation: 5, alive: Liveness.ALIVE}), + new EntityState({generation: 5, alive: Liveness.DEAD}), + new EntityState({generation: 5, alive: Liveness.ALIVE}), + new EntityState({generation: 5, alive: Liveness.ALIVE}), + new EntityState({generation: 5, alive: Liveness.INACTIVE}), + new EntityState({generation: 5, alive: Liveness.ALIVE}), + ], + apple: [ + generic({generation: 5}), + generic({generation: 5}), + generic({generation: -1}), + generic({generation: -1}), + generic({generation: 5}), + generic({generation: 5}), + ], + banana: { + 3: generic({generation: 5, peeled: false}), + 4: generic({generation: 5, peeled: true}), + }, + carrot: { + 0: generic({generation: 5, cronch: 1}), + 1: generic({generation: 5, cronch: 1}), + 2: generic({generation: 4, cronch: 10}), + 3: generic({generation: 5, cronch: 1}), + }, + }); } class EcsJoinTest { constructor(pre: HTMLElement) { - const data = new TestData(); + const data = new TestData({ + + }); pre.innerText = JSON.stringify({ "apples": Join(data, "apple"), "bananas": Join(data, "banana"), @@ -59,7 +115,7 @@ class EcsJoinTest { class EcsLookupTest { constructor(pre: HTMLElement) { - const data = new TestData(); + const data = makeTestData(); const applesMaybeCarrots = Join(data, "apple", "id").map(([apple, id]) => ({ apple, maybeCarrot: Lookup(data, id, "carrot")[0] @@ -70,7 +126,7 @@ class EcsLookupTest { class EcsRemoveTest { constructor(pre: HTMLElement) { - const data = new TestData(); + const data = makeTestData(); const beforeDelete = Join(data, "apple", "carrot", "id",); Remove(data, [0, 5]); const afterDelete = Join(data, "apple", "carrot", "id"); @@ -83,12 +139,12 @@ class EcsRemoveTest { class EcsCreateTest { constructor(pre: HTMLElement) { - const data = new TestData(); + const data = makeTestData(); const beforeCreate = Join(data, "apple", "banana", "carrot", "id"); const createdId = Create(data, { - apple: {}, - banana: {peeled: false}, - carrot: {cronch: 11} + apple: generic({}), + banana: generic({peeled: false}), + carrot: generic({cronch: 11}) }); const afterCreate = Join(data, "apple", "banana", "carrot", "id"); pre.innerText = JSON.stringify({ @@ -100,7 +156,7 @@ class EcsCreateTest { } class LoopTest { - data = new Data(); + data = new Data({}); constructor(public canvas: HTMLCanvasElement, cx: CanvasRenderingContext2D, keys: KeyControl) { const drawSet = new DrawSet(); @@ -112,9 +168,9 @@ class LoopTest { Y: 200, VAngle: Math.PI }), - bounds: new Polygon([-50, 50, -60, 250, 60, 250, 50, 50]), - collisionTargetClass: new CollisionClass("block"), - renderBounds: new RenderBounds("#0a0", 0), + bounds: new PolygonComponent({points: [-50, 50, -60, 250, 60, 250, 50, 50]}), + collisionTargetClass: new CollisionClass({ name: "block"}), + renderBounds: new RenderBounds({color: "#0a0", layer: 0}), }); // triangles @@ -125,12 +181,12 @@ class LoopTest { Angle: angle, VAngle: -Math.PI/10 }), - bounds: new Polygon([70, 0, 55, 40, 85, 40]), - collisionSourceClass: new CollisionClass("tri"), - renderBounds: new RenderBounds( - "#d40", - 0 - ) + bounds: new PolygonComponent({points: [70, 0, 55, 40, 85, 40]}), + collisionSourceClass: new CollisionClass({ name: "tri"}), + renderBounds: new RenderBounds({ + color: "#d40", + layer: 0, + }) })); const loop = new Loop(30, diff --git a/src/Game/Death.ts b/src/Game/Death.ts index e4348c6..cdb2fbe 100644 --- a/src/Game/Death.ts +++ b/src/Game/Death.ts @@ -1,5 +1,5 @@ import { PlaySfx } from "../Applet/Audio"; -import { Location, Polygon, RenderBounds } from "../Ecs/Components"; +import { Location, Polygon, PolygonComponent, RenderBounds } from "../Ecs/Components"; import { Create, Id, Join, Lookup, Remove } from "../Ecs/Data"; import { Data, Lifetime, Teams, World } from "./GameComponents"; @@ -60,13 +60,20 @@ function SpawnPuff(data: Data, world: World, x: number, y: number, size: number, VX: (Math.random() + 0.5) * 400 * Math.cos(angle), VY: (Math.random() + 0.5) * 400 * -Math.sin(angle) }), - bounds: new Polygon([ + bounds: new PolygonComponent({points: [ -size, -size, -size, size, size, size, size, -size - ]), - renderBounds: new RenderBounds(color, /*world.smokeLayer*/ 0), - lifetime: new Lifetime(Math.random() / 3) + ]}), + renderBounds: new RenderBounds({ + color, + // TODO: work out standard layers + layer: 1 + }), + // TODO: randomization breaks determinism. Might be safe for a smoke puff that doesn't effect gameplay, but bad hygeine + lifetime: new Lifetime({ + time: Math.random() / 3 + }) }); } diff --git a/src/Game/GameComponents.ts b/src/Game/GameComponents.ts index f73fb01..b6460ad 100644 --- a/src/Game/GameComponents.ts +++ b/src/Game/GameComponents.ts @@ -1,7 +1,7 @@ import { KeyName } from "../Applet/Keyboard"; import { DrawSet, Layer } from "../Applet/Render"; -import { Data as EcsData } from "../Ecs/Components"; -import { copy, Join, SparseStore } from "../Ecs/Data"; +import { ComponentSchema, Data as EcsData } from "../Ecs/Components"; +import { Component, copySparse, Join, StateForSchema, Store } from "../Ecs/Data"; import { DumbMotion } from "../Ecs/Location"; import { INPUT_FREQUENCY, LockstepProcessor } from "../Ecs/Lockstep"; import { Buttons } from "./Input"; @@ -57,20 +57,35 @@ export class World { } } -export class Data extends EcsData { - boss: SparseStore = {}; - bullet: SparseStore = {}; - hp: SparseStore = {}; - lifetime: SparseStore = {}; - message: SparseStore = {}; +interface GameSchema extends ComponentSchema { + boss: Boss; + bullet: Bullet; + hp: Hp; + lifetime: Lifetime; + message: Message; +} - constructor(source?: Partial) { - super(source); - if(source?.boss) this.boss = copy(source.boss); - if(source?.bullet) this.bullet = copy(source.bullet); - if(source?.hp) this.hp = copy(source.hp); - if(source?.lifetime) this.lifetime = copy(source.lifetime); - if(source?.message) this.message = copy(source.message); +export class Data extends EcsData implements StateForSchema { + boss: Store; + bullet: Store; + hp: Store; + lifetime: Store; + message: Store; + + // globals + debugLayer = new Layer(2); + + constructor(from: Partial) { + super(from); + this.boss = copySparse(from.boss); + this.bullet = copySparse(from.bullet); + this.hp = copySparse(from.hp); + this.lifetime = copySparse(from.lifetime); + this.message = copySparse(from.message); + } + + clone() { + return new Data(this); } } @@ -78,41 +93,74 @@ export enum Teams { PLAYER, ENEMY } -export class Bullet { - hit = false; - constructor( - public team: Teams, - public attack: number - ) {}; +export class Bullet extends Component { + hit: boolean; + team: Teams; + attack: number; + constructor(from: Partial) { + super(from); + this.hit = from.hit ?? false; + this.team = from.team ?? Teams.ENEMY; + this.attack = from.attack ?? 1; + } + clone(): Bullet { + return new Bullet(this); + } } -export class Hp { - receivedDamage = 0; - constructor( - public team: Teams, - public hp: number - ) {}; +export class Hp extends Component { + receivedDamage: number; + team: Teams; + hp: number; + constructor(from: Partial) { + super(from); + this.receivedDamage = from.receivedDamage ?? 0; + this.team = from.team ?? Teams.ENEMY; + this.hp = from.hp ?? 10; + } + clone(): Hp { + return new Hp(this); + } } -export class Lifetime { - constructor( - public time: number - ) {}; +export class Lifetime extends Component { + time: number; + constructor(from: Partial & {time: number}) { + super(from); + this.time = from.time; + } + clone(): Lifetime { + return new Lifetime(this); + } } -export class Boss { - constructor( - public name: string - ) {} +export class Boss extends Component { + name: string; + constructor(from: Partial) { + super(from); + this.name = from.name ?? ""; + } + clone(): Boss { + return new Boss(this); + } } -export class Message { +export class Message extends Component { targetY = 0; - constructor( - public layer: Layer, - public color: string, - public message: string, - public timeout = 3 - ) {} + layer: number; + color: string; + message: string; + timeout = 3; + constructor(from: Partial) { + super(from); + this.targetY = from.targetY ?? 0; + this.layer = from.layer ?? 1; + this.color = from.color ?? "#000"; + this.message = from.message ?? ""; + this.timeout = from.timeout ?? 3; + } + clone(): Message { + return new Message(this); + } } export class Engine implements LockstepProcessor { diff --git a/src/Game/Main.ts b/src/Game/Main.ts index 49c59d2..7e477af 100644 --- a/src/Game/Main.ts +++ b/src/Game/Main.ts @@ -3,7 +3,7 @@ import subscribe from "callbag-subscribe"; import { KeyControl, KeyName } from "../Applet/Keyboard"; import { DrawSet } from "../Applet/Render"; -import { Location, Polygon, RenderBounds } from "../Ecs/Components"; +import { Location, Polygon, PolygonComponent, RenderBounds } from "../Ecs/Components"; import { Create } from "../Ecs/Data"; import { RunRenderBounds } from "../Ecs/Renderers"; import { LockstepClient } from "../Net/LockstepClient"; @@ -40,15 +40,18 @@ export class Main extends LockstepClient { } initState(patch: Partial) { - const newState = new Data(); + const newState = new Data(patch); Create(newState, { location: new Location({ X: 200, Y: 200, }), - bounds: new Polygon([-30, 0, 30, 0, 0, 40]), - renderBounds: new RenderBounds("#a0f", 0), + bounds: new PolygonComponent({points: [-30, 0, 30, 0, 0, 40]}), + renderBounds: new RenderBounds({ + color: "#a0f", + layer: 0 + }), }); return newState; diff --git a/src/Game/Message.ts b/src/Game/Message.ts index 00e6307..8f26abd 100644 --- a/src/Game/Message.ts +++ b/src/Game/Message.ts @@ -1,7 +1,7 @@ -import { Join, Remove } from "../Ecs/Data"; -import { Data, World, GamePhase } from "./GameComponents"; import { DrawSet } from "../Applet/Render"; +import { Join, Remove } from "../Ecs/Data"; import { TransformCx } from "../Ecs/Location"; +import { Data, GamePhase, World } from "./GameComponents"; /*export function SpawnMessage(color: string, text: string) { return function(data: Data, world: World, x: number, timeoutDelta = 0): Id { @@ -57,7 +57,7 @@ export function ReapMessages(data: Data, {width, height, debug}: World) { export function RenderMessages(data: Data, drawSet: DrawSet) { drawSet.queue(...Join(data, "message", "location").map( - ([{layer, color, message}, location]) => layer.toRender((cx, dt) => { + ([{layer, color, message}, location]) => data.layers[layer].toRender((cx, dt) => { TransformCx(cx, location, dt); cx.font = `${FONT_SIZE}px monospace`; cx.fillStyle = color;