From 144cd044e8cd683f112a757920e88cea09cb1b65 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Sat, 25 May 2024 03:53:35 +0200 Subject: [PATCH] Add pages to docs --- docs/api-types.md | 45 ++++ docs/api/world.md | 189 ++++++++++++++++ docs/world.md | 5 + lib/init.lua | 12 +- mkdocs.yml | 186 ++++++++++++++++ tests/world.lua | 552 ++++++++++++++++++++++++---------------------- 6 files changed, 711 insertions(+), 278 deletions(-) create mode 100644 docs/api-types.md create mode 100644 docs/api/world.md create mode 100644 docs/world.md create mode 100644 mkdocs.yml diff --git a/docs/api-types.md b/docs/api-types.md new file mode 100644 index 0000000..b446999 --- /dev/null +++ b/docs/api-types.md @@ -0,0 +1,45 @@ +# World + +A World contains all ECS data +Games can have multiple worlds, although typically only one is necessary. These worlds are isolated from each other, meaning they donot share the same entities nor component IDs. + +--- + +# Entity + +An unique id. + +Entities consist out of a number unique to the entity in the lower 32 bits, and a counter used to track entity liveliness in the upper 32 bits. When an id is recycled, its generation count is increased. This causes recycled ids to be very large (>4 billion), which is normal. + +--- + +# QueryIter + +A result from the `World:query` function. + +Queries are used to iterate over entities that match against the set collection of components. + +Calling it in a loop will allow iteration over the results. + +```lua +for id, enemy, charge, model in world:query(Enemy, Charge, Model) do + -- Do something +end +``` + +### QueryIter.without + +QueryIter.without(iter: QueryIter + ...: [Entity](../api-types/Entity)): QueryIter + + +Create a new Query Iterator from the filter + +#### Parameters + world The world. + ... The collection of components to filter archetypes against. + +#### Returns + +The new query iterator. + diff --git a/docs/api/world.md b/docs/api/world.md new file mode 100644 index 0000000..cf2afaa --- /dev/null +++ b/docs/api/world.md @@ -0,0 +1,189 @@ +# World + +### World.new + +World.new(): [World](../api-types/World) + +Create a new world. + +#### Returns +A new world + +--- + +### World.entity + +World.entity(world: [World](../api-types/World)): [Entity](../api-types/Entity) + +Creates an entity in the world. + +#### Returns +A new entiity id + +--- + +### World.target + +World.target(world: [World](../api-types/World), + entity: [Entity](../api-types/Entity), + rel: [Entity](../api-types/Entity)): [Entity](../api-types/Entity) + +Get the target of a relationship. + +This will return a target (second element of a pair) of the entity for the specified relationship. + +#### Parameters + world The world. + entity The entity. + rel The relationship between the entity and the target. + +#### Returns + +The first target for the relationship + +--- + +### World.add + +World.add(world: [World](../api-types/World), + entity: [Entity](../api-types/Entity), + id: [Entity](../api-types/Entity)): [Entity](../api-types/Entity) + +Add a (component) id to an entity. + +This operation adds a single (component) id to an entity. +If the entity already has the id, this operation will have no side effects. + +#### Parameters + world The world. + entity The entity. + id The id to add. + +--- + +### World.remove + +World.remove(world: [World](../api-types/World), + entity: [Entity](../api-types/Entity), + id: [Entity](../api-types/Entity)): [Entity](../api-types/Entity) + +Remove a (component) id to an entity. + +This operation removes a single (component) id to an entity. +If the entity already has the id, this operation will have no side effects. + +#### Parameters + world The world. + entity The entity. + id The id to add. + +--- + +### World.get + +World.get(world: [World](../api-types/World), + entity: [Entity](../api-types/Entity), + id: [Entity](../api-types/Entity)): any + +Gets the component data. + +#### Parameters + world The world. + entity The entity. + id The id of component to get. + +#### Returns +The component data, nil if the entity does not have the componnet. + +--- + +### World.set + +World.set(world: [World](../api-types/World), + entity: [Entity](../api-types/Entity), + id: [Entity](../api-types/Entity) + data: any) + +Set the value of a component. + +#### Parameters + world The world. + entity The entity. + id The id of the componment set. + data The data to the component. + +--- + +### World.query + +World.query(world: [World](../api-types/World), + ...: [Entity](../api-types/Entity)): [QueryIter](../api-types/QueryIter) + +Create a QueryIter from the list of filters. + +#### Parameters + world The world. + ... The collection of components to match entities against. + +#### Returns + +The query iterator. + +--- + +# Pair + +### pair + +pair(first: [Entity](../api-types/Entity), + second: [Entity](../api-types/Entity)): [Entity](../api-types/Entity) + +Creates a composite key. + +#### Parameters + first The first element. + second The second element. + +#### Returns + +The pair of the two elements + +--- + +### IS_PAIR + +jecs.IS_PAIR(id: [Entity](../api-types/Entity)): boolean + +Creates a composite key. + +#### Parameters + id The id to check. + +#### Returns + +If id is a pair. + +--- + +# Constants + +### OnAdd + +--- + +### OnRemove + +--- + +### Rest + +--- + +### OnSet + +--- + +### Wildcard + +Matches any id, returns all matches. + diff --git a/docs/world.md b/docs/world.md new file mode 100644 index 0000000..5376278 --- /dev/null +++ b/docs/world.md @@ -0,0 +1,5 @@ +# World + +A World contains all ECS data +Games can have multiple worlds, although typically only one is necessary. These worlds are isolated from each other. +is queryable and can be used to get entities with a specific set of components. Entities are simply ever-increasing integers. diff --git a/lib/init.lua b/lib/init.lua index 5503ce8..9ef0b10 100644 --- a/lib/init.lua +++ b/lib/init.lua @@ -103,14 +103,6 @@ local function ECS_IS_PAIR(e: number) return (e % 2 ^ 4) // FLAGS_PAIR ~= 0 end -function separate(e: number) - local _typeFlags = e % 0x10 - -- Revert to //= after highligting gets fixed - -- - e = e // ECS_ID_FLAGS_MASK - return e // ECS_ENTITY_MASK, e % ECS_GENERATION_MASK, _typeFlags -end - -- HIGH 24 bits LOW 24 bits local function ECS_GENERATION(e: i53) e = e // 0x10 @@ -118,7 +110,9 @@ local function ECS_GENERATION(e: i53) end local function ECS_GENERATION_INC(e: i53) - local id, generation, flags = separate(e) + local flags = e // 0x10 + local id = flags // ECS_ENTITY_MASK + local generation = flags % ECS_GENERATION_MASK return ECS_COMBINE(id, generation + 1) + flags end diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..5029861 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,186 @@ +site_name: Fusion +site_url: https://elttob.uk/Fusion/ +repo_name: dphfox/Fusion +repo_url: https://github.com/dphfox/Fusion + +extra: + version: + provider: mike + +theme: + name: material + custom_dir: docs/assets/overrides + logo: assets/logo + favicon: assets/logo-dark.svg + palette: + - media: "(prefers-color-scheme: dark)" + scheme: fusiondoc-dark + toggle: + icon: octicons/sun-24 + title: Switch to light theme + - media: "(prefers-color-scheme: light)" + scheme: fusiondoc-light + toggle: + icon: octicons/moon-24 + title: Switch to dark theme + font: + text: Plus Jakarta Sans + code: JetBrains Mono + features: + - navigation.tabs + - navigation.top + - navigation.sections + - navigation.instant + - navigation.indexes + - search.suggest + - search.highlight + icon: + repo: octicons/mark-github-16 + +extra_css: + - assets/theme/fusiondoc.css + - assets/theme/colours.css + - assets/theme/code.css + - assets/theme/paragraph.css + - assets/theme/page.css + - assets/theme/admonition.css + - assets/theme/404.css + - assets/theme/api-reference.css + - assets/theme/dev-tools.css + +extra_javascript: + - assets/scripts/smooth-scroll.js + +nav: + - Home: index.md + - Tutorials: + - Get Started: tutorials/index.md + - Installing Fusion: tutorials/get-started/installing-fusion.md + - Developer Tools: tutorials/get-started/developer-tools.md + - Getting Help: tutorials/get-started/getting-help.md + - Fundamentals: + - Scopes: tutorials/fundamentals/scopes.md + - Values: tutorials/fundamentals/values.md + - Observers: tutorials/fundamentals/observers.md + - Computeds: tutorials/fundamentals/computeds.md + - Tables: + - ForValues: tutorials/tables/forvalues.md + - ForKeys: tutorials/tables/forkeys.md + - ForPairs: tutorials/tables/forpairs.md + - Animation: + - Tweens: tutorials/animation/tweens.md + - Springs: tutorials/animation/springs.md + - Roblox: + - Hydration: tutorials/roblox/hydration.md + - New Instances: tutorials/roblox/new-instances.md + - Parenting: tutorials/roblox/parenting.md + - Events: tutorials/roblox/events.md + - Change Events: tutorials/roblox/change-events.md + - Outputs: tutorials/roblox/outputs.md + - References: tutorials/roblox/references.md + - Best Practices: + - Components: tutorials/best-practices/components.md + - Instance Handling: tutorials/best-practices/instance-handling.md + - Callbacks: tutorials/best-practices/callbacks.md + - State: tutorials/best-practices/state.md + - Sharing Values: tutorials/best-practices/sharing-values.md + - Error Safety: tutorials/best-practices/error-safety.md + - Optimisation: tutorials/best-practices/optimisation.md + + - Examples: + - Home: examples/index.md + - Cookbook: + - examples/cookbook/index.md + - Player List: examples/cookbook/player-list.md + - Animated Computed: examples/cookbook/animated-computed.md + - Fetch Data From Server: examples/cookbook/fetch-data-from-server.md + - Light & Dark Theme: examples/cookbook/light-and-dark-theme.md + - Button Component: examples/cookbook/button-component.md + - Loading Spinner: examples/cookbook/loading-spinner.md + - Drag & Drop: examples/cookbook/drag-and-drop.md + - API Reference: + - api-reference/index.md + - General: + - Errors: api-reference/general/errors.md + - Types: + - Contextual: api-reference/general/types/contextual.md + - Version: api-reference/general/types/version.md + - Members: + - Contextual: api-reference/general/members/contextual.md + - Safe: api-reference/general/members/safe.md + - version: api-reference/general/members/version.md + - Memory: + - Types: + - Scope: api-reference/memory/types/scope.md + - ScopedObject: api-reference/memory/types/scopedobject.md + - Task: api-reference/memory/types/task.md + - Members: + - deriveScope: api-reference/memory/members/derivescope.md + - doCleanup: api-reference/memory/members/docleanup.md + - scoped: api-reference/memory/members/scoped.md + - State: + - Types: + - UsedAs: api-reference/state/types/usedas.md + - Computed: api-reference/state/types/computed.md + - Dependency: api-reference/state/types/dependency.md + - Dependent: api-reference/state/types/dependent.md + - For: api-reference/state/types/for.md + - Observer: api-reference/state/types/observer.md + - StateObject: api-reference/state/types/stateobject.md + - Use: api-reference/state/types/use.md + - Value: api-reference/state/types/value.md + - Members: + - Computed: api-reference/state/members/computed.md + - ForKeys: api-reference/state/members/forkeys.md + - ForPairs: api-reference/state/members/forpairs.md + - ForValues: api-reference/state/members/forvalues.md + - Observer: api-reference/state/members/observer.md + - peek: api-reference/state/members/peek.md + - Value: api-reference/state/members/value.md + - Roblox: + - Types: + - Child: api-reference/roblox/types/child.md + - PropertyTable: api-reference/roblox/types/propertytable.md + - SpecialKey: api-reference/roblox/types/specialkey.md + - Members: + - Attribute: api-reference/roblox/members/attribute.md + - AttributeChange: api-reference/roblox/members/attributechange.md + - AttributeOut: api-reference/roblox/members/attributeout.md + - Children: api-reference/roblox/members/children.md + - Hydrate: api-reference/roblox/members/hydrate.md + - New: api-reference/roblox/members/new.md + - OnChange: api-reference/roblox/members/onchange.md + - OnEvent: api-reference/roblox/members/onevent.md + - Out: api-reference/roblox/members/out.md + - Ref: api-reference/roblox/members/ref.md + - Animation: + - Types: + - Animatable: api-reference/animation/types/animatable.md + - Spring: api-reference/animation/types/spring.md + - Tween: api-reference/animation/types/tween.md + - Members: + - Tween: api-reference/animation/members/tween.md + - Spring: api-reference/animation/members/spring.md + - Extras: + - Home: extras/index.md + - Backgrounds: extras/backgrounds.md + - Brand Guidelines: extras/brand-guidelines.md + +markdown_extensions: + - admonition + - attr_list + - meta + - md_in_html + - pymdownx.superfences + - pymdownx.betterem + - pymdownx.details + - pymdownx.tabbed: + alternate_style: true + - pymdownx.inlinehilite + - toc: + permalink: true + - pymdownx.highlight: + guess_lang: false + - pymdownx.emoji: + emoji_index: !!python/name:materialx.emoji.twemoji + emoji_generator: !!python/name:materialx.emoji.to_svg diff --git a/tests/world.lua b/tests/world.lua index f0eff7d..79a2686 100644 --- a/tests/world.lua +++ b/tests/world.lua @@ -1,5 +1,5 @@ -local testkit = require("../testkit") local jecs = require("../lib/init") +local testkit = require("../testkit") local __ = jecs.Wildcard local ECS_ID, ECS_GENERATION = jecs.ECS_ID, jecs.ECS_GENERATION local ECS_GENERATION_INC = jecs.ECS_GENERATION_INC @@ -11,329 +11,343 @@ local ECS_PAIR_OBJECT = jecs.ECS_PAIR_OBJECT local TEST, CASE, CHECK, FINISH, SKIP = testkit.test() local function CHECK_NO_ERR(s: string, fn: (T...) -> (), ...: T...) - local ok, err: string? = pcall(fn, ...) + local ok, err: string? = pcall(fn, ...) - if not CHECK(not ok, 2) then - local i = string.find(err :: string, " ") - assert(i) - local msg = string.sub(err :: string, i+1) - CHECK(msg == s, 2) - end + if not CHECK(not ok, 2) then + local i = string.find(err :: string, " ") + assert(i) + local msg = string.sub(err :: string, i + 1) + CHECK(msg == s, 2) + end end local N = 10 -TEST("world", function() - do CASE "should be iterable" - local world = jecs.World.new() - local A = world:component() - local B = world:component() - local eA = world:entity() - world:set(eA, A, true) - local eB = world:entity() - world:set(eB, B, true) - local eAB = world:entity() - world:set(eAB, A, true) - world:set(eAB, B, true) +TEST("world", function() + do + CASE("should be iterable") + local world = jecs.World.new() + local A = world:component() + local B = world:component() + local eA = world:entity() + world:set(eA, A, true) + local eB = world:entity() + world:set(eB, B, true) + local eAB = world:entity() + world:set(eAB, A, true) + world:set(eAB, B, true) - local count = 0 - for id, data in world do - count += 1 - if id == eA then - CHECK(data[A] == true) - CHECK(data[B] == nil) - elseif id == eB then - CHECK(data[A] == nil) - CHECK(data[B] == true) - elseif id == eAB then - CHECK(data[A] == true) - CHECK(data[B] == true) - end - end + local count = 0 + for id, data in world do + count += 1 + if id == eA then + CHECK(data[A] == true) + CHECK(data[B] == nil) + elseif id == eB then + CHECK(data[A] == nil) + CHECK(data[B] == true) + elseif id == eAB then + CHECK(data[A] == true) + CHECK(data[B] == true) + end + end - -- components are registered in the entity index as well - -- so this test has to add 2 to account for them - CHECK(count == 3 + 2) - end + -- components are registered in the entity index as well + -- so this test has to add 2 to account for them + CHECK(count == 3 + 2) + end - do CASE "should query all matching entities" - local world = jecs.World.new() - local A = world:component() - local B = world:component() + do + CASE("should query all matching entities") + local world = jecs.World.new() + local A = world:component() + local B = world:component() - local entities = {} - for i = 1, N do - local id = world:entity() + local entities = {} + for i = 1, N do + local id = world:entity() - world:set(id, A, true) - if i > 5 then world:set(id, B, true) end - entities[i] = id - end + world:set(id, A, true) + if i > 5 then + world:set(id, B, true) + end + entities[i] = id + end - for id in world:query(A) do - table.remove(entities, CHECK(table.find(entities, id))) - end + for id in world:query(A) do + table.remove(entities, CHECK(table.find(entities, id))) + end - CHECK(#entities == 0) + CHECK(#entities == 0) + end - end + do + CASE("should query all matching entities when irrelevant component is removed") + local world = jecs.World.new() + local A = world:component() + local B = world:component() + local C = world:component() - do CASE "should query all matching entities when irrelevant component is removed" - local world = jecs.World.new() - local A = world:component() - local B = world:component() - local C = world:component() + local entities = {} + for i = 1, N do + local id = world:entity() - local entities = {} - for i = 1, N do - local id = world:entity() + -- specifically put them in disorder to track regression + -- https://github.com/Ukendio/jecs/pull/15 + world:set(id, B, true) + world:set(id, A, true) + if i > 5 then + world:remove(id, B) + end + entities[i] = id + end - -- specifically put them in disorder to track regression - -- https://github.com/Ukendio/jecs/pull/15 - world:set(id, B, true) - world:set(id, A, true) - if i > 5 then world:remove(id, B) end - entities[i] = id - end + local added = 0 + for id in world:query(A) do + added += 1 + table.remove(entities, CHECK(table.find(entities, id))) + end - local added = 0 - for id in world:query(A) do - added += 1 - table.remove(entities, CHECK(table.find(entities, id))) - end + CHECK(added == N) + end - CHECK(added == N) - end + do + CASE("should query all entities without B") + local world = jecs.World.new() + local A = world:component() + local B = world:component() - do CASE "should query all entities without B" - local world = jecs.World.new() - local A = world:component() - local B = world:component() + local entities = {} + for i = 1, N do + local id = world:entity() - local entities = {} - for i = 1, N do - local id = world:entity() + world:set(id, A, true) + if i < 5 then + entities[i] = id + else + world:set(id, B, true) + end + end - world:set(id, A, true) - if i < 5 then - entities[i] = id - else - world:set(id, B, true) - end - - end + for id in world:query(A):without(B) do + table.remove(entities, CHECK(table.find(entities, id))) + end - for id in world:query(A):without(B) do - table.remove(entities, CHECK(table.find(entities, id))) - end + CHECK(#entities == 0) + end - CHECK(#entities == 0) + do + CASE("should allow setting components in arbitrary order") + local world = jecs.World.new() - end + local Health = world:entity() + local Poison = world:component() - do CASE "should allow setting components in arbitrary order" - local world = jecs.World.new() + local id = world:entity() + world:set(id, Poison, 5) + world:set(id, Health, 50) - local Health = world:entity() - local Poison = world:component() + CHECK(world:get(id, Poison) == 5) + end - local id = world:entity() - world:set(id, Poison, 5) - world:set(id, Health, 50) + do + CASE("should allow deleting components") + local world = jecs.World.new() - CHECK(world:get(id, Poison) == 5) - end + local Health = world:entity() + local Poison = world:component() - do CASE "should allow deleting components" - local world = jecs.World.new() + local id = world:entity() + world:set(id, Poison, 5) + world:set(id, Health, 50) + local id1 = world:entity() + world:set(id1, Poison, 500) + world:set(id1, Health, 50) - local Health = world:entity() - local Poison = world:component() + world:delete(id) - local id = world:entity() - world:set(id, Poison, 5) - world:set(id, Health, 50) - local id1 = world:entity() - world:set(id1, Poison, 500) - world:set(id1, Health, 50) + CHECK(world:get(id, Poison) == nil) + CHECK(world:get(id, Health) == nil) + CHECK(world:get(id1, Poison) == 500) + CHECK(world:get(id1, Health) == 50) + end - world:delete(id) + do + CASE("should allow remove that doesn't exist on entity") + local world = jecs.World.new() - CHECK(world:get(id, Poison) == nil) - CHECK(world:get(id, Health) == nil) - CHECK(world:get(id1, Poison) == 500) - CHECK(world:get(id1, Health) == 50) + local Health = world:entity() + local Poison = world:component() - end + local id = world:entity() + world:set(id, Health, 50) + world:remove(id, Poison) - do CASE "should allow remove that doesn't exist on entity" - local world = jecs.World.new() + CHECK(world:get(id, Poison) == nil) + CHECK(world:get(id, Health) == 50) + end - local Health = world:entity() - local Poison = world:component() + do + CASE("should increment generation") + local world = jecs.World.new() + local e = world:entity() + CHECK(ECS_ID(e) == 1 + jecs.Rest) + CHECK(getAlive(world.entityIndex, ECS_ID(e)) == e) + CHECK(ECS_GENERATION(e) == 0) -- 0 + e = ECS_GENERATION_INC(e) + CHECK(ECS_GENERATION(e) == 1) -- 1 + end - local id = world:entity() - world:set(id, Health, 50) - world:remove(id, Poison) + do + CASE("should get alive from index in the dense array") + local world = jecs.World.new() + local _e = world:entity() + local e2 = world:entity() + local e3 = world:entity() - CHECK(world:get(id, Poison) == nil) - CHECK(world:get(id, Health) == 50) - end + CHECK(IS_PAIR(world:entity()) == false) - do CASE "should increment generation" - local world = jecs.World.new() - local e = world:entity() - CHECK(ECS_ID(e) == 1 + jecs.Rest) - CHECK(getAlive(world.entityIndex, ECS_ID(e)) == e) - CHECK(ECS_GENERATION(e) == 0) -- 0 - e = ECS_GENERATION_INC(e) - CHECK(ECS_GENERATION(e) == 1) -- 1 - end + local pair = ECS_PAIR(e2, e3) + CHECK(IS_PAIR(pair) == true) + CHECK(ECS_PAIR_RELATION(world.entityIndex, pair) == e2) + CHECK(ECS_PAIR_OBJECT(world.entityIndex, pair) == e3) + end - do CASE "should get alive from index in the dense array" - local world = jecs.World.new() - local _e = world:entity() - local e2 = world:entity() - local e3 = world:entity() + do + CASE("should allow querying for relations") + local world = jecs.World.new() + local Eats = world:entity() + local Apples = world:entity() + local bob = world:entity() - CHECK(IS_PAIR(world:entity()) == false) + world:set(bob, ECS_PAIR(Eats, Apples), true) + for e, bool in world:query(ECS_PAIR(Eats, Apples)) do + CHECK(e == bob) + CHECK(bool) + end + end - local pair = ECS_PAIR(e2, e3) - CHECK(IS_PAIR(pair) == true) - CHECK(ECS_PAIR_RELATION(world.entityIndex, pair) == e2) - CHECK(ECS_PAIR_OBJECT(world.entityIndex, pair) == e3) - end + do + CASE("should allow wildcards in queries") + local world = jecs.World.new() + local Eats = world:entity() + local Apples = world:entity() + local bob = world:entity() - do CASE "should allow querying for relations" - local world = jecs.World.new() - local Eats = world:entity() - local Apples = world:entity() - local bob = world:entity() - - world:set(bob, ECS_PAIR(Eats, Apples), true) - for e, bool in world:query(ECS_PAIR(Eats, Apples)) do - CHECK(e == bob) - CHECK(bool) - end - end - - do CASE "should allow wildcards in queries" - local world = jecs.World.new() - local Eats = world:entity() - local Apples = world:entity() - local bob = world:entity() - - world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples") - - local w = jecs.Wildcard - for e, data in world:query(ECS_PAIR(Eats, w)) do - CHECK(e == bob) - CHECK(data == "bob eats apples") - end - for e, data in world:query(ECS_PAIR(w, Apples)) do - CHECK(e == bob) - CHECK(data == "bob eats apples") - end - end + world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples") - do CASE "should match against multiple pairs" - local world = jecs.World.new() - local Eats = world:entity() - local Apples = world:entity() - local Oranges =world:entity() - local bob = world:entity() - local alice = world:entity() - - world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples") - world:set(alice, ECS_PAIR(Eats, Oranges), "alice eats oranges") - - local w = jecs.Wildcard - local count = 0 - for e, data in world:query(ECS_PAIR(Eats, w)) do - count += 1 - if e == bob then - CHECK(data == "bob eats apples") - else - CHECK(data == "alice eats oranges") - end - end + local w = jecs.Wildcard + for e, data in world:query(ECS_PAIR(Eats, w)) do + CHECK(e == bob) + CHECK(data == "bob eats apples") + end + for e, data in world:query(ECS_PAIR(w, Apples)) do + CHECK(e == bob) + CHECK(data == "bob eats apples") + end + end - CHECK(count == 2) - count = 0 + do + CASE("should match against multiple pairs") + local world = jecs.World.new() + local Eats = world:entity() + local Apples = world:entity() + local Oranges = world:entity() + local bob = world:entity() + local alice = world:entity() - for e, data in world:query(ECS_PAIR(w, Apples)) do - count += 1 - CHECK(data == "bob eats apples") - end - CHECK(count == 1) - end + world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples") + world:set(alice, ECS_PAIR(Eats, Oranges), "alice eats oranges") - do CASE "should only relate alive entities" - - local world = jecs.World.new() - local Eats = world:entity() - local Apples = world:entity() - local Oranges = world:entity() - local bob = world:entity() - local alice = world:entity() - - world:set(bob, Apples, "apples") - world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples") - world:set(alice, ECS_PAIR(Eats, Oranges), "alice eats oranges") + local w = jecs.Wildcard + local count = 0 + for e, data in world:query(ECS_PAIR(Eats, w)) do + count += 1 + if e == bob then + CHECK(data == "bob eats apples") + else + CHECK(data == "alice eats oranges") + end + end - world:delete(Apples) - local Wildcard = jecs.Wildcard - - local count = 0 - for _, data in world:query(ECS_PAIR(Wildcard, Apples)) do - count += 1 - end + CHECK(count == 2) + count = 0 - world:delete(ECS_PAIR(Eats, Apples)) - - CHECK(count == 0) - CHECK(world:get(bob, ECS_PAIR(Eats, Apples)) == nil) - end + for e, data in world:query(ECS_PAIR(w, Apples)) do + count += 1 + CHECK(data == "bob eats apples") + end + CHECK(count == 1) + end - do CASE "should error when setting invalid pair" - local world = jecs.World.new() - local Eats = world:entity() - local Apples = world:entity() - local bob = world:entity() + do + CASE("should only relate alive entities") - world:delete(Apples) + local world = jecs.World.new() + local Eats = world:entity() + local Apples = world:entity() + local Oranges = world:entity() + local bob = world:entity() + local alice = world:entity() - CHECK_NO_ERR("Apples should be dead", function() - world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples") - end) - end + world:set(bob, Apples, "apples") + world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples") + world:set(alice, ECS_PAIR(Eats, Oranges), "alice eats oranges") - do CASE "should find target for ChildOf" - local world = jecs.World.new() + world:delete(Apples) + local Wildcard = jecs.Wildcard - local ChildOf = world:component() - local Name = world:component() + local count = 0 + for _, data in world:query(ECS_PAIR(Wildcard, Apples)) do + count += 1 + end - local function parent(entity) - return world:target(entity, ChildOf) - end + world:delete(ECS_PAIR(Eats, Apples)) - local bob = world:entity() - local alice = world:entity() - local sara = world:entity() - - world:add(bob, ECS_PAIR(ChildOf, alice)) - world:set(bob, Name, "bob") - world:add(sara, ECS_PAIR(ChildOf, alice)) - world:set(sara, Name, "sara") - CHECK(parent(bob) == alice) -- O(1) + CHECK(count == 0) + CHECK(world:get(bob, ECS_PAIR(Eats, Apples)) == nil) + end - local count = 0 - for _, name in world:query(Name, ECS_PAIR(ChildOf, alice)) do - print(name) - count += 1 - end - CHECK(count == 2) - end + do + CASE("should error when setting invalid pair") + local world = jecs.World.new() + local Eats = world:entity() + local Apples = world:entity() + local bob = world:entity() + + world:delete(Apples) + + world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples") + end + + do + CASE("should find target for ChildOf") + local world = jecs.World.new() + + local ChildOf = world:component() + local Name = world:component() + + local function parent(entity) + return world:target(entity, ChildOf) + end + + local bob = world:entity() + local alice = world:entity() + local sara = world:entity() + + world:add(bob, ECS_PAIR(ChildOf, alice)) + world:set(bob, Name, "bob") + world:add(sara, ECS_PAIR(ChildOf, alice)) + world:set(sara, Name, "sara") + CHECK(parent(bob) == alice) -- O(1) + + local count = 0 + for _, name in world:query(Name, ECS_PAIR(ChildOf, alice)) do + print(name) + count += 1 + end + CHECK(count == 2) + end end) -FINISH() \ No newline at end of file +FINISH() +