import { describe, it, expect } from 'bun:test'; import { World, Component, COMPONENT_KEY } from '@common/rpg/core/world'; import { component } from '@common/rpg/utils/decorators'; import { Serialization } from '@common/rpg/core/serialization'; import { Stat } from '@common/rpg/components/stat'; import { Effect } from '@common/rpg/components/effect'; import { EffectSystem } from '@common/rpg/systems/effect'; // ---------- helpers ---------- function roundtrip(world: World): World { return Serialization.deserialize(Serialization.serialize(world)) as World; } function isSymbolKeyed(world: World, entityId: string, ctor: new (...a: any[]) => Component): boolean { const entity = world.getEntity(entityId)!; for (const [, component] of entity) { if (component instanceof ctor) { return typeof component[COMPONENT_KEY] === 'symbol'; } } return false; } // ---------- named-key round-trip ---------- describe('Serialization — named key', () => { it('preserves a string-keyed component', () => { const w = new World(); const e = w.createEntity('player'); e.add(new Stat({ value: 42 }), 'str'); const w2 = roundtrip(w); expect(w2.getEntity('player')!.get(Stat, 'str')!.value).toBe(42); }); it('preserves two components with different named keys', () => { const w = new World(); const e = w.createEntity('player'); e.add(new Stat({ value: 10 }), 'str'); e.add(new Stat({ value: 5 }), 'int'); const w2 = roundtrip(w); const e2 = w2.getEntity('player')!; expect(e2.get(Stat, 'str')!.value).toBe(10); expect(e2.get(Stat, 'int')!.value).toBe(5); }); }); // ---------- symbol-key round-trip ---------- describe('Serialization — anonymous (symbol) key', () => { it('restores a symbol-keyed component as symbol-keyed', () => { const w = new World(); w.createEntity('e').add(new Stat({ value: 7 })); // no key → Symbol const w2 = roundtrip(w); expect(isSymbolKeyed(w2, 'e', Stat)).toBeTrue(); }); it('restored symbol-keyed component is found by type lookup', () => { const w = new World(); const e = w.createEntity('e'); e.add(new Stat({ value: 7 })); const w2 = roundtrip(w); expect(w2.getEntity('e')!.get(Stat)!.value).toBe(7); }); it('two anonymous components of the same type both survive round-trip', () => { const w = new World(); const e = w.createEntity('e'); e.add(new Stat({ value: 3 })); e.add(new Stat({ value: 9 })); const w2 = roundtrip(w); const stats = w2.getEntity('e')!.getAll(Stat); expect(stats.length).toBe(2); expect(stats.map(s => s.value).sort()).toEqual([3, 9]); }); it('anonymous and named component of the same type both survive round-trip', () => { const w = new World(); const e = w.createEntity('e'); e.add(new Stat({ value: 10 }), 'str'); e.add(new Stat({ value: 20 })); // Symbol key const w2 = roundtrip(w); const e2 = w2.getEntity('e')!; expect(e2.get(Stat, 'str')!.value).toBe(10); // anonymous one still reachable by type const all = e2.getAll(Stat); expect(all.length).toBe(2); }); }); // ---------- effect + stat round-trip ---------- describe('Serialization — Effect round-trip', () => { it('multiple anonymous Effects both survive round-trip', () => { const w = new World(); w.addSystem(new EffectSystem()); const e = w.createEntity('target'); e.add(new Stat({ value: 10 }), 'str'); e.add(new Effect({ targetKey: 'str', delta: 3 })); e.add(new Effect({ targetKey: 'str', delta: 7 })); const w2 = roundtrip(w); w2.addSystem(new EffectSystem()); expect(w2.getEntity('target')!.getAll(Effect).length).toBe(2); }); it('world globals survive round-trip', () => { const w = new World(); w.globals.score = 99; w.globals.level = 3; const w2 = roundtrip(w); expect(w2.globals.score).toBe(99); expect(w2.globals.level).toBe(3); }); it('entity counter continues from saved value', () => { const w = new World(); w.createEntity(); w.createEntity(); const w2 = roundtrip(w); const e = w2.createEntity(); expect(e.id).toBe('entity_3'); }); });