From be254d661dc19cd94ccdf0f3ee80193099355782 Mon Sep 17 00:00:00 2001 From: Zaine Arch Date: Sat, 9 May 2026 23:03:42 +0100 Subject: [PATCH] rpg fleshing out x2 --- assets/scripts/ash-below-lake.js | 94 +++++++++++++++++++++++++------- play/rpg.org | 1 + posts/posts-list.org | 2 +- sitemap.org | 4 +- 4 files changed, 77 insertions(+), 24 deletions(-) diff --git a/assets/scripts/ash-below-lake.js b/assets/scripts/ash-below-lake.js index 77da0bc..1baef82 100644 --- a/assets/scripts/ash-below-lake.js +++ b/assets/scripts/ash-below-lake.js @@ -138,9 +138,10 @@ let keys = new Set(); let last = performance.now(); let saveBusy = false; + let backendAvailable = true; bind(); - load(); + loadLocal(); requestAnimationFrame(frame); function freshState() { @@ -193,8 +194,13 @@ function bind() { root.querySelector("[data-rpg-start]").addEventListener("click", start); root.querySelector("[data-rpg-save]").addEventListener("click", save); - root.querySelector("[data-rpg-load]").addEventListener("click", load); + root.querySelector("[data-rpg-load]").addEventListener("click", () => load(true)); root.querySelector("[data-rpg-reset]").addEventListener("click", newGame); + root.querySelector("[data-rpg-sound]").addEventListener("click", () => { + audio.start(); + audio.setTrack(state.started ? areas[state.area].track : "title"); + els.status.textContent = audio.supported() ? "Sound on." : "This browser does not expose Web Audio."; + }); root.querySelector("[data-rpg-act]").addEventListener("click", act); root.querySelectorAll("[data-rpg-move]").forEach((button) => { button.addEventListener("click", () => move(button.dataset.rpgMove)); @@ -227,18 +233,25 @@ async function newGame() { state = freshState(); localStorage.removeItem(fallbackKey); - try { - await fetch(`/api/play/rpg/save/${slot}`, { method: "DELETE" }); - } catch (_error) { - /* Local reset is enough when offline. */ + if (backendAvailable) { + try { + const response = await fetch(`/api/play/rpg/save/${slot}`, { method: "DELETE" }); + if (response.status === 404) backendAvailable = false; + } catch (_error) { + backendAvailable = false; + } } audio.setTrack("title"); + els.status.textContent = backendAvailable ? "New game started and local save cleared." : "New game started locally. Backend save endpoint is not available."; renderHud(); } async function save() { localStorage.setItem(fallbackKey, JSON.stringify(state)); - if (saveBusy) return; + if (saveBusy || !backendAvailable) { + els.status.textContent = backendAvailable ? "Save already running." : "Saved locally. Backend save endpoint is not available."; + return; + } saveBusy = true; try { const response = await fetch(`/api/play/rpg/save/${slot}`, { @@ -246,33 +259,58 @@ headers: { "Content-Type": "application/json" }, body: JSON.stringify({ payload: state }) }); - els.status.textContent = response.ok ? "Saved to backend." : "Saved locally. Backend rejected the save."; + if (response.status === 404) { + backendAvailable = false; + els.status.textContent = "Saved locally. Backend save endpoint is not deployed yet."; + } else { + els.status.textContent = response.ok ? "Saved to backend." : "Saved locally. Backend rejected the save."; + } } catch (_error) { + backendAvailable = false; els.status.textContent = "Saved locally. Backend save API was not reachable."; } finally { saveBusy = false; } } - async function load() { + async function load(allowBackend) { + if (loadLocal("Loaded local save.")) return; + if (!allowBackend || !backendAvailable) { + els.status.textContent = backendAvailable ? "No local save found." : "No local save found. Backend save endpoint is not available."; + return; + } try { const response = await fetch(`/api/play/rpg/save/${slot}`); - if (response.ok) { - const data = await response.json(); - state = normalize(data.payload); - els.status.textContent = "Loaded from backend."; - audio.setTrack(state.started ? areas[state.area].track : "title"); + if (response.status === 404) { + backendAvailable = false; + els.status.textContent = "No local save found. Backend save endpoint is not deployed yet."; return; } + if (!response.ok) { + els.status.textContent = "No local save found. Backend did not return a save."; + return; + } + const data = await response.json(); + state = normalize(data.payload); + localStorage.setItem(fallbackKey, JSON.stringify(state)); + els.status.textContent = "Loaded from backend."; + audio.setTrack(state.started ? areas[state.area].track : "title"); } catch (_error) { - /* Fall through to localStorage. */ + backendAvailable = false; + els.status.textContent = "No local save found. Backend save API was not reachable."; } + } + + function loadLocal(message) { const local = localStorage.getItem(fallbackKey); if (local) { state = normalize(JSON.parse(local)); - els.status.textContent = "Loaded local save."; + if (message) els.status.textContent = message; + audio.setTrack(state.started ? areas[state.area].track : "title"); + return true; } audio.setTrack(state.started ? areas[state.area].track : "title"); + return false; } function normalize(candidate) { @@ -637,11 +675,11 @@ for (let y = 0; y < rows; y += 1) { for (let x = 0; x < cols; x += 1) { const wall = area.walls.has(`${x},${y}`); - const c = area.floor[(x + y + Math.floor(now / 800)) % area.floor.length]; + const c = area.floor[(x + y) % area.floor.length]; ctx.fillStyle = wall ? "#0b0d10" : c; ctx.fillRect(x * tile, y * tile, tile, tile); if (!wall) { - ctx.fillStyle = "rgba(255,255,255,0.025)"; + ctx.fillStyle = (x + y) % 2 === 0 ? "rgba(255,255,255,0.018)" : "rgba(0,0,0,0.035)"; ctx.fillRect(x * tile, y * tile + 30, tile, 2); } } @@ -896,12 +934,20 @@ void: [110, 98, 82, 73] }; function start() { - if (ctx) return; - ctx = new (window.AudioContext || window.webkitAudioContext)(); + const Audio = window.AudioContext || window.webkitAudioContext; + if (!Audio) return; + if (ctx) { + if (ctx.state === "suspended") ctx.resume(); + return; + } + ctx = new Audio(); master = ctx.createGain(); - master.gain.value = 0.055; + master.gain.value = 0.14; master.connect(ctx.destination); + if (ctx.state === "suspended") ctx.resume(); loop(); + const notes = patterns[track] || patterns.title; + tone(notes[0], 0.22, "triangle", 0.9); } function loop() { clearInterval(timer); @@ -929,12 +975,18 @@ } return { start, + supported() { + return Boolean(window.AudioContext || window.webkitAudioContext); + }, setTrack(next) { track = next; + if (ctx && ctx.state === "suspended") ctx.resume(); if (ctx) loop(); + if (ctx) tone((patterns[track] || patterns.title)[0], 0.16, track === "battle" ? "sawtooth" : "triangle", 0.65); }, sfx(kind) { if (!ctx) return; + if (ctx.state === "suspended") ctx.resume(); const map = { item: 660, talk: 440, battle: 130, hit: 180, listen: 523, forget: 96, hurt: 80 }; tone(map[kind] || 330, kind === "forget" ? 0.8 : 0.18, kind === "forget" ? "sawtooth" : "square", kind === "hurt" ? 1.3 : 0.8); } diff --git a/play/rpg.org b/play/rpg.org index d4cb983..1d771d3 100644 --- a/play/rpg.org +++ b/play/rpg.org @@ -27,6 +27,7 @@ +
diff --git a/posts/posts-list.org b/posts/posts-list.org index 5019c21..532eaad 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/sitemap.org b/sitemap.org index 4c8c206..95405a5 100755 --- a/sitemap.org +++ b/sitemap.org @@ -50,15 +50,15 @@ - 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/life.org][Tag: life]] - [[file:tags/emacs.org][Tag: emacs]] - [[file:tags/education.org][Tag: education]] - [[file:tags/update.org][Tag: update]] - - [[file:tags/life.org][Tag: life]] - [[file:tags/reading.org][Tag: reading]] - [[file:tags/maths.org][Tag: maths]] - [[file:tags/insights.org][Tag: insights]]