# Dino Haptics

> Chrome's offline dinosaur game, rebuilt and wired to a haptic device over Intiface / the Buttplug protocol. Each jump, near miss and crash fires its own pulse, shaped as an envelope with a peak, a duration and an attack, so overlapping events sum instead of cutting each other off. One HTML file — no build step, no dependencies, and no network call but the local websocket.

## Two things ship here
1. **`index.html`** — the game. One file, no build, no dependencies. This is the product; everything below about constraints applies to it.
2. **`bridge/`** — optional. Drives the **real `chrome://dino`** instead of this rebuild. Python, needs a Chrome launched with a debug port. Ignore it unless the user explicitly asks for the real game.

## Read this first
`AGENTS.md` is the procedure: the three pieces, the mappings table, and how to retune it well. This file is the index; that file is the job.

## Run it
```bash
python3 -m http.server 8099   # then open localhost:8099
```
It plays with **no device attached**, so you can develop, screenshot and test the whole thing without hardware. `jump()` and `setChromeMode(true)` are callable from the console — that is how you drive it headlessly.

## The one thing to understand before editing
The feel comes from the **envelope**, not the events. `engine.fire(peak, durationMs)` pushes a pulse with an attack of `min(60, dur * 0.25)` ms; overlapping pulses take the max rather than cutting each other off, and `engine.level()` is what reaches the device. Change durations before you change powers.

## The design surface
`MAPPINGS` — an array near the bottom of `index.html`. Each entry becomes a live UI control; `power` is a percentage.

| id | fires on | default |
| --- | --- | --- |
| `jump` | every jump | on, 45%, 220 ms |
| `land` | touchdown | off, 25%, 120 ms |
| `nearmiss` | a cactus cleared | off, 35%, 180 ms |
| `milestone` | every 100 points | on, 65%, 500 ms |
| `crash` | game over | on, 85%, 900 ms |

To add an event: append to `MAPPINGS`, then call `engine.fire(cfg.<id>.power / 100, cfg.<id>.dur)` where it happens in the game loop. The UI builds itself from the array.

## Connecting a device
Intiface Central must be running with its server started (`ws://localhost:12345`) and the toy paired there. Then use the page's **Device** panel. Two constraints that will otherwise waste your time: Intiface accepts **one client at a time**, and it **drops all devices the moment its last client disconnects**.

## Controls
`Space` / `↑` / tap — jump · `C` — Chrome mode (assembles the real ERR_INTERNET_DISCONNECTED page around the canvas) · `F` — fullscreen · `Esc` — exit / panic stop.

## Constraints
- No analytics, CDN, font host or any other network call. The point is that it needs no internet; breaking that breaks the joke and the privacy claim at once.
- Keep it one file.
- Never commit anything identifying a device or a user.

## Safety
Output is capped by the **Max cap** slider, `Esc` is a panic stop, and output zeroes on game over, tab hide and page close. Keep those behaviours if you refactor.


## bridge/ — driving the real chrome://dino (optional)
Chrome forbids extensions and content scripts on `chrome://` pages, so the real game cannot be hooked the usual way. The bridge attaches over the **DevTools Protocol** instead, patches `Trex.prototype.startJump` and `Runner.prototype.gameOver` inside the page, and receives their calls through `Runtime.addBinding`. Real game events, with live speed and score. No keyboard hook, no Accessibility permission.

```bash
cd bridge && ./play.sh        # checks Intiface, launches a debug Chrome, starts the bridge
```

Two facts about the page internals that cost real debugging time:
- `Trex` and `Runner` are **lexical globals**, not properties of `window`. `window.Trex` is `undefined` while `typeof Trex === "function"`.
- It is **`Runner.getInstance()`**, not `Runner.instance_`, as of Chrome 153.

This is pinned to Chrome internals and will break when they change. If `install: hooked:` comes back empty, re-introspect with `Object.getOwnPropertyNames(Trex.prototype)` rather than guessing.

`--remote-debugging-port` lets any local process drive that Chrome instance. It binds localhost only and `play.sh` uses a throwaway profile; close it when done.
