---
id: world-map-and-portals/014
title: One body everywhere — entity ids move from the world to the account, and the on-screen keyboard finally runs at all
epic: world-map-and-portals
state: done
priority: 1
blocked_by: []
estimate: M
created: 2026-08-16
updated: 2026-08-18
claimed_by: null
claimed_at: null
delivers: []
review_artifact: land-of-lor/game/shell/verify-touch-keyboard.mjs
---

## 1. Entity ids belong to the account

Kris, reading 013's write-up: *"entity ids absolutely should not be per-world, obviously they
should be per account."* Right — and it is the root cause one level below 013. A stale body
could only exist because the same player wore a **different id in every Area**.

`accounts.entity_id` is minted once, at account creation, and travels to the world process
with the join grant beside `display_name` and `character_seed`. The world uses it verbatim and
only mints one when it wasn't given one (a standalone or load-test run with no director).

- **Migration is one UPDATE on boot** for accounts predating the column, `gen_random_uuid()`
  per row so 80 accounts can't collide with each other. 23 backfilled on production; 26
  accounts, 26 distinct ids, none missing.
- **There is no entity data to convert.** A body already standing in a world under an older
  per-world id is **renamed on first contact** — same cell, same everything — and any other
  body for that account is dropped. A player keeps the spot they left off.
- **Not the account id itself**, deliberately: every peer in an Area sees every other body's
  `entityId` in `world_update`, and an account id is not theirs to see. It stays the same
  short opaque token it always was.

Two portal tests asserted *"a fresh entity gets a new, different random id"* — that assertion
**was** the per-world assumption. They now assert the id is the same, and that what proves the
entity left is its **position**: a fresh body on the spawn cell rather than the transferred one
where it walked to.

## 2. The on-screen keyboard had never worked, anywhere

Kris asked for a way to enter a name in Chrome on an iPad. There already was one, shipped that
morning and marked *"not verified on a real iPad"*. It had never worked on any device in any
browser. **Three** separate faults, each fatal on its own, and none of them findable by
reading:

1. **The feature was never on.** The availability probe tested `probe is bool`, and
   `JavaScriptBridge` hands a JS boolean back as an **int** (`probe=1`, `TYPE_INT`).
   `available()` was false everywhere, so `attach()` returned immediately and no field was ever
   hooked up. This is why no amount of reasoning about iOS gesture policy explained the iPad:
   nothing was running to be refused.
2. **`focus()` was always a frame late.** WebKit — every browser on iPadOS, Chrome included —
   opens the keyboard only for a `focus()` inside the gesture's own DOM handler. The old design
   focused from Godot's `focus_entered`, on the *stated assumption* that the signal fires inside
   the tap handler. It does not: Godot buffers input and flushes it at the top of the next
   frame. The engine cannot win this, so it stopped trying — it publishes where its fields are
   (as fractions of the viewport) and the browser hit-tests `touchstart` itself.
3. **The engine took the focus back.** Godot's web export calls `canvas.focus()` from its own
   touch listener, and focuses a hidden IME div whenever a `LineEdit` takes GUI focus — either
   one blurs the input and shuts the keyboard a frame after it opened. `preventDefault` does not
   stop propagation, so a tap landing on a field is now swallowed outright, gesture and all.

The consequence is the shape of the whole thing: **the DOM input IS the input**, and the
LineEdit mirrors it through one callback registered once. Nothing gives the field GUI focus.

**`shell/verify-touch-keyboard.mjs`** is the new check, and the reason this one is trustworthy:
it drives the real exported build in headless Chrome over CDP with touch emulation, taps
through to the signup field, and asserts the input is focused **inside** the touch event, that
the bar survives a second (the canvas-steals-focus failure), and that typing reaches the engine.
**Every one of the three faults was caught by it, not by reading the code.** 8/8.

What it still cannot check is WebKit's own policy — no iPad here — but nothing in the design
now depends on a browser being lenient about it, which is exactly what the old one did.

## Verified

Director 155 tests, Godot 116, `verify-touch-keyboard` 8/8, and on production after deploying:
`verify-home-door` 10/10, `verify-accounts-e2e` 32/32. Shipped as **APK v6** and a web
re-export at `db6a49b`.

Housekeeping: the `DoorProbe` and `MapProbe` characters were cleared from the Village (the
recurring live-verifier hazard first noted in 010) — which also cleared Kris's own bodies in
the Village and The Badlands, so he respawns at the entry cell there. Home, account and
collection untouched.