Empty a deleted tile's cells on every tilemap (#268)
Deleting a tile left the cells painted with it in the tilemaps. They drew nothing, blocked nothing and did not show in the delete dialog, but still read as a tile for "Tile at" and stopped a fill. - DeleteAssetCommand empties the cells of a deleted tile on every layer of every tilemap in the same step. Undo puts the tile and its cells back together. - The delete dialog counts them, for example "3 cell(s) on 1 tilemap(s) are painted with it and become empty." (cellsPainted). - presentTile treats a cell naming a missing tile as empty. "Tile at" (World.tileAt) and floodFill use it, so cells already in existing files read and fill as empty. There is no migration. - The brush, the rectangle and the fill check the tile in hand. A tile deleted after it was picked no longer paints new invisible cells, the editor says "Choose a tile to paint with." instead. - Unchanged on purpose: the picker still reports such a cell as naming a tile that no longer exists, and erasing removes it. Tests: - ./check.sh: 195 files, 2823 tests, CHECK EXIT 0 (+6). - GM_STEPS=39,40: ALL PASSED, 100 checks (4 new in 40t). - Full e2e sum is now 1590 (1586 confirmed by CI on round 8, plus 4). - Counter-checks, all fail as expected: 5 in unit tests (delete without emptying, undo without cells, fill and "Tile at" without presentTile, wrong map count) and 3 in the browser (dialog without count, delete without emptying, brush without the check). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
14972d057f
commit
9b5eeaab57
11 files changed
+310
-38
No files matched your search
@@ -3,8 +3,10 @@
|
||||
* edge neighbours that hold the same tile – empty included – and nothing outside the grid.
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildAsset } from '../../src/core/commands/assetCommands';
|
||||
import type { TilemapLayer } from '../../src/core/model/Asset';
|
||||
import { cellKey, floodFill } from '../../src/core/model/tilemap';
|
||||
import { createEmptyProject } from '../../src/core/model/Project';
|
||||
import { type Cell, cellKey, floodFill } from '../../src/core/model/tilemap';
|
||||
|
||||
/** A layer drawn as text: one row per line, `.` empty, any other letter the tile of that name. */
|
||||
function layerOf(rows: string[]): TilemapLayer {
|
||||
@@ -26,22 +28,29 @@ function shown(rows: string[], filled: { x: number; y: number }[]): string[] {
|
||||
|
||||
const GRID = { columns: 5, rows: 4 };
|
||||
|
||||
/** A project with a tile for each of these letters, so a cell naming one of them names a real tile. */
|
||||
const PROJECT = createEmptyProject('p', 'P');
|
||||
for (const id of ['a', 'b', 'g', 'w']) PROJECT.assets.push(buildAsset('tile', id.toUpperCase(), id));
|
||||
|
||||
const fill = (props: Record<string, number>, layer: TilemapLayer, start: Cell, tileId: string) =>
|
||||
floodFill(PROJECT, props, layer, start, tileId);
|
||||
|
||||
describe('filling a tilemap layer', () => {
|
||||
it('takes the connected cells through edges, not through corners', () => {
|
||||
const rows = ['..w..', '.w.w.', 'w...w', '.w.w.'];
|
||||
const cells = floodFill(GRID, layerOf(rows), { x: 2, y: 2 }, 'g');
|
||||
const cells = fill(GRID, layerOf(rows), { x: 2, y: 2 }, 'g');
|
||||
expect(shown(rows, cells)).toEqual(['..w..', '.w*w.', 'w***w', '.w*w.']);
|
||||
});
|
||||
|
||||
it('fills a whole empty grid and stops at its edge', () => {
|
||||
const cells = floodFill(GRID, layerOf([]), { x: 0, y: 0 }, 'g');
|
||||
const cells = fill(GRID, layerOf([]), { x: 0, y: 0 }, 'g');
|
||||
expect(cells).toHaveLength(20);
|
||||
expect(cells.every(({ x, y }) => x >= 0 && y >= 0 && x < 5 && y < 4)).toBe(true);
|
||||
});
|
||||
|
||||
it('replaces one tile with another and leaves the empty cells beside it', () => {
|
||||
const rows = ['aa...', 'a.bb.', 'aaab.', '.....'];
|
||||
const cells = floodFill(GRID, layerOf(rows), { x: 0, y: 0 }, 'b');
|
||||
const cells = fill(GRID, layerOf(rows), { x: 0, y: 0 }, 'b');
|
||||
expect(shown(rows, cells)).toEqual(['**...', '*.bb.', '***b.', '.....']);
|
||||
});
|
||||
|
||||
@@ -49,26 +58,36 @@ describe('filling a tilemap layer', () => {
|
||||
const layer = layerOf(['aaaaa']);
|
||||
layer.cells[cellKey(5, 0)] = 'a';
|
||||
layer.cells[cellKey(-1, 0)] = 'a';
|
||||
const cells = floodFill(GRID, layer, { x: 2, y: 0 }, 'g');
|
||||
const cells = fill(GRID, layer, { x: 2, y: 0 }, 'g');
|
||||
expect(cells.map((c) => c.x).sort()).toEqual([0, 1, 2, 3, 4]);
|
||||
expect(floodFill(GRID, layer, { x: 5, y: 0 }, 'g')).toEqual([]);
|
||||
expect(fill(GRID, layer, { x: 5, y: 0 }, 'g')).toEqual([]);
|
||||
});
|
||||
|
||||
it('changes nothing when the cell already holds the tile', () => {
|
||||
expect(floodFill(GRID, layerOf(['aaaaa']), { x: 1, y: 0 }, 'a')).toEqual([]);
|
||||
expect(floodFill(GRID, layerOf([]), { x: 1, y: 1 }, '')).toEqual([]);
|
||||
expect(fill(GRID, layerOf(['aaaaa']), { x: 1, y: 0 }, 'a')).toEqual([]);
|
||||
expect(fill(GRID, layerOf([]), { x: 1, y: 1 }, '')).toEqual([]);
|
||||
});
|
||||
|
||||
it('can empty a region, too', () => {
|
||||
const rows = ['aa.a.'];
|
||||
const cells = floodFill(GRID, layerOf(rows), { x: 1, y: 0 }, '');
|
||||
const cells = fill(GRID, layerOf(rows), { x: 1, y: 0 }, '');
|
||||
expect(shown(rows, cells)).toEqual(['**.a.']);
|
||||
});
|
||||
|
||||
it('counts a cell naming a missing tile as empty, from wherever the fill starts (#268)', () => {
|
||||
// `x` is no tile of the project: those cells draw nothing, so they belong to the empty region.
|
||||
const rows = ['.x.w.', 'xx.w.', '...w.', 'wwww.'];
|
||||
const want = ['***w.', '***w.', '***w.', 'wwww.'];
|
||||
expect(shown(rows, fill(GRID, layerOf(rows), { x: 0, y: 0 }, 'g'))).toEqual(want);
|
||||
expect(shown(rows, fill(GRID, layerOf(rows), { x: 1, y: 1 }, 'g'))).toEqual(want);
|
||||
// Emptying from one changes nothing: it is empty already.
|
||||
expect(fill(GRID, layerOf(rows), { x: 1, y: 0 }, '')).toEqual([]);
|
||||
});
|
||||
|
||||
it('fills a large map in one go', () => {
|
||||
const props = { columns: 300, rows: 300 };
|
||||
const started = performance.now();
|
||||
expect(floodFill(props, layerOf([]), { x: 150, y: 150 }, 'g')).toHaveLength(90_000);
|
||||
expect(fill(props, layerOf([]), { x: 150, y: 150 }, 'g')).toHaveLength(90_000);
|
||||
expect(performance.now() - started).toBeLessThan(2000);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user