- touch.rs: gesture recognition (tap, double tap, long press, drag, two-finger pan/pinch/rotate) - Touch mode: Auto (touch device or first touch) / On / Off in Settings -> Game - Larger interface factor on touch screens, top bar wraps, smaller minimap, fewer messages - Toolbar grouped into categories with pop-up rows and a menu button, also on the desktop - Long press shows information or drops held units; double tap with the hand slaps - Savegame name via the browser's text dialog when the name field is tapped - Touch help texts (EN/DE) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
186 lines
7.6 KiB
JavaScript
186 lines
7.6 KiB
JavaScript
// DungeonCraft browser plugin for miniquad: storage, file download/upload, save-on-leave,
|
|
// HTTP requests (savegame service), 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
|
|
var http = {}; // request id -> { done, status, body }
|
|
var httpNext = 1;
|
|
|
|
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); });
|
|
|
|
// Asynchronous HTTP request; the result is polled by the game.
|
|
// status 0 = network error (e.g. no service, or the page was opened as a local file)
|
|
function httpStart(method, url, body) {
|
|
var id = httpNext++;
|
|
var req = { done: false, status: 0, body: new Uint8Array(0) };
|
|
http[id] = req;
|
|
var ctrl = typeof AbortController !== "undefined" ? new AbortController() : null;
|
|
var timer = setTimeout(function () { if (ctrl) ctrl.abort(); }, 15000);
|
|
try {
|
|
var opts = { method: method, cache: "no-store", headers: {} };
|
|
if (ctrl) opts.signal = ctrl.signal;
|
|
if (body !== null) { opts.body = body; opts.headers["Content-Type"] = "application/json"; }
|
|
fetch(url, opts).then(function (r) {
|
|
req.status = r.status;
|
|
return r.arrayBuffer();
|
|
}).then(function (buf) {
|
|
req.body = new Uint8Array(buf);
|
|
req.done = true;
|
|
clearTimeout(timer);
|
|
}).catch(function () {
|
|
req.status = 0;
|
|
req.done = true;
|
|
clearTimeout(timer);
|
|
});
|
|
} catch (e) {
|
|
req.done = true;
|
|
}
|
|
return id;
|
|
}
|
|
|
|
function register(importObject) {
|
|
var env = importObject.env;
|
|
env.dc_http = function (mp, ml, up, ul, bp, bl) {
|
|
var body = bl > 0 ? bytes(bp, bl).slice() : null;
|
|
return httpStart(str(mp, ml), str(up, ul), body);
|
|
};
|
|
// -2 = still running, otherwise the HTTP status (0 = network error)
|
|
env.dc_http_poll = function (id) {
|
|
var r = http[id];
|
|
if (!r) return 0;
|
|
return r.done ? r.status : -2;
|
|
};
|
|
env.dc_http_len = function (id) {
|
|
var r = http[id];
|
|
return r ? r.body.length : 0;
|
|
};
|
|
// copies the response body and forgets the request
|
|
env.dc_http_take = function (id, ptr) {
|
|
var r = http[id];
|
|
if (!r) return;
|
|
bytes(ptr, r.body.length).set(r.body);
|
|
delete http[id];
|
|
};
|
|
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;
|
|
};
|
|
// browser text dialog (touch devices have no keyboard in the canvas); result via dc_storage_take
|
|
env.dc_prompt = function (tp, tl, dp, dl) {
|
|
var v = window.prompt(str(tp, tl), str(dp, dl));
|
|
if (v === null || v === undefined) { pending = null; return -1; }
|
|
pending = new TextEncoder().encode(v);
|
|
return pending.length;
|
|
};
|
|
// the device's main pointer is a finger (phones, tablets)
|
|
env.dc_coarse_pointer = function () {
|
|
try { return window.matchMedia("(pointer: coarse)").matches ? 1 : 0; } catch (e) { return 0; }
|
|
};
|
|
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" });
|
|
})();
|