DR McCullochHOMESHOPABOUT

Conversation engine

Spider
Bubble

A Turkish bath house with Shahmaran, Erlik, Ay Dede and Alara sitting in the steam, built in the ten days between the 20th and the 30th of January 2025.

I wrote the talking twice: first as a Python generator that assembles conversations out of pools of phrases, then as a port of it into C#. On the last day I sat down and wrote the four characters by hand instead, one word underlined in every answer for the player to take away, and their files carry no responses block, which is the one thing the generated half of the loader is gated on. The pools in data.json holda great many distinct three-line conversations and nobody ever asked for one of them, so they are all still sitting in the file unsaid.

5 scenario files2,237 lines of my C#32 underlined words2 of them match nothing

turn it ↓

I Β· the crank

Spin any phrase and watch which line of which array it lands on.

This is generate-interactive.py rebuilt in the browser and pointed at the real pools, which are being fetched as you read. Choose a file along the top:data.json is the Python engine's own, example_scenario.json is the port of it that went into Unity and the four named ones are the characters I wrote by hand and never wired a generator into. Click any phrase to spin that slot on its own, or click the console and use the keys the script prints across the top of the terminal.

The Python engine’s own pools. Four objects, all three rungs and the only file with a top-level speculations array.

script
question_structure

Qgenerate_question(data)β€”

R1generate_response1(data, object_key, direction)β€”
direction

R2generate_response2(data, valid_items, action_type, item_key)β€”

this conversation is one of
β€”
phrases in this file's pools
β€”
bytes your browser fetched
β€”

the roll nothing committed yet

    The two scripts differ in one thing

    generate-random.py picks the third line from any of the eight action items in the file without caring what the second line was about, andgenerate-interactive.py reads left_items orright_items off the outcome you chose and offers only those. Flip the switch and the total moves from β€” to β€”, because the outcomes in data.json never name three of the eight.

    Where the third clause went

    The Python template has three parts and the loader's has two, so switching the template above drops the third clause while you watch and divides the count by exactly fifteen. Nothing in my C# ever reads mystical_questions, though the field is declared at ConversationModels.cs:82 and every one of the six files carries fifteen of them per object.

    return data["question_structure"].replace(
        "[observation]", observation
    ).replace(
        "[cause]", cause
    ).replace(
        "[mystical_question]", mystical_question
    ), object_key

    II Β· the gate

    The loader looks for a colon with nothing after it.

    LoadConversationFromJson finds the parts of the file it needs by searching the raw text for exact strings, and three of the strings it looks for have no space after the colon while every file on disk is pretty-printed with one. Press the probe and watchIndexOf run against the actual bytes of whichever file you pick.

    nothing read yet
    int structureStart = jsonString.IndexOf("\"question_structure\":\"") + 21;
    int structureEnd = jsonString.IndexOf("\"", structureStart);
    if (structureStart >= 21 && structureEnd != -1) { … }
    else jsonData.question_structure = "[observation] Is it because [cause]";
    int start = jsonString.IndexOf($"\"{arrayName}\":[") + arrayName.Length + 4;
    if (start < arrayName.Length + 4 || end == -1) return new string[0];

    The branch that builds generated questions

    if (jsonData.responses.outcomes.items != null && jsonData.responses.outcomes.items.Count > 0)

    Four of the five scenario files have no responses key at all, onlyconversations, objects and actions. The dictionary comes back empty, the count is zero and the branch is stepped over, so what Shahmaran, Erlik, Ay Dede and Alara offer you is exactly the eight questions I typed.

    The rung with no case

    The third rung is written and reachable from nothing. LoadActionOptions at line 316, ExecuteAction at 359 and AddResponseOption at 876 have no call site anywhere under Assets/GGJ2025, and the switch inOnClickConversation carries three cases:

    case "response":   // line 118
    case "simple":     // line 124
    case "question":   // line 129
    default:           // logs "Unknown option type"

    There is no case for "action", and the R2 rung on the console above is running that method's logic anyway, which is the only reason you can read a line of it. The C# and the Python build the same sentence out of the same two arrays:

    string actionText = $"Maybe you should {action} {itemData.name}, {result}";
    return f"Maybe you should {action} {item_data['name']}, {result}"

    One underline where it does not belong

    When the pools were copied into Unity as example_scenario.json, all sixty observations gained a capital letter, because the loader's template drops theTell me, why off the front and the phrase has to start the sentence. Fourteen of the fifteen acknowledgements were raised the same way, and the one left alone already began with an I. One other edit went in: the seventh cause under water_systemarrived carrying the game's own markup.

    data.json Β· causes[6]of air bubbles hosting a party in there?
    example_scenario.json Β· causes[6]of air bubbles hosting a party in there?

    CreateDialogOption runs the inventory regex across every label it builds, so a generated question drawing that cause would try to pick up an object calledhosting. No such item exists among the thirty-three, so the lookup returns nothing and the game writes its warning to the log.

    III Β· the words

    Thirty-two words I could take out of people's mouths.

    This half of it shipped: click the underlined word in any answer and the regex lifts it off the sentence, looks it up by label in the inventory manager and drops it on the shelf as an object. Two of the thirty-two match no item, so they stay where they are and the game writes a warning to the log instead.

    The lookup is on the literal word

    var matches = System.Text.RegularExpressions.Regex.Matches(text, @"<u>(.*?)</u>");
    AC.InvItem itemToAdd = AC.KickStarter.inventoryManager.GetItem(itemName);
    AC.KickStarter.runtimeInventory.Add(itemToAdd.id);

    GetItem matches on item.label == _name, so I filled the manager with the underlined words themselves and thirty-one of its thirty-three entries carry one as a label: perch, crevices, pleassse, silly,the. Two of Alara's, help and water, have no entry, so those lookups return null and log

    Could not find item '{itemName}' in inventory manager

    Two of the thirty-three are both labelled move, at id 6 and id 22; one more is a leftover called Inventory item 1 at id 0. The recipe wants id 6.

    You cannot make a door out of one person

    Neither live recipe can be filled out of one mouth. unlock is Shahmaran's androom is Erlik's, and both of them sit in Outside; move is Ay Dede's and door is Alara's, and both of them sit in Hammam. Each pair fuses to the item labelled key at id 5 and fires an action list that turns the crafting menu off and switches scene.

    1. 0Start
    2. 1Outside
    3. 2Hammam
    4. 3Exit
    5. 4Loading

    Build order out of EditorBuildSettings.asset.Recipe_Door_OnCreate switches to 2 and Recipe_Door2_OnCreate to 3, so the first key takes you into Hammam and the second takes you out toExit. Crafting_OnFailRecipe holds one action, a pause, with nothing after it. The third recipe asks for item ids 1 and 3, which no item in the manager carries.

    IV Β· the frozen roll

    Every question in the inspections is a line out of data.json.

    hammam_mystery.twee sits in the root of the repo, a Harlowe story with the ifid HAMMAM-MYSTERY-2025 and fourteen passages, each carrying the coordinate the editor left it at. Seven repeat the snake's conversation as it sits inexample_scenario.json and four are inspections whose three question lines are single entries in data.json. Walk into one and every line is traced back to the pool it came out of.

    β€”

    int numOptionsToShow = 8; // Always show 3 of each option, regardless of combinations count

    The comment says three and the number says eight; the prefab agrees with the number: it carries eight buttons, btnOption1 and btnOption1 (1) through(7).

    Ten days

    1. 20 Janf4d4bfcInitial commit
    2. 22 Jan8903674wip(chat-engine): initial prototype for simple dynamic story generation
    3. 24 Jan6659e2cfeat(chat-engine): add conversation system and JSON-based dialogue options
    4. 24 Janba16942fix(chat-engine): fixed order of stages
    5. 26 Jan939d852final commit
    6. 26 Jan1999a60build
    7. 28 Jan220a2acfix(everything): it works!
    8. 30 Janfd9a14bexperiment(bubbles): playing with bubbles
    9. 30 Jan34c91aerefactor(ui): adjust conversation UI layout and parameters

    These are nine of the twenty-two commits with the dates and messages verbatim: the generator lands on the 22nd, the port on the 24th and all four hand-written characters arrive together on the 26th in the one called final commit.

    My C#, on top of Adventure Creator 1.82.4

    1. Logic/ConversationFromFile.cs970the loader, the regex, the three rungs
    2. Actions/ActionConversationFromFile.cs380the Adventure Creator action that starts one
    3. Effects/BubbleFloat.cs248drift, wobble, spawn radius, lifetime
    4. Models/ConversationModels.cs243the JSON shapes and two hand-rolled dictionary parsers
    5. UI/DialogOptionHeightAdjuster.cs146squares every option button around its text
    6. UI/VirtualizedConversationUI.cs111pooling for the option buttons
    7. Utils/JsonParser.cs49FindMatchingBrace, which is what the parsers stand on
    8. Systems/ConversationSystem.cs39an interface the loader declares at line 48 and never calls
    9. UI/ButtonToFront.cs31hover raises a bubble above its neighbours
    10. Actions/LeaveConversation.cs20one button, one call, EndConversation
    11. ten files2,237lines, counted with wc

    What is in the build

    Shahmaran, as painted in the build
    ShahmaranCharacters/shahmaran/Adsz_Resim.pngA horned woman with long dark hair in a coral wrap, her blue-scaled serpent tail reared up to face her. The file is called Adsz_Resim, Turkish for untitled picture.
    Erlik, as painted in the build
    ErlikCharacters/elrik/erlik.pngA bull-headed figure in a red checked wrap with a band of small skulls across the brow and a cattle skull on the staff. His folder is spelled elrik, and so is his hotspot.
    Ay Dede, as painted in the build
    Ay DedeCharacters/ay dede/ay_dede.pngA seated moustachioed figure in a gold horned helmet and a red-striped wrap. His file is 274 by 412 where Erlik and Alara are 1000 by 1500 and Shahmaran is 1500 by 1000, so he stays small here too.
    Alara, as painted in the build
    AlaraCharacters/alara/shahmaran.pngA pale winged spirit with glowing spiral markings in a red-orange striped wrap. Her painting sits in the alara folder under the filename shahmaran.png, and its guid turns up only in Hammam.unity.

    The two backgrounds are painted at 2743 by 1351 and both of them are up there behind this page. Nothing in the repo records who painted any of it, so I'm not going to say, and nothing in the repo states the jam theme either. ProjectSettings.assetgives the company as Spider Bubbles, the product as GGJ2025 and the version as 0.1.0, and the Mac build left a folder on my machine naming the app it went out asThe Bath House.app.

    The five scenario files, data.json and hammam_mystery.twee ship with this page byte for byte and are fetched and parsed in your browser, so every phrase, index and count above is the file's own. The item and recipe table is extracted rather than quoted, out of 1,890 lines of Unity YAML, and it says so in its own _note field. Where I could not verify something, it is not here.

    Spider Bubble Β· Global Game Jam 2025 Β· DR McCulloch

    ← back home