window.player = { uid: NaN };
window.elem = {
info: document.getElementById("miscinfo"),
identform: document.getElementById("identform"),
statusline: document.getElementsByClassName("statusline")[0],
bcont: document.getElementById("board-container"),
board: document.getElementById("board"),
cursor_frame: document.getElementById("cursor-frame"),
volslider: document.getElementById("volslider"),
chat_div: document.getElementById("chat-div")
};
const U32MAX = Math.pow(2,32) - 1;
window.queued_pos = undefined;
window.assets = {
audio: {
explosion: registerMediaElem(new Audio("../explosion.opus"))
}
};
function registerMediaElem(melm) {
let record = { data: melm, loaded: false };
melm.addEventListener("canplaythrough", e => {
record.loaded = true;
});
return record;
}
document.addEventListener('roomloading', function() {
//window.elem.info.innerHTML = "loading...";
// TODO/FIXME: this should check all the assets, once we have more than one
if (assets.audio.explosion.loaded) {
room.socket = connect(); // drive the game with socket events from then on
} else {
setTimeout(function() { document.dispatchEvent(new Event('roomloading')) }, 500);
}
});
window.room = {
name: undefined,
bconf: { w: NaN, h: NaN, tile_w: NaN, tile_h: NaN, mine_ratio: undefined },
board: {},
cbounds: {},
socket: undefined,
last_packet: undefined,
identity: JSON.parse(localStorage.getItem("identity")),
cursors: new Map(),
};
if (room.identity == null) {
elem.statusline.style.display = "none";
elem.identform.style.display = "initial";
} else {
join();
}
function join() {
if (room.identity == null) {
room.identity = {};
room.identity.name = document.getElementById("name-in").value;
room.identity.clr = document.getElementById("clr-in").value;
localStorage.setItem("identity", JSON.stringify(room.identity));
}
elem.identform.style.display = "none";
elem.statusline.style.display = "flex";
document.dispatchEvent(new Event('roomloading'));
}
function clear_ident() {
localStorage.removeItem("identity");
document.location.reload();
}
function connect() {
let wsproto = (window.location.protocol == "https:")? "wss:": "ws:";
let s = new WebSocket(`${wsproto}//${location.hostname}:${location.port}${location.pathname}/ws`);
s.onopen = function() {
s.send(`register ${room.identity.name} ${room.identity.clr}`);
}
s.onmessage = function(e) {
room.last_packet = e;
let d = e.data;
if (typeof d == "object") {
d.arrayBuffer().then(acceptBoard);
elem.info.onclick = undefined;
elem.info.innerHTML = `${room.name} (${room.bconf.w}x${room.bconf.h}) >> Running, ${room.bconf.mine_ratio} tiles are mines`;
} else if (typeof e.data == "string") {
let fields = d.split(" ");
switch (fields[0]) {
case "pos": {
let posdata = JSON.parse(fields[1]);
posdata.forEach(pdat => {
let oid = Number(pdat[0]);
let x = pdat[1][0];
let y = pdat[1][1];
let curs = room.cursors.get(oid);
if (oid != player.uid) {
if (curs != undefined) {
movCursor(curs, [x, y]);
} else {
console.log("livepos sys incoherent");
}
}
});
} break;
case "players": {
let pdata = JSON.parse(fields[1]);
console.log(pdata);
pdata.forEach(p => {
let oid = Number(p[0]);
let name = p[1];
let clr = p[2];
console.log(oid, name, clr);
if (!room.cursors.has(oid)) {
createCursor(oid, name, clr);
}
});
} break;
case "regack": {
room.name = fields[1];
name = fields[2];
player.uid = Number(fields[3]);
let dims = fields[4].split("x");
room.bconf.w = Number(dims[0]);
room.bconf.h = Number(dims[1]);
room.bconf.mine_ratio = fields[5];
createCursor(player.uid, name, room.identity.clr);
setup_chat(name, room.name);
} break;
case "win": {
elem.info.innerHTML = "You win! Click here to play again.";
elem.info.onclick = e => { s.send("reset") };
} break;
case "lose": {
let badone = fields[1];
elem.info.innerHTML = `You lost, ${badone} was blown up. Click here to retry.`;
elem.info.onclick = e => { s.send("reset") };
assets.audio.explosion.data.play();
} break;
case "logoff": {
let oid = Number(fields[1]);
room.cursors.get(oid).elem.remove();
room.cursors.get(oid).selwin.remove();
room.cursors.delete(oid);
} break;
}
}
}
s.onerror = function(e) { elem.info.innerHTML += `
Connection error: ${e}`; }
s.onclose = function(e) { elem.info.innerHTML = "Connection closed"; }
return s;
}
function acceptBoard(data) {
let dataarr = new Uint8Array(data);
let vals = fflate.inflateSync(dataarr);
room.board = vals.reduce((s,c) => {
let v = String.fromCodePoint(c);
if (v == ' ') {
s = s + " ";
} else {
s = s + v;
}
return s;
}, "");
let last = room.board[0];
let last_idx = 0;
let split_board = [];
for (let i = 1; i < room.board.length+1; i++) {
let cur = room.board[i];
let gamechars = /^[CFO# 1-8]+$/;
if ((cur != last && gamechars.test(cur)) || cur == undefined) {
let txt = room.board.substr(last_idx, i-last_idx);
switch(txt[0]) {
case 'O':
txt = `${txt}`;
break;
case 'C':
txt = `${txt}`;
break;
case 'F':
txt = `${txt}`;
break;
case '1': txt = `${txt}`; break;
case '2': txt = `${txt}`; break;
case '3': txt = `${txt}`; break;
case '4': txt = `${txt}`; break;
case '5': txt = `${txt}`; break;
case '6': txt = `${txt}`; break;
default: txt = `${txt}`; break;
}
split_board.push(txt);
last_idx = i;
}
last = room.board[i];
}
elem.board.innerHTML = split_board.join("");
room.cbounds = getBoardBounds();
}
function createCursor(id, name, clr) {
// shit doesn't line up
let cursor = document.createElement("div");
cursor.style.position = "absolute";
let nametag = document.createElement("p");
nametag.innerHTML = name;
nametag.classList.add('cursor-name');
let selection_window = document.createElement("div");
selection_window.style.backgroundColor = clr + "a0";
selection_window.style.position = "absolute";
selection_window.classList.add('cursor');
cursor.appendChild(nametag);
cursor.classList.add('cursor');
cursor.style.color = clr;
elem.cursor_frame.append(cursor);
elem.cursor_frame.append(selection_window);
let c = { name: name, elem: cursor, selwin: selection_window };
if (id == window.player.uid) {
document.addEventListener('mousemove', e => {
let bcoords = pageToBoard(e.pageX, e.pageY);
movCursor(c, bcoords);
window.queued_pos = bcoords;
},
false);
}
room.cursors.set(id, {name: name, elem: cursor, selwin: selection_window});
return cursor;
}
function pageToBoard(x,y) {
return [
Math.floor((x - room.cbounds.ox) * (U32MAX / room.cbounds.w)),
Math.floor((y - room.cbounds.oy) * (U32MAX / room.cbounds.h))
];
}
function boardToPage(b) {
return [
b[0] * (room.cbounds.w / U32MAX),
b[1] * (room.cbounds.h / U32MAX)
];
}
function movCursor(c, b) {
let p = boardToPage(b);
c.elem.style.left = p[0] + 'px';
c.elem.style.top = p[1] + 'px';
movSelWin(c.selwin, b);
}
function movSelWin(win, b) {
let tpos = tilepos(b);
if (tpos[0] > (room.bconf.w - 1) || tpos[0] < 0 || tpos[1] > (room.bconf.h - 1) || tpos[1] < 0) {
win.style.display = "none";
} else {
win.style.display = "";
}
win.style.left = (tpos[0] * room.bconf.tile_w) + 'px';
win.style.top = (tpos[1] * room.bconf.tile_h) + 'px';
win.style.width = room.bconf.tile_w + 'px';
win.style.height = room.bconf.tile_h + 'px';
}
function getBoardBounds() {
let a = elem.bcont.getBoundingClientRect();
let b = elem.board.getBoundingClientRect();
room.bconf.tile_w = b.width / room.bconf.w;
room.bconf.tile_h = 48;
return {
ox: b.x + window.scrollX,
oy: a.y + window.scrollY,
w: b.width,
h: a.height
};
}
window.onresize = () => {
room.cbounds = getBoardBounds();
}
elem.bcont.onclick = function(e) {
let bcoords = pageToBoard(e.pageX, e.pageY);
let tpos = tilepos(bcoords);
let cmd = `reveal ${tpos[0]} ${tpos[1]}`;
room.socket.send(cmd);
}
elem.bcont.oncontextmenu = function(e) {
let bcoords = pageToBoard(e.pageX, e.pageY);
let tpos = tilepos(bcoords);
let cmd = `flag ${tpos[0]} ${tpos[1]}`;
room.socket.send(cmd);
return false;
}
// these are board coords, [0..2**32)
function tilepos(b) {
return [
Math.floor(room.bconf.w * b[0]/U32MAX),
Math.floor(room.bconf.h * b[1]/U32MAX)
];
}
function volChanged() {
let newVol = elem.volslider.value;
localStorage.setItem("audioVolume", JSON.stringify(newVol));
for (i of Object.keys(assets.audio)) {
assets.audio[i].data.volume = newVol;
}
}
elem.volslider.onchange = volChanged;
let storedVol = localStorage.getItem("audioVolume");
if (storedVol) { elem.volslider.value = JSON.parse(storedVol); }
volChanged();
(function sendPos() {
let qp = window.queued_pos;
if (qp) {
room.socket.send(`pos ${qp[0]} ${qp[1]}`);
window.queued_pos = undefined;
}
setTimeout(function() {
sendPos();
}, 16);
})();
(function heartbeat() {
setTimeout(function() {
room.socket.send("<3");
heartbeat();
}, 30000);
})();
function setup_chat(name, room_name) {
let chat_iframe = document.createElement("iframe");
// FIXME the url should come from the server
chat_iframe.setAttribute("src", "/gamja?channels=" + encodeURIComponent(`#mines-${room_name}`) + "&nick=" + encodeURIComponent(name));
elem.chat_div.appendChild(chat_iframe);
}