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
@@ -120,6 +120,17 @@ export class GraphInterpreter {
|
||||
this.follow(node, port, ctx, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* A data input of an event node, read ahead of its pulse: what decides whether the event concerns
|
||||
* the node at all (#160). Null for an input the node does not have.
|
||||
*/
|
||||
eventInput(node: GraphNode, key: string, ctx: NodeContext): Value {
|
||||
this.frame = this.emptyFrame(this.graph);
|
||||
this.steps = 0;
|
||||
const port = this.def(node)?.inputs.find((p) => p.key === key);
|
||||
return port ? this.inputValue(node, port, ctx) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The `On state entered` / `On state exited` nodes watching one machine. A node whose machine
|
||||
* input carries a running value rather than a pick has no state outputs at all, so it is never
|
||||
|
||||
@@ -7,7 +7,7 @@ import { cellToWorld, worldToCell } from '../../../model/liveTilemap';
|
||||
import { worldMatrixOf } from '../../NodeContext';
|
||||
import type { NodeDef, PortDef } from '../../NodeRegistry';
|
||||
import { asEntity, asNumber, asString, type Value } from '../../values';
|
||||
import { bool, exec, int, num, obj } from '../shared';
|
||||
import { bool, exec, int, num, obj, tilePort } from '../shared';
|
||||
|
||||
/** The layer by name, picked from the layers of every map in the project; none = the first. */
|
||||
const layerPort = (): PortDef => ({
|
||||
@@ -17,14 +17,6 @@ const layerPort = (): PortDef => ({
|
||||
field: { key: 'layer', label: 'Layer', kind: 'tileLayer', default: '' },
|
||||
});
|
||||
|
||||
/** A tile asset; unconnected, it shows the tile picker, and none chosen empties the cell. */
|
||||
const tilePort = (key = 'tile', label = key): PortDef => ({
|
||||
key,
|
||||
label,
|
||||
type: 'string',
|
||||
field: { key, label: 'Tile', kind: 'asset', default: '', assetTypes: ['tile'] },
|
||||
});
|
||||
|
||||
const CELL_INPUTS: readonly PortDef[] = [int('cellX', 0, 'cell x'), int('cellY', 0, 'cell y')];
|
||||
|
||||
/** A cell counts in whole columns and rows, so a fraction arriving over an edge is floored. */
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { hiddenOrder, orderParam } from '../../eventOrder';
|
||||
import { collisionOutputs, contactOutputs } from '../shared';
|
||||
import { collisionOutputs, contactOutputs, perCellPort, tileOutputs } from '../shared';
|
||||
|
||||
export const OnCollisionEnter: NodeDef = {
|
||||
type: 'OnCollisionEnter',
|
||||
@@ -9,9 +9,11 @@ export const OnCollisionEnter: NodeDef = {
|
||||
description:
|
||||
'Runs once when a collider of the owner starts overlapping another collider. While playing, ' +
|
||||
'normal x/y point from the other collider towards the own one, impulse says how hard they met and ' +
|
||||
'point x/y where (all 0 for a trigger).',
|
||||
inputs: [],
|
||||
outputs: [...collisionOutputs(), ...contactOutputs()],
|
||||
'point x/y where (all 0 for a trigger). Walking onto a trigger tile of a tilemap runs it too, with ' +
|
||||
'the tile and its cell: once for a whole stretch of touching cells of that tile, or with "per cell" ' +
|
||||
'for every cell.',
|
||||
inputs: [perCellPort()],
|
||||
outputs: [...collisionOutputs(), ...contactOutputs(), ...tileOutputs()],
|
||||
params: [orderParam],
|
||||
hiddenParams: hiddenOrder,
|
||||
};
|
||||
@@ -1,14 +1,16 @@
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { hiddenOrder, orderParam } from '../../eventOrder';
|
||||
import { collisionOutputs } from '../shared';
|
||||
import { collisionOutputs, perCellPort, tileOutputs } from '../shared';
|
||||
|
||||
export const OnCollisionExit: NodeDef = {
|
||||
type: 'OnCollisionExit',
|
||||
label: 'On collision exit',
|
||||
category: 'event',
|
||||
description: 'Runs once when a collider of the owner stops overlapping another collider.',
|
||||
inputs: [],
|
||||
outputs: collisionOutputs(),
|
||||
description:
|
||||
'Runs once when a collider of the owner stops overlapping another collider – or leaves a trigger tile ' +
|
||||
'of a tilemap: the whole stretch of touching cells of that tile, or with "per cell" each cell.',
|
||||
inputs: [perCellPort()],
|
||||
outputs: [...collisionOutputs(), ...tileOutputs()],
|
||||
params: [orderParam],
|
||||
hiddenParams: hiddenOrder,
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { hiddenOrder, orderParam } from '../../eventOrder';
|
||||
import { collisionOutputs, contactOutputs } from '../shared';
|
||||
import { collisionOutputs, contactOutputs, perCellPort, tileOutputs } from '../shared';
|
||||
|
||||
export const OnCollisionStay: NodeDef = {
|
||||
type: 'OnCollisionStay',
|
||||
@@ -9,9 +9,11 @@ export const OnCollisionStay: NodeDef = {
|
||||
description:
|
||||
'Runs while a collider of the owner overlaps another collider: every frame, or with an interval once ' +
|
||||
'each time that long has passed since they met. While playing, normal x/y, impulse and point x/y tell ' +
|
||||
'how they touch right now (all 0 for a trigger and in the editor run).',
|
||||
inputs: [],
|
||||
outputs: [...collisionOutputs(), ...contactOutputs()],
|
||||
'how they touch right now (all 0 for a trigger and in the editor run). On a trigger tile of a ' +
|
||||
'tilemap, tile and cell x/y say which: the cell nearest to the own collider, or with "per cell" each ' +
|
||||
'cell it lies on.',
|
||||
inputs: [perCellPort()],
|
||||
outputs: [...collisionOutputs(), ...contactOutputs(), ...tileOutputs()],
|
||||
params: [
|
||||
{
|
||||
key: 'interval',
|
||||
|
||||
@@ -44,6 +44,34 @@ export const contactOutputs = (): PortDef[] => [
|
||||
num('pointY', 'point y'),
|
||||
];
|
||||
|
||||
/** A tile asset; unconnected as an input, it shows the tile picker. */
|
||||
export const tilePort = (key = 'tile', label = key): PortDef => ({
|
||||
key,
|
||||
label,
|
||||
type: 'string',
|
||||
field: { key, label: 'Tile', kind: 'asset', default: '', assetTypes: ['tile'] },
|
||||
});
|
||||
|
||||
export const PER_CELL_INPUT = 'perCell';
|
||||
|
||||
/**
|
||||
* Which pairs a collision event hears of a map's trigger tiles (#160): off, one per area of a tile –
|
||||
* a pond is entered once –, on, one per cell.
|
||||
*/
|
||||
export const perCellPort = (): PortDef => ({
|
||||
key: PER_CELL_INPUT,
|
||||
label: 'per cell',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
field: { key: PER_CELL_INPUT, label: 'Per cell (trigger tiles)', kind: 'boolean', default: false },
|
||||
});
|
||||
|
||||
/**
|
||||
* The trigger tile a collider lies on (#160) and its cell in the map, which is then `other`. No
|
||||
* tile and cell -1, -1 for anything else: another collider, or a map's solid cells.
|
||||
*/
|
||||
export const tileOutputs = (): PortDef[] => [tilePort(), num('cellX', 'cell x'), num('cellY', 'cell y')];
|
||||
|
||||
export const keyParam: PropertyDef = { key: 'key', label: 'Key', kind: 'key', default: 'ArrowRight' };
|
||||
|
||||
/** No `assetTypes`: any placeable asset is a valid answer here. */
|
||||
|
||||
Reference in new issue
Block a user