#include "ScriptTest.h" #include "zeroerr/assert.h" #include "zeroerr/unittest.h" static const char* kScriptEcsContent = R"SQ( function testEcsReady() { return eve.ecsReady() } function testDefineClassAndComponent() { class Position extends eve.Component { x = 0.0 y = 0.0 } class Velocity extends eve.Component { x = 1.0 y = 2.0 } class Moveable extends eve.Entity { pos = Position vel = Velocity } local e = Moveable.create() if (e == null) return false if (!e.isAlive()) return false if (e.getId() <= 0) return false if (e.pos.x != 0.0 || e.pos.y != 0.0) return false if (e.vel.x != 1.0 || e.vel.y != 2.0) return false e.pos.x = 10.0 e.vel.x = 3.0 if (e.pos.x != 10.0) return false if (e.getComponent(Position).x != 10.0) return false if (!e.hasComponent(Velocity)) return false return true } function testViewIncludesSubclass() { class Health extends eve.Component { hp = 100 } class Actor extends eve.Entity { hp = Health } class Enemy extends Actor { } local a = Actor.create() local b = Enemy.create() a.hp.hp = 50 b.hp.hp = 80 local actors = eve.view(Actor) if (actors.len() < 2) return false local enemies = eve.view(Enemy) if (enemies.len() < 1) return false local foundEnemy = false foreach (e in actors) { if (e == b) foundEnemy = true } if (!foundEnemy) return false return true } function testSystemUpdate() { class Position extends eve.Component { x = 0.0 } class Velocity extends eve.Component { x = 5.0 } class Mover extends eve.Entity { pos = Position vel = Velocity } class MoveSys extends eve.System { constructor() { base.constructor(Mover) } function update(dt) { foreach (e in entities()) { e.pos.x += e.vel.x * dt } } } local e = Mover.create() local sys = MoveSys() sys.update(2.0) if (e.pos.x != 10.0) return false e.destroy() if (e.isAlive()) return false // destroyed entities leave the view foreach (ent in eve.view(Mover)) { if (ent == e) return false } return true } function testSystemDynamicSpawnDespawn() { class Position extends eve.Component { x = 0.0 } class Velocity extends eve.Component { x = 1.0 } class Mover extends eve.Entity { pos = Position vel = Velocity } class MoveSys extends eve.System { constructor() { base.constructor(Mover) } function update(dt) { local dying = [] foreach (e in entities()) { e.pos.x += e.vel.x * dt if (e.pos.x >= 2.0) dying.push(e) } foreach (e in dying) e.destroy() } } local sys = MoveSys() local a = Mover.create() local b = Mover.create() a.vel.x = 1.0 b.vel.x = 1.0 if (eve.view(Mover).len() != 2) return false sys.update(1.0) if (a.pos.x != 1.0 || b.pos.x != 1.0) return false if (eve.view(Mover).len() != 2) return false sys.update(1.0) if (a.isAlive() || b.isAlive()) return false if (eve.view(Mover).len() != 0) return false local c = Mover.create() c.vel.x = 1.0 sys.update(0.5) if (!c.isAlive() || c.pos.x != 0.5) return false if (eve.view(Mover).len() != 1) return false // A later entities() call in the same update sees newly created instances. class SpawnSys extends eve.System { snapshot = 0 constructor() { base.constructor(Mover) } function update(dt) { snapshot = entities().len() local extra = Mover.create() extra.pos.x = 99.0 if (entities().len() != snapshot + 1) return false foreach (e in entities()) { if (e.pos.x >= 2.0) e.destroy() } return true } } local spawnSys = SpawnSys() if (!spawnSys.update(0.0)) return false if (spawnSys.snapshot != 1) return false if (eve.view(Mover).len() != 1) return false foreach (e in eve.view(Mover)) { if (e != c) return false e.destroy() } return eve.view(Mover).len() == 0 } function testSystemQueryArrayAndSubclass() { class Hp extends eve.Component { v = 1 } class Actor extends eve.Entity { hp = Hp } class Enemy extends Actor {} class Prop extends eve.Entity { hp = Hp } class TickSys extends eve.System { constructor() { base.constructor([Actor, Prop]) } function update(dt) { foreach (e in entities()) e.hp.v += 1 } } local actor = Actor.create() local enemy = Enemy.create() local prop = Prop.create() local sys = TickSys() sys.update(0.0) if (actor.hp.v != 2 || enemy.hp.v != 2 || prop.hp.v != 2) return false sys.setQuery(Enemy) sys.update(0.0) if (actor.hp.v != 2 || enemy.hp.v != 3 || prop.hp.v != 2) return false local emptySys = eve.System() if (emptySys.entities().len() != 0) return false actor.destroy() enemy.destroy() prop.destroy() return true } function testTypeMarkersAndEntityContainer() { class Status extends eve.Component { alive = eve.Boolean name = eve.String score = eve.Number } class Thing extends eve.EntityContainer { status = Status } local t = Thing.create() if (t.status.alive != false) return false if (t.status.name != "") return false if (t.status.score != 0.0) return false t.status.alive = true t.status.name = "orb" t.status.score = 3.5 if (!t.status.alive || t.status.name != "orb" || t.status.score != 3.5) return false return eve.EntityContainer == eve.Entity } function testStaticComponentsTable() { class Pos extends eve.Component { x = 1.0 } class Node extends eve.Entity { static components = { pos = Pos } pos = null } local n = Node.create() if (n.pos == null) return false if (n.pos.x != 1.0) return false return true } function testShaderSystemClassExists() { if (!("ShaderSystem" in eve)) return false // Construction without GPU is allowed; update() no-ops until setGpgpu. class Pos extends eve.Component { x = 0.0; y = 0.0 } class Vel extends eve.Component { x = 1.0; y = 0.0 } class Mover extends eve.Entity { pos = Pos; vel = Vel } local sys = eve.ShaderSystem(Mover) sys.bindFields(0, "pos", ["x", "y"]) sys.bindFields(1, "vel", ["x", "y"]) sys.update(0.016) return true } function testViewCacheStableAndInvalidated() { class Pos extends eve.Component { x = 0.0 } class Node extends eve.Entity { pos = Pos } class Sub extends Node {} local a = Node.create() local b = Sub.create() // No changes between calls → same cached array identity (zero allocation // per frame in System.entities()). local v1 = eve.view(Node) if (v1.len() != 2) return false local v2 = eve.view(Node) if (v1 != v2) return false local sv1 = eve.view(Sub) if (sv1.len() != 1) return false // create() invalidates the cached view (and subclass views via the chain) local c = Node.create() if (eve.view(Node).len() != 3) return false if (eve.view(Sub).len() != 1) return false // destroy() invalidates too; dead entities leave the view immediately a.destroy() b.destroy() c.destroy() if (eve.view(Node).len() != 0) return false return true } function testViewCacheBaseClassIncludesSubclasses() { class Pos extends eve.Component { x = 0.0 } class Node extends eve.Entity { pos = Pos } class Sub extends Node {} local n = Node.create() local s = Sub.create() if (eve.view(Node).len() != 2) return false if (eve.view(eve.Entity).len() < 2) return false n.destroy() s.destroy() return true } function testComponentDefaultsAndSlotsCached() { class C extends eve.Component { v = eve.Number; s = eve.String } class E extends eve.Entity { c = C } local a = E.create() if (a.c.v != 0.0 || a.c.s != "") return false a.c.v = 5.0 // Second instance gets fresh resolved defaults, not the mutated values. local b = E.create() if (b.c.v != 0.0 || b.c.s != "") return false if (a.c.v != 5.0) return false if (a.getComponent(C) != a.c) return false if (!a.hasComponent(C)) return false a.destroy() b.destroy() return true } )SQ"; UnitSciptTest(ScriptEcsTest, kScriptEcsContent); TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.ready") { CHECK(vm.callFunc(vm.findFunc("testEcsReady"), vm).toBool()); } TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.defineClassAndComponent") { CHECK(vm.callFunc(vm.findFunc("testDefineClassAndComponent"), vm).toBool()); } TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.viewIncludesSubclass") { CHECK(vm.callFunc(vm.findFunc("testViewIncludesSubclass"), vm).toBool()); } TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.systemUpdate") { CHECK(vm.callFunc(vm.findFunc("testSystemUpdate"), vm).toBool()); } TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.systemDynamicSpawnDespawn") { CHECK(vm.callFunc(vm.findFunc("testSystemDynamicSpawnDespawn"), vm).toBool()); } TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.systemQueryArrayAndSubclass") { CHECK(vm.callFunc(vm.findFunc("testSystemQueryArrayAndSubclass"), vm).toBool()); } TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.typeMarkersAndEntityContainer") { CHECK(vm.callFunc(vm.findFunc("testTypeMarkersAndEntityContainer"), vm).toBool()); } TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.staticComponentsTable") { CHECK(vm.callFunc(vm.findFunc("testStaticComponentsTable"), vm).toBool()); } TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.shaderSystemClassExists") { CHECK(vm.callFunc(vm.findFunc("testShaderSystemClassExists"), vm).toBool()); } TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.viewCacheStableAndInvalidated") { CHECK(vm.callFunc(vm.findFunc("testViewCacheStableAndInvalidated"), vm).toBool()); } TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.viewCacheBaseClassIncludesSubclasses") { CHECK(vm.callFunc(vm.findFunc("testViewCacheBaseClassIncludesSubclasses"), vm).toBool()); } TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.componentDefaultsAndSlotsCached") { CHECK(vm.callFunc(vm.findFunc("testComponentDefaultsAndSlotsCached"), vm).toBool()); }