Give a list one struct type so every item keeps its shape (#254)
A list of structs used to let every item carry its own fields. Adding an item meant typing the same fields again, and nothing kept the items in step with the struct asset from #253. A list can now name a struct type for its items, and the asset keeps them in shape. - A list variable and a list field get the row Items with Mixed, every struct asset and Make struct type. Make struct type collects every field any item has, so no item loses a field. - A typed item carries its type under the key `$struct` (ITEM_TYPE_KEY). listItems does not count it as a field. - A list that holds anything but structs refuses a type, also when the offending item sits in an override. The console names the list and the count, and the list stays as it was. - In a typed list `+ Item` adds an item with the defaults, without the kind menu. Field names are locked, values stay editable. - The rows of a typed item take their kind from the struct asset (declaredKind in InlineList.ts). An asset field shows the asset picker instead of a text field, as reported in the issue. - A struct item in a mixed list gets its own Type row. Switching a list back to Mixed keeps the type on each item. - syncStructTypes brings marked items into shape and carries renames into them, also inside overrides and nested lists. One undo step. - The delete dialog of a struct asset also counts typed lists. - The game never sees the key. World and sharedState strip it with withoutItemTypes and bagWithoutItemTypes, so a struct stays a plain object at runtime. - Add item and Set item get the setting Type. With a type they only accept a struct with exactly its field names (fitsType in SetItem.ts). Anything else is left out and logged. - The item indent from #252 is back to its old width, the narrow one squeezed the value fields. Not done: writing a variable does not check the type, and the List node offers no type. Found on the way: a test helper in list-of-structs.test.ts called nodes without params. It now passes the same arguments as the interpreter. Tests: - ./check.sh: 183 files, 2679 tests, CHECK EXIT 0 - e2e GM_STEPS=1,21,22,32,54,55: ALL PASSED, 150 checks - e2e GM_STEPS=1-6,21,22,32,54,55 before the follow-up: 404 checks - New: tests/core/typed-lists.test.ts, e2e module 55 with 13 checks - Counter-checks bite in unit and browser, originals restored by cmp Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
61bbb96a96
commit
b0d0868f55
25 files changed
+1446
-86
No files matched your search
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { type GraphLookup, NO_LOOKUP } from '../../src/core/graph/lookup';
|
||||
import { AddItem } from '../../src/core/graph/nodes/data/AddItem';
|
||||
import { Fill } from '../../src/core/graph/nodes/data/Fill';
|
||||
import { IndexOf } from '../../src/core/graph/nodes/data/IndexOf';
|
||||
@@ -13,10 +14,18 @@ const cells = (...pairs: [number, number][]) => pairs.map(([x, y]) => ({ x, y })
|
||||
/** A `list` variable holding struct items, the way a seeder writes it. */
|
||||
const shapesDef = (value: unknown) => variableDef('shapes', 'list', value as never);
|
||||
|
||||
/** The node interface only needs `run`; nothing here reads the context. */
|
||||
/** The node interface only needs `run`; nothing here reads the context, and no node has a setting. */
|
||||
const run = (def: { run?: unknown }, inputs: Record<string, unknown>) =>
|
||||
(def.run as (ctx: unknown, i: Record<string, unknown>) => { outputs: Record<string, unknown> })({}, inputs)
|
||||
.outputs;
|
||||
(
|
||||
def.run as (
|
||||
ctx: unknown,
|
||||
i: Record<string, unknown>,
|
||||
params: Record<string, unknown>,
|
||||
def: unknown,
|
||||
entry: string,
|
||||
lookup: GraphLookup,
|
||||
) => { outputs: Record<string, unknown> }
|
||||
)({}, inputs, {}, def, '', NO_LOOKUP).outputs;
|
||||
|
||||
describe('a list whose items are structs', () => {
|
||||
/** Each item keeps the fields it carries: there is no declaration left to fill or drop any. */
|
||||
|
||||
@@ -234,7 +234,7 @@ describe('a struct typed by a Struct asset', () => {
|
||||
const f = makeSpriteScene();
|
||||
const loot = lootType(f);
|
||||
const chest = dropper(f, loot);
|
||||
expect(structTypeUsages(f.project, loot.id)).toEqual({ places: 1, nodes: 0 });
|
||||
expect(structTypeUsages(f.project, loot.id)).toEqual({ places: 1, lists: 0, nodes: 0 });
|
||||
f.stack.execute(new DeleteAssetCommand(loot.id));
|
||||
expect(drop(chest).fields.map((x) => x.key)).toEqual(['Name', 'Amount']);
|
||||
expect(chest.props.drop).toEqual({ Name: 'Coin', Amount: 1 });
|
||||
|
||||
@@ -0,0 +1,360 @@
|
||||
/**
|
||||
* Typed lists (#254): a list naming a Struct asset holds items of that type only, each marked with
|
||||
* it, kept in its shape as the type changes. A struct item in a mixed list may carry a type of its
|
||||
* own. A run never sees the mark, and `Add item` with a type takes a fitting struct only.
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { CreateAssetCommand, SetAssetPropertyCommand } from '../../src/core/commands/assetCommands';
|
||||
import { SetOverrideCommand } from '../../src/core/commands/sceneObjectCommands';
|
||||
import {
|
||||
MakeListItemTypeCommand,
|
||||
SetListItemTypeCommand,
|
||||
SetStructTypeCommand,
|
||||
WithStructTypes,
|
||||
} from '../../src/core/commands/structTypeCommands';
|
||||
import {
|
||||
AddStructFieldCommand,
|
||||
AddVariableCommand,
|
||||
RemoveVariableCommand,
|
||||
RenameVariableCommand,
|
||||
} from '../../src/core/commands/variableCommands';
|
||||
import type { Command } from '../../src/core/commands/Command';
|
||||
import { declaredVariables } from '../../src/core/graph/sharedState';
|
||||
import type { BehaviorAsset, StructAsset } from '../../src/core/model/Asset';
|
||||
import { graphLookup } from '../../src/core/model/graphLookup';
|
||||
import { ITEM_TYPE_KEY, withoutItemTypes } from '../../src/core/model/Properties';
|
||||
import { listItemTypeAt, structTypeUsages, typedItem } from '../../src/core/model/structTypes';
|
||||
import { deserializeProject, serializeProject } from '../../src/core/store/serialize';
|
||||
import {
|
||||
fakeContext,
|
||||
makeBehavior,
|
||||
makeSpriteScene,
|
||||
makeVariables,
|
||||
place,
|
||||
runDef,
|
||||
type SceneFixture,
|
||||
} from '../helpers';
|
||||
|
||||
/** `Loot` = { Name: "Coin", Amount: 1 } */
|
||||
function lootType(f: SceneFixture): StructAsset {
|
||||
const create = new CreateAssetCommand('struct', 'Loot');
|
||||
f.stack.execute(create);
|
||||
const id = create.asset.id;
|
||||
f.stack.execute(new AddVariableCommand(id, 'Name', 'text'));
|
||||
f.stack.execute(new AddVariableCommand(id, 'Amount', 'number'));
|
||||
f.stack.execute(new SetAssetPropertyCommand(id, 'Name', 'Coin'));
|
||||
f.stack.execute(new SetAssetPropertyCommand(id, 'Amount', 1));
|
||||
return create.asset as StructAsset;
|
||||
}
|
||||
|
||||
/** A behavior with a list variable `loots` holding `items`. */
|
||||
function chestWith(f: SceneFixture, items: unknown[]): BehaviorAsset {
|
||||
const chest = makeBehavior(f, 'Chest');
|
||||
f.stack.execute(new AddVariableCommand(chest.id, 'loots', 'list'));
|
||||
f.stack.execute(new SetAssetPropertyCommand(chest.id, 'loots', items as never));
|
||||
return chest;
|
||||
}
|
||||
|
||||
const loots = (chest: BehaviorAsset) => chest.variables.find((v) => v.key === 'loots')!;
|
||||
const target = (chest: BehaviorAsset, path: string[] = []) => ({ ownerId: chest.id, key: 'loots', path });
|
||||
const onType = (
|
||||
f: SceneFixture,
|
||||
command: Command,
|
||||
rename?: ConstructorParameters<typeof WithStructTypes>[1],
|
||||
) => f.stack.execute(new WithStructTypes(command, rename));
|
||||
const rename = (loot: StructAsset, from: string, to: string) => ({ structType: loot.id, path: [], from, to });
|
||||
|
||||
describe('a list typed by a Struct asset', () => {
|
||||
it('brings every item into the type, marked with it, keeping what fits', () => {
|
||||
const f = makeSpriteScene();
|
||||
const loot = lootType(f);
|
||||
const chest = chestWith(f, [{ Name: 'Gem', Old: 4 }, {}]);
|
||||
f.stack.execute(new SetListItemTypeCommand(target(chest), loot.id));
|
||||
expect(loots(chest)).toMatchObject({ itemType: loot.id });
|
||||
expect(chest.props.loots).toEqual([
|
||||
{ Name: 'Gem', Amount: 1, [ITEM_TYPE_KEY]: loot.id },
|
||||
{ Name: 'Coin', Amount: 1, [ITEM_TYPE_KEY]: loot.id },
|
||||
]);
|
||||
f.stack.undo();
|
||||
expect(loots(chest)).not.toHaveProperty('itemType');
|
||||
expect(chest.props.loots).toEqual([{ Name: 'Gem', Old: 4 }, {}]);
|
||||
});
|
||||
|
||||
it('refuses a list holding anything but structs, and changes nothing', () => {
|
||||
const f = makeSpriteScene();
|
||||
const loot = lootType(f);
|
||||
const chest = chestWith(f, [{ Name: 'Gem' }, 3, 'x']);
|
||||
expect(() => f.stack.execute(new SetListItemTypeCommand(target(chest), loot.id))).toThrow(
|
||||
'loots holds 2 items that are no struct – only a list of structs can take a type',
|
||||
);
|
||||
expect(loots(chest)).not.toHaveProperty('itemType');
|
||||
expect(chest.props.loots).toEqual([{ Name: 'Gem' }, 3, 'x']);
|
||||
});
|
||||
|
||||
it('looks at the overrides too: a placed object holding a number refuses the type', () => {
|
||||
const f = makeSpriteScene();
|
||||
const loot = lootType(f);
|
||||
const chest = chestWith(f, [{ Name: 'Gem' }]);
|
||||
const placed = place(f, chest.id, { x: 0, y: 0 });
|
||||
f.stack.execute(new SetOverrideCommand(f.sceneId, placed, 'loots', [{ Name: 'Ruby' }, 5]));
|
||||
expect(() => f.stack.execute(new SetListItemTypeCommand(target(chest), loot.id))).toThrow(
|
||||
/1 item that is/,
|
||||
);
|
||||
});
|
||||
|
||||
it('follows the type: a field added, renamed and removed reaches every item, the overrides too', () => {
|
||||
const f = makeSpriteScene();
|
||||
const loot = lootType(f);
|
||||
const chest = chestWith(f, [{ Name: 'Gem', Amount: 7 }]);
|
||||
const placed = place(f, chest.id, { x: 0, y: 0 });
|
||||
f.stack.execute(new SetOverrideCommand(f.sceneId, placed, 'loots', [{ Name: 'Ruby', Amount: 3 }]));
|
||||
f.stack.execute(new SetListItemTypeCommand(target(chest), loot.id));
|
||||
onType(f, new AddVariableCommand(loot.id, 'Rare', 'boolean'));
|
||||
expect(chest.props.loots).toEqual([{ Name: 'Gem', Amount: 7, Rare: false, [ITEM_TYPE_KEY]: loot.id }]);
|
||||
onType(f, new RenameVariableCommand(loot.id, 'Amount', 'Count'), rename(loot, 'Amount', 'Count'));
|
||||
expect(chest.props.loots).toEqual([{ Name: 'Gem', Count: 7, Rare: false, [ITEM_TYPE_KEY]: loot.id }]);
|
||||
expect(f.object(placed).overrides.loots).toEqual([
|
||||
{ Name: 'Ruby', Count: 3, Rare: false, [ITEM_TYPE_KEY]: loot.id },
|
||||
]);
|
||||
onType(f, new RemoveVariableCommand(loot.id, 'Name'));
|
||||
expect(chest.props.loots).toEqual([{ Count: 7, Rare: false, [ITEM_TYPE_KEY]: loot.id }]);
|
||||
f.stack.undo();
|
||||
f.stack.undo();
|
||||
expect(chest.props.loots).toEqual([{ Name: 'Gem', Amount: 7, Rare: false, [ITEM_TYPE_KEY]: loot.id }]);
|
||||
});
|
||||
|
||||
it('goes back to mixed with the items keeping their marks, so nothing is lost', () => {
|
||||
const f = makeSpriteScene();
|
||||
const loot = lootType(f);
|
||||
const chest = chestWith(f, [{ Name: 'Gem' }]);
|
||||
f.stack.execute(new SetListItemTypeCommand(target(chest), loot.id));
|
||||
f.stack.execute(new SetListItemTypeCommand(target(chest), ''));
|
||||
expect(loots(chest)).not.toHaveProperty('itemType');
|
||||
expect(chest.props.loots).toEqual([{ Name: 'Gem', Amount: 1, [ITEM_TYPE_KEY]: loot.id }]);
|
||||
expect(structTypeUsages(f.project, loot.id).lists).toBe(0);
|
||||
});
|
||||
|
||||
it('types a list field inside a struct variable, and a list inside a typed item follows its type', () => {
|
||||
const f = makeSpriteScene();
|
||||
const loot = lootType(f);
|
||||
const chest = makeBehavior(f, 'Chest');
|
||||
f.stack.execute(new AddVariableCommand(chest.id, 'room', 'struct'));
|
||||
f.stack.execute(new AddStructFieldCommand(chest.id, 'room', { key: 'loots', kind: 'list' }));
|
||||
f.stack.execute(new SetAssetPropertyCommand(chest.id, 'room', { loots: [{ Name: 'Gem' }] }));
|
||||
f.stack.execute(new SetListItemTypeCommand({ ownerId: chest.id, key: 'room', path: ['loots'] }, loot.id));
|
||||
expect(chest.props.room).toEqual({ loots: [{ Name: 'Gem', Amount: 1, [ITEM_TYPE_KEY]: loot.id }] });
|
||||
expect(structTypeUsages(f.project, loot.id)).toEqual({ places: 0, lists: 1, nodes: 0 });
|
||||
// A Bag type whose `inside` is a list of Loot: a Bag item's `inside` is typed through it.
|
||||
const bag = new CreateAssetCommand('struct', 'Bag');
|
||||
f.stack.execute(bag);
|
||||
f.stack.execute(new AddVariableCommand(bag.asset.id, 'inside', 'list'));
|
||||
f.stack.execute(new SetListItemTypeCommand({ ownerId: bag.asset.id, key: 'inside', path: [] }, loot.id));
|
||||
const holder = chestWith(f, [{ inside: [{ Name: 'Axe' }] }]);
|
||||
f.stack.execute(new SetListItemTypeCommand(target(holder), bag.asset.id));
|
||||
expect(holder.props.loots).toEqual([
|
||||
{ inside: [{ Name: 'Axe', Amount: 1, [ITEM_TYPE_KEY]: loot.id }], [ITEM_TYPE_KEY]: bag.asset.id },
|
||||
]);
|
||||
expect(listItemTypeAt(f.project, loots(holder), holder.props.loots, [0, 'inside'])).toBe(loot.id);
|
||||
expect(listItemTypeAt(f.project, loots(holder), holder.props.loots, [])).toBe(bag.asset.id);
|
||||
});
|
||||
|
||||
it('makes a type from its items: every field any item has, at the first value it has', () => {
|
||||
const f = makeSpriteScene();
|
||||
const chest = chestWith(f, [
|
||||
{ Name: 'Gem', Amount: 2 },
|
||||
{ Name: 'Axe', Rare: true },
|
||||
]);
|
||||
const make = new MakeListItemTypeCommand(target(chest));
|
||||
f.stack.execute(make);
|
||||
const type = f.project.assets.find((a) => a.id === make.assetId) as StructAsset;
|
||||
expect(type).toMatchObject({
|
||||
type: 'struct',
|
||||
name: 'loots',
|
||||
props: { Name: 'Gem', Amount: 2, Rare: true },
|
||||
});
|
||||
expect(type.variables.map((v) => `${v.key}:${v.kind}`)).toEqual([
|
||||
'Name:text',
|
||||
'Amount:number',
|
||||
'Rare:boolean',
|
||||
]);
|
||||
expect(chest.props.loots).toEqual([
|
||||
{ Name: 'Gem', Amount: 2, Rare: true, [ITEM_TYPE_KEY]: type.id },
|
||||
{ Name: 'Axe', Amount: 2, Rare: true, [ITEM_TYPE_KEY]: type.id },
|
||||
]);
|
||||
f.stack.undo();
|
||||
expect(f.project.assets.some((a) => a.id === make.assetId)).toBe(false);
|
||||
expect(chest.props.loots).toEqual([
|
||||
{ Name: 'Gem', Amount: 2 },
|
||||
{ Name: 'Axe', Rare: true },
|
||||
]);
|
||||
});
|
||||
|
||||
it('survives saving and loading, and a stale item in a file is brought in line on load', () => {
|
||||
const f = makeSpriteScene();
|
||||
const loot = lootType(f);
|
||||
const chest = chestWith(f, [{ Name: 'Gem' }]);
|
||||
f.stack.execute(new SetListItemTypeCommand(target(chest), loot.id));
|
||||
const raw = serializeProject(f.project) as { assets: Record<string, unknown>[] };
|
||||
const stored = raw.assets.find((a) => a.id === chest.id) as {
|
||||
variables: Record<string, unknown>[];
|
||||
props: Record<string, unknown>;
|
||||
};
|
||||
expect(stored.variables[0]).toMatchObject({ itemType: loot.id });
|
||||
stored.props.loots = [{ Name: 'Ruby', Stale: 1 }];
|
||||
const reloaded = deserializeProject(raw).assets.find((a) => a.id === chest.id) as BehaviorAsset;
|
||||
expect(reloaded.props.loots).toEqual([{ Name: 'Ruby', Amount: 1, [ITEM_TYPE_KEY]: loot.id }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('a struct item with a type of its own, in a mixed list', () => {
|
||||
it('follows its type while the other items are left alone', () => {
|
||||
const f = makeSpriteScene();
|
||||
const loot = lootType(f);
|
||||
const chest = chestWith(f, [typedItem(f.project, loot, { Name: 'Gem' }), { Name: 'Own', x: 1 }, 5]);
|
||||
onType(f, new RenameVariableCommand(loot.id, 'Name', 'Title'), rename(loot, 'Name', 'Title'));
|
||||
expect(chest.props.loots).toEqual([
|
||||
{ Title: 'Gem', Amount: 1, [ITEM_TYPE_KEY]: loot.id },
|
||||
{ Name: 'Own', x: 1 },
|
||||
5,
|
||||
]);
|
||||
});
|
||||
|
||||
it('reaches a typed struct nested inside an item of its own fields', () => {
|
||||
const f = makeSpriteScene();
|
||||
const loot = lootType(f);
|
||||
const chest = chestWith(f, [{ prize: { Name: 'Gem', [ITEM_TYPE_KEY]: loot.id } }]);
|
||||
expect(chest.props.loots).toEqual([{ prize: { Name: 'Gem', [ITEM_TYPE_KEY]: loot.id } }]);
|
||||
onType(f, new AddVariableCommand(loot.id, 'Rare', 'boolean'));
|
||||
expect(chest.props.loots).toEqual([
|
||||
{ prize: { Name: 'Gem', Amount: 1, Rare: false, [ITEM_TYPE_KEY]: loot.id } },
|
||||
]);
|
||||
});
|
||||
|
||||
it('is typed inside a typed struct variable too', () => {
|
||||
const f = makeSpriteScene();
|
||||
const loot = lootType(f);
|
||||
const chest = makeBehavior(f, 'Chest');
|
||||
f.stack.execute(new AddVariableCommand(chest.id, 'drop', 'struct'));
|
||||
f.stack.execute(new AddStructFieldCommand(chest.id, 'drop', { key: 'more', kind: 'list' }));
|
||||
f.stack.execute(
|
||||
new SetStructTypeCommand({ kind: 'variable', ownerId: chest.id, key: 'drop', path: [] }, ''),
|
||||
);
|
||||
f.stack.execute(new SetAssetPropertyCommand(chest.id, 'drop', { more: [typedItem(f.project, loot)] }));
|
||||
onType(f, new RemoveVariableCommand(loot.id, 'Amount'));
|
||||
expect(chest.props.drop).toEqual({ more: [{ Name: 'Coin', [ITEM_TYPE_KEY]: loot.id }] });
|
||||
});
|
||||
});
|
||||
|
||||
describe('a run never sees the mark', () => {
|
||||
it('is taken out of variables, behavior defaults and placed objects', () => {
|
||||
const f = makeSpriteScene();
|
||||
const loot = lootType(f);
|
||||
const chest = chestWith(f, [{ Name: 'Gem' }]);
|
||||
f.stack.execute(new SetListItemTypeCommand(target(chest), loot.id));
|
||||
const shared = makeVariables(f, 'Globals', []);
|
||||
f.stack.execute(new AddVariableCommand(shared.id, 'bag', 'list'));
|
||||
f.stack.execute(new SetAssetPropertyCommand(shared.id, 'bag', [typedItem(f.project, loot)]));
|
||||
expect(shared.props.bag).toEqual([{ Name: 'Coin', Amount: 1, [ITEM_TYPE_KEY]: loot.id }]);
|
||||
expect(declaredVariables(f.project)[shared.id]!.bag).toEqual([{ Name: 'Coin', Amount: 1 }]);
|
||||
expect(graphLookup(f.project).behavior(chest.id)!.defaults.loots).toEqual([{ Name: 'Gem', Amount: 1 }]);
|
||||
expect(withoutItemTypes({ a: [{ b: { [ITEM_TYPE_KEY]: 'x', c: 1 } }] })).toEqual({
|
||||
a: [{ b: { c: 1 } }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Add item with a type', () => {
|
||||
const run = (f: SceneFixture, loot: StructAsset, value: unknown) => {
|
||||
const ctx = fakeContext();
|
||||
const out = runDef(
|
||||
'AddItem',
|
||||
ctx,
|
||||
{ list: [{ Name: 'Gem', Amount: 1 }], value: value as never, index: -1 },
|
||||
{ structType: loot.id },
|
||||
'exec',
|
||||
graphLookup(f.project),
|
||||
).outputs;
|
||||
return { result: out?.result, logs: ctx.logs };
|
||||
};
|
||||
|
||||
it('adds a struct with exactly the fields of the type', () => {
|
||||
const f = makeSpriteScene();
|
||||
const loot = lootType(f);
|
||||
expect(run(f, loot, { Amount: 3, Name: 'Axe' })).toEqual({
|
||||
result: [
|
||||
{ Name: 'Gem', Amount: 1 },
|
||||
{ Amount: 3, Name: 'Axe' },
|
||||
],
|
||||
logs: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('leaves the list as it was and says so for anything else', () => {
|
||||
const f = makeSpriteScene();
|
||||
const loot = lootType(f);
|
||||
const unchanged = [{ Name: 'Gem', Amount: 1 }];
|
||||
expect(run(f, loot, { Name: 'Axe' })).toEqual({
|
||||
result: unchanged,
|
||||
logs: ['Add item: Name: Axe is no Loot – not added'],
|
||||
});
|
||||
expect(run(f, loot, { Name: 'Axe', Amount: 1, Extra: 0 }).result).toEqual(unchanged);
|
||||
expect(run(f, loot, 7)).toEqual({ result: unchanged, logs: ['Add item: 7 is no Loot – not added'] });
|
||||
});
|
||||
|
||||
it('checks nothing without a type, or with one that is gone', () => {
|
||||
const f = makeSpriteScene();
|
||||
const out = runDef(
|
||||
'AddItem',
|
||||
fakeContext(),
|
||||
{ list: [], value: 7, index: -1 },
|
||||
{ structType: 'gone' },
|
||||
'exec',
|
||||
graphLookup(f.project),
|
||||
);
|
||||
expect(out.outputs?.result).toEqual([7]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Set item with a type', () => {
|
||||
const run = (f: SceneFixture, loot: StructAsset, value: unknown, structType = loot.id) => {
|
||||
const ctx = fakeContext();
|
||||
const out = runDef(
|
||||
'SetItem',
|
||||
ctx,
|
||||
{ list: [{ Name: 'Gem', Amount: 1 }, 5], index: 0, value: value as never },
|
||||
{ structType },
|
||||
'exec',
|
||||
graphLookup(f.project),
|
||||
).outputs;
|
||||
return { result: out?.result, logs: ctx.logs };
|
||||
};
|
||||
|
||||
it('sets a struct with exactly the fields of the type', () => {
|
||||
const f = makeSpriteScene();
|
||||
const loot = lootType(f);
|
||||
expect(run(f, loot, { Amount: 3, Name: 'Axe' })).toEqual({
|
||||
result: [{ Amount: 3, Name: 'Axe' }, 5],
|
||||
logs: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('leaves the list as it was and says so for anything else', () => {
|
||||
const f = makeSpriteScene();
|
||||
const loot = lootType(f);
|
||||
const unchanged = [{ Name: 'Gem', Amount: 1 }, 5];
|
||||
expect(run(f, loot, { Name: 'Axe' })).toEqual({
|
||||
result: unchanged,
|
||||
logs: ['Set item: Name: Axe is no Loot – not set'],
|
||||
});
|
||||
expect(run(f, loot, { Name: 'Axe', Amount: 1, Extra: 0 }).result).toEqual(unchanged);
|
||||
expect(run(f, loot, 7)).toEqual({ result: unchanged, logs: ['Set item: 7 is no Loot – not set'] });
|
||||
});
|
||||
|
||||
it('checks nothing without a type, or with one that is gone', () => {
|
||||
const f = makeSpriteScene();
|
||||
const loot = lootType(f);
|
||||
// Without a check the slot keeps its kind, as ever: a struct slot takes no number.
|
||||
expect(run(f, loot, { Other: 1 }, '').result).toEqual([{ Other: 1 }, 5]);
|
||||
expect(run(f, loot, { Other: 1 }, 'gone').logs).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user