From a2bc77805c80a64b89531d53ce369edc7b184ed6 Mon Sep 17 00:00:00 2001 From: Zaine Arch Date: Sat, 9 May 2026 22:38:51 +0100 Subject: [PATCH] rpg initial commit --- assets/scripts/play.js | 865 +++++++++++++++++++++++++++++++++++++++++ assets/styles/play.css | 646 ++++++++++++++++++++++++++++++ build-site.el | 3 + index.org | 5 + play.org | 57 +++ play/bookshelf.org | 22 ++ play/constellation.org | 22 ++ play/ink.org | 18 + play/memory.org | 25 ++ play/poem.org | 19 + play/recipe.org | 19 + play/rpg.org | 60 +++ play/sigil.org | 23 ++ play/study.org | 24 ++ play/terminal.org | 22 ++ play/timeline.org | 22 ++ posts/posts-list.org | 2 +- recently-updated.org | 24 +- sitemap.org | 23 +- 19 files changed, 1883 insertions(+), 18 deletions(-) create mode 100644 assets/scripts/play.js create mode 100644 assets/styles/play.css create mode 100644 play.org create mode 100644 play/bookshelf.org create mode 100644 play/constellation.org create mode 100644 play/ink.org create mode 100644 play/memory.org create mode 100644 play/poem.org create mode 100644 play/recipe.org create mode 100644 play/rpg.org create mode 100644 play/sigil.org create mode 100644 play/study.org create mode 100644 play/terminal.org create mode 100644 play/timeline.org diff --git a/assets/scripts/play.js b/assets/scripts/play.js new file mode 100644 index 0000000..442d051 --- /dev/null +++ b/assets/scripts/play.js @@ -0,0 +1,865 @@ +(function () { + const $ = (sel, root = document) => root.querySelector(sel); + const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel)); + const shuffle = (items) => items.map((value) => ({ value, sort: Math.random() })).sort((a, b) => a.sort - b.sort).map((item) => item.value); + const quips = [ + "A margin note blinks, then pretends it did not.", + "Somewhere, a bookmark changes its mind.", + "The archive is pleased by unnecessary curiosity.", + "A tiny bell rings in a room you have not built yet.", + "The page remembers that you came here." + ]; + + document.addEventListener("DOMContentLoaded", () => { + const root = $(".play-root[data-play-page]"); + if (!root) return; + const page = root.dataset.playPage; + const inits = { hub, memory, constellation, poem, bookshelf, recipe, timeline, ink, terminal, study, sigil, rpg }; + if (inits[page]) inits[page](root); + }); + + function hub(root) { + const links = $$(".play-grid a", root); + const nodes = $$(".play-orbit__node", root); + const title = $("#play-hub-title", root); + const kind = $("#play-hub-kind", root); + const desc = $("#play-hub-desc", root); + const cta = $("#play-hub-link", root); + const whisper = document.createElement("p"); + let active = 0; + + whisper.className = "play-whisper"; + whisper.setAttribute("aria-live", "polite"); + $(".play-console__screen", root).appendChild(whisper); + + nodes.forEach((node, index) => { + const angle = (index / nodes.length) * Math.PI * 2 - Math.PI / 2; + node.style.left = `${50 + Math.cos(angle) * 43}%`; + node.style.top = `${50 + Math.sin(angle) * 43}%`; + node.addEventListener("click", () => setActive(index)); + }); + + function setActive(index) { + active = (index + links.length) % links.length; + const link = links[active]; + links.forEach((item, i) => item.classList.toggle("is-active", i === active)); + nodes.forEach((item, i) => item.classList.toggle("is-active", i === active)); + title.textContent = link.querySelector("strong").textContent; + kind.textContent = link.dataset.kind; + desc.textContent = link.dataset.desc; + cta.href = link.href; + whisper.textContent = quips[active % quips.length]; + } + + links.forEach((link, index) => { + link.addEventListener("mouseenter", () => setActive(index)); + link.addEventListener("focus", () => setActive(index)); + }); + $("[data-play-prev]", root).addEventListener("click", () => setActive(active - 1)); + $("[data-play-next]", root).addEventListener("click", () => setActive(active + 1)); + $("[data-play-random]", root).addEventListener("click", () => setActive(Math.floor(Math.random() * links.length))); + $(".play-orbit", root).addEventListener("dblclick", () => { + whisper.textContent = "You knocked twice. The cabinet knocked once back."; + root.classList.toggle("is-odd"); + }); + root.addEventListener("keydown", (event) => { + if (event.key === "ArrowLeft") setActive(active - 1); + if (event.key === "ArrowRight") setActive(active + 1); + }); + setActive(0); + } + + function memory(root) { + const symbols = ["Ink", "Lamp", "Tea", "Book", "Map", "Key"]; + const board = $("[data-memory-board]", root); + const turnsEl = $("[data-memory-turns]", root); + const matchesEl = $("[data-memory-matches]", root); + const messageEl = $("[data-memory-message]", root); + let open = []; + let turns = 0; + let matches = 0; + let deck = []; + + function render() { + board.innerHTML = ""; + open = []; + turns = 0; + matches = 0; + deck = shuffle([...symbols, ...symbols]); + turnsEl.textContent = "0"; + matchesEl.textContent = "0"; + messageEl.textContent = "Cards are shuffled."; + deck.forEach((symbol, index) => { + const card = document.createElement("button"); + card.type = "button"; + card.className = "memory-card"; + card.dataset.symbol = symbol; + card.textContent = "?"; + card.style.setProperty("--tilt", `${(index % 5) - 2}deg`); + card.setAttribute("aria-label", "Hidden card"); + card.addEventListener("click", () => flip(card)); + board.appendChild(card); + }); + } + + function flip(card) { + if (card.classList.contains("is-open") || card.classList.contains("is-matched") || open.length === 2) return; + card.classList.add("is-open"); + card.textContent = card.dataset.symbol; + card.setAttribute("aria-label", card.dataset.symbol); + open.push(card); + if (open.length !== 2) return; + turns += 1; + turnsEl.textContent = String(turns); + const [a, b] = open; + if (a.dataset.symbol === b.dataset.symbol) { + a.classList.add("is-matched"); + b.classList.add("is-matched"); + open = []; + matches += 1; + matchesEl.textContent = String(matches); + messageEl.textContent = matches === symbols.length ? `Cabinet solved in ${turns} turns.` : "A drawer clicks open."; + } else { + messageEl.textContent = "No match. The cabinet quietly re-files them."; + setTimeout(() => { + a.classList.remove("is-open"); + b.classList.remove("is-open"); + a.textContent = "?"; + b.textContent = "?"; + a.setAttribute("aria-label", "Hidden card"); + b.setAttribute("aria-label", "Hidden card"); + open = []; + }, 650); + } + } + + $("[data-memory-reset]", root).addEventListener("click", render); + $("[data-memory-peek]", root).addEventListener("click", () => { + const hidden = $$(".memory-card:not(.is-open):not(.is-matched)", root); + hidden.forEach((card) => { + card.textContent = card.dataset.symbol; + card.classList.add("is-peeking"); + }); + messageEl.textContent = "A very brief lapse in scholarly discipline."; + setTimeout(() => { + hidden.forEach((card) => { + if (!card.classList.contains("is-open") && !card.classList.contains("is-matched")) card.textContent = "?"; + card.classList.remove("is-peeking"); + }); + }, 900); + }); + render(); + } + + function constellation(root) { + const canvas = $("[data-star-canvas]", root); + const ctx = canvas.getContext("2d"); + const linesEl = $("[data-star-lines]", root); + const nameEl = document.createElement("strong"); + const stars = Array.from({ length: 18 }, () => ({ x: 50 + Math.random() * 800, y: 45 + Math.random() * 430 })); + const lines = []; + let selected = null; + nameEl.className = "play-live-note"; + linesEl.closest(".play-scorebar").appendChild(nameEl); + + function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = "#18130f"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.strokeStyle = "rgba(219, 184, 104, 0.65)"; + ctx.lineWidth = 2; + lines.forEach(([a, b]) => { + ctx.beginPath(); + ctx.moveTo(stars[a].x, stars[a].y); + ctx.lineTo(stars[b].x, stars[b].y); + ctx.stroke(); + }); + stars.forEach((star, index) => { + ctx.beginPath(); + ctx.fillStyle = index === selected ? "#f6d889" : "#fff8e8"; + ctx.arc(star.x, star.y, index === selected ? 7 : 4, 0, Math.PI * 2); + ctx.fill(); + }); + linesEl.textContent = String(lines.length); + if (lines.length >= 3) nameEl.textContent = `Named: ${pick(["The Patient Spoon", "The South Window", "The Tired Comet", "The Fifth Errand"])}`; + } + + canvas.addEventListener("click", (event) => { + const rect = canvas.getBoundingClientRect(); + const x = ((event.clientX - rect.left) / rect.width) * canvas.width; + const y = ((event.clientY - rect.top) / rect.height) * canvas.height; + const hit = stars.findIndex((star) => Math.hypot(star.x - x, star.y - y) < 22); + if (hit < 0) return; + if (selected === null) selected = hit; + else if (selected !== hit) { + lines.push([selected, hit]); + selected = hit; + } + draw(); + }); + $("[data-star-reset]", root).addEventListener("click", () => { + lines.length = 0; + selected = null; + nameEl.textContent = ""; + draw(); + }); + draw(); + } + + function poem(root) { + const output = $("[data-poem-output]", root); + const a = ["In the margin", "By the kettle", "Under the desk lamp", "Between two errands", "After the house quiets"]; + const b = ["a careful thought", "an old joke", "a stubborn question", "a useful mistake", "a bright scrap"]; + const c = ["learns to wait.", "asks for a second reading.", "finds its proper shelf.", "becomes tomorrow's note.", "keeps the page warm."]; + let count = 0; + function make() { + count += 1; + output.innerHTML = `${pick(a)}
${pick(b)}
${pick(c)}`; + output.dataset.stamp = count % 5 === 0 ? "approved by the margin" : ""; + } + $("[data-poem-generate]", root).addEventListener("click", make); + make(); + } + + function bookshelf(root) { + const titles = ["Algebra at Breakfast", "Cabinet of Weather", "Domestic Orbits", "Evening Margins", "Household Engines", "Zettels and Tea"]; + const shelf = $("[data-bookshelf]", root); + const status = $("[data-bookshelf-status]", root); + let sortedOnce = false; + setupSortable(shelf, render, check); + $("[data-bookshelf-shuffle]", root).addEventListener("click", () => render(shuffle(titles))); + render(shuffle(titles)); + + function render(items) { + shelf.innerHTML = ""; + items.forEach((title) => { + const book = document.createElement("div"); + book.className = "book-spine"; + book.draggable = true; + book.dataset.value = title; + book.textContent = title; + shelf.appendChild(book); + }); + check(); + } + function check() { + const current = $$(".book-spine", shelf).map((book) => book.dataset.value); + const sorted = current.join("|") === titles.join("|"); + status.textContent = sorted ? "Shelf sorted. A secret pamphlet slides out." : "Unsorted"; + if (sorted && !sortedOnce) { + sortedOnce = true; + const pamphlet = document.createElement("div"); + pamphlet.className = "booklet"; + pamphlet.textContent = "Pamphlet: On the Correct Order of Small Things"; + shelf.appendChild(pamphlet); + } + } + } + + function recipe(root) { + const wheel = $("[data-recipe-spin]", root); + const result = $("[data-recipe-result]", root); + const groups = [ + ["Rice", "Flatbread", "Roast potatoes", "Noodles"], + ["Lemon chicken", "Spiced lentils", "Tomato eggs", "Pepper stew"], + ["Cucumber salad", "Mint yoghurt", "Pickled onions", "Charred greens"], + ["Serve with stories", "Eat outside", "Use the blue plates", "Make extra tea"] + ]; + let spin = 0; + let feast = 0; + wheel.addEventListener("click", () => { + spin += 540 + Math.floor(Math.random() * 540); + feast += 1; + wheel.style.transform = `rotate(${spin}deg)`; + result.innerHTML = groups.map((group) => `
  • ${pick(group)}
  • `).join("") + (feast % 4 === 0 ? "
  • Bonus: someone gets the last crispy bit.
  • " : ""); + }); + wheel.click(); + } + + function timeline(root) { + const items = [ + { year: "2022", text: "The personal web habit begins." }, + { year: "2025", text: "Org publishing becomes the main site engine." }, + { year: "2025", text: "Weekly reviews and career notes grow into a library." }, + { year: "2026", text: "Dashboards, services, and authoring tools join the site." }, + { year: "2026", text: "The play wing opens." } + ]; + const list = $("[data-timeline-list]", root); + const status = $("[data-timeline-status]", root); + const paradox = document.createElement("button"); + paradox.type = "button"; + paradox.textContent = "Paradox"; + $("[data-timeline-shuffle]", root).insertAdjacentElement("afterend", paradox); + setupSortable(list, render, check); + $("[data-timeline-shuffle]", root).addEventListener("click", () => render(shuffle(items))); + paradox.addEventListener("click", () => render([...items].reverse())); + render(shuffle(items)); + + function render(source) { + list.innerHTML = ""; + source.forEach((item, index) => { + const card = document.createElement("div"); + card.className = "timeline-card"; + card.draggable = true; + card.dataset.value = String(items.indexOf(item)); + card.innerHTML = `${item.year}${item.text}`; + list.appendChild(card); + }); + check(); + } + function check() { + const current = $$(".timeline-card", list).map((card) => Number(card.dataset.value)); + status.textContent = current.every((value, index) => value === index) ? "Timeline restored" : "Arrange the cards"; + } + } + + function ink(root) { + const canvas = $("[data-ink-canvas]", root); + const ctx = canvas.getContext("2d"); + const drops = []; + let moon = false; + function addDrop(x, y, heavy) { + drops.push({ x, y, r: heavy ? 18 : 8, life: heavy ? 1.8 : 1, hue: 28 + Math.random() * 35 }); + } + function pointer(event, heavy) { + const rect = canvas.getBoundingClientRect(); + addDrop(((event.clientX - rect.left) / rect.width) * canvas.width, ((event.clientY - rect.top) / rect.height) * canvas.height, heavy); + } + canvas.addEventListener("pointermove", (event) => pointer(event, false)); + canvas.addEventListener("click", (event) => pointer(event, true)); + canvas.addEventListener("dblclick", () => { + moon = !moon; + }); + function frame() { + ctx.fillStyle = "rgba(24, 19, 15, 0.16)"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + if (moon) { + ctx.beginPath(); + ctx.fillStyle = "rgba(246, 216, 137, 0.72)"; + ctx.arc(760, 88, 34, 0, Math.PI * 2); + ctx.fill(); + } + drops.forEach((drop) => { + drop.r += 0.7; + drop.life -= 0.012; + ctx.beginPath(); + ctx.strokeStyle = `hsla(${drop.hue}, 65%, 68%, ${Math.max(drop.life, 0)})`; + ctx.lineWidth = 2; + ctx.arc(drop.x, drop.y, drop.r, 0, Math.PI * 2); + ctx.stroke(); + }); + for (let i = drops.length - 1; i >= 0; i -= 1) if (drops[i].life <= 0) drops.splice(i, 1); + requestAnimationFrame(frame); + } + frame(); + } + + function terminal(root) { + const log = $("[data-terminal-log]", root); + const form = $("[data-terminal-form]", root); + const input = $("[data-terminal-input]", root); + let kindness = 0; + const responses = { + help: "Commands: help, look, map, open drawer, read note, brew tea, hum, knock, save, inventory, clear", + look: "A narrow archive room. A lamp hums. A drawer is labelled MAYBE IMPORTANT.", + map: "You are between the reading desk, the family shelf, and the door back to Play.", + "open drawer": "Inside: a brass key, a receipt, and a note folded twice.", + "read note": "The note says: keep the site useful, but leave a few doors ajar.", + "brew tea": "The room smells briefly of cardamom. Nothing else changes, which is enough.", + hum: "You hum four careful notes. Something behind the wall hums five back.", + knock: "Knock. Knock. ... A polite pause. Knock.", + save: "You save your place in the archive. The archive saves its place in you.", + inventory: "You are carrying: a brass key, a warm cup, and one unreasonable hope." + }; + function write(text) { + log.textContent += `${text}\n`; + log.scrollTop = log.scrollHeight; + } + form.addEventListener("submit", (event) => { + event.preventDefault(); + const cmd = input.value.trim().toLowerCase(); + if (!cmd) return; + input.value = ""; + if (cmd === "clear") { + log.textContent = ""; + return; + } + write(`> ${cmd}`); + if (cmd === "pet book") { + kindness += 1; + write(kindness > 2 ? "The book follows you for exactly three pages." : "The book accepts this with suspicious dignity."); + } else { + write(responses[cmd] || "The archive declines to understand that command."); + } + }); + write("Archive terminal ready. Type help. The cursor is listening."); + input.focus(); + } + + function study(root) { + const time = $("[data-study-time]", root); + const scene = $("[data-study-scene]", root); + let total = 300; + let left = total; + let timer = null; + const note = document.createElement("div"); + note.className = "play-live-note"; + note.textContent = "The lamp is cold."; + time.insertAdjacentElement("afterend", note); + function draw() { + const done = 1 - left / total; + scene.style.setProperty("--lamp", String(0.18 + done * 0.72)); + scene.style.setProperty("--glow", `${done * 70}px`); + const m = String(Math.floor(left / 60)).padStart(2, "0"); + const s = String(left % 60).padStart(2, "0"); + time.textContent = `${m}:${s}`; + if (left === 0) note.textContent = "Focus complete. The desk looks proud."; + else if (done > 0.66) note.textContent = "The page has warmed through."; + else if (done > 0.33) note.textContent = "The lamp has settled into its work."; + } + function start() { + if (timer) return; + timer = setInterval(() => { + left = Math.max(0, left - 1); + draw(); + if (left === 0) pause(); + }, 1000); + } + function pause() { + clearInterval(timer); + timer = null; + } + $("[data-study-start]", root).addEventListener("click", start); + $("[data-study-pause]", root).addEventListener("click", pause); + $("[data-study-reset]", root).addEventListener("click", () => { + pause(); + left = total; + draw(); + }); + draw(); + } + + function sigil(root) { + const canvas = $("[data-sigil-canvas]", root); + const ctx = canvas.getContext("2d"); + const initials = $("[data-sigil-initials]", root); + const motto = $("[data-sigil-motto]", root); + let wax = "#9c6b2f"; + const waxButton = document.createElement("button"); + waxButton.type = "button"; + waxButton.textContent = "New wax"; + $("[data-sigil-form]", root).appendChild(waxButton); + function draw() { + const text = (initials.value || "ZXH").toUpperCase(); + const phrase = motto.value || "Learn, make, remember"; + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = "#fbf7ec"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.translate(260, 260); + ctx.strokeStyle = wax; + ctx.lineWidth = 10; + ctx.beginPath(); + ctx.arc(0, 0, 190, 0, Math.PI * 2); + ctx.stroke(); + ctx.lineWidth = 2; + for (let i = 0; i < 12; i += 1) { + ctx.rotate(Math.PI / 6); + ctx.beginPath(); + ctx.moveTo(0, -150); + ctx.lineTo(0, -185); + ctx.stroke(); + } + ctx.rotate(-Math.PI * 2); + ctx.fillStyle = "#2a2118"; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.font = "900 94px Georgia, serif"; + ctx.fillText(text.slice(0, 4), 0, -10); + ctx.font = "700 22px Georgia, serif"; + ctx.fillText(phrase.slice(0, 34), 0, 88); + ctx.setTransform(1, 0, 0, 1, 0, 0); + } + waxButton.addEventListener("click", () => { + wax = pick(["#9c6b2f", "#8a3f3f", "#486b57", "#3e5f8a", "#5f4b8b"]); + draw(); + }); + $("[data-sigil-form]", root).addEventListener("submit", (event) => { + event.preventDefault(); + draw(); + }); + initials.addEventListener("input", draw); + motto.addEventListener("input", draw); + draw(); + } + + function rpg(root) { + const canvas = $("[data-rpg-canvas]", root); + const ctx = canvas.getContext("2d"); + const els = { + speaker: $("[data-rpg-speaker]", root), + line: $("[data-rpg-line]", root), + room: $("[data-rpg-room]", root), + route: $("[data-rpg-route]", root), + hearts: $("[data-rpg-hearts]", root), + quests: $("[data-rpg-quests]", root), + inventory: $("[data-rpg-inventory]", root), + status: $("[data-rpg-status]", root) + }; + const tile = 32; + const saveSlot = "hollow-archive"; + const fallbackKey = "play:rpg:hollow-archive"; + const items = { + lamp: { label: "Desk Lamp", room: "entrance", x: 10, y: 8, color: "#f2c94c" }, + page: { label: "Loose Page", room: "stacks", x: 18, y: 5, color: "#f7efe0" }, + key: { label: "Basement Key", room: "garden", x: 4, y: 11, color: "#d59b45" } + }; + const npcs = { + archivist: { name: "Archivist", room: "entrance", x: 6, y: 6, color: "#b98bff" }, + shade: { name: "Shy Shade", room: "stacks", x: 17, y: 9, color: "#6ed0d4" }, + gate: { name: "Iron Door", room: "garden", x: 19, y: 10, color: "#8a8f98" } + }; + const rooms = { + entrance: { + name: "Entrance", + floor: "#353029", + exits: [{ x: 22, y: 7, to: "stacks", px: 1, py: 7 }], + walls: rects([[0, 0, 24, 1], [0, 15, 24, 1], [0, 0, 1, 16], [23, 0, 1, 6], [23, 9, 1, 7], [8, 3, 1, 8], [14, 7, 5, 1]]) + }, + stacks: { + name: "Stacks", + floor: "#242d35", + exits: [{ x: 0, y: 7, to: "entrance", px: 22, py: 7 }, { x: 23, y: 12, to: "garden", px: 1, py: 12 }], + walls: rects([[0, 0, 24, 1], [0, 15, 24, 1], [0, 0, 1, 6], [0, 9, 1, 7], [23, 0, 1, 11], [23, 14, 1, 2], [4, 3, 2, 10], [10, 2, 2, 11], [16, 3, 2, 5]]) + }, + garden: { + name: "Moon Garden", + floor: "#21362e", + exits: [{ x: 0, y: 12, to: "stacks", px: 22, py: 12 }], + walls: rects([[0, 0, 24, 1], [0, 15, 24, 1], [0, 0, 1, 11], [0, 14, 1, 2], [23, 0, 1, 16], [7, 4, 10, 1], [7, 10, 1, 4], [13, 10, 1, 4]]) + } + }; + let state = freshState(); + let running = false; + + function freshState() { + return { + room: "entrance", + player: { x: 3, y: 7, facing: "down" }, + inventory: [], + flags: {}, + route: "Undecided", + hearts: 3, + ending: null, + message: { speaker: "Archivist", line: "The archive waits. Find the lamp, help the shade, then decide what to do with the locked door." } + }; + } + + function rects(sources) { + const set = new Set(); + sources.forEach(([x, y, w, h]) => { + for (let yy = y; yy < y + h; yy += 1) for (let xx = x; xx < x + w; xx += 1) set.add(`${xx},${yy}`); + }); + return set; + } + + function draw() { + const room = rooms[state.room]; + ctx.fillStyle = "#14110e"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + for (let y = 0; y < 16; y += 1) { + for (let x = 0; x < 24; x += 1) { + ctx.fillStyle = room.walls.has(`${x},${y}`) ? "#181716" : room.floor; + ctx.fillRect(x * tile, y * tile, tile, tile); + ctx.strokeStyle = "rgba(255,255,255,0.035)"; + ctx.strokeRect(x * tile, y * tile, tile, tile); + } + } + room.exits.forEach((exit) => drawGlyph(exit.x, exit.y, "#c48a41", "door")); + Object.entries(items).forEach(([id, item]) => { + if (item.room === state.room && !state.inventory.includes(id)) drawGlyph(item.x, item.y, item.color, "item"); + }); + Object.values(npcs).forEach((npc) => { + if (npc.room === state.room) drawGlyph(npc.x, npc.y, npc.color, npc.name === "Iron Door" ? "doorNpc" : "npc"); + }); + drawPlayer(); + renderHud(); + requestAnimationFrame(draw); + } + + function drawGlyph(x, y, color, kind) { + const px = x * tile; + const py = y * tile; + ctx.fillStyle = color; + if (kind === "item") { + ctx.fillRect(px + 10, py + 10, 12, 12); + ctx.fillStyle = "rgba(255,255,255,0.45)"; + ctx.fillRect(px + 13, py + 7, 6, 6); + } else if (kind === "door" || kind === "doorNpc") { + ctx.fillRect(px + 7, py + 4, 18, 25); + ctx.fillStyle = "#211812"; + ctx.fillRect(px + 20, py + 16, 3, 3); + } else { + ctx.beginPath(); + ctx.arc(px + 16, py + 12, 9, 0, Math.PI * 2); + ctx.fill(); + ctx.fillRect(px + 8, py + 20, 16, 8); + } + } + + function drawPlayer() { + const px = state.player.x * tile; + const py = state.player.y * tile; + ctx.fillStyle = "#f35f5f"; + ctx.beginPath(); + ctx.moveTo(px + 16, py + 7); + ctx.bezierCurveTo(px + 2, py + 2, px + 1, py + 22, px + 16, py + 28); + ctx.bezierCurveTo(px + 31, py + 22, px + 30, py + 2, px + 16, py + 7); + ctx.fill(); + ctx.fillStyle = "#fff8e8"; + ctx.fillRect(px + 11, py + 13, 4, 4); + ctx.fillRect(px + 18, py + 13, 4, 4); + } + + function renderHud() { + els.speaker.textContent = state.message.speaker; + els.line.textContent = state.ending ? endingLine() : state.message.line; + els.room.textContent = rooms[state.room].name; + els.route.textContent = state.route; + els.hearts.textContent = String(state.hearts); + els.inventory.innerHTML = state.inventory.length + ? state.inventory.map((id) => `
  • ${items[id].label}
  • `).join("") + : "
  • Empty
  • "; + const questRows = [ + ["Find a light", state.inventory.includes("lamp")], + ["Return the loose page to the shade", state.flags.shadeHelped], + ["Open, force, or leave the iron door", Boolean(state.ending)] + ]; + els.quests.innerHTML = questRows.map(([text, done]) => `
  • ${done ? "Done: " : ""}${text}
  • `).join(""); + } + + function move(dx, dy, facing) { + if (!running || state.ending) return; + state.player.facing = facing; + const nx = state.player.x + dx; + const ny = state.player.y + dy; + const room = rooms[state.room]; + const exit = room.exits.find((candidate) => candidate.x === nx && candidate.y === ny); + if (exit) { + state.room = exit.to; + state.player.x = exit.px; + state.player.y = exit.py; + say("Narrator", `You enter ${rooms[state.room].name}.`); + autosave(); + return; + } + if (nx < 0 || ny < 0 || nx > 23 || ny > 15 || room.walls.has(`${nx},${ny}`) || npcAt(nx, ny)) return; + state.player.x = nx; + state.player.y = ny; + const item = itemAt(nx, ny); + if (item) take(item); + } + + function act() { + if (!running) return start(); + if (state.ending) return; + const front = inFront(); + const npc = npcAt(front.x, front.y); + if (npc) talk(npc); + else { + const here = itemAt(state.player.x, state.player.y); + if (here) take(here); + else say("Narrator", "Dust moves in the light. Nothing asks to be changed here."); + } + autosave(); + } + + function inFront() { + const delta = { up: [0, -1], down: [0, 1], left: [-1, 0], right: [1, 0] }[state.player.facing] || [0, 1]; + return { x: state.player.x + delta[0], y: state.player.y + delta[1] }; + } + + function itemAt(x, y) { + return Object.keys(items).find((id) => { + const item = items[id]; + return item.room === state.room && item.x === x && item.y === y && !state.inventory.includes(id); + }); + } + + function npcAt(x, y) { + return Object.keys(npcs).find((id) => { + const npc = npcs[id]; + return npc.room === state.room && npc.x === x && npc.y === y; + }); + } + + function take(id) { + state.inventory.push(id); + if (id === "lamp") state.route = "Gentle"; + if (id === "key" && !state.flags.shadeHelped) state.route = "Power"; + say("Found", `${items[id].label} joined your inventory.`); + } + + function talk(id) { + if (id === "archivist") { + if (!state.inventory.includes("lamp")) say("Archivist", "Take the lamp from the lower desk. The stacks dislike being crossed in the dark."); + else if (!state.flags.shadeHelped) say("Archivist", "A loose page has gone missing. The quiet reader in the stacks knows where it belongs."); + else say("Archivist", "You have been kind to a forgotten page. The garden door will remember that."); + } + if (id === "shade") { + if (!state.inventory.includes("page")) say("Shy Shade", "I lost the page with my name on it. It fell somewhere nearby."); + else { + state.flags.shadeHelped = true; + state.inventory = state.inventory.filter((item) => item !== "page"); + state.route = "Mercy"; + say("Shy Shade", "You returned my page instead of keeping it. Take the honest route through the garden."); + } + } + if (id === "gate") { + if (state.flags.shadeHelped) end("mercy"); + else if (state.inventory.includes("key")) end("power"); + else end("quiet"); + } + } + + function end(kind) { + state.ending = kind; + state.route = kind === "mercy" ? "Mercy" : kind === "power" ? "Power" : "Quiet"; + say("Ending", endingLine()); + save(); + } + + function endingLine() { + if (state.ending === "mercy") return "Mercy ending: the iron door opens without a sound, and every returned page remembers your name."; + if (state.ending === "power") return "Power ending: the key turns, but the archive grows colder around the missing page."; + if (state.ending === "quiet") return "Quiet ending: you leave the locked door alone. Some mysteries stay intact."; + return state.message.line; + } + + function say(speaker, line) { + state.message = { speaker, line }; + } + + function start() { + running = true; + say("Archivist", "Walk the archive. Speak gently, or take what you need. The route will notice."); + els.status.textContent = "Started. Progress autosaves after room changes and actions."; + } + + async function save() { + localStorage.setItem(fallbackKey, JSON.stringify(state)); + try { + const response = await fetch(`/api/play/rpg/save/${saveSlot}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ payload: state }) + }); + if (!response.ok) throw new Error(`Save failed: ${response.status}`); + els.status.textContent = "Saved to backend."; + } catch (_error) { + els.status.textContent = "Saved locally. Backend save API was not reachable."; + } + } + + async function load() { + try { + const response = await fetch(`/api/play/rpg/save/${saveSlot}`); + if (response.ok) { + const data = await response.json(); + state = normalizeState(data.payload); + running = true; + els.status.textContent = "Loaded from backend."; + return; + } + } catch (_error) { + /* Fall through to local save. */ + } + const local = localStorage.getItem(fallbackKey); + if (local) { + state = normalizeState(JSON.parse(local)); + running = true; + els.status.textContent = "Loaded local save."; + } else { + els.status.textContent = "No save found."; + } + } + + function normalizeState(candidate) { + return Object.assign(freshState(), candidate || {}, { + player: Object.assign(freshState().player, (candidate && candidate.player) || {}), + inventory: Array.isArray(candidate && candidate.inventory) ? candidate.inventory.filter((id) => items[id]) : [], + flags: Object.assign({}, (candidate && candidate.flags) || {}), + message: Object.assign(freshState().message, (candidate && candidate.message) || {}) + }); + } + + function autosave() { + save(); + } + + $("[data-rpg-start]", root).addEventListener("click", start); + $("[data-rpg-save]", root).addEventListener("click", save); + $("[data-rpg-load]", root).addEventListener("click", load); + $("[data-rpg-reset]", root).addEventListener("click", async () => { + state = freshState(); + running = false; + localStorage.removeItem(fallbackKey); + try { + await fetch(`/api/play/rpg/save/${saveSlot}`, { method: "DELETE" }); + els.status.textContent = "Reset and cleared backend save."; + } catch (_error) { + els.status.textContent = "Reset locally. Backend save API was not reachable."; + } + }); + $("[data-rpg-act]", root).addEventListener("click", act); + $$("[data-rpg-move]", root).forEach((button) => { + const moves = { up: [0, -1, "up"], down: [0, 1, "down"], left: [-1, 0, "left"], right: [1, 0, "right"] }; + button.addEventListener("click", () => move(...moves[button.dataset.rpgMove])); + }); + window.addEventListener("keydown", (event) => { + if (!root.isConnected) return; + const tag = event.target.tagName; + if (tag === "INPUT" || tag === "TEXTAREA") return; + const keys = { + ArrowUp: [0, -1, "up"], w: [0, -1, "up"], + ArrowDown: [0, 1, "down"], s: [0, 1, "down"], + ArrowLeft: [-1, 0, "left"], a: [-1, 0, "left"], + ArrowRight: [1, 0, "right"], d: [1, 0, "right"] + }; + if (keys[event.key]) { + event.preventDefault(); + move(...keys[event.key]); + } + if (event.key === " " || event.key === "Enter") { + event.preventDefault(); + act(); + } + }); + draw(); + load(); + } + + function setupSortable(container, _render, after) { + let dragged = null; + container.addEventListener("dragstart", (event) => { + dragged = event.target.closest("[draggable='true']"); + if (dragged) event.dataTransfer.effectAllowed = "move"; + }); + container.addEventListener("dragover", (event) => { + event.preventDefault(); + const target = event.target.closest("[draggable='true']"); + if (!dragged || !target || target === dragged) return; + const rect = target.getBoundingClientRect(); + const before = event.clientY < rect.top + rect.height / 2 || event.clientX < rect.left + rect.width / 2; + container.insertBefore(dragged, before ? target : target.nextSibling); + after(); + }); + container.addEventListener("dragend", () => { + dragged = null; + after(); + }); + } + + function pick(items) { + return items[Math.floor(Math.random() * items.length)]; + } +})(); diff --git a/assets/styles/play.css b/assets/styles/play.css new file mode 100644 index 0000000..5307e3a --- /dev/null +++ b/assets/styles/play.css @@ -0,0 +1,646 @@ +.play-root { + --play-ink: #2a2118; + --play-paper: color-mix(in oklab, var(--surface) 86%, #f4ead2 14%); + --play-brass: #9c6b2f; + --play-green: #486b57; + --play-red: #8a3f3f; + --play-blue: #3e5f8a; + max-width: 1100px; + margin: 2rem auto 4rem; + padding: 0 1rem; +} + +.play-root * { + box-sizing: border-box; +} + +.play-root.is-odd { + filter: saturate(1.08) contrast(1.02); +} + +.play-hero, +.play-console, +.play-tool { + border: 1px solid var(--border); + border-radius: 8px; + background: var(--play-paper); + box-shadow: 0 18px 45px rgba(35, 26, 16, 0.08); +} + +.play-hero { + display: grid; + grid-template-columns: minmax(0, 1.2fr) minmax(280px, 0.8fr); + align-items: center; + gap: 2rem; + padding: clamp(1.25rem, 4vw, 3rem); + overflow: hidden; +} + +.play-kicker { + margin: 0 0 0.45rem; + color: var(--play-brass); + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.play-hero h1, +.play-page-head h1 { + margin: 0; + color: var(--heading); +} + +.play-lede, +.play-page-head p { + max-width: 62ch; + margin-top: 0.75rem; + color: var(--muted); +} + +.play-orbit { + position: relative; + aspect-ratio: 1; + min-height: 300px; + border: 1px dashed color-mix(in oklab, var(--border) 70%, var(--play-brass)); + border-radius: 50%; + background: + linear-gradient(90deg, transparent 49.8%, color-mix(in oklab, var(--border) 70%, transparent) 50%, transparent 50.2%), + linear-gradient(0deg, transparent 49.8%, color-mix(in oklab, var(--border) 70%, transparent) 50%, transparent 50.2%); +} + +.play-orbit::after { + content: "PLAY"; + position: absolute; + inset: 35%; + display: grid; + place-items: center; + border-radius: 50%; + border: 1px solid var(--border); + color: var(--muted); + font-weight: 800; + letter-spacing: 0.16em; +} + +.play-orbit__node { + position: absolute; + width: 42px; + height: 42px; + border: 1px solid var(--border); + border-radius: 50%; + background: var(--surface); + color: var(--fg); + cursor: pointer; + transform: translate(-50%, -50%); +} + +.play-orbit__node.is-active, +.play-orbit__node:hover { + background: var(--play-brass); + color: #fff8e8; +} + +.play-console { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 1rem; + margin: 1rem 0; + padding: 1rem; +} + +.play-console__screen { + min-height: 190px; + padding: 1.25rem; + border-radius: 6px; + background: + repeating-linear-gradient(0deg, rgba(0, 0, 0, 0.025), rgba(0, 0, 0, 0.025) 1px, transparent 1px, transparent 7px), + color-mix(in oklab, var(--surface) 92%, var(--play-green) 8%); +} + +.play-console__screen h2 { + margin: 0.2rem 0 0.5rem; +} + +.play-whisper, +.play-live-note { + margin: 0.7rem 0 0; + color: var(--muted); + font-size: 0.92rem; + font-style: italic; +} + +.play-console__eyebrow { + margin: 0; + color: var(--play-green); + font-weight: 800; + text-transform: uppercase; +} + +.play-console__controls { + display: flex; + flex-wrap: wrap; + gap: 0.6rem; + align-content: start; +} + +.play-root button, +.play-primary, +.sigil-form button { + min-height: 42px; + border: 1px solid color-mix(in oklab, var(--border) 70%, var(--play-brass)); + border-radius: 6px; + padding: 0.55rem 0.85rem; + background: var(--surface); + color: var(--fg); + font: inherit; + cursor: pointer; +} + +.play-root button:hover, +.play-primary:hover, +.sigil-form button:hover { + background: var(--play-ink); + color: #fff8e8; + text-decoration: none; +} + +.play-primary { + display: inline-flex; + align-items: center; + justify-content: center; + margin-top: 0.5rem; + background: var(--play-brass); + color: #fff8e8; + font-weight: 800; +} + +.play-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); + gap: 0.8rem; +} + +.play-grid a { + min-height: 135px; + padding: 1rem; + border: 1px solid var(--border); + border-radius: 8px; + background: color-mix(in oklab, var(--surface) 88%, var(--play-paper) 12%); + color: var(--fg); + transition: transform 160ms ease, border-color 160ms ease, background 160ms ease; +} + +.play-grid a:hover, +.play-grid a.is-active { + transform: translateY(-3px); + border-color: var(--play-brass); + background: color-mix(in oklab, var(--surface) 76%, var(--play-brass) 24%); + text-decoration: none; +} + +.play-grid span { + display: block; + color: var(--muted); + font-size: 0.8rem; + font-weight: 800; +} + +.play-grid strong { + display: block; + margin-top: 2rem; + color: var(--heading); + font-size: 1.05rem; +} + +.play-back { + display: inline-flex; + margin-bottom: 1rem; + color: var(--muted); +} + +.play-page-head { + margin-bottom: 1rem; +} + +.play-tool { + padding: 1rem; +} + +.play-scorebar { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 0.75rem; + margin-bottom: 1rem; + color: var(--muted); +} + +.memory-board { + display: grid; + grid-template-columns: repeat(4, minmax(72px, 1fr)); + gap: 0.7rem; +} + +.memory-card { + aspect-ratio: 1.08; + border: 1px solid var(--border); + border-radius: 8px; + background: + radial-gradient(circle at 50% 38%, rgba(246, 216, 137, 0.15), transparent 28%), + linear-gradient(135deg, #211812, var(--play-ink)); + color: #e7c978; + font-size: clamp(1.3rem, 4vw, 2.2rem); + font-weight: 900; + transform: rotate(var(--tilt, 0deg)); + transition: transform 180ms ease, background 180ms ease, color 180ms ease; +} + +.memory-card.is-open, +.memory-card.is-matched, +.memory-card.is-peeking { + background: color-mix(in oklab, var(--surface) 72%, var(--play-green) 28%); + color: var(--fg); + transform: rotate(0deg) translateY(-2px); +} + +.memory-card.is-matched { + outline: 2px solid color-mix(in oklab, var(--play-brass) 70%, transparent); +} + +.play-canvas, +.sigil-canvas { + display: block; + width: 100%; + max-height: 70vh; + border: 1px solid var(--border); + border-radius: 8px; + background: #18130f; +} + +.poem-tool { + display: grid; + gap: 1rem; +} + +.poem-slip { + min-height: 220px; + padding: 1.2rem; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--surface); + font-family: Georgia, "Times New Roman", serif; + font-size: 1.25rem; + line-height: 1.7; +} + +.poem-slip[data-stamp]:not([data-stamp=""])::after { + content: attr(data-stamp); + display: inline-block; + margin-top: 1rem; + padding: 0.2rem 0.5rem; + border: 2px solid var(--play-red); + color: var(--play-red); + font-size: 0.75rem; + font-weight: 800; + text-transform: uppercase; + transform: rotate(-4deg); +} + +.bookshelf, +.timeline-list { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + align-items: stretch; +} + +.book-spine, +.timeline-card { + touch-action: none; + border: 1px solid var(--border); + border-radius: 7px; + padding: 0.8rem; + background: var(--surface); + cursor: grab; + user-select: none; +} + +.book-spine { + width: 120px; + min-height: 230px; + writing-mode: vertical-rl; + text-orientation: mixed; + color: #fff8e8; + font-weight: 800; +} + +.book-spine:nth-child(3n+1) { background: var(--play-red); } +.book-spine:nth-child(3n+2) { background: var(--play-green); } +.book-spine:nth-child(3n+3) { background: var(--play-blue); } + +.booklet { + flex: 1 1 220px; + min-height: 110px; + padding: 0.9rem; + border: 1px dashed var(--play-brass); + border-radius: 7px; + background: color-mix(in oklab, var(--surface) 74%, var(--play-brass) 26%); + font-family: Georgia, "Times New Roman", serif; + font-weight: 700; +} + +.timeline-list { + flex-direction: column; +} + +.timeline-card { + display: grid; + grid-template-columns: 5rem 1fr; + gap: 1rem; +} + +.timeline-card span { + color: var(--play-brass); + font-weight: 900; +} + +.recipe-tool { + display: grid; + grid-template-columns: minmax(180px, 280px) minmax(0, 1fr); + gap: 1rem; + align-items: center; +} + +.recipe-wheel { + aspect-ratio: 1; + width: 100%; + border-radius: 50% !important; + background: + conic-gradient(from 0deg, var(--play-red), var(--play-brass), var(--play-green), var(--play-blue), var(--play-red)); + color: #fff8e8 !important; + font-weight: 900; + transition: transform 900ms cubic-bezier(.18, .88, .2, 1.15); +} + +.recipe-result { + display: grid; + gap: 0.7rem; + padding: 0; + margin: 0; + list-style: none; +} + +.recipe-result li { + padding: 0.85rem; + border: 1px solid var(--border); + border-radius: 7px; + background: var(--surface); +} + +.terminal-tool { + background: #15120e; + color: #f2e3bd; +} + +.terminal-log { + min-height: 310px; + max-height: 460px; + overflow: auto; + padding: 1rem; + border: 1px solid rgba(242, 227, 189, 0.2); + border-radius: 7px; + font-family: ui-monospace, SFMono-Regular, Consolas, monospace; + white-space: pre-wrap; +} + +.terminal-form { + display: flex; + gap: 0.6rem; + margin-top: 0.8rem; +} + +.terminal-form label { + display: flex; + flex: 1; + align-items: center; + gap: 0.5rem; +} + +.terminal-form input, +.sigil-form input { + width: 100%; + border: 1px solid var(--border); + border-radius: 6px; + padding: 0.65rem; + background: var(--surface); + color: var(--fg); + font: inherit; +} + +.study-tool { + display: grid; + justify-items: center; + gap: 1rem; +} + +.study-scene { + position: relative; + width: min(100%, 520px); + aspect-ratio: 1.6; + border: 1px solid var(--border); + border-radius: 8px; + background: + radial-gradient(circle at 50% 22%, rgba(244, 210, 113, var(--lamp, 0.18)), transparent 30%), + linear-gradient(#27211b 0 62%, #4b3425 62% 100%); + overflow: hidden; +} + +.study-scene span { + position: absolute; + left: 42%; + bottom: 30%; + width: 16%; + height: 38%; + border-radius: 50% 50% 6px 6px; + background: #b88639; + box-shadow: 0 0 calc(20px + var(--glow, 0px)) rgba(244, 210, 113, 0.7); +} + +.study-time { + font-size: clamp(2.4rem, 8vw, 5rem); + font-weight: 900; + font-variant-numeric: tabular-nums; +} + +.sigil-tool { + display: grid; + grid-template-columns: minmax(240px, 320px) minmax(0, 520px); + gap: 1rem; + align-items: start; +} + +.sigil-form { + display: grid; + gap: 0.85rem; +} + +.sigil-form label { + display: grid; + gap: 0.35rem; + color: var(--muted); +} + +.sigil-canvas { + background: var(--surface); +} + +.rpg-shell { + display: grid; + grid-template-columns: minmax(0, 768px) minmax(260px, 1fr); + gap: 1rem; + align-items: start; +} + +.rpg-stage { + display: grid; + gap: 0.75rem; +} + +.rpg-canvas { + display: block; + width: 100%; + aspect-ratio: 1.5; + image-rendering: pixelated; + border: 2px solid #18130f; + border-radius: 8px; + background: #14110e; +} + +.rpg-dialogue, +.rpg-panel { + border: 1px solid var(--border); + border-radius: 8px; + background: color-mix(in oklab, var(--surface) 86%, #18130f 14%); +} + +.rpg-dialogue { + min-height: 118px; + padding: 0.9rem; +} + +.rpg-dialogue strong, +.rpg-list h2 { + color: var(--heading); +} + +.rpg-dialogue p, +.rpg-status { + margin: 0.35rem 0 0; + color: var(--muted); +} + +.rpg-panel { + display: grid; + gap: 1rem; + padding: 1rem; +} + +.rpg-actions, +.rpg-mobile-pad { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.5rem; +} + +.rpg-mobile-pad { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.rpg-mobile-pad [data-rpg-move="up"], +.rpg-mobile-pad [data-rpg-move="down"] { + grid-column: 2; +} + +.rpg-mobile-pad [data-rpg-move="left"] { + grid-column: 1; +} + +.rpg-mobile-pad [data-rpg-act] { + grid-column: 2; +} + +.rpg-mobile-pad [data-rpg-move="right"] { + grid-column: 3; +} + +.rpg-stats { + display: grid; + gap: 0.55rem; + margin: 0; +} + +.rpg-stats div { + display: flex; + justify-content: space-between; + gap: 1rem; + padding-bottom: 0.45rem; + border-bottom: 1px solid var(--border); +} + +.rpg-stats dt, +.rpg-list h2 { + margin: 0; + color: var(--muted); + font-size: 0.78rem; + font-weight: 800; + text-transform: uppercase; +} + +.rpg-stats dd { + margin: 0; + font-weight: 800; +} + +.rpg-list ul { + display: grid; + gap: 0.45rem; + margin: 0.45rem 0 0; + padding: 0; + list-style: none; +} + +.rpg-list li { + padding: 0.55rem; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--surface); +} + +.rpg-list li.is-done { + border-color: color-mix(in oklab, var(--play-green) 70%, var(--border)); + background: color-mix(in oklab, var(--surface) 72%, var(--play-green) 28%); +} + +@media (max-width: 760px) { + .play-hero, + .play-console, + .recipe-tool, + .sigil-tool, + .rpg-shell { + grid-template-columns: 1fr; + } + + .play-orbit { + min-height: 260px; + } + + .memory-board { + grid-template-columns: repeat(3, minmax(64px, 1fr)); + } + + .book-spine { + width: calc(50% - 0.4rem); + min-height: 160px; + } +} diff --git a/build-site.el b/build-site.el index 368a529..c78ddcf 100755 --- a/build-site.el +++ b/build-site.el @@ -204,6 +204,7 @@ FMT / ARGS are passed to `format'." "media.css" "wird-tracker.css" "zhd.css" + "play.css" ) "\n") "\n" @@ -223,6 +224,7 @@ FMT / ARGS are passed to `format'." "sitemap-interactive.js" "wird-tracker.js" "zhd.js" + "play.js" ) "\n") (format " @@ -282,6 +284,7 @@ FMT / ARGS are passed to `format'." Home Blogs Career + Play Dashboard
    diff --git a/index.org b/index.org index c0544fe..c024a8b 100755 --- a/index.org +++ b/index.org @@ -67,6 +67,11 @@ Sitemap map + + + Play + lab + Status diff --git a/play.org b/play.org new file mode 100644 index 0000000..eb2b044 --- /dev/null +++ b/play.org @@ -0,0 +1,57 @@ +#+TITLE: Play +#+OPTIONS: num:nil title:nil toc:nil +#+NO_SIDENOTES: t +#+DATE: <2026-05-09 Sat> + +#+BEGIN_EXPORT html +
    +
    +
    +

    Interactive wing

    +

    Play

    +

    A small cabinet of browser toys, puzzles, sketch surfaces, and quiet experiments.

    +
    + +
    + +
    + +
    + + + +
    +
    + + +
    +#+END_EXPORT diff --git a/play/bookshelf.org b/play/bookshelf.org new file mode 100644 index 0000000..3f34919 --- /dev/null +++ b/play/bookshelf.org @@ -0,0 +1,22 @@ +#+TITLE: Bookshelf Sort +#+OPTIONS: num:nil title:nil toc:nil +#+NO_SIDENOTES: t +#+DATE: <2026-05-09 Sat> + +#+BEGIN_EXPORT html +
    + Back to Play +
    +

    04 / Puzzle

    +

    Bookshelf Sort

    +

    Drag the books into alphabetical order by title.

    +
    +
    +
    + Unsorted + +
    +
    +
    +
    +#+END_EXPORT diff --git a/play/constellation.org b/play/constellation.org new file mode 100644 index 0000000..0883e16 --- /dev/null +++ b/play/constellation.org @@ -0,0 +1,22 @@ +#+TITLE: Constellation Desk +#+OPTIONS: num:nil title:nil toc:nil +#+NO_SIDENOTES: t +#+DATE: <2026-05-09 Sat> + +#+BEGIN_EXPORT html +
    + Back to Play +
    +

    02 / Sketch

    +

    Constellation Desk

    +

    Click stars to connect them. Reset the plate when the sky becomes too busy.

    +
    +
    +
    + Lines: 0 + +
    + +
    +
    +#+END_EXPORT diff --git a/play/ink.org b/play/ink.org new file mode 100644 index 0000000..dad155b --- /dev/null +++ b/play/ink.org @@ -0,0 +1,18 @@ +#+TITLE: Ink Pond +#+OPTIONS: num:nil title:nil toc:nil +#+NO_SIDENOTES: t +#+DATE: <2026-05-09 Sat> + +#+BEGIN_EXPORT html +
    + Back to Play +
    +

    07 / Canvas

    +

    Ink Pond

    +

    Move across the page to stir the pond. Click to drop a darker blot.

    +
    +
    + +
    +
    +#+END_EXPORT diff --git a/play/memory.org b/play/memory.org new file mode 100644 index 0000000..6819bcb --- /dev/null +++ b/play/memory.org @@ -0,0 +1,25 @@ +#+TITLE: Memory Cabinet +#+OPTIONS: num:nil title:nil toc:nil +#+NO_SIDENOTES: t +#+DATE: <2026-05-09 Sat> + +#+BEGIN_EXPORT html +
    + Back to Play +
    +

    01 / Game

    +

    Memory Cabinet

    +

    Match six pairs of archive cards. Fewer turns is better.

    +
    +
    +
    + Turns: 0 + Matches: 0/6 + Cards are shuffled. + + +
    +
    +
    +
    +#+END_EXPORT diff --git a/play/poem.org b/play/poem.org new file mode 100644 index 0000000..5514c12 --- /dev/null +++ b/play/poem.org @@ -0,0 +1,19 @@ +#+TITLE: Marginalia Machine +#+OPTIONS: num:nil title:nil toc:nil +#+NO_SIDENOTES: t +#+DATE: <2026-05-09 Sat> + +#+BEGIN_EXPORT html +
    + Back to Play +
    +

    03 / Generator

    +

    Marginalia Machine

    +

    Press the lever to assemble tiny notes from the edge of an imaginary notebook.

    +
    +
    + +
    +
    +
    +#+END_EXPORT diff --git a/play/recipe.org b/play/recipe.org new file mode 100644 index 0000000..a384968 --- /dev/null +++ b/play/recipe.org @@ -0,0 +1,19 @@ +#+TITLE: Supper Wheel +#+OPTIONS: num:nil title:nil toc:nil +#+NO_SIDENOTES: t +#+DATE: <2026-05-09 Sat> + +#+BEGIN_EXPORT html +
    + Back to Play +
    +

    05 / Spinner

    +

    Supper Wheel

    +

    Spin for a cheerful menu: base, main, side, and table note.

    +
    +
    + +
      +
      +
      +#+END_EXPORT diff --git a/play/rpg.org b/play/rpg.org new file mode 100644 index 0000000..d53feaa --- /dev/null +++ b/play/rpg.org @@ -0,0 +1,60 @@ +#+TITLE: Hollow Archive +#+OPTIONS: num:nil title:nil toc:nil +#+NO_SIDENOTES: t +#+DATE: <2026-05-09 Sat> + +#+BEGIN_EXPORT html +
      + Back to Play +
      +

      11 / Role Playing Game

      +

      Hollow Archive

      +

      A tiny 2D RPG base: move through rooms, speak with characters, collect items, complete quests, save progress, and steer the ending.

      +
      + +
      +
      + +
      + Archivist +

      Press Start to enter the archive.

      +
      +
      + + +
      +
      +#+END_EXPORT diff --git a/play/sigil.org b/play/sigil.org new file mode 100644 index 0000000..3823391 --- /dev/null +++ b/play/sigil.org @@ -0,0 +1,23 @@ +#+TITLE: Sigil Press +#+OPTIONS: num:nil title:nil toc:nil +#+NO_SIDENOTES: t +#+DATE: <2026-05-09 Sat> + +#+BEGIN_EXPORT html +
      + Back to Play +
      +

      10 / Maker

      +

      Sigil Press

      +

      Enter initials and a motto to stamp a little academic seal.

      +
      +
      +
      + + + +
      + +
      +
      +#+END_EXPORT diff --git a/play/study.org b/play/study.org new file mode 100644 index 0000000..e6d9ab5 --- /dev/null +++ b/play/study.org @@ -0,0 +1,24 @@ +#+TITLE: Study Lamp +#+OPTIONS: num:nil title:nil toc:nil +#+NO_SIDENOTES: t +#+DATE: <2026-05-09 Sat> + +#+BEGIN_EXPORT html +
      + Back to Play +
      +

      09 / Timer

      +

      Study Lamp

      +

      Start a short focus session and let the desk scene warm up.

      +
      +
      +
      +
      05:00
      +
      + + + +
      +
      +
      +#+END_EXPORT diff --git a/play/terminal.org b/play/terminal.org new file mode 100644 index 0000000..32ffd76 --- /dev/null +++ b/play/terminal.org @@ -0,0 +1,22 @@ +#+TITLE: Archive Terminal +#+OPTIONS: num:nil title:nil toc:nil +#+NO_SIDENOTES: t +#+DATE: <2026-05-09 Sat> + +#+BEGIN_EXPORT html +
      + Back to Play +
      +

      08 / Story

      +

      Archive Terminal

      +

      Try commands such as help, look, open drawer, read note, map, hum, save, and clear.

      +
      +
      +
      +
      + + +
      +
      +
      +#+END_EXPORT diff --git a/play/timeline.org b/play/timeline.org new file mode 100644 index 0000000..a3e81e7 --- /dev/null +++ b/play/timeline.org @@ -0,0 +1,22 @@ +#+TITLE: Timeline Tangle +#+OPTIONS: num:nil title:nil toc:nil +#+NO_SIDENOTES: t +#+DATE: <2026-05-09 Sat> + +#+BEGIN_EXPORT html +
      + Back to Play +
      +

      06 / Puzzle

      +

      Timeline Tangle

      +

      Drag the cards from earliest to latest.

      +
      +
      +
      + Arrange the cards + +
      +
      +
      +
      +#+END_EXPORT diff --git a/posts/posts-list.org b/posts/posts-list.org index a1cadfb..b08b4cd 100755 --- a/posts/posts-list.org +++ b/posts/posts-list.org @@ -4,7 +4,7 @@ See the categories: @@html:Categories@@ * Posts: -- [[file:career/career-list.org][Career List]] @@html:@@ +- [[file:career/career-list.org][Career List]] @@html:@@ - [[file:career/ha-dr.org][High Availability, Disaster Recovery and Business Continuity]] @@html:@@ @@html:@@ @@html:@@ - [[file:career/javascript.org][Understands the Javascript language]] @@html:@@ @@html:@@ @@html:@@ - [[file:career/restful-api.org][Restful API]] @@html:@@ @@html:@@ @@html:@@ diff --git a/recently-updated.org b/recently-updated.org index a5f2f7f..345ba03 100755 --- a/recently-updated.org +++ b/recently-updated.org @@ -10,21 +10,21 @@ - [[file:home/guide/setup.org][Setup]] @@html:@@ - [[file:home/contact.org][Contact]] @@html:@@ - [[file:blogs/2026/05-may/authoring-service-09-05-26.org][Authoring Service]] @@html:@@ +- [[file:play.org][Play]] @@html:@@ +- [[file:play/timeline.org][Timeline Tangle]] @@html:@@ +- [[file:play/terminal.org][Archive Terminal]] @@html:@@ +- [[file:play/study.org][Study Lamp]] @@html:@@ +- [[file:play/sigil.org][Sigil Press]] @@html:@@ +- [[file:play/rpg.org][Hollow Archive]] @@html:@@ +- [[file:play/recipe.org][Supper Wheel]] @@html:@@ +- [[file:play/poem.org][Marginalia Machine]] @@html:@@ +- [[file:play/memory.org][Memory Cabinet]] @@html:@@ +- [[file:play/ink.org][Ink Pond]] @@html:@@ +- [[file:play/constellation.org][Constellation Desk]] @@html:@@ +- [[file:play/bookshelf.org][Bookshelf Sort]] @@html:@@ - [[file:blogs/2026/05-may/ai-datacamp-08-05.org][AI Datacamp]] @@html:@@ - [[file:blogs/2026/05-may/using-codex-07-05-26.org][Using Codex]] @@html:@@ - [[file:blogs/2026/05-may/03-05-week-review.org][[03-05-2026] - Weekly Review]] @@html:@@ - [[file:blogs/2026/05-may/sun-soak-01-05.org][Soaking in the sun]] @@html:@@ - [[file:blogs/2026/04-april/wise-words-29-04.org][Wise Words I need to engrain]] @@html:@@ - [[file:blogs/2026/04-april/april-almost-over-28-04.org][April is almost over...]] @@html:@@ -- [[file:blogs/2026/04-april/26-04-week-review.org][[26-04-2026] - Weekly Review]] @@html:@@ -- [[file:blogs/2026/04-april/adding-more-wird-22-04-26.org][Adding another Wird]] @@html:@@ -- [[file:blogs/2026/04-april/sitting-outside-in-the-sun-21-04-26.org][Sitting in the sun]] @@html:@@ -- [[file:blogs/2026/04-april/19-04-week-review.org][[19-04-2026] - Weekly Review]] @@html:@@ -- [[file:blogs/2026/04-april/ending-the-week-17-04-26.org][End of week thoughts]] @@html:@@ -- [[file:blogs/2026/04-april/16-04-26.org][Rambles]] @@html:@@ -- [[file:blogs/2026/04-april/comparison-14-04.org][Comparison is the thief of joy]] @@html:@@ -- [[file:blogs/2026/04-april/12-04-week-review.org][[12-04-2026] - Weekly Review]] @@html:@@ -- [[file:blogs/2026/04-april/starting-new-rotation-08-04-26.org][Starting new rotation]] @@html:@@ -- [[file:blogs/2026/04-april/action-plan-07-04-26.org][Action plan to change teams]] @@html:@@ -- [[file:blogs/2026/04-april/weekly-target-06-04-26.org][[06-04-2026] - Weekly target]] @@html:@@ -- [[file:blogs/2026/04-april/making-notes-through-org-noter-06-05.org][Making Notes through Org Noter]] @@html:@@ diff --git a/sitemap.org b/sitemap.org index 94407d0..f456950 100755 --- a/sitemap.org +++ b/sitemap.org @@ -1,6 +1,7 @@ #+TITLE: Sitemap - [[file:index.org][Home]] +- [[file:play.org][Play]] - [[file:wip.org][Work in progress]] - [[file:recently-updated.org][Recently Updated]] - blogs @@ -14,12 +15,12 @@ - home - [[file:home/countdown.org][Countdown]] - [[file:home/backlog.org][Backlog]] - - [[file:home/categories.org][Categories]] - [[file:home/contact.org][Contact]] - [[file:home/notes.org][Notes]] - [[file:home/services.org][Service]] - [[file:home/status.org][Competency Status Board]] - [[file:home/wird-tracker.org][Wird Tracker]] + - [[file:home/categories.org][Categories]] - guide - [[file:home/guide/setup.org][Setup]] - [[file:home/guide/wird-tracker-guide.org][Wird Tracker — Technical Guide]] @@ -49,17 +50,29 @@ - tags - [[file:tags/life.sync-conflict-20260417-233423-VT6366A.org][Tag: life]] - [[file:tags/review.sync-conflict-20260328-203248-VT6366A.org][Tag: review]] - - [[file:tags/introduction.org][Tag: introduction]] - [[file:tags/learning.org][Tag: learning]] + - [[file:tags/introduction.org][Tag: introduction]] - [[file:tags/notes.org][Tag: notes]] - [[file:tags/website.org][Tag: website]] - [[file:tags/review.org][Tag: review]] - - [[file:tags/update.org][Tag: update]] - [[file:tags/life.org][Tag: life]] - [[file:tags/emacs.org][Tag: emacs]] - [[file:tags/education.org][Tag: education]] + - [[file:tags/update.org][Tag: update]] - [[file:tags/reading.org][Tag: reading]] - - [[file:tags/maths.org][Tag: maths]] - [[file:tags/insights.org][Tag: insights]] + - [[file:tags/maths.org][Tag: maths]] - lima - - [[file:lima/lima-list.org][Lima]] \ No newline at end of file + - [[file:lima/lima-list.org][Lima]] +- play + - [[file:play/bookshelf.org][Bookshelf Sort]] + - [[file:play/sigil.org][Sigil Press]] + - [[file:play/memory.org][Memory Cabinet]] + - [[file:play/constellation.org][Constellation Desk]] + - [[file:play/study.org][Study Lamp]] + - [[file:play/poem.org][Marginalia Machine]] + - [[file:play/ink.org][Ink Pond]] + - [[file:play/timeline.org][Timeline Tangle]] + - [[file:play/recipe.org][Supper Wheel]] + - [[file:play/terminal.org][Archive Terminal]] + - [[file:play/rpg.org][Hollow Archive]] \ No newline at end of file