Make phase a block property rather than a distinct type

This commit is contained in:
James Campbell
2026-08-02 00:03:58 -04:00
parent a17a68a07e
commit 7b7b005823
5 changed files with 515 additions and 201 deletions

View File

@@ -104,29 +104,30 @@ Any trap can also take:
| `type` | Key parameters | Behavior |
|-----------------|----------------|----------|
| `spike` | `direction` (`up`/`down`/`left`/`right`), `trigger`, `delay` | Deadly spike; active while its `trigger` holds. The sprite auto-rotates to point the way `direction` faces. |
| `block` | see below | The all-in-one block: stationary, sliding, patrolling, deadly, fake, and/or crumbling. |
| `block` | see below | The all-in-one block: stationary, sliding, patrolling, deadly, fake, crumbling, and/or phasing. |
| `arrow_shooter` | `direction`, `speed`, `interval`, `trigger`, `delay` | A wall turret firing deadly arrows every `interval` seconds while its `trigger` holds. |
| `warp` | `to` [col,row] | Invisible tile that teleports the player to `to` on contact. Re-arms once you leave it. |
| `phase_block` | `trigger`, `fade` | Invisible and intangible until its `trigger` fires, then fades into a solid over `fade` seconds (and fades back out when the trigger releases). If you're standing in the cell the instant it *starts* appearing, you die — and if you die while one is mid-fade it snaps fully visible for the death freeze. |
### The `block` trap
One trap covers stationary blocks, sliders, patrolling platforms, spike blocks,
fake blocks, and crumbling blocks — compose the behaviour from options (which
combine, e.g. a moving platform that crumbles):
fake blocks, crumbling blocks, and phase blocks — compose the behaviour from
options (which combine, e.g. a moving platform that crumbles, or a phase block
that's always moving):
| option | meaning |
|---|---|
| `path` [[col,row],…] | waypoints it travels between (default just `[at]` = stationary) |
| `move` [dcol,drow] | shorthand for a 2-point path `[at, at+move]` (a slider) |
| `mode` | `once` (default): advance to the last point while triggered, retreat to the first when not — the slider/dropper. `loop` / `pingpong`: cycle the whole path continuously (a patrol). |
| `trigger` | when it moves (default `always`). A patrol is just a path + the default `always` trigger. |
| `deadly` (bool) | `true` → a hazard (spikes) instead of a solid |
| `trigger` | when it activates (default `always`). A patrol is just a path + the default `always` trigger. A block that both moves and phases can give each its own trigger via a per-target map (see Triggers). |
| `deadly` (bool) | `true` → a hazard (spikes) instead of a solid. On a phase block the hazard is live only once materialised. |
| `fake` (bool) | looks solid but you fall straight through it |
| `crumble` (bool) + `crumble_delay`, `respawn` | gives way `crumble_delay` s after you stand on it, vanishes, then reappears after `respawn` s (killing you if you're standing where it re-forms) |
| `phase` (bool) + `fade` | invisible/intangible until triggered, then fades into a solid (or a hazard, if `deadly`) over `fade` s (default 0.3) and back out when the trigger releases. Forming into you is lethal, but a non-deadly one first shoves you clear if you're only clipping an edge; if you die while one is mid-fade it snaps fully visible for the death freeze. |
| `speed` | px/s |
| `sprite` | override sprite (default: `spike_block` if deadly, `fake_block` if fake, `crumble_block` if crumble, else `moving_block`) |
| `delay` / `release` | (`once`) hysteresis: the trigger must hold `delay` s to start extending and be clear `release` s (default 0.1) to start retracting — stops boundary jitter |
| `sprite` | override sprite (default: `spike_block` if deadly, `fake_block` if fake, `crumble_block` if crumble, `phase_block` if phase, else `moving_block`) |
| `delay` / `release` | hold `delay` s to activate (may be a per-target map); in `once` motion, be clear `release` s (default 0.1) to start retracting — stops boundary jitter |
| `sense` | `home` (default): a slider senses its trigger from its resting cell, so moving away can't toggle its own trigger. `current`: senses from the live position — for a block you *ride* (e.g. a dropper) so it stays put while ridden instead of pulling back |
```yaml
@@ -145,6 +146,15 @@ combine, e.g. a moving platform that crumbles):
path: [[9, 10], [14, 10], [14, 6]]
mode: loop
sprite: patrol_block
- type: block # deadly block that patrols AND phases in near you
at: [9, 3]
phase: true
deadly: true
move: [4, 0]
trigger:
motion: always # always sliding back and forth
phase: { within: 3 } # but only solid/deadly when you're close
```
A `once` block runs a *committed stroke*: once it starts moving it runs all the
@@ -183,6 +193,21 @@ trigger:
*continuously* this long before the trap arms; leaving the condition resets the
countdown. Handy for "linger and it strikes" spikes.
**Per-target triggers.** A `block` can both move and phase, and each behaviour
can take its own trigger (and `delay`). Give a single condition to drive both,
or a map keyed by target (`motion` / `phase`) with `default` covering the rest:
```yaml
trigger:
default: always # phase (and anything else)
motion: { within: 5 } # motion only
delay: { motion: 0.1, phase: 0.3 }
```
A map that names some targets but omits `default` leaves the unnamed ones with
no trigger (off) — the debug view warns if that silently disables a capability.
An unknown target name is an error.
### Getting crushed
You also die if a moving (non-deadly) `block` **pinches** you: presses you