Add a terrain autotile kind for 6x4 sheets like TD Plains
game-maker / gate (push) Successful in 3m7s
game-maker / suite (push) Successful in 9m54s

Sheets such as "TD - Tileset - Plains" draw an autotile as a 6 x 4
block: a 3 x 3 nine-slice, a vertical and a horizontal strip and a
single tile on the left, the inner corners on the right. The floor
kind only knew the RPG Maker 2 x 3 layout, so these sheets could not
be used as autotiles at all.

- New TileKind "terrain" with 16 px tiles in a 6 x 4 block. All 47
  neighbour combinations are resolved per quarter, so the renderer,
  the collision and the palette stay untouched.
- Whole tiles are used where the sheet draws them: the 16 side cases,
  the four single notches and the two diagonal pairs. Hand-drawn
  strips and single blocks therefore look as drawn.
- Only the missing cases are composed, for example an edge with a
  notch or two notches side by side. They take the side tile and put
  the notch quarters in.
- A notch also takes the quarter above or below from its notch tile
  when fill would stand there. Tall cliff notches keep their wall.
  On ground and stones fill meets fill, so nothing changes there.
- The label rule for floor and terrain now runs through one notch
  function. Floor still passes the MZ table test.

Not touched: the tile board only refits on open and on Fit. Switching
an open tile to a kind with another tile size leaves it small until
Fit is pressed. That was the case before.

Tests:
- ./check.sh: 156 files, 2290 tests, CHECK EXIT 0 (before 2283).
- e2e 39,40: ALL PASSED, 90 checks (before 86). 39j checks 47 tiles,
  pixels of single, notch, pair and composed tiles, and undo to floor.
- Counter-checks: without whole notch and pair tiles one unit test
  and one e2e check fail. Without the taken quarter one of each
  fails. Originals restored by cmp and rebuilt.
- Check sum: 1363 measured by CI plus 4 is 1367, computed only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-09-24 06:14:51 +02:00
1 parent 32cb5b4ba5
commit c1a702bc39
6 files changed
+297 -28

No files matched your search

+108
View File
@@ -274,6 +274,114 @@ describe('autotile blocks', () => {
});
});
/** The four quarters of the whole tile at tile column `tx`, row `ty`, written out as [x, y]. */
const whole = (tx: number, ty: number) => [
[2 * tx, 2 * ty],
[2 * tx + 1, 2 * ty],
[2 * tx, 2 * ty + 1],
[2 * tx + 1, 2 * ty + 1],
];
const pairs = (quadrants: { x: number; y: number }[]) => quadrants.map((q) => [q.x, q.y]);
const ALL_SIDES = { n: true, e: true, s: true, w: true };
const ALL_AROUND = { ...ALL_SIDES, ne: true, se: true, sw: true, nw: true };
describe('terrain block', () => {
it('produces 47 different tiles, all inside the 6 × 4 block and none from its two empty tiles', () => {
const terrain = autotileCombinations('terrain');
expect(terrain).toHaveLength(47);
for (const { quadrants } of terrain)
for (const q of quadrants) {
expect(q.x).toBeLessThan(12);
expect(q.y).toBeLessThan(8);
expect(q.x >= 8 && q.y >= 6).toBe(false);
}
});
it('takes each of the 16 ways the sides join as a whole tile of the 4 × 4 on the left', () => {
const drawn = new Set<string>();
for (let mask = 0; mask < 16; mask++) {
const sides = { n: !!(mask & 1), e: !!(mask & 2), s: !!(mask & 4), w: !!(mask & 8) };
const quadrants = pairs(
tileQuadrants('terrain', around({ ...sides, ne: true, se: true, sw: true, nw: true })),
);
const [x, y] = quadrants[0]!;
expect(quadrants).toEqual(whole(x! / 2, y! / 2));
expect(x).toBeLessThan(8);
drawn.add(`${x},${y}`);
}
expect(drawn.size).toBe(16);
expect(pairs(tileQuadrants('terrain', around({})))).toEqual(whole(0, 3));
expect(pairs(tileQuadrants('terrain', around({ n: true, s: true })))).toEqual(whole(0, 1));
expect(pairs(tileQuadrants('terrain', around({ e: true, w: true })))).toEqual(whole(2, 3));
expect(pairs(tileQuadrants('terrain', around(ALL_AROUND)))).toEqual(whole(2, 1));
expect(pairs(loneTileQuadrants('terrain'))).toEqual(whole(0, 3));
});
it('takes a single notch and the two diagonal pairs whole from the columns on the right', () => {
expect(pairs(tileQuadrants('terrain', around({ ...ALL_AROUND, se: false })))).toEqual(whole(4, 0));
expect(pairs(tileQuadrants('terrain', around({ ...ALL_AROUND, sw: false })))).toEqual(whole(5, 0));
expect(pairs(tileQuadrants('terrain', around({ ...ALL_AROUND, ne: false })))).toEqual(whole(4, 1));
expect(pairs(tileQuadrants('terrain', around({ ...ALL_AROUND, nw: false })))).toEqual(whole(5, 1));
expect(pairs(tileQuadrants('terrain', around({ ...ALL_SIDES, ne: true, sw: true })))).toEqual(
whole(4, 2),
);
expect(pairs(tileQuadrants('terrain', around({ ...ALL_SIDES, nw: true, se: true })))).toEqual(
whole(5, 2),
);
});
it('puts the notch into an edge tile and brings the quarter over or under it along', () => {
// Joined north, east and south, open to the west and the north-east: the left edge tile, with
// the top-right notch and, below it, the quarter of the same notch tile.
const edge = pairs(tileQuadrants('terrain', around({ n: true, e: true, s: true, se: true })));
expect(edge).toEqual([
[2, 2],
[9, 2],
[2, 3],
[9, 3],
]);
// Two notches side by side, the south-east and the south-west: each brings its top quarter.
const both = pairs(tileQuadrants('terrain', around({ ...ALL_SIDES, ne: true, nw: true })));
expect(both).toEqual([
[10, 0],
[9, 0],
[10, 1],
[9, 1],
]);
});
it('leaves an edge above a notch alone – only a plain inside quarter is brought along', () => {
// Open to the north: the top edge tile keeps its top half, only the south-east notch goes in.
const topEdge = pairs(tileQuadrants('terrain', around({ e: true, s: true, w: true, sw: true })));
expect(topEdge).toEqual([
[4, 0],
[5, 0],
[4, 1],
[9, 1],
]);
});
it('labels the notched corners like a floor does', () => {
const labels = autotileCombinations('terrain').map((c) => c.label);
expect(labels[0]).toBe('·');
expect(labels).toContain('NESW-ne,se,sw,nw');
expect(labels).toContain('NES-ne');
});
it('reads a 6 × 4 block as a terrain tile and says so when the block is the wrong shape', () => {
const fixture = makeTile(96, 192, 'terrain');
fixture.stack.execute(
new AddTilePictureCommand(fixture.tileId, cutBlock(fixture, { x: 0, y: 0, w: 96, h: 64 })),
);
const info = readTile(fixture.project, fixture.assetOf());
expect(info.problem).toBe('');
expect(info.tile).toBe(16);
const wrong = makeTile(96, 192, 'terrain');
wrong.stack.execute(new AddTilePictureCommand(wrong.tileId, wrong.texture.id));
expect(readTile(wrong.project, wrong.assetOf()).problem).toContain('terrain block is 6 × 4 tiles');
});
});
describe('tile asset', () => {
it('derives the tile size from the block and accepts a whole texture as one', () => {
const fixture = makeTile(96, 144);