Skip to content

Commit

Permalink
feat!: added state
Browse files Browse the repository at this point in the history
BREAKING CHANGE: split schedule in dynamic (new) and static
  • Loading branch information
tsukinoko-kun committed Jul 26, 2024
1 parent 8a72c1c commit 3362755
Show file tree
Hide file tree
Showing 20 changed files with 406 additions and 57 deletions.
23 changes: 18 additions & 5 deletions apps/demo/src/counter.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import {
type App,
Commands,
Entity,
inState,
OnEnter,
OnExit,
query,
res,
Schedule,
UiAnchor,
UiButton,
UiInteraction,
UiNode,
UiStyle,
UiText,
Update,
} from "@tsukinoko-kun/ecs.ts"
import { Location } from "../../../lib/builtin/state"

// this resource is used to store the counter value
class Counter {
Expand Down Expand Up @@ -42,6 +47,7 @@ function spawnUi() {
new UiText("ECS.ts on GitHub"),
new UiStyle().set("display", "block"),
)
parent.spawn(new UiAnchor("./meep"), new UiText("Meep"))
parent.spawn(
new UiButton(),
new UiText("Click me!"),
Expand Down Expand Up @@ -74,10 +80,17 @@ function updateButtonText() {
}
}

function despawnUi() {
for (const [entity] of query.root([Entity], query.and(UiNode))) {
Commands.despawn(entity)
}
}

// this plugin bundles everything that is needed for this counter example to work
export function counterPlugin(app: App) {
export function CounterPlugin(app: App) {
Commands.insertResource(new Counter())
app.addSystem(Schedule.Startup, spawnUi)
.addSystem(Schedule.Update, incrementCounter)
.addSystem(Schedule.Update, updateButtonText)
app.addSystem(OnEnter(Location.fromPath("/")), spawnUi)
.addSystem(Update, incrementCounter.runIf(inState(Location.fromPath("/"))))
.addSystem(Update, updateButtonText.runIf(inState(Location.fromPath("/"))))
.addSystem(OnExit(Location.fromPath("/")), despawnUi)
}
8 changes: 5 additions & 3 deletions apps/demo/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { App, DefaultPlugin, HtmlPlugin } from "@tsukinoko-kun/ecs.ts"
import { counterPlugin } from "./counter"
import { App, DefaultPlugin, HtmlPlugin, RouterPlugin } from "@tsukinoko-kun/ecs.ts"
import { CounterPlugin } from "./counter"

const app = new App()

Expand All @@ -8,7 +8,9 @@ app
.addPlugin(DefaultPlugin)
// the HtmlPlugin is for rendering the UI to the DOM
.addPlugin(HtmlPlugin("#app"))
// the RouterPlugin is for working with the browser's location (URL)
.addPlugin(RouterPlugin.withBasePath("/ecs.ts/"))
// the counterPlugin is our custom plugin
.addPlugin(counterPlugin)
.addPlugin(CounterPlugin)

app.run()
42 changes: 33 additions & 9 deletions lib/app.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { Plugin } from "./plugin"
import { inWorld, setCurrentWorld, World } from "./world"
import type { System } from "./system"
import { Schedule } from "./schedule"
import { First, Last, PostStartup, PostUpdate, PreStartup, PreUpdate, type Schedule, Startup, Update } from "./schedule"
import { Time } from "./builtin"
import { Debug } from "./debug"
import type { State } from "./state"

export class App {
private readonly plugins = new Array<Plugin>()
Expand All @@ -26,6 +27,11 @@ export class App {
return this
}

public insertState(stateValue: State): this {
this.world.insertState(stateValue)
return this
}

public addSystem(schedule: Schedule, system: System): this {
this.world.addSystem(schedule, system)
return this
Expand All @@ -34,9 +40,18 @@ export class App {
public async run(): Promise<void> {
setCurrentWorld(this.world)

await Promise.all(this.world.getSystemsBySchedule(Schedule.PreStartup).map((system) => system()))
await Promise.all(this.world.getSystemsBySchedule(Schedule.Startup).map((system) => system()))
await Promise.all(this.world.getSystemsBySchedule(Schedule.PostStartup).map((system) => system()))
await Promise.all(this.world.getSystemsBySchedule(PreStartup).map((system) => system()))
await Promise.all(this.world.getSystemsBySchedule(Startup).map((system) => system()))
await Promise.all(this.world.getSystemsBySchedule(PostStartup).map((system) => system()))

const transitions = this.world.applyNextState()
if (transitions.enter.length) {
for (const [schedule, systems] of this.world.getDynamicSystems()) {
if (schedule([], transitions.enter)) {
await Promise.all(systems.map((system) => system()))
}
}
}

setCurrentWorld(null)

Expand All @@ -49,13 +64,22 @@ export class App {
time.elapsed = elapsed
}

await Promise.all(this.world.getSystemsBySchedule(Schedule.First).map((system) => system()))
await Promise.all(this.world.getSystemsBySchedule(First).map((system) => system()))

await Promise.all(this.world.getSystemsBySchedule(Schedule.PreUpdate).map((system) => system()))
await Promise.all(this.world.getSystemsBySchedule(Schedule.Update).map((system) => system()))
await Promise.all(this.world.getSystemsBySchedule(Schedule.PostUpdate).map((system) => system()))
await Promise.all(this.world.getSystemsBySchedule(PreUpdate).map((system) => system()))
await Promise.all(this.world.getSystemsBySchedule(Update).map((system) => system()))
await Promise.all(this.world.getSystemsBySchedule(PostUpdate).map((system) => system()))

await Promise.all(this.world.getSystemsBySchedule(Schedule.Last).map((system) => system()))
await Promise.all(this.world.getSystemsBySchedule(Last).map((system) => system()))

const transitions = this.world.applyNextState()
if (transitions.exit.length || transitions.enter.length) {
for (const [schedule, systems] of this.world.getDynamicSystems()) {
if (schedule(transitions.exit, transitions.enter)) {
await Promise.all(systems.map((system) => system()))
}
}
}

setCurrentWorld(null)
requestAnimationFrame(update)
Expand Down
6 changes: 3 additions & 3 deletions lib/builtin/plugins/default.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { Plugin } from "../../plugin"
import { LogicalButtonInput, PhysicalButtonInput } from "../resources"
import { Schedule } from "../../schedule"
import { Last } from "../../schedule"
import { resetLogicalButtonInput, resetPhysicalButtonInput } from "../systems/buttonInput"
import { Commands } from "../../commands"

export const DefaultPlugin: Plugin = (app) => {
Commands.insertResource(new LogicalButtonInput())
Commands.insertResource(new PhysicalButtonInput())
app.addSystem(Schedule.Last, resetLogicalButtonInput)
app.addSystem(Schedule.Last, resetPhysicalButtonInput)
app.addSystem(Last, resetLogicalButtonInput)
app.addSystem(Last, resetPhysicalButtonInput)
}
8 changes: 4 additions & 4 deletions lib/builtin/plugins/html.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Plugin } from "../../plugin"
import { HtmlRoot } from "../resources"
import { Schedule } from "../../schedule"
import { Last, Startup, Update } from "../../schedule"
import { cleanupHtmlInteraction, htmlInteraction, renderHtmlRoot } from "../systems"
import { Commands } from "../../commands"

Expand All @@ -9,8 +9,8 @@ export function HtmlPlugin(rootElement: Element): Plugin
export function HtmlPlugin(root: string | Element): Plugin {
return (app) => {
Commands.insertResource(new HtmlRoot(root))
app.addSystem(Schedule.Update, renderHtmlRoot)
app.addSystem(Schedule.Startup, htmlInteraction)
app.addSystem(Schedule.Last, cleanupHtmlInteraction)
app.addSystem(Update, renderHtmlRoot)
app.addSystem(Startup, htmlInteraction)
app.addSystem(Last, cleanupHtmlInteraction)
}
}
1 change: 1 addition & 0 deletions lib/builtin/plugins/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./default"
export * from "./html"
export * from "./router"
15 changes: 15 additions & 0 deletions lib/builtin/plugins/router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { App } from "../../app"
import { Location } from "../state"
import { updateLocationState } from "../systems/location"
import { Startup } from "../../schedule"
import { BasePath } from "../resources/basePath"

export function RouterPlugin(app: App) {
app.insertResource(new BasePath("/")).insertState(Location.windowLocation()).addSystem(Startup, updateLocationState)
}

RouterPlugin.withBasePath = (basePath: string) => (app: App) => {
app.insertResource(new BasePath(basePath))
.insertState(Location.windowLocation())
.addSystem(Startup, updateLocationState)
}
7 changes: 7 additions & 0 deletions lib/builtin/resources/basePath.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export class BasePath {
public readonly basePath: string

public constructor(basePath: string) {
this.basePath = basePath
}
}
28 changes: 25 additions & 3 deletions lib/builtin/state/location.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,36 @@
import type { Equals } from "../../traits"
import { res } from "../../resource"
import { BasePath } from "../resources/basePath"

function trimBasePath(basePath: string, path: string): string {
if (basePath === "/") {
return path
}
if (path.startsWith(basePath)) {
path = path.slice(basePath.length)
}
if (!path.startsWith("/")) {
path = "/" + path
}
return path
}

export class Location implements Equals {
private readonly path: string
public path: string

public constructor(path: string) {
private constructor(path: string) {
this.path = path
}

public static windowLocation(): Location {
return new Location(window.location.pathname)
const bp = res(BasePath).basePath
const pn = window.location.pathname
return new Location(trimBasePath(bp, pn))
}

public static fromPath(path: string): Location {
const bp = res(BasePath).basePath
return new Location(trimBasePath(bp, path))
}

public equals(other: this): boolean {
Expand Down
47 changes: 47 additions & 0 deletions lib/builtin/systems/location.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { nextState } from "../../state"
import { Location } from "../state"
import { inWorld, useWorld } from "../../world"

export function updateLocationState() {
const origin = window.location.origin
const world = useWorld()

window.addEventListener("popstate", (ev) => {
ev.preventDefault()
inWorld(world, () => {
nextState(Location.windowLocation())
})
})

window.addEventListener("click", (ev) => {
let ns: Location | null = null

for (const el of parentChain(ev.target as Element)) {
if (el.tagName === "A") {
const url = new URL((el as HTMLAnchorElement).href)
if (url.origin !== origin) {
return
}
ev.preventDefault()
history.pushState({}, "", (el as HTMLAnchorElement).href)
ns = new Location(url.pathname)
break
}
}

if (ns) {
inWorld(world, () => {
nextState(ns!)
})
}
})
}

function* parentChain(el: Element): IterableIterator<Element> {
yield el
let p = el.parentElement
while (p) {
yield p
p = p.parentElement
}
}
40 changes: 32 additions & 8 deletions lib/debug.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
import type { World } from "./world"
import type { Entity } from "./entity"
import { Schedule } from "./schedule"
import { First, Last, PostStartup, PostUpdate, PreStartup, PreUpdate, Startup, Update } from "./schedule"
import { identDisplay } from "./identify"

export const Debug = {
worlds: new Array<World>(),
}

function symbolDisplayName(symbol: symbol): string {
return symbol.toString().replace(/^Symbol\((.*)\)$/, "$1")
}

function debugEntity(w: World, e: Entity): object {
return {
id: e.id,
components: Array.from(w.getEntityComponents(e).keys()).map(symbolDisplayName),
components: Array.from(w.getEntityComponents(e).keys()).map(identDisplay),
children: e.children.map((child) => debugEntity(w, child)),
}
}
Expand All @@ -27,10 +24,37 @@ function systemName(system: Function): string {
const entities = world.getEntities().map((entity) => debugEntity(world, entity))
let systems = {}
for (const [schedule, systemList] of world.getSystems().entries()) {
const s = typeof schedule === "string" ? schedule : Schedule[schedule]
let s = schedule.toString()
switch (schedule) {
case PreStartup:
s = "PreStartup"
break
case Startup:
s = "Startup"
break
case PostStartup:
s = "PostStartup"
break
case First:
s = "First"
break
case PreUpdate:
s = "PreUpdate"
break
case Update:
s = "PostStartup"
break
case PostUpdate:
s = "PostUpdate"
break
case Last:
s = "Last"
break
}
// @ts-ignore
systems[s] = systemList.map((system) => systemName(system))
}
return { entities, systems }
const state = Array.from(world.getStates()).map((s) => JSON.parse(JSON.stringify(s)))
return { entities, systems, state }
}),
})
4 changes: 4 additions & 0 deletions lib/identify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ export type Ident = symbol

const __identifier__ = Symbol("__identifier__")

export function identDisplay(symbol: Ident): string {
return symbol.toString().replace(/^Symbol\((.*)\)$/, "$1")
}

export function identify<T extends Object>(o: T | (new (...arg: any[]) => T)): Ident {
if (__identifier__ in o) {
return o[__identifier__] as Ident
Expand Down
2 changes: 2 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ export * from "./plugin"
export * from "./query"
export * from "./resource"
export * from "./schedule"
export * from "./state"
export * from "./system"
export * from "./traits"
export * from "./world"

export * from "./builtin"
2 changes: 1 addition & 1 deletion lib/resource.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useWorld } from "./world"

export function res<T>(type: { new (...args: any[]): T }): T {
export function res<T extends Object>(type: { new (...args: any[]): T }): T {
const world = useWorld()
return world.getResource(type)
}
Loading

0 comments on commit 3362755

Please sign in to comment.