Birdoggydog's Builds

Seating a dungeon's NPCs from its map file in Godot

The starting dungeon's characters, lockers and keys are joined to its four floors, and a lockpicking number turned out to have no right answer.

The Breakout Site has its people in it now. Chief Scientist Carl, Helena and Subject 29 are on the squares my map gives them on floor 1, a new game inside the site starts with nothing, and Carl hands over the one Facility Key, which opens nine doors. A tenth door wants a different key, and that one is on a dead orderly lying next to it.

50 seconds in first person on floor 1. The starting locker, Carl giving the Facility Key and dying, a cell opened with it, the Ward Door refusing it, the orderly’s body searched for the Ward Key, Subject 29 eaten for 120 juice, then the security door and the lift to floor 2. The party is placed in front of each scene and not walked between them, and the game’s debug text is on the right of the picture.

This joins the two posts from earlier today. Stacking dungeon floors and building lifts built the place, and letting NPCs die and leave bodies built the people in a sealed test room. That test room is deleted.

Naming the people in the dungeon file

I didn’t change the map. It still has three npc things where I drew them, and the dungeon’s JSON file says who each one is in a refinements block, addressed by type and square. The lockers and doors get their content the same way:

{
  "id": "breakout_site",
  "stack": "up",
  "start_with_nothing": true,
  "site_key": {"id": "breakout_site_facility_key", "name": "Facility Key"},
  "added": [{"t": "npc", "x": 23, "y": 17, "name": "Ward Orderly"}],
  "refinements": {
    "npc@17,4":   {"npc": "chief_scientist_carl"},
    "npc@17,15":  {"npc": "helena"},
    "npc@26,19":  {"npc": "subject_29"},
    "npc@23,17":  {"npc": "breakout_ward_orderly"},
    "chest@14,5": {"stock": "breakout_start",  "title": "Vat-side Locker"},
    "chest@34,2": {"stock": "breakout_office", "title": "Chief Scientist's Locker", "code": "4004"},
    "door@24,18": {"name": "Ward Door", "key_item": "breakout_ward_key", "difficulty": 1}
  }
}

The orderly isn’t on my map at all, so the file adds him as a fourth npc thing at (23,17).

Three of those refinements are new. code gives a door or chest its code when the map doesn’t draw one. Carl says his locker’s code out loud as he dies, so 4004 now sits beside the locker in the dungeon file and not in a separate lock table. difficulty sets a door’s pick difficulty over whatever the map says. key_item names the content key a door wants.

start_with_nothing is a new field on the dungeon. The function that puts a new game’s party on the dungeon’s entrance square now ends by checking it, and if it’s true the inventory is cleared, everything equipped that isn’t an organ comes off, and the spell binders are blanked. It runs there because the level has already given the party its normal start by then. A normal new game is unchanged.

The locker by the vat needed one line. A chest built from a stock now also keeps the stock’s id, which is what the rule from the earlier post looks for, so it adds a weapon and spell sheets for each character the first time it’s opened.

How does a dead NPC get to his square?

An imported dungeon keeps a table of NPC id to position, one row per npc thing that names a character. A living NPC was already moved onto his spot when his node entered the scene tree. The dead were the gap, for two reasons. A body isn’t in the group that NPCs are found by. And the code that lays a body adds it to the tree first and then positions it from the saved state, so moving it when it enters the tree gets overwritten.

So the dungeon manager defers the body’s move until after it’s been laid:

func _on_node_added(node: Node) -> void:
    if node is NpcBody:
        _seat_body.call_deferred(node)     # once it has been laid
        return
    ...

func seat_all(dungeon: ImportedDungeon) -> void:
    var story := Story.of(self)
    if story == null:
        return
    for npc_id: String in dungeon.npc_spots:
        NpcFates.seat(story, npc_id,
                dungeon.global_transform * (dungeon.npc_spots[npc_id] as Transform3D))

_seat_body does the same lookup for one body. seat_all runs for a dungeon and each of its floors when it’s built at a world marker.

NpcFates.seat moves the live node if there is one and the body if there is one. If the saved state has an entry for that NPC, it also rewrites the position in it. That’s why a save made after Carl dies puts his body back on his square in the site and not where his own file says. Subject 29, once eaten, has no node and no body, so nothing moves and he’s still gone after a load.

Keeping one Facility Key and one door for another key

The floors work gave the dungeon a site key: a key that isn’t on any map and opens every key-locked door that no key drawn on the maps names. The story work had its own stand-in key for Carl to give. I deleted the stand-in, and Carl’s dialog gives the site key.

I’d said I wanted a few doors to need keys from bodies around the level. One is built. A door with a key_item is skipped when the site key’s list of locks is worked out:

if refined is Dictionary and str(refined.get("key_item", "")) != "":
    continue
if door != "" and not named.has(door) and not all:
    locks.append(door)

The importer warns if that door has no key lock and reports an error if the key doesn’t exist. Subject 29’s cell is that door. The Facility Key opens 8 doors on floor 1 and 1 on floor 2, and his cell is the tenth key door. Which other doors get their own keys, and whose bodies they’re on, I haven’t decided.

The Ward Key names its door by id, and the content validator checks ids against a registry built from the running level. The real world scene doesn’t build this dungeon yet, so the registry now also lists the doors and chests of a placed dungeon straight from its plan, marked "built": false.

I’d also drawn two security doors side by side across one hall by accident. One is off the map now, and the other is 6 m wide.

How hard should the doors be to pick?

I wanted the pick difficulty set slightly under what a brand new player could have if they put everything they could into lockpicking. I had an agent work that number out from the game’s rules, and the answer was weird: it’s rank 1, whatever you do at creation.

A class gives Pick Locks at rank 1 at most. The one extra skill a character picks is learned at rank 1. Everybody starts with 0 juice, and no stat, item or organ adds to a rank. Juice can only be spent in a safe house, and the site doesn’t have one. Carl’s 60 juice ampoule would buy rank 3, and there’s nowhere in the site to use it.

The default party also has rank 1, because its Shank’s default extra skill is Pick Locks.

The picking rule is that a rank at or over the difficulty always opens the lock. A rank over half the difficulty gets 1 try in 2. Anything lower gets 1 in 10, with a 1 in 10 chance of a jam. So at difficulty 1, any party with a picker opens every key door and never needs a key. At 2, every rank 1 picker is down to 1 in 10. No number separates a dedicated picker from the default party.

The ten key doors are at 1 for now, and that’s a placeholder. I haven’t decided what changes. There are three options on the list. I could leave it, so keys only matter to a party built with no picker. I could add a class or creation choice that grows a picker past rank 1, move the Shank’s default skill off Pick Locks, and set doors one under the new top rank. Or I could put a safe house in the site, so Carl’s ampoule can go into the skill, and set the doors at 2.

What isn’t done?

The game’s real world scene still doesn’t build the site. All of this runs in the older code-built test world that the tests and the film use.

The two quests still can’t be finished, because they end in a town with nothing in it. A body is still the NPC’s own model laid down and darkened, and the numbers are still placeholders.

The phone designer’s copy of floor 1 still has both security doors, and its list of what the dungeon needs says three things are missing that are built. A tool to refresh that page is being built now.

The story test moved onto the real site and passes 342 checks with 0 failures, run alone. The walk test with the new pick checks passes 100. A run of 31 related tests had 0 failures, but I don’t have a run of the whole suite for this change.

Eight captioned frames from the film in two columns: the locker, Carl talking, a cell door, the orderly’s body in a corridor, Subject 29 talking, Subject 29 eaten, the security door and the lift in motion.
The film's frames in order, each with its caption.