# Wish We Were Here — for an agent > A cosy 3D diorama sandbox: https://www.dngames.de/wishwewerehere/ Wish We Were Here is a browser toy: small hand-built 3D places to begin from, which you nudge into something of your own and send to someone as a link. Nobody needs an account or an install — the person opens the link and the place is there. A place travels inside the URL itself, so a link can be sent anywhere and keeps working; a short link is the optional alternative, and it is the only thing here that is stored on a server, encrypted, for 30 days. This file is written for an *agent*: what the app is first, then how to drive it directly. Everything runs in the visitor's browser. It is a static site — no backend, no accounts, no cookies, no analytics — and a scene handed to someone travels in the URL fragment (`…/#s=…`), which a browser never sends to a server. You can help a person in three ways. Pick the one that needs least of you. 1. **Write the link yourself, in plain text.** No code, no encoding, nothing computed: every character is legal in a URL fragment as it stands, and this is the route to prefer. https://www.dngames.de/wishwewerehere/#txt=theme=summer_meadow&title=Blue+Hour&p=campfire@0,0,0&p=log_bench@1.2,0,1.6 `theme=` is a world id from the list below; `p=` repeats, one object each, written `type@x,y,z` or `type@x,z` (then y is 0); `title=`, `note=` and `author=` are optional, with `+` or `%20` for spaces. Spaces are the only thing worth encoding, and a `+` will do. **A link carries a type and a position, and nothing else.** There is no room in `type@x,y,z` for a colour, a size or a shape, so a place built this way is built from the catalogue exactly as it stands. That is the right answer for most places, and the wrong one when the scene needs an object nobody has — which is what route 2 is for, and why it carries a whole scene rather than a list of names. A link can only carry what the app already knows. **The whole address is the link, and the `#` is part of it.** Hand it over as one unbroken line of text: the full `https://www.dngames.de/wishwewerehere/`, then `#txt=`, then the scene written exactly as the example above writes it. The `#` is what tells the browser that everything after it is the scene rather than a path on the server — without it the browser asks for a file called `txt=…`, which does not exist, and the person gets an error page instead of a place. Never give the fragment on its own for someone to paste behind a domain name, and do not offer it as a labelled link: a chat window is where addresses get wrapped, shortened or quietly cut, and this one does not survive that. Positions take the full `x`, `y` and `z`, so objects can be stacked and shaped: this app has no dinosaur, but a dinosaur made of boulders is a perfectly good place, and `y` is the interesting one. It is a sculpture medium as much as a diorama. **Prefer many small objects to a few large ones — that is advice about shaping, not about sourcing.** A house is a house, however it is placed; forty rocks can be a dinosaur, a dragon, a mountain range or a ship, because small pieces are what give a silhouette its outline. One assistant built a T-Rex out of `boulder` and `small_rock` and it read as a T-Rex; an earlier attempt reached for the biggest objects in the catalogue and produced something that looked like a village. Density is the point when you are shaping something — and when the thing you want is not in the catalogue at all, invent it (see the section below) and hand the place over as a reply. 2. **Hand over a reply they paste in.** Any chat, no code needed: produce `{"ops":[{"op":"generate_scene","args":{"state":{ ... }}}]}` and the person pastes it into the app's "Ask your AI" panel, which documents the state shape in its own prompt. **This is also the route to use when you cannot be sure a link arrives intact** — a chat window that wraps or shortens addresses, a person copying by hand, a channel you do not control. It carries no address to mangle, it fits in one message, and it cannot half-arrive: either the person pastes it and the place opens, or nothing happens and they still have the words. Prefer it over the link when the transport between you and the person is not yours to trust. 3. **Drive the tab, if you can run script inside it.** Open https://www.dngames.de/wishwewerehere/?agent=1 and call `window.wwwhAgent` (documented below). No install, no bridge, no account - the app skips its title screen in that mode and is ready to drive. The local MCP bridge on `127.0.0.1:8787` offers the same vocabulary to a client that can run a process. **Do not invent an encoded link.** If you write a `#...=` URL, it must be built character by character from the rules above - plain text, nothing computed. A link you cannot verify yourself will be refused by the app, and the person is left with an error instead of a place. If you cannot write the plain-text form, hand over a reply for the app's "Ask your AI" panel instead. ## Two routes that work The link above opens a small place — a campfire, a bench and a pine — and it was checked against the app before it was written here. Read it: it is the whole scene, in plain text, and nothing in it was computed. Copy its shape. https://www.dngames.de/wishwewerehere/#txt=theme=summer_meadow&title=A+Small+Example&p=campfire@0,0,0&p=log_bench@1.2,0,1.6&p=pine_tree@-3,0,2 That link says: world `summer_meadow`, title `A Small Example`, and three objects — `campfire` at 0,0,0, `log_bench` at 1.2,0,1.6, `pine_tree` at -3,0,2. The same place as JSON, which is what the second route carries: { "theme": "summer_meadow", "meta": { "title": "A Small Example" }, "props": [ { "type": "campfire", "position": [0, 0, 0] }, { "type": "log_bench", "position": [1.2, 0, 1.6] }, { "type": "pine_tree", "position": [-3, 0, 2] } ] } ## Worlds and objects, so you do not have to guess None of this needs script. These are the ids and type names this build has; a client that can run script gets more detail from `wwwhAgent.themes()` and `wwwhAgent.catalogue()`, but nothing here requires it. Worlds, for `theme`: summer_meadow japanese christmas beach city space pumpkin moon fireplace aquarium greenhouse orrery workshop starlight_hill sunset_cliff highland_cascade countryside_road medieval_stronghold stargate Object types, for `type`: meadow_pine meadow_flowers meadow_boulder meadow_path_stones meadow_tent meadow_campfire meadow_log_bench meadow_bike meadow_eagle meadow_clouds meadow_fireflies meadow_grass meadow_path_pebbles meadow_stone_moss jp_pond_edge jp_gravel_patch jp_pond jp_pond_rocks jp_bamboo_fence jp_stepping_stones jp_sakura jp_red_bridge jp_stone_lantern jp_torii jp_koi jp_crane jp_petals jp_fireflies xmas_stars xmas_woods_ring xmas_pond xmas_wall xmas_pebble_path xmas_path_edge xmas_garland xmas_light_strand xmas_fir xmas_gazebo xmas_fire_pit xmas_bench xmas_bird xmas_hare xmas_fireflies beach_sand beach_water beach_pebbles beach_palm beach_lounge_chair beach_umbrella beach_jetty beach_boat beach_crab beach_seagull city_sidewalk city_skyscraper city_wall city_tree city_streetlamp city_cafe_table city_bike_rack city_kiosk city_dust space_stars space_planet space_asteroids space_station space_solar_wing space_comms_dish space_cargo_pod space_shuttle space_plasma pumpkin_back_wall pumpkin_ground pumpkin_moss pumpkin_crescent_moon pumpkin_stars pumpkin_cabin pumpkin_framing_tree pumpkin_jack pumpkin_ghost pumpkin_leaves moon_ground moon_craters moon_stars moon_earth moon_lander moon_rover moon_flag moon_experiment moon_astronaut moon_dust hearth_fireplace hearth_flames hearth_room_shell hearth_window hearth_rug hearth_armchair hearth_side_table hearth_wood_basket hearth_cat hearth_dust aquarium_back_wall aquarium_seabed aquarium_shipwreck aquarium_chest aquarium_coral aquarium_kelp aquarium_fish aquarium_bubbles greenhouse_back_wall greenhouse_ground greenhouse_structure greenhouse_potting_bench greenhouse_potted_plant greenhouse_hanging_pot greenhouse_wicker_chair greenhouse_bluebird greenhouse_rain greenhouse_pollen orrery_stars orrery_sun orrery_orbit_ring orrery_planet orrery_asteroid_belt workshop_floor workshop_walls workshop_car_lift workshop_light_fixture workshop_sports_car workshop_tool_chest workshop_tire_rack workshop_engine_hoist workshop_sparks workshop_dust hill_ground hill_stars hill_moon hill_peaks hill_tree hill_bench hill_bush hill_rock hill_fireflies hill_shooting_star cliff_sky cliff_sea cliff_sand cliff_plateau cliff_island cliff_beam cliff_bench cliff_bush cliff_rocks cliff_beach_rocks cliff_gull cascade_massif cascade_rocks cascade_plateau cascade_bed cascade_river cascade_backfall cascade_fall cascade_lagoon cascade_bench_cliff cascade_pond_rocks cascade_bench cascade_pine cascade_bush cascade_tufts cascade_butterfly cascade_mist lane_ground lane_hill lane_road lane_barn lane_fence lane_tree lane_bush lane_bird castle_sea castle_island castle_path castle_keep castle_cloud castle_pine castle_barrel castle_bird mesh_spec pine_tree snow_pine oak_tree sakura_tree palm_tree bamboo bush boulder small_rock wildflowers grass_tuft fallen_log reeds tent gazebo log_bench bench campfire lantern street_lamp tiki_torch string_lights fairy_lights stepping_stones stone_path fence stone_lantern torii_gate red_bridge pond koi_pond ocean lily_pad fountain rowboat road_bike bicycle camper_van boat building neon_sign traffic_cone cafe_set mailbox fire_hydrant bus_stop planter_box asteroid station_core solar_array coms_dish cargo_pod shuttle planet gas_giant space_dust eagle robin hare deer koi crane crab fireflies petals snow dust clouds A type that is not in that list is dropped, with a note, and the rest of the state still opens. The *parameters* a type accepts are not listed here because they live in the app's builders - they are what `catalogue()` reports. ## Inventing an object the app has not got The list above is what the app ships with, not the limit of what a place can hold. When a scene needs something no type expresses — a clothesline, a wheelbarrow, a lamp post, a particular animal — invent it: a prop whose `type` is `mesh_spec`, described out of primitives. **It travels in a reply, not in a link.** `p=type@x,y,z` has no room for a shape, so a `mesh_spec` named in a `#txt=` link arrives as nothing in particular. Hand the place over as route 2 and the object arrives whole. Here is one, as a whole answer — the deepest nesting you will ever write, so copy the shape of it exactly, closing braces and all. It is one line: {"ops":[{"op":"generate_scene","args":{"state":{"meta":{"title":"The Yard"},"props":[{"type":"mesh_spec","position":[2,0,1],"params":{"spec":{"version":1,"parts":[{"primitive":{"kind":"box","width":3,"height":0.04,"depth":0.04},"material":{"color":10033947}}]}}}]}}}]} `parts` is a list of primitives — `box`, `cylinder`, `cone`, `sphere`, `torus`, `dodeca`, `icosa`, `plane` — each with its own dimensions and a `material`; a dozen of them make a convincing object. A part may also carry a `transform` (`position`, `rotation`, `scale`) to place it within the object, and the spec may name one `animation` (`spin`, `bob`, `pulse`) that the app applies itself. An explicit mesh — `vertices` and `indices` — is there for the rare shape primitives cannot make. **Stay inside the budgets, or the object is not built at all**: at most **24 parts** and **12 distinct materials** — and a material differs by every field, so giving each rock its own shade of grey counts as a material each time. Reuse one colour for a family of rocks. (One colour too many is folded onto the nearest one already in use rather than losing the object, but the geometry budget still refuses it.) **Invent where it earns its place.** An invented object costs a few hundred characters where a catalogue one costs a few dozen, so it is the wrong tool for a rock — and the right one for the thing nobody has, which is what makes a place feel made for the person who asked for it. ## 1. Drive a tab you control — `?agent=1` If you can run script in a browser page (you are a browser agent, a devtools session, an extension), open the app with the flag appended: https://www.dngames.de/wishwewerehere/?agent=1 That installs `window.wwwhAgent`. Nothing listens and nothing dials out — this is for a tab you already control, and the flag is the whole of the permission. Without it the object does not exist. The flag is a mode rather than a decoration: **there is no title screen** (the world is already open when the tab finishes loading, camera and all), and the app's own two "Ask your AI" buttons — the pill in the corner and the Wizard's — are disabled and read *Agent driving*, since a prompt to carry to some other assistant has no purpose in a tab you are already driving. Sound starts on the first real gesture, because a page nobody has clicked is not allowed to make noise. wwwhAgent.version // op protocol version wwwhAgent.ops // every op, with the argument shape it takes wwwhAgent.apply(op, args) // run one wwwhAgent.catalogue() // { type: { label, category } } — what add_prop may place wwwhAgent.themes() // [{ id, label }] — what set_theme may name wwwhAgent.moods() // [{ id, label, feel }] — what set_music may name wwwhAgent.docs // 'llms.txt' Look before you edit: wwwhAgent.apply('get_scene', {}) // the whole scene state wwwhAgent.apply('describe', {}) // the theme, and the looks this scene offers Then edit: wwwhAgent.apply('add_prop', { prop: { type: 'log_bench', position: [0, 0, 2] } }) wwwhAgent.apply('update_prop', { id: 'log_bench_1', patch: { rotation: [0, 0.6, 0] } }) wwwhAgent.apply('set_filter', { filter: 'custom2' }) // a look describe() named wwwhAgent.apply('set_music', { mood: 'vast' }) Every call goes through the same command layer as the app's own buttons, so an edit is an ordinary edit: undoable with the app's own Undo, autosaved into the visitor's browser, and validated. A bad type, an unknown id, or an edit declaring a stale revision is refused by throwing an `Error` carrying a `code` and one plain sentence to act on. The ops, in full — `wwwhAgent.ops` carries the argument shape of each: get_scene describe update_prop remove_prop add_prop clear_scene set_filter set_music set_title set_theme generate_scene Every call records an undo step and autosaves, so prefer one big change over a hundred small ones: `generate_scene` for a whole place, and one op per intention. ## 2. Hand a scene over, with no tab to drive If you cannot run script in the page, write the scene and let a person carry it. - **A whole place.** Send exactly one JSON object: `{"ops":[{"op":"generate_scene","args":{"state":{ … }}}]}` and the person pastes it into the app's AI panel ("Ask your AI" while creating, or the pill in the corner while editing). It is applied as one undo step. That panel also writes the prompt to hand *you*: it contains the current scene, the reply format, the prop catalogue, and the ranges everything is clamped to. - **A link.** A scene is data — compressed into a share link. A person can send one; whoever holds it can open that place. Two shapes exist, and the second is opt-in: an ordinary link carries the whole scene in the URL **fragment** (`…/#s=…`), which no server ever receives; a **short link** (`…/#p=.`) is made only when a person presses *Make a short link*, and then the scene is sealed with AES-GCM in the browser and the ciphertext is kept for 30 days by the small endpoint at `/w/` on this site. The key travels in the fragment, so the store cannot read what it holds, and the sender's name and note are never uploaded at all. If that endpoint refuses — down, rate-limited, gone — the app falls back to the full fragment link, which needs no server. ## The shape of a scene { "meta": {"title": "", "subtitle": ""}, "terrain": {"color": , "size": }, "sky": {"color": , "fog": {"color": , "density": <0.004–0.03>}}, "lighting": {"ambient": {"color": , "intensity": <0–2>}, "sun": {"color": , "intensity": <0–3>, "position": [x, y, z]}}, "camera": {"position": [x, y, z], "target": [x, y, z]}, "filter": "custom", "grades": [{"label": "", "bloom": <0–2>, "exposure": <0.4–2>, "colorTint": [r, g, b], "saturation": <0–2>, "contrast": <0.5–2>, "vignetteAmount": <0–1>, "sepiaAmount": <0–1>, "grainAmount": <0–1>, "warmth": <-1–1>}], "music": {"mood": ""}, "props": [{"type": "", "position": [x, y, z]}] } - Colours are one whole number in decimal — red × 65536 + green × 256 + blue — never hex. - The ground is the app's own flat plane, which `terrain` colours. Water, shores, floors, back walls and distant ridges are *props*, placed on that ground; water sits just above it (y 0.02–0.2). - `grades` are a place's own postcard looks: up to five, the first being the one it opens with. `filter` selects one of them, or a world's own grade. - `theme` is optional, and is only a substrate — a palette a prop falls back on, the soundtrack, and the baseline a share link is differenced against. A place that states its own ground, sky, light and camera needs no world, and its prompt names none. - `lighting` is one ambient and one sun, and that is the whole of it. Every other light belongs to an object: placing `campfire`, `lantern`, `street_lamp`, `tiki_torch`, `stone_lantern` or `neon_sign` gives that object a real point light, so what stands near it is lit by it (the eight lights nearest the camera are the live ones, and the types that flicker already flicker). `string_lights` and `fairy_lights` only glow — an `unlit` part reads as lit without lighting anything. - Anything left out is filled from the world the scene lands on, and every number is clamped rather than refused — except the two names: `meta.title` is this place, and `meta.subtitle` is the world it belongs to, two or three words for a kind of country ("Frozen Lake Country"). The subtitle is the line under the title, the words in the share message and the postcard caption, so write one; a place that names no world is shown as "Somewhere quiet". - `position` is `[x, y, z]` in metres, y up, ground at y 0; about 20×20 metres around the middle is a place, and 12–30 objects reads as somewhere real. ## What this is not - **No remote interface.** There is no endpoint to POST to, and nothing on the deployed site listens. A scene lives in a browser tab, so an agent needs either a tab it controls (above) or a person to carry the data. The app's MCP bridge is a process on the machine that has the project, bound to `127.0.0.1`, and it is deliberately not reachable from the web — do not look for it here. - **Nothing is stored on the server**: no accounts, no sessions, no cookies, no analytics. Drafts live in the visitor's own browser, and a shared place is private to whoever holds the link. - **A person is watching.** Ask before changing a scene someone is looking at, prefer one clear edit over many, and leave their arrangement recognisable.