/** * An asset as a value (#237): Spawn and Is asset take it on a port, fed by the Asset literal, an Asset * variable or a list item – and a pick saved before the setting became a port still spawns. Plus * Child by name, which finds the nearest object of a name below another. */ import { describe, expect, it } from 'vitest'; import { CreateAssetCommand } from '../../src/core/commands/assetCommands'; import { AddVariableCommand } from '../../src/core/commands/variableCommands'; import { isKindWith, VARIABLE_KINDS } from '../../src/core/properties/kinds'; import { tagged } from '../../src/core/model/Properties'; import { makeBehavior, graphEditor, makeSpriteScene, place, roundTrip, startWorld, type SceneFixture, } from '../helpers'; function sprite(f: SceneFixture, name: string): string { const command = new CreateAssetCommand('sprite', name); f.stack.execute(command); return command.asset.id; } describe('the asset kind', () => { it('can be declared, picked and tagged in a list', () => { expect(VARIABLE_KINDS).toContain('asset'); for (const role of ['field', 'port', 'tagged', 'reference']) expect(isKindWith(role, 'asset')).toBe(true); }); it('keeps an Asset variable and its value through saving', () => { const f = makeSpriteScene('Crate'); const behavior = makeBehavior(f, 'Spawner'); f.stack.execute(new AddVariableCommand(behavior.id, 'what', 'asset')); behavior.props.what = f.spriteId; const loaded = roundTrip(f.project).assets.find((a) => a.id === behavior.id)!; expect(loaded.type === 'behavior' && loaded.variables.find((v) => v.key === 'what')?.kind).toBe('asset'); expect(loaded.props.what).toBe(f.spriteId); }); }); describe('Spawn with an asset input', () => { const spawnedOf = (f: SceneFixture, world: ReturnType['world']) => world.entities.filter((e) => e.assetId === f.spriteId).length; it('places the asset an Asset literal hands it over an edge', () => { const f = makeSpriteScene('Crate'); const behavior = makeBehavior(f, 'Spawner'); const edit = graphEditor(f, behavior); const start = edit.addNode('OnStart'); const literal = edit.addNode('Asset', { value: f.spriteId }); const spawn = edit.addNode('Spawn'); edit.wire([ [start, 'exec', spawn, 'exec'], [literal, 'value', spawn, 'asset'], ]); place(f, behavior.id, { x: 0, y: 0 }); const run = startWorld(f, { behaviors: true }); run.frame(); expect(spawnedOf(f, run.world)).toBe(1); }); it('places the asset a list item tagged $asset names', () => { const f = makeSpriteScene('Crate'); const behavior = makeBehavior(f, 'Spawner'); const edit = graphEditor(f, behavior); const start = edit.addNode('OnStart'); const list = edit.addNode('List', { value: [tagged('asset', f.spriteId)] }); const item = edit.addNode('ItemAt'); const spawn = edit.addNode('Spawn'); edit.wire([ [start, 'exec', spawn, 'exec'], [list, 'value', item, 'list'], [item, 'item', spawn, 'asset'], ]); place(f, behavior.id, { x: 0, y: 0 }); const run = startWorld(f, { behaviors: true }); run.frame(); expect(spawnedOf(f, run.world)).toBe(1); }); it('still spawns what a project saved before the pick became a port', () => { const f = makeSpriteScene('Crate'); const behavior = makeBehavior(f, 'Spawner'); const edit = graphEditor(f, behavior); const start = edit.addNode('OnStart'); // The old setting and the new inline value share the key `asset`. const spawn = edit.addNode('Spawn', { asset: f.spriteId }); edit.wire([[start, 'exec', spawn, 'exec']]); place(f, behavior.id, { x: 0, y: 0 }); const loaded = { ...f, project: roundTrip(f.project) }; const run = startWorld(loaded, { behaviors: true }); run.frame(); expect(spawnedOf(loaded, run.world)).toBe(1); }); }); describe('Child by name', () => { /** * Hitbox (top level, first in the scene) · Player › Weapon › Hitbox · Player › Weapon › Tag · * Player › Tag (placed after the deeper one). */ function scene() { const f = makeSpriteScene('Player'); const hitbox = sprite(f, 'Hitbox'); const weapon = sprite(f, 'Weapon'); const tag = sprite(f, 'Tag'); const loose = place(f, hitbox, { x: 0, y: 0 }); const player = place(f, f.spriteId, { x: 0, y: 0 }); const blade = place(f, weapon, { x: 0, y: 0 }, player); const inner = place(f, hitbox, { x: 0, y: 0 }, blade); const deepTag = place(f, tag, { x: 0, y: 0 }, blade); const nearTag = place(f, tag, { x: 0, y: 0 }, player); return { f, loose, player, inner, deepTag, nearTag }; } it('finds a grandchild below the object, not a namesake elsewhere', () => { const { f, loose, player, inner } = scene(); const { world } = startWorld(f); const found = world.childByName(world.findByObjectId(player)!, 'Hitbox'); expect(found?.objectId).toBe(inner); expect(world.findByName('Hitbox')?.objectId).toBe(loose); }); it('takes a child before a grandchild of the same name', () => { const { f, player, nearTag } = scene(); const { world } = startWorld(f); expect(world.childByName(world.findByObjectId(player)!, 'Tag')?.objectId).toBe(nearTag); }); it('finds nothing above or beside the object', () => { const { f, inner } = scene(); const { world } = startWorld(f); expect(world.childByName(world.findByObjectId(inner)!, 'Player')).toBeUndefined(); expect(world.childByName(world.findByObjectId(inner)!, 'Hitbox')).toBeUndefined(); }); it('hands the found child on in a running graph', () => { const { f, loose, player, inner } = scene(); const behavior = makeBehavior(f, 'Cleaner'); const edit = graphEditor(f, behavior); const start = edit.addNode('OnStart'); const owner = edit.addNode('Owner'); const child = edit.addNode('ChildByName', { name: 'Hitbox' }); const destroy = edit.addNode('Destroy'); edit.wire([ [start, 'exec', destroy, 'exec'], [owner, 'object', child, 'object'], [child, 'child', destroy, 'object'], ]); place(f, behavior.id, { x: 0, y: 0 }, player); const run = startWorld(f, { behaviors: true }); run.frame(); expect(run.world.findByObjectId(inner)).toBeUndefined(); expect(run.world.findByObjectId(loose)).toBeDefined(); }); });