From 15aa87664ecd2d01555e7d3c92cab67e993b912d Mon Sep 17 00:00:00 2001 From: prompt.ac/@jeffrey Date: Sat, 09 May 2026 03:27:06 +0000 Subject: [PATCH] arena: 3P solid-mesh self + post-commit auto-deploy on main 🧍 Local player now renders as a solid humanoid mesh whenever zoom- level > 0 (3rd-person), built from the same `buildRemoteBody` as remote players so what other tabs see of you matches what you see of yourself. 1P keeps the bodyFeet / bodyArms wireframe POV helpers since that's what you'd actually see from your own eyes. 🤖 Post-commit hook now auto-deploys arena on commits to main when any of `disks/arena.mjs`, `lib/{arena-world,pmove,cam-doll}.mjs`, `session-server/{arena-manager,session}.mjs`, or `shared/` change. Pipeline (all background): 1. push HEAD to knot (origin) so lith pulls the right commit 2. fish lith/deploy.fish — knot pull + service restart 3. wait 75s for the lith systemd knot→github mirror 4. fish session-server/deploy.fish — VPS pull from github + node restart Why both: lib/pmove.mjs is shared physics. Lith ships the client copy, session-server runs the authoritative copy. If they fall out of sync the reconciler in arena-manager.mjs will fight client prediction and movement glitches. Yesterday's arena commit deployed lith but not session-server, which is exactly what produced the "player keeps snapping back" feel. Logs land in `.git/arena-auto-deploy.log`. `AC_NO_AUTO_DEPLOY=1` in the env skips the deploys (e.g. while bisecting). SCORE.md has the matching entry under Backend so future agents pick up the contract. Co-Authored-By: Claude Opus 4.7 --- .githooks/post-commit | 44 ++++++++++++++++++++++++++++++++++++++++++++ SCORE.md | 11 ++++++++++- system/public/aesthetic.computer/disks/arena.mjs | 26 +++++++++++++++++++++----- 3 file(s) changed, 75 insertion(s)(+), 6 deletion(s)(-) diff --git a/.githooks/post-commit b/.githooks/post-commit --- a/.githooks/post-commit +++ b/.githooks/post-commit @@ -35,4 +35,48 @@ echo "🔮 AT frontend changed — deploying to PDS..." bash "$REPO_ROOT/at/scripts/deploy-at-frontend.sh" 2>&1 | tail -3 & fi +# 🏟️ Auto-deploy arena: lith (client) + session-server (multiplayer authority) +# when any arena-touching file changes on main. Set AC_NO_AUTO_DEPLOY=1 in the +# env to skip (e.g. while bisecting / hotfixing without going live). +# +# Pipeline: +# 1. push HEAD to knot (origin) so lith's pull matches local +# 2. fire lith/deploy.fish — pulls knot, restarts service +# 3. wait for the knot→github mirror (lith's 60s systemd timer) +# 4. fire session-server/deploy.fish — VPS git pull + node restart +# +# Both deploys run async so they don't block the commit. Logs land in +# .git/arena-auto-deploy.log for postmortems. +CURRENT_BRANCH="$(git -C "$REPO_ROOT" symbolic-ref --short HEAD 2>/dev/null || true)" +if [ "${AC_NO_AUTO_DEPLOY:-}" != "1" ] && [ "$CURRENT_BRANCH" = "main" ]; then + ARENA_TOUCH_RE='^(system/public/aesthetic.computer/disks/arena\.mjs|system/public/aesthetic.computer/lib/(arena-world|pmove|cam-doll)\.mjs|session-server/(arena-manager|session)\.mjs|shared/.*)$' + if git diff-tree --no-commit-id --name-only -r HEAD | grep -qE "$ARENA_TOUCH_RE"; then + echo "🏟️ arena change on main — auto-deploying lith + session-server in background…" + DEPLOY_LOG="$REPO_ROOT/.git/arena-auto-deploy.log" + ( + printf "\n=== %s commit=%s ===\n" "$(date -Iseconds)" "$(git -C "$REPO_ROOT" rev-parse --short HEAD)" >> "$DEPLOY_LOG" + # 1. Push to knot if local is ahead (idempotent if already pushed). + LOCAL_HEAD=$(git -C "$REPO_ROOT" rev-parse HEAD) + REMOTE_HEAD=$(git -C "$REPO_ROOT" rev-parse origin/main 2>/dev/null || echo none) + if [ "$LOCAL_HEAD" != "$REMOTE_HEAD" ]; then + echo "→ pushing $LOCAL_HEAD to origin/main" >> "$DEPLOY_LOG" + git -C "$REPO_ROOT" push origin main >> "$DEPLOY_LOG" 2>&1 || \ + echo "⚠ push to knot failed — skipping deploys" >> "$DEPLOY_LOG" + fi + # 2. lith (pulls from knot directly). + echo "→ fish lith/deploy.fish" >> "$DEPLOY_LOG" + fish "$REPO_ROOT/lith/deploy.fish" >> "$DEPLOY_LOG" 2>&1 || \ + echo "⚠ lith deploy failed" >> "$DEPLOY_LOG" + # 3. give knot→github mirror time to sync (lith systemd timer = 60s). + sleep 75 + # 4. session-server (pulls from github via VPS). + echo "→ fish session-server/deploy.fish" >> "$DEPLOY_LOG" + fish "$REPO_ROOT/session-server/deploy.fish" >> "$DEPLOY_LOG" 2>&1 || \ + echo "⚠ session-server deploy failed" >> "$DEPLOY_LOG" + echo "→ done" >> "$DEPLOY_LOG" + ) & + disown 2>/dev/null || true + fi +fi + exit 0 diff --git a/SCORE.md b/SCORE.md --- a/SCORE.md +++ b/SCORE.md @@ -57,10 +57,19 @@ - `disks/*.mjs` — Individual pieces (programs) - `lib/*.mjs` — Shared libraries and utilities **Backend** -- `session-server/` — Real-time multiplayer (Socket.io) +- `session-server/` — Real-time multiplayer (Socket.io + geckos.io UDP). Hosted on its own DigitalOcean VPS; deployed via `fish session-server/deploy.fish` (sshes to the box, `git pull` from github main, restart node `session.mjs`). Houses `arena-manager.mjs` which is the authoritative pmove + snapshot pipeline for `disks/arena.mjs`. - `lith/` — Production monolith deploy (Express + Caddy on a DigitalOcean VPS, pulled from the tangled knot `git@knot.aesthetic.computer:aesthetic.computer/core` via `lith/deploy.fish`). Express adapts the handlers in `system/netlify/functions/` as routes — the `netlify/functions/` path is historical; Netlify is no longer the host. +- `lith/mirror/` — knot↔github bidirectional mirror (systemd timer, every 60s). Lets us push to knot only while still letting downstream consumers (session-server VPS, mirrors) pull from github. - `help/bridge/` — Local Express bridge on @jeffrey's macbook that spawns the host `claude` CLI and streams it as SSE; reached publicly through `help.aesthetic.computer` via the existing droplet proxy + autossh reverse tunnel. Powers the `aa` piece (admin-only phone-side chat with the macbook's claude). Auto-runs under launchd as `computer.aesthetic.aa-bridge` and `computer.aesthetic.aa-tunnel`. - Authentication and data storage + +**Arena auto-deploy (post-commit hook)** — Any commit on `main` that touches `disks/arena.mjs`, `lib/{arena-world,pmove,cam-doll}.mjs`, `session-server/{arena-manager,session}.mjs`, or `shared/` triggers a paired deploy in the background: +1. push HEAD to knot (`origin`) so lith pulls the right commit +2. `fish lith/deploy.fish` — pulls knot, restarts the monolith +3. wait ~75s for the knot→github mirror +4. `fish session-server/deploy.fish` — VPS pull from github + node restart + +This is critical because `lib/pmove.mjs` is shared physics: client (lith) and server (session-server) MUST match, otherwise `arena-manager.mjs`'s reconciler will fight the client's prediction and movement glitches. Logs land in `.git/arena-auto-deploy.log`. Set `AC_NO_AUTO_DEPLOY=1` to skip (e.g. while bisecting). **Languages** - `kidlisp/` — KidLisp dialect (Lisp for generative art) diff --git a/system/public/aesthetic.computer/disks/arena.mjs b/system/public/aesthetic.computer/disks/arena.mjs --- a/system/public/aesthetic.computer/disks/arena.mjs +++ b/system/public/aesthetic.computer/disks/arena.mjs @@ -653,8 +653,9 @@ let shadowGround; // ring + cross — standing on the ground let shadowAir; // diagonal X — airborne let shadowCrouch; // dense inner dot + outer ring — crouched let plumbLine; // vertical line from ground to the player's feet -let bodyFeet; // two foot wireframes, anchored to ground + yaw -let bodyArms; // two arm wireframes, anchored to eye + yaw +let bodyFeet; // two foot wireframes, anchored to ground + yaw (1P only) +let bodyArms; // two arm wireframes, anchored to eye + yaw (1P only) +let myBody; // solid humanoid mesh (3P only — same shape as remotes) let platformEdge; // bright outline at the ground's perimeter let obstacleForms = []; // solid pillar + wall meshes (one Form per shape) let obstacleEdges = []; // bright top-rim line forms paired with obstacleForms @@ -2220,6 +2221,12 @@ }, { pos: [0, 0, 0], rot: [0, 0, 0], scale: 1 }, ); bodyArms.noFade = true; + + // 🧍 Solid humanoid for 3rd-person. Same builder as remote players so the + // local avatar matches what other tabs see. Only painted when zoomLevel > 0 + // (1P uses the bodyFeet/bodyArms wireframe helpers instead). + const myColor = isGuestHandle(myHandle) ? specColor(myHandle) : handleColor(myHandle); + myBody = buildRemoteBody(Form, myColor, isGuestHandle(myHandle)); } function sim({ system, pen, screen }) { @@ -2439,6 +2446,12 @@ bodyArms.position[1] = playerWorldY - crouchDrop + bob; bodyArms.position[2] = playerCamZ; bodyArms.rotation[1] = playerFacing; } + if (myBody) { + myBody.position[0] = playerCamX; + myBody.position[1] = playerWorldY; + myBody.position[2] = playerCamZ; + myBody.rotation[1] = playerFacing; + } // 🐛 Once-per-second debug dump. Paste this back into chat to diagnose. debugDumpTimer += 1; @@ -2606,9 +2619,12 @@ if (activeShadow && onSolidGround) ink(255, 255, 255).form(activeShadow); if (plumbLine && onSolidGround && plumbLine.scale[1] > 0.05) { ink(255, 255, 255).form(plumbLine); } - // Feet + arms render regardless of ground state (they fall with you). - // Dropped entirely in LOW perf mode — wireframes are nice-to-have. - if (!perfLowMode) { + // 1P: thin wireframes for hands/feet (your own POV references). + // 3P: solid humanoid mesh — same builder as remote players, so what + // other tabs see is what you see of yourself. + if (zoomLevel > 0 && myBody) { + ink(255).form(myBody); + } else if (!perfLowMode) { if (bodyFeet) ink(255).form(bodyFeet); if (bodyArms) ink(255).form(bodyArms); } -- tangled.sh