[lane F] The deploy stops checking a URL nobody visits, and the frame rate gets a switch

Audit item 14.

THE DEPLOY VERIFIED THE ONE URL A PLAYER NEVER LOADS. Every check in deploy.sh appends
?v=$BUST — which is exactly what makes it bypass Cloudflare's edge — so all six could pass
green while every actual player was still being served the previous build from a POP. The
script's answer was a line of prose at the end asking a human to remember to purge. It now
fetches js/boot.js the way a browser does, with no query string, and byte-compares it to what
was just shipped; a stale edge fails the deploy and prints the purge steps. No credentials
needed, and it asks the question the prose was only gesturing at.

This is the same mistake as the `assets` bug and the unwinnable saliva tide: verifying the
mechanism rather than the path the player actually takes through it. Third time, so it is
written into the tool this time rather than into a comment.

QUALITY SWITCH. renderer.setPixelRatio was hardcoded to min(dpr, 2), so a retina laptop drew
four times the pixels of a 1x one with no way for its owner to decline — and this game is
fill-bound, not geometry-bound (fogged tube, additive glows, a full-screen damage layer), so
that is the lever that moves. `performance` caps it at 1. On the title beside difficulty,
saved, same reload path (the pixel ratio is set at boot, before any card exists).

save.js's two one-line setters collapsed into setOpt(key, value) now that there are two options
and a third is obvious.

Note on verification: the chip persists and boot reads it, confirmed live. The resolution
change itself is not observable in the harness — that browser pane runs at devicePixelRatio 1,
where min(1,1) and min(1,2) are the same number.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-26 22:29:59 +10:00
parent 426bf3f33d
commit bc891527ed
4 changed files with 47 additions and 8 deletions

View File

@ -94,5 +94,25 @@ echo " · $LIVE (no slash) -> HTTP $REDIR (301/302 to /gutsy/ expected)"
[ "$fail" = "0" ] || { echo " ✗ DEPLOY UNVERIFIED"; exit 1; }
echo " ✓ deployed: $LIVE/"
echo
echo "Cloudflare fronts partly.party — purge the cache if you redeploy over existing files."
# ---------------------------------------------------------------------
# THE CHECK ABOVE VERIFIES A URL NOBODY VISITS.
# Every check() appends ?v=$BUST, which is precisely what makes it bypass Cloudflare's edge
# cache — so all six can pass while every actual player is still being served the PREVIOUS
# build from a POP. The script's answer to that was a line of prose at the end telling a human
# to remember. This asks the question properly: fetch a module the way a browser does, with no
# query string, and compare it to the bytes we just shipped.
# ---------------------------------------------------------------------
echo "[5/5] Checking what a PLAYER gets (no cache-buster) ..."
EDGE="$(curl -fsS --max-time 20 "$LIVE/js/boot.js" 2>/dev/null || true)"
if [ -z "$EDGE" ]; then
echo " ⚠ could not fetch $LIVE/js/boot.js un-busted — cannot tell whether the edge is stale"
elif [ "$EDGE" = "$(cat "$ROOT/web/js/boot.js")" ]; then
echo " ✓ edge is serving this build"
else
echo " ✗ STALE EDGE — players are still being served the previous build."
echo " Cloudflare has partly.party/gutsy/js/*.js cached. Purge it:"
echo " Cloudflare dashboard -> partly.party -> Caching -> Configuration -> Purge Everything"
echo " (or Custom Purge on $LIVE/js/). Then re-run this script to confirm."
exit 1
fi

View File

@ -45,7 +45,11 @@ addEventListener('error', (e) => fail('script error', e?.message));
addEventListener('unhandledrejection', (e) => fail('failed to load', e?.reason?.message ?? e?.reason));
const renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
// THE ONE PERF LEVER WORTH HAVING. Device pixel ratio is quadratic in fill cost, so a
// retina laptop was rendering 4x the pixels of a 1x one with no way for its owner to say no.
// Capped at 1 on `low`: everything else in this game is geometry-light and fill-heavy (fogged
// tube, additive glows, a full-screen damage layer), so this is the lever that moves.
renderer.setPixelRatio(Math.min(devicePixelRatio, store.load().quality === 'low' ? 1 : 2));
renderer.setSize(innerWidth, innerHeight);
document.getElementById('game').appendChild(renderer.domElement);
// A laptop sleeping or a display being hotplugged takes the GL context away; without this the

View File

@ -15,7 +15,7 @@ const KEY = 'guts.save';
// beats the one on file.
const RANK = ['none', 'bronze', 'silver', 'gold', 'platinum'];
const EMPTY = () => ({ v: 1, diff: 'normal', cleared: 0, best: {} });
const EMPTY = () => ({ v: 1, diff: 'normal', quality: 'high', cleared: 0, best: {} });
export function load() {
try {
@ -36,7 +36,8 @@ export function save(s) {
return s;
}
export function setDiff(id) { const s = load(); s.diff = id; return save(s); }
/** Set one option. Generic because there are two of them now and there will be three. */
export function setOpt(key, value) { const s = load(); s[key] = value; return save(s); }
/**
* File a finished run.
@ -88,9 +89,11 @@ if (typeof process !== 'undefined' && process.argv?.[1]?.endsWith('save.js') &&
ok(load().best.L1_mouth.medal === 'platinum', 'a better medal replaces');
ok(load().best.L1_mouth.score === 1500, 'a worse score does not');
setDiff('hard');
setOpt('diff', 'hard');
ok(load().diff === 'hard', 'difficulty persists');
ok(load().best.L1_mouth, 'setDiff does not clobber records');
ok(load().best.L1_mouth, 'setOpt does not clobber records');
setOpt('quality', 'low');
ok(load().quality === 'low' && load().diff === 'hard', 'options do not clobber each other');
clearCampaign(); clearCampaign();
ok(load().cleared === 2, 'clears count');

View File

@ -446,11 +446,23 @@ export function createCards({
paintDiff();
dBtn.addEventListener('click', () => {
S.diff = dOrder[(dOrder.indexOf(S.diff) + 1) % dOrder.length];
store.setDiff(S.diff);
store.setOpt('diff', S.diff);
paintDiff();
go({ diff: null }); // drop any ?diff= override so the save is what takes effect
});
// Quality. Same reload path as difficulty for the same reason — the renderer's pixel ratio
// is set once at boot, before anything this card can reach exists.
const qBtn = el('button', 'chip', menu);
qBtn.type = 'button';
qBtn.style.color = C.soft;
qBtn.textContent = `quality · ${S.quality === 'low' ? 'performance' : 'full'}`;
qBtn.title = 'caps the render resolution — the one lever that moves the frame rate on a laptop';
qBtn.addEventListener('click', () => {
store.setOpt('quality', S.quality === 'low' ? 'high' : 'low');
go({});
});
// The one thing a save is FOR: proof you finished. Nothing in this game acknowledged a
// completed campaign anywhere the player could see it.
if (S.cleared > 0) {