Add a terrain autotile kind for 6x4 sheets like TD Plains
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:
1 parent
32cb5b4ba5
commit
c1a702bc39
6 files changed
+297
-28
No files matched your search
+84
-2
@@ -16,8 +16,12 @@ const QUAD = 24;
|
||||
|
||||
/**
|
||||
* The sheet as a paste: 8 × 10 quarter tiles. Floor blocks at x = 0 and x = 96 (told apart by their
|
||||
* blue), the wall block below the first, the rest transparent.
|
||||
* blue), the wall block below the first, and in the corner left over a terrain block of 16 px tiles,
|
||||
* 12 × 8 quarters of 8 px.
|
||||
*/
|
||||
const TERRAIN_QUAD = 8;
|
||||
const TERRAIN_AT = { x: QUAD * 4, y: QUAD * 6 };
|
||||
|
||||
const PASTE_SHEET = `(async () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = ${QUAD * 8};
|
||||
@@ -32,6 +36,11 @@ const PASTE_SHEET = `(async () => {
|
||||
g.fillStyle = 'rgb(' + r + ',' + gr + ',' + b + ')';
|
||||
g.fillRect(qx * ${QUAD}, qy * ${QUAD}, ${QUAD}, ${QUAD});
|
||||
}
|
||||
for (let qy = 0; qy < 8; qy++)
|
||||
for (let qx = 0; qx < 12; qx++) {
|
||||
g.fillStyle = 'rgb(' + (20 + qx * 20) + ',' + (40 + qy * 25) + ',240)';
|
||||
g.fillRect(${TERRAIN_AT.x} + qx * ${TERRAIN_QUAD}, ${TERRAIN_AT.y} + qy * ${TERRAIN_QUAD}, ${TERRAIN_QUAD}, ${TERRAIN_QUAD});
|
||||
}
|
||||
const blob = await new Promise((resolve) => canvas.toBlob(resolve, 'image/png'));
|
||||
const transfer = new DataTransfer();
|
||||
transfer.items.add(new File([blob], 'autotiles.png', { type: 'image/png' }));
|
||||
@@ -41,7 +50,11 @@ const PASTE_SHEET = `(async () => {
|
||||
/** The colour a quarter of a block was drawn in – the same formula as the paste above. */
|
||||
export function quadColour(kind, qx, qy, step = 0) {
|
||||
const [r, g, b] =
|
||||
kind === 'wall' ? [30 + qy * 60, 40 + qx * 40, 20] : [40 + qx * 60, 30 + qy * 40, step ? 200 : 90];
|
||||
kind === 'terrain'
|
||||
? [20 + qx * 20, 40 + qy * 25, 240]
|
||||
: kind === 'wall'
|
||||
? [30 + qy * 60, 40 + qx * 40, 20]
|
||||
: [40 + qx * 60, 30 + qy * 40, step ? 200 : 90];
|
||||
return `#${[r, g, b].map((c) => c.toString(16).padStart(2, '0')).join('')}`;
|
||||
}
|
||||
|
||||
@@ -95,6 +108,7 @@ export async function tile(t) {
|
||||
{ id: 'at-floor-2', name: 'Floor 2', x: 96, y: 0, w: 96, h: 144 },
|
||||
{ id: 'at-wall', name: 'Wall', x: 0, y: 144, w: 96, h: 96 },
|
||||
{ id: 'at-plain', name: 'Plain', x: 0, y: 0, w: 48, h: 48 },
|
||||
{ id: 'at-terrain', name: 'Terrain', x: ${TERRAIN_AT.x}, y: ${TERRAIN_AT.y}, w: 96, h: 64 },
|
||||
);
|
||||
__gm.state.project.assets.push({
|
||||
id: 'at-grass',
|
||||
@@ -343,6 +357,74 @@ export async function tile(t) {
|
||||
JSON.stringify(board),
|
||||
);
|
||||
|
||||
// 39j. A terrain block: whole tiles where the sheet draws them, composed where it does not.
|
||||
const terrainAt = await json('__gm.tile.kindPickerAt()');
|
||||
await t.click(terrainAt.x, terrainAt.y);
|
||||
await clickText('Terrain (6 × 4)');
|
||||
await mutate("__gm.state.project.assets.find((a) => a.id === 'at-grass').pictures = ['at-terrain']");
|
||||
await settle();
|
||||
board = await json('__gm.tile.board()');
|
||||
check(
|
||||
'a 6 × 4 terrain block gives 47 tiles at 16 px',
|
||||
board.kind === 'terrain' && board.combinations === 47 && board.tile === 16 && board.problem === '',
|
||||
JSON.stringify(board),
|
||||
);
|
||||
const wholeTile = (tx, ty) => [
|
||||
[2 * tx, 2 * ty],
|
||||
[2 * tx + 1, 2 * ty],
|
||||
[2 * tx, 2 * ty + 1],
|
||||
[2 * tx + 1, 2 * ty + 1],
|
||||
];
|
||||
const terrainLone = await tileReads(t, 0, wholeTile(0, 3), 'terrain');
|
||||
const terrainInside = await tileReads(
|
||||
t,
|
||||
await json('__gm.tile.sampleTile(3, 2)'),
|
||||
wholeTile(2, 1),
|
||||
'terrain',
|
||||
);
|
||||
// Row 5, column 3 of the sample map has the hole in its north-east and a gap in its south-west.
|
||||
const terrainPair = await tileReads(
|
||||
t,
|
||||
await json('__gm.tile.sampleTile(5, 3)'),
|
||||
wholeTile(5, 2),
|
||||
'terrain',
|
||||
);
|
||||
check(
|
||||
'the lone, the surrounded and the two-notch tile are the whole tiles the sheet draws',
|
||||
terrainLone.ok && terrainInside.ok && terrainPair.ok,
|
||||
`${terrainLone.detail} | ${terrainInside.detail} | ${terrainPair.detail}`,
|
||||
);
|
||||
// An edge with a notch is not in the sheet: the left edge tile with the notch put in, and the
|
||||
// quarter below the notch brought along from the same notch tile.
|
||||
const composed = [
|
||||
[2, 2],
|
||||
[9, 2],
|
||||
[2, 3],
|
||||
[9, 3],
|
||||
];
|
||||
let composedIndex = -1;
|
||||
for (let i = 0; i < board.combinations && composedIndex < 0; i++) {
|
||||
const quadrants = (await json(`__gm.tile.tile(${i})`))?.quadrants ?? [];
|
||||
if (JSON.stringify(quadrants.map((q) => [q.x, q.y])) === JSON.stringify(composed)) composedIndex = i;
|
||||
}
|
||||
const edgeNotch =
|
||||
composedIndex < 0
|
||||
? { ok: false, detail: 'no such tile' }
|
||||
: await tileReads(t, composedIndex, composed, 'terrain');
|
||||
check('an edge with a notch is composed from the edge and the notch tile', edgeNotch.ok, edgeNotch.detail);
|
||||
await shot('39-terrain');
|
||||
await mutate(
|
||||
"__gm.state.project.assets.find((a) => a.id === 'at-grass').pictures = ['at-floor-1', 'at-floor-2']",
|
||||
);
|
||||
await undo();
|
||||
await settle();
|
||||
board = await json('__gm.tile.board()');
|
||||
check(
|
||||
'and undo makes it a floor block again',
|
||||
board.kind === 'floor' && board.combinations === 47 && board.problem === '',
|
||||
JSON.stringify(board),
|
||||
);
|
||||
|
||||
// Leave the tab as the rest of the suite expects to find the editor.
|
||||
await openTab(t, 'scene');
|
||||
await evaluate('__gm.state.select(null)');
|
||||
|
||||
Reference in new issue
Block a user