Make phase a block property rather than a distinct type
This commit is contained in:
79
SPEC.md
79
SPEC.md
@@ -341,6 +341,37 @@ Composites evaluate *all* children each frame (so nested timers keep ticking).
|
||||
long before the trap arms; leaving the condition resets the countdown. This is
|
||||
arm hysteresis (e.g. "linger and it strikes").
|
||||
|
||||
### 12.1 Per-target triggers and delays
|
||||
|
||||
A trap with more than one triggerable behaviour — a `block` can both **move**
|
||||
and **phase** — lets each behaviour take its own trigger and delay. The value is
|
||||
either:
|
||||
|
||||
- a **single** condition/scalar (the common case), applied to every target; or
|
||||
- a **per-target map** keyed by target name, with `default` covering the rest.
|
||||
|
||||
Target names are disjoint from the condition keywords above, so a bare condition
|
||||
like `{ within: 2 }` is never mistaken for a map. The block's targets are
|
||||
`motion` and `phase`.
|
||||
|
||||
```yaml
|
||||
trigger: { within: 2 } # both motion and phase
|
||||
trigger:
|
||||
default: always # phase (and any other target)
|
||||
motion: { within: 5 } # motion only
|
||||
delay: { motion: 0.1, phase: 0.3 } # per-target arm delays
|
||||
```
|
||||
|
||||
- If a map names some targets but omits `default`, the **unnamed targets get no
|
||||
trigger and never fire** — a `phase` block with `trigger: { motion: … }` and
|
||||
no `default` never materialises. (In the debug view this prints a warning,
|
||||
since it's usually a mistake.)
|
||||
- An unknown target name raises an error at load time.
|
||||
- Each target keeps its own condition instance and arm countdown, so their
|
||||
timers never interfere.
|
||||
- A scalar `delay` applies to all targets; an unnamed target without a `default`
|
||||
delay falls back to `0`.
|
||||
|
||||
---
|
||||
|
||||
## 13. Trap catalog
|
||||
@@ -354,20 +385,21 @@ holds.
|
||||
|
||||
### 13.2 `block` — the all-in-one block
|
||||
One trap covering stationary blocks, sliders, patrolling platforms, spike
|
||||
blocks, fake blocks, and crumbling blocks. Options combine.
|
||||
blocks, fake blocks, crumbling blocks, and phase blocks. Options combine.
|
||||
|
||||
| Option | Meaning |
|
||||
|---|---|
|
||||
| `path: [[col,row],…]` | waypoints it travels between (default `[at]` = stationary) |
|
||||
| `move: [dcol,drow]` | shorthand for a 2-point path `[at, at+move]` (a slider) |
|
||||
| `mode` | `once` (default): extend to the last waypoint while triggered, retreat to the first when not — slider/dropper. `loop` / `pingpong`: cycle the whole path continuously — a patrol. |
|
||||
| `trigger` | when it moves (default `always`); a patrol is a path + `always` |
|
||||
| `trigger` | when it activates (default `always`); a patrol is a path + `always`. May be a per-target map (`motion` / `phase`) — see §12.1 |
|
||||
| `speed` | px/s (default 140) |
|
||||
| `deadly` (bool) | hazard (spikes) instead of a solid |
|
||||
| `deadly` (bool) | hazard (spikes) instead of a solid. On a `phase` block the hazard is live only while it's materialised |
|
||||
| `fake` (bool) | drawn like a solid block but non-collidable (you fall through) |
|
||||
| `crumble` (bool) + `crumble_delay`, `respawn` | gives way `crumble_delay` s after you stand on it, vanishes, then re-forms after `respawn` s (killing you if you're standing where it re-forms) |
|
||||
| `sprite` | override sprite (default: `spike_block` if deadly, `fake_block` if fake, `crumble_block` if crumble, else `moving_block`) |
|
||||
| `delay` / `release` | (`once`) hysteresis: hold `delay` s to start extending, be clear `release` s (default 0.1) to start retracting |
|
||||
| `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 — see the phasing note below |
|
||||
| `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, §12.1); in `once` motion, be clear `release` s (default 0.1) to start retracting |
|
||||
| `sense` | `home` (default): sense the trigger from the resting cell; `current`: sense from the live position (for blocks you ride, so they stay put while ridden) |
|
||||
|
||||
Behavior notes:
|
||||
@@ -375,11 +407,24 @@ Behavior notes:
|
||||
endpoint without reversing, and only reconsiders its trigger while parked at
|
||||
an endpoint. Combined with `home` sensing, this eliminates the jitter a
|
||||
slider would otherwise get from moving out of its own sensor range.
|
||||
- A non-deadly, non-fake block is a **solid** that reports itself as a
|
||||
**carrier** so the player rides it; a moving block can **shove** or **crush**
|
||||
the player (§5.2, §5.6).
|
||||
- Movement runs first each frame, then the crumble state machine, so a moving
|
||||
platform can also crumble.
|
||||
- A non-deadly, non-fake, non-phasing (or materialised) block is a **solid**
|
||||
that reports itself as a **carrier** so the player rides it; a moving block
|
||||
can **shove** or **crush** the player (§5.2, §5.6).
|
||||
- Movement, crumble, and phasing run in that order each frame, so a block can
|
||||
combine them — e.g. a block that's always moving (`motion: always`) but only
|
||||
**phases** in when the player is near (`phase: { within: N }`). Motion is
|
||||
trigger-driven; crumble is contact-driven; phasing is trigger-driven.
|
||||
- **Phasing**: while the `phase` trigger holds the block fades in and becomes
|
||||
collidable (a solid, or — with `deadly` — a hazard that's live only once
|
||||
materialised); when it releases, it fades back out and goes intangible. If the
|
||||
player overlaps the cell the instant it *starts* forming, a non-deadly block
|
||||
is forgiving about edge clips: when they're only clipping an edge (overlap ≤
|
||||
half a tile on the shallowest axis) it shoves them out and solidifies behind
|
||||
them, staying lethal only when it forms through their middle (a deep overlap)
|
||||
or the shove would press them into another solid (a crush). A `deadly` phase
|
||||
block skips the shove and simply kills. If the player dies while a phase block
|
||||
is mid-fade, it snaps fully visible before the death freeze (via the
|
||||
`finalize_on_death` hook — see §7/§11).
|
||||
|
||||
### 13.3 `arrow_shooter`
|
||||
A wall turret that fires a deadly projectile every `interval` seconds while its
|
||||
@@ -399,18 +444,8 @@ and destination tiles and fades out over ~0.35 s (expanding/thinning rings drawn
|
||||
procedurally, no sprite), so the teleport reads on screen even though the tile
|
||||
itself is invisible.
|
||||
|
||||
### 13.5 `phase_block`
|
||||
Invisible and intangible until its `trigger` fires, then it fades into a solid
|
||||
over `fade` seconds (alpha 0→1) and fades back out when the trigger releases. If
|
||||
the player overlaps the cell the instant it *starts* forming, it's forgiving
|
||||
about edge clips: when they're only clipping an edge (overlap ≤ half a tile on
|
||||
the shallowest axis) it shoves them out of the cell and solidifies behind them.
|
||||
It stays lethal only when the block forms through the player's middle (a deep
|
||||
overlap) or the shove would press them into another solid (squished against
|
||||
something — a crush, as usual). In the lethal case it stays intangible that frame
|
||||
so they're killed rather than displaced. If the player dies while it is mid-fade,
|
||||
it snaps fully visible before the death freeze (via the `finalize_on_death`
|
||||
hook — see §7/§11).
|
||||
(A *phase block* — invisible/intangible until triggered, then fades into a solid
|
||||
or hazard — is not a separate type: it's the `block` option `phase: true`, §13.2.)
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user