# 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 endpoint** — `GET /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: ```bash 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: ```bash 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 ```bash 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 ```bash 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 ```bash 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 ```bash npm start ``` You should see `Newbury Nights listening on http://127.0.0.1:33033` (or whatever `PORT` you set). Test it locally: ```bash 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`): ```bash 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) ```