Skip to content

Commit 1cabbe8

Browse files
committed
🔖 version bump
1 parent 743b50b commit 1cabbe8

File tree

7 files changed

+90
-9
lines changed

7 files changed

+90
-9
lines changed

packages/publish/README.md

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,35 @@ const containsAnything = world.query(Contains('*')) // Returns [inventory, chest
217217
const relatesToGold = world.query(Wildcard(gold)) // Returns [inventory, chest, dwarf]
218218
```
219219
220+
#### Removing relationships
221+
222+
A relationship targets a specific entity, so we need to likewise remove relationships with specific entities.
223+
224+
```js
225+
// Add a specific relation
226+
player.add(Likes(apple))
227+
player.add(Likes(banana))
228+
229+
// Remove that same relation
230+
player.remove(Likes(apple))
231+
232+
player.has(apple) // false
233+
player.has(banana) // true
234+
```
235+
236+
However, a wildcard can be used to remove all relationships of a kind — for all targets — from an entity.
237+
238+
```js
239+
player.add(Likes(apple))
240+
player.add(Likes(banana))
241+
242+
// Remove all Likes relations
243+
player.remove(Likes('*'))
244+
245+
player.has(apple) // false
246+
player.has(banana) // false
247+
```
248+
220249
### Query modifiers
221250
222251
Modifiers are used to filter query results enabling powerful patterns. All modifiers can be mixed together.
@@ -550,30 +579,35 @@ const { entityId, generation, worldId } = unpackEntity(entity)
550579
551580
### Trait
552581
553-
A trait is a specific block of data. They are added to entities to build up its overall data signature. If you are familiar with ECS, it is our version of a component. It is called a trait instead to not get confused with React or web components.
582+
Traits are self-contained slices of data you attach to an entity to define its state. They serve the same purpose as components in a traditional ECS. We call them traits to avoid confusion with React or web components.
554583
555584
A trait can be created with a schema that describes the kind of data it will hold.
556585
557586
```js
558587
const Position = trait({ x: 0, y: 0, z: 0 })
559588
```
560589
561-
In cases where the data needs to be initialized for each instance of the trait created, a callback can be passed in to be used a as a lazy initializer.
590+
A schema supports primitive values with **no** nested objects or arrays. In cases where the data needs to initialized for each instance of the trait, or complex structures are required, a callback initializer can be used.
562591
563592
```js
564-
//The items array will be shared between every instance of this trait
593+
//Arrays and objects are not allowed in trait schemas
565594
const Inventory = trait({
566595
items: [],
596+
vec3: { x: 0, y: 0, z: 0}
567597
max: 10,
568598
})
569599

570-
//With a lazy initializer, each instance will now get its own array
600+
//Use a callback initializer for arrays and objects
571601
const Inventory = trait({
572602
items: () => [],
603+
vec3: () => ({ x: 0, y: 0, z: 0})
573604
max: 10,
574605
})
575606
```
576607
608+
> ℹ️ **Why not support nested schemas?**<br>
609+
> It looks obvious to support nested stores, but doing so makes algorithms that work with the data exponentially more complex. If all data can be assumed scalar then any operation is guaranteed to be the simplest and fastest algorithm possible. This is called the First Normal Form in relational database theory. [You can read more here](https://www.dataorienteddesign.com/dodbook/node3.html#SECTION00340000000000000000).
610+
577611
Sometimes a trait only has one field that points to an object instance. In cases like this, it is useful to skip the schema and use a callback directly in the trait.
578612
579613
```js

packages/publish/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "koota",
3-
"version": "0.4.4",
3+
"version": "0.5.0",
44
"description": "🌎 Performant real-time state management for React and TypeScript",
55
"license": "ISC",
66
"type": "module",

packages/publish/tests/core/query-modifiers.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import {
1212

1313
const Position = trait({ x: 0, y: 0 });
1414
const IsActive = trait();
15-
const Foo = trait({});
16-
const Bar = trait({});
15+
const Foo = trait();
16+
const Bar = trait();
1717

1818
describe('Query modifiers', () => {
1919
const world = createWorld();

packages/publish/tests/core/query.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import { $internal, cacheQuery, createWorld, IsExcluded, Not, Or, trait } from '
44
const Position = trait({ x: 0, y: 0 });
55
const Name = trait({ name: 'name' });
66
const IsActive = trait();
7-
const Foo = trait({});
8-
const Bar = trait({});
7+
const Foo = trait();
8+
const Bar = trait();
99

1010
describe('Query', () => {
1111
const world = createWorld();

packages/publish/tests/core/relation.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ describe('Relation', () => {
5757
expect(target).toBe(player);
5858
expect(goblin.has(Targeting(player))).toBe(true);
5959
expect(goblin.has(Targeting(guard))).toBe(false);
60+
61+
// Remove the target and check if the target is removed
62+
goblin.remove(Targeting(player));
63+
target = goblin.targetFor(Targeting);
64+
65+
expect(target).toBe(undefined);
6066
});
6167

6268
it('should auto remove target and its descendants', () => {
@@ -210,4 +216,20 @@ describe('Relation', () => {
210216
expect(world.has(apple)).toBe(true);
211217
expect(world.has(cherry)).toBe(true);
212218
});
219+
220+
it('should remove all relations with a wildcard', () => {
221+
const Likes = relation();
222+
223+
const person = world.spawn();
224+
const apple = world.spawn();
225+
const banana = world.spawn();
226+
227+
person.add(Likes(apple));
228+
person.add(Likes(banana));
229+
230+
person.remove(Likes('*'));
231+
232+
expect(person.has(Likes(apple))).toBe(false);
233+
expect(person.has(Likes(banana))).toBe(false);
234+
});
213235
});

packages/publish/tests/core/trait.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ describe('Trait', () => {
2929
expect(typeof Test === 'function').toBe(true);
3030
});
3131

32+
it('should throw an error if the schema contains an object or array', () => {
33+
// @ts-expect-error - we want to test the error case
34+
expect(() => trait({ object: { a: 1, b: 2 } })).toThrow();
35+
// @ts-expect-error - we want to test the error case
36+
expect(() => trait({ array: [1, 2, 3] })).toThrow();
37+
});
38+
3239
it('should add and remove traits to an entity', () => {
3340
const entity = world.spawn();
3441

packages/publish/tests/core/world.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,24 @@ describe('World', () => {
9292
expect(world.has(Time)).toBe(false);
9393
});
9494

95+
it('should set singletons', () => {
96+
const Test = trait({ last: 0, delta: 0 });
97+
const world = createWorld(Test);
98+
99+
world.set(Test, { last: 1, delta: 1 });
100+
101+
expect(world.get(Test)!.last).toBe(1);
102+
expect(world.get(Test)!.delta).toBe(1);
103+
104+
// Use callbacks to set.
105+
world.set(Test, (prev) => {
106+
return { last: prev.last + 1, delta: prev.delta + 1 };
107+
});
108+
109+
expect(world.get(Test)!.last).toBe(2);
110+
expect(world.get(Test)!.delta).toBe(2);
111+
});
112+
95113
it('should observe traits', () => {
96114
const TimeOfDay = trait({ hour: 0 });
97115
const world = createWorld(TimeOfDay);

0 commit comments

Comments
 (0)