Make Join work with any # of arguments, and make "id" a pseudo component instead of always returned
This commit is contained in:
parent
8d83917286
commit
50b4313100
7 changed files with 49 additions and 95 deletions
|
@ -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) {
|
export function FindCollisions(data: Data, cellSize: number, callback: (className: string, sourceId: Id, targetId: Id) => void) {
|
||||||
const spatialMap: Record<string, Candidate[]> = {};
|
const spatialMap: Record<string, Candidate[]> = {};
|
||||||
|
|
||||||
Join(data, "collisionTargetClass", "location", "bounds").map(
|
Join(data, "collisionTargetClass", "location", "bounds", "id").map(
|
||||||
([id, targetClass, location, poly]) => {
|
([targetClass, location, poly, id]) => {
|
||||||
const workingPoly = TfPolygon(poly, location);
|
const workingPoly = TfPolygon(poly, location);
|
||||||
const candidate = {
|
const candidate = {
|
||||||
id, class: targetClass, poly: workingPoly
|
id, class: targetClass, poly: workingPoly
|
||||||
|
@ -95,8 +95,8 @@ export function FindCollisions(data: Data, cellSize: number, callback: (classNam
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
Join(data, "collisionSourceClass", "location", "bounds").map(
|
Join(data, "collisionSourceClass", "location", "bounds", "id").map(
|
||||||
([id, sourceClass, location, poly]) => {
|
([sourceClass, location, poly, id]) => {
|
||||||
const workingPoly = TfPolygon(poly, location);
|
const workingPoly = TfPolygon(poly, location);
|
||||||
const candidates: Record<string, Candidate> = {};
|
const candidates: Record<string, Candidate> = {};
|
||||||
hashPolygon(workingPoly, cellSize).forEach(key => {
|
hashPolygon(workingPoly, cellSize).forEach(key => {
|
||||||
|
|
100
src/Ecs/Data.ts
100
src/Ecs/Data.ts
|
@ -27,14 +27,15 @@ type StoreKeysOf<DATA> = {
|
||||||
};
|
};
|
||||||
type StoreKeys<DATA extends Data> = StoreKeysOf<DATA>[keyof DATA];
|
type StoreKeys<DATA extends Data> = StoreKeysOf<DATA>[keyof DATA];
|
||||||
type ItemType<S> = S extends Record<number, infer T & HasGeneration> ? T : never;
|
type ItemType<S> = S extends Record<number, infer T & HasGeneration> ? T : never;
|
||||||
type StoreType<DATA extends Data, K> = K extends keyof DATA ? ItemType<DATA[K]> : never;
|
type StoreType<DATA extends Data, K> = K extends "id" ? Id : K extends keyof DATA ? ItemType<DATA[K]> : never;
|
||||||
type StoreTypes<DATA extends Data, K> = {
|
type StoreTypes<DATA extends Data, K extends (keyof DATA | "id")[]> = {
|
||||||
[I in keyof K]: StoreType<DATA, K[I]>;
|
[I in keyof K]: StoreType<DATA, K[I]>;
|
||||||
};
|
};
|
||||||
type MaybeStoreTypes<DATA extends Data, K> = {
|
type MaybeStoreTypes<DATA extends Data, K> = {
|
||||||
[I in keyof K]: StoreType<DATA, K[I]> | null;
|
[I in keyof K]: StoreType<DATA, K[I]> | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an entity in the store
|
* Create an entity in the store
|
||||||
* @param data store
|
* @param data store
|
||||||
|
@ -114,75 +115,23 @@ export function Lookup<DATA extends Data, K extends StoreKeys<DATA>[]>(data: DAT
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ergonomic Join typings
|
|
||||||
export function Join<
|
|
||||||
DATA extends Data,
|
|
||||||
A extends StoreKeys<DATA>,
|
|
||||||
> (
|
|
||||||
data: DATA,
|
|
||||||
a: A,
|
|
||||||
): [
|
|
||||||
Id,
|
|
||||||
StoreType<DATA, A> & {}
|
|
||||||
][];
|
|
||||||
export function Join<
|
|
||||||
DATA extends Data,
|
|
||||||
A extends StoreKeys<DATA>,
|
|
||||||
B extends StoreKeys<DATA>,
|
|
||||||
> (
|
|
||||||
data: DATA,
|
|
||||||
a: A,
|
|
||||||
b: B,
|
|
||||||
): [
|
|
||||||
Id,
|
|
||||||
StoreType<DATA, A> & {},
|
|
||||||
StoreType<DATA, B> & {},
|
|
||||||
][];
|
|
||||||
export function Join<
|
|
||||||
DATA extends Data,
|
|
||||||
A extends StoreKeys<DATA>,
|
|
||||||
B extends StoreKeys<DATA>,
|
|
||||||
C extends StoreKeys<DATA>,
|
|
||||||
> (
|
|
||||||
data: DATA,
|
|
||||||
a: A,
|
|
||||||
b: B,
|
|
||||||
c: C,
|
|
||||||
): [
|
|
||||||
Id,
|
|
||||||
StoreType<DATA, A> & {},
|
|
||||||
StoreType<DATA, B> & {},
|
|
||||||
StoreType<DATA, C> & {},
|
|
||||||
][];
|
|
||||||
export function Join<
|
|
||||||
DATA extends Data,
|
|
||||||
A extends StoreKeys<DATA>,
|
|
||||||
B extends StoreKeys<DATA>,
|
|
||||||
C extends StoreKeys<DATA>,
|
|
||||||
D extends StoreKeys<DATA>,
|
|
||||||
> (
|
|
||||||
data: DATA,
|
|
||||||
a: A,
|
|
||||||
b: B,
|
|
||||||
c: C,
|
|
||||||
d: D,
|
|
||||||
): [
|
|
||||||
Id,
|
|
||||||
StoreType<DATA, A> & {},
|
|
||||||
StoreType<DATA, B> & {},
|
|
||||||
StoreType<DATA, C> & {},
|
|
||||||
StoreType<DATA, D> & {},
|
|
||||||
][];
|
|
||||||
/**
|
/**
|
||||||
* Query a Data collection for all Alive entities possessing the named set of Components.
|
* 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 extends Data, K extends StoreKeys<DATA>[]>(data: DATA, ...components: K): [Id, ...{}[]][] {
|
export function Join<DATA extends Data, K extends (StoreKeys<DATA> | "id")[]>(data: DATA, ...components: K): StoreTypes<DATA, K>[] {
|
||||||
const entities = data.entity;
|
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 results: StoreTypes<DATA, K>[] = [];
|
||||||
const firstStore = stores[0];
|
const firstStore = stores.filter(store => store !== "id")[0] as Store<{}>|SparseStore<{}>;
|
||||||
if(Array.isArray(firstStore)) {
|
if(Array.isArray(firstStore)) {
|
||||||
for(let id = 0; id < firstStore.length; id++) {
|
for(let id = 0; id < firstStore.length; id++) {
|
||||||
JoinLoop(id, entities, stores, results);
|
JoinLoop(id, entities, stores, results);
|
||||||
|
@ -194,13 +143,18 @@ export function Join<DATA extends Data, K extends StoreKeys<DATA>[]>(data: DATA,
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
function JoinLoop(id: number, entities: Store<EntityState>, stores: (Store<{}>|SparseStore<{}>)[], results: [Id, ...{}[]][]) {
|
function JoinLoop<DATA extends Data, K extends (StoreKeys<DATA> | "id")[]>(id: number, entities: Store<EntityState>, stores: (Store<{}> | SparseStore<{}> | "id")[], results: StoreTypes<DATA, K>[]) {
|
||||||
const result: [Id, ...{}[]] = [[id, -1]];
|
const fullId: Id = [id, -1];
|
||||||
|
const result: (HasGeneration | Id)[] = [];
|
||||||
|
|
||||||
let generation = -1;
|
let generation = -1;
|
||||||
for (const store of stores) {
|
for (const store of stores) {
|
||||||
|
if(store === "id") {
|
||||||
|
result.push(fullId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const component = store[id];
|
const component = store[id];
|
||||||
if(component && (component.generation == generation || generation == -1)) {
|
if (component && (component.generation == generation || generation == -1)) {
|
||||||
generation = component.generation;
|
generation = component.generation;
|
||||||
result.push(component);
|
result.push(component);
|
||||||
} else {
|
} else {
|
||||||
|
@ -208,12 +162,12 @@ function JoinLoop(id: number, entities: Store<EntityState>, 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];
|
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
|
// backpatch generation now that it's known
|
||||||
result[0][1] = generation;
|
fullId[1] = generation;
|
||||||
|
|
||||||
results.push(result);
|
results.push(result as StoreTypes<DATA, K>);
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ export function TfPolygon({points}: Polygon, {X, Y, Angle}: Location): Polygon {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DumbMotion(data: Data, interval: number) {
|
export function DumbMotion(data: Data, interval: number) {
|
||||||
Join(data, "location").forEach(([id, location]) => {
|
Join(data, "location").forEach(([location]) => {
|
||||||
location.X += location.VX * interval;
|
location.X += location.VX * interval;
|
||||||
location.Y += location.VY * interval;
|
location.Y += location.VY * interval;
|
||||||
location.Angle += location.VAngle * interval;
|
location.Angle += location.VAngle * interval;
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { DrawSet, Layer } from "../Applet/Render";
|
||||||
|
|
||||||
export function RunRenderBounds(data: Data, drawSet: DrawSet) {
|
export function RunRenderBounds(data: Data, drawSet: DrawSet) {
|
||||||
drawSet.queue(...Join(data, "renderBounds", "location", "bounds").map(
|
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);
|
TransformCx(cx, location, dt);
|
||||||
cx.fillStyle = color;
|
cx.fillStyle = color;
|
||||||
cx.beginPath();
|
cx.beginPath();
|
||||||
|
@ -19,7 +19,7 @@ export function RunRenderBounds(data: Data, drawSet: DrawSet) {
|
||||||
|
|
||||||
export function RunRenderSprites(data: Data, drawSet: DrawSet) {
|
export function RunRenderSprites(data: Data, drawSet: DrawSet) {
|
||||||
drawSet.queue(...Join(data, "renderSprite", "location").map(
|
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);
|
TransformCx(cx, location, dt);
|
||||||
sheet.render(cx, index, offsetX, offsetY);
|
sheet.render(cx, index, offsetX, offsetY);
|
||||||
}))
|
}))
|
||||||
|
|
|
@ -62,7 +62,7 @@ export class EcsJoinTest {
|
||||||
export class EcsLookupTest {
|
export class EcsLookupTest {
|
||||||
constructor(pre: HTMLElement) {
|
constructor(pre: HTMLElement) {
|
||||||
const data = new TestData();
|
const data = new TestData();
|
||||||
const applesMaybeCarrots = Join(data, "apple").map(([id, apple]) => ({
|
const applesMaybeCarrots = Join(data, "apple", "id").map(([apple, id]) => ({
|
||||||
apple,
|
apple,
|
||||||
maybeCarrot: Lookup(data, id, "carrot")[0]
|
maybeCarrot: Lookup(data, id, "carrot")[0]
|
||||||
}));
|
}));
|
||||||
|
@ -74,9 +74,9 @@ export class EcsLookupTest {
|
||||||
export class EcsRemoveTest {
|
export class EcsRemoveTest {
|
||||||
constructor(pre: HTMLElement) {
|
constructor(pre: HTMLElement) {
|
||||||
const data = new TestData();
|
const data = new TestData();
|
||||||
const beforeDelete = Join(data, "apple", "carrot");
|
const beforeDelete = Join(data, "apple", "carrot", "id",);
|
||||||
Remove(data, [0, 5]);
|
Remove(data, [0, 5]);
|
||||||
const afterDelete = Join(data, "apple", "carrot");
|
const afterDelete = Join(data, "apple", "carrot", "id");
|
||||||
pre.innerText = JSON.stringify({
|
pre.innerText = JSON.stringify({
|
||||||
beforeDelete,
|
beforeDelete,
|
||||||
afterDelete
|
afterDelete
|
||||||
|
@ -88,13 +88,13 @@ export class EcsRemoveTest {
|
||||||
export class EcsCreateTest {
|
export class EcsCreateTest {
|
||||||
constructor(pre: HTMLElement) {
|
constructor(pre: HTMLElement) {
|
||||||
const data = new TestData();
|
const data = new TestData();
|
||||||
const beforeCreate = Join(data, "apple", "banana", "carrot");
|
const beforeCreate = Join(data, "apple", "banana", "carrot", "id");
|
||||||
const createdId = Create(data, {
|
const createdId = Create(data, {
|
||||||
apple: {},
|
apple: {},
|
||||||
banana: {peeled: false},
|
banana: {peeled: false},
|
||||||
carrot: {cronch: 11}
|
carrot: {cronch: 11}
|
||||||
});
|
});
|
||||||
const afterCreate = Join(data, "apple", "banana", "carrot");
|
const afterCreate = Join(data, "apple", "banana", "carrot", "id");
|
||||||
pre.innerText = JSON.stringify({
|
pre.innerText = JSON.stringify({
|
||||||
beforeCreate,
|
beforeCreate,
|
||||||
afterCreate,
|
afterCreate,
|
||||||
|
|
|
@ -5,18 +5,18 @@ import { Data, World, Lifetime, Teams } from "./GameComponents";
|
||||||
|
|
||||||
export function SelfDestructMinions(data: Data, world: World) {
|
export function SelfDestructMinions(data: Data, world: World) {
|
||||||
const bossKilled = Join(data, "boss", "hp")
|
const bossKilled = Join(data, "boss", "hp")
|
||||||
.filter(([id, boss, {hp}]) => hp < 0)
|
.filter(([_boss, {hp}]) => hp < 0)
|
||||||
.length > 0;
|
.length > 0;
|
||||||
|
|
||||||
if(bossKilled) {
|
if(bossKilled) {
|
||||||
Join(data, "hp")
|
Join(data, "hp")
|
||||||
.filter(([id, hp]) => hp.team == Teams.ENEMY)
|
.filter(([hp]) => hp.team == Teams.ENEMY)
|
||||||
.forEach(([id, hp]) => hp.hp = 0);
|
.forEach(([hp]) => hp.hp = 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CheckHp(data: Data, world: World) {
|
export function CheckHp(data: Data, world: World) {
|
||||||
Join(data, "hp").forEach(([id, hp]) => {
|
Join(data, "hp", "id").forEach(([hp, id]) => {
|
||||||
if(hp.hp <= 0) {
|
if(hp.hp <= 0) {
|
||||||
// remove from game
|
// remove from game
|
||||||
Remove(data, id);
|
Remove(data, id);
|
||||||
|
@ -26,7 +26,7 @@ export function CheckHp(data: Data, world: World) {
|
||||||
|
|
||||||
export function CheckLifetime(data: Data, world: World, interval: number) {
|
export function CheckLifetime(data: Data, world: World, interval: number) {
|
||||||
let particles = 0;
|
let particles = 0;
|
||||||
Join(data, "lifetime").forEach(([id, lifetime]) => {
|
Join(data, "lifetime", "id").forEach(([lifetime, id]) => {
|
||||||
lifetime.time -= interval;
|
lifetime.time -= interval;
|
||||||
if(lifetime.time <= 0) {
|
if(lifetime.time <= 0) {
|
||||||
// remove from game
|
// remove from game
|
||||||
|
@ -37,7 +37,7 @@ export function CheckLifetime(data: Data, world: World, interval: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SmokeDamage(data: Data, world: World) {
|
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
|
// convert dealt damage to particles
|
||||||
const puffs = Math.floor(hp.receivedDamage / 3);
|
const puffs = Math.floor(hp.receivedDamage / 3);
|
||||||
SpawnBlast(data, world, X, Y, 2, "#000", puffs);
|
SpawnBlast(data, world, X, Y, 2, "#000", puffs);
|
||||||
|
|
|
@ -20,10 +20,10 @@ const FONT_SIZE = 16;
|
||||||
const ADVANCE = 20;
|
const ADVANCE = 20;
|
||||||
export function ArrangeMessages(data: Data, world: World, interval: number) {
|
export function ArrangeMessages(data: Data, world: World, interval: number) {
|
||||||
const messages = Join(data, "message", "location");
|
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;
|
let y = world.height / 3;
|
||||||
messages.forEach(([id, message, location]) => {
|
messages.forEach(([message, location]) => {
|
||||||
message.targetY = y;
|
message.targetY = y;
|
||||||
y += ADVANCE;
|
y += ADVANCE;
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ export function ArrangeMessages(data: Data, world: World, interval: number) {
|
||||||
|
|
||||||
export function ReapMessages(data: Data, {width, height, debug}: World) {
|
export function ReapMessages(data: Data, {width, height, debug}: World) {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
Join(data, "message", "location").forEach(([id, message, {X, Y}]) => {
|
Join(data, "message", "location", "id").forEach(([_message, {X}, id]) => {
|
||||||
count++;
|
count++;
|
||||||
if(X > width * 2) {
|
if(X > width * 2) {
|
||||||
Remove(data, id);
|
Remove(data, id);
|
||||||
|
@ -57,7 +57,7 @@ export function ReapMessages(data: Data, {width, height, debug}: World) {
|
||||||
|
|
||||||
export function RenderMessages(data: Data, drawSet: DrawSet) {
|
export function RenderMessages(data: Data, drawSet: DrawSet) {
|
||||||
drawSet.queue(...Join(data, "message", "location").map(
|
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);
|
TransformCx(cx, location, dt);
|
||||||
cx.font = `${FONT_SIZE}px monospace`;
|
cx.font = `${FONT_SIZE}px monospace`;
|
||||||
cx.fillStyle = color;
|
cx.fillStyle = color;
|
||||||
|
|
Loading…
Reference in a new issue