This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user