Phone
The phone (Game design document §7.5, §5.5): the satire's main channel and how missions talk to the player. Contacts text and call, apps are screens of short text with buttons, the map app drops a waypoint, and the world's terminals, keypads and people open the same panel. It rests on the items and is written in the mission language.
The phone is an item; its entries are the missions'
- The item:
phoneis a common item (packages/sim/src/items.ts), taggedphone. Every character starts with one in the inventory's last slot (so whatever is picked up goes in the first seven) when the room runs a game whoserules.phoneis true, the default; free roam, with no missions, has none (Tthere takes out a phone with only the radio on it: Sound and the radio). It is never dropped nor given; a mission maytakethe tagphoneandgiveit back. - The entries: its contacts, messages (with their replies), installed apps, call log and waypoint are entries of the phone, not items in a container. They are data the mission interpreter keeps per player (
packages/missions/src/phone.ts, part of the runtime's state, so its determinism covers them) and sends to that player alone. A party's mission (Social) sends its messages, calls, contacts and apps to every member's phone; each member's own phone takes what they ask of it. An item in a slot is a thing a crate or a trunk can hold; a message is not, so the inventory stays a list of things and the phone a list of entries. - Without the item the entries stay but nothing opens:
Tshows nothing, a call is missed. What the world opens (a terminal's screen, a keypad, a conversation face to face) does not need the phone. - Whose: contacts, dialogues and apps are a mission's (its
contacts,dialogues,apps). A conversation, an installed app and an open screen belong to the mission run that opened them and go when it ends; contacts, messages, the call log and the waypoint stay for the room (a reply to a message of a run that ended is not offered any more).
Using it
| Key | Does |
|---|---|
T | takes the phone out, or puts it away (an app's screen closes with it) |
Esc | back a page (an app's screen closes); from home, puts it away |
1–9 | press the page's numbered buttons, instead of selecting a slot, while the phone is out |
| mouse | clicks the buttons and the map, never a shot; over the panel it does not aim; a click on the game still aims and fires |
| everything else | still the game's: W/A/S/D, Space, Shift, E. The world does not pause; the car keeps rolling under the phone |
The panel is part of the game's picture, drawn in the scene with Babylon's GUI (apps/client/src/gui/phone-blocks.ts builds it from the logic in ui/phone-view.ts; the client UI explains the layer), on the right above the minimap; a page taller than the room left scrolls inside it with the wheel. Home lists Messages (with the unread count), Contacts, Calls, Map, Camera, Jobs (in the city: below) and the installed apps. The messages, the contacts and the calls are listed newest first: the panel stands on its bottom edge, so what arrives goes on top and the button under the pointer stays put; a click is a press and a release on the same button, wherever the panel has it, so a message arriving mid-click never loses it. The buttons never take the keyboard: the keys stay the game's; an app form's fields take it while typed in (a click on the field; digits only; Enter presses the screen's first button with what the fields hold, Esc gives the keys back to the game). A conversation shows over any page. What the world opens, and a call ringing, take the phone out by themselves; when they end, a phone the world took out is put away again.
Messages
message { from, text, choices? } puts a message from one of the mission's contacts in the phone (a notice on screen too, the unread count on the Messages tile). Up to three replies, each { id, text, if?, do?, failText? }: picked, the reply's if is checked; when it holds the reply is sent (shown as "You: …") and its do runs, once; when it does not, failText answers under the message and nothing is sent. The older sendMessage { from, text } (a name, no contact) lands in the phone too, without replies. The Messages page marks them read; a message with an id (message { id, from, text }, sendMessage { id, from, text }) is heard by the mission's rules once read (message:<id>.read), or replied to.
Calls and conversations
A dialogue is nodes of short lines and choices, from its first node:
{
"id": "onboarding_call",
"nodes": [
{
"id": "terms",
"line": "Do you accept the Terms, the Terms of the Terms, and the vibes?",
"choices": [
{
"id": "accept",
"text": "I accept.",
"do": [{ "setFlag": "accepted" }],
"next": "rating"
},
{ "id": "read", "text": "Can I read them first?", "next": "pages" }
]
},
{ "id": "pages", "line": "They're 900 pages and a podcast.", "next": "rating" },
{ "id": "rating", "line": "Your starting rating is 4.1. Nobody knows why." }
]
}- A node's
speakeris one of the mission's contacts (the one the player talks with when absent). Its choices each check theirifwhen picked (failTextanswers and the line stays), run theirdo, and go tonextor end the conversation; a node without choices goes on to itsnext(the panel's "…"), or ends it ("Bye"). Every node must be reachable from the first. - Calls in:
call { from, dialogue }rings the phone for 20 s: answer or decline. Without a phone, or with a conversation already on, the call is missed. - Calls out: a contact added with
addContact { contact, dialogue }answers with that dialogue while the mission runs; atalkobjective naming acontactmakes that contact answer with its dialogue; anyone else does not answer. - Face to face: an
npcactor of thecontactrole stands at a place; the interaction key says Talk within 1.5 m and runs its dialogue in the same panel (atalkobjective naming the actor plays its own). Walking 4 m away or knocking them down ends it. - Events:
dialogue:<id>.choice:<choice>,dialogue:<id>.ended(reached an end) anddialogue:<id>.declined(declined, rung out, hung up, walked away from). Thetalkobjective is done once the dialogue has ended in the mission (or, withchoice, once that choice was picked). The call log keeps answered, made, missed, declined and unanswered calls. - The help line: a mission's
helpline(one of its contacts) is in the phone from the mission's start and always answers: a call that no dialogue of theirs answers plays one line, thehintof the objective under way (else what the tracker says), so a player who is stuck calls them ("Stuck? T phone, call Dana" on the tracker; see the Missions guide).
Apps
An app is screens of up to 400 characters, each with a short form (up to three fields: a code or a number) and up to four buttons { id, text, if?, do?, go?, failText? }. A button checks its if when pressed, with the form's fields (formIs { field, value }, the value { form }): when it holds, its do runs, rules hear app:<app>.<button>, and go moves to another screen; when it does not, failText (else "Nothing happens.") shows on the screen. The game only checks; "hacking" is choices, timers and items, never code. A button can be pressed again, so one that gives items needs a counter in its if.
RentaCar, the example (packages/missions/content/test-phone.json): the rental car stands locked; Unlock works within 10 m of a car tagged rental and hands over that car's key, an item tagged key:vehicle:rental_car, which is what opens it (E Unlock at its door; see the Vehicles guide):
{
"id": "unlock",
"text": "Unlock",
"if": {
"all": [{ "near": { "tag": "rental", "within": 10 } }, { "lt": [{ "counter": "keys" }, 1] }]
},
"do": [{ "give": { "item": "rental_key" } }, { "add": { "counter": "keys", "value": 1 } }],
"go": "unlocked",
"failText": "No RentaCar within 10 m. The app can't walk for you."
}installApp puts one of the mission's apps on the phone (it goes when the mission ends), removeApp takes it off, hasApp tells whether it is there.
Jobs
In the city (a hub's room, never a run's) the phone has a Jobs app (GAME_DESIGN §13: finding play without leaving the street), the client's own: its data comes from the platform through the page (apps/client/src/renderer/jobs.ts asks GET /playlists, GET /missions?level= and GET /lobbies/mine when its page is shown and every 5 s while it stays; ui/jobs-view.ts makes the page's lists), never from the game server. Its page, top to bottom:
- Invitations: the lobbies friends invited the player to (
phone-job-invite-<lobby>); - Lobbies needing players: the public lobbies of the playlists still gathering with a place left, on any level (
phone-job-lobby-<lobby>: its mission, level and players); - Missions: those of the level the player is on, then the other levels' multiplayer missions (Any's rotation), each with how it is played (
phone-job-mission-<game>-<mission>); - Quick play: Co-op, Versus, Races, Anything (
phone-job-quick-<playlist>).
Picking a lobby or an invitation opens the lobby's page (?lobby=); Quick play goes to the lobby it placed the player in, or straight into the run under way it seated them in; a solo mission opens its run at once; a multiplayer one its fullest public lobby gathering, or a new one. The panel stands on its bottom edge, so the lists that change are on top and what arrives goes at the top of its list (onTop: what was shown keeps its order), Quick play and Back at the bottom: an invitation or a lobby that comes moves nothing under the pointer; and on this page the digit keys count from the bottom (keysOf: 1 its last button, Back, 2 the one above it), so a key pressed as an entry arrives on top still picks what it showed; each list shows a few at most. A page whose platform does not answer (a server played on its own, the local development's) says that jobs need the platform (phone-jobs-state); a choice the platform refused says why (phone-jobs-error). window.__poc().phone.jobs lists what the page shows (null in a run's room).
Terminals and keypads
- A terminal actor (
kind:terminal,keypad,atm,vending; all work alike) is a post at a place. The interaction key says Use (or Hack, itsprompt) within 1.2 m and opens one of the mission's apps on its first screen (orscreen) in the phone's panel, from the world; no phone is needed. Walking 4 m away closes it.usenames it; rules hearterminal:<id>.used. - A keypad door is a door actor with a
code(3 to 8 digits): locked as the mission starts, the key says Enter code and opens a keypad; a wrong code says so, the right one unlocks the door and swings it open away from the player. Rules heardoor:<id>.unlocked(as for a door unlocked by its key or anunlock), and theunlockobjective is done.
The map and the waypoint
The Map page draws the level north up; a click drops the waypoint there, Clear removes it. A mission sets one with waypoint (a place) and clears it with clearWaypoint. The minimap shows it as a cyan diamond with an arrow on its edge pointing the way from the player; it clears itself once the player is within 8 m.
The camera
Every phone has a Camera (its tile on home, no mission installs it). Its page says how many photos it took and what the last one got; its shutter is its first button (1), and while the page is out the minimap draws its frame, a cyan cone along the character's aim. A photo is taken along the aim (move the mouse; over the panel it does not aim, so the aim holds while a button is clicked): the server frames it (@foi/ai's inFrame: within 60 m, about 23° either way of the aim, nothing of the level or the world in between but the subject) and the missions read what was in it, the actors and the game's places. A photograph objective is done by a photo of its subject taken from its zone; the camera says so ("Got it."), or that the subject was not in the frame, or that the photo was not taken from the spot.
On the wire and on the server
- The server sends each player a
phonetext frame (@foi/protocol'sPhoneMessage) when their phone changed; the client sendsphonecommands (open, close, press, reply, answer, decline, hang up, choose, call, read, waypoint, clear waypoint, photo), inputs the interpreter checks like any other: the phone held, the app installed, the button on the screen open, the reply still open. The interaction key at a contact, a terminal or a keypad is aninteractevent of the simulation the mission host turns into the same kind of input. - Snapshots carry the terminals (a box the client twins, so the prompt and the predicted character see it), a character's role (a contact, drawn cyan) and a keypad's lock (amber);
PROTOCOL_VERSION6. apps/server/src/mission-host.tsspawns the people (a character of the contact role, standing) and the terminals, binds the keypad doors, queues the phone commands (16 a player between two steps, 8 taken a step) and sends the phones. Texts of contacts, messages and replies, dialogues and apps are moderated at publishing like the game's other texts (apps/api/src/moderation.ts).
Mini level and tests
test-phone(built fromapps/level-editor/assets/kits/test-phone.recipe.json): an office with one door north of the spawn, open ground to Gary from Facilities (north-west), the Smart Kiosk (north-east) and the RentaCar bay (20 m west). Its game, Day One: Mo texts with two replies, "I'm human." brings GigLoop Onboarding's call whose choices set a flag, Gary texts the keypad's code and talks in person, the RentaCar app unlocks the car within 10 m, the kiosk sets a waypoint. E2E-140 to E2E-149 (tests/e2e/specs/levels/test-phone/).test-mission-verbs: the camera's photo of a mural from its spot (E2E-374), a message read heard by a mission (E2E-371).tests/e2e/specs/platform/continuous.spec.ts: the Jobs app lists a stranger's public lobby on another level, opens it, and the invitation sent from it reaches a friend's Jobs app on top (E2E-682).- Unit tests:
packages/missions/src/runtime-phone.test.ts,validate-phone.test.ts,runtime-verbs.test.ts(the camera's notes, a message read);packages/sim/src/interactables.test.ts;packages/net-client/src/mirror-items.test.ts;apps/server/src/phone.test.ts;apps/client/src/ui/phone-view.test.ts,ui/jobs-view.test.ts,renderer/jobs.test.ts,apps/client/src/gui/blocks.test.ts(the Jobs page's buttons where they were when an entry arrives).
What comes next
The fake web (pages of marked-up text with links, the satire's best channel), conversations voiced by an AI ([LATER]), roles beyond contact (guards, shopkeepers), and terminals placed by the level pipeline rather than by a mission.