348 lines
11 KiB
JavaScript
348 lines
11 KiB
JavaScript
const $ = document.querySelector.bind(document);
|
|
const $$ = document.querySelectorAll.bind(document);
|
|
|
|
let current_level = 1;
|
|
let initial_mines_count = 10;
|
|
let field_width = 8;
|
|
let field_height = 8;
|
|
|
|
let mines_placed = false;
|
|
let game_finished = false;
|
|
let ui_timer;
|
|
let seconds_elapsed = 0;
|
|
|
|
let sounds = new Map([
|
|
["reveal", [
|
|
new Audio("./res/sfx/reveal_1.wav"),
|
|
new Audio("./res/sfx/reveal_2.wav"),
|
|
new Audio("./res/sfx/reveal_3.wav"),
|
|
new Audio("./res/sfx/reveal_4.wav")
|
|
]],
|
|
["flag", [
|
|
new Audio("./res/sfx/flag_1.wav"),
|
|
new Audio("./res/sfx/flag_2.wav"),
|
|
new Audio("./res/sfx/flag_3.wav")
|
|
]],
|
|
["unflag", [
|
|
new Audio("./res/sfx/unflag_1.wav"),
|
|
new Audio("./res/sfx/unflag_2.wav"),
|
|
new Audio("./res/sfx/unflag_3.wav"),
|
|
new Audio("./res/sfx/unflag_4.wav")
|
|
]],
|
|
["game_over", []],
|
|
["game_win", []]
|
|
]);
|
|
|
|
let graphics_unknown = new Image;
|
|
graphics_unknown.src = "./res/gfx/unknown-1.png";
|
|
let graphics_known = new Image;
|
|
graphics_known.src = "./res/gfx/known.png";
|
|
let graphics_flagged = new Image;
|
|
graphics_flagged.src = "./res/gfx/flagged.png";
|
|
let graphics_wood_particle = new Image;
|
|
graphics_wood_particle.src = "./res/gfx/wood_particle.png";
|
|
|
|
|
|
// Cosmetic functions that spawn particles, play sounds, and so on.
|
|
|
|
// Does all the bells and whistles.
|
|
function play_sound(sound_name) {
|
|
let sound_arr = sounds.get(sound_name);
|
|
let to_play = sound_arr[Math.floor(Math.random() * sound_arr.length)];
|
|
to_play.preservesPitch = false;
|
|
to_play.playbackRate = 0.75 + Math.random() / 4;
|
|
// to_play.currentTime = 0;
|
|
to_play.play();
|
|
}
|
|
|
|
function get_dom_bb_proper(element) {
|
|
let r = element.getBoundingClientRect();
|
|
return {
|
|
w: r.right - r.left,
|
|
h: r.bottom - r.top,
|
|
x: element.offsetLeft - element.scrollLeft,
|
|
y: element.offsetTop - element.scrollTop
|
|
};
|
|
}
|
|
|
|
function reveal_cell_cosmetic(cell) {
|
|
play_sound("reveal");
|
|
let bb = get_dom_bb_proper(cell);
|
|
// Some wood particles
|
|
for (let i = 0; i < 16; ++i) {
|
|
let x = bb.x + bb.w / 2;
|
|
let y = bb.y + bb.h / 2;
|
|
spawn_particle(x, y, (Math.random() * 2 - 1) / 4, -Math.random() / 2, 0,
|
|
(Math.random() * 2 - 1) / 8, graphics_wood_particle,
|
|
graphics_wood_particle.width, graphics_wood_particle.height);
|
|
}
|
|
// Pop the coffin lid off.
|
|
spawn_particle(bb.x, bb.y, (Math.random() * 2 - 1) / 16, -0.3, 0,
|
|
(Math.random() * 2 - 1) / 16, graphics_unknown, bb.w, bb.h);
|
|
}
|
|
|
|
function flag_cell_cosmetic(cell) {
|
|
play_sound("flag");
|
|
let bb = get_dom_bb_proper(cell);
|
|
// Spawn some dirt particles.
|
|
}
|
|
|
|
function unflag_cell_cosmetic(cell) {
|
|
play_sound("unflag");
|
|
let bb = get_dom_bb_proper(cell);
|
|
// Spawn dirt particles and make the tombstone fall off.
|
|
spawn_particle(bb.x, bb.y, (Math.random() * 2 - 1) / 24, -0.2, 0,
|
|
(Math.random() * 2 - 1) / 24, graphics_flagged, bb.w, bb.h);
|
|
}
|
|
|
|
// Shows the game won/over popup.
|
|
function show_popup(victory) {
|
|
document.body.classList.remove("popup-victory", "popup-failure");
|
|
if (victory) {
|
|
document.body.classList.add("popup-victory");
|
|
$("#message").innerText = "You've won!"
|
|
} else {
|
|
document.body.classList.add("popup-failure");
|
|
$("#message").innerText = "You've lost!"
|
|
}
|
|
}
|
|
|
|
// Actual game logic.
|
|
|
|
// Game state saving.
|
|
window.addEventListener("beforeunload", function() {
|
|
if (mines_placed && !game_finished) {
|
|
// We need to save: the minefield, the elapsed time. These also
|
|
// get prefixed by the current level.
|
|
localStorage.setItem(current_level + "-minefield_html", $("#minefield").innerHTML);
|
|
localStorage.setItem(current_level + "-seconds_elapsed", seconds_elapsed);
|
|
} else if (document.body.classList.contains("game-view")) {
|
|
localStorage.removeItem(current_level + "-minefield_html");
|
|
localStorage.removeItem(current_level + "-seconds_elapsed");
|
|
}
|
|
});
|
|
|
|
function get_seconds_readable(seconds) {
|
|
let date = new Date(null);
|
|
date.setSeconds(seconds);
|
|
return date.toISOString().slice(14, 19);
|
|
}
|
|
|
|
function start_timing() {
|
|
ui_timer = window.setInterval(function() {
|
|
seconds_elapsed += 1;
|
|
$("#time").innerText = get_seconds_readable(seconds_elapsed);
|
|
}, 1000);
|
|
}
|
|
|
|
function stop_timing() {
|
|
window.clearInterval(ui_timer);
|
|
}
|
|
|
|
function restart_game() {
|
|
game_finished = false;
|
|
mines_placed = false;
|
|
// Based on the level, we calculate the grid size and amount
|
|
// of mines. The height of the grid increases every two
|
|
// levels.
|
|
field_height = 8 + Math.floor(current_level / 2);
|
|
minefield_initialize();
|
|
update_particle_canvas_size();
|
|
// The amount of mines, meanwhile, increases by one I
|
|
// think. Just a quick thought. This makes it so the amount of
|
|
// mines is around four times less than the amount of cells.
|
|
initial_mines_count = 10 + current_level;
|
|
stop_timing();
|
|
seconds_elapsed = 0;
|
|
document.body.classList.remove("main-menu", "popup-victory", "popup-failure");
|
|
document.body.classList.add("game-view");
|
|
$("#time").innerText = get_seconds_readable(seconds_elapsed);
|
|
}
|
|
|
|
function start_or_continue_game(level) {
|
|
let minefield_html = localStorage.getItem(current_level + "-minefield_html");
|
|
$("#level").innerText = current_level;
|
|
if (minefield_html == null) {
|
|
restart_game();
|
|
} else {
|
|
// If we have a minefield in localStorage, load it back into
|
|
// the DOM.
|
|
$("#minefield").innerHTML = minefield_html;
|
|
// We need to re-add events to the cells.
|
|
for (let y = 0; y < field_height; y++) {
|
|
for (let x = 0; x < field_width; x++) {
|
|
let cell = $("#c" + x + "-" + y);
|
|
cell.onclick = cell_onclick.bind(cell);
|
|
cell.oncontextmenu = cell_oncontextmenu.bind(cell);
|
|
cell.x = x;
|
|
cell.y = y;
|
|
}
|
|
}
|
|
mines_placed = true;
|
|
seconds_elapsed = parseInt(localStorage.getItem(current_level + "-seconds_elapsed"));
|
|
$("#time").innerText = get_seconds_readable(seconds_elapsed);
|
|
start_timing();
|
|
}
|
|
}
|
|
|
|
function minefield_initialize() {
|
|
$("#minefield").innerHTML = "";
|
|
for (let y = 0; y < field_height; y++) {
|
|
for (let x = 0; x < field_width; x++) {
|
|
let cell = document.createElement("div");
|
|
cell.id = "c" + x + "-" + y;
|
|
cell.x = x;
|
|
cell.y = y;
|
|
cell.classList.add("unknown");
|
|
|
|
cell.onclick = cell_onclick.bind(cell);
|
|
cell.oncontextmenu = cell_oncontextmenu.bind(cell);
|
|
$("#minefield").appendChild(cell);
|
|
}
|
|
}
|
|
}
|
|
|
|
function cell_onclick() {
|
|
if (!mines_placed) {
|
|
place_mines(this.x, this.y);
|
|
start_timing();
|
|
}
|
|
if (this.classList.contains("unknown") && !this.classList.contains("flagged")) {
|
|
if (this.classList.contains("mine")) {
|
|
this.classList.remove("mine");
|
|
this.classList.add("exploded");
|
|
lose_game();
|
|
return;
|
|
} else {
|
|
reveal_mines(this);
|
|
}
|
|
} else if (this.classList.contains("flagged")) {
|
|
unflag_cell_cosmetic(this);
|
|
this.classList.remove("flagged");
|
|
}
|
|
}
|
|
|
|
function cell_oncontextmenu() {
|
|
if (this.classList.contains("unknown") && !this.classList.contains("flagged")) {
|
|
flag_cell_cosmetic(this);
|
|
this.classList.add("flagged");
|
|
} else if (this.classList.contains("flagged")) {
|
|
unflag_cell_cosmetic(this);
|
|
this.classList.remove("flagged");
|
|
}
|
|
check_victory();
|
|
return false;
|
|
}
|
|
|
|
function place_mines(avoid_x, avoid_y) {
|
|
for (let i = 0; i < initial_mines_count; i++) {
|
|
let x = Math.floor(Math.random() * field_width);
|
|
let y = Math.floor(Math.random() * field_height);
|
|
if (Math.abs(x - avoid_x) <= 1 && Math.abs(y - avoid_y) <= 1) {
|
|
--i;
|
|
} else {
|
|
$("#c" + x + "-" + y).classList.add("mine");
|
|
}
|
|
}
|
|
mines_placed = true;
|
|
}
|
|
|
|
function get_neighbours(cell) {
|
|
let cells = [];
|
|
for (let x = cell.x - 1; x <= cell.x + 1; ++x) {
|
|
for (let y = cell.y - 1; y <= cell.y + 1; ++y) {
|
|
if (x >= 0 && x < field_width && y >= 0 && y < field_height && !(cell.x == x && cell.y == y)) {
|
|
cells.push($("#c" + x + "-" + y));
|
|
}
|
|
}
|
|
}
|
|
return cells;
|
|
}
|
|
|
|
function count_neighbouring_mines(cell) {
|
|
count = 0;
|
|
get_neighbours(cell).forEach(function(c) {
|
|
if (c.classList.contains("mine") || c.classList.contains("exploded")) {
|
|
count += 1;
|
|
}
|
|
})
|
|
return count;
|
|
}
|
|
|
|
function reveal_mines(cell) {
|
|
let n_mines = count_neighbouring_mines(cell);
|
|
|
|
if (cell.classList.contains("unknown")) {
|
|
reveal_cell_cosmetic(cell);
|
|
cell.classList.remove("unknown");
|
|
cell.classList.add("known");
|
|
}
|
|
|
|
// Check for victory.
|
|
if (!game_finished) {
|
|
check_victory();
|
|
}
|
|
|
|
if (n_mines == 0) {
|
|
get_neighbours(cell).forEach(function(c) {
|
|
if (c.classList.contains("unknown")) {
|
|
window.setTimeout(function() {
|
|
reveal_mines(c);
|
|
}, 100);
|
|
// reveal_mines(c);
|
|
}
|
|
if (c.classList.contains("flagged")) {
|
|
unflag_cell_cosmetic(c);
|
|
c.classList.remove("flagged");
|
|
}
|
|
});
|
|
} else if (n_mines > 0 && !cell.classList.contains("mine")) {
|
|
cell.innerText = "" + n_mines;
|
|
cell.classList.add("n" + n_mines);
|
|
}
|
|
}
|
|
|
|
function check_victory() {
|
|
for (let y = 0; y < field_height; ++y) {
|
|
for (let x = 0; x < field_width; ++x) {
|
|
let cell = $("#c" + x + "-" + y);
|
|
if (cell.classList.contains("unknown") && !(cell.classList.contains("flagged") && cell.classList.contains("mine"))) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
// If we get here, we've won
|
|
game_finished = true;
|
|
stop_timing();
|
|
// Set the maximum level to be one higher, meaning the player unlocked the next level.
|
|
if (current_level == maximum_level) {
|
|
localStorage.setItem("maximum_level", maximum_level + 1);
|
|
}
|
|
show_popup(true);
|
|
// Save a high score.
|
|
let cur_score = localStorage.getItem(current_level + "-high_score");
|
|
if (cur_score == null) {
|
|
localStorage.setItem(current_level + "-high_score", seconds_elapsed)
|
|
} else if (parseInt(cur_score) > seconds_elapsed) {
|
|
localStorage.setItem(current_level + "-high_score", seconds_elapsed)
|
|
}
|
|
// Send a message proclaiming we've won.
|
|
window.webxdc.sendUpdate({
|
|
payload: {},
|
|
info: window.webxdc.selfName + " finished Level " + current_level + " in " + get_seconds_readable(seconds_elapsed) + "!"
|
|
}, window.webxdc.selfName + " won a game of Minesweeper");
|
|
return true;
|
|
}
|
|
|
|
function lose_game() {
|
|
game_finished = true;
|
|
stop_timing();
|
|
show_popup(false);
|
|
$$("#minefield div").forEach(function(cell) {
|
|
reveal_mines(cell);
|
|
if (cell.classList.contains("exploded")) {
|
|
cell.innerText = "";
|
|
}
|
|
});
|
|
}
|