Files
feuerwerk/tests/viewport/layer-z.test.ts
T

184 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest';
import { CreateAssetCommand } from '../../src/core/commands/assetCommands';
import {
AddTilemapLayerCommand,
RenameTilemapLayerCommand,
SetTilemapLayerHiddenCommand,
SetTilemapLayerZCommand,
} from '../../src/core/commands/tilemapCommands';
import type { TilemapAsset } from '../../src/core/model/Asset';
import { findAsset } from '../../src/core/model/Project';
import { layerZ } from '../../src/core/model/tilemap';
import { deserializeProject, serializeProject } from '../../src/core/store/serialize';
import { drawSlots } from '../../src/viewport/drawOrder';
import { collectHolderItems, collectWorldItems } from '../../src/viewport/items';
import { makeSpriteScene, place, roundTrip, startWorld } from '../helpers';
/**
* A scene with a tilemap of two layers, "Ground" and "Roofs", and the sprite "Box" – each placed
* at the `z` asked for, the map first in the hierarchy.
*/
function makeScene(mapZ: number, boxZ: number) {
const f = makeSpriteScene('Box');
const map = new CreateAssetCommand('tilemap', 'Village');
f.stack.execute(map);
const mapId = map.asset.id;
const assetOf = () => findAsset(f.project, mapId) as TilemapAsset;
const ground = assetOf().layers[0]!.id;
f.stack.execute(new RenameTilemapLayerCommand(mapId, ground, 'Ground'));
const roofsAdd = new AddTilemapLayerCommand(mapId);
f.stack.execute(roofsAdd);
f.stack.execute(new RenameTilemapLayerCommand(mapId, roofsAdd.layerId, 'Roofs'));
const mapObject = place(f, mapId, { x: 0, y: 0, z: mapZ });
const box = place(f, f.spriteId, { x: 0, y: 0, z: boxZ });
const names = new Map([
[mapObject, 'map'],
[box, 'box'],
]);
const layerNames = new Map([
[ground, 'Ground'],
[roofsAdd.layerId, 'Roofs'],
]);
return { f, mapId, mapObject, box, ground, roofs: roofsAdd.layerId, assetOf, names, layerNames };
}
type Scene = ReturnType<typeof makeScene>;
/** The draw order of the scene view, back to front, as readable names. */
function sceneOrder(s: Scene): string[] {
return readable(s, drawSlots(collectHolderItems(s.f.project, s.f.sceneOf(), 0).items));
}
function readable(s: Scene, slots: { id: string; layerId: string }[]): string[] {
return slots.map((slot) =>
slot.layerId ? s.layerNames.get(slot.layerId)! : (s.names.get(slot.id) ?? slot.id),
);
}
describe('the z of a tilemap layer (#174)', () => {
it('sets, undoes and redoes, and stores 0 as nothing', () => {
const s = makeScene(0, 0);
const roofs = () => s.assetOf().layers[1]!;
s.f.stack.execute(new SetTilemapLayerZCommand(s.mapId, s.roofs, 10));
expect(roofs().z).toBe(10);
s.f.stack.execute(new SetTilemapLayerZCommand(s.mapId, s.roofs, -2.5));
expect(roofs().z).toBe(-2.5);
s.f.stack.undo();
expect(roofs().z).toBe(10);
s.f.stack.execute(new SetTilemapLayerZCommand(s.mapId, s.roofs, 0));
expect('z' in roofs()).toBe(false);
expect(layerZ(roofs())).toBe(0);
s.f.stack.undo();
s.f.stack.undo();
expect('z' in roofs()).toBe(false);
s.f.stack.redo();
expect(roofs().z).toBe(10);
});
it('refuses what is not a number', () => {
const s = makeScene(0, 0);
for (const z of [Number.NaN, Number.POSITIVE_INFINITY])
expect(() => s.f.stack.execute(new SetTilemapLayerZCommand(s.mapId, s.roofs, z))).toThrow(
'A layer z is a number',
);
expect('z' in s.assetOf().layers[1]!).toBe(false);
});
it('survives a round trip, and loading keeps only a finite number other than 0', () => {
const s = makeScene(0, 0);
s.f.stack.execute(new SetTilemapLayerZCommand(s.mapId, s.roofs, 10));
const back = findAsset(roundTrip(s.f.project), s.mapId) as TilemapAsset;
expect(back.layers.map((l) => l.z)).toEqual([undefined, 10]);
const saved = serializeProject(s.f.project) as {
assets: { id: string; layers?: Record<string, unknown>[] }[];
};
const layers = saved.assets.find((a) => a.id === s.mapId)!.layers!;
for (const [raw, kept] of [
[0, undefined],
['5', undefined],
[null, undefined],
[true, undefined],
[-3, -3],
] as const) {
layers[1]!.z = raw;
const loaded = findAsset(deserializeProject(saved), s.mapId) as TilemapAsset;
expect(loaded.layers[1]!.z, `stored ${String(raw)}`).toBe(kept);
}
});
it('with every layer at 0 draws as before: the layers right after their map, in list order', () => {
expect(sceneOrder(makeScene(0, 0))).toEqual(['map', 'Ground', 'Roofs', 'box']);
expect(sceneOrder(makeScene(1, 0))).toEqual(['box', 'map', 'Ground', 'Roofs']);
});
it('adds to the map’s own z, so a figure walks between the ground and the roofs', () => {
const s = makeScene(0, 5);
s.f.stack.execute(new SetTilemapLayerZCommand(s.mapId, s.roofs, 10));
expect(sceneOrder(s)).toEqual(['map', 'Ground', 'box', 'Roofs']);
});
it('moves every layer along when the map itself is moved in z – it is relative, not absolute', () => {
const s = makeScene(20, 5);
s.f.stack.execute(new SetTilemapLayerZCommand(s.mapId, s.roofs, 10));
// Roofs at 30 and Ground at 20 are both above the box at 5, still ten apart.
expect(sceneOrder(s)).toEqual(['box', 'map', 'Ground', 'Roofs']);
});
it('sorts a layer with a negative z below its map and below what shares the map’s z', () => {
const s = makeScene(0, 0);
s.f.stack.execute(new SetTilemapLayerZCommand(s.mapId, s.roofs, -1));
expect(sceneOrder(s)).toEqual(['Roofs', 'map', 'Ground', 'box']);
});
it('breaks a tie of z by the hierarchy, and between layers by their list order', () => {
const s = makeScene(0, 5);
s.f.stack.execute(new SetTilemapLayerZCommand(s.mapId, s.ground, 5));
s.f.stack.execute(new SetTilemapLayerZCommand(s.mapId, s.roofs, 5));
expect(sceneOrder(s)).toEqual(['map', 'Ground', 'Roofs', 'box']);
});
it('leaves the symbols out, whatever their z: they are drawn above everything', () => {
const s = makeScene(0, 5);
const camera = new CreateAssetCommand('camera', 'Camera');
s.f.stack.execute(camera);
place(s.f, camera.asset.id, { x: 0, y: 0, z: 7 });
expect(sceneOrder(s)).toEqual(['map', 'Ground', 'Roofs', 'box']);
});
it('leaves a hidden layer out of the order', () => {
const s = makeScene(0, 5);
s.f.stack.execute(new SetTilemapLayerZCommand(s.mapId, s.roofs, 10));
s.f.stack.execute(new SetTilemapLayerHiddenCommand(s.mapId, s.roofs, true));
expect(sceneOrder(s)).toEqual(['map', 'Ground', 'box']);
});
it('gives two placements of one map the same steps from their own z', () => {
const s = makeScene(0, 5);
s.f.stack.execute(new SetTilemapLayerZCommand(s.mapId, s.roofs, 10));
const high = place(s.f, s.mapId, { x: 0, y: 0, z: 100 });
s.names.set(high, 'high');
const slots = drawSlots(collectHolderItems(s.f.project, s.f.sceneOf(), 0).items);
expect(slots.map((slot) => [readable(s, [slot])[0], slot.z])).toEqual([
['map', 0],
['Ground', 0],
['box', 5],
['Roofs', 10],
['high', 100],
['Ground', 100],
['Roofs', 110],
]);
});
it('holds in the running game too, and follows the map when a run changes its z', () => {
const s = makeScene(0, 5);
s.f.stack.execute(new SetTilemapLayerZCommand(s.mapId, s.roofs, 10));
const { world } = startWorld(s.f);
const gameOrder = () => readable(s, drawSlots(collectWorldItems(world).items));
expect(gameOrder()).toEqual(['map', 'Ground', 'box', 'Roofs']);
const map = world.entities.find((e) => e.objectId === s.mapObject)!;
map.z = -20;
expect(gameOrder()).toEqual(['map', 'Ground', 'Roofs', 'box']);
});
});