From 50b431310033c06bea9c73f518169124a00d6257 Mon Sep 17 00:00:00 2001 From: Tangent Wantwight Date: Thu, 16 Jan 2020 01:54:43 -0500 Subject: [PATCH] Make Join work with any # of arguments, and make "id" a pseudo component instead of always returned --- src/Ecs/Collision.ts | 8 ++-- src/Ecs/Data.ts | 100 ++++++++++++------------------------------- src/Ecs/Location.ts | 2 +- src/Ecs/Renderers.ts | 4 +- src/Ecs/test.ts | 10 ++--- src/Game/Death.ts | 12 +++--- src/Game/Message.ts | 8 ++-- 7 files changed, 49 insertions(+), 95 deletions(-) diff --git a/src/Ecs/Collision.ts b/src/Ecs/Collision.ts index 2ed346c..b673364 100644 --- a/src/Ecs/Collision.ts +++ b/src/Ecs/Collision.ts @@ -82,8 +82,8 @@ function collideConvexPolygons({points: aPoints}: Polygon, {points: bPoints}: Po export function FindCollisions(data: Data, cellSize: number, callback: (className: string, sourceId: Id, targetId: Id) => void) { const spatialMap: Record = {}; - Join(data, "collisionTargetClass", "location", "bounds").map( - ([id, targetClass, location, poly]) => { + Join(data, "collisionTargetClass", "location", "bounds", "id").map( + ([targetClass, location, poly, id]) => { const workingPoly = TfPolygon(poly, location); const candidate = { id, class: targetClass, poly: workingPoly @@ -95,8 +95,8 @@ export function FindCollisions(data: Data, cellSize: number, callback: (classNam } ); - Join(data, "collisionSourceClass", "location", "bounds").map( - ([id, sourceClass, location, poly]) => { + Join(data, "collisionSourceClass", "location", "bounds", "id").map( + ([sourceClass, location, poly, id]) => { const workingPoly = TfPolygon(poly, location); const candidates: Record = {}; hashPolygon(workingPoly, cellSize).forEach(key => { diff --git a/src/Ecs/Data.ts b/src/Ecs/Data.ts index eadc634..d076f46 100644 --- a/src/Ecs/Data.ts +++ b/src/Ecs/Data.ts @@ -27,14 +27,15 @@ type StoreKeysOf = { }; type StoreKeys = StoreKeysOf[keyof DATA]; type ItemType = S extends Record ? T : never; -type StoreType = K extends keyof DATA ? ItemType : never; -type StoreTypes = { +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; }; + /** * Create an entity in the store * @param data store @@ -114,75 +115,23 @@ export function Lookup[]>(data: DAT } } -// Ergonomic Join typings -export function Join< - DATA extends Data, - A extends StoreKeys, -> ( - data: DATA, - a: A, -): [ - Id, - StoreType & {} -][]; -export function Join< - DATA extends Data, - A extends StoreKeys, - B extends StoreKeys, -> ( - data: DATA, - a: A, - b: B, -): [ - Id, - StoreType & {}, - StoreType & {}, -][]; -export function Join< - DATA extends Data, - A extends StoreKeys, - B extends StoreKeys, - C extends StoreKeys, -> ( - data: DATA, - a: A, - b: B, - c: C, -): [ - Id, - StoreType & {}, - StoreType & {}, - StoreType & {}, -][]; -export function Join< - DATA extends Data, - A extends StoreKeys, - B extends StoreKeys, - C extends StoreKeys, - D extends StoreKeys, -> ( - data: DATA, - a: A, - b: B, - c: C, - d: D, -): [ - Id, - StoreType & {}, - StoreType & {}, - StoreType & {}, - StoreType & {}, -][]; /** * Query a Data collection for all Alive entities possessing the named set of Components. - * @returns an array of tuples containing the matching entity [ID, generation]s & associated Components + * "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[]>(data: DATA, ...components: K): [Id, ...{}[]][] { +export function Join | "id")[]>(data: DATA, ...components: K): StoreTypes[] { const entities = data.entity; - const stores = components.map(name => data[name] as unknown as (Store<{}>|SparseStore<{}>)); + const stores = components.map(name => { + if(name == "id") { + return "id"; + } else { + return data[name] as unknown as (Store<{}>|SparseStore<{}>) + }; + }); - const results: [Id, ...{}[]][] = []; - const firstStore = stores[0]; + 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++) { JoinLoop(id, entities, stores, results); @@ -194,13 +143,18 @@ export function Join[]>(data: DATA, } return results; } -function JoinLoop(id: number, entities: Store, stores: (Store<{}>|SparseStore<{}>)[], results: [Id, ...{}[]][]) { - const result: [Id, ...{}[]] = [[id, -1]]; +function JoinLoop | "id")[]>(id: number, entities: Store, stores: (Store<{}> | SparseStore<{}> | "id")[], results: StoreTypes[]) { + const fullId: Id = [id, -1]; + const result: (HasGeneration | Id)[] = []; let generation = -1; for (const store of stores) { + if(store === "id") { + result.push(fullId); + continue; + } const component = store[id]; - if(component && (component.generation == generation || generation == -1)) { + if (component && (component.generation == generation || generation == -1)) { generation = component.generation; result.push(component); } else { @@ -208,12 +162,12 @@ function JoinLoop(id: number, entities: Store, stores: (Store<{}>|S } } - // only accept active entities (do this check here) + // only accept active entities (do this check here, where the generation is known) const entity = entities[id]; - if(entity.alive != Liveness.ALIVE || generation != entity.generation) return; + if (entity.alive != Liveness.ALIVE || generation != entity.generation) return; // backpatch generation now that it's known - result[0][1] = generation; + fullId[1] = generation; - results.push(result); + results.push(result as StoreTypes); } diff --git a/src/Ecs/Location.ts b/src/Ecs/Location.ts index e7ed7ff..7e0ef06 100644 --- a/src/Ecs/Location.ts +++ b/src/Ecs/Location.ts @@ -20,7 +20,7 @@ export function TfPolygon({points}: Polygon, {X, Y, Angle}: Location): Polygon { } export function DumbMotion(data: Data, interval: number) { - Join(data, "location").forEach(([id, location]) => { + Join(data, "location").forEach(([location]) => { location.X += location.VX * interval; location.Y += location.VY * interval; location.Angle += location.VAngle * interval; diff --git a/src/Ecs/Renderers.ts b/src/Ecs/Renderers.ts index f42198f..4ffb333 100644 --- a/src/Ecs/Renderers.ts +++ b/src/Ecs/Renderers.ts @@ -5,7 +5,7 @@ import { DrawSet, Layer } from "../Applet/Render"; export function RunRenderBounds(data: Data, drawSet: DrawSet) { drawSet.queue(...Join(data, "renderBounds", "location", "bounds").map( - ([id, {color, layer}, location, {points}]) => layer.toRender((cx, dt) => { + ([{color, layer}, location, {points}]) => layer.toRender((cx, dt) => { TransformCx(cx, location, dt); cx.fillStyle = color; cx.beginPath(); @@ -19,7 +19,7 @@ export function RunRenderBounds(data: Data, drawSet: DrawSet) { export function RunRenderSprites(data: Data, drawSet: DrawSet) { drawSet.queue(...Join(data, "renderSprite", "location").map( - ([id, {sheet, layer, index, offsetX, offsetY}, location]) => layer.toRender((cx, dt) => { + ([{sheet, layer, index, offsetX, offsetY}, location]) => layer.toRender((cx, dt) => { TransformCx(cx, location, dt); sheet.render(cx, index, offsetX, offsetY); })) diff --git a/src/Ecs/test.ts b/src/Ecs/test.ts index 88960a9..da87993 100644 --- a/src/Ecs/test.ts +++ b/src/Ecs/test.ts @@ -62,7 +62,7 @@ export class EcsJoinTest { export class EcsLookupTest { constructor(pre: HTMLElement) { const data = new TestData(); - const applesMaybeCarrots = Join(data, "apple").map(([id, apple]) => ({ + const applesMaybeCarrots = Join(data, "apple", "id").map(([apple, id]) => ({ apple, maybeCarrot: Lookup(data, id, "carrot")[0] })); @@ -74,9 +74,9 @@ export class EcsLookupTest { export class EcsRemoveTest { constructor(pre: HTMLElement) { const data = new TestData(); - const beforeDelete = Join(data, "apple", "carrot"); + const beforeDelete = Join(data, "apple", "carrot", "id",); Remove(data, [0, 5]); - const afterDelete = Join(data, "apple", "carrot"); + const afterDelete = Join(data, "apple", "carrot", "id"); pre.innerText = JSON.stringify({ beforeDelete, afterDelete @@ -88,13 +88,13 @@ export class EcsRemoveTest { export class EcsCreateTest { constructor(pre: HTMLElement) { const data = new TestData(); - const beforeCreate = Join(data, "apple", "banana", "carrot"); + const beforeCreate = Join(data, "apple", "banana", "carrot", "id"); const createdId = Create(data, { apple: {}, banana: {peeled: false}, carrot: {cronch: 11} }); - const afterCreate = Join(data, "apple", "banana", "carrot"); + const afterCreate = Join(data, "apple", "banana", "carrot", "id"); pre.innerText = JSON.stringify({ beforeCreate, afterCreate, diff --git a/src/Game/Death.ts b/src/Game/Death.ts index 72fca06..b48b07c 100644 --- a/src/Game/Death.ts +++ b/src/Game/Death.ts @@ -5,18 +5,18 @@ import { Data, World, Lifetime, Teams } from "./GameComponents"; export function SelfDestructMinions(data: Data, world: World) { const bossKilled = Join(data, "boss", "hp") - .filter(([id, boss, {hp}]) => hp < 0) + .filter(([_boss, {hp}]) => hp < 0) .length > 0; if(bossKilled) { Join(data, "hp") - .filter(([id, hp]) => hp.team == Teams.ENEMY) - .forEach(([id, hp]) => hp.hp = 0); + .filter(([hp]) => hp.team == Teams.ENEMY) + .forEach(([hp]) => hp.hp = 0); } } export function CheckHp(data: Data, world: World) { - Join(data, "hp").forEach(([id, hp]) => { + Join(data, "hp", "id").forEach(([hp, id]) => { if(hp.hp <= 0) { // remove from game Remove(data, id); @@ -26,7 +26,7 @@ export function CheckHp(data: Data, world: World) { export function CheckLifetime(data: Data, world: World, interval: number) { let particles = 0; - Join(data, "lifetime").forEach(([id, lifetime]) => { + Join(data, "lifetime", "id").forEach(([lifetime, id]) => { lifetime.time -= interval; if(lifetime.time <= 0) { // remove from game @@ -37,7 +37,7 @@ export function CheckLifetime(data: Data, world: World, interval: number) { } export function SmokeDamage(data: Data, world: World) { - Join(data, "hp", "location").forEach(([id, hp, {X, Y}]) => { + Join(data, "hp", "location").forEach(([hp, {X, Y}]) => { // convert dealt damage to particles const puffs = Math.floor(hp.receivedDamage / 3); SpawnBlast(data, world, X, Y, 2, "#000", puffs); diff --git a/src/Game/Message.ts b/src/Game/Message.ts index 33114a7..00e6307 100644 --- a/src/Game/Message.ts +++ b/src/Game/Message.ts @@ -20,10 +20,10 @@ const FONT_SIZE = 16; const ADVANCE = 20; export function ArrangeMessages(data: Data, world: World, interval: number) { const messages = Join(data, "message", "location"); - messages.sort(([{}, {timeout: timeoutA}, {}], [{}, {timeout: timeoutB}, {}]) => timeoutA - timeoutB); + messages.sort(([{timeout: timeoutA}, {}], [{timeout: timeoutB}, {}]) => timeoutA - timeoutB); let y = world.height / 3; - messages.forEach(([id, message, location]) => { + messages.forEach(([message, location]) => { message.targetY = y; y += ADVANCE; @@ -47,7 +47,7 @@ export function ArrangeMessages(data: Data, world: World, interval: number) { export function ReapMessages(data: Data, {width, height, debug}: World) { let count = 0; - Join(data, "message", "location").forEach(([id, message, {X, Y}]) => { + Join(data, "message", "location", "id").forEach(([_message, {X}, id]) => { count++; if(X > width * 2) { Remove(data, 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( - ([id, {layer, color, message}, location]) => layer.toRender((cx, dt) => { + ([{layer, color, message}, location]) => layer.toRender((cx, dt) => { TransformCx(cx, location, dt); cx.font = `${FONT_SIZE}px monospace`; cx.fillStyle = color;