Skip to content

Commit

Permalink
feat: added button
Browse files Browse the repository at this point in the history
  • Loading branch information
tsukinoko-kun committed Jul 23, 2024
1 parent 12c9e2f commit 6e723e8
Show file tree
Hide file tree
Showing 9 changed files with 200 additions and 56 deletions.
81 changes: 70 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
```
1 change: 1 addition & 0 deletions lib/builtin/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./uiButton"
export * from "./uiNode"
export * from "./uiStyle"
export * from "./uiText"
1 change: 1 addition & 0 deletions lib/builtin/components/uiButton.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export class UiButton {}
2 changes: 1 addition & 1 deletion lib/builtin/plugins/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
15 changes: 10 additions & 5 deletions lib/builtin/systems/html.ts
Original file line number Diff line number Diff line change
@@ -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"

Expand All @@ -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)
}

Expand Down
2 changes: 2 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
69 changes: 51 additions & 18 deletions lib/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,35 @@ import type { Component } from "./component"
import { useWorld } from "./world"
import { type Ident, identify } from "./identify"

export type QueryRule = (components: IterableIterator<Ident>) => boolean
export type QueryRule = (components: Array<Ident>) => boolean

export function queryAnd(...components: Component[]): QueryRule {
type Q = (<T extends any[]>(
types: { [K in keyof T]: new (...arg: any[]) => T[K] },
...filter: QueryRule[]
) => Generator<T>) & {
/**
* 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: <T extends any[]>(
types: { [K in keyof T]: new (...arg: any[]) => T[K] },
...filter: QueryRule[]
) => Generator<T>
}

function queryAnd(...components: Component[]): QueryRule {
const andComponentIdents = components.map(identify)

return (test: IterableIterator<Ident>) => {
for (const tc of test) {
if (!andComponentIdents.includes(tc)) {
return (test: Array<Ident>) => {
for (const ident of andComponentIdents) {
if (!test.includes(ident)) {
return false
}
}
Expand All @@ -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<T extends any[]>(
return (test: Array<Ident>) => {
for (const ident of notComponentIdents) {
if (test.includes(ident)) {
return false
}
}

return true
}
}

function* normalQuery<T extends any[]>(
types: { [K in keyof T]: new (...arg: any[]) => T[K] },
...filter: QueryRule[]
): Generator<T> {
Expand Down Expand Up @@ -55,8 +85,7 @@ export function* query<T extends any[]>(

// apply filters
for (const f of filter) {
if (!f(c.keys())) {
console.log("filter failed")
if (!f(Array.from(c.keys()))) {
continue entity_loop
}
}
Expand All @@ -81,10 +110,7 @@ export function* query<T extends any[]>(
}
}

/**
* Same as query, but only queries the root entities (entities without parents)
*/
export function* queryRoot<T extends any[]>(
function* queryRoot<T extends any[]>(
types: { [K in keyof T]: new (...arg: any[]) => T[K] },
...filter: QueryRule[]
): Generator<T> {
Expand Down Expand Up @@ -117,8 +143,7 @@ export function* queryRoot<T extends any[]>(

// apply filters
for (const f of filter) {
if (!f(c.keys())) {
console.log("filter failed")
if (!f(Array.from(c.keys()))) {
continue entity_loop
}
}
Expand All @@ -142,3 +167,11 @@ export function* queryRoot<T extends any[]>(
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
61 changes: 61 additions & 0 deletions test/src/counter.ts
Original file line number Diff line number Diff line change
@@ -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)
}
24 changes: 3 additions & 21 deletions test/src/index.ts
Original file line number Diff line number Diff line change
@@ -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()

0 comments on commit 6e723e8

Please sign in to comment.