Let a tile report a touch instead of blocking it (#160)
A tile could be solid, one-way and scaled, but not report without blocking. Water, lava, spikes or a goal zone needed a trigger object of their own laid over the map. Now a tile has a switch Trigger, and the collision events name the tile and the cell they met. - A tile has the switch "Trigger (reports, never blocks)" under Solid. Its cells hold nothing up: no Move, no falling body, no ray. They raise On collision enter, stay and exit with the tilemap as other. - The three events have the outputs tile, cell x and cell y. Without a tile, tile is empty and the cell is -1, -1. - The three events have the input per cell. Off, the default, gives one event per area: cells of the same tile that touch side by side. On gives one event per cell. - The scene view outlines trigger cells in yellow like a trigger collider, merged to rectangles but never with solid cells. - Is touching with the map counts trigger tiles, and Set tile works while the game runs. - Both systems find the touches with the same overlap check in runtime/systems/tileTriggers.ts, not through the physics engine. Decided here: - Trigger wins over Solid. A tile with both does not block, as with a collider. - Water beside lava is two areas, and so are two ponds that touch only at a corner. - An area names the cell nearest to the centre of the collider, and on exit the cell it named last. - The solid cells of a map still do not name their tile. - Normal, impulse and point are 0, as with a trigger collider. - Collision scale and outlines shape a trigger cell like a solid one. One-way, friction and bounciness have no effect. Tests: - Gate on stein2: 205 files, 3104 tests (3067 + 37), exit 0 - e2e 39,40: 106 checks. e2e 1-8,39-43: 490 checks. Both on melektaus-atx. No full run, the sum of 1648 (1645 + 3) is reckoned and CI measures it. - 37 unit tests in tests/core/tile-triggers.test.ts, nearly all against both systems - Three e2e checks with a real mouse in block 40l-iv - 24 unit counter-probes fail tests. Two did not at first, tests were added for them. - Two browser counter-probes fail their checks - Two screenshots looked at (yellow outline, the node with per cell) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2ff3a78607
commit
275e50af01
20 files changed
+1161
-57
No files matched your search
@@ -96,6 +96,27 @@ const tileGraph = {
|
||||
],
|
||||
};
|
||||
|
||||
/** A behavior that logs the tile and the cell of every `On collision enter` (#160), in three lines. */
|
||||
function triggerGraph(prefix) {
|
||||
const id = (name) => `${prefix}-${name}`;
|
||||
return {
|
||||
nodes: [
|
||||
node(id('enter'), 'OnCollisionEnter', 0, 0),
|
||||
node(id('tile'), 'Log', 400, 0),
|
||||
node(id('x'), 'Log', 700, 0),
|
||||
node(id('y'), 'Log', 1000, 0),
|
||||
],
|
||||
edges: [
|
||||
edge(id('e1'), id('enter'), 'exec', id('tile'), 'exec'),
|
||||
edge(id('e2'), id('enter'), 'tile', id('tile'), 'message'),
|
||||
edge(id('e3'), id('tile'), 'exec', id('x'), 'exec'),
|
||||
edge(id('e4'), id('enter'), 'cellX', id('x'), 'message'),
|
||||
edge(id('e5'), id('x'), 'exec', id('y'), 'exec'),
|
||||
edge(id('e6'), id('enter'), 'cellY', id('y'), 'message'),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/** Whether a box lies inside another, give or take a pixel of rounding. */
|
||||
function inside(inner, outer) {
|
||||
return (
|
||||
@@ -1229,6 +1250,88 @@ export async function tilemap(t) {
|
||||
passed && passed.x > -170 + 20,
|
||||
`world x=${passed?.x}`,
|
||||
);
|
||||
// 40l-iv. Trigger tiles (#160). The switch sits on the tile next to Solid; the map then outlines
|
||||
// the cells in the trigger colour and blocks nothing there. Two behaviors under the crate
|
||||
// listen as it walks across the block: one hears the area, one – "per cell" ticked on its
|
||||
// node – every cell.
|
||||
await select({ kind: 'asset', assetId: 'at-grass' });
|
||||
await settle();
|
||||
const triggerRow = await json("__gm.inspectorField('Trigger (reports, never blocks)')");
|
||||
if (triggerRow) await click(triggerRow.x, triggerRow.y);
|
||||
await settle();
|
||||
const reporting = await json(`__gm.tilemap.solids(${JSON.stringify(MAP_OBJECT)})`);
|
||||
check(
|
||||
'a tile has a Trigger switch; with it the map outlines its cells as triggers and none as solid',
|
||||
Boolean(triggerRow) &&
|
||||
(await t.asset('at-grass'))?.props.trigger === true &&
|
||||
reporting?.visible === true &&
|
||||
reporting.rects.length === 0 &&
|
||||
JSON.stringify(reporting.triggers) ===
|
||||
JSON.stringify([
|
||||
{ x: 0, y: 0, w: 3, h: 3 },
|
||||
{ x: 4, y: 0, w: 1, h: 1 },
|
||||
]),
|
||||
JSON.stringify({ triggerRow, props: (await t.asset('at-grass'))?.props, reporting }),
|
||||
);
|
||||
await shot('40-tilemap-trigger');
|
||||
await mutate(`
|
||||
const assets = __gm.state.project.assets;
|
||||
const ear = (id, name, graph) => {
|
||||
const made = structuredClone(assets.find((a) => a.id === 'mover'));
|
||||
Object.assign(made, { id, name, props: { enabled: true, runInEditor: false }, variables: [], graph });
|
||||
assets.push(made);
|
||||
};
|
||||
ear('tt-area', 'Area ear', ${JSON.stringify(triggerGraph('ta'))});
|
||||
ear('tt-cells', 'Cell ear', ${JSON.stringify(triggerGraph('tc'))});
|
||||
const object = (id, assetId, name) => ({ id, assetId, name, position: { x: 0, y: 0, z: 0 },
|
||||
editorOffset: { x: 0, y: 0 }, overrides: {}, parentId: 'tm-crate', instanceEdits: {} });
|
||||
__gm.state.activeScene.objects.push(
|
||||
object('tm-crate-area', 'tt-area', 'Area ear'), object('tm-crate-cells', 'tt-cells', 'Cell ear'));`);
|
||||
await select({ kind: 'node', assetId: 'tt-cells', nodeId: 'tc-enter' });
|
||||
await settle();
|
||||
const perCellField = await json("__gm.inspectorField('per cell', true)");
|
||||
if (perCellField) await click(perCellField.x, perCellField.y);
|
||||
await settle();
|
||||
const enterParams = await json(
|
||||
"__gm.state.project.assets.find((a) => a.id === 'tt-cells').graph.nodes.find((n) => n.id === 'tc-enter').params",
|
||||
);
|
||||
check(
|
||||
'On collision enter has a "per cell" switch, ticked on the node with the mouse',
|
||||
Boolean(perCellField) && enterParams.perCell === true,
|
||||
JSON.stringify({ perCellField, enterParams }),
|
||||
);
|
||||
await shot('40-tilemap-trigger-node');
|
||||
await evaluate('__gm.state.select(null)');
|
||||
await openTab(t, 'scene');
|
||||
const triggerLogFrom = t.consoleLog.length;
|
||||
const crossed = await playAgainstMap();
|
||||
const heardBy = (name) => {
|
||||
const lines = t.consoleLog
|
||||
.slice(triggerLogFrom)
|
||||
.filter((line) => line.startsWith(`[behavior ${name}]`))
|
||||
.map((line) => line.split('] ').pop());
|
||||
const events = [];
|
||||
for (let i = 0; i + 2 < lines.length; i += 3) events.push(lines.slice(i, i + 3).join(' '));
|
||||
return events;
|
||||
};
|
||||
const areaHeard = heardBy('Area ear');
|
||||
const cellsHeard = heardBy('Cell ear');
|
||||
const names = (events) => events.every((event) => /^at-grass [0-4] [0-2]$/.test(event));
|
||||
check(
|
||||
'the crate walks across the trigger tiles: one enter for the block, one per cell with "per cell"',
|
||||
crossed &&
|
||||
crossed.x > -170 + 20 &&
|
||||
areaHeard.length === 1 &&
|
||||
cellsHeard.length > 1 &&
|
||||
new Set(cellsHeard).size === cellsHeard.length &&
|
||||
names(areaHeard) &&
|
||||
names(cellsHeard),
|
||||
JSON.stringify({ x: crossed?.x, areaHeard, cellsHeard }),
|
||||
);
|
||||
await mutate(`
|
||||
const p = __gm.state.project;
|
||||
p.assets = p.assets.filter((a) => !a.id.startsWith('tt-'));
|
||||
delete p.assets.find((a) => a.id === 'at-grass').props.trigger;`);
|
||||
await mutate(`
|
||||
const scene = __gm.state.activeScene;
|
||||
scene.objects = scene.objects.filter((o) => !o.id.startsWith('tm-crate'));
|
||||
|
||||
Reference in new issue
Block a user