const PROBES = [ { id: "feed-freeze", code: "01", title: "Feed Freeze", blurb: "Stop mid-scroll when the real cue hits.", trait: "doomscroll impulse", nextHint: "No compass. Landmarks only.", }, { id: "landmark", code: "02", title: "Landmark Labyrinth", blurb: "Navigate a block with street cues — no arrow.", trait: "GPS dependence", nextHint: "Memorize a scene. Spot the swaps.", }, { id: "scene-swap", code: "03", title: "Scene Swap", blurb: "Glance, then tap every change.", trait: "glance memory", nextHint: "Write past the autocomplete.", }, { id: "anti-complete", code: "04", title: "Anti-Complete", blurb: "Finish the line unlike a predictive model.", trait: "autocomplete gravity", nextHint: "Hold one question through three hops.", }, { id: "deep-link", code: "05", title: "Deep Link Amnesia", blurb: "Survive the rabbit hole. Recall the start.", trait: "context thrash", nextHint: null, }, ]; const SPEED_SECONDS = 300; const RUN_ORDER = PROBES.map((p) => p.id); const FEED_POSTS = [ { handle: "@voidscroll", body: "unskippable lore drop about nothing in particular" }, { handle: "@dopamine.inc", body: "you won’t believe what happens at 0:07" }, { handle: "@groupchat", body: "replying “lol” to keep the thread alive" }, { handle: "@algo", body: "recommended because you paused for 0.4s once" }, { handle: "@muted", body: "a calm photo of a plant. you scroll past it." }, { handle: "@thread", body: "1/47 a nuanced take that will not stay nuanced" }, { handle: "@notify", body: "someone liked your like of a like" }, { handle: "@clip", body: "same joke, third platform, zero context" }, { handle: "@live", body: "going live to ask if anyone is awake" }, { handle: "@ads", body: "shop the exact hoodie from the crashout" }, { handle: "@recap", body: "your year in review arrived in July" }, { handle: "@brain", body: "hot take that agrees with you too hard" }, ]; const BAIT_BODIES = [ "BREAKING: tap now or stay mid", "limited drop — 2 left in your size", "your ex viewed your story (maybe)", "this will ruin your whole week", ]; const LANDMARKS = { "1,1": "blue café", "1,3": "bike rack", "2,2": "old oak", "3,1": "laundromat", "3,3": "neon diner", }; const AC_PROMPTS = [ { stem: "The best way to spend a Sunday is", suggestions: ["relaxing at home", "catching up on sleep", "hanging with friends"], predicted: [ "relaxing", "at", "home", "sleeping", "in", "bed", "watching", "netflix", "with", "friends", "family", "catching", "up", "on", "rest", "doing", "nothing", "chilling", "brunch", "coffee", ], }, { stem: "When my phone dies I usually", suggestions: ["feel anxious", "look for a charger", "borrow someone’s"], predicted: [ "feel", "anxious", "panic", "look", "for", "a", "charger", "plug", "it", "in", "ask", "someone", "borrow", "get", "nervous", "freak", "out", "wait", "until", "home", ], }, { stem: "A perfect group chat message is", suggestions: ["short and funny", "a meme", "just lol"], predicted: [ "short", "and", "funny", "a", "meme", "gif", "lol", "lmao", "relatable", "quick", "reply", "something", "witty", "joke", "react", "emoji", "just", "yes", "ok", "sure", ], }, ]; const DEEP_LINKS = [ { question: "What year was the Eiffel Tower completed?", start: { title: "Eiffel Tower — overview", body: "Iron lattice tower on the Champ de Mars in Paris. Built as the entrance arch for the 1889 World’s Fair.", links: [ { label: "Construction timeline →", next: 1 }, { label: "Gustave Eiffel bio →", next: 2 }, { label: "Other Paris landmarks →", next: 3 }, ], }, pages: [ null, { title: "Construction timeline", body: "Work began in 1887. Rivets, wrought iron, and a lot of arguing with Parisians who hated the look.", links: [ { label: "1889 World’s Fair exhibits →", next: 4 }, { label: "Workers & safety →", next: 5 }, { label: "Rival tower proposals →", next: 3 }, ], }, { title: "Gustave Eiffel", body: "Engineer behind bridges, the Statue of Liberty’s internal frame, and one very tall argument.", links: [ { label: "Statue of Liberty frame →", next: 4 }, { label: "His other bridges →", next: 5 }, { label: "Back to tower facts →", next: 1 }, ], }, { title: "Other Paris landmarks", body: "Louvre, Notre-Dame, Sacré-Cœur — none of them are the thing you were asked about.", links: [ { label: "Louvre pyramid drama →", next: 5 }, { label: "Notre-Dame rebuild →", next: 4 }, { label: "Return to Eiffel notes →", next: 1 }, ], }, { title: "World’s Fair rabbit hole", body: "Exhibits included colonial pavilions, new electric lights, and souvenirs nobody needed.", links: [ { label: "Electric lighting craze →", next: 5 }, { label: "Fair attendance myths →", next: 3 }, { label: "One more hop →", next: 5 }, ], }, { title: "You are deep now", body: "Something about rivets, tourism, and a gift shop. The original question is getting soggy.", links: [], }, ], choices: [ "What year was the Eiffel Tower completed?", "Who designed the Louvre pyramid?", "When did Notre-Dame catch fire?", "How tall is the Statue of Liberty?", ], answer: 0, }, { question: "Which planet has the most moons?", start: { title: "Solar system cheat sheet", body: "Eight planets, countless rocks, and a comment section arguing about Pluto.", links: [ { label: "Gas giants compared →", next: 1 }, { label: "Moon definition drama →", next: 2 }, { label: "Pluto discourse →", next: 3 }, ], }, pages: [ null, { title: "Gas giants", body: "Jupiter and Saturn trade the moon crown depending on which year the catalogs update.", links: [ { label: "Saturn’s rings & moons →", next: 4 }, { label: "Jupiter’s big four →", next: 5 }, { label: "Uranus joins chat →", next: 3 }, ], }, { title: "What counts as a moon?", body: "Capture objects, temporary moons, and telescopes finding new pebbles every season.", links: [ { label: "Temporary moons →", next: 4 }, { label: "Naming conventions →", next: 5 }, { label: "Back to giants →", next: 1 }, ], }, { title: "Pluto discourse", body: "Not a planet (officially). Still has moons. Still starts fights at dinner.", links: [ { label: "Dwarf planet list →", next: 5 }, { label: "Kuiper belt tour →", next: 4 }, { label: "Gas giants again →", next: 1 }, ], }, { title: "Catalog update spiral", body: "New tiny moons announced. Press releases. Someone edits Wikipedia at 2am.", links: [ { label: "Telescope gossip →", next: 5 }, { label: "Naming petitions →", next: 3 }, { label: "Final hop →", next: 5 }, ], }, { title: "Orbiting the point", body: "You’ve collected trivia. The starting question is somewhere behind you.", links: [], }, ], choices: [ "Which planet has the most moons?", "Is Pluto a planet?", "How many rings does Saturn have?", "What is the Kuiper belt?", ], answer: 0, }, ]; const state = { mode: null, screen: "home", lastProbe: null, lastResult: null, traitScores: loadTraits(), run: null, }; const $ = (sel, root = document) => root.querySelector(sel); const $$ = (sel, root = document) => [...root.querySelectorAll(sel)]; const clamp = (n, a, b) => Math.max(a, Math.min(b, n)); const avg = (arr) => (arr.length ? arr.reduce((s, n) => s + n, 0) / arr.length : 0); function loadTraits() { try { return JSON.parse(localStorage.getItem("brainrot-traits") || "{}"); } catch { return {}; } } function saveTrait(probeId, score) { state.traitScores[probeId] = score; localStorage.setItem("brainrot-traits", JSON.stringify(state.traitScores)); } function probeById(id) { return PROBES.find((p) => p.id === id); } function showScreen(name) { state.screen = name; $$(".screen").forEach((el) => { el.classList.toggle("active", el.dataset.screen === name); }); if (name === "lab") renderLab(); if (name === "home") { stopRunClock(); setRunHud(false); animateHomeMeter(); } } function animateHomeMeter() { const fill = $("#homeMeter"); if (!fill) return; fill.style.width = "8%"; requestAnimationFrame(() => { fill.style.width = `${28 + Math.random() * 40}%`; }); } function stampGrade(score) { if (score < 35) return { text: "INTACT", grade: "grade-intact" }; if (score < 60) return { text: "MILD", grade: "grade-mild" }; return { text: "FRIED", grade: "grade-fried" }; } function applyStamp(score) { const el = $("#resultStamp"); if (!el) return; const g = stampGrade(score); el.textContent = g.text; el.className = `stamp ${g.grade}`; requestAnimationFrame(() => { el.classList.add("show"); }); } function setRunHud(on) { $("#runHud").hidden = !on; $("#app").classList.toggle("run-active", on); if (on && state.run) updateRunHud(); } function updateRunHud() { if (!state.run) return; const done = state.run.results.length; $("#runProbeLabel").textContent = `Probe ${Math.min(done + 1, RUN_ORDER.length)} of ${RUN_ORDER.length}`; const left = Math.max(0, state.run.endsAt - performance.now()); const m = Math.floor(left / 60000); const s = Math.floor((left % 60000) / 1000); $("#runClock").textContent = `${m}:${String(s).padStart(2, "0")}`; $("#runProgress").style.width = `${((SPEED_SECONDS * 1000 - left) / (SPEED_SECONDS * 1000)) * 100}%`; } function stopRunClock() { if (state.run?.clockId) clearInterval(state.run.clockId); } function startSpeedRun() { abortActiveProbe(); state.mode = "run"; state.run = { results: [], index: 0, endsAt: performance.now() + SPEED_SECONDS * 1000, clockId: null, timedOut: false, }; setRunHud(true); state.run.clockId = setInterval(() => { updateRunHud(); if (performance.now() >= state.run.endsAt) { state.run.timedOut = true; finishSpeedRun(); } }, 200); launchProbe(RUN_ORDER[0], { skipReady: false }); } function startLabProbe(id) { abortActiveProbe(); state.mode = "lab"; state.run = null; setRunHud(false); launchProbe(id); } function launchProbe(id, opts = {}) { state.lastProbe = id; const starters = { "feed-freeze": () => resetFeedFreeze(opts), landmark: () => resetLandmark(), "scene-swap": () => resetSceneSwap(opts), "anti-complete": () => resetAntiComplete(), "deep-link": () => resetDeepLink(), }; starters[id]?.(); showScreen(id); } function abortActiveProbe() { ff.running = false; clearInterval(ff.timerId); cancelAnimationFrame(ss.timerId); clearTimeout(ss.memorizeId); } function completeProbe(result) { state.lastResult = result; saveTrait(result.probeId, result.score); if (state.mode === "run") { state.run.results.push(result); const next = state.run.index + 1; if (state.run.timedOut || next >= RUN_ORDER.length) { finishSpeedRun(); return; } state.run.index = next; showInterstitial(result, probeById(RUN_ORDER[next])); return; } showProbeResult(result); } function showInterstitial(done, next) { $("#ixEyebrow").textContent = `${done.title} · ${done.score} pts`; $("#ixTitle").textContent = next.title; $("#ixBlurb").textContent = done.nextHint || next.blurb; $("#ixMeter").style.width = `${(state.run.results.length / RUN_ORDER.length) * 100}%`; showScreen("interstitial"); setTimeout(() => { if (state.mode !== "run" || state.run?.timedOut) return; launchProbe(next.id, { skipReady: true }); }, 1400); } function finishSpeedRun() { stopRunClock(); abortActiveProbe(); const results = state.run?.results || []; const completed = results.length; const scores = results.map((r) => r.score); const base = avg(scores); const incompletePenalty = (RUN_ORDER.length - completed) * 12; const timeBonus = completed === RUN_ORDER.length && !state.run.timedOut ? -8 : state.run.timedOut ? 6 : 0; const score = Math.round(clamp(base + incompletePenalty + timeBonus, 0, 99)); const blurb = completed === 0 ? "Time’s up before a single probe landed. The clinic has notes." : completed < RUN_ORDER.length ? `You cleared ${completed}/${RUN_ORDER.length} before the clock cooked you.` : score < 35 ? "Full circuit. Attention mostly intact. Suspicious." : score < 60 ? "Full circuit with noticeable rot. Hydrate and touch grass." : "Full circuit, heavily fried. Your thumbs filed a complaint."; setRunHud(false); state.mode = "run-done"; showCompositeResult({ title: "Speed run", label: "brainrot score", score, blurb, results, completed, }); } function showProbeResult(r) { $("#resultEyebrow").textContent = "probe result"; $("#resultTitle").textContent = r.title; $("#resultLabel").textContent = r.label; $("#resultScore").textContent = String(r.score); $("#resultUnit").textContent = "brainrot pts"; $("#resultBlurb").textContent = r.blurb; $("#resultBreakdown").hidden = true; $("#againBtn").textContent = "Retry probe"; $("#resultSecondary").textContent = "Back to lab"; $("#resultMeter").style.width = "0%"; $("#resultStamp").className = "stamp"; showScreen("result"); requestAnimationFrame(() => { $("#resultMeter").style.width = `${clamp(r.score, 0, 100)}%`; applyStamp(r.score); }); } function showCompositeResult({ title, label, score, blurb, results }) { $("#resultEyebrow").textContent = "clinic summary"; $("#resultTitle").textContent = title; $("#resultLabel").textContent = label; $("#resultScore").textContent = String(score); $("#resultUnit").textContent = "brainrot score"; $("#resultBlurb").textContent = blurb; const box = $("#resultBreakdown"); box.hidden = false; box.innerHTML = RUN_ORDER.map((id) => { const probe = probeById(id); const hit = results.find((r) => r.probeId === id); return `
${probe.title}${ hit ? hit.score : "—" }
`; }).join(""); $("#againBtn").textContent = "Run again"; $("#resultSecondary").textContent = "Open the lab"; $("#resultMeter").style.width = "0%"; $("#resultStamp").className = "stamp"; showScreen("result"); requestAnimationFrame(() => { $("#resultMeter").style.width = `${clamp(score, 0, 100)}%`; applyStamp(score); }); } function renderLab() { const list = $("#probeList"); list.innerHTML = ""; for (const probe of PROBES) { const scored = state.traitScores[probe.id]; const li = document.createElement("li"); const btn = document.createElement("button"); btn.type = "button"; btn.className = "probe"; btn.innerHTML = ` ${probe.code} ${probe.title} ${probe.blurb} ${scored == null ? "play" : scored} `; btn.addEventListener("click", () => startLabProbe(probe.id)); li.appendChild(btn); list.appendChild(li); } const card = $("#profileCard"); const ids = Object.keys(state.traitScores); if (!ids.length) { card.hidden = true; return; } card.hidden = false; $("#traitBars").innerHTML = PROBES.map((p) => { const s = state.traitScores[p.id]; if (s == null) return ""; return `
${p.trait}${s}
`; }).join(""); } function exitProbe() { abortActiveProbe(); if (state.mode === "run") { stopRunClock(); state.mode = null; state.run = null; setRunHud(false); showScreen("home"); return; } showScreen("lab"); } /* ——— Feed Freeze ——— */ const ff = { running: false, freezeWindow: false, scrolledDuringFreeze: false, touching: false, targetShownAt: 0, timerId: null, scrollHandler: null, }; function resetFeedFreeze(opts = {}) { clearInterval(ff.timerId); ff.running = false; ff.freezeWindow = false; ff.scrolledDuringFreeze = false; ff.touching = false; ff.targetShownAt = 0; const feed = $("#ffFeed"); feed.classList.remove("locked"); feed.innerHTML = ""; $("#ffBanner").hidden = true; $("#ffFreeze").hidden = true; $("#ffFreeze").disabled = true; $("#ffTimer").textContent = state.mode === "run" ? "10.0" : "12.0"; $("#ffBrief").innerHTML = "Scroll. When the real post flashes green, freeze — stop scrolling. Baits don’t count."; for (let i = 0; i < 24; i++) { const post = FEED_POSTS[i % FEED_POSTS.length]; const card = document.createElement("article"); card.className = "feed-card"; card.innerHTML = `
${post.handle}
${post.body}
`; feed.appendChild(card); } feed.scrollTop = 40; if (opts.skipReady || state.mode === "run") { $("#ffReady").hidden = true; setTimeout(() => beginFeedFreeze(), 350); } else { $("#ffReady").hidden = false; } } function beginFeedFreeze() { const feed = $("#ffFeed"); const freezeBtn = $("#ffFreeze"); const budget = state.mode === "run" ? 10000 : 12000; $("#ffReady").hidden = true; freezeBtn.hidden = false; freezeBtn.disabled = true; ff.running = true; const deadline = performance.now() + budget; const onScroll = () => { if (ff.freezeWindow) ff.scrolledDuringFreeze = true; }; feed.addEventListener("scroll", onScroll, { passive: true }); ff.scrollHandler = onScroll; feed.addEventListener( "touchstart", () => { ff.touching = true; }, { passive: true } ); feed.addEventListener( "touchend", () => { ff.touching = false; }, { passive: true } ); ff.timerId = setInterval(() => { if (state.mode === "run" && state.run?.timedOut) { endFeedFreeze("timeout"); return; } const left = Math.max(0, deadline - performance.now()); $("#ffTimer").textContent = (left / 1000).toFixed(1); if (left <= 0) endFeedFreeze("timeout"); }, 50); const baitAt = 1800 + Math.random() * 1600; const realAt = baitAt + 1500 + Math.random() * 2000; setTimeout(() => { if (ff.running) flashCard(feed, "bait"); }, baitAt); setTimeout(() => { if (!ff.running) return; flashCard(feed, "target"); ff.freezeWindow = true; ff.scrolledDuringFreeze = false; ff.targetShownAt = performance.now(); $("#ffBanner").hidden = false; freezeBtn.disabled = false; $("#ffBrief").textContent = "Green flash — stop scrolling and tap “I froze”."; }, realAt); } function flashCard(feed, kind) { $$(".feed-card.target, .feed-card.bait", feed).forEach((el) => { el.classList.remove("target", "bait"); }); const cards = $$(".feed-card", feed); const stage = $("#ffStage").getBoundingClientRect(); const visible = cards.filter((card) => { const r = card.getBoundingClientRect(); return r.top < stage.bottom - 40 && r.bottom > stage.top + 40; }); const card = (visible.length ? visible : cards)[ Math.floor(Math.random() * (visible.length ? visible.length : cards.length)) ]; if (!card) return; if (kind === "bait") { card.classList.add("bait"); card.querySelector(".body").textContent = BAIT_BODIES[Math.floor(Math.random() * BAIT_BODIES.length)]; setTimeout(() => card.classList.remove("bait"), 1100); } else { card.classList.add("target"); card.querySelector(".body").textContent = "this is the real one — freeze now"; } } function endFeedFreeze(reason) { if (!ff.running) return; ff.running = false; clearInterval(ff.timerId); $("#ffFeed").classList.add("locked"); $("#ffBanner").hidden = true; $("#ffFreeze").hidden = true; if (ff.scrollHandler) $("#ffFeed").removeEventListener("scroll", ff.scrollHandler); let score = 0; let blurb = ""; if (reason === "timeout") { score = 90; blurb = "Never froze. The feed filed you as inventory."; } else if (!ff.freezeWindow) { score = 76; blurb = "Froze on bait. The algorithm sent a thank-you note."; } else { const reaction = performance.now() - ff.targetShownAt; const dragging = ff.scrolledDuringFreeze || ff.touching; if (dragging) { score = clamp(Math.round(52 + reaction / 45), 52, 88); blurb = "You tapped freeze while still scrolling. Impulse over intention."; } else if (reaction < 450) { score = 14; blurb = "Clean stop. Either discipline or a dead battery."; } else if (reaction < 900) { score = 36; blurb = "Decent brakes. Mild thumb-rot."; } else { score = 62; blurb = "Slow freeze. Negotiations with your thumb ran long."; } } completeProbe({ probeId: "feed-freeze", title: "Feed Freeze", label: "doomscroll impulse", score: Math.round(score), blurb, nextHint: probeById("feed-freeze").nextHint, }); } /* ——— Landmark Labyrinth ——— */ const lm = { size: 5, walls: new Set(["0,1", "0,3", "2,0", "2,4", "4,1", "4,3", "1,2"]), pos: { x: 0, y: 4 }, dir: 0, goal: { x: 4, y: 0 }, moves: 0, wrong: 0, }; const DIR = [ { x: 0, y: -1 }, { x: 1, y: 0 }, { x: 0, y: 1 }, { x: -1, y: 0 }, ]; function resetLandmark() { lm.pos = { x: 0, y: 4 }; lm.dir = 0; lm.moves = 0; lm.wrong = 0; $("#lmMoves").textContent = "0"; $("#lmMission").innerHTML = "Reach the neon diner courtyard (top-right glow). No north arrow. Your pink dot’s notch is facing."; $("#lmBrief").textContent = "Turn with ↺ ↻, walk with ↑. Walls block. Landmarks are the only map."; renderLandmark(); } function renderLandmark() { const map = $("#lmMap"); const grid = document.createElement("div"); grid.className = "map-grid"; grid.style.gridTemplateColumns = `repeat(${lm.size}, 1fr)`; for (let y = 0; y < lm.size; y++) { for (let x = 0; x < lm.size; x++) { const key = `${x},${y}`; const cell = document.createElement("div"); cell.className = "cell"; if (lm.walls.has(key)) cell.classList.add("wall"); if (x === lm.goal.x && y === lm.goal.y) cell.classList.add("goal"); if (LANDMARKS[key]) { cell.innerHTML = `${LANDMARKS[key]}`; } if (x === lm.pos.x && y === lm.pos.y) { cell.innerHTML = `
`; } grid.appendChild(cell); } } map.innerHTML = ""; map.appendChild(grid); $("#lmMoves").textContent = String(lm.moves); } function lmTurn(delta) { lm.dir = (lm.dir + delta + 4) % 4; renderLandmark(); } function lmForward() { const n = { x: lm.pos.x + DIR[lm.dir].x, y: lm.pos.y + DIR[lm.dir].y, }; const key = `${n.x},${n.y}`; lm.moves += 1; if ( n.x < 0 || n.y < 0 || n.x >= lm.size || n.y >= lm.size || lm.walls.has(key) ) { lm.wrong += 1; renderLandmark(); return; } lm.pos = n; renderLandmark(); if (lm.pos.x === lm.goal.x && lm.pos.y === lm.goal.y) endLandmark(); } function endLandmark() { const optimal = 8; const excess = Math.max(0, lm.moves - optimal); const score = clamp(Math.round(18 + excess * 7 + lm.wrong * 9), 10, 95); const blurb = score < 30 ? "You built a mental map. GPS would be jealous." : score < 55 ? "Got there with some bumping. Mild turn-by-turn atrophy." : "Wandering. The blue café watched you circle like a lost tourist."; completeProbe({ probeId: "landmark", title: "Landmark Labyrinth", label: "GPS dependence", score, blurb, nextHint: probeById("landmark").nextHint, }); } /* ——— Scene Swap ——— */ const ss = { phase: "idle", hits: new Set(), misses: 0, timerId: null, memorizeId: null, changes: [], }; function resetSceneSwap(opts = {}) { cancelAnimationFrame(ss.timerId); clearTimeout(ss.memorizeId); ss.phase = "idle"; ss.hits = new Set(); ss.misses = 0; $("#ssDone").hidden = true; $("#ssTimer").textContent = "3.0"; $("#ssBrief").textContent = "Memorize the park scene. Then tap every change. False taps hurt."; $("#ssStage").innerHTML = ""; if (opts.skipReady || state.mode === "run") { $("#ssReady").hidden = true; setTimeout(() => beginSceneSwap(), 300); } else { $("#ssReady").hidden = false; } } function sceneSvg(changed) { const birdY = changed.includes("bird") ? 42 : 58; const ballX = changed.includes("ball") ? 250 : 210; const windowLit = changed.includes("window"); const benchGone = changed.includes("bench"); return ` ${ benchGone ? "" : `` } `; } function beginSceneSwap() { ss.phase = "memorize"; $("#ssReady").hidden = true; const pool = ["bird", "ball", "window", "bench"]; ss.changes = pool.sort(() => Math.random() - 0.5).slice(0, 3); $("#ssStage").innerHTML = sceneSvg([]); $("#ssBrief").textContent = "Look carefully…"; const start = performance.now(); const dur = state.mode === "run" ? 2500 : 3200; const tick = () => { const left = Math.max(0, dur - (performance.now() - start)); $("#ssTimer").textContent = (left / 1000).toFixed(1); if (left > 0) ss.timerId = requestAnimationFrame(tick); }; ss.timerId = requestAnimationFrame(tick); ss.memorizeId = setTimeout(() => { ss.phase = "find"; $("#ssStage").innerHTML = sceneSvg(ss.changes); $("#ssBrief").textContent = `Tap the ${ss.changes.length} changes. Then lock in.`; $("#ssDone").hidden = false; $("#ssTimer").textContent = "go"; wireSceneHotspots(); }, dur); } function wireSceneHotspots() { $$(".hotspot", $("#ssStage")).forEach((el) => { el.addEventListener("click", () => { if (ss.phase !== "find") return; const id = el.dataset.id; if (ss.changes.includes(id)) { ss.hits.add(id); el.classList.add("found"); } else { ss.misses += 1; el.classList.add("miss"); } }); }); } function endSceneSwap() { if (ss.phase !== "find") return; ss.phase = "done"; const found = ss.hits.size; const total = ss.changes.length; const missPenalty = ss.misses * 14; const missChanges = (total - found) * 22; const score = clamp(Math.round(12 + missChanges + missPenalty), 8, 96); const blurb = found === total && ss.misses === 0 ? "Sharp glance memory. The feed hasn’t sanded you down yet." : found === total ? "You found the swaps but poked the scenery. Impulsive tapping." : `Caught ${found}/${total}. Scrolling trains the eyes to skim, not store.`; completeProbe({ probeId: "scene-swap", title: "Scene Swap", label: "glance memory", score, blurb, nextHint: probeById("scene-swap").nextHint, }); } /* ——— Anti-Complete ——— */ function resetAntiComplete() { const prompt = AC_PROMPTS[Math.floor(Math.random() * AC_PROMPTS.length)]; state._ac = prompt; $("#acStem").textContent = prompt.stem; $("#acSuggest").innerHTML = prompt.suggestions .map((s) => `${s}`) .join(""); $("#acInput").value = ""; setTimeout(() => $("#acInput").focus(), 200); } function endAntiComplete() { const text = ($("#acInput").value || "").trim(); if (!text) { $("#acInput").focus(); return; } const predicted = new Set(state._ac.predicted); const tokens = text .toLowerCase() .replace(/[^a-z0-9\s']/g, " ") .split(/\s+/) .filter(Boolean); const hits = tokens.filter((t) => predicted.has(t)).length; const ratio = tokens.length ? hits / tokens.length : 1; const shortPenalty = tokens.length < 4 ? 18 : 0; const score = clamp(Math.round(ratio * 85 + shortPenalty), 8, 95); const blurb = score < 30 ? "Weird on purpose. Predictive text would never." : score < 55 ? "Half original, half autocomplete gravity." : "You drifted into the suggestion cloud. Homogenized."; completeProbe({ probeId: "anti-complete", title: "Anti-Complete", label: "autocomplete gravity", score, blurb, nextHint: probeById("anti-complete").nextHint, }); } /* ——— Deep Link ——— */ const dl = { pack: null, hops: 0, page: null, }; function resetDeepLink() { dl.pack = DEEP_LINKS[Math.floor(Math.random() * DEEP_LINKS.length)]; dl.hops = 0; dl.page = "start"; $("#dlHop").textContent = "0/3"; $("#dlBrief").textContent = `Your question: “${dl.pack.question}” — remember it.`; renderDeepPage(dl.pack.start); } function renderDeepPage(page) { $("#dlCard").innerHTML = `

${page.title}

${page.body}

`; const actions = $("#dlActions"); actions.innerHTML = ""; if (dl.hops >= 3) { $("#dlBrief").textContent = "What was your original question?"; $("#dlHop").textContent = "3/3"; dl.pack.choices.forEach((label, i) => { const btn = document.createElement("button"); btn.type = "button"; btn.className = "link-choice"; btn.textContent = label; btn.addEventListener("click", () => endDeepLink(i)); actions.appendChild(btn); }); return; } page.links.forEach((link) => { const btn = document.createElement("button"); btn.type = "button"; btn.className = "link-choice"; btn.textContent = link.label; btn.addEventListener("click", () => { dl.hops += 1; $("#dlHop").textContent = `${dl.hops}/3`; const next = dl.pack.pages[link.next] || dl.pack.pages[5]; renderDeepPage(next); }); actions.appendChild(btn); }); } function endDeepLink(choice) { const correct = choice === dl.pack.answer; const score = correct ? 18 : 82; const blurb = correct ? "Held the thread. Attention residue didn’t eat the brief." : "Lost the starting question in the hops. Classic deep-link amnesia."; completeProbe({ probeId: "deep-link", title: "Deep Link Amnesia", label: "context thrash", score, blurb, nextHint: null, }); } /* ——— Wire UI ——— */ function wireUi() { $("#speedRunBtn").addEventListener("click", startSpeedRun); $("#labBtn").addEventListener("click", () => { state.mode = "lab"; showScreen("lab"); }); $$(".exit-probe").forEach((btn) => btn.addEventListener("click", exitProbe)); $$("[data-nav]").forEach((btn) => { btn.addEventListener("click", () => { abortActiveProbe(); stopRunClock(); state.run = null; setRunHud(false); showScreen(btn.dataset.nav); }); }); $("#ffReady").addEventListener("click", beginFeedFreeze); $("#ffFreeze").addEventListener("click", () => endFeedFreeze("freeze")); $("#lmLeft").addEventListener("click", () => lmTurn(-1)); $("#lmRight").addEventListener("click", () => lmTurn(1)); $("#lmForward").addEventListener("click", lmForward); $("#ssReady").addEventListener("click", beginSceneSwap); $("#ssDone").addEventListener("click", endSceneSwap); $("#acSubmit").addEventListener("click", endAntiComplete); $("#againBtn").addEventListener("click", () => { if (state.mode === "run-done" || $("#againBtn").textContent.includes("Run")) { startSpeedRun(); return; } startLabProbe(state.lastProbe); }); $("#resultSecondary").addEventListener("click", () => { state.mode = "lab"; setRunHud(false); showScreen("lab"); }); $("#resultBack").addEventListener("click", () => { setRunHud(false); showScreen(state.mode === "run-done" ? "home" : "lab"); }); } wireUi(); animateHomeMeter();