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:
adminandClaude Opus 5.5 committed 2026-10-02 07:28:39 +02:00
1 parent 2ff3a78607
commit 275e50af01
20 files changed
+1161 -57

No files matched your search

+13 -2
View File
@@ -18,6 +18,7 @@ import {
Solids,
} from './solids';
import type { System } from './System';
import { TileTriggers } from './tileTriggers';
/**
* Sorts collider overlaps into enter / stay / exit per tick, detection only; colliders of one body never
@@ -26,7 +27,8 @@ import type { System } from './System';
*
* A placed tilemap is a solid body of its own: its blocking cells, merged into rectangles, stop a
* move and report a collision with the tilemap as the other side. Two tilemaps never meet – they
* are ground, not things that walk into each other.
* are ground, not things that walk into each other. Its trigger tiles block nothing and report
* the cells overlapped (`TileTriggers`, #160).
*/
export class CollisionSystem implements System, CollisionSource {
enters: CollisionPair[] = [];
@@ -34,6 +36,7 @@ export class CollisionSystem implements System, CollisionSource {
exits: CollisionPair[] = [];
private previous = new Map<string, CollisionPair>();
private readonly solids = new Solids();
private readonly triggers = new TileTriggers();
update(world: World): void {
this.solids.reset();
@@ -56,11 +59,17 @@ export class CollisionSystem implements System, CollisionSource {
for (const [key, pair] of current) (this.previous.has(key) ? this.stays : this.enters).push(pair);
for (const [key, pair] of this.previous) if (!current.has(key)) this.exits.push(pair);
this.previous = current;
// A map's trigger tiles (#160) are pairs of their own, told after the others.
this.triggers.update(world);
this.enters.push(...this.triggers.enters);
this.stays.push(...this.triggers.stays);
this.exits.push(...this.triggers.exits);
}
stop(): void {
this.previous = new Map();
this.solids.reset();
this.triggers.stop();
this.enters = [];
this.stays = [];
this.exits = [];
@@ -80,7 +89,9 @@ export class CollisionSystem implements System, CollisionSource {
touching(world: World, a: Entity, b: Entity): boolean {
for (const ca of partsOf(world, a)) {
for (const cb of partsOf(world, b)) {
if (ca !== cb && collidersMeet(ca, cb) && this.overlaps(world, ca, cb)) return true;
if (ca === cb) continue;
if (collidersMeet(ca, cb) && this.overlaps(world, ca, cb)) return true;
if (this.triggers.touching(world, ca, cb)) return true;
}
}
return false;