Skip to content

Tutorials · Worlds and Levels · Chapter 12

Cause and effect

By the end

  • lever.rgs, one script for every lever: thrown once with E, it says pulled to whatever its Signal names
  • A drawbridge in place of bridge-block: an empty Solid bridge layer on the vale’s map, which drawbridge.rgs fills plank by plank when bridge-lever is thrown
  • Two artifacts, a cracked block and a gate whose lever names its own grate; the gate dropped twice and each copy renamed
  • A cracked wall, a column of scene tiles on a cracked layer, which the pilgrim’s strike brings down in one step
  • The tower lever, and keyhole.rgs taking the door away when it hears it
On this page

When to reach for a Signal

The vale’s ravine is still crossed by chapter 1’s grey bridge-block. This chapter takes it away and gives the world levers, and things they work: a drawbridge over the ravine, two gates in the crypt’s east, and the tower door; and a cracked wall the pilgrim strikes down. Four levers, one script: what each one works is the node its Signal names.

Reach for a Signal when one thing should set off another somewhere else: a lever and what it opens. The lever’s script says a name, pulled, and its Signal says who hears it; the hearer’s script listens for the name. Neither script names the other, so one lever script serves every lever. Reach for the tile writes, set_tile and clear_tile, when the map itself should change while the game plays: a bridge laid plank by plank, a door taken away.

What collides, what draws, what sorts: a Signal draws nothing and collides with nothing; it is a list of names and who hears each. A lever is a Sprite on Props, and its Area notices him and stops nothing. The drawbridge’s planks are tiles on a Solid layer, so each holds him up once it is laid. A grate is a Sprite on Props with a Collider, and a cracked block a Sprite on Ground with a Collider of its own, which the map places as a scene tile.

A lever

Every lever in Hollowmere is one picture, art/lever.png, 16 by 16, which came with the starter, a lever thrown to the left, and one script, lever.rgs.

  1. Double-click scenes/vale.ron in Assets. In Assets, click Assets in the path bar, so the script is written at the project’s top, then click Create, choose New script, type lever and press Enter. Click the Code tab, then lever.rgs in its Scripts list, replace everything in it with the script below, and press Ctrl+S. Click the Viewport tab.
rgs lever.rgs
/// A lever. Standing at it, the pilgrim throws it with the action key: its
/// handle swings over, and it says pulled to whoever its Signal is wired
/// to. A lever is thrown once and stays thrown. Every lever in Hollowmere
/// is this script; what each one works is the node its Signal names.
component Lever : Area {
    /// Whether the pilgrim is within reach: inside the lever's Area.
    #[readonly]
    near: bool = false,
    /// Whether it has been thrown.
    #[readonly]
    pulled: bool = false,

    fn on_enter(other: NodeId) {
        // Something has come within reach. Only the pilgrim, who is in the
        // group pilgrim, can throw it.
        if in_group(other, "pilgrim") {
            near = true;
        }
    }

    fn on_exit(other: NodeId) {
        if in_group(other, "pilgrim") {
            near = false;
        }
    }

    fn loop() {
        // The action key (E) throws it, once: its picture is flipped, so the
        // handle leans the other way, and whoever its Signal names hears
        // pulled on the next step.
        if near && !pulled && pressed("action") {
            pulled = true;
            set_flag("SpriteComponent", "flip.x", true);
            emit("pulled");
        }
    }
}
component Lever : Area, near and pulled
A script with an Area base: the lever’s Area is its reach. near is whether he is inside it, set by on_enter and cleared by on_exit; pulled is whether it has been thrown. Both are #[readonly], greyed in the Inspector, to watch.
pressed
Whether an action went down this step: action is E or Enter.
set_flag with flip.x
Ticks the Sprite’s Flip X: the picture is mirrored, and the handle leans the other way. The lever’s base is its own mirror, so only the handle moves.
emit
Says a name, pulled, to whoever the node’s Signal wires it to. They hear it on the next step, never the same one. !pulled keeps a lever from saying it twice: a lever is thrown once.

The drawbridge

The drawbridge is laid by the vale’s map itself, into a layer of its own, empty until the lever is thrown.

  1. Select buildings and click the Tilemap tab. Click ground’s row and press +: an empty layer goes in over ground and lights. Double-click its name and rename it bridge.
  2. Open bridge’s gear and tick Solid. Leave the rest as a new layer comes: z 0, its eye open, One-way, Blocks nav and Occluder unticked. Paint nothing on it. Press Ctrl+S.
The Layers band: 3 layers; back at z -1; ground at z 0, SN on its gear; bridge, lit, at z 0, S on its gear; bridge’s gear open on its list: Solid ticked, One-way, Blocks nav and Occluder unticked, and under Collision layers, Every layer ticked
bridge’s gear: Solid, the rest as a new layer comes.
  1. In the Hierarchy, select bridge-block, press Delete, and press Delete in the box that asks: the ravine is open.
  2. In Assets, with Assets clicked in the path bar, click Create, choose New script, type drawbridge and press Enter. Click the Code tab, then drawbridge.rgs in its Scripts list, replace everything in it with the script below, and press Ctrl+S. Click the Viewport tab.
  3. Select buildings, and drag drawbridge.rgs from Assets onto its Inspector: it adds Drawbridge, with nothing to set. Press Ctrl+S.
rgs drawbridge.rgs
/// The drawbridge over the ravine, laid by the vale's tilemap itself. When
/// the lever by the ravine is pulled, planks are laid across, one at a
/// time, west to east, into the map's bridge layer. The layer is solid, so
/// a plank laid there can be walked on.
component Drawbridge {
    fn loop() {
        // Wait for the lever: its Signal sends pulled to this map.
        await signal("pulled");
        // Then the planks, a tenth of a second apart: the slab, tile 489,
        // into row 0, the row under the walking line, of this map's bridge
        // layer, from column 120, at the ravine's west lip, to column 129,
        // at its east. They are written out one by one because a for loop
        // runs all its turns in one step and cannot wait between them, and
        // there is no while.
        set_tile(this(), "bridge", vec2(120.0, 0.0), 489);
        await wait(0.1);
        set_tile(this(), "bridge", vec2(121.0, 0.0), 489);
        await wait(0.1);
        set_tile(this(), "bridge", vec2(122.0, 0.0), 489);
        await wait(0.1);
        set_tile(this(), "bridge", vec2(123.0, 0.0), 489);
        await wait(0.1);
        set_tile(this(), "bridge", vec2(124.0, 0.0), 489);
        await wait(0.1);
        set_tile(this(), "bridge", vec2(125.0, 0.0), 489);
        await wait(0.1);
        set_tile(this(), "bridge", vec2(126.0, 0.0), 489);
        await wait(0.1);
        set_tile(this(), "bridge", vec2(127.0, 0.0), 489);
        await wait(0.1);
        set_tile(this(), "bridge", vec2(128.0, 0.0), 489);
        await wait(0.1);
        set_tile(this(), "bridge", vec2(129.0, 0.0), 489);
        // The bridge is laid. The loop goes round and waits again, for a
        // lever that is thrown only once.
    }
}
component Drawbridge and await signal
A script with no base, on the map’s node. signal("pulled") waits until that name reaches this node: the lever’s Signal will send it here.
set_tile and this
Paints a tile, by its number, into a cell of a layer of a map, from the next step: here the slab, #489, the one the hut’s step is painted with, into row 0 of this map’s bridge layer, columns 120 to 129, x 1920 to 2080. this() is the node the script is on, the map.
await wait
Waits that many seconds, then goes on with the next line: a plank each tenth of a second. A wait counts down a step of 1/60 s at a time, and six steps leave a sliver of the tenth still to run, so it runs out on the seventh: the planks are 7 steps apart.
Why the planks are written out
A for runs all its turns in one step and cannot wait between them, and there is no while. A field counting the columns, a plank each turn of loop(), would come round a step later each turn.
Writing tiles while the game plays
set_tile and clear_tile change a map’s cells for the run only: the map’s file, in the editor and in an export, and the scene the next time it is entered, are as painted. Here bridge stays empty in the file.

Now the lever. Every lever is built the same way; this first one stands on the hills’ top by the ravine, 16 short of its west lip at x 1888.

  1. Select vale, press + Add node and choose Sprite: the menu opens on Add child of vale. Rename it bridge-lever, and type its Position 1872, 0.
  2. In its Sprite, drag art/lever.png onto Texture, type Size 16, 16, its own, and Anchor 0.5, 1, so it stands on its foot, and set Sort Layer to Props.
  3. Press Add component and choose AreaComponent. In its Shape, type Size 24, 32 and Offset 0, -16, and leave the Area’s Layer and Mask as they come, pilgrim, as his: x 1860 to 1884, y -32 to 0, his reach.
  4. Drag lever.rgs from Assets onto its Inspector: it adds Lever, Near and Pulled greyed.
  5. Press Add component and choose SignalComponent. Press the + beside Connections: a row, [0]. Open it, type pulled into Signal, and for To click it and choose buildings from the list, or drag buildings from the Hierarchy onto it. Press Ctrl+S.
The Inspector on bridge-lever, cut to its Lever and its Signal: Lever, Near and Pulled unticked and greyed; the SignalComponent, Connections [1 items] and its +, [0] open with its arrows and bin, Signal pulled, To buildings beside a picker
bridge-lever’s Signal: pulled, to buildings.
The Signal’s Connections
The names this node says, and who hears each: a row a name and a hearer. + adds a row; its arrows move it, and its bin takes it off. A name with no hearer, or a hearer with no name, is a row still being typed, and draws no wire.
The connection’s Signal and To
Signal is the name, as the script says it, pulled. To is the node that hears it, chosen from the list or dragged from the Hierarchy. The scene keeps the node by its number, not its name.

With the lever selected, the Viewport draws where its signals go.

  1. Select bridge-lever, press Frame, and pick 25% from the zoom menu at the Viewport’s foot.
The editor at 25%: the whole vale a thin strip across the middle of the Viewport, from the hut at the left to the chapel at the right; bridge-lever selected on the hills’ top beside the ravine, and from it a thin magenta arrow running west along the walking line to the vale’s west end, pulled written over its middle; the Inspector shows bridge-lever, Position 1872.0 and 0.0, and its AreaComponent
bridge-lever’s wire: west, to buildings, the map, whose node is at the vale’s west end. The Assets list already shows crack.rgs, which you make in the next section.
  1. Click Gizmos at the Viewport’s foot.
The Gizmos menu open at the Viewport’s foot, over its corner and the edge of the Inspector: All and None; Collision, Triggers, Lights, Navigation, Paths, Forces, Framing, Markers, Audio, Shadows and Routes ticked; Bodies unticked; Signals ticked; Selected only and Hide while playing unticked; the Gizmos button under it
The Gizmos menu: Signals ticked, as it comes.
A Signal’s wires
While a node with a Signal is selected, the Viewport draws an arrow from it to each node it names, and over the arrow’s middle the names that travel it, in the order of its rows. They follow the nodes in Play too. A hearer that is not in the scene draws none: Problems says so.
The Gizmos menu’s Signals
Draws a selected node’s wires; untick it to hide them. Ticked as it comes.
  1. Press Run game: it runs the startup scene, the vale. Walk him east to the lever, and on past it.
The game by the ravine: the lever on the hills’ top, its handle leaning left, unthrown, and him past the ravine’s west lip, dropping into it, nothing laid across; the town and the trees below, the clouds and the sun behind
Past the lever, with the bridge gone.

Without the lever, the ravine swallows him: from the lever, with D held, he walks off the lip and falls.

  1. Close the game’s window, or press Stop game, which stands where Run game was while it runs. Then press Run game again, walk him to the lever, stop on it and press E.
The game by the ravine, close: him standing on the hills’ top by the ravine’s west lip; slab planks are laid across the ravine one after another, from its west lip eastward, out to the edge of the view
Watch the ravine: a plank is laid in the next cell east every tenth of a second, west to east.

Each plank comes 7 steps after the one before, and all ten are laid within 1.2 seconds of the tap, out to x 2080, past the view’s edge here. With D held he walks across, to x 2200 on the far side, never below the walking line.

The game over the ravine: him walking east on the row of slab planks laid across it, the hills’ top and the lever behind him at the left, the town and the trees below
Across the drawbridge.

A second E lays nothing more: the lever stays as it was, says nothing, and no cell changes after the tenth. The planks are laid in the map’s own copy for the run; its file keeps bridge empty.

Two artifacts

The crypt’s cracked wall and its gates are each made once and saved as artifacts: a cracked block, which the map places in a column of cells, and a gate, a lever and its grate, dropped twice. Each is made in a new scene and never saved there, so the crypt is left as it was. Their pictures came with the starter: art/crack.png, 16 by 16, the crypt sheet’s tile #59 pixel for pixel, and art/grate.png, 16 by 64, a gate’s iron bars.

  1. In Assets, with Assets clicked in the path bar, click Create, choose New script, type crack and press Enter. Click the Code tab, then crack.rgs in its Scripts list, replace everything in it with the script below, and press Ctrl+S. Click the Viewport tab.
rgs crack.rgs
/// A cracked block in a wall. It is a scene tile: the map places one on
/// every cell painted with it. When a block is struck and goes, the block
/// above it comes down too, and that one brings down the next, up the wall.
component Crack {
    fn on_destroy() {
        // The map this block stands in: the nearest node in the group map.
        let map = nearest("map");
        // The cell above this one, 16 up, is cleared on the cracked layer.
        // Its block goes the same way, and tells the one above it in turn.
        clear_tile(map, "cracked", cell_at(map, position() - vec2(0.0, 16.0)));
    }
}
A scene tile’s farewell, on_destroy
Runs once, as the node goes. A scene tile’s node goes when its cell is emptied or painted over, so a block’s farewell runs when the block is struck or cleared.
nearest
The nearest node in a group, as the step began: here map, which halls joins below, so each block finds the map it stands in.
clear_tile, cell_at and position
clear_tile empties a cell of a layer of a map; cell_at is the cell a point in the world falls in; position() is where this node is. Here the cracked cell 16 above the block: the block above goes the same way, and tells the one above it in turn.
  1. Double-click scenes/crypt.ron in Assets and press Ctrl+S. Choose File → New scene: an empty scene takes the crypt’s place.
  2. Press + Add node and choose Sprite. Rename it crack. In its Sprite, drag art/crack.png onto Texture, type Size 16, 16, its own, and set Sort Layer to Ground. Leave Anchor at 0.5, 0.5: a scene tile’s node stands on its cell’s middle.
  3. Press Add component and choose ColliderComponent. Type its Size 16, 16; leave its Offset at 0, 0 and Solid ticked.
  4. Drag crack.rgs from Assets onto its Inspector: it adds Crack.
  5. Right-click crack in the Hierarchy and choose Save as artifact…. In the save dialog, open the artifacts folder and save the file as crack.artifact.

Now the gate, whose grate rises when its lever is thrown.

  1. In Assets, with Assets clicked in the path bar, click Create, choose New script, type grate and press Enter. Click the Code tab, then grate.rgs in its Scripts list, replace everything in it with the script below, and press Ctrl+S. Click the Viewport tab.
rgs grate.rgs
/// A gate's grate. When its lever is pulled, it rises out of the way, up
/// in front of the stone over the gate, and stays up.
component Grate {
    /// Whether it has risen. A grate opens once.
    #[readonly]
    open: bool = false,

    fn loop() {
        // Its lever's Signal sends pulled here. Heard, the grate slides up
        // by its own height, 64, over a second, easing in and out.
        if !open && heard("pulled") {
            open = true;
            await anneal_by(vec2(0.0, -64.0), 1.0, .InOutSine);
        }
    }
}
component Grate, open and heard
A script with no base. heard("pulled") is true on a step that name reached this node; open keeps the grate from rising twice.
anneal_by
Moves this node by a distance over the seconds it is given, along a curve: up 64, the grate’s own height, over a second, easing in and out.
  1. Choose File → New scene: an empty scene takes its place, without asking, and the unsaved copy of crack goes with it.
  2. Press + Add node and choose Node. Rename it gate.
  3. Select gate, press + Add node and choose Sprite: the menu opens on Add child of gate. Rename it lever, type its Position -32, 0, and build it as bridge-lever was built: its picture, its AreaComponent and Lever. Leave its Signal for now.
  4. Select gate again, press + Add node and choose Sprite. Rename it door. In its Sprite, drag art/grate.png onto Texture, type Size 16, 64 and Anchor 0.5, 1, and set Sort Layer to Props.
  5. Press Add component and choose ColliderComponent. Type its Size 16, 64 and Offset 0, -32: x -8 to 8, y -64 to 0. Drag grate.rgs onto its Inspector: it adds Grate.
  6. Select lever, add its SignalComponent, press +, type pulled into Signal, and choose door for To.
  7. Right-click gate in the Hierarchy, choose Save as artifact…, and save it in artifacts as gate.artifact.
  8. Double-click artifacts/gate.artifact in Assets. The editor asks Save the open scene?: press Discard. The artifact opens as a scene of its own. Select lever, press Frame, and pick 400% from the zoom menu.
The editor on gate.artifact at 400%: the Hierarchy holds gate, with lever and door under it, lever lit; in the Viewport the lever inside the yellow box of its Area, and east of it the grate, outlined in green, a magenta arrow from the lever’s foot to the grate’s with pulled written over it; the Inspector shows lever, Position -32.0 and 0.0, and its AreaComponent: Size 24.0 and 32.0, Offset 0.0 and -16.0
gate.artifact: one lever, wired to its own grate.
Save as artifact…
In a node’s right-click menu in the Hierarchy: writes the node and everything under it into a file, an artifact, and puts a copy of that file in its place, as one step to undo. A copy follows its artifact; what is changed on a copy, its name among them, is the copy’s own.
One artifact’s lever names its own door
Inside the artifact the lever’s To is the artifact’s door. Each copy dropped into a scene is numbered afresh, and its lever’s To is its own copy’s door: two copies, two levers, each opening its own grate.
  1. Double-click scenes/crypt.ron in Assets to go back to the crypt.

The crypt’s map

  1. Select halls and click the Tilemap tab. Click tile #59, the cracked block, in the picker, and drag artifacts/crack.artifact from Assets onto Scene’s slot in the Tile band.
The Tile band for tile 59: an empty dark preview, Cell 59 of 64 and, in amber, Places a scene; Tint white; Art origin x 0.00 y 0.00, Z offset 0, Y sort 0.00 and Weight 1.00; Animation, still, and Add frame; Data, no values, and Add value; Scene c75baa12; Draws as #59
Tile #59: its Scene holds the cracked block, by the start of its id.
  1. Click ground’s row and press +, and rename the new layer cracked. Leave it as it comes, not Solid: each block stops him with its own Collider.
  2. With #59 held, draw a Line up column 132, from cell 132, -12 to 132, -1: twelve blocks, x 2112 to 2128, y -192 to 0.
  3. Click ground’s row, then the stone row in the Terrains band, armed. With Rect, lay the stone over each gate in the table below, from the ceiling’s underside down to y -64, over the two columns its grate will stand across.
OverCells (column; row)World (x; y)
gate-1141 and 142; -12 to -52256 to 2288; -192 to -64
gate-2172 and 173; -12 to -52752 to 2784; -192 to -64

Each Rect changes 18 cells, 16 new and the ceiling’s row over them laid again; each is the edge its neighbours call for, so there is no plain stone for Variety to roll. The grate, 64 tall, will meet the stone: shut, it hangs from it, and he cannot jump over it; open, it slides up in front of the stone, on Props over Ground, its foot level with the stone’s, y -64, high over his 30.

  1. Click torches’ row and tile #57, the torch’s bracket, in the picker: the brush is on tiles again. With Brush, click cell 135, -6: x 2160 to 2176, y -96 to -80. Press Ctrl+S.

Its light, at 2168, -88, two cells east of the cracked column at the halls’ torches’ height, lights the column, so it reads as a thing to strike, and the rubble the strike will leave; mural-3’s wall it barely reaches.

In the Viewport each cell painted with a scene tile shows only its marker, as the torches’ cells do, and in the cracked column the twelve markers’ labels lie over one another. Play shows the blocks themselves.

  1. Press F5: Play runs the open scene. Select mural-3, press Frame, and pick 200% from the zoom menu.
The editor in Play at 200%, at step 1: the crypt’s brick wall over its floor; the cracked column, a stack of stone blocks each outlined in green, rising out of the top of the view; a torch burning east of it, the ring of its light arcing over the wall; mural-3’s outline empty to the west, and at the right edge the stone laid over the first gate; in the Hierarchy, under halls, tile 132,-12 to tile 132,-1, then tile 7,-6, tile 21,-6, tile 35,-6, tile 49,-6, tile 63,-6, tile 77,-6 and tile 135,-6; the Inspector on mural-3
In Play, each cell of the column is its block, a node of its own.

The strike

The pilgrim strikes with the action key: a cracked block just ahead of him is painted over with rubble, #60, and its farewell brings down the rest.

  1. Press F5 again to stop. Select halls, and in its Node’s Groups type map and press +: crack.rgs’s nearest("map") finds the map by it.
  2. Double-click pilgrim.rgs in Assets. In the Code tab, replace its first two lines, its /// comment, with the three below.
rgs part of pilgrim.rgs
/// The pilgrim in the crypt. Down drops him through a ledge he stands on,
/// a tile that hurts sends him back to the checkpoint, and the action key
/// strikes the wall in front of him, which breaks a cracked block.
  1. At the end of loop(), after the hazards’ if, add the strike below, and press Ctrl+S. Click the Viewport tab.
rgs part of pilgrim.rgs
        // The action key (E) strikes the wall just ahead of him: the cell 20
        // east of his feet and 4 above them. A cracked block there, tile 59
        // on the cracked layer, breaks into rubble, tile 60.
        if pressed("action") {
            let ahead = cell_at(node(map), position() + vec2(20.0, -4.0));
            if tile_at(node(map), "cracked", ahead) == 59 {
                set_tile(node(map), "cracked", ahead, 60);
            }
        }
tile_at and node
tile_at is the tile painted in a cell of a layer, as the step began: its number, 0 for an empty cell. node(map) is the map the script’s Map row picked, halls, as the calls take it.
set_tile over a scene tile
Painting #60 over a cracked block’s cell takes the block away: its node goes, and its farewell runs.

The strike reads the cell 20 east of his feet and 4 above them. Stand close to the wall: from anywhere between x 2092 and the wall’s face, which stops his feet at 2106, it reaches the column; further back it reads an empty cell.

  1. Press F5, walk him east to the cracked wall until it stops him, and press E.
The game at the cracked wall, close: him standing at the column of cracked blocks, a torch burning higher up east of it; in one step the whole column is gone, a few broken stones lying at its foot, and the brick wall shows where it stood
Watch the column: struck once, it is gone in that step, and rubble lies at its foot.

The whole column falls in the strike’s own step: rows -12 to -2 emptied, and the rubble in row -1. With D held he walks on through where it stood. Nothing past the column changes, and a second strike, on the rubble, changes nothing. On a copy whose crack.rgs has no farewell, only the struck block goes, and the eleven above it stand.

Two gates

Two copies of the gate go further east in the crypt, under the stone laid for them.

  1. Press F5 again to stop. Select halls, and drag artifacts/gate.artifact from Assets into the Viewport: a copy of the gate lands where you drop it. Type its Position 2272, 0.
  2. In the Inspector’s Name row, rename it gate-1. Select its lever and rename it gate-1-lever, and its door, gate-1-door.
  3. Select gate-1 itself, not its door, and drop gate.artifact again. Type its Position 2768, 0, and rename it and its two gate-2, gate-2-lever and gate-2-door. Press Ctrl+S.
The Hierarchy: crypt, and under it gate-2, open over gate-2-lever and gate-2-door, gate-2-door lit; then gate-1 over gate-1-lever and gate-1-door; each of the six with a dot after its name; then pilgrim with tag and lantern, camera, halls with its map chip, ambient, checkpoint, crypt-bounds, mural-1 to mural-3, keyhole over bell-view, and bell over bell-rope and bell-chain
Two copies, renamed: each name the copy’s own.
A drop
An artifact dragged from Assets into the Viewport makes a copy of it where it lands. The copy goes into the selected node’s parent, at the head of its list: each gate lands in crypt, and the second over the first.
A member renamed, an override
A copy’s nodes follow the artifact, but a name typed in the Inspector’s Name row is the copy’s own, an override, and so is a Position typed: the Hierarchy marks each with a dot after its name. A member renamed in the Hierarchy instead, its row double-clicked, keeps its name the same way.
  1. Press F5, walk him east, strike the cracked wall down with E, and walk on to gate-1’s lever. Press E there.
The game at the first gate, close: him standing west of a grate that hangs from the stone over the passage; the grate slides up in front of the stone, its foot rising clear of the passage, and stays up
Watch the grate: it slides up its own height over a second, in front of the stone over it, and stays up.

Each lever raises its own grate 64 within 1.2 seconds, while the other gate stays shut: in the game, each copy’s lever names its own copy’s door. A shut gate cannot be jumped: the grate and the stone over it are one wall.

The tower door

The fourth lever stands at the tower door, inside the keyhole’s Area, and its Signal tells the keyhole, whose script now takes the door away.

  1. Press F5 again to stop. Select crypt, press + Add node and choose Sprite: the menu opens on Add child of crypt. Rename it tower-lever, type its Position 3328, 0, and build it as every lever, its Signal’s To keyhole.
  2. Double-click keyhole.rgs in Assets, replace everything in it with the script below, and press Ctrl+S. Click the Viewport tab.
  3. Select keyhole: its Keyhole has a Map row now, empty. Click it and choose halls. Press Ctrl+S.
rgs keyhole.rgs
/// The keyhole in the tower's door. While the pilgrim stands before the
/// door he peers through it, and the bell beyond shows in the keyhole's
/// shape. When he steps away, the keyhole is only a keyhole again. When the
/// tower lever is pulled, the door is taken away, and with it the keyhole.
component Keyhole : Area {
    /// The crypt's tilemap, whose ground layer holds the door.
    #[node_path]
    map: i32 = 0,

    fn on_enter(other: NodeId) {
        // Something has come to the door. Only the pilgrim, who is in the
        // group pilgrim, peers through, and only while the door is there:
        // while the cell the keyhole is in still holds the keyhole's planks,
        // tile 63. The keyhole hangs from the cell's foot, so its cell is the
        // one 8 above it.
        let cell = cell_at(node(map), position() - vec2(0.0, 8.0));
        if in_group(other, "pilgrim") && tile_at(node(map), "ground", cell) == 63 {
            // The keyhole's mask is switched on, so what lies behind the
            // door shows in its shape.
            set_flag("SpriteMaskComponent", "enabled", true);
        }
    }

    fn on_exit(other: NodeId) {
        // The pilgrim steps back from the door: the mask is switched off,
        // and the bell is hidden again.
        if in_group(other, "pilgrim") {
            set_flag("SpriteMaskComponent", "enabled", false);
        }
    }

    fn loop() {
        // The tower lever's Signal sends pulled here. Heard, the door's six
        // cells are cleared from the ground layer: the keyhole's cell and the
        // one beside it, east, on the keyhole's row and the rows above and
        // below it. The keyhole is switched off, for there is nothing left
        // to peer through.
        if heard("pulled") {
            let keyhole = cell_at(node(map), position() - vec2(0.0, 8.0));
            for row in -1..2 {
                for column in 0..2 {
                    clear_tile(node(map), "ground", keyhole + vec2(column, row));
                }
            }
            set_flag("SpriteMaskComponent", "enabled", false);
        }
    }
}
#[node_path] and map
A field the Inspector shows as a node picker, Map: the map whose cells the keyhole reads and clears. Nothing picked is 0, nobody.
The keyhole’s cell
The keyhole hangs from its cell’s foot, so its cell is the one 8 above its node. On entering, it switches on only while that cell still holds #63.
heard in loop()
True on the step the tower lever’s pulled reaches the keyhole: then the door’s six cells, the keyhole’s, the one east of it, and those above and below both, are cleared from ground, and the mask is switched off.
  1. Press F5 and walk him east, striking the wall and throwing each gate’s lever as before, to the tower lever at the door. Press E there.
The game at the tower door, close: him standing before the tower door, the bell showing in the keyhole’s head; at once the six cells of planks are gone, the doorway showing the stone back wall behind, and the keyhole with them
Watch the door: at the tap its planks are gone in a step, and with them the keyhole and the bell in it.

The door’s six cells are cleared within three steps of the keyhole hearing the lever, and the keyhole is off. He walks through to the tower, x 3440, the doorway level with the floor; back at the door the keyhole stays dark, since its cell holds no keyhole any more.

  1. Press F5 again to stop.

The levers and what they work

LeverPositionIts Signal’s ToWhat it works
bridge-lever, in vale1872, 0buildingsdrawbridge.rgs: ten planks on bridge
gate-1-lever, in gate-1-32, 0gate-1-doorgrate.rgs: up 64 over a second
gate-2-lever, in gate-2-32, 0gate-2-doorthe same
tower-lever, in crypt3328, 0keyholekeyhole.rgs: the door’s six cells cleared
WhatSettings
Every leverSprite lever.png, 16 by 16, Anchor 0.5, 1, Props; AreaComponent 24 by 32 at 0, -16; Lever; a Signal, pulled
crack.artifactSprite crack.png, 16 by 16, Ground; ColliderComponent 16 by 16; Crack
gate.artifactgate; its lever; its door: Sprite grate.png, 16 by 64, Anchor 0.5, 1, Props; ColliderComponent 16 by 64 at 0, -32; Grate
gate-1, gate-2copies at 2272, 0 and 2768, 0
the vale’s mapbridge: Solid, empty
the crypt’s maptile #59’s Scene crack.artifact; cracked, a column of #59; stone over each gate; a torch at 135, -6
hallsGroups map
keyholeMap halls

Check it works