The tile list in the tilemap section is a palette of pictures: each tile shows its lone tile (the four convex corners for floor and wall, the picture itself for none) with its name below, laid out with the image-picker grid (#139)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4c411c1ff3
commit
bfbcbe604c
9 files changed
+331
-28
No files matched your search
@@ -38,7 +38,7 @@ npm run build # writes dist/ and regenerates index.html; the folder is wh
|
|||||||
The browser suite and the three example games need a machine with a GPU:
|
The browser suite and the three example games need a machine with a GPU:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 1021 checks (GM_STEPS=20 for one step)
|
node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 1023 checks (GM_STEPS=20 for one step)
|
||||||
node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs
|
node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs
|
||||||
node tools/seed-tetris.mjs && node tools/smoke-tetris.mjs
|
node tools/seed-tetris.mjs && node tools/smoke-tetris.mjs
|
||||||
node tools/seed-actor.mjs && node tools/smoke-actor.mjs
|
node tools/seed-actor.mjs && node tools/smoke-actor.mjs
|
||||||
|
|||||||
@@ -118,6 +118,14 @@ const NO_NEIGHBOURS: Neighbours = {
|
|||||||
nw: false,
|
nw: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The tile standing for its kind in a palette (#139): the lone tile, all four convex corners. It is
|
||||||
|
* the one RPG Maker shows too – the block's top-left tile, which is pixel for pixel the same.
|
||||||
|
*/
|
||||||
|
export function loneTileQuadrants(kind: TileKind): TileQuadrants {
|
||||||
|
return tileQuadrants(kind, NO_NEIGHBOURS);
|
||||||
|
}
|
||||||
|
|
||||||
const SIDES = ['n', 'e', 's', 'w'] as const;
|
const SIDES = ['n', 'e', 's', 'w'] as const;
|
||||||
const DIAGONALS = ['ne', 'se', 'sw', 'nw'] as const;
|
const DIAGONALS = ['ne', 'se', 'sw', 'nw'] as const;
|
||||||
|
|
||||||
|
|||||||
@@ -559,9 +559,13 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
|
|||||||
const button = currentTilemapSection()?.tools.get(tool as TilemapTool);
|
const button = currentTilemapSection()?.tools.get(tool as TilemapTool);
|
||||||
return button ? centreOf(button) : null;
|
return button ? centreOf(button) : null;
|
||||||
},
|
},
|
||||||
tileButtonAt: (assetId: string) => {
|
tileButtonAt: (assetId: string) =>
|
||||||
const button = currentTilemapSection()?.tiles.get(assetId);
|
currentTilemapSection()?.palette?.swatchGlobal(assetId)?.centre ?? null,
|
||||||
return button ? centreOf(button) : null;
|
/** A tile's box in the palette: its centre, where its picture sits and the palette's columns (#139). */
|
||||||
|
tileSwatch: (assetId: string) => {
|
||||||
|
const palette = currentTilemapSection()?.palette;
|
||||||
|
const swatch = palette?.swatchGlobal(assetId);
|
||||||
|
return palette && swatch ? { ...swatch, columns: palette.columns } : null;
|
||||||
},
|
},
|
||||||
addLayerAt: () => {
|
addLayerAt: () => {
|
||||||
const button = currentTilemapSection()?.addLayer;
|
const button = currentTilemapSection()?.addLayer;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import type { TilemapAsset } from '../../core/model/Asset';
|
|||||||
import { isLayerVisible } from '../../core/model/tilemap';
|
import { isLayerVisible } from '../../core/model/tilemap';
|
||||||
import { Button, Checkbox, HStack, Theme } from '../../ui';
|
import { Button, Checkbox, HStack, Theme } from '../../ui';
|
||||||
import type { EditorState, TilemapTool } from '../state';
|
import type { EditorState, TilemapTool } from '../state';
|
||||||
|
import { TilePalette } from '../tile/TilePalette';
|
||||||
import { addGap, addNote, addTitle, type InspectorHost } from './host';
|
import { addGap, addNote, addTitle, type InspectorHost } from './host';
|
||||||
|
|
||||||
const TOOLS: { tool: TilemapTool; label: string; hint: string }[] = [
|
const TOOLS: { tool: TilemapTool; label: string; hint: string }[] = [
|
||||||
@@ -42,7 +43,8 @@ export function paintedLayerId(state: EditorState, asset: TilemapAsset): string
|
|||||||
*/
|
*/
|
||||||
export interface TilemapSectionWidgets {
|
export interface TilemapSectionWidgets {
|
||||||
tools: Map<TilemapTool, Button>;
|
tools: Map<TilemapTool, Button>;
|
||||||
tiles: Map<string, Button>;
|
/** Null while the project has no tile. */
|
||||||
|
palette: TilePalette | null;
|
||||||
layers: { id: string; name: Button; visible: Checkbox; up: Button; down: Button; remove: Button }[];
|
layers: { id: string; name: Button; visible: Checkbox; up: Button; down: Button; remove: Button }[];
|
||||||
addLayer: Button;
|
addLayer: Button;
|
||||||
}
|
}
|
||||||
@@ -58,7 +60,7 @@ export function buildTilemapSection(host: InspectorHost, asset: TilemapAsset): v
|
|||||||
const paint = state.tilemapPaint;
|
const paint = state.tilemapPaint;
|
||||||
const widgets: TilemapSectionWidgets = {
|
const widgets: TilemapSectionWidgets = {
|
||||||
tools: new Map(),
|
tools: new Map(),
|
||||||
tiles: new Map(),
|
palette: null,
|
||||||
layers: [],
|
layers: [],
|
||||||
addLayer: new Button('Add layer', {
|
addLayer: new Button('Add layer', {
|
||||||
onClick: () => state.execute(new AddTilemapLayerCommand(asset.id)),
|
onClick: () => state.execute(new AddTilemapLayerCommand(asset.id)),
|
||||||
@@ -79,23 +81,19 @@ export function buildTilemapSection(host: InspectorHost, asset: TilemapAsset): v
|
|||||||
addNote(host, 'Shift-drag still pans the view.', 'small');
|
addNote(host, 'Shift-drag still pans the view.', 'small');
|
||||||
|
|
||||||
addTitle(host, 'TILES');
|
addTitle(host, 'TILES');
|
||||||
const tiles = state.project.assets.filter((a) => a.type === 'tile');
|
const tiles = tileIdsOf(state);
|
||||||
if (tiles.length === 0) addNote(host, 'The project has no tile yet.');
|
if (tiles.length === 0) addNote(host, 'The project has no tile yet.');
|
||||||
for (const tile of tiles) {
|
else {
|
||||||
const button = new Button(tile.name, {
|
// Choosing a tile is how one starts painting, so it reaches for the brush by itself.
|
||||||
align: 'left',
|
widgets.palette = new TilePalette(state, tiles, paint.tileId, (tileId) =>
|
||||||
// Choosing a tile is how one starts painting, so it reaches for the brush by itself.
|
state.setTilemapPaint({
|
||||||
onClick: () =>
|
tileId,
|
||||||
state.setTilemapPaint({
|
tool: paint.tool === 'select' || paint.tool === 'erase' ? 'brush' : paint.tool,
|
||||||
tileId: tile.id,
|
}),
|
||||||
tool: paint.tool === 'select' || paint.tool === 'erase' ? 'brush' : paint.tool,
|
);
|
||||||
}),
|
host.body.add(widgets.palette);
|
||||||
});
|
|
||||||
button.toggled = paint.tileId === tile.id;
|
|
||||||
widgets.tiles.set(tile.id, button);
|
|
||||||
host.body.add(button);
|
|
||||||
}
|
}
|
||||||
if (tiles.length > 0 && !tiles.some((a) => a.id === paint.tileId))
|
if (tiles.length > 0 && !tiles.includes(paint.tileId))
|
||||||
addNote(host, 'Choose a tile to paint with.', 'small');
|
addNote(host, 'Choose a tile to paint with.', 'small');
|
||||||
|
|
||||||
addTitle(host, 'LAYERS');
|
addTitle(host, 'LAYERS');
|
||||||
@@ -140,13 +138,19 @@ export function buildTilemapSection(host: InspectorHost, asset: TilemapAsset): v
|
|||||||
host.body.add(widgets.addLayer);
|
host.body.add(widgets.addLayer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function tileIdsOf(state: EditorState): string[] {
|
||||||
|
return state.project.assets.filter((a) => a.type === 'tile').map((a) => a.id);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* What the section is built from, for the inspector's rebuild check: the layers and what is
|
* What the section is built from, for the inspector's rebuild check: the layers, the tiles on offer
|
||||||
* painted with. The cells themselves are deliberately absent – a stroke must not rebuild the
|
* and what is painted with. The cells themselves are deliberately absent – a stroke must not
|
||||||
* panel under the pointer that is drawing it.
|
* rebuild the panel under the pointer that is drawing it – and so are the tiles' pictures and
|
||||||
|
* names, which the palette repaints by itself.
|
||||||
*/
|
*/
|
||||||
export function tilemapSignature(state: EditorState, asset: TilemapAsset): string {
|
export function tilemapSignature(state: EditorState, asset: TilemapAsset): string {
|
||||||
const paint = state.tilemapPaint;
|
const paint = state.tilemapPaint;
|
||||||
const layers = asset.layers.map((l) => `${l.id}:${l.name}:${isLayerVisible(l) ? 1 : 0}`).join(',');
|
const layers = asset.layers.map((l) => `${l.id}:${l.name}:${isLayerVisible(l) ? 1 : 0}`).join(',');
|
||||||
return `${paint.tool}|${paint.tileId}|${paintedLayerId(state, asset)}|${layers}`;
|
const tiles = tileIdsOf(state).join(',');
|
||||||
|
return `${paint.tool}|${paint.tileId}|${paintedLayerId(state, asset)}|${layers}|${tiles}`;
|
||||||
}
|
}
|
||||||
@@ -26,10 +26,21 @@ export interface GridMetrics {
|
|||||||
height: number;
|
height: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function gridMetrics(width: number, count: number, gap = TILE_GAP): GridMetrics {
|
/** How small and how large a tile may get; the tile palette of a tilemap wants smaller ones (#139). */
|
||||||
const columns = Math.max(1, Math.floor((width + gap) / (TILE_MIN + gap)));
|
export interface TileSizes {
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function gridMetrics(
|
||||||
|
width: number,
|
||||||
|
count: number,
|
||||||
|
gap = TILE_GAP,
|
||||||
|
sizes: TileSizes = { min: TILE_MIN, max: TILE_MAX },
|
||||||
|
): GridMetrics {
|
||||||
|
const columns = Math.max(1, Math.floor((width + gap) / (sizes.min + gap)));
|
||||||
const spread = (width - gap * (columns - 1)) / columns;
|
const spread = (width - gap * (columns - 1)) / columns;
|
||||||
const tile = Math.max(TILE_MIN, Math.min(TILE_MAX, Math.floor(spread)));
|
const tile = Math.max(sizes.min, Math.min(sizes.max, Math.floor(spread)));
|
||||||
const rows = Math.ceil(count / columns);
|
const rows = Math.ceil(count / columns);
|
||||||
const height = rows * (tile + CAPTION_HEIGHT) + Math.max(0, rows - 1) * gap;
|
const height = rows * (tile + CAPTION_HEIGHT) + Math.max(0, rows - 1) * gap;
|
||||||
return { columns, tile, rows, height };
|
return { columns, tile, rows, height };
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
import { Container, type FederatedPointerEvent, Graphics, Sprite, Text } from 'pixi.js';
|
||||||
|
import { loneTileQuadrants } from '../../core/model/autotile';
|
||||||
|
import { findAssetOf } from '../../core/model/Project';
|
||||||
|
import { quadrantRef, readTile } from '../../core/model/tile';
|
||||||
|
import {
|
||||||
|
drawControlBox,
|
||||||
|
ScrollContainer,
|
||||||
|
type Size,
|
||||||
|
Stack,
|
||||||
|
Theme,
|
||||||
|
textStyle,
|
||||||
|
truncate,
|
||||||
|
Widget,
|
||||||
|
} from '../../ui';
|
||||||
|
import { textureLibrary } from '../../viewport/TextureLibrary';
|
||||||
|
import { gridMetrics, type GridMetrics, tileAt, tileRect } from '../texture/imageGrid';
|
||||||
|
import type { EditorState } from '../state';
|
||||||
|
|
||||||
|
/** Smaller than the image picker's: a tile is 48 px, and a palette should show several in a row. */
|
||||||
|
const SIZES = { min: 56, max: 72 };
|
||||||
|
const GAP = 6;
|
||||||
|
/** Inset of the picture inside its box, so the selection frame stays visible around it. */
|
||||||
|
const PAD = 4;
|
||||||
|
|
||||||
|
interface Swatch {
|
||||||
|
box: Container;
|
||||||
|
background: Graphics;
|
||||||
|
sprites: [Sprite, Sprite, Sprite, Sprite];
|
||||||
|
/** Stands where the picture would be while the tile has none that fits. */
|
||||||
|
empty: Text;
|
||||||
|
caption: Text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The tiles of the project as a grid of pictures (#139), in the painting section of a tilemap: one
|
||||||
|
* sees what one paints with, and it takes a fraction of the room a list of buttons did. Each tile is
|
||||||
|
* shown alone – for an autotile the lone tile of its block, the one RPG Maker shows as well – and
|
||||||
|
* as its first picture: the inspector has no clock, and a still picture names a tile well enough.
|
||||||
|
*
|
||||||
|
* The tiles are fixed when it is built; the section rebuilds when that list changes. A picture
|
||||||
|
* edited or a texture arriving only repaints.
|
||||||
|
*/
|
||||||
|
export class TilePalette extends Widget {
|
||||||
|
private metrics: GridMetrics = gridMetrics(0, 0, GAP, SIZES);
|
||||||
|
private readonly swatches: Swatch[] = [];
|
||||||
|
private hoveredIndex = -1;
|
||||||
|
private pressedIndex = -1;
|
||||||
|
/** The width a relayout was last asked for, so a height that will not settle cannot loop. */
|
||||||
|
private askedAt = -1;
|
||||||
|
private readonly stops: (() => void)[];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly state: EditorState,
|
||||||
|
private readonly tileIds: readonly string[],
|
||||||
|
private readonly selectedId: string,
|
||||||
|
private readonly onPick: (tileId: string) => void,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
this.cursor = Theme.hoverCursor;
|
||||||
|
for (const _ of tileIds) this.swatches.push(this.buildSwatch());
|
||||||
|
this.stops = [
|
||||||
|
state.on('project', () => this.invalidate()),
|
||||||
|
textureLibrary.onChange(() => this.invalidate()),
|
||||||
|
];
|
||||||
|
|
||||||
|
this.on('pointermove', (e: FederatedPointerEvent) => this.hover(this.indexUnder(e)));
|
||||||
|
this.on('pointerout', () => {
|
||||||
|
this.pressedIndex = -1;
|
||||||
|
this.hover(-1);
|
||||||
|
});
|
||||||
|
this.on('pointerdown', (e: FederatedPointerEvent) => {
|
||||||
|
if (e.button === 0) this.pressedIndex = this.indexUnder(e);
|
||||||
|
});
|
||||||
|
this.on('pointerup', (e: FederatedPointerEvent) => {
|
||||||
|
const index = this.indexUnder(e);
|
||||||
|
const pressed = this.pressedIndex;
|
||||||
|
this.pressedIndex = -1;
|
||||||
|
if (e.button === 0 && index >= 0 && index === pressed) this.onPick(this.tileIds[index]!);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Where a tile's box and its picture sit on screen (test hook); null when it is not offered. */
|
||||||
|
swatchGlobal(
|
||||||
|
tileId: string,
|
||||||
|
): { centre: { x: number; y: number }; picture: { x: number; y: number; w: number } } | null {
|
||||||
|
const index = this.tileIds.indexOf(tileId);
|
||||||
|
if (index < 0) return null;
|
||||||
|
const rect = tileRect(index, this.metrics, GAP);
|
||||||
|
const origin = this.getGlobalPosition();
|
||||||
|
const x = origin.x + rect.x;
|
||||||
|
const y = origin.y + rect.y;
|
||||||
|
return {
|
||||||
|
centre: { x: x + rect.w / 2, y: y + this.metrics.tile / 2 },
|
||||||
|
picture: { x: x + PAD, y: y + PAD, w: this.metrics.tile - PAD * 2 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** How many tiles share a row – the check scripts read this. */
|
||||||
|
get columns(): number {
|
||||||
|
return this.metrics.columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
override measure(): Size {
|
||||||
|
const width = this.w || this.expectedWidth();
|
||||||
|
return { w: this.minWidth, h: gridMetrics(width, this.tileIds.length, GAP, SIZES).height };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The height depends on the width, and the first measure comes before any width was given. So
|
||||||
|
* the palette guesses it from the scroll area it sits in, less the padding of the stacks between.
|
||||||
|
*/
|
||||||
|
private expectedWidth(): number {
|
||||||
|
let inset = 0;
|
||||||
|
for (let parent = this.parent; parent; parent = parent.parent) {
|
||||||
|
if (parent instanceof ScrollContainer) return Math.max(0, parent.w - inset);
|
||||||
|
if (parent instanceof Stack) inset += parent.padding * 2;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
override layout(): void {
|
||||||
|
this.metrics = gridMetrics(this.w, this.tileIds.length, GAP, SIZES);
|
||||||
|
this.swatches.forEach((swatch, index) => {
|
||||||
|
const rect = tileRect(index, this.metrics, GAP);
|
||||||
|
swatch.box.position.set(rect.x, rect.y);
|
||||||
|
});
|
||||||
|
// The guess was off – a scrollbar took its width, or the column was dragged narrower – so the
|
||||||
|
// grid needs another number of rows than it was given. The scroll area measures again.
|
||||||
|
if (this.metrics.height !== this.h && this.askedAt !== this.w) {
|
||||||
|
this.askedAt = this.w;
|
||||||
|
queueMicrotask(() => {
|
||||||
|
if (!this.destroyed) this.scrollArea()?.relayoutContent();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private scrollArea(): ScrollContainer | null {
|
||||||
|
for (let parent = this.parent; parent; parent = parent.parent)
|
||||||
|
if (parent instanceof ScrollContainer) return parent;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
override redraw(): void {
|
||||||
|
const side = this.metrics.tile;
|
||||||
|
this.swatches.forEach((swatch, index) => this.drawSwatch(swatch, this.tileIds[index]!, index, side));
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawSwatch(swatch: Swatch, tileId: string, index: number, side: number): void {
|
||||||
|
const selected = tileId === this.selectedId;
|
||||||
|
const hovered = index === this.hoveredIndex;
|
||||||
|
swatch.background.clear();
|
||||||
|
drawControlBox(swatch.background, side, side, {
|
||||||
|
fill: selected ? Theme.colors.selection : hovered ? Theme.colors.controlHover : Theme.colors.control,
|
||||||
|
border: selected ? Theme.colors.accent : Theme.colors.border,
|
||||||
|
});
|
||||||
|
|
||||||
|
const asset = findAssetOf(this.state.project, tileId, 'tile');
|
||||||
|
const info = asset ? readTile(this.state.project, asset) : null;
|
||||||
|
const picture = info?.problem === '' ? info.pictures[0] : undefined;
|
||||||
|
const quadrants = info ? loneTileQuadrants(info.kind) : null;
|
||||||
|
const half = (side - PAD * 2) / 2;
|
||||||
|
swatch.sprites.forEach((sprite, corner) => {
|
||||||
|
const texture =
|
||||||
|
picture && info && quadrants
|
||||||
|
? textureLibrary.get(quadrantRef(picture, info.quad, quadrants[corner]!))
|
||||||
|
: null;
|
||||||
|
sprite.visible = texture !== null;
|
||||||
|
if (!texture) return;
|
||||||
|
sprite.texture = texture;
|
||||||
|
sprite.position.set(PAD + (corner % 2) * half, PAD + Math.floor(corner / 2) * half);
|
||||||
|
sprite.width = half;
|
||||||
|
sprite.height = half;
|
||||||
|
});
|
||||||
|
// A tile without a usable picture still has to be pickable, and look like it is missing something.
|
||||||
|
swatch.empty.visible = !picture;
|
||||||
|
swatch.empty.position.set(
|
||||||
|
Math.round((side - swatch.empty.width) / 2),
|
||||||
|
Math.round((side - swatch.empty.height) / 2),
|
||||||
|
);
|
||||||
|
|
||||||
|
swatch.caption.style = textStyle(selected ? 'accent' : 'muted');
|
||||||
|
swatch.caption.text = truncate(asset?.name ?? '', swatch.caption.style, side);
|
||||||
|
swatch.caption.position.set(0, side + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildSwatch(): Swatch {
|
||||||
|
const box = new Container();
|
||||||
|
const background = new Graphics();
|
||||||
|
const sprites = [new Sprite(), new Sprite(), new Sprite(), new Sprite()] as Swatch['sprites'];
|
||||||
|
const empty = new Text({ text: '?', style: textStyle('muted') });
|
||||||
|
const caption = new Text({ style: textStyle('small') });
|
||||||
|
box.addChild(background, ...sprites, empty, caption);
|
||||||
|
box.eventMode = 'none';
|
||||||
|
this.addChild(box);
|
||||||
|
return { box, background, sprites, empty, caption };
|
||||||
|
}
|
||||||
|
|
||||||
|
private indexUnder(e: FederatedPointerEvent): number {
|
||||||
|
const local = this.toLocal(e.global);
|
||||||
|
return tileAt(local.x, local.y, this.tileIds.length, this.metrics, GAP);
|
||||||
|
}
|
||||||
|
|
||||||
|
private hover(index: number): void {
|
||||||
|
if (index === this.hoveredIndex) return;
|
||||||
|
this.hoveredIndex = index;
|
||||||
|
this.invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
override destroy(): void {
|
||||||
|
for (const stop of this.stops) stop();
|
||||||
|
super.destroy({ children: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import type { TileAsset } from '../../src/core/model/Asset';
|
|||||||
import { isPlaceableType } from '../../src/core/model/AssetTypes';
|
import { isPlaceableType } from '../../src/core/model/AssetTypes';
|
||||||
import {
|
import {
|
||||||
autotileCombinations,
|
autotileCombinations,
|
||||||
|
loneTileQuadrants,
|
||||||
type Neighbours,
|
type Neighbours,
|
||||||
neighboursAt,
|
neighboursAt,
|
||||||
SAMPLE_PATTERN,
|
SAMPLE_PATTERN,
|
||||||
@@ -249,6 +250,28 @@ describe('autotile blocks', () => {
|
|||||||
expect([corner.n, corner.e, corner.ne]).toEqual([true, true, false]);
|
expect([corner.n, corner.e, corner.ne]).toEqual([true, true, false]);
|
||||||
expect(tileQuadrants('floor', corner)[1]).toEqual({ x: 3, y: 0 });
|
expect(tileQuadrants('floor', corner)[1]).toEqual({ x: 3, y: 0 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows each kind in the palette as its lone tile, the four convex corners (#139)', () => {
|
||||||
|
expect(loneTileQuadrants('floor')).toEqual([
|
||||||
|
{ x: 0, y: 2 },
|
||||||
|
{ x: 3, y: 2 },
|
||||||
|
{ x: 0, y: 5 },
|
||||||
|
{ x: 3, y: 5 },
|
||||||
|
]);
|
||||||
|
expect(loneTileQuadrants('wall')).toEqual([
|
||||||
|
{ x: 0, y: 0 },
|
||||||
|
{ x: 3, y: 0 },
|
||||||
|
{ x: 0, y: 3 },
|
||||||
|
{ x: 3, y: 3 },
|
||||||
|
]);
|
||||||
|
// A plain tile is its picture as it is, each corner its own quarter.
|
||||||
|
expect(loneTileQuadrants('none')).toEqual([
|
||||||
|
{ x: 0, y: 0 },
|
||||||
|
{ x: 1, y: 0 },
|
||||||
|
{ x: 0, y: 1 },
|
||||||
|
{ x: 1, y: 1 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('tile asset', () => {
|
describe('tile asset', () => {
|
||||||
|
|||||||
@@ -28,6 +28,19 @@ describe('gridMetrics', () => {
|
|||||||
expect(gridMetrics(140, 1).tile).toBe(TILE_MAX);
|
expect(gridMetrics(140, 1).tile).toBe(TILE_MAX);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('takes other limits for the tile size, as the tile palette of a tilemap does (#139)', () => {
|
||||||
|
const sizes = { min: 56, max: 72 };
|
||||||
|
// A 300 px inspector column: four boxes of 56 fit, spread to 70 each.
|
||||||
|
const m = gridMetrics(300, 5, 6, sizes);
|
||||||
|
expect(m.columns).toBe(4);
|
||||||
|
expect(m.tile).toBe(70);
|
||||||
|
expect(m.rows).toBe(2);
|
||||||
|
expect(m.height).toBe(2 * (70 + CAPTION_HEIGHT) + 6);
|
||||||
|
// 170 px hold two columns of 82 – capped at the upper limit, not at the picker's.
|
||||||
|
expect(gridMetrics(170, 5, 6, sizes).tile).toBe(72);
|
||||||
|
expect(tileAt(tileRect(1, m, 6).x + 2, 2, 5, m, 6)).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
it('reports the height the scroll container has to give it', () => {
|
it('reports the height the scroll container has to give it', () => {
|
||||||
const m = gridMetrics(WIDTH, 7);
|
const m = gridMetrics(WIDTH, 7);
|
||||||
expect(m.rows).toBe(2);
|
expect(m.rows).toBe(2);
|
||||||
|
|||||||
@@ -112,6 +112,33 @@ export async function tilemap(t) {
|
|||||||
section.tools && section.grass && Boolean(section.layers),
|
section.tools && section.grass && Boolean(section.layers),
|
||||||
JSON.stringify(section),
|
JSON.stringify(section),
|
||||||
);
|
);
|
||||||
|
// The tiles are a grid of pictures (#139), and each shows its block's lone tile: the four convex
|
||||||
|
// corners, read off the palette in one call like a cell of the map.
|
||||||
|
await inInspector(t, '__gm.tilemap.tileButtonAt("at-grass")');
|
||||||
|
const swatch = await json(`(() => {
|
||||||
|
const s = __gm.tilemap.tileSwatch("at-grass");
|
||||||
|
if (!s) return null;
|
||||||
|
const q = s.picture.w / 4;
|
||||||
|
const points = [[q, q], [3 * q, q], [q, 3 * q], [3 * q, 3 * q]]
|
||||||
|
.map(([x, y]) => [Math.round(s.picture.x + x), Math.round(s.picture.y + y)]);
|
||||||
|
return { columns: s.columns, read: points.map(([x, y]) => __gm.pixelAt(x, y)) };
|
||||||
|
})()`);
|
||||||
|
const lone = [
|
||||||
|
[0, 2],
|
||||||
|
[3, 2],
|
||||||
|
[0, 5],
|
||||||
|
[3, 5],
|
||||||
|
].map(([qx, qy]) => quadColour('floor', qx, qy));
|
||||||
|
check(
|
||||||
|
'the palette shows a tile as its picture, the lone tile of the block',
|
||||||
|
swatch !== null && swatch.read.every((colour, i) => colourDistance(colour, lone[i]) <= 12),
|
||||||
|
JSON.stringify({ swatch, want: lone }),
|
||||||
|
);
|
||||||
|
check(
|
||||||
|
'and puts several tiles side by side in the inspector',
|
||||||
|
swatch !== null && swatch.columns >= 3,
|
||||||
|
JSON.stringify(swatch),
|
||||||
|
);
|
||||||
// Clicked through the hook rather than by its text: the tile's own row in the asset panel
|
// Clicked through the hook rather than by its text: the tile's own row in the asset panel
|
||||||
// reads "Grass" as well, and a click there would only select the tile.
|
// reads "Grass" as well, and a click there would only select the tile.
|
||||||
check(
|
check(
|
||||||
|
|||||||
Reference in new issue
Block a user