Birdoggydog's Builds

Adding switches, grates and secret covers to dungeon sheets

Six new fixture types for the Godot dungeon importer, how a lever's state survives streaming, and what happened when they went onto the Breakout Site.

A dungeon sheet in Blobber can place six things it couldn’t before: a switch (a console, a lever or a valve wheel), a window, a grate, a door broken partly open, a cover over a secret passage, and a meat choke. The next day I put them on the Breakout Site’s four floors, all except the window.

53 seconds in first person in a small test dungeon. A grate that’s seen and shot through but not passed, a window and the meat choke, a lever that opens a door and kills a laser grid and stays thrown through a save and a load, a broken door walked through, a wall plate found and pulled off, and a vent grate named “Suspicious Grate” pulled off.

Everything is built from plain boxes for now. The meat choke is a gray-box stand-in, and the real growth is a separate piece of work.

Why did the sheet need more types?

The Breakout Site’s maps ask for things the importer could only report as “not understood”. There’s a security console that opens doors and turns off traps, glass and bars between rooms, doors broken half open, a plate over a secret crawl, and a meat growth across a corridor. Seven rows of the site’s manifest were waiting on these types.

I also wanted hidden covers to be found by looking. I asked for text that shows the name of whatever the cursor is over, with the grate saying something like “suspicious grate” and being interactive. The game already shows a prompt under the crosshair for every usable thing, so that prompt is the label and I didn’t build a second one. A grate cover’s default name is “Suspicious Grate” and a panel’s is “Suspicious Panel”.

Planning six fixtures in one script

All six types live in one static script, dungeon_fixtures.gd, which has the plan, the checks, the build and the rule for who can pass. There are three node types. DungeonBarrier is a StaticBody3D used for the window, grate, choke and broken door. DungeonSwitch is also a StaticBody3D. DungeonCover extends the existing DungeonDoor. The older importer files only got small blocks at their dispatch tables.

The plan keeps a dictionary called barred from a square to the fixture across it. The planner’s path check and the monsters’ fit check both ask one function, DungeonFixtures.lets(plan, at, width, height, flies):

  • A window, a grate and a choke never let anything through.
  • A cover lets things through only once it’s been pulled off.
  • A broken door never lets a flyer through, and it lets a walker through only if width + 0.1 <= gap.

A broken door is its own thing type and not a field on door, so locks, keys and the designer page’s key lists never see it. Its gap is 1.2 m by default. A refinement can change it, but it has to be at least 0.6 m and it’s trimmed to the doorway’s width minus 0.3 m. The slab hangs 2.5 degrees crooked with a scrap on the floor.

A switch has a kind (console, lever or valve), a does (open, lock or toggle) and works, which is a list of the ids of doors, traps and covers on the same sheet. It has to be on a full square of floor and it’s set 0.25 m off the nearest wall.

How does a thrown lever survive a save?

A switch’s state is a story flag, not a value on the switch. I went that way because the save’s lock section and the world streamer don’t write out a door that’s unlocked and unsealed. A door that a switch opened would get built sealed again when the site streamed back in. Story flags are always saved.

is_thrown() reads the flag, and falls back to the switch’s boolean when there’s no story. throw() sets the flag and applies it with sound and movement. settle() applies it silently and never moves a door, so a build or a load restores everything without a noise. _process polls the flag every frame and settles when it differs from what was last applied, which means a story script can set the flag and the door follows.

A trap that’s switched off isn’t saved on the trap either. Trap.set_disabled(true) stops its processing and its tick, and hides every lit node (an OmniLight3D, or a mesh whose first surface is a StandardMaterial3D with emission on). The switch puts it back from the flag after a load.

The only new thing in the save is the cover’s found state.

Seeing and shooting through bars

Every fixture is an ordinary collider, so nothing walks through one. Sight and shots are handled by a static helper, SeeThrough, which keeps two arrays of collider RIDs. Windows, chokes, grates and a shut grate cover go in the sight list. Grates and the grate cover also go in the shot list.

SeeThrough.sight(query) appends the sight list to a ray’s exclude, and the monster’s can_see() calls it. SeeThrough.shot(query) is called by projectiles and by the party’s crosshair ray. So a window and a choke stop shots and a grate doesn’t. Monsters see through glass, bars and the choke and will come up to them, and a spitter will spit at the glass. I kept that as the default.

Finding a plate that looks like wall

A panel cover is built from the wall’s material with a thin dark seam on each edge as the only sign it’s there. It shows no prompt until the party is within 1.9 m with the panel under the crosshair. Then it plays the unlock sound, says “// THIS PLATE IS NOT WALL”, and E pulls it off. A grate cover is found from the start. A switch can pull either one off, found or not. A cover never shuts again, and monsters never open one but pass through an open one if they fit.

Because the cover is a subclass of the door, the existing door save section and the world streamer handle it with no new kind. That subclass was also the one bug the test harness found. The cover’s _ready() never called super(), so the door code it extends never ran and the cover had no collider and no leaves. A script’s _ready() replaces the one it extends unless it calls it.

Checking a sheet for switches that can’t be reached

The sheet checks index every thing by id. A switch that names an id that isn’t on its sheet, or names something that isn’t a door, trap or cover, is an error. A thing controlled by two switches and a switch that controls nothing are warnings.

There’s also a reach check. It walks from the entrance with the fixtures in place. Windows, grates and chokes block, and a door blocks until its switch has been reached. It repeats, throwing every switch it reaches, and warns about a switch nobody can get to and the door that never opens because of it.

The harness has 128 checks and they all pass.

Putting them on the Breakout Site

The designer page isn’t published with the six types yet, so a sheet can’t have them. The dungeon’s JSON file has hand-edited parts, added and refinements, that are kept across a re-import, so everything went in there:

"added": [
  {"t": "switch", "x": 32, "y": 28, "kind": "console", "does": "open",
   "works": ["<door>", "<plate>", "<plate>"], "name": "Security Console"},
  {"t": "broken_door", "x": 2, "y": 32},
  {"t": "cover", "x": 16, "y": 27, "kind": "grate"},
  {"t": "choke", "x": 33, "y": 21}
],
"refinements": {
  "door@2,32": {"skip": true},
  "broken_door@2,32": {"name": "Security Door 2"},
  "cover@27,3": {"front": "east"}
}
65 seconds in the Breakout Site. Floor 1’s security hall with two plates, a broken security door hanging partly open, the east corridor looking like bare wall, a crouch through the cover into the first subject’s cell, the bathroom’s vent grate, then floor 2’s sealed freight gate and the meat choke east of the valve room.

Floor 1 got a Security Console that opens one security door and switches off two plates, two broken security doors, a Suspicious Grate over the bathrooms’ crawl, a Suspicious Panel over the crawl into the first subject’s cell, and a choke across a hall. Floor 2 got a Steam Valve that opens the Freight Gate, which is sealed until the valve is turned, and a choke. Floor 3 got a grate and a broken Quarantine Door, and floor 4 got a choke.

Three things went differently from the plan:

  • The console can’t control security doors 2 and 3, because the map makes them broken doors and a broken door can’t be controlled. So it controls the one intact security door and the two plates.
  • I couldn’t turn a sheet’s door into a broken door, because two things on one square were refused. I changed the planner so a thing that a refinement skips no longer claims its square. The sheet’s door is skipped and a broken_door is added on the same square.
  • The importer faced the panel over the crawl the wrong way. It faces a cover toward the shortest way in from the entrance, and here the shortest way goes through a locked door, so the panel’s face was on the side the party can’t reach first. A cover now accepts a front refinement naming the side, and the importer checks that side is along the passage.

The test for the site has 114 checks, and a selection of 34 related tests passed after I fixed two that failed because the site now has a sealed gate and broken doors.

What’s left?

  • No window is on the site. A window is glass across an open square, and the map has no open square where its notes want one. That’s a change to the map, not the file.
  • The designer page isn’t published with the six types, so they live in the file and not in the page’s sheets.
  • A switch only controls things on its own floor. I looked at what crossing floors would take and didn’t build it.
  • A re-import moves four bodies I’d placed by hand in the middle of their squares to 0.9 m off the middle, against a wall. I haven’t decided whether the tool should leave a written position alone.
  • I haven’t thrown the valve and console forms or the lock and toggle modes in the test dungeon, and I haven’t watched a monster at glass. That’s only tested with sight rays and path checks.