The autotile asset is renamed to tile and gains a plain None (1 × 1) kind, which is now the default; older projects load their autotiles as floor tiles (#131)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a65132bb5a
commit
732f6518ae
35 files changed
+786
-617
No files matched your search
@@ -0,0 +1,477 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
CreateAssetCommand,
|
||||
CreateTextureCommand,
|
||||
SetAssetPropertyCommand,
|
||||
} from '../../src/core/commands/assetCommands';
|
||||
import {
|
||||
AddTilePictureCommand,
|
||||
MoveTilePictureCommand,
|
||||
RemoveTilePictureCommand,
|
||||
} from '../../src/core/commands/tileCommands';
|
||||
import { AddFrameCommand } from '../../src/core/commands/frameCommands';
|
||||
import type { TileAsset } from '../../src/core/model/Asset';
|
||||
import { isPlaceableType } from '../../src/core/model/AssetTypes';
|
||||
import {
|
||||
autotileCombinations,
|
||||
type Neighbours,
|
||||
neighboursAt,
|
||||
SAMPLE_PATTERN,
|
||||
tileQuadrants,
|
||||
} from '../../src/core/model/autotile';
|
||||
import {
|
||||
pictureIndexAt,
|
||||
quadrantRect,
|
||||
readTile,
|
||||
TILE_KIND_KEY,
|
||||
type TileKind,
|
||||
tileKind,
|
||||
tilePlayback,
|
||||
} from '../../src/core/model/tile';
|
||||
import { deserializeProject } from '../../src/core/store/serialize';
|
||||
import { findAsset, type Project } from '../../src/core/model/Project';
|
||||
import { tilesUsing } from '../../src/core/model/usages';
|
||||
import { makeProject, roundTrip } from '../helpers';
|
||||
|
||||
const FILE = `${'a1b2c3d4'.repeat(8)}.png`;
|
||||
|
||||
/**
|
||||
* A sheet the size of the RPG-Maker sheets #118 was written from: 48 px tiles, 8 × 4 blocks. The
|
||||
* tile is switched to a floor block, which is what most of these tests are about.
|
||||
*/
|
||||
function makeTile(width = 768, height = 576, kind: TileKind = 'floor') {
|
||||
const fixture = makeProject();
|
||||
const texture = new CreateTextureCommand('Outside', { file: FILE, width, height });
|
||||
fixture.stack.execute(texture);
|
||||
const tile = new CreateAssetCommand('tile', 'Grass');
|
||||
fixture.stack.execute(tile);
|
||||
if (kind !== 'none') fixture.stack.execute(new SetAssetPropertyCommand(tile.asset.id, TILE_KIND_KEY, kind));
|
||||
return {
|
||||
...fixture,
|
||||
texture: texture.asset,
|
||||
tileId: tile.asset.id,
|
||||
assetOf: () => findAsset(fixture.project, tile.asset.id) as TileAsset,
|
||||
};
|
||||
}
|
||||
|
||||
/** Cuts one block out of the sheet and returns the frame id. */
|
||||
function cutBlock(
|
||||
fixture: ReturnType<typeof makeTile>,
|
||||
rect: { x: number; y: number; w: number; h: number },
|
||||
): string {
|
||||
const command = new AddFrameCommand(fixture.texture.id, rect);
|
||||
fixture.stack.execute(command);
|
||||
return command.frameId;
|
||||
}
|
||||
|
||||
/**
|
||||
* `Tilemap.FLOOR_AUTOTILE_TABLE` and `Tilemap.WALL_AUTOTILE_TABLE` of RPG Maker MZ, generated from
|
||||
* its `js/rmmz_core.js` and not typed by hand. Each row is one tile as four quarters – top-left,
|
||||
* top-right, bottom-left, bottom-right – in quadrant units from the block's top-left corner.
|
||||
*/
|
||||
// prettier-ignore
|
||||
const MZ_FLOOR: readonly (readonly (readonly number[])[])[] = [
|
||||
[[2, 4], [1, 4], [2, 3], [1, 3]],
|
||||
[[2, 0], [1, 4], [2, 3], [1, 3]],
|
||||
[[2, 4], [3, 0], [2, 3], [1, 3]],
|
||||
[[2, 0], [3, 0], [2, 3], [1, 3]],
|
||||
[[2, 4], [1, 4], [2, 3], [3, 1]],
|
||||
[[2, 0], [1, 4], [2, 3], [3, 1]],
|
||||
[[2, 4], [3, 0], [2, 3], [3, 1]],
|
||||
[[2, 0], [3, 0], [2, 3], [3, 1]],
|
||||
[[2, 4], [1, 4], [2, 1], [1, 3]],
|
||||
[[2, 0], [1, 4], [2, 1], [1, 3]],
|
||||
[[2, 4], [3, 0], [2, 1], [1, 3]],
|
||||
[[2, 0], [3, 0], [2, 1], [1, 3]],
|
||||
[[2, 4], [1, 4], [2, 1], [3, 1]],
|
||||
[[2, 0], [1, 4], [2, 1], [3, 1]],
|
||||
[[2, 4], [3, 0], [2, 1], [3, 1]],
|
||||
[[2, 0], [3, 0], [2, 1], [3, 1]],
|
||||
[[0, 4], [1, 4], [0, 3], [1, 3]],
|
||||
[[0, 4], [3, 0], [0, 3], [1, 3]],
|
||||
[[0, 4], [1, 4], [0, 3], [3, 1]],
|
||||
[[0, 4], [3, 0], [0, 3], [3, 1]],
|
||||
[[2, 2], [1, 2], [2, 3], [1, 3]],
|
||||
[[2, 2], [1, 2], [2, 3], [3, 1]],
|
||||
[[2, 2], [1, 2], [2, 1], [1, 3]],
|
||||
[[2, 2], [1, 2], [2, 1], [3, 1]],
|
||||
[[2, 4], [3, 4], [2, 3], [3, 3]],
|
||||
[[2, 4], [3, 4], [2, 1], [3, 3]],
|
||||
[[2, 0], [3, 4], [2, 3], [3, 3]],
|
||||
[[2, 0], [3, 4], [2, 1], [3, 3]],
|
||||
[[2, 4], [1, 4], [2, 5], [1, 5]],
|
||||
[[2, 0], [1, 4], [2, 5], [1, 5]],
|
||||
[[2, 4], [3, 0], [2, 5], [1, 5]],
|
||||
[[2, 0], [3, 0], [2, 5], [1, 5]],
|
||||
[[0, 4], [3, 4], [0, 3], [3, 3]],
|
||||
[[2, 2], [1, 2], [2, 5], [1, 5]],
|
||||
[[0, 2], [1, 2], [0, 3], [1, 3]],
|
||||
[[0, 2], [1, 2], [0, 3], [3, 1]],
|
||||
[[2, 2], [3, 2], [2, 3], [3, 3]],
|
||||
[[2, 2], [3, 2], [2, 1], [3, 3]],
|
||||
[[2, 4], [3, 4], [2, 5], [3, 5]],
|
||||
[[2, 0], [3, 4], [2, 5], [3, 5]],
|
||||
[[0, 4], [1, 4], [0, 5], [1, 5]],
|
||||
[[0, 4], [3, 0], [0, 5], [1, 5]],
|
||||
[[0, 2], [3, 2], [0, 3], [3, 3]],
|
||||
[[0, 2], [1, 2], [0, 5], [1, 5]],
|
||||
[[0, 4], [3, 4], [0, 5], [3, 5]],
|
||||
[[2, 2], [3, 2], [2, 5], [3, 5]],
|
||||
[[0, 2], [3, 2], [0, 5], [3, 5]],
|
||||
[[0, 0], [1, 0], [0, 1], [1, 1]],
|
||||
];
|
||||
|
||||
// prettier-ignore
|
||||
const MZ_WALL: readonly (readonly (readonly number[])[])[] = [
|
||||
[[2, 2], [1, 2], [2, 1], [1, 1]],
|
||||
[[0, 2], [1, 2], [0, 1], [1, 1]],
|
||||
[[2, 0], [1, 0], [2, 1], [1, 1]],
|
||||
[[0, 0], [1, 0], [0, 1], [1, 1]],
|
||||
[[2, 2], [3, 2], [2, 1], [3, 1]],
|
||||
[[0, 2], [3, 2], [0, 1], [3, 1]],
|
||||
[[2, 0], [3, 0], [2, 1], [3, 1]],
|
||||
[[0, 0], [3, 0], [0, 1], [3, 1]],
|
||||
[[2, 2], [1, 2], [2, 3], [1, 3]],
|
||||
[[0, 2], [1, 2], [0, 3], [1, 3]],
|
||||
[[2, 0], [1, 0], [2, 3], [1, 3]],
|
||||
[[0, 0], [1, 0], [0, 3], [1, 3]],
|
||||
[[2, 2], [3, 2], [2, 3], [3, 3]],
|
||||
[[0, 2], [3, 2], [0, 3], [3, 3]],
|
||||
[[2, 0], [3, 0], [2, 3], [3, 3]],
|
||||
[[0, 0], [3, 0], [0, 3], [3, 3]],
|
||||
];
|
||||
|
||||
const FLOOR_BLOCK = { x: 96, y: 0, w: 96, h: 144 };
|
||||
const WALL_BLOCK = { x: 0, y: 0, w: 96, h: 96 };
|
||||
|
||||
function around(present: Partial<Neighbours>): Neighbours {
|
||||
return { n: false, e: false, s: false, w: false, ne: false, se: false, sw: false, nw: false, ...present };
|
||||
}
|
||||
|
||||
describe('autotile blocks', () => {
|
||||
it('is a library asset like a texture: nothing places it, the tilemap of #128 does', () => {
|
||||
expect(isPlaceableType('tile')).toBe(false);
|
||||
});
|
||||
|
||||
it('produces 47 tiles from a floor block and 16 from a wall, each of them different', () => {
|
||||
const floor = autotileCombinations('floor');
|
||||
const wall = autotileCombinations('wall');
|
||||
expect(floor).toHaveLength(47);
|
||||
expect(wall).toHaveLength(16);
|
||||
const signature = (quadrants: { x: number; y: number }[]) =>
|
||||
quadrants.map((q) => `${q.x},${q.y}`).join(' ');
|
||||
expect(new Set(floor.map((c) => signature(c.quadrants))).size).toBe(47);
|
||||
expect(new Set(wall.map((c) => signature(c.quadrants))).size).toBe(16);
|
||||
});
|
||||
|
||||
/**
|
||||
* The whole point of #118: the tiles must come out of the block the way RPG Maker MZ takes them,
|
||||
* or a seam runs through every joint. `MZ_FLOOR` and `MZ_WALL` below are its tables verbatim, so
|
||||
* this test fails the moment the model drifts away from them.
|
||||
*/
|
||||
it('produces exactly the quarters of the RPG-Maker tables', () => {
|
||||
const signature = (quadrants: readonly (readonly number[])[]) =>
|
||||
quadrants.map((q) => `${q[0]},${q[1]}`).join(' ');
|
||||
const mine = (kind: 'floor' | 'wall') =>
|
||||
new Set(autotileCombinations(kind).map((c) => signature(c.quadrants.map((q) => [q.x, q.y]))));
|
||||
// MZ's floor table has one row more than there are neighbourhoods: its last one is the block's
|
||||
// top-left tile, which is the same picture as the lone tile of row 46.
|
||||
expect(mine('floor')).toEqual(new Set(MZ_FLOOR.slice(0, 47).map(signature)));
|
||||
expect(mine('wall')).toEqual(new Set(MZ_WALL.map(signature)));
|
||||
});
|
||||
|
||||
it('takes a lone tile from the four convex corners and a surrounded one from the inside', () => {
|
||||
// Row 46 of MZ's table.
|
||||
expect(tileQuadrants('floor', around({}))).toEqual([
|
||||
{ x: 0, y: 2 },
|
||||
{ x: 3, y: 2 },
|
||||
{ x: 0, y: 5 },
|
||||
{ x: 3, y: 5 },
|
||||
]);
|
||||
// Row 0, and the reason a joint is seamless: the inside is taken turned by half a tile, so the
|
||||
// top-left corner of the tile is the quarter at the bottom right of the block's inside.
|
||||
const all = { n: true, e: true, s: true, w: true, ne: true, se: true, sw: true, nw: true };
|
||||
expect(tileQuadrants('floor', all)).toEqual([
|
||||
{ x: 2, y: 4 },
|
||||
{ x: 1, y: 4 },
|
||||
{ x: 2, y: 3 },
|
||||
{ x: 1, y: 3 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('draws a straight edge towards the side that is missing', () => {
|
||||
// Only the north neighbour: the top half is inside, the bottom half is the lower border.
|
||||
const north = tileQuadrants('floor', around({ n: true }));
|
||||
expect(north[0]).toEqual({ x: 0, y: 4 });
|
||||
expect(north[1]).toEqual({ x: 3, y: 4 });
|
||||
expect(north[2]).toEqual({ x: 0, y: 5 });
|
||||
expect(north[3]).toEqual({ x: 3, y: 5 });
|
||||
});
|
||||
|
||||
it('puts a notch where both sides are there but the diagonal between them is not', () => {
|
||||
const withDiagonal = tileQuadrants('floor', around({ n: true, w: true, nw: true }));
|
||||
const without = tileQuadrants('floor', around({ n: true, w: true }));
|
||||
// Only the corner the diagonal belongs to changes, and it changes into the notch – which sits
|
||||
// in the block's top *right* tile, not the top left one.
|
||||
expect(withDiagonal[0]).toEqual({ x: 2, y: 4 });
|
||||
expect(without[0]).toEqual({ x: 2, y: 0 });
|
||||
expect(without.slice(1)).toEqual(withDiagonal.slice(1));
|
||||
});
|
||||
|
||||
it('ignores the diagonals on a wall, which has no notch to draw', () => {
|
||||
const bare = tileQuadrants('wall', around({ n: true, w: true }));
|
||||
const diagonal = tileQuadrants('wall', around({ n: true, w: true, nw: true }));
|
||||
expect(bare).toEqual(diagonal);
|
||||
expect(bare[0]).toEqual({ x: 2, y: 2 });
|
||||
// Every quarter a wall can use lies in its own 2 × 2 block: four quadrant rows, no more.
|
||||
for (const combination of autotileCombinations('wall'))
|
||||
for (const quadrant of combination.quadrants) expect(quadrant.y).toBeLessThan(4);
|
||||
});
|
||||
|
||||
it('labels a tile by the sides it joins and the corners it notches', () => {
|
||||
const floor = autotileCombinations('floor');
|
||||
expect(floor[0]?.label).toBe('·');
|
||||
const surrounded = floor.find(
|
||||
(c) => c.neighbours.n && c.neighbours.e && c.neighbours.s && c.neighbours.w,
|
||||
);
|
||||
expect(surrounded?.label).toBe('NESW-ne,se,sw,nw');
|
||||
});
|
||||
|
||||
it('reads the sample map so that the hole in it asks for notches', () => {
|
||||
// Row 2, column 4 sits right above the hole: joined to the north, open to the south.
|
||||
const above = neighboursAt(SAMPLE_PATTERN, 2, 4);
|
||||
expect(above.n).toBe(true);
|
||||
expect(above.s).toBe(false);
|
||||
expect(above.se).toBe(false);
|
||||
// Row 5, column 3 is the cell below the hole's bottom-left corner: joined north and east,
|
||||
// with the hole itself sitting in the north-east – exactly the case the notch is drawn for.
|
||||
const corner = neighboursAt(SAMPLE_PATTERN, 5, 3);
|
||||
expect([corner.n, corner.e, corner.ne]).toEqual([true, true, false]);
|
||||
expect(tileQuadrants('floor', corner)[1]).toEqual({ x: 3, y: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('tile asset', () => {
|
||||
it('derives the tile size from the block and accepts a whole texture as one', () => {
|
||||
const fixture = makeTile(96, 144);
|
||||
fixture.stack.execute(new AddTilePictureCommand(fixture.tileId, fixture.texture.id));
|
||||
const info = readTile(fixture.project, fixture.assetOf());
|
||||
expect(info.problem).toBe('');
|
||||
expect(info.tile).toBe(48);
|
||||
expect(info.quad).toBe(24);
|
||||
expect(info.pictures).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('cuts a quarter tile out of the frame the picture names', () => {
|
||||
const fixture = makeTile();
|
||||
const frame = cutBlock(fixture, FLOOR_BLOCK);
|
||||
fixture.stack.execute(new AddTilePictureCommand(fixture.tileId, frame));
|
||||
const info = readTile(fixture.project, fixture.assetOf());
|
||||
expect(info.tile).toBe(48);
|
||||
// The block starts at x = 96, so its third quadrant column starts 48 px further right.
|
||||
expect(quadrantRect(info.pictures[0]!, info.quad, { x: 2, y: 5 })).toEqual({
|
||||
x: 96 + 48,
|
||||
y: 120,
|
||||
w: 24,
|
||||
h: 24,
|
||||
});
|
||||
});
|
||||
|
||||
it('complains about a block that does not fit the kind, without dropping it', () => {
|
||||
const fixture = makeTile();
|
||||
const wall = cutBlock(fixture, WALL_BLOCK);
|
||||
fixture.stack.execute(new AddTilePictureCommand(fixture.tileId, wall));
|
||||
const asFloor = readTile(fixture.project, fixture.assetOf());
|
||||
expect(asFloor.problem).toContain('floor block');
|
||||
expect(asFloor.tile).toBe(0);
|
||||
expect(fixture.assetOf().pictures).toEqual([wall]);
|
||||
// The same picture is a wall block, so switching the kind is all it takes.
|
||||
fixture.stack.execute(new SetAssetPropertyCommand(fixture.tileId, TILE_KIND_KEY, 'wall'));
|
||||
const asWall = readTile(fixture.project, fixture.assetOf());
|
||||
expect(asWall.problem).toBe('');
|
||||
expect(asWall.tile).toBe(48);
|
||||
});
|
||||
|
||||
it('rejects an animation whose steps are not the same size, and a picture that is gone', () => {
|
||||
const fixture = makeTile();
|
||||
const big = cutBlock(fixture, FLOOR_BLOCK);
|
||||
const small = cutBlock(fixture, { x: 0, y: 0, w: 32, h: 48 });
|
||||
fixture.stack.execute(new AddTilePictureCommand(fixture.tileId, big));
|
||||
fixture.stack.execute(new AddTilePictureCommand(fixture.tileId, small));
|
||||
expect(readTile(fixture.project, fixture.assetOf()).problem).toContain('different sizes');
|
||||
// A dangling id is named as missing rather than silently skipped.
|
||||
fixture.assetOf().pictures = ['gone'];
|
||||
expect(readTile(fixture.project, fixture.assetOf()).problem).toContain('no longer exist');
|
||||
fixture.assetOf().pictures = [];
|
||||
expect(readTile(fixture.project, fixture.assetOf()).problem).toContain('No picture yet');
|
||||
});
|
||||
|
||||
it('steps through the pictures, loop and ping-pong, and holds still on a single one', () => {
|
||||
// Three pictures at 4 fps: a quarter second each.
|
||||
const loop = [0, 0.26, 0.51, 0.76, 1.01].map((t) => pictureIndexAt(3, 'loop', 4, t));
|
||||
expect(loop).toEqual([0, 1, 2, 0, 1]);
|
||||
const pingpong = [0, 0.26, 0.51, 0.76, 1.01, 1.26].map((t) => pictureIndexAt(3, 'pingpong', 4, t));
|
||||
// Turns on the last picture, so the middle one is never shown twice in a row.
|
||||
expect(pingpong).toEqual([0, 1, 2, 1, 0, 1]);
|
||||
expect(pictureIndexAt(1, 'loop', 4, 99)).toBe(0);
|
||||
expect(pictureIndexAt(3, 'loop', 0, 99)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tile pictures', () => {
|
||||
it('adds, removes and reorders with undo', () => {
|
||||
const fixture = makeTile();
|
||||
const a = cutBlock(fixture, FLOOR_BLOCK);
|
||||
const b = cutBlock(fixture, { ...FLOOR_BLOCK, x: 192 });
|
||||
const c = cutBlock(fixture, { ...FLOOR_BLOCK, x: 288 });
|
||||
for (const id of [a, b, c]) fixture.stack.execute(new AddTilePictureCommand(fixture.tileId, id));
|
||||
expect(fixture.assetOf().pictures).toEqual([a, b, c]);
|
||||
|
||||
fixture.stack.execute(new MoveTilePictureCommand(fixture.tileId, 2, 0));
|
||||
expect(fixture.assetOf().pictures).toEqual([c, a, b]);
|
||||
fixture.stack.undo();
|
||||
expect(fixture.assetOf().pictures).toEqual([a, b, c]);
|
||||
|
||||
fixture.stack.execute(new RemoveTilePictureCommand(fixture.tileId, 1));
|
||||
expect(fixture.assetOf().pictures).toEqual([a, c]);
|
||||
fixture.stack.undo();
|
||||
expect(fixture.assetOf().pictures).toEqual([a, b, c]);
|
||||
|
||||
// Inserting somewhere other than the end, and taking it out again.
|
||||
fixture.stack.execute(new AddTilePictureCommand(fixture.tileId, c, 1));
|
||||
expect(fixture.assetOf().pictures).toEqual([a, c, b, c]);
|
||||
fixture.stack.undo();
|
||||
expect(fixture.assetOf().pictures).toEqual([a, b, c]);
|
||||
});
|
||||
|
||||
it('takes only textures and frames, and only places that exist', () => {
|
||||
const fixture = makeTile();
|
||||
const sprite = new CreateAssetCommand('sprite', 'Box');
|
||||
fixture.stack.execute(sprite);
|
||||
expect(() => new AddTilePictureCommand(fixture.tileId, sprite.asset.id).execute(fixture.project)).toThrow(
|
||||
/not a texture or a frame/,
|
||||
);
|
||||
expect(() => new RemoveTilePictureCommand(fixture.tileId, 0).execute(fixture.project)).toThrow(
|
||||
/no picture 1/,
|
||||
);
|
||||
fixture.stack.execute(new AddTilePictureCommand(fixture.tileId, fixture.texture.id));
|
||||
expect(() => new MoveTilePictureCommand(fixture.tileId, 0, 3).execute(fixture.project)).toThrow(
|
||||
/no place 4/,
|
||||
);
|
||||
});
|
||||
|
||||
it('counts as a use of the texture, so deleting it is not silent', () => {
|
||||
const fixture = makeTile();
|
||||
const frame = cutBlock(fixture, FLOOR_BLOCK);
|
||||
fixture.stack.execute(new AddTilePictureCommand(fixture.tileId, frame));
|
||||
expect(tilesUsing(fixture.project, [frame]).map((a) => a.name)).toEqual(['Grass']);
|
||||
expect(tilesUsing(fixture.project, ['somewhere else'])).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tile in the document', () => {
|
||||
it('survives a save and keeps only what it can read', () => {
|
||||
const fixture = makeTile();
|
||||
const frame = cutBlock(fixture, FLOOR_BLOCK);
|
||||
fixture.stack.execute(new AddTilePictureCommand(fixture.tileId, frame));
|
||||
fixture.stack.execute(new SetAssetPropertyCommand(fixture.tileId, TILE_KIND_KEY, 'wall'));
|
||||
const asset = tileOf(roundTrip(fixture.project));
|
||||
expect(asset.pictures).toEqual([frame]);
|
||||
expect(tileKind(asset)).toBe('wall');
|
||||
|
||||
// Junk in the list is dropped, and an unreadable kind or playback falls back to the default –
|
||||
// for the kind that is the plain tile.
|
||||
const raw = JSON.parse(JSON.stringify(fixture.project)) as {
|
||||
assets: Record<string, unknown>[];
|
||||
};
|
||||
const stored = raw.assets.find((a) => a.type === 'tile')!;
|
||||
stored.pictures = [frame, 7, '', null, 'gone'];
|
||||
(stored.props as Record<string, unknown>).kind = 'diagonal';
|
||||
(stored.props as Record<string, unknown>).playback = 'once';
|
||||
const read = tileOf(roundTrip(raw as unknown as Project));
|
||||
expect(read.pictures).toEqual([frame, 'gone']);
|
||||
expect(tileKind(read)).toBe('none');
|
||||
expect(tilePlayback(read)).toBe('loop');
|
||||
});
|
||||
|
||||
/**
|
||||
* #131 renamed the asset and made a plain tile the default. A project saved before that holds
|
||||
* `autotile` assets, and one of them without a stored kind meant a floor block: it has to open as
|
||||
* the same floor, not turn into a plain tile.
|
||||
*/
|
||||
it('reads an autotile of an older project as the tile it was', () => {
|
||||
const raw = {
|
||||
id: 'p',
|
||||
assets: [
|
||||
{ id: 's', type: 'scene', name: 'Main', props: {}, objects: [] },
|
||||
{ id: 'a', type: 'autotile', name: 'Old grass', props: {}, pictures: ['x'] },
|
||||
{ id: 'b', type: 'autotile', name: 'Old wall', props: { kind: 'wall', fps: 2 }, pictures: [] },
|
||||
{ id: 'c', type: 'tile', name: 'New', props: {}, pictures: [] },
|
||||
],
|
||||
};
|
||||
const project = deserializeProject(raw);
|
||||
const byId = (id: string) => project.assets.find((a): a is TileAsset => a.id === id && a.type === 'tile');
|
||||
expect(byId('a')?.pictures).toEqual(['x']);
|
||||
expect(tileKind(byId('a')!)).toBe('floor');
|
||||
expect(tileKind(byId('b')!)).toBe('wall');
|
||||
expect(byId('b')?.props.fps).toBe(2);
|
||||
expect(tileKind(byId('c')!)).toBe('none');
|
||||
expect(project.assets.some((a) => (a.type as string) === 'autotile')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('plain tile', () => {
|
||||
it('is what a new tile is', () => {
|
||||
const fixture = makeTile(96, 96, 'none');
|
||||
expect(tileKind(fixture.assetOf())).toBe('none');
|
||||
});
|
||||
|
||||
it('is one tile that ignores its neighbours', () => {
|
||||
expect(autotileCombinations('none')).toHaveLength(1);
|
||||
const lone = tileQuadrants('none', around({}));
|
||||
const all = { n: true, e: true, s: true, w: true, ne: true, se: true, sw: true, nw: true };
|
||||
// Every corner takes its own quarter of the picture – the picture drawn as it is.
|
||||
expect(lone).toEqual([
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 1, y: 0 },
|
||||
{ x: 0, y: 1 },
|
||||
{ x: 1, y: 1 },
|
||||
]);
|
||||
expect(tileQuadrants('none', all)).toEqual(lone);
|
||||
expect(tileQuadrants('none', around({ n: true, w: true }))).toEqual(lone);
|
||||
});
|
||||
|
||||
it('takes any square picture as the tile, an odd side too', () => {
|
||||
const fixture = makeTile(768, 576, 'none');
|
||||
const square = cutBlock(fixture, { x: 48, y: 0, w: 48, h: 48 });
|
||||
fixture.stack.execute(new AddTilePictureCommand(fixture.tileId, square));
|
||||
const info = readTile(fixture.project, fixture.assetOf());
|
||||
expect(info.problem).toBe('');
|
||||
expect(info.tile).toBe(48);
|
||||
expect(quadrantRect(info.pictures[0]!, info.quad, { x: 1, y: 1 })).toEqual({
|
||||
x: 72,
|
||||
y: 24,
|
||||
w: 24,
|
||||
h: 24,
|
||||
});
|
||||
|
||||
const odd = makeTile(25, 25, 'none');
|
||||
odd.stack.execute(new AddTilePictureCommand(odd.tileId, odd.texture.id));
|
||||
expect(readTile(odd.project, odd.assetOf()).tile).toBe(25);
|
||||
});
|
||||
|
||||
it('complains about a picture that is not square', () => {
|
||||
const fixture = makeTile(96, 144, 'none');
|
||||
fixture.stack.execute(new AddTilePictureCommand(fixture.tileId, fixture.texture.id));
|
||||
const info = readTile(fixture.project, fixture.assetOf());
|
||||
expect(info.problem).toContain('one square tile');
|
||||
expect(info.problem).toContain('96 × 144');
|
||||
expect(info.tile).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
function tileOf(project: Project): TileAsset {
|
||||
const asset = project.assets.find((a): a is TileAsset => a.type === 'tile');
|
||||
if (!asset) throw new Error('no tile in the project');
|
||||
return asset;
|
||||
}
|
||||
Reference in new issue
Block a user