This commit is contained in:
@@ -138,9 +138,10 @@
|
|||||||
let keys = new Set();
|
let keys = new Set();
|
||||||
let last = performance.now();
|
let last = performance.now();
|
||||||
let saveBusy = false;
|
let saveBusy = false;
|
||||||
|
let backendAvailable = true;
|
||||||
|
|
||||||
bind();
|
bind();
|
||||||
load();
|
loadLocal();
|
||||||
requestAnimationFrame(frame);
|
requestAnimationFrame(frame);
|
||||||
|
|
||||||
function freshState() {
|
function freshState() {
|
||||||
@@ -193,8 +194,13 @@
|
|||||||
function bind() {
|
function bind() {
|
||||||
root.querySelector("[data-rpg-start]").addEventListener("click", start);
|
root.querySelector("[data-rpg-start]").addEventListener("click", start);
|
||||||
root.querySelector("[data-rpg-save]").addEventListener("click", save);
|
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-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.querySelector("[data-rpg-act]").addEventListener("click", act);
|
||||||
root.querySelectorAll("[data-rpg-move]").forEach((button) => {
|
root.querySelectorAll("[data-rpg-move]").forEach((button) => {
|
||||||
button.addEventListener("click", () => move(button.dataset.rpgMove));
|
button.addEventListener("click", () => move(button.dataset.rpgMove));
|
||||||
@@ -227,18 +233,25 @@
|
|||||||
async function newGame() {
|
async function newGame() {
|
||||||
state = freshState();
|
state = freshState();
|
||||||
localStorage.removeItem(fallbackKey);
|
localStorage.removeItem(fallbackKey);
|
||||||
try {
|
if (backendAvailable) {
|
||||||
await fetch(`/api/play/rpg/save/${slot}`, { method: "DELETE" });
|
try {
|
||||||
} catch (_error) {
|
const response = await fetch(`/api/play/rpg/save/${slot}`, { method: "DELETE" });
|
||||||
/* Local reset is enough when offline. */
|
if (response.status === 404) backendAvailable = false;
|
||||||
|
} catch (_error) {
|
||||||
|
backendAvailable = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
audio.setTrack("title");
|
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();
|
renderHud();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
localStorage.setItem(fallbackKey, JSON.stringify(state));
|
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;
|
saveBusy = true;
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/play/rpg/save/${slot}`, {
|
const response = await fetch(`/api/play/rpg/save/${slot}`, {
|
||||||
@@ -246,33 +259,58 @@
|
|||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ payload: state })
|
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) {
|
} catch (_error) {
|
||||||
|
backendAvailable = false;
|
||||||
els.status.textContent = "Saved locally. Backend save API was not reachable.";
|
els.status.textContent = "Saved locally. Backend save API was not reachable.";
|
||||||
} finally {
|
} finally {
|
||||||
saveBusy = false;
|
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 {
|
try {
|
||||||
const response = await fetch(`/api/play/rpg/save/${slot}`);
|
const response = await fetch(`/api/play/rpg/save/${slot}`);
|
||||||
if (response.ok) {
|
if (response.status === 404) {
|
||||||
const data = await response.json();
|
backendAvailable = false;
|
||||||
state = normalize(data.payload);
|
els.status.textContent = "No local save found. Backend save endpoint is not deployed yet.";
|
||||||
els.status.textContent = "Loaded from backend.";
|
|
||||||
audio.setTrack(state.started ? areas[state.area].track : "title");
|
|
||||||
return;
|
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) {
|
} 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);
|
const local = localStorage.getItem(fallbackKey);
|
||||||
if (local) {
|
if (local) {
|
||||||
state = normalize(JSON.parse(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");
|
audio.setTrack(state.started ? areas[state.area].track : "title");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalize(candidate) {
|
function normalize(candidate) {
|
||||||
@@ -637,11 +675,11 @@
|
|||||||
for (let y = 0; y < rows; y += 1) {
|
for (let y = 0; y < rows; y += 1) {
|
||||||
for (let x = 0; x < cols; x += 1) {
|
for (let x = 0; x < cols; x += 1) {
|
||||||
const wall = area.walls.has(`${x},${y}`);
|
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.fillStyle = wall ? "#0b0d10" : c;
|
||||||
ctx.fillRect(x * tile, y * tile, tile, tile);
|
ctx.fillRect(x * tile, y * tile, tile, tile);
|
||||||
if (!wall) {
|
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);
|
ctx.fillRect(x * tile, y * tile + 30, tile, 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -896,12 +934,20 @@
|
|||||||
void: [110, 98, 82, 73]
|
void: [110, 98, 82, 73]
|
||||||
};
|
};
|
||||||
function start() {
|
function start() {
|
||||||
if (ctx) return;
|
const Audio = window.AudioContext || window.webkitAudioContext;
|
||||||
ctx = new (window.AudioContext || window.webkitAudioContext)();
|
if (!Audio) return;
|
||||||
|
if (ctx) {
|
||||||
|
if (ctx.state === "suspended") ctx.resume();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctx = new Audio();
|
||||||
master = ctx.createGain();
|
master = ctx.createGain();
|
||||||
master.gain.value = 0.055;
|
master.gain.value = 0.14;
|
||||||
master.connect(ctx.destination);
|
master.connect(ctx.destination);
|
||||||
|
if (ctx.state === "suspended") ctx.resume();
|
||||||
loop();
|
loop();
|
||||||
|
const notes = patterns[track] || patterns.title;
|
||||||
|
tone(notes[0], 0.22, "triangle", 0.9);
|
||||||
}
|
}
|
||||||
function loop() {
|
function loop() {
|
||||||
clearInterval(timer);
|
clearInterval(timer);
|
||||||
@@ -929,12 +975,18 @@
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
start,
|
start,
|
||||||
|
supported() {
|
||||||
|
return Boolean(window.AudioContext || window.webkitAudioContext);
|
||||||
|
},
|
||||||
setTrack(next) {
|
setTrack(next) {
|
||||||
track = next;
|
track = next;
|
||||||
|
if (ctx && ctx.state === "suspended") ctx.resume();
|
||||||
if (ctx) loop();
|
if (ctx) loop();
|
||||||
|
if (ctx) tone((patterns[track] || patterns.title)[0], 0.16, track === "battle" ? "sawtooth" : "triangle", 0.65);
|
||||||
},
|
},
|
||||||
sfx(kind) {
|
sfx(kind) {
|
||||||
if (!ctx) return;
|
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 };
|
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);
|
tone(map[kind] || 330, kind === "forget" ? 0.8 : 0.18, kind === "forget" ? "sawtooth" : "square", kind === "hurt" ? 1.3 : 0.8);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
<button type="button" data-rpg-save>Save</button>
|
<button type="button" data-rpg-save>Save</button>
|
||||||
<button type="button" data-rpg-load>Load</button>
|
<button type="button" data-rpg-load>Load</button>
|
||||||
<button type="button" data-rpg-reset>New Game</button>
|
<button type="button" data-rpg-reset>New Game</button>
|
||||||
|
<button type="button" data-rpg-sound>Sound</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rpg-mobile-pad" aria-label="Movement controls">
|
<div class="rpg-mobile-pad" aria-label="Movement controls">
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
See the categories: @@html:<a href="../home/categories.html">Categories</a>@@
|
See the categories: @@html:<a href="../home/categories.html">Categories</a>@@
|
||||||
|
|
||||||
* Posts:
|
* Posts:
|
||||||
- [[file:career/career-list.org][Career List]] @@html:<span class="post-date">09-05-2026 22:36</span>@@
|
- [[file:career/career-list.org][Career List]] @@html:<span class="post-date">09-05-2026 22:50</span>@@
|
||||||
- [[file:career/ha-dr.org][High Availability, Disaster Recovery and Business Continuity]] @@html:<span class="post-date">11-03-2026 17:18</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
- [[file:career/ha-dr.org][High Availability, Disaster Recovery and Business Continuity]] @@html:<span class="post-date">11-03-2026 17:18</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
||||||
- [[file:career/javascript.org][Understands the Javascript language]] @@html:<span class="post-date">11-03-2026 16:52</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
- [[file:career/javascript.org][Understands the Javascript language]] @@html:<span class="post-date">11-03-2026 16:52</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
||||||
- [[file:career/restful-api.org][Restful API]] @@html:<span class="post-date">15-02-2026 23:00</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
- [[file:career/restful-api.org][Restful API]] @@html:<span class="post-date">15-02-2026 23:00</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
||||||
|
|||||||
@@ -50,15 +50,15 @@
|
|||||||
- tags
|
- tags
|
||||||
- [[file:tags/life.sync-conflict-20260417-233423-VT6366A.org][Tag: life]]
|
- [[file:tags/life.sync-conflict-20260417-233423-VT6366A.org][Tag: life]]
|
||||||
- [[file:tags/review.sync-conflict-20260328-203248-VT6366A.org][Tag: review]]
|
- [[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/learning.org][Tag: learning]]
|
||||||
|
- [[file:tags/introduction.org][Tag: introduction]]
|
||||||
- [[file:tags/notes.org][Tag: notes]]
|
- [[file:tags/notes.org][Tag: notes]]
|
||||||
- [[file:tags/website.org][Tag: website]]
|
- [[file:tags/website.org][Tag: website]]
|
||||||
- [[file:tags/review.org][Tag: review]]
|
- [[file:tags/review.org][Tag: review]]
|
||||||
|
- [[file:tags/life.org][Tag: life]]
|
||||||
- [[file:tags/emacs.org][Tag: emacs]]
|
- [[file:tags/emacs.org][Tag: emacs]]
|
||||||
- [[file:tags/education.org][Tag: education]]
|
- [[file:tags/education.org][Tag: education]]
|
||||||
- [[file:tags/update.org][Tag: update]]
|
- [[file:tags/update.org][Tag: update]]
|
||||||
- [[file:tags/life.org][Tag: life]]
|
|
||||||
- [[file:tags/reading.org][Tag: reading]]
|
- [[file:tags/reading.org][Tag: reading]]
|
||||||
- [[file:tags/maths.org][Tag: maths]]
|
- [[file:tags/maths.org][Tag: maths]]
|
||||||
- [[file:tags/insights.org][Tag: insights]]
|
- [[file:tags/insights.org][Tag: insights]]
|
||||||
|
|||||||
Reference in New Issue
Block a user