jessikitty 25832dd58f Keep WebP animating on iOS by attaching the source img to the DOM
iOS pauses animation on a detached <img>, so the CanvasTexture sampled a
frozen frame. Attach the img hidden off-screen (opacity 0.01) so WebKit
keeps advancing the WebP; remove it on clear.
2026-06-22 09:53:36 +10:00
2026-06-17 11:43:18 +10:00
2026-06-17 11:43:06 +10:00
2026-06-17 22:23:39 +10:00

Newbury Nights

A fan-made AR ghost-hunting web app — a tribute to the AR mechanics of the LEGO® Hidden Side™ app. The focus is the Hunter Mode AR loop: raise your phone as a ghost detector, scan the colour wheel to uncover gloom, lock on, and blast ghosts down before your battery dies.

Fan-made tribute. Not affiliated with, sponsored by, or endorsed by the LEGO Group. LEGO® and Hidden Side™ are trademarks of the LEGO Group.

What's inside

  • Backend — Node + Express 5, SQLite via better-sqlite3, bcrypt password hashing, JWT auth (8h), multer image uploads.
  • Frontend — Three.js (ES module import maps via CDN), getUserMedia camera passthrough with DeviceOrientation gyro look (iOS Safari friendly), BarcodeDetector QR scanning with a manual code fallback.
  • Ghost rendering — animated-GIF billboards (texture pumped via texture.needsUpdate each frame) when a ghost has an uploaded image; procedural Three.js wisp meshes otherwise.
  • Spawning — rarity-weighted: ★ common down to ★★★★ legendary.
  • Admin panel (/admin) — JWT-gated. Upload / enable / disable / delete ghost images, create sets with scan codes linked to ghost rosters (many-to-many via set_ghosts).
  • Public endpointGET /api/scan/:code requires no auth and returns a set's ghost roster.

Data

The ghost roster, stats, abilities, and boss→set references are seeded from data/*.json, which are generated from the source spreadsheet by scripts/extract_ghosts.py. Three ghost types (red / yellow / blue) and four rarity tiers drive the colour-wheel and damage mechanics.

To regenerate the JSON from a spreadsheet:

python3 scripts/extract_ghosts.py path/to/Ghost_Data.xlsx data

Step-by-step setup

This walks through a fresh deploy on the Ubuntu server, behind the existing nginx (which already terminates HTTPS).

1. Prerequisites

Node 18 or newer, plus the build toolchain so better-sqlite3 can compile its native module:

node --version                      # expect v18+; install via nvm or nodesource if missing
sudo apt-get update
sudo apt-get install -y build-essential python3

build-essential is required — without it npm install fails while compiling better-sqlite3.

2. Get the code and install dependencies

git clone https://gitea.hideawaygaming.com.au/jessikitty/newbury-nights.git
cd newbury-nights
npm install

npm install compiles the native SQLite module, so it may take a minute on first run.

3. Configure environment

cp .env.example .env

.env is loaded automatically at startup via dotenv, so npm start and npm run seed read it directly — no process manager or manual export needed. Edit .env and set, at minimum:

  • JWT_SECRET — a long random string (e.g. openssl rand -hex 32). This signs login tokens; treat it like a password.
  • ADMIN_USER / ADMIN_PASS — the bootstrap admin login created on first seed. You'll change the password from the admin panel right after.
  • PORT — defaults to 33033; change if that port is taken. Keep it in sync with the nginx upstream (step 6).

4. Seed the database

npm run seed

This creates db/newbury.sqlite, loads the 111 ghosts / 36 abilities / 17 sets from data/*.json, and creates the admin user. Expected output ends with something like Seed complete: { ghosts: 111, abilities: 36, sets: 17, rosterLinks: 153 }.

Re-running npm run seed is safe — it only seeds ghost/set data when the ghosts table is empty, so it never clobbers admin edits. It always ensures an admin user exists.

5. Start the app

npm start

You should see Newbury Nights listening on http://127.0.0.1:33033 (or whatever PORT you set). Test it locally:

curl http://127.0.0.1:33033/healthz          # -> {"ok":true}
curl http://127.0.0.1:33033/api/scan/NN-70419 # -> Wrecked Shrimp Boat roster + Captain Archibald

For production, run it under a process manager so it restarts on reboot/crash — e.g. pm2 start server.js --name newbury-nights, or a systemd unit. dotenv still loads .env under either.

6. Put nginx in front (HTTPS)

Camera and gyro APIs only work in a secure context, so the site must be served over HTTPS. Copy the example server block and adjust server_name and certificate paths (the example upstream already points at 127.0.0.1:33033 — match it to your PORT):

sudo cp deploy/nginx.conf.example /etc/nginx/sites-available/newbury-nights
# edit server_name + ssl_certificate paths to match your domain/certs
sudo ln -s /etc/nginx/sites-available/newbury-nights /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

The app runs plain HTTP on 127.0.0.1:PORT; nginx terminates TLS and forwards to it. app.set('trust proxy', 1) is already set so secure cookies work behind the proxy.

7. First login and lock-down

  1. Open https://your-domain/admin.
  2. Sign in with the ADMIN_USER / ADMIN_PASS from your .env.
  3. Go to the Account tab and change the password immediately.
  4. Under Ghosts, upload billboard GIFs and enable/disable ghosts. Under Sets, edit scan codes and rosters.

8. Play

Open https://your-domain/ on a phone. Use Scan a Set (camera QR or type a code like NN-70419), Free Hunt for a quick random session, or Ghost Index to browse the roster. Allow camera and motion access when prompted.

nginx

See deploy/nginx.conf.example for the full reverse-proxy block referenced in step 6.

API quick reference

Public:

  • GET /api/scan/:code — set roster + boss for a scan code
  • GET /api/freehunt?n=&type= — rarity-weighted random spawns
  • GET /api/ghosts?type=&rarity=&boss= — public ghost index (enabled only)
  • GET /api/abilities — ability reference

Auth:

  • POST /auth/login · POST /auth/logout · GET /auth/me · POST /auth/change-password

Admin (JWT required):

  • GET/POST /api/admin/ghosts, PATCH/DELETE /api/admin/ghosts/:id, POST /api/admin/ghosts/:id/image
  • GET/POST /api/admin/sets, PATCH/DELETE /api/admin/sets/:id, PUT /api/admin/sets/:id/roster

Project layout

server.js              Express app + static hosting
db/index.js            SQLite schema + connection
routes/                auth, public api, admin api, auth middleware
scripts/extract_ghosts.py   xlsx -> data/*.json
scripts/seed.js        seed DB from data/*.json + bootstrap admin
data/                  ghosts.json, abilities.json, sets.json
public/                index.html (game), admin.html, css/, js/
deploy/                nginx.conf.example
uploads/               uploaded ghost billboards (gitignored)
S
Description
Fan-made AR ghost-hunting web app — tribute to LEGO Hidden Side AR mechanics. Node/Express + SQLite + Three.js. Not affiliated with the LEGO Group.
Readme 286 KiB
Languages
JavaScript 58.7%
CSS 19.4%
HTML 15.5%
Python 6.4%