ELSEWHERE THE SOMNOLESCENT MULTIVERSE NAVIGATION GAME PSEUDO RFC STYLE IMPLEMENTATION GUIDE BY MARITEAUX FORWARD COMMENTS TO ME WHEREVER YOU TALK TO ME OR AT MARITEAUX@SOMNOLESCENT.NET I. ABSTRACT =========== > No, the kind of storytelling I'm talking about is less like a conveyor belt > of events trudling by in front of us and more about immersing us in a big, > soapy bath of ideas and settings and feelings and letting us find our own > route to the plughole. In fact, why don't we just call it that? Immersion > storytelling! - Yahtzee Croshaw, Extra Punctuation I've been interested in the concept of a text adventure exploration platform through Web browsers for some time now, but I've never been sure how exactly it should work. My earliest attempt circa 2019 used static HTML documents numbered and put together by hand in a big, linking web, but this was inflexible, annoying to maintain, and limited. I rebuilt it earlier this year with PHP and JSON, and while easier to maintain, pages were still numbered, which made keeping track of where scenes lead unclear. Character interactions have always been forced to be their own sets of scenes, even in the same location, bloating an already large project file. The aim is to codify an efficient system for putting together "immersion storytelling" adventures, where the player is set into a world and can freely navigate between settings, exploring and interacting with characters as they desire. There are very few true game mechanics, which is why I don't call this a game system. Think of it more like a story version of Google Street View. II. AIMS OF THIS SYSTEM ======================= The Elsewhere system should hit these targets: 1. Provide a way to describe scenes with metadata (visuals, location names), exits to other scenes, character interactions, or other websites altogether. Each scene should also feature text for return visits, so as to create a more dynamic environment that can avoid continuity errors. 2. Multiple worlds need to be accessible, but separately written. Elsewhere is meant to navigate the Greater Somnolescent Multiverse, and each world in the Multiverse being separate makes them easier to maintain and add onto and requires less data to be loaded and read through at any given time. 3. Provide a way to structure character interactions that can be linked to from scenes, but operate independently of them, ideally allowing characters to bring the player to new scenes and continue their interaction there. 4. Characters should only need to be interacted with once, so the system should also remove them the environment after the interaction is completed. 5. Be simple to add content to. Everything should be named, not numbered. These names should appear to the player (and writer) wherever possible to make connecting Web addresses and in-game locations obvious. 6. Be easy to interact with. Paragraphs of text and links to scene exits in a list format should suffice. 7. Keep track of the player's progress through the world (most recent scene visited, characters talked to) in some fashion. III. DATA FORMATS AND STRUCTURES ================================ Elsewhere is written in PHP. PHP handles the parsing of the scene and character files, formatting into a page, and writing cookies to the player's browser to save progress. This should accommodate as much or as little as the writer wants to include in the scene and character files for maximum flexibility in building the story; in other words, expect only the true basics of a scene, the description of the scene and one exit, and then handle anything else included, like additional exits, attached images, or perhaps developer commentary, as necessary. Character and scene data are written in JSON files. JSON is ideal for being structured around objects with data attached to them. Each scene and each character can be its own object with any amount of additional metadata and objects attached to it, keeping the scene file organized. Here's a real world example of the benefits of JSON for this purpose if you're unsure what that means. Let's say you have an intersection with three buildings. The first building is a car lot. A player, presented with this scenario, could reasonably be expected to want to go inside the car lot, or they might want to go to the sandwich shop next door, or maybe the place that fixes aquariums and sells fish next to those. Those scenes can have their own scenes underneath them for, say, the dealership attached to the lot, or the bathroom of the sandwich place. In my previous attempts, each of these would be their own flat numbered scenes, say 1 for the intersection, 2 for the lot, 3 for the sandwich shop, and 4 for the fish place. Confusing for me to implement, and unclear on a Web address level what leads where. JSON lets me embed the smaller and more granular scenes inside the larger scenes, where they're organized and easy for a JSON parser to traverse. That JSON might look something like: { "cherry-avenue": { "description": "You stand at the intersection of Cherry Avenue and King's Road. It's annoyingly busy, and people drive like maniacs. As the very slow lights turn red, you're able to cross and see three buildings around you, one of which is a used car lot and dealership, the other two an oddly-located sandwich restaurant and an even stranger aquarium maintenance and fish shop.", "scenes": { "walts-auto": { "description": "A lot of a couple dozen cars, all seemingly Kias, surrounds you as you step onto the lot. A futuristic, blocky glass building emblazoned with \"Walt's Auto Sales and Repairs\" is straight ahead through the lot." }, "joeys-subs": { "description": "An upstart sandwich shop with a sign for \"Joey's Subs\" out front sits slightly further down the road. The walls are painted up a funky blue, and past a few small sets of tables and chairs, three guys in gloves and hairnets are playing with the toppings with tongs, preparing subs or perhaps just looking busy." }, "serene-aquariums": { "description": "Frankly, you wouldn't have even expected this place to be open from the condition on the outside of it, but indeed, Serene Aquariums is open for business. The place looks like a converted laundromat, but it's hard to argue with the mood lighting and large tanks of guppies, bettas, snails, and shrimp floating in tanks much better kept than the facade of the building itself." } } } } Subscenes can be nested inside the intersection's scenes, as many as needed. These will show up in the address bar of the browser as nested directories, ie cherry-avenue/serene-aquariums/. The rest of this section will explain the exact structure of the above in greater detail. For clarity, no optional fields will appear in the examples. There are full scenes and character interactions with all optional fields present in section IV, "Sample Scenes and Characters". i. FRONTEND =========== The normal interaction loop of Elsewhere is as follows: 1. The player's browser sends a request for a scene or a character interaction. Should no specific scene be requested, the first scene will be sent at the start of the adventure, or if a cookie is present with a last-visited scene specified, Elsewhere sends back that scene so the player can pick up where they left off. 2a. If the request is for a scene name, which happens over HTTP GET (as scenes are always accessible and thus the request should be repeatable), Elsewhere navigates the specific world's JSON file for the named scene until it finds it. A missing scene will be treated as an invalid request. 2b. If the request is for a character interaction ID, which happens over HTTP POST (as character interactions per-save are ephemeral), Elsewhere checks if that character is present in the seen character cookie data. If seen, the request is invalid. 2c. If not present in the cookie data, the lookup should be in the character data file instead, where the interaction ID matched with the requested scene name will be used. This enables character interactions over multiple locations, or multiple steps of the conversation to happen in one location. 2d. If the scene the character interaction takes place in is different than the current scene, HTTP 307 Temporary Redirect is used to update the URL for the player in their browser while repeating the POST lookup on that new page. 2e. If the request is invalid, default data specifying the error should be sent with suggestions for what the player can do next. Missing scenes should feature a link to the start of the adventure. Characters who have already been interacted with should have a special scene notifying the player of their absence, letting them continue from the specified scene. 3. If the request is successful, Elsewhere parses out the description, additional metadata for the scene, and exits, and formats it onto the page. In the case of a character interaction exit, If a character has been encountered according to the seen character cookie data, their exit will be not be displayed. From here, the player can choose any of the valid exits to continue. 4a. On a successful scene load, the cookie data gets updated for the player's current and previous scenes. The former is so the player can pick up where they left off next session, and the latter is so differing text and exits can be presented to the player should they return to that previous scene. 4b. If a character interaction has concluded leading back to a scene, that character should be added to the seen character cookie data so they can't be encountered again. ii. SCENE DATA FORMAT ===================== A scene is a discrete location that the player can visit through a URL link or exit from another scene. These are JSON objects that, at minimum, require a name, a scene description, and a single exit. They can additionally have multiple exits, subscenes, images attached, and other such metadata. { "scene-name": { "description": "", "exits": [ { "type": "", "destination": "", "text": "" } ] } } The description string is the set of paragraphs that describe to the player their surroundings. The location string (not pictured) is a label for the current scene that will be displayed on the page to the player. Both of these can feature HTML formatting for visual interest. A similar field to description, return_description, will be used on the second and subsequent visits instead. 1. EXITS ======== An exit is simply a link to another location. This location can be another scene, a character interaction, or a Web location, where the player's browser will be taken to a different site or part of the site entirely. The exits array is structured like so: { "exits": [ { "type": "", "destination": "" "text": "" } ] } type can be either "scene" or "character" (where a scene path is expected), or "external" (where a Web address is expected). If "character" is specified, the character to be interacted with should be specified in a character string. The location regardless of type is specified in the destination string. The text string is for the text displayed to the player for where that exit leads. Like scene descriptions and locations, exit text can have HTML embedded into it. A similar array, return_exits, will be used when the player visits that scene a second time. This is optional, and if it doesn't exist will simply reuse the normal exits array. An important distinction to make is that subscenes are not necessarily considered exits to that scene. It might seem obvious to treat them as such in implementation, but there's a few good reasons why this isn't the case: 1. The writer might not want you to access every subscene immediately, say, a librarian who gives you access to a back room through correctly navigating a conversation. 2. Not all exits are subscenes. Character interactions count as exits. Web addresses to other destinations count as exits. A teleporter may jump you into another world entirely still within Elsewhere. Without the distinction, you would have exits specified in the subscene list and exits to other destinations specified in their own object, which would be confusing to implement. Subscenes are purely for organizing where scenes are located in the world for ease of programmatic access. Exits specify where scenes can go, even if that means specifying each subscene as an exit by hand. 2. SCENE PATHS ============== Scene paths are the way locations in Elsewhere are loaded. These are structured like paths on a website, either absolute (with a leading slash, to start at the beginning of the scene tree) or relative (missing a leading slash, treating the current working scene as the start of the tree). These also directly correspond with the Web address where that scene can be accessed through the browser, as if it were a page on a site. A dot for a path means to stay at the current scene, useful for character interactions. In our example of buildings from earlier, accessing the car lot with a relative path would be done like so: { "exits": [ { "type": "scene", "destination": "walts-auto" "text": "Head onto the car lot." } ] } With an absolute path: { "exits": [ { "type": "scene", "destination": "/springfield/cherry-avenue/walts-auto/" "text": "Head onto the car lot." } ] } If loading in a scene from an absolute path, the first "directory" specifies which world the scene belongs to, and thus which world JSON file will be loaded and parsed. This absolute path loads in springfield.json, for example. From there, after splitting the requested path at each slash, the PHP will find the desired scene ID, and should that not be the end of the path, check its subscenes for further scenes. If at any point a scene with that ID can't be found, an error is thrown instead and default data is loaded. When creating links to further scenes through exits, absolute paths start at the top URL of the adventure script (say, cammy.somnolescent.net/elsewhere/) and then get appended to that. Relative links simply get appended to the current URL. iii. CHARACTER DATA FORMAT ========================== Character interactions are similar in structure to scenes, but they're separated out for two major reasons: 1. Under previous systems, each step of a long character interaction would have to be implemented as its own scene. While simple, it resulted in a lot of unclear bloat for which scenes actually took the player to new locations and which ones were simply them being chatty with an NPC. 2. Since there was no distinction between characters and scenes, characters could be encountered and interacted with multiple times unless a new path through an entirely new set of scenes were written to avoid them, again bloating the scene file and creating more work. The other two options, write an entirely new set of dialogue and options for a second conversation or simply have the same conversation play again, are obviously undesirable. In Elsewhere, character interactions are considered ephemeral and largely meaningless, really there to give the player more information or comic relief and not to affect the world noticeably. Characters disappear after their dialogue tree ends and cannot be interacted with again on the same save, which is an acceptable tradeoff for simplicity's sake. If needs be, another character with a new ID (ie "sebastian" and "sebastian2") can be made. This is all possible because Elsewhere uses two different HTTP methods for accessing scene and character data, GET and POST. GET is the traditional way of accessing pages, accessible through a URL. Elsewhere uses these requests for scenes, as scenes are always accessible. Interactions, though, are not always accessible, and browser implementations of POST (normally used for submitting data to a server) take that into account. You can't complete a POST request through a URL, and the same request can't happen twice with the same results. This makes character interactions difficult to stumble into and ephemeral, exactly what we need. While POST requests don't normally redirect (as opposed to GET requests, which nearly always request a brand new page), setting a Redirect header lets us switch scenes during a character interaction, allowing NPCs to bring the player to new locations and potentially unlocking entirely new, previously inaccessible scenes. Once the interaction ends, the player will be left with normal scene data (and actually technically return scene data, as the player will have been there prior). { "character-name": { "interaction-id": { "valid_scenes": [ "" ], "body": "", "exits": [ { "type": "", "destination": "", "text": "" } ] } } } The two big differences between a character interaction and a scene is that interactions are organized by interaction IDs underneath a character object (so one ID per step of the conversation), and each interaction is sanity checked to make sure they're being requested on the correct scene as specified in the valid_scenes array. That way, a character can't teleport the player around the world, and conversations occurring across locations have a bit of extra safety in case the writer messes something up. iv. SAVE DATA FORMAT ==================== Save data is written into HTTP cookies. Visited scenes and characters spoken to are stored in a comma-separated array each. These are read at the start of each server interaction to determine if the return description and exits should be used for a scene, and if an exit for a character already seen should be visible to the player. IV. SAMPLE SCENE AND CHARACTER ============================== Below are a fully decked out example scene and character interaction to demonstrate all optional fields. i. SAMPLE SCENE =============== { "library-foyer": { "image": "https://cammy.somnolescent,net/elsewhere/images/pennyverse/library.png", "location": "Apricot Bay Public Library", "description": "
You enter into a cozy brick building with a large bay window out front. It's the Apricot Bay Public library, and though its wooden floors are worn and its red striped stucco walls distinctly dated, it's got something of a pull to it. You're not sure if it's calming or intriguing, but you almost want to spend some time exploring it. Given the two levels and three sections (you're in the foyer area, with the front desk to your right, and the other two seem to be for children's books and for media, respectively), there certainly plenty to see.
", "return_description": "You return to the foyer of the library. Other animal patrons of various colors, builds, fuzziness, and demeanors occasionally cross your path. There's rickety wooden stairs to your left leading up to the library's study areas and ancient textbook collection, out forwards is the similarly woefully outdated media section, and there's a beige box PC sitting under a tiny, boxy CRT on the librarian's desk. In fairness to Apricot Bay, though, they are a small coastal town, and you get the odd feeling you've actually been transported back in time anyway.
", "scenes": { "library-upstairs": { "description": "Your feet meet carpet of some nebulous thickness and softness as you crest the top of the stairs. These are where the study rooms of the library are located. Ironically, despite the many wooden tables, you see no one study, only a weirdly large hyena creature in a maroon jacket having a nap in one of the bean bag chairs on the other side of the room. I guess if there's any function a library has, it's for some peace and quiet.
", "exits": [ { "type": "scene", "destination": "library-study", "text": "Good point. Enter in one of the study rooms for a quick breather." }, { "type": "scene", "destination": "library-foyer", "text": "Return downstairs." } ] }, "library-media": { "description": "Most of the media section wraps around a small outcropping of tables. Only one person is studying at them, an absolutely tiny fennec fox girl in a cyan skirt and blouse whose hardcover book is so comically large, she can hide behind it. You think it best not to disturb her. Otherwise, the media selection seems, well, antiquated. When was the last time you'd even seen and 8-track tape? Microfiche? 8mm film cannisters? What decade did you warp to?
Well, neat to see that stuff, anyway.
", "exits": [ { "type": "scene", "destination": "library-foyer", "text": "Head back to the foyer of the library." } ] } }, "exits": [ { "type": "scene", "destination": "library-upstairs", "text": "Ascend the stairs towards the upstairs." }, { "type": "scene", "destination": "library-media", "text": "Head back in the direction of the media section." }, { "type": "character", "character": "theophrastus", "interaction_id": "1", "destination": ".", "text": "Approach the front desk. Is anyone here?" } ], "return_exits": [ { "type": "scene", "destination": "library-upstairs", "text": "Head upstairs." }, { "type": "scene", "destination": "library-media", "text": "Have a look in the media section." }, { "type": "character", "character": "theophrastus", "interaction_id": "1", "destination": ".", "text": "Go talk to the front desk, assuming someone shows up there at some point." } ] } } ii. SAMPLE CHARACTER ==================== { "theophrastus": { "1": { "valid_scenes": [ "library-foyer" ], "body": "As you approach the front desk, from a small supply room behind it appears an old, hunched-back brown rabbit with fur in similar condition to his spine, his green vest matched with a lighter green dress shirt underneath.
Yes, hello! he says in a strange accent you can't place—sing-songy, sort of Irish-y but really not. Whatever it is, it's new on you, and he croaks it too. Haven't kept you waiting long, have I?
", "exits": [ { "type": "character", "character": "theophrastus", "interaction_id": "2", "destination": ".", "text": "\"Not at all. I'm just passing through the library, looking around.\"" }, { "type": "character", "character": "theophrastus", "interaction_id": "3", "destination": ".", "text": "\"Only...a half hour, maybe?\"" }, { "type": "scene", "destination": "/pennyverse/library-square/", "text": "What is he, a wizard? I'm outta here." } ] } } }