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
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
CreateAssetCommand,
|
||||
CreateTextureCommand,
|
||||
DeleteAssetCommand,
|
||||
SetAssetPropertyCommand,
|
||||
} from '../../src/core/commands/assetCommands';
|
||||
import { AddTilePictureCommand } from '../../src/core/commands/tileCommands';
|
||||
@@ -37,6 +38,7 @@ import {
|
||||
tilemapSize,
|
||||
} from '../../src/core/model/tilemap';
|
||||
import { DIMMED_LAYER_ALPHA, layerAlpha } from '../../src/viewport/tilemap';
|
||||
import { cellsPainted } from '../../src/core/model/usages';
|
||||
import { groupByTile, tileMesh } from '../../src/viewport/tilemapMesh';
|
||||
import { makeProject, roundTrip } from '../helpers';
|
||||
|
||||
@@ -182,7 +184,7 @@ describe('painting a tilemap', () => {
|
||||
const f = makeTilemap();
|
||||
f.paint([{ x: 0, y: 0 }]);
|
||||
const layer = f.assetOf().layers[0]!;
|
||||
expect(floodFill(f.assetOf().props, layer, { x: 0, y: 0 }, f.tileId)).toEqual([]);
|
||||
expect(floodFill(f.project, f.assetOf().props, layer, { x: 0, y: 0 }, f.tileId)).toEqual([]);
|
||||
expect(paintTiles(layer, f.tilemapId, [])).toBeNull();
|
||||
});
|
||||
|
||||
@@ -554,3 +556,66 @@ describe('the mesh a layer becomes', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleting a tile that is painted (#268)', () => {
|
||||
/** Grass on two layers of one map and on a second map, with a second tile beside it. */
|
||||
function painted() {
|
||||
const f = makeTilemap();
|
||||
const stone = new CreateAssetCommand('tile', 'Stone');
|
||||
f.stack.execute(stone);
|
||||
f.paint([
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 1, y: 0 },
|
||||
]);
|
||||
f.paint([{ x: 2, y: 0 }], stone.asset.id);
|
||||
const upper = new AddTilemapLayerCommand(f.tilemapId);
|
||||
f.stack.execute(upper);
|
||||
f.stack.execute(new PaintTilesCommand(f.tilemapId, upper.layerId, [{ x: 5, y: 5, tileId: f.tileId }]));
|
||||
const other = new CreateAssetCommand('tilemap', 'Cave');
|
||||
f.stack.execute(other);
|
||||
const otherLayer = (findAsset(f.project, other.asset.id) as TilemapAsset).layers[0]!.id;
|
||||
f.stack.execute(new PaintTilesCommand(other.asset.id, otherLayer, [{ x: 3, y: 3, tileId: f.tileId }]));
|
||||
const allCells = () =>
|
||||
f.project.assets.flatMap((a) => (a.type === 'tilemap' ? a.layers.map((l) => ({ ...l.cells })) : []));
|
||||
return { ...f, stoneId: stone.asset.id, allCells };
|
||||
}
|
||||
|
||||
it('counts the cells and the maps they are on', () => {
|
||||
const f = painted();
|
||||
expect(cellsPainted(f.project, f.tileId)).toEqual({ tilemaps: 2, cells: 4 });
|
||||
expect(cellsPainted(f.project, f.stoneId)).toEqual({ tilemaps: 1, cells: 1 });
|
||||
});
|
||||
|
||||
it('empties those cells in the same step and leaves every other tile where it was', () => {
|
||||
const f = painted();
|
||||
f.stack.execute(new DeleteAssetCommand(f.tileId));
|
||||
expect(findAsset(f.project, f.tileId)).toBeUndefined();
|
||||
expect(f.allCells()).toEqual([{ '2,0': f.stoneId }, {}, {}]);
|
||||
expect(cellsPainted(f.project, f.tileId)).toEqual({ tilemaps: 0, cells: 0 });
|
||||
});
|
||||
|
||||
it('an undo brings the tile and its cells back together, a redo takes both again', () => {
|
||||
const f = painted();
|
||||
const before = f.allCells();
|
||||
f.stack.execute(new DeleteAssetCommand(f.tileId));
|
||||
f.stack.undo();
|
||||
expect(findAsset(f.project, f.tileId)?.type).toBe('tile');
|
||||
expect(f.allCells()).toEqual(before);
|
||||
const grass = layerTiles(f.project, f.assetOf().props, f.assetOf().layers[0]!).filter(
|
||||
(tile) => tile.tileId === f.tileId,
|
||||
);
|
||||
expect(grass.map(({ x, y }) => `${x},${y}`)).toEqual(['0,0', '1,0']);
|
||||
f.stack.redo();
|
||||
expect(f.allCells()).toEqual([{ '2,0': f.stoneId }, {}, {}]);
|
||||
});
|
||||
|
||||
it('deleting a tile nobody painted leaves the maps alone', () => {
|
||||
const f = painted();
|
||||
const lone = new CreateAssetCommand('tile', 'Lone');
|
||||
f.stack.execute(lone);
|
||||
const layers = f.project.assets.flatMap((a) => (a.type === 'tilemap' ? a.layers : []));
|
||||
const cellObjects = layers.map((l) => l.cells);
|
||||
f.stack.execute(new DeleteAssetCommand(lone.asset.id));
|
||||
expect(layers.every((l, i) => l.cells === cellObjects[i])).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user