diff --git a/README.md b/README.md index 3e00333..8857f67 100644 --- a/README.md +++ b/README.md @@ -2,22 +2,81 @@ Strongly inspired by Bevy +## Example + ```ts -import { App, Commands, DefaultPlugin, HtmlPlugin, Schedule, UiNode, UiText } from "@tsukinoko-kun/ecs.ts" +// index.ts +import { App, DefaultPlugin, HtmlPlugin } from "@tsukinoko-kun/ecs.ts" +import { counterPlugin } from "./counter" const app = new App() -app.addPlugin(DefaultPlugin) - .addPlugin(HtmlPlugin("#app")) - .addPlugin((world) => { - world.addSystem(Schedule.Start, () => { - Commands.spawn(new UiNode()).withChildren((parent) => { - parent.spawn(new UiText("meep")) - }) +app.addPlugin(DefaultPlugin).addPlugin(HtmlPlugin("#app")).addPlugin(counterPlugin) + +app.run() +``` + +```ts +// counter.ts +import { + Commands, + LogicalButtonInput, + query, + res, + Schedule, + UiButton, + UiNode, + UiStyle, + UiText, + type World, +} from "@tsukinoko-kun/ecs.ts" + +class Counter { + public value = 0 +} + +class CounterMarker {} - Commands.spawn(new UiText("hello world")) - }) +function spawnUi() { + Commands.spawn( + new UiNode(), + new UiStyle() + .set("backgroundColor", "whitesmoke") + .set("border", "solid 1px gray") + .set("padding", "0.5rem 1rem") + .set("margin", "1rem 0"), + ).withChildren((parent) => { + parent.spawn(new UiText("a"), new UiStyle().set("color", "red")) + parent.spawn(new UiText("b"), new UiStyle().set("color", "blue")) + parent.spawn(new UiText("c"), new UiStyle().set("color", "green")) }) -app.run() + Commands.spawn(new CounterMarker(), new UiButton(), new UiText("Click me!")) +} + +function incrementCounter() { + const btn = res(LogicalButtonInput) + if (btn.pressed("b")) { + const counter = res(Counter) + counter.value++ + } +} + +function updateButtonText() { + const counter = res(Counter) + for (const [text] of query([UiText, CounterMarker])) { + if (counter.value === 0) { + text.value = "Press 'b' to increment the counter!" + } else { + text.value = `Counter: ${counter.value}\nPress 'b' to increment further!` + } + } +} + +export function counterPlugin(world: World) { + world.insertResource(new Counter()) + world.addSystem(Schedule.Start, spawnUi) + world.addSystem(Schedule.Update, incrementCounter) + world.addSystem(Schedule.Update, updateButtonText) +} ``` diff --git a/lib/builtin/components/index.ts b/lib/builtin/components/index.ts index c0e89c9..4c0287d 100644 --- a/lib/builtin/components/index.ts +++ b/lib/builtin/components/index.ts @@ -1,3 +1,4 @@ +export * from "./uiButton" export * from "./uiNode" export * from "./uiStyle" export * from "./uiText" diff --git a/lib/builtin/components/uiButton.ts b/lib/builtin/components/uiButton.ts new file mode 100644 index 0000000..70b6624 --- /dev/null +++ b/lib/builtin/components/uiButton.ts @@ -0,0 +1 @@ +export class UiButton {} diff --git a/lib/builtin/plugins/html.ts b/lib/builtin/plugins/html.ts index 1ed8a08..84e79fe 100644 --- a/lib/builtin/plugins/html.ts +++ b/lib/builtin/plugins/html.ts @@ -6,6 +6,6 @@ import { renderHtmlRoot } from "../systems" export function HtmlPlugin(rootSelector: string): Plugin { return (world) => { world.insertResource(new HtmlRoot(rootSelector)) - world.addSystem(Schedule.PostStart, renderHtmlRoot) + world.addSystem(Schedule.Update, renderHtmlRoot) } } diff --git a/lib/builtin/systems/html.ts b/lib/builtin/systems/html.ts index 14f4de9..96c54ed 100644 --- a/lib/builtin/systems/html.ts +++ b/lib/builtin/systems/html.ts @@ -1,7 +1,7 @@ import { res } from "../../resource" import { HtmlRoot } from "../resources" -import { queryRoot } from "../../query" -import { UiStyle, UiText } from "../components" +import { query } from "../../query" +import { UiButton, UiStyle, UiText } from "../components" import { Entity } from "../../entity" import { Commands } from "../../commands" @@ -11,23 +11,28 @@ export function renderHtmlRoot(): void { let el = document.createElement("div") function render(e: Entity, el: HTMLElement): void { - const entitiesHtml = document.createElement("div") - el.appendChild(entitiesHtml) + let entitiesHtml = document.createElement("div") as HTMLElement for (const c of Commands.components(e)) { if (c instanceof UiText) { entitiesHtml.innerText += c.value } else if (c instanceof UiStyle) { entitiesHtml.style.cssText = c.css + } else if (c instanceof UiButton) { + const innerHtml = entitiesHtml.innerHTML + entitiesHtml = document.createElement("button") + entitiesHtml.innerHTML = innerHtml } } + el.appendChild(entitiesHtml) + for (const child of e.children) { render(child, entitiesHtml) } } - for (const [e] of queryRoot([Entity])) { + for (const [e] of query.root([Entity])) { render(e, el) } diff --git a/lib/index.ts b/lib/index.ts index 34378ee..2593956 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -6,5 +6,7 @@ export * from "./plugin" export * from "./query" export * from "./resource" export * from "./schedule" +export * from "./system" +export * from "./world" export * from "./builtin" diff --git a/lib/query.ts b/lib/query.ts index ddcf98c..f6af5bd 100644 --- a/lib/query.ts +++ b/lib/query.ts @@ -2,14 +2,35 @@ import type { Component } from "./component" import { useWorld } from "./world" import { type Ident, identify } from "./identify" -export type QueryRule = (components: IterableIterator) => boolean +export type QueryRule = (components: Array) => boolean -export function queryAnd(...components: Component[]): QueryRule { +type Q = (( + types: { [K in keyof T]: new (...arg: any[]) => T[K] }, + ...filter: QueryRule[] +) => Generator) & { + /** + * Query filter function that requires more components to be present then on the query itself + */ + and: (...components: Component[]) => QueryRule + /** + * Query filter function that requires components to **not** be present + */ + not: (...components: Component[]) => QueryRule + /** + * Same as query, but only queries the root entities (entities without parents) + */ + root: ( + types: { [K in keyof T]: new (...arg: any[]) => T[K] }, + ...filter: QueryRule[] + ) => Generator +} + +function queryAnd(...components: Component[]): QueryRule { const andComponentIdents = components.map(identify) - return (test: IterableIterator) => { - for (const tc of test) { - if (!andComponentIdents.includes(tc)) { + return (test: Array) => { + for (const ident of andComponentIdents) { + if (!test.includes(ident)) { return false } } @@ -18,12 +39,21 @@ export function queryAnd(...components: Component[]): QueryRule { } } -// query([UiText], queryAnd(UiText, UiText)) +export function queryNot(...components: Component[]): QueryRule { + const notComponentIdents = components.map(identify) -/** - * Get all entities with the specified components (children are included) - */ -export function* query( + return (test: Array) => { + for (const ident of notComponentIdents) { + if (test.includes(ident)) { + return false + } + } + + return true + } +} + +function* normalQuery( types: { [K in keyof T]: new (...arg: any[]) => T[K] }, ...filter: QueryRule[] ): Generator { @@ -55,8 +85,7 @@ export function* query( // apply filters for (const f of filter) { - if (!f(c.keys())) { - console.log("filter failed") + if (!f(Array.from(c.keys()))) { continue entity_loop } } @@ -81,10 +110,7 @@ export function* query( } } -/** - * Same as query, but only queries the root entities (entities without parents) - */ -export function* queryRoot( +function* queryRoot( types: { [K in keyof T]: new (...arg: any[]) => T[K] }, ...filter: QueryRule[] ): Generator { @@ -117,8 +143,7 @@ export function* queryRoot( // apply filters for (const f of filter) { - if (!f(c.keys())) { - console.log("filter failed") + if (!f(Array.from(c.keys()))) { continue entity_loop } } @@ -142,3 +167,11 @@ export function* queryRoot( yield x } } + +/** + * Get all entities with the specified components (children are included) + */ +export const query = normalQuery as Q +query.and = queryAnd +query.not = queryNot +query.root = queryRoot diff --git a/test/src/counter.ts b/test/src/counter.ts new file mode 100644 index 0000000..2bba1b4 --- /dev/null +++ b/test/src/counter.ts @@ -0,0 +1,61 @@ +import { + Commands, + LogicalButtonInput, + query, + res, + Schedule, + UiButton, + UiNode, + UiStyle, + UiText, + type World, +} from "@tsukinoko-kun/ecs.ts" + +class Counter { + public value = 0 +} + +class CounterMarker {} + +function spawnUi() { + Commands.spawn( + new UiNode(), + new UiStyle() + .set("backgroundColor", "whitesmoke") + .set("border", "solid 1px gray") + .set("padding", "0.5rem 1rem") + .set("margin", "1rem 0"), + ).withChildren((parent) => { + parent.spawn(new UiText("a"), new UiStyle().set("color", "red")) + parent.spawn(new UiText("b"), new UiStyle().set("color", "blue")) + parent.spawn(new UiText("c"), new UiStyle().set("color", "green")) + }) + + Commands.spawn(new CounterMarker(), new UiButton(), new UiText("Click me!")) +} + +function incrementCounter() { + const btn = res(LogicalButtonInput) + if (btn.pressed("b")) { + const counter = res(Counter) + counter.value++ + } +} + +function updateButtonText() { + const counter = res(Counter) + for (const [text] of query([UiText], query.and(CounterMarker))) { + if (counter.value === 0) { + text.value = "Press 'b' to increment the counter!" + } else { + text.value = `Counter: ${counter.value}\nPress 'b' to increment further!` + } + } +} + +export function counterPlugin(world: World) { + world.insertResource(new Counter()) + world.addSystem(Schedule.Start, spawnUi) + world.addSystem(Schedule.Update, incrementCounter) + world.addSystem(Schedule.Update, updateButtonText) +} diff --git a/test/src/index.ts b/test/src/index.ts index 528d0dd..7dd5ad5 100644 --- a/test/src/index.ts +++ b/test/src/index.ts @@ -1,26 +1,8 @@ -import { App, Commands, DefaultPlugin, HtmlPlugin, Schedule, UiNode, UiStyle, UiText } from "@tsukinoko-kun/ecs.ts" +import { App, DefaultPlugin, HtmlPlugin } from "@tsukinoko-kun/ecs.ts" +import { counterPlugin } from "./counter" const app = new App() -app.addPlugin(DefaultPlugin) - .addPlugin(HtmlPlugin("#app")) - .addPlugin((world) => { - world.addSystem(Schedule.Start, () => { - Commands.spawn( - new UiNode(), - new UiStyle() - .set("backgroundColor", "whitesmoke") - .set("border", "solid 1px gray") - .set("padding", "0.5rem 1rem") - .set("margin", "1rem 0"), - ).withChildren((parent) => { - parent.spawn(new UiText("a"), new UiStyle().set("color", "red")) - parent.spawn(new UiText("b"), new UiStyle().set("color", "blue")) - parent.spawn(new UiText("c"), new UiStyle().set("color", "green")) - }) - - Commands.spawn(new UiText("hello world")) - }) - }) +app.addPlugin(DefaultPlugin).addPlugin(HtmlPlugin("#app")).addPlugin(counterPlugin) app.run()