Skip to content

feat(dsl): a place that kills is declared, not faked with the art (spec-0031 §8 — lethal volumes) - #347

Open
stellarfeline wants to merge 3 commits into
mainfrom
feat/lethal-volume
Open

feat(dsl): a place that kills is declared, not faked with the art (spec-0031 §8 — lethal volumes)#347
stellarfeline wants to merge 3 commits into
mainfrom
feat/lethal-volume

Conversation

@stellarfeline

Copy link
Copy Markdown
Owner

What this is

lethal_volumes[] (DSL v0.10, spec-0031 acceptance criterion 8): a declared
box that kills whatever enters it, and says — in the campaign's own words —
what killed it.

"lethal_volumes": [
  {
    "id": "lethal/the-drop",
    "region": { "anchor": "anchor/cliff-base", "extent": [40, 4, 40] },
    "message": "The undertow takes you.",
    "damage_type": "fall"
  }
]

The commissioning case was the Z1 cliff. The alternative — making the world's
horizon void so the fall kills anyway — is rejected and not reintroduced:
it changes approved art to obtain a behaviour, and it serves exactly one fiction.
Nothing here knows what a cliff is; the same declaration is a lava pit, an acid
pool, an out-of-bounds plane or the bottom of a lift shaft.

Why it is a primitive and not authored content wearing one

  • region is the existing zone object class, not a new one: StealthZone
    (anchor ± extent), which damage-players's in filter already reuses, and
    which the compiler resolves through the single Plan::zone_box. A private twin
    with the same two fields is tools/check-capability-ownership.py check C by
    construction, and would have forked box resolution at the next capability.
  • damage_type is the curated DamageKind shared with damage-players. No
    new enum, no new registry, and the same totem guarantee: a volume can no more
    void a held totem than a scripted hit can. It is what words vanilla's own
    broadcast (fallfell from a high place) while message says what the
    place was.
  • No arming/disarming gate. Deliberate scope call, stated rather than
    smuggled: a conditionally-lethal volume needs DAG-conditional reasoning exactly
    like close-gate, and if it is ever wanted it belongs to the shared gate
    struct — not to a second bespoke field here.

How the completability proof reasons about it

A volume that kills is, for a route, a volume no route may enter. Its cells
become impassable in the shared nav::World (World::with_lethal) — not
solid, so nothing may stand on top of one either — which is the same move
close-gate's seal makes, and for the same reason: every route proof in the
engine inherits it instead of re-deriving it (critical path, checkpoint
no-stranding DW0315, branch paths, the trap forced-cell set, the exported
harness waypoints).

Two diagnostics on top:

code rule
DW0510 the only route to a critical-path objective runs through a volume, or an objective's only footing lies inside one. Derived from a counterfactual — the leg is re-routed over the identical world with lethality removed — so the message names the volume instead of reporting "no collision-free path" over geometry that is perfectly walkable.
DW0511 a posted place lies inside a volume: a place reached by declaration, not by walking, which no route proof can see. Two families, one defect — a respawn seat (entry spawn / set-checkpoint / bonfire: the death loop) and a posted body (an NPC anchor, a cast placement, an actor anchor: the volume deletes it on tick one and the delve loses its speaker in silence).
DW0512 a blank message — a volume that kills in silence. There is no compiler default that could be right for a cliff, a lava pit and an acid pool at once.

The second family of DW0511 is a finding this PR made on itself. The first
draft of the CI fixture put the volume on the Keeper's post; the rule was widened
to catch it, and the widened rule is tested and documented.

Interaction with the party's machinery

The kill is an ordinary /damage — exactly what damage-players, a trap payload
and a timed gate's crush emit. The vanilla deathCount edge (dw.deaths /
dw.death_ack), the checkpoint re-seat (cp_respawn_check) and
keep_inventory therefore see the death they already handle. No second death
detector, and nothing for one to do.
The seam for the concurrent
feat/on-death-root work is that same /damage → deathCount edge; an on_death
root fires for a lethal-volume death with no change here.

Why the wording is a component, not a custom damage type

Vanilla's own spelling for "a death message the pack wrote" is a datapack
damage_type with a message_id. It is rejected on an existing invariant,
not a preference: vanilla builds that message with no fallback, so it would
ship a raw death.attack.… key to any player who declines the resource-pack
prompt — which spec-0029 §3 makes the delve's playable-in-English guarantee
against, and which DW0185 would not catch (the emitted literal is the key, not
the authored string). The wording travels emit::tr
{"translate":…,"fallback":…} like every other player-visible string, and enters
the l10n inventory as lethal.<id>.message. Vanilla's broadcast still fires: the
party reads who died, the victim reads what the place was.

What CI now proves

  • Runtime, on a live 1.21.11 server. A new tier-2 PackTest pass boots the
    lethal-volume fixture and runs one generated template per volume, which
    summons a body into the box and asserts it died. Binding count: 1 template,
    1 volume
    , and the template proves its own binding — it asserts the dummy is
    inside the volume's own selector box before asserting the kill. Measured
    red→green: with the damage amount stripped to 0 the template fails
    (Expected #hp_leth dw.sys to match ..0, but got 2000); with it, 9/9 tests
    pass.
  • validation/lethal-gate.json states what the proofs examined — volumes
    declared vs resolved, world cells closed, posted places tested, critical-path
    legs routed, PackTest templates generated. Emitted only for a campaign that
    declares a volume, so a file that exists and reports zero is a finding.
  • Each of DW0510 / DW0511 / DW0512 has a test asserting the code and a
    docs/reference/compiler.md entry (tools/check-dw-codes.py, both directions).
  • The existing total l10n-surface gate (l10n_surface.rs) classified the new
    string field as unclassified and failed until it was inventoried — it caught
    this, not a human.

Version discipline and byte identity

SUPPORTED_DSL_VERSION0.10.0, with a per-stage DW0141 fence on the quests
stage and the harness allowlist raised in lockstep
(tools/check-harness-dsl-version.py).

Demonstrated, not asserted. Baseline hashes were captured before the change
over 6 campaigns (5 valid DSL fixtures + nobodys-cave-island) = 987 output
files
. After the change, holding only the engine's own identity stamp at
0.9.0, all 987 files are byte-identical. The stamp isolates the one real
delta: creator-datapack/layout.json's "version" field — the engine identity
string every dsl_version bump moves, in a validation/authoring overlay excluded
from the shipped delve image — plus the manifest hash of it.

(hollow-vigil is red on main for unrelated reasons — DW0331 option width,
DW0465 no cast ledger — so it is not in the set. nobodys-cave and
the-drowned-bell ship only out/ trees, no stage documents.)

What the spec got wrong

  • AC8 says "states the death message" without saying through which channel,
    and the obvious reading (vanilla's death screen, via damage_type.message_id)
    is blocked by spec-0029 §3. Worth recording in the spec: this surface delivers
    the line as a chat component to the victim; the vanilla death screen shows
    vanilla's own wording, chosen by damage_type.
  • AC8 omits the completability half entirely — it appears only in the prose
    §"Lethal volume". A volume is geometry, and geometry that kills interacts with
    reachability; the acceptance criterion should say so.
  • "kills whatever enters it" is not literally implementable. The engine's own
    machinery lives in the world (interaction hitboxes, cutscene cameras, display
    entities), and a volume drawn across a dolly would erase the camera mid-shot.
    The emitted sweep exempts those five types by name and deliberately not
    content bodies — a mob that walks into the lava dies, which is the mechanism
    working. That exemption list is a compiler constant, documented, and it is what
    made DW0511's posted-body family necessary.

Coordination

Shares crates/dsl/src/stages.rs and envelope.rs with feat/runtime-state
(the 0.10.0 entry, ordinal, is_v10 — one version for the whole spec-0031
surface, which is the natural merge) and touches neither effects.rs nor the
gate struct. DW codes were taken from a distinctive block (DW0510DW0512)
rather than "next free" to avoid the parallel-branch collision class
tools/check-dw-codes.py warns about.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AjQ5p1Kv5MrkGPumi7yXWL

stellarfeline and others added 3 commits August 9, 2026 20:02
…ec-0031 §8)

A cliff whose fall must be fatal was going to be obtained by making the
world's horizon `void`. That changes approved art to get a behaviour, and it
serves exactly one fiction. `lethal_volumes[]` (DSL v0.10) is the mechanism
instead: a declared box that kills whatever enters it, worded by the
campaign's own strings — a cliff base, a lava pit, an acid pool, an
out-of-bounds plane, the bottom of a lift shaft.

Three things make it a primitive rather than authored content wearing one:

* `region` is the EXISTING anchor-centred box (`StealthZone`, already reused
  by `damage-players`'s `in` filter), resolved through the one
  `Plan::zone_box`. A twin struct with the same two fields would be
  check-capability-ownership check C by construction.
* `damage_type` is the curated `DamageKind` shared with `damage-players`, so
  a volume can no more void a held totem than a scripted hit can — and it is
  what words vanilla's own broadcast while `message` says what the place was.
* the kill is an ordinary `/damage`, so the vanilla deathCount edge, the
  checkpoint re-seat and keep_inventory see the death they already handle.
  No second death detector, and nothing for one to do.

The completability proof owns it, because a box that kills is a box no route
may enter. Its cells are impassable in the shared nav `World`, exactly as a
`close-gate`'s seal is solid, so every route proof in the engine inherits it —
critical path, checkpoint no-stranding, branch paths, trap forced cells,
harness waypoints. `DW0510` is derived from a counterfactual (re-route with
lethality removed) so the message names the volume instead of sending the
author to fix walkable geometry. `DW0511` is the half routing cannot see: a
place reached by DECLARATION, not by walking — a respawn seat (the death
loop) or a posted body (an NPC the volume deletes on tick one, silently).
That second family was found by this feature's own CI fixture, whose first
draft put the volume on the Keeper's post.

The wording is a `{translate,fallback}` component, not a custom damage type's
`message_id`: vanilla builds that message with no fallback, so it would ship
a raw `death.attack.…` key to any player who declines the resource pack, and
spec-0029 §3 makes the delve playable-in-English an invariant.

CI now proves: `DW0510`/`DW0511`/`DW0512` each by a test asserting the code;
the l10n surface gate classifies the wording as inventoried; a tier-2
PackTest pass that summons a body into the box on a live 1.21.11 server and
asserts it dies (measured red->green — with the damage amount stripped the
template fails "got 2000"); and `validation/lethal-gate.json` states the
binding counts, emitted only for a campaign that declares a volume.

Byte identity: with the engine's own version stamp held at 0.9.0, all 987
output files across 6 campaigns (5 fixtures + nobodys-cave-island) are
byte-identical. The only real delta is `creator-datapack/layout.json`'s
`"version"` field — the engine identity stamp every dsl_version bump moves —
and the manifest hash of it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AjQ5p1Kv5MrkGPumi7yXWL
Caught by `tools/check-reference-versions.py`, which landed on main between
this branch's base and its first CI run — exactly the gate it was written for
(a header stale in the OLDER direction, acted on by the next authoring
session picking a stage envelope's dsl_version).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AjQ5p1Kv5MrkGPumi7yXWL
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant