Files
feuerwerk/src/core/graph/nodes/shared.ts
T
adminandClaude Opus 5.5 275e50af01 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>
2026-10-02 07:28:39 +02:00

145 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { PropertyValue } from '../../model/Properties';
import type { PropertyDef } from '../../properties/PropertySchema';
import type { NodeDef, PortDef } from '../NodeRegistry';
import { asBoolean, asNumber } from '../values';
export const exec = (key = 'exec', label = ''): PortDef => ({ key, label, type: 'exec' });
export const num = (key: string, label = key): PortDef => ({ key, label, type: 'number' });
export const bool = (key: string, label = key): PortDef => ({ key, label, type: 'boolean' });
export const str = (key: string, label = key): PortDef => ({ key, label, type: 'string' });
export const obj = (key: string, label = key): PortDef => ({ key, label, type: 'object' });
export const list = (key: string, label = key): PortDef => ({ key, label, type: 'list' });
export const struct = (key: string, label = key): PortDef => ({ key, label, type: 'struct' });
export const any = (key: string, label = key): PortDef => ({ key, label, type: 'any' });
export const withDefault = (port: PortDef, value: PropertyValue): PortDef => ({ ...port, default: value });
/** A whole-number input (an index, a count): its field shows no decimals, the node floors it anyway. */
export const int = (key: string, value = 0, label = key): PortDef => ({
key,
label,
type: 'number',
default: value,
field: { key, label, kind: 'number', default: value, step: 1, decimals: 0 },
});
/** Outputs of the three collision events: the other body, its collider and the own collider. */
export const collisionOutputs = (): PortDef[] => [
exec(),
obj('other'),
obj('otherCollider', 'other collider'),
obj('collider'),
];
/**
* How a contact touches: which way the other side pushes (a unit vector pointing from it towards the
* own collider), how hard, and the middle of the contact points in the world. The physics engine
* works them out (#146, #149).
*/
export const contactOutputs = (): PortDef[] => [
num('normalX', 'normal x'),
num('normalY', 'normal y'),
num('impulse'),
num('pointX', 'point x'),
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. */
const assetParam: PropertyDef = { key: 'asset', label: 'Asset', kind: 'asset', default: '' };
/**
* The `asset` input of `Spawn` and `Is asset`; unconnected, it shows the asset picker. The key is the
* one the setting had before it became a port, so a saved pick is the inline value now.
*/
export const assetPort = (): PortDef => ({ key: 'asset', label: 'asset', type: 'string', field: assetParam });
/**
* The `sound` input of the two sound nodes; unconnected, it shows the sound picker – the way
* `machinePort` shows the machine picker. A `Sound` literal is what feeds it through an edge.
*/
export const soundPort = (): PortDef => ({
key: 'sound',
label: 'sound',
type: 'string',
field: { key: 'sound', label: 'Sound', kind: 'sound', default: '' },
});
export function binaryMath(type: string, description: string, fn: (a: number, b: number) => number): NodeDef {
return {
type,
label: type,
category: 'data',
description,
inputs: [num('a'), num('b')],
outputs: [num('result')],
params: [],
run: (_ctx, inputs) => ({ outputs: { result: fn(asNumber(inputs.a), asNumber(inputs.b)) } }),
};
}
export function unaryMath(type: string, description: string, fn: (value: number) => number): NodeDef {
return {
type,
label: type,
category: 'data',
description,
inputs: [num('value')],
outputs: [num('result')],
params: [],
run: (_ctx, inputs) => ({ outputs: { result: fn(asNumber(inputs.value)) } }),
};
}
export function binaryLogic(
type: string,
description: string,
fn: (a: boolean, b: boolean) => boolean,
): NodeDef {
return {
type,
label: type,
category: 'data',
description,
inputs: [bool('a'), bool('b')],
outputs: [bool('result')],
params: [],
run: (_ctx, inputs) => ({ outputs: { result: fn(asBoolean(inputs.a), asBoolean(inputs.b)) } }),
};
}
/** Scales (x, y) to length 1; the zero vector stays zero. */
export function normalize(x: number, y: number): { x: number; y: number; length: number } {
const length = Math.hypot(x, y);
return length === 0 ? { x: 0, y: 0, length: 0 } : { x: x / length, y: y / length, length };
}