@benev/archimedes 0.1.0-2 → 0.1.0-4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -10
- package/package.json +4 -4
- package/s/ecs/index.ts +6 -4
- package/s/ecs/parts/apply-change.ts +33 -0
- package/s/ecs/parts/change.ts +27 -0
- package/s/ecs/parts/entities.ts +98 -0
- package/s/ecs/parts/execute-systems.ts +18 -0
- package/s/ecs/parts/lifecycle.ts +30 -0
- package/s/ecs/parts/types.ts +25 -0
- package/s/ecs/test/setup-example.ts +49 -0
- package/s/ecs/test.ts +75 -48
- package/x/ecs/index.d.ts +6 -3
- package/x/ecs/index.js +6 -3
- package/x/ecs/index.js.map +1 -1
- package/x/ecs/parts/apply-change.d.ts +3 -0
- package/x/ecs/parts/apply-change.js +30 -0
- package/x/ecs/parts/apply-change.js.map +1 -0
- package/x/ecs/parts/change.d.ts +13 -0
- package/x/ecs/parts/change.js +15 -0
- package/x/ecs/parts/change.js.map +1 -0
- package/x/ecs/parts/entities.d.ts +11 -0
- package/x/ecs/parts/entities.js +70 -0
- package/x/ecs/parts/entities.js.map +1 -0
- package/x/ecs/parts/execute-systems.d.ts +3 -0
- package/x/ecs/parts/execute-systems.js +12 -0
- package/x/ecs/parts/execute-systems.js.map +1 -0
- package/x/ecs/parts/lifecycle.d.ts +3 -0
- package/x/ecs/parts/lifecycle.js +19 -0
- package/x/ecs/parts/lifecycle.js.map +1 -0
- package/x/ecs/parts/types.d.ts +21 -0
- package/x/ecs/parts/types.js +9 -0
- package/x/ecs/parts/types.js.map +1 -0
- package/x/ecs/test/setup-example.d.ts +15 -0
- package/x/ecs/test/setup-example.js +32 -0
- package/x/ecs/test/setup-example.js.map +1 -0
- package/x/ecs/test.d.ts +2 -0
- package/x/ecs/test.js +73 -48
- package/x/ecs/test.js.map +1 -1
- package/s/ecs/parts/changers.ts +0 -16
- package/s/ecs/parts/world.ts +0 -68
- package/s/ecs/test/setup-example-world.ts +0 -42
- package/s/ecs/types.ts +0 -19
- package/s/ecs/utils/is-match.ts +0 -7
- package/s/ecs/utils/optimizer.ts +0 -38
- package/x/ecs/parts/changers.d.ts +0 -4
- package/x/ecs/parts/changers.js +0 -11
- package/x/ecs/parts/changers.js.map +0 -1
- package/x/ecs/parts/world.d.ts +0 -11
- package/x/ecs/parts/world.js +0 -59
- package/x/ecs/parts/world.js.map +0 -1
- package/x/ecs/test/setup-example-world.d.ts +0 -10
- package/x/ecs/test/setup-example-world.js +0 -31
- package/x/ecs/test/setup-example-world.js.map +0 -1
- package/x/ecs/types.d.ts +0 -12
- package/x/ecs/types.js +0 -2
- package/x/ecs/types.js.map +0 -1
- package/x/ecs/utils/is-match.d.ts +0 -2
- package/x/ecs/utils/is-match.js +0 -4
- package/x/ecs/utils/is-match.js.map +0 -1
- package/x/ecs/utils/optimizer.d.ts +0 -9
- package/x/ecs/utils/optimizer.js +0 -37
- package/x/ecs/utils/optimizer.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,19 +1,95 @@
|
|
|
1
1
|
|
|
2
|
-

|
|
3
3
|
|
|
4
|
-
#
|
|
4
|
+
# 🌀 archimedes, netlogic for multiplayer web games
|
|
5
5
|
|
|
6
|
-
> [***"do not disturb my circles!"***](https://en.wikipedia.org/wiki/noli_turbare_circulos_meos!)
|
|
6
|
+
> [***"do not disturb my circles!"***](https://en.wikipedia.org/wiki/noli_turbare_circulos_meos!)
|
|
7
7
|
> — *archimedes, c. 212 bc*
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
```bash
|
|
10
|
+
npm install @benev/archimedes
|
|
11
|
+
```
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
- 🧩 [**#ecs,**](#ecs) entities, components, systems.
|
|
14
|
+
- 🔮 [**#sim,**](#sim) code like it's single-player, archimedes makes it multiplayer.
|
|
15
|
+
- 🌎 [**#net,**](#net) whole-world rollforward, everything is clientside predicted for insta-feels.
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
<br/><a id="ecs"></a>
|
|
20
|
+
|
|
21
|
+
## 🧩 ecs — entities, components, systems
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import {Entities, asSystems, change, executeSystems} from "@benev/archimedes"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
1. ***define components.*** json-friendly data that entities could have.
|
|
28
|
+
```ts
|
|
29
|
+
export type MyComponents = {
|
|
30
|
+
health: number
|
|
31
|
+
bleed: number
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
1. ***create entities map.*** fancy caching for speedy-fast lookups.
|
|
35
|
+
```ts
|
|
36
|
+
export const entitiesWritable = new Entities<MyComponents>()
|
|
37
|
+
|
|
38
|
+
// only let systems touch the readonly variant!
|
|
39
|
+
export const entities = entitiesWritable.readonly()
|
|
40
|
+
```
|
|
41
|
+
1. ***define systems.*** select entities by components. yields changes.
|
|
42
|
+
```ts
|
|
43
|
+
const systems = asSystems<MyComponents>(
|
|
44
|
+
function* bleeding() {
|
|
45
|
+
for (const [id, components] of entities.select("health", "bleed")) {
|
|
46
|
+
if (components.bleed >= 0) {
|
|
47
|
+
const health = components.health - components.bleed
|
|
48
|
+
const bleed = components.bleed - 1
|
|
49
|
+
yield change.merge(id, {health, bleed})
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
function* death() {
|
|
55
|
+
for (const [id, components] of entities.select("health")) {
|
|
56
|
+
if (components.health <= 0)
|
|
57
|
+
yield change.delete(id)
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
)
|
|
61
|
+
```
|
|
62
|
+
1. ***create your first entity.***
|
|
63
|
+
```ts
|
|
64
|
+
const wizardId = makeId()
|
|
65
|
+
entitiesWritable.set(wizardId, {health: 100, bleed: 2})
|
|
66
|
+
|
|
67
|
+
console.log(entities.get(wizardId)?.health)
|
|
68
|
+
// 100
|
|
69
|
+
```
|
|
70
|
+
1. ***execute systems to simulate each tick.***
|
|
71
|
+
```ts
|
|
72
|
+
executeSystems(entitiesWritable, systems)
|
|
73
|
+
|
|
74
|
+
console.log(entities.get(wizardId)?.health)
|
|
75
|
+
// 98
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
<br/><a id="sim"></a>
|
|
81
|
+
|
|
82
|
+
## 🔮 sim — networkable simulation architecture
|
|
83
|
+
|
|
84
|
+
*coming soon*
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
<br/><a id="net"></a>
|
|
89
|
+
|
|
90
|
+
## 🌎 net — connect and run multiplayer games
|
|
91
|
+
|
|
92
|
+
*coming soon*
|
|
13
93
|
|
|
14
|
-
🌎 **whole-world rollforward**
|
|
15
|
-
clientside prediction applies to the whole simulation. all effects of player inputs feel instant.
|
|
16
94
|
|
|
17
|
-
🧩 **ecs architecture**
|
|
18
|
-
program your simulation with entities, components, and systems.
|
|
19
95
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@benev/archimedes",
|
|
3
|
-
"version": "0.1.0-
|
|
3
|
+
"version": "0.1.0-4",
|
|
4
4
|
"description": "game ecs with auto networking",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -10,20 +10,20 @@
|
|
|
10
10
|
"s"
|
|
11
11
|
],
|
|
12
12
|
"scripts": {
|
|
13
|
-
"build": "
|
|
13
|
+
"build": "rm -rf x && mkdir x && tsc",
|
|
14
14
|
"dev": "octo 'node --watch x/test.js' 'tsc -w'",
|
|
15
15
|
"test": "node x/test.js",
|
|
16
16
|
"count": "find s -path '*/_archive' -prune -o -name '*.ts' -exec wc -l {} +"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@e280/renraku": "^0.5.7",
|
|
20
|
-
"@e280/stz": "^0.2.
|
|
20
|
+
"@e280/stz": "^0.2.29",
|
|
21
21
|
"@msgpack/msgpack": "^3.1.3",
|
|
22
22
|
"sparrow-rtc": "^0.2.15"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@e280/octo": "^0.1.0",
|
|
26
|
-
"@e280/science": "^0.1.
|
|
26
|
+
"@e280/science": "^0.1.10",
|
|
27
27
|
"npm-run-all": "^4.1.5",
|
|
28
28
|
"typescript": "^6.0.2"
|
|
29
29
|
},
|
package/s/ecs/index.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
|
|
2
|
-
export * from "./parts/
|
|
2
|
+
export * from "./parts/apply-change.js"
|
|
3
|
+
export * from "./parts/change.js"
|
|
4
|
+
export * from "./parts/entities.js"
|
|
5
|
+
export * from "./parts/execute-systems.js"
|
|
6
|
+
export * from "./parts/lifecycle.js"
|
|
3
7
|
export * from "./parts/make-id.js"
|
|
4
|
-
export * from "./parts/
|
|
5
|
-
|
|
6
|
-
export * from "./types.js"
|
|
8
|
+
export * from "./parts/types.js"
|
|
7
9
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
|
|
2
|
+
import {Entities} from "./entities.js"
|
|
3
|
+
import {ChangeSet, Change, Components, ChangeKind, ChangeMerge, ChangeDrop} from "./types.js"
|
|
4
|
+
|
|
5
|
+
export function applyChange<C extends Components>(entities: Entities<C>, change: Change<C>) {
|
|
6
|
+
switch (change[0]) {
|
|
7
|
+
case ChangeKind.Set: return applySet<C>(entities, <ChangeSet<C>>change)
|
|
8
|
+
case ChangeKind.Merge: return applyMerge<C>(entities, <ChangeMerge<C>>change)
|
|
9
|
+
case ChangeKind.Drop: return applyDrop<C>(entities, <ChangeDrop<C>>change)
|
|
10
|
+
default: throw new Error(`unknown change kind "${change[0]}"`)
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function applySet<C extends Components>(entities: Entities<C>, [, id, components]: ChangeSet<C>) {
|
|
15
|
+
if (components) entities.set(id, components as Partial<C>)
|
|
16
|
+
else entities.delete(id)
|
|
17
|
+
return id
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function applyMerge<C extends Components>(entities: Entities<C>, [, id, patch]: ChangeMerge<C>) {
|
|
21
|
+
const components = entities.guarantee(id, () => ({}))
|
|
22
|
+
Object.assign(components, patch)
|
|
23
|
+
entities.set(id, components)
|
|
24
|
+
return id
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function applyDrop<C extends Components>(entities: Entities<C>, [, id, keys]: ChangeDrop<C>) {
|
|
28
|
+
const components = entities.guarantee(id, () => ({}))
|
|
29
|
+
for (const key of keys) delete components[key]
|
|
30
|
+
entities.set(id, components)
|
|
31
|
+
return id
|
|
32
|
+
}
|
|
33
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
|
|
2
|
+
import {makeId} from "./make-id.js"
|
|
3
|
+
import {Components, ChangeSet, ChangeMerge, Id, ChangeKind, ChangeDrop} from "./types.js"
|
|
4
|
+
|
|
5
|
+
export const change = {
|
|
6
|
+
|
|
7
|
+
/** create a new entity with the given components */
|
|
8
|
+
create: (components: Partial<Components>): ChangeSet<Components> =>
|
|
9
|
+
[ChangeKind.Set, makeId(), components],
|
|
10
|
+
|
|
11
|
+
/** overwrite a whole entity to the given components */
|
|
12
|
+
set: (id: Id, components: Partial<Components>): ChangeSet<Components> =>
|
|
13
|
+
[ChangeKind.Set, id, components],
|
|
14
|
+
|
|
15
|
+
/** remove the entity */
|
|
16
|
+
delete: (id: Id): ChangeSet<Components> =>
|
|
17
|
+
[ChangeKind.Set, id],
|
|
18
|
+
|
|
19
|
+
/** update or add the given components onto the entity */
|
|
20
|
+
merge: (id: Id, components: Partial<Components>): ChangeMerge<Components> =>
|
|
21
|
+
[ChangeKind.Merge, id, components],
|
|
22
|
+
|
|
23
|
+
/** delete specific components off the entity */
|
|
24
|
+
drop: <C extends Components>(id: Id, ...keys: (keyof C)[]): ChangeDrop<C> =>
|
|
25
|
+
[ChangeKind.Drop, id, keys],
|
|
26
|
+
}
|
|
27
|
+
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
|
|
2
|
+
import {GMap} from "@e280/stz"
|
|
3
|
+
import {Components, Id, Select} from "./types.js"
|
|
4
|
+
|
|
5
|
+
export class Entities<C extends Components> extends GMap<Id, Partial<C>> {
|
|
6
|
+
#index = new GMap<Set<keyof C>, GMap<Id, Partial<C>>>()
|
|
7
|
+
|
|
8
|
+
set(id: Id, components: Partial<C>) {
|
|
9
|
+
super.set(id, components)
|
|
10
|
+
for (const [set, entities] of this.#index) {
|
|
11
|
+
if (componentsSatisfySet(components, set))
|
|
12
|
+
entities.set(id, components)
|
|
13
|
+
else
|
|
14
|
+
entities.delete(id)
|
|
15
|
+
}
|
|
16
|
+
return this
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
delete(id: Id) {
|
|
20
|
+
const didDelete = super.delete(id)
|
|
21
|
+
if (!didDelete)
|
|
22
|
+
return false
|
|
23
|
+
|
|
24
|
+
for (const entities of this.#index.values())
|
|
25
|
+
entities.delete(id)
|
|
26
|
+
|
|
27
|
+
return true
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
clear() {
|
|
31
|
+
super.clear()
|
|
32
|
+
for (const entities of this.#index.values())
|
|
33
|
+
entities.clear()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
*select<K extends keyof C>(...componentKeys: K[]): Iterable<[Id, Select<C, K>]> {
|
|
37
|
+
const cached = this.#getCache(componentKeys)
|
|
38
|
+
if (cached)
|
|
39
|
+
yield* cached
|
|
40
|
+
else
|
|
41
|
+
yield* this.#makeCache(componentKeys)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
readonly() {
|
|
45
|
+
return this as EntitiesReadonly<C>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
#getCache<K extends keyof C>(componentKeys: K[]) {
|
|
49
|
+
for (const set of this.#index.keys()) {
|
|
50
|
+
if (setHasSameValuesAsArray(set, componentKeys))
|
|
51
|
+
return this.#index.require(set) as GMap<Id, Select<C, K>>
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
*#makeCache<K extends keyof C>(componentKeys: K[]) {
|
|
56
|
+
const set = new Set(componentKeys)
|
|
57
|
+
const entities = new GMap<Id, Partial<C>>()
|
|
58
|
+
this.#index.set(set, entities)
|
|
59
|
+
|
|
60
|
+
for (const entity of this) {
|
|
61
|
+
const [id, components] = entity
|
|
62
|
+
|
|
63
|
+
if (componentsSatisfyKeys(components, componentKeys)) {
|
|
64
|
+
entities.set(id, components)
|
|
65
|
+
yield entity as [Id, Select<C, K>]
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type EntitiesReadonly<C extends Components> = Pick<Entities<Readonly<C>>, (
|
|
72
|
+
| "has"
|
|
73
|
+
| "get"
|
|
74
|
+
| "require"
|
|
75
|
+
| "keys"
|
|
76
|
+
| "values"
|
|
77
|
+
| "entries"
|
|
78
|
+
| "select"
|
|
79
|
+
| typeof Symbol.iterator
|
|
80
|
+
)>
|
|
81
|
+
|
|
82
|
+
function componentsSatisfySet(components: Components, set: Set<PropertyKey>) {
|
|
83
|
+
for (const key of set)
|
|
84
|
+
if (!(key in components))
|
|
85
|
+
return false
|
|
86
|
+
return true
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function setHasSameValuesAsArray(set: Set<unknown>, keys: unknown[]) {
|
|
90
|
+
if (set.size !== keys.length)
|
|
91
|
+
return false
|
|
92
|
+
return keys.every(key => set.has(key))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function componentsSatisfyKeys(components: object, keys: PropertyKey[]) {
|
|
96
|
+
return keys.every(key => key in components)
|
|
97
|
+
}
|
|
98
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
|
|
2
|
+
import {Entities} from "./entities.js"
|
|
3
|
+
import {applyChange} from "./apply-change.js"
|
|
4
|
+
import {Change, Components, System} from "./types.js"
|
|
5
|
+
|
|
6
|
+
export function executeSystems<C extends Components>(entities: Entities<C>, systems: System<C>[]) {
|
|
7
|
+
const changes: Change<C>[] = []
|
|
8
|
+
|
|
9
|
+
for (const system of systems) {
|
|
10
|
+
for (const change of system()) {
|
|
11
|
+
applyChange(entities, change)
|
|
12
|
+
changes.push(change)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return changes
|
|
17
|
+
}
|
|
18
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
|
|
2
|
+
import {GMap} from "@e280/stz"
|
|
3
|
+
import {EntitiesReadonly} from "./entities.js"
|
|
4
|
+
import {Components, Id, LifecycleCallbacks, LifecycleEnter} from "./types.js"
|
|
5
|
+
|
|
6
|
+
export function lifecycle<C extends Components, K extends keyof C>(
|
|
7
|
+
entities: EntitiesReadonly<C>,
|
|
8
|
+
componentKeys: K[],
|
|
9
|
+
enter: LifecycleEnter<C, K>
|
|
10
|
+
) {
|
|
11
|
+
|
|
12
|
+
const alive = new GMap<Id, LifecycleCallbacks<C, K>>()
|
|
13
|
+
const sel = () => entities.select(...componentKeys)
|
|
14
|
+
|
|
15
|
+
return function*() {
|
|
16
|
+
const current = new Map(sel())
|
|
17
|
+
|
|
18
|
+
for (const [id, callbacks] of alive) {
|
|
19
|
+
if (current.has(id)) continue
|
|
20
|
+
alive.delete(id)
|
|
21
|
+
callbacks.exit(id)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
for (const [id, components] of current) {
|
|
25
|
+
const callbacks = alive.guarantee(id, () => enter(id, components))
|
|
26
|
+
callbacks.tick(id, components)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
|
|
2
|
+
export type Id = string
|
|
3
|
+
export type Components = Record<string, unknown>
|
|
4
|
+
export type AsComponents<C extends Components> = C
|
|
5
|
+
export type Select<C extends Components, K extends keyof C> = Pick<C, K> & Partial<C>
|
|
6
|
+
export type System<C extends Components> = () => Generator<Change<C>>
|
|
7
|
+
|
|
8
|
+
export const asSystem = <C extends Components>(system: System<C>) => system
|
|
9
|
+
export const asSystems = <C extends Components>(...systems: System<C>[]) => systems
|
|
10
|
+
|
|
11
|
+
export enum ChangeKind {Set, Merge, Drop}
|
|
12
|
+
export type ChangeSet<C extends Components> = [kind: ChangeKind.Set, id: Id, components?: Partial<C>]
|
|
13
|
+
export type ChangeMerge<C extends Components> = [kind: ChangeKind.Merge, id: Id, patch: Partial<C>]
|
|
14
|
+
export type ChangeDrop<C extends Components> = [kind: ChangeKind.Drop, id: Id, keys: (keyof C)[]]
|
|
15
|
+
export type Change<C extends Components> = ChangeSet<C> | ChangeMerge<C> | ChangeDrop<C>
|
|
16
|
+
|
|
17
|
+
export type LifecycleCallbacks<C extends Components, K extends keyof C> = {
|
|
18
|
+
tick: (id: Id, components: Select<C, K>) => void
|
|
19
|
+
exit: (id: Id) => void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type LifecycleEnter<C extends Components, K extends keyof C> = (
|
|
23
|
+
(id: Id, components: Select<C, K>) => LifecycleCallbacks<C, K>
|
|
24
|
+
)
|
|
25
|
+
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
|
|
2
|
+
import {change} from "../parts/change.js"
|
|
3
|
+
import {asSystems} from "../parts/types.js"
|
|
4
|
+
import {Entities} from "../parts/entities.js"
|
|
5
|
+
|
|
6
|
+
export function setupExample() {
|
|
7
|
+
type MyComponents = {
|
|
8
|
+
health: number
|
|
9
|
+
bleed: number
|
|
10
|
+
mana: number
|
|
11
|
+
manaRegen: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const writableEntities = new Entities<MyComponents>()
|
|
15
|
+
const entities = writableEntities.readonly()
|
|
16
|
+
|
|
17
|
+
const systems = asSystems<MyComponents>(
|
|
18
|
+
function* manaRegen() {
|
|
19
|
+
for (const [id, components] of entities.select("mana", "manaRegen")) {
|
|
20
|
+
if (components.manaRegen !== 0) {
|
|
21
|
+
const mana = components.mana + components.manaRegen
|
|
22
|
+
yield change.merge(id, {mana})
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
function* bleeding() {
|
|
28
|
+
for (const [id, components] of entities.select("health", "bleed")) {
|
|
29
|
+
if (components.bleed >= 0) {
|
|
30
|
+
const health = components.health - components.bleed
|
|
31
|
+
const bleed = components.bleed - 1
|
|
32
|
+
yield change.merge(id, {health, bleed})
|
|
33
|
+
}
|
|
34
|
+
if (components.bleed <= 0)
|
|
35
|
+
yield change.drop(id, "bleed")
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
function* death() {
|
|
40
|
+
for (const [id, components] of entities.select("health")) {
|
|
41
|
+
if (components.health <= 0)
|
|
42
|
+
yield change.delete(id)
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return {entities: writableEntities, systems}
|
|
48
|
+
}
|
|
49
|
+
|
package/s/ecs/test.ts
CHANGED
|
@@ -1,79 +1,104 @@
|
|
|
1
1
|
|
|
2
2
|
import {suite, test, expect} from "@e280/science"
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import {change} from "./parts/change.js"
|
|
4
|
+
import {lifecycle} from "./parts/lifecycle.js"
|
|
5
|
+
import {applyChange} from "./parts/apply-change.js"
|
|
6
|
+
import {setupExample} from "./test/setup-example.js"
|
|
7
|
+
import {executeSystems} from "./parts/execute-systems.js"
|
|
5
8
|
import {setupLifecycleCounts} from "./test/setup-lifecycle-counts.js"
|
|
6
9
|
|
|
7
10
|
export default suite({
|
|
8
11
|
"create an entity": test(async() => {
|
|
9
|
-
const {
|
|
10
|
-
expect(
|
|
11
|
-
|
|
12
|
-
expect(
|
|
12
|
+
const {entities} = setupExample()
|
|
13
|
+
expect(entities.size).is(0)
|
|
14
|
+
applyChange(entities, change.create({health: 100}))
|
|
15
|
+
expect(entities.size).is(1)
|
|
13
16
|
}),
|
|
14
17
|
|
|
15
18
|
"delete an entity": test(async() => {
|
|
16
|
-
const {
|
|
17
|
-
const id =
|
|
18
|
-
expect(
|
|
19
|
-
|
|
20
|
-
expect(
|
|
19
|
+
const {entities} = setupExample()
|
|
20
|
+
const id = applyChange(entities, change.create({health: 100}))
|
|
21
|
+
expect(entities.size).is(1)
|
|
22
|
+
applyChange(entities, change.delete(id))
|
|
23
|
+
expect(entities.size).is(0)
|
|
24
|
+
}),
|
|
25
|
+
|
|
26
|
+
"merge components into entity": test(async() => {
|
|
27
|
+
const {entities} = setupExample()
|
|
28
|
+
const id = applyChange(entities, change.create({health: 100, mana: 100}))
|
|
29
|
+
expect(entities.require(id).health).is(100)
|
|
30
|
+
applyChange(entities, change.merge(id, {health: 99}))
|
|
31
|
+
expect(entities.require(id).health).is(99)
|
|
32
|
+
expect(entities.require(id).mana).is(100)
|
|
21
33
|
}),
|
|
22
34
|
|
|
23
35
|
"select an entity": test(async() => {
|
|
24
|
-
const {
|
|
25
|
-
|
|
26
|
-
expect([...
|
|
36
|
+
const {entities} = setupExample()
|
|
37
|
+
applyChange(entities, change.create({health: 100}))
|
|
38
|
+
expect([...entities.select("health")].length).is(1)
|
|
39
|
+
}),
|
|
40
|
+
|
|
41
|
+
"drop components from entity": test(async() => {
|
|
42
|
+
const {entities} = setupExample()
|
|
43
|
+
const id = applyChange(entities, change.create({health: 100, mana: 100}))
|
|
44
|
+
expect([...entities.select("health", "mana")].length).is(1)
|
|
45
|
+
applyChange(entities, change.drop(id, "mana"))
|
|
46
|
+
expect("mana" in entities.require(id)).is(false)
|
|
47
|
+
expect([...entities.select("health", "mana")].length).is(0)
|
|
27
48
|
}),
|
|
28
49
|
|
|
29
50
|
"select two entities": test(async() => {
|
|
30
|
-
const {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
expect([...
|
|
51
|
+
const {entities} = setupExample()
|
|
52
|
+
applyChange(entities, change.create({health: 100}))
|
|
53
|
+
applyChange(entities, change.create({health: 100}))
|
|
54
|
+
expect([...entities.select("health")].length).is(2)
|
|
34
55
|
}),
|
|
35
56
|
|
|
36
57
|
"select with no component keys selects all": test(async() => {
|
|
37
|
-
const {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
expect([...
|
|
58
|
+
const {entities} = setupExample()
|
|
59
|
+
applyChange(entities, change.create({health: 100}))
|
|
60
|
+
applyChange(entities, change.create({health: 100}))
|
|
61
|
+
expect([...entities.select()].length).is(2)
|
|
41
62
|
}),
|
|
42
63
|
|
|
43
64
|
"select doesn't include non-match": test(async() => {
|
|
44
|
-
const {
|
|
45
|
-
|
|
46
|
-
expect([...
|
|
65
|
+
const {entities} = setupExample()
|
|
66
|
+
applyChange(entities, change.create({health: 100}))
|
|
67
|
+
expect([...entities.select("mana")].length).is(0)
|
|
47
68
|
}),
|
|
48
69
|
|
|
49
70
|
"select includes entities with extra components": test(async() => {
|
|
50
|
-
const {
|
|
51
|
-
|
|
52
|
-
expect([...
|
|
71
|
+
const {entities} = setupExample()
|
|
72
|
+
applyChange(entities, change.create({health: 100, mana: 100}))
|
|
73
|
+
expect([...entities.select("health")].length).is(1)
|
|
53
74
|
}),
|
|
54
75
|
|
|
55
76
|
"wizard regens mana": test(async() => {
|
|
56
|
-
const {
|
|
57
|
-
const
|
|
58
|
-
const
|
|
77
|
+
const {entities, systems} = setupExample()
|
|
78
|
+
const creating = change.create({health: 100, mana: 50, manaRegen: 1})
|
|
79
|
+
const wizardId = creating[1]
|
|
80
|
+
applyChange(entities, creating)
|
|
81
|
+
const changes = executeSystems(entities, systems)
|
|
59
82
|
expect(changes.length).is(1)
|
|
60
|
-
expect(
|
|
83
|
+
expect(entities.require(wizardId).mana).is(51)
|
|
61
84
|
}),
|
|
62
85
|
|
|
63
86
|
"death by bleeding": test(async() => {
|
|
64
|
-
const {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
expect(
|
|
69
|
-
|
|
70
|
-
expect(
|
|
87
|
+
const {entities, systems} = setupExample()
|
|
88
|
+
const creating = change.create({health: 3, bleed: 2})
|
|
89
|
+
const wizardId = creating[1]
|
|
90
|
+
applyChange(entities, creating)
|
|
91
|
+
expect(entities.require(wizardId).health).is(3)
|
|
92
|
+
executeSystems(entities, systems)
|
|
93
|
+
expect(entities.require(wizardId).health).is(1)
|
|
94
|
+
executeSystems(entities, systems)
|
|
95
|
+
expect(entities.has(wizardId)).is(false)
|
|
71
96
|
}),
|
|
72
97
|
|
|
73
98
|
"lifecycles": test(async() => {
|
|
74
|
-
const {
|
|
99
|
+
const {entities} = setupExample()
|
|
75
100
|
const counts = setupLifecycleCounts()
|
|
76
|
-
const system =
|
|
101
|
+
const system = lifecycle(entities.readonly(), ["health"], () => {
|
|
77
102
|
counts.enters++
|
|
78
103
|
return {
|
|
79
104
|
tick: () => void counts.ticks++,
|
|
@@ -82,18 +107,20 @@ export default suite({
|
|
|
82
107
|
})
|
|
83
108
|
counts.expect(0, 0, 0)
|
|
84
109
|
|
|
85
|
-
const
|
|
86
|
-
|
|
110
|
+
const creating = change.create({health: 100, mana: 50})
|
|
111
|
+
const wizardId = creating[1]
|
|
112
|
+
applyChange(entities, creating)
|
|
113
|
+
executeSystems(entities, [system])
|
|
87
114
|
counts.expect(1, 1, 0)
|
|
88
115
|
|
|
89
|
-
|
|
90
|
-
|
|
116
|
+
applyChange(entities, change.merge(wizardId, {health: 100, mana: 100}))
|
|
117
|
+
executeSystems(entities, [system])
|
|
91
118
|
counts.expect(1, 2, 0)
|
|
92
119
|
|
|
93
|
-
|
|
94
|
-
|
|
120
|
+
applyChange(entities, change.delete(wizardId))
|
|
121
|
+
executeSystems(entities, [system])
|
|
95
122
|
counts.expect(1, 2, 1)
|
|
96
|
-
expect(
|
|
123
|
+
expect(entities.size).is(0)
|
|
97
124
|
}),
|
|
98
125
|
})
|
|
99
126
|
|
package/x/ecs/index.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
export * from "./parts/
|
|
1
|
+
export * from "./parts/apply-change.js";
|
|
2
|
+
export * from "./parts/change.js";
|
|
3
|
+
export * from "./parts/entities.js";
|
|
4
|
+
export * from "./parts/execute-systems.js";
|
|
5
|
+
export * from "./parts/lifecycle.js";
|
|
2
6
|
export * from "./parts/make-id.js";
|
|
3
|
-
export * from "./parts/
|
|
4
|
-
export * from "./types.js";
|
|
7
|
+
export * from "./parts/types.js";
|
package/x/ecs/index.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
export * from "./parts/
|
|
1
|
+
export * from "./parts/apply-change.js";
|
|
2
|
+
export * from "./parts/change.js";
|
|
3
|
+
export * from "./parts/entities.js";
|
|
4
|
+
export * from "./parts/execute-systems.js";
|
|
5
|
+
export * from "./parts/lifecycle.js";
|
|
2
6
|
export * from "./parts/make-id.js";
|
|
3
|
-
export * from "./parts/
|
|
4
|
-
export * from "./types.js";
|
|
7
|
+
export * from "./parts/types.js";
|
|
5
8
|
//# sourceMappingURL=index.js.map
|
package/x/ecs/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../s/ecs/index.ts"],"names":[],"mappings":"AACA,cAAc,qBAAqB,CAAA;AACnC,cAAc,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../s/ecs/index.ts"],"names":[],"mappings":"AACA,cAAc,yBAAyB,CAAA;AACvC,cAAc,mBAAmB,CAAA;AACjC,cAAc,qBAAqB,CAAA;AACnC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,sBAAsB,CAAA;AACpC,cAAc,oBAAoB,CAAA;AAClC,cAAc,kBAAkB,CAAA"}
|