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
Open
feat(dsl): a place that kills is declared, not faked with the art (spec-0031 §8 — lethal volumes)#347stellarfeline wants to merge 3 commits into
stellarfeline wants to merge 3 commits into
Conversation
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
lethal_volumes[](DSL v0.10, spec-0031 acceptance criterion 8): a declaredbox that kills whatever enters it, and says — in the campaign's own words —
what killed it.
The commissioning case was the Z1 cliff. The alternative — making the world's
horizonvoid 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
regionis the existing zone object class, not a new one:StealthZone(
anchor ± extent), whichdamage-players'sinfilter already reuses, andwhich the compiler resolves through the single
Plan::zone_box. A private twinwith the same two fields is
tools/check-capability-ownership.pycheck C byconstruction, and would have forked box resolution at the next capability.
damage_typeis the curatedDamageKindshared withdamage-players. Nonew 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 (
fall→ fell from a high place) whilemessagesays what theplace was.
smuggled: a conditionally-lethal volume needs DAG-conditional reasoning exactly
like
close-gate, and if it is ever wanted it belongs to the shared gatestruct — 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) — notsolid, 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 theengine inherits it instead of re-deriving it (critical path, checkpoint
no-stranding
DW0315, branch paths, the trap forced-cell set, the exportedharness waypoints).
Two diagnostics on top:
DW0510DW0511set-checkpoint/bonfire: the death loop) and a posted body (an NPC anchor, acastplacement, an actor anchor: the volume deletes it on tick one and the delve loses its speaker in silence).DW0512message— 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
DW0511is a finding this PR made on itself. The firstdraft 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 whatdamage-players, a trap payloadand a timed gate's crush emit. The vanilla
deathCountedge (dw.deaths/dw.death_ack), the checkpoint re-seat (cp_respawn_check) andkeep_inventorytherefore see the death they already handle. No second deathdetector, and nothing for one to do. The seam for the concurrent
feat/on-death-rootwork is that same/damage→ deathCount edge; anon_deathroot 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_typewith amessage_id. It is rejected on an existing invariant,not a preference: vanilla builds that message with no
fallback, so it wouldship a raw
death.attack.…key to any player who declines the resource-packprompt — which spec-0029 §3 makes the delve's playable-in-English guarantee
against, and which
DW0185would not catch (the emitted literal is the key, notthe authored string). The wording travels
emit::tr→{"translate":…,"fallback":…}like every other player-visible string, and entersthe l10n inventory as
lethal.<id>.message. Vanilla's broadcast still fires: theparty reads who died, the victim reads what the place was.
What CI now proves
lethal-volumefixture and runs one generated template per volume, whichsummons 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 testspass.
validation/lethal-gate.jsonstates what the proofs examined — volumesdeclared 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.
DW0510/DW0511/DW0512has a test asserting the code and adocs/reference/compiler.mdentry (tools/check-dw-codes.py, both directions).l10n_surface.rs) classified the newstring field as unclassified and failed until it was inventoried — it caught
this, not a human.
Version discipline and byte identity
SUPPORTED_DSL_VERSION→0.10.0, with a per-stageDW0141fence on the questsstage 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 outputfiles. 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 realdelta:
creator-datapack/layout.json's"version"field — the engine identitystring every
dsl_versionbump moves, in a validation/authoring overlay excludedfrom the shipped delve image — plus the manifest hash of it.
(
hollow-vigilis red onmainfor unrelated reasons —DW0331option width,DW0465no cast ledger — so it is not in the set.nobodys-caveandthe-drowned-bellship onlyout/trees, no stage documents.)What the spec got wrong
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.§"Lethal volume". A volume is geometry, and geometry that kills interacts with
reachability; the acceptance criterion should say so.
machinery lives in the world (
interactionhitboxes, cutscene cameras, displayentities), 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.rsandenvelope.rswithfeat/runtime-state(the
0.10.0entry,ordinal,is_v10— one version for the whole spec-0031surface, which is the natural merge) and touches neither
effects.rsnor thegate struct. DW codes were taken from a distinctive block (
DW0510–DW0512)rather than "next free" to avoid the parallel-branch collision class
tools/check-dw-codes.pywarns about.🤖 Generated with Claude Code
https://claude.ai/code/session_01AjQ5p1Kv5MrkGPumi7yXWL