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
@@ -11,7 +11,7 @@ import { parentChain } from '../../core/model/walk';
|
||||
import type { Entity } from '../Entity';
|
||||
import type { AnimationSystem } from './AnimationSystem';
|
||||
import type { Input } from './Input';
|
||||
import { bodyOf, type CollisionPair, type CollisionSource, type Contact } from './solids';
|
||||
import { bodyOf, type CollisionPair, type CollisionSource, type Contact, pairKey } from './solids';
|
||||
|
||||
/** What one node last handed on, and when – the graph tab shows the newest of every instance. */
|
||||
export interface Produced {
|
||||
@@ -116,11 +116,32 @@ function sides(instance: Instance, pairs: readonly CollisionPair[]): Side[] {
|
||||
});
|
||||
}
|
||||
|
||||
export function collisionEnterPulses(instance: Instance, collisions: CollisionSource | null): Pulse[] {
|
||||
/** Reads an event node's "per cell" input for its instance (#160). */
|
||||
export type PerCell = (instance: Instance, node: GraphNode) => boolean;
|
||||
|
||||
/**
|
||||
* What of the pairs one node hears. A trigger tile is told twice, per cell and per area (#160), and
|
||||
* the node's "per cell" input picks one; it is only read where a tile is among the pairs at all.
|
||||
*/
|
||||
function heardBy(instance: Instance, node: GraphNode, all: Side[], perCell: PerCell): Side[] {
|
||||
if (!all.some((side) => side.pair.tile)) return all;
|
||||
const cells = perCell(instance, node);
|
||||
return all.filter((side) => !side.pair.tile || side.pair.tile.perCell === cells);
|
||||
}
|
||||
|
||||
export function collisionEnterPulses(
|
||||
instance: Instance,
|
||||
collisions: CollisionSource | null,
|
||||
perCell: PerCell,
|
||||
): Pulse[] {
|
||||
const entered = sides(instance, collisions?.enters ?? []);
|
||||
return instance.interpreter
|
||||
.eventNodes('OnCollisionEnter')
|
||||
.flatMap((node) => entered.map((side) => ({ instance, node, outputs: sideOutputs(side, side.pair) })));
|
||||
return instance.interpreter.eventNodes('OnCollisionEnter').flatMap((node) =>
|
||||
heardBy(instance, node, entered, perCell).map((side) => ({
|
||||
instance,
|
||||
node,
|
||||
outputs: sideOutputs(side, side.pair),
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,6 +153,7 @@ export function collisionStayPulses(
|
||||
instance: Instance,
|
||||
collisions: CollisionSource | null,
|
||||
dt: number,
|
||||
perCell: PerCell,
|
||||
): Pulse[] {
|
||||
if (!collisions) return [];
|
||||
const interpreter = instance.interpreter;
|
||||
@@ -146,10 +168,10 @@ export function collisionStayPulses(
|
||||
const interval = Math.max(0, Number(node.params.interval ?? 0)) || 0;
|
||||
const clocks = interpreter.nodeState.get(node.id) ?? {};
|
||||
const running: Record<string, number> = {};
|
||||
for (const side of stays) {
|
||||
for (const side of heardBy(instance, node, stays, perCell)) {
|
||||
let fires = 1;
|
||||
if (interval > 0) {
|
||||
const key = `${side.pair.a.id}|${side.pair.b.id}`;
|
||||
const key = pairKey(side.pair);
|
||||
let elapsed = (clocks[key] ?? 0) + dt;
|
||||
for (fires = 0; elapsed >= interval - 1e-9; fires++) elapsed -= interval;
|
||||
running[key] = elapsed;
|
||||
@@ -163,11 +185,17 @@ export function collisionStayPulses(
|
||||
return pulses;
|
||||
}
|
||||
|
||||
export function collisionExitPulses(instance: Instance, collisions: CollisionSource | null): Pulse[] {
|
||||
export function collisionExitPulses(
|
||||
instance: Instance,
|
||||
collisions: CollisionSource | null,
|
||||
perCell: PerCell,
|
||||
): Pulse[] {
|
||||
const left = sides(instance, collisions?.exits ?? []);
|
||||
return instance.interpreter
|
||||
.eventNodes('OnCollisionExit')
|
||||
.flatMap((node) => left.map((side) => ({ instance, node, outputs: sideOutputs(side) })));
|
||||
.flatMap((node) =>
|
||||
heardBy(instance, node, left, perCell).map((side) => ({ instance, node, outputs: sideOutputs(side) })),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -189,7 +217,14 @@ export function animationFinishedPulses(instance: Instance, animations: Animatio
|
||||
|
||||
/** What a collision event hands on, seen from the behavior's side; `contact` only for enter and stay. */
|
||||
function sideOutputs({ own, other, pair }: Side, contact?: Partial<Contact> | null): Record<string, Value> {
|
||||
const outputs: Record<string, Value> = { other: bodyOf(other), otherCollider: other, collider: own };
|
||||
const outputs: Record<string, Value> = {
|
||||
other: bodyOf(other),
|
||||
otherCollider: other,
|
||||
collider: own,
|
||||
tile: pair.tile?.tileId ?? '',
|
||||
cellX: pair.tile?.cell.x ?? -1,
|
||||
cellY: pair.tile?.cell.y ?? -1,
|
||||
};
|
||||
if (contact !== undefined) {
|
||||
// The pair's normal points towards `a`; seen from `b` it points the other way.
|
||||
const sign = own === pair.a ? 1 : -1;
|
||||
|
||||
Reference in new issue
Block a user