Skip to content

Doors open within reach, switches in their rest pose, town teleporters ride - #37

Merged
codingncaffeine merged 3 commits into
codingncaffeine:mainfrom
qwerin:fix/doors-switches-teleporters
Nov 9, 2026
Merged

codingncaffeine merged 3 commits into
codingncaffeine:mainfrom
qwerin:fix/doors-switches-teleporters

Conversation

@qwerin

@qwerin qwerin commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Three fixes for usable world objects, found play-testing multiplayer_world (Elddim). The first two also apply to the single-player campaign.

Doors open only within reach. A door click ran OpenDoorNear straight from the click handler, which opened the closed door nearest the click from any distance; the template's DoorUseRange was read but never checked.

  • A click now queues a walk-up-and-open like chests and levers (_pendingDoorUse); the acting character walks to the click and the door opens on arrival.
  • In range = XZ distance from the hinge ≤ the leaf's footprint radius + [aspect] use_range (default 1.5), same floor (within 3 u). The footprint term is needed because a closed door's nav obstacle keeps the walker about one leaf length from the hinge (UseRange.DoorInRange, unit-tested).
  • The use_toggle "It won't budge." line now shows on arrival rather than on the click.

Skinned props drawn in their rest pose. The switch family's clips (a_i_glb_switch-06-open/close.prs, also lever_glb_01/02/03) hold the root bone at (0,-0.183,-1.0) for the whole clip while the bind pose has it at z 0. DS1 draws the prop in that animated pose and the maps are placed for it; SiegeFX drew static skinned props at bind, so the teleporter call button (lever_glb_06) floated ~1 u over its platform in all 9 MP towns, and SP wall switches (dm_r8a, cr_r1) stood ~1 u off their walls.

  • AnimationRuntime.TryRootRestOffset returns the root's skin matrix only when the clip holds the root still and away from bind; a moving root (e.g. statue_cav_rise_01) is left alone.
  • PropRestPose picks the resting clip (chore_default, else chore_open/close; also the anim_files:00 = shorthand). The static prop path applies restOffset * world before scale_multiplier, so the offset itself is not scaled (other towns' buttons use scale 2 and only sit right that way). Cached per template+mesh, logged once as [rest-pose].
  • Sweep of Logic.dsres: applies to lever_glb_01/02/03/06/06_message (-1 Z) and lever_gob_01 (+0.757 Z, not checked against geometry); a few others are ≤ 0.007.

Town teleporters ride. Every MP town's transport stone is an elevator_instant_4s_1c gizmo driving two cars between two shafts: the town car and its twin in new_hub. A press raises both cars, swaps them between the shafts and sinks them, so the riders step off in the hub; the 8 towns pair with new_hub's 8 gizmos (other_elevator), 9 more pairs link regions point to point; actioninfo is a mood name here.

  • ElevatorStore only read connect_node / connect_node_levelN; it now parses the connect_node_location1/2 form into ElevatorDef.Instant, with InstantElevatorPlan as the skrit's state machine in data.
  • new_hub has no stitch_helper, so no world position: it streams in when a teleporter is pressed and WorldLayout places it as an island at a pinned, deterministic offset. The press is queued and handled from TickElevators.
  • The ride moves both cars, carries riders through the swap, applies the moods, re-bakes nav, and moves objects placed on a car node with it (the button rides its car). This applies to all lifts.
  • Trigger change: rows mixing receive_world_message with a volume condition in boundary mode wait_for_every_message / wait_for_first_message now treat the volume as a gate on the message (the editor labels these "message checker"). Before, stepping onto the platform launched the teleporter by itself, and arriving launched it straight back. World.dsmap has ~30 rows of this form; they now need their message too. Receipts pass, but those SP spots were not played through.

Checks

  • dotnet build SiegeFX.sln -c Release -warnaserror: clean. dotnet test: 147/147, with new UseRangeTests, PropRestPoseTests, InstantElevatorTests (data-free).
  • tools/receipts.sh: all PASS; parity ledger better (template-fields 397 -> 400), no regressions.
  • Runtime runs on multiplayer_world town_center in a co-op development branch carrying these commits (checked from logs and engine screenshots, not a full play-through): doors open once the hero reaches them ([door] … reached: actor 2.1u from the hinge), lever_glb_06 now sits at y 6.6 on the 6.55 platform (was ~7.6), and a scripted press rides town → hub → town with the rider carried both ways.

RenderHost becomes partial here as in #36 (same one-line change).

🤖 Generated with Claude Code

qwerin and others added 3 commits November 4, 2026 20:32
…n the air

The MP town transport stone's floating button is lever_glb_06
(m_i_glb_switch-06) placed on the teleport-hub elevator node 1.13 above
the platform. Its press clips (a_i_glb_switch-06-open/close) hold Bone01
one unit down mesh -Z from the bind for the whole clip; DS1 draws the GO
posed, and every placement is authored for the posed mesh. SiegeFX drew
the static prop at bind, so the button hung ~1 unit off its surface.

The whole lever_glb_01/02/03/06 switch family carries the same offset:
the dm_r8a and cr_r1 wall switches likewise sit exactly 1.0 in front of
their wall faces at bind and land flush once posed.

Static skinned props (not rigid-root) now apply the root bone's skin
matrix from their rest clip (chore_default, else chore_open, at t=0)
when the clip holds the root still and off the bind. A root that moves
through the clip (statue_cav_rise_01) is left alone. The offset is
applied inside the scale_multiplier, unscaled: the other MP towns'
buttons keep the template's scale 2 and sit on their platform only then.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 99557177da6f117dd9077c49abe5d20cde33fd89)
A door click opened the nearest door to the click point at once, from any
distance (OpenDoorNear ran straight from the click handler). It now queues
a walk-up-and-open like chests and levers: the acting character walks to
the click and the door opens only when they are within its reach — the
leaf's footprint radius plus [aspect] use_range, measured from the hinge,
same floor (UseRange.DoorInRange, unit-tested).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a press

Every multiplayer_world town's transport stone is an elevator_instant_4s_1c
gizmo. The skrit (components/elevators/elevator_instant_4s_1c.skrit) drives
TWO cars between two long shaft nodes: the town car (elevator_node) and its
twin in the new_hub region (e2_elevator_node). A press raises both cars to
level 2 of their own shaft (duration_locationN_moving_to_lv2, default 2s),
pops each to level 2 of the other shaft (SRequestNodeConnection), and sinks
them to level 1 there (duration_..._to_lv1, 16s authored) - the riders step
off in the hub. The 8 towns (town_center, des_r1, snow_town, oasis,
mine_town, swamp_town, castle_town, ocean) each pair with one of new_hub's 8
gizmos (other_elevator); 9 more pairs link regions point to point.
Actioninfo is a mood name here (multiplayer_world_teleport darkens the
shaft, multiplayer_world_<dest>_teleport sets the arrival).

ElevatorStore only read connect_node / connect_node_levelN, so these gizmos
were skipped and the button did nothing. Now:
- ElevatorStore parses the connect_node_location1/2 form (8 docks, both
  cars, durations, actioninfo, effects) into ElevatorDef.Instant;
  InstantElevatorPlan is the skrit's state machine as data.
- new_hub has no stitch_helper, so no world position: it streams in when a
  teleporter is pressed and WorldLayout places it as an island at a pinned,
  deterministic offset (same on every co-op machine). The press is queued
  and handled from TickElevators (streaming adds triggers / rebuilds nav).
- The runtime rides both cars, carries riders through the pop, applies the
  moods to this machine's hero, re-bakes nav, and moves objects placed on a
  car node with it (the button and its use points ride the car).
- The town and hub gizmos of one teleporter share one runtime (scid alias).
- Trigger rows mixing receive_world_message with a volume condition in
  boundary mode wait_for_every/first_message now treat the volume as a gate
  on the message (the editor annotates it "message checker"). Before, the
  conditions were independent sources: stepping onto the platform launched
  the teleporter, and arriving on it launched it straight back.

Verified in --play-region town_center: town -> hub -> town round trip,
rider carried both ways, button usable at the far end.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codingncaffeine
codingncaffeine merged commit c3671d0 into codingncaffeine:main Nov 9, 2026
8 checks passed
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.

2 participants