Files
dungeoncraft/www/dc_plugin.js
T
Richard ZinkandClaude Opus 5.5 925be17e82 Own XM/MOD replayer, audio mixer with crossfades, synthesised chip sound effects, music director
- src/xm.rs: FastTracker 2 XM and ProTracker MOD replayer (verified against libopenmpt)
- src/audio.rs: music players with crossfade + effect voices, rendered on demand
- src/sfx.rs: 29 synthesised chip sound effects
- www/dc_plugin.js: AudioContext started on first input, streams samples from WASM
- music follows the game: title, rotating build tracks, battle, gate assault, jingles
- macroquad audio removed; all tests run natively with cargo test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 06:10:28 +01:00

121 lines
4.8 KiB
JavaScript

// DungeonCraft browser plugin for miniquad: storage, file download/upload, save-on-leave, audio output.
(function () {
"use strict";
var pending = null; // value handed over by dc_storage_get
var file = null; // bytes of a file chosen via the file picker
var cache = {}; // key -> string, persisted to localStorage when the page is hidden/closed
function bytes(ptr, len) {
return new Uint8Array(wasm_memory.buffer, ptr, len);
}
function str(ptr, len) {
return new TextDecoder().decode(bytes(ptr, len));
}
function flushCache() {
for (var k in cache) {
try { window.localStorage.setItem(k, cache[k]); } catch (e) { }
}
}
window.addEventListener("pagehide", flushCache);
document.addEventListener("visibilitychange", function () {
if (document.visibilityState === "hidden") flushCache();
});
// hidden <input type=file> for loading savegames
var input = document.createElement("input");
input.type = "file";
input.accept = ".dcsave,.json,application/json";
input.style.display = "none";
input.addEventListener("change", function () {
var f = input.files && input.files[0];
if (!f) return;
f.arrayBuffer().then(function (buf) { file = new Uint8Array(buf); });
input.value = "";
});
document.addEventListener("DOMContentLoaded", function () { document.body.appendChild(input); });
function register(importObject) {
var env = importObject.env;
env.dc_storage_get = function (kp, kl) {
var v = null;
try { v = window.localStorage.getItem(str(kp, kl)); } catch (e) { }
if (v === null) { pending = null; return -1; }
pending = new TextEncoder().encode(v);
return pending.length;
};
env.dc_storage_take = function (ptr) {
if (pending === null) return;
bytes(ptr, pending.length).set(pending);
pending = null;
};
env.dc_storage_set = function (kp, kl, vp, vl) {
try { window.localStorage.setItem(str(kp, kl), str(vp, vl)); } catch (e) { }
};
env.dc_cache_set = function (kp, kl, vp, vl) {
cache[str(kp, kl)] = str(vp, vl);
};
env.dc_download = function (np, nl, dp, dl) {
var blob = new Blob([bytes(dp, dl).slice()], { type: "application/json" });
var a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = str(np, nl);
document.body.appendChild(a);
a.click();
setTimeout(function () { URL.revokeObjectURL(a.href); a.remove(); }, 1000);
};
env.dc_open_file = function () {
file = null;
input.click();
};
env.dc_file_poll = function () {
return file === null ? -1 : file.length;
};
env.dc_file_take = function (ptr) {
if (file === null) return;
bytes(ptr, file.length).set(file);
file = null;
};
}
// ---- audio: the Rust mixer renders on demand (dc_audio_render), started by the first user input
var actx = null;
var stats = { frames: 0, peak: 0 };
// diagnostics, e.g. for automated tests: state of the audio output
window.dc_audio_stats = function () {
return { state: actx ? actx.state : "not started", frames: stats.frames, peak: stats.peak };
};
function startAudio() {
if (typeof wasm_exports === "undefined" || !wasm_exports.dc_audio_render) return;
if (actx) {
if (actx.state !== "running") actx.resume();
return;
}
var AC = window.AudioContext || window.webkitAudioContext;
if (!AC) return;
actx = new AC();
wasm_exports.dc_audio_init(actx.sampleRate);
var node = actx.createScriptProcessor(2048, 0, 2);
node.onaudioprocess = function (e) {
var n = e.outputBuffer.length;
var l = e.outputBuffer.getChannelData(0);
var r = e.outputBuffer.getChannelData(1);
var ptr = wasm_exports.dc_audio_render(n);
if (!ptr) { l.fill(0); r.fill(0); return; }
var buf = new Float32Array(wasm_memory.buffer, ptr, n * 2);
var peak = 0;
for (var i = 0; i < n; i++) {
l[i] = buf[2 * i]; r[i] = buf[2 * i + 1];
var a = Math.abs(l[i]); if (a > peak) peak = a;
}
stats.frames += n;
stats.peak = Math.max(stats.peak * 0.9, peak);
};
node.connect(actx.destination);
}
["mousedown", "keydown", "touchstart"].forEach(function (ev) {
window.addEventListener(ev, startAudio, true);
});
miniquad_add_plugin({ register_plugin: register, version: 1, name: "dungeoncraft" });
})();