Files
kestrelsnest-blog/static/dndlabyrinth/js/main.js
T
Eric WagonerandClaude Fable 5 1b9e6e32d8 Add D&D labyrinth at /dndlabyrinth/
Already live on the server via rsync; bringing the repo in sync.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-11-10 20:02:22 -05:00

1208 lines
52 KiB
JavaScript

var labyrinth = {
VERSION: 1.1,
DIR_TYPE: {
North: 0,
East: 1,
South: 2,
West: 3
},
PATH_TYPE: {
Undefined: 0,
Open: 1,
Wall: 2,
Door: 3
},
GAME_STATE: {
Wait: 0,
WarriorOneSelectRoom: 1,
WarriorTwoSelectRoom: 2,
WarriorOneTurn: 3,
WarriorTwoTurn: 4,
GameOver: 5
},
DRAGON_STATE: {
Asleep: 0,
Awake: 1
},
settings: {
debug: false,
travelSpeed: 200,
doorProb: .1,
wallProb: .8,
removeWallThreshold: 2,
removeWallProb: 0,
edgeWallBias: true,
treasureRoomDistance: 3,
maxLives: 3,
baseMoves: 2,
movesPerLife: 2,
movesWithTreasure: 4,
doorClosedProb: .2,
dragonWakeDistance: 3,
dragonFollows: 1
},
elements: {
board: null,
grid: null,
buttons: [],
chambers: [],
help: null
},
maze: {
chamberPaths: [],
stack: null,
visited: {},
lastChamber: null,
cancelTravel: false
},
game: {
init: false,
state: 0,
delayTimeout: null,
numberOfWarriors: 1,
warrior: [
{
lives: null,
secretRoom: null,
position: null,
moves: null
},
{
lives: null,
secretRoom: null,
position: null,
moves: null
}
],
dragon: {
position: null,
state: null,
visible: false
},
treasure: {
room: null,
warrior: -1,
visible: false
},
switchButtonOn: false,
level: 1,
help: null,
helpTimeout: null,
helpMessage: ""
},
audio: {
on: new Audio("audio/on.mp3"),
levelOne: new Audio("audio/levelOne.mp3"),
levelTwo: new Audio("audio/levelTwo.mp3"),
dragonFlying: new Audio("audio/dragonFlying.mp3"),
defeat: new Audio("audio/defeat.mp3"),
dragonAttacks: new Audio("audio/dragonAttacks.mp3"),
dragonWakes: new Audio("audio/dragonWakes.mp3"),
wall: new Audio("audio/wall.mp3"),
door: new Audio("audio/door.mp3"),
illegalMove: new Audio("audio/illegalMove.mp3"),
warriorMoves: new Audio("audio/warriorMoves.mp3"),
warriorOne: new Audio("audio/warriorOne.mp3"),
winner: new Audio("audio/winner.mp3"),
warriorTwo: new Audio("audio/warriorTwo.mp3"),
treasure: new Audio("audio/treasure.mp3")
},
log(msg) {
if (this.settings.debug) console.log(msg);
},
trackEvent(action, category, label, value) {
if (window.gaEnabled) {
try {
gtag('event', action, {
'event_category': category,
'event_label': label,
'value': value
});
}
catch (ex) { }
}
},
startMessageResolve: null,
serialAudio: new Audio(),
audioPlayResolve: null,
audioPlayTimeout: null,
audioMetadataListener() {
var duration = this.serialAudio.duration;
var self = this;
this.audioPlayTimeout = setTimeout(function() {
self.audioPlayTimeout = null;
var resolve = self.audioPlayResolve;
self.audioPlayResolve = null;
resolve();
}, duration * 1000);
},
playAudio(audio) {
return new Promise((resolve, reject) => {
if (this.audioPlayTimeout && this.audioPlayResolve) {
var timeout = this.audioPlayTimeout;
this.audioPlayTimeout = null;
clearTimeout(timeout);
var previousResolve = this.audioPlayResolve;
this.audioPlayResolve = null;
previousResolve();
}
this.audioPlayResolve = resolve;
this.serialAudio.src = audio.src;
this.serialAudio.play();
});
},
init() {
if (!window.URL || !window.Promise) {
alert("Sorry! Your browser is not supported.");
}
else {
let location = new URL(document.location);
if (location.searchParams.get("debug") == "1") this.settings.debug = true;
let travelSpeed = location.searchParams.get("travelSpeed");
if (!isNaN(parseInt(travelSpeed))) this.settings.travelSpeed = travelSpeed;
if (this.settings.debug) {
document.body.className = document.body.className + " debug";
var container = document.getElementById("container");
container.className = " layoutDebug";
document.addEventListener("onclick", event => {
if (event.keyCode === 88) this.maze.cancelTravel = true;
});
}
else {
document.addEventListener("contextmenu", function(event){
event.preventDefault();
}, false);
}
this.loadSettings();
this.initMenus();
this.initAudio();
this.initStartMessage();
}
},
loadSettings() {
try {
let helpEnabled = localStorage.getItem("helpEnabled");
this.toggleHelp(helpEnabled == null || helpEnabled == "true");
}
catch(ex) {}
},
saveSetting(key, value) {
try {
localStorage.setItem(key, value);
}
catch(ex) {}
},
toggleHelp(checked) {
let helpCheckbox = document.getElementById("help");
helpCheckbox.checked = checked;
let helpMessage = document.getElementById("helpMessage");
helpMessage.className = checked ? "show" : "";
this.saveSetting("helpEnabled", checked);
},
initMenus() {
var self = this;
document.getElementById("exit").onclick = function() {
self.trackEvent("Game Exit");
document.body.className = document.body.className.replace(" play", "");
};
document.getElementById("reset").onclick = function() {
self.trackEvent("Game Reset");
self.reset.call(self);
};
document.getElementById("help").onclick = function() {
self.trackEvent("Help " + (this.checked ? "Enabled" : "Disabled"));
self.toggleHelp.call(self, this.checked);
};
},
initStartMessage() {
var self = this;
document.getElementById("start").addEventListener("click", function() {
self.trackEvent("Game Started");
self.start.call(self);
});
},
initAudio() {
this.audioIndex = [
[
this.audio.dragonFlying,
this.audio.dragonAttacks,
this.audio.wall,
this.audio.illegalMove,
this.audio.warriorOne,
this.audio.warriorTwo
],
[
this.audio.defeat,
this.audio.dragonWakes,
this.audio.door,
this.audio.warriorMoves,
this.audio.winner,
this.audio.treasure
]
];
this.audioNameIndex = [
[
"Dragon Flying",
"Dragon Attacks",
"Wall",
"Illegal Move",
"Warrior One",
"Warrior Two"
],
[
"Defeat",
"Dragon Wakes",
"Door",
"Warrior Moves",
"Winner",
"Treasure"
]
];
var self = this;
this.serialAudio.addEventListener("loadedmetadata", function () {
self.audioMetadataListener.call(self)
}, false);
},
reset() {
this.initBoard();
this.initMaze().then(() => {
this.startGame();
});
},
initBoard() {
this.elements.help = document.getElementById("helpMessage");
this.elements.board = document.getElementById("board");
let board = this.elements.board;
board.innerHTML = "";
this.elements.grid = document.createElement("div");
let grid = this.elements.grid
grid.id = "grid";
this.elements.buttons = [];
this.elements.chambers = [];
var chamberPaths = this.maze.chamberPaths;
chamberPaths.splice(0);
for(let r=0; r<8; r++) {
chamberPaths.push([]);
let row = document.createElement("div");
row.className = "row";
for(let c=0; c<9; c++) {
let col = document.createElement("div");
col.className = "col";
let cell = document.createElement("div");
cell.className = "cell";
if (c == 0) {
var self = this;
col.id = "button-" + (r).toString();
col.className += " button";
if (r == 0) {
col.className += " switch";
var self = this;
col.onmousedown = function() {
self.toggleSwitch.call(self);
}
}
else if (r == 1) {
col.onmousedown = function(event) {
if (event.which == 1) {
self.nextTurn.call(self);
} else if (event.which == 3) {
self.toggleLevel.call(self);
}
}
}
else {
col.onmousedown = function(event) {
if (event.which == 1) {
self.playAudioClick.call(self, r-2, false);
} else if (event.which == 3) {
self.playAudioClick.call(self, r-2, true);
}
}
}
}
else {
chamberPaths[r].push([
r==0 ? this.PATH_TYPE.Wall : this.PATH_TYPE.Undefined,
c==8 ? this.PATH_TYPE.Wall : this.PATH_TYPE.Undefined,
r==7 ? this.PATH_TYPE.Wall : this.PATH_TYPE.Undefined,
c==1 ? this.PATH_TYPE.Wall : this.PATH_TYPE.Undefined
]);
col.id = "chamber-" + (r).toString() + "-" + (c-1).toString();
col.className += " chamber";
cell.onmousedown = function(event) {
self.chamberSelect([r, c-1], event);
}
}
if (c == 0) {
this.elements.buttons.push(col);
}
else {
if (c == 1) this.elements.chambers.push([]);
this.elements.chambers[this.elements.chambers.length - 1].push(col);
if (r < 7) {
let southWall = document.createElement("div");
southWall.className = "southWall";
col.appendChild(southWall);
}
if (c < 8) {
let eastWall = document.createElement("div");
eastWall.className = "eastWall";
col.appendChild(eastWall);
}
}
col.appendChild(cell);
row.appendChild(col);
}
grid.appendChild(row);
}
board.appendChild(grid);
},
showSep(row, col, dir, type) {
if (this.settings.debug) {
var classSuffix = type == this.PATH_TYPE.Wall ? "WallDebug" : (type == this.PATH_TYPE.Door ? "DoorDebug" : "OpenDebug");
var chamberCell = this.elements.chambers[row][col].querySelector(".cell");
var classPrefix = "";
if (dir == this.DIR_TYPE.North) classPrefix = "north";
if (dir == this.DIR_TYPE.East) classPrefix = "east";
if (dir == this.DIR_TYPE.South) classPrefix = "south";
if (dir == this.DIR_TYPE.West) classPrefix = "west";
chamberCell.className = chamberCell.className.replace(new RegExp("\\s" + classPrefix + "[^\\s]+"), "") + " " + classPrefix + classSuffix;
}
},
setPath(row, col, dir, type) {
var chamberPaths = this.maze.chamberPaths;
chamberPaths[row][col][dir] = type;
if (dir == this.DIR_TYPE.East || dir == this.DIR_TYPE.South) this.showSep(row, col, dir, type);
if (dir == this.DIR_TYPE.North && row != 0) {
chamberPaths[row - 1][col][this.DIR_TYPE.South] = type;
this.showSep(row -1 , col, this.DIR_TYPE.South, type);
} else if (dir == this.DIR_TYPE.East && col != 7) {
chamberPaths[row][col + 1][this.DIR_TYPE.West] = type;
} else if (dir == this.DIR_TYPE.South && row != 7) {
chamberPaths[row + 1][col][this.DIR_TYPE.North] = type;
} else if (dir == this.DIR_TYPE.West && col != 0) {
chamberPaths[row][col - 1][this.DIR_TYPE.East] = type;
this.showSep(row, col - 1, this.DIR_TYPE.East, type);
}
},
getPaths(row, col) {
var chamberPaths = this.maze.chamberPaths;
return {
north: chamberPaths[row][col][this.DIR_TYPE.North],
east: chamberPaths[row][col][this.DIR_TYPE.East],
south: chamberPaths[row][col][this.DIR_TYPE.South],
west: chamberPaths[row][col][this.DIR_TYPE.West]
}
},
wallOrNot() {
return Math.random() <= this.settings.wallProb ? this.PATH_TYPE.Wall : this.openOrDoor();
},
openOrDoor() {
return Math.random() <= this.settings.doorProb ? this.PATH_TYPE.Door : this.PATH_TYPE.Open;
},
travelMaze() {
return new Promise((resolve, reject) => {
var stack = this.maze.stack;
var pos = stack[stack.length - 1];
var wallProb = this.settings.wallProb;
var visited = this.maze.visited;
var chamberPaths = this.maze.chamberPaths;
var row = pos[0];
var col = pos[1];
visited[row.toString() + "_" + col.toString()] = true;
if (this.settings.debug) {
var lastChamber = this.maze.lastChamber;
var chamber = this.elements.chambers[row][col];
if (chamber.className.indexOf("traveledDebug") == -1) chamber.className += " traveledDebug";
chamber.className += " travelingDebug";
if (lastChamber) lastChamber.className = lastChamber.className.replace(" travelingDebug", "");
this.maze.lastChamber = chamber;
var chamberCell = chamber.querySelector(".cell");
if (chamberCell.innerHTML == "") chamberCell.innerHTML = stack.length;
}
if (this.settings.edgeWallBias && row > 0 && chamberPaths[row][col][this.DIR_TYPE.North] == this.PATH_TYPE.Undefined && visited[(row - 1).toString() + "_" + col.toString()]) {
this.setPath(row, col, this.DIR_TYPE.North, this.wallOrNot(col == 0 || col == 11 ? 1 : wallProb)); // Bias for keeping walls near the left/right edges
}
if (this.settings.edgeWallBias && col < 7 && chamberPaths[row][col][this.DIR_TYPE.East] == this.PATH_TYPE.Undefined && visited[row.toString() + "_" + ((col + 1).toString()).toString()]) {
this.setPath(row, col, this.DIR_TYPE.East, this.wallOrNot(row == 0 || row == 11 ? 1 : wallProb)); // Bias for keeping walls near the top/bottom edges
}
if (this.settings.edgeWallBias && row < 7 && chamberPaths[row][col][this.DIR_TYPE.South] == this.PATH_TYPE.Undefined && visited[(row + 1).toString() + "_" + col.toString()]) {
this.setPath(row, col, this.DIR_TYPE.South, this.wallOrNot(col == 0 || col == 11 ? 1 : wallProb)); // Bias for keeping walls near the left/right edges
}
if (this.settings.edgeWallBias && col > 0 && chamberPaths[row][col][this.DIR_TYPE.West] == this.PATH_TYPE.Undefined && visited[row.toString() + "_" + ((col - 1).toString()).toString()]) {
this.setPath(row, col, this.DIR_TYPE.West, this.wallOrNot(row == 0 || row == 11 ? 1 : wallProb)); // Bias for keeping walls near the top/bottom edges
}
var opts = [];
var paths = this.getPaths(row, col);
if (paths.north == this.PATH_TYPE.Undefined) {
opts.push(this.DIR_TYPE.North)
} else if (this.settings.edgeWallBias && row == 0 && paths.south == this.PATH_TYPE.Undefined) {
opts.push(this.DIR_TYPE.South); opts.push(this.DIR_TYPE.South); // Extra bias to turn south if at top
}
if (paths.east == this.PATH_TYPE.Undefined) {
opts.push(this.DIR_TYPE.East);
} else if (this.settings.edgeWallBias && col == 7 && paths.west == this.PATH_TYPE.Undefined) {
opts.push(this.DIR_TYPE.West); opts.push(this.DIR_TYPE.West); // Extra bias to turn west if at right
}
if (paths.south == this.PATH_TYPE.Undefined) {
opts.push(this.DIR_TYPE.South);
} else if (this.settings.edgeWallBias && row == 7 && paths.north == this.PATH_TYPE.Undefined) {
opts.push(this.DIR_TYPE.North); opts.push(this.DIR_TYPE.North); // Extra bias to turn north if at bottom
}
if (paths.west == this.PATH_TYPE.Undefined) {
opts.push(this.DIR_TYPE.West);
} else if (this.settings.edgeWallBias && col == 0 && paths.east == this.PATH_TYPE.Undefined) {
opts.push(this.DIR_TYPE.East); opts.push(this.DIR_TYPE.East); // Extra bias to turn east if at left
}
if (opts.length == 0) {
if (this.settings.debug) {
var popRow = stack[stack.length - 1][0];
var popCol = stack[stack.length - 1][1];
var cell = this.elements.chambers[popRow][popCol].querySelector(".cell");
cell.innerHTML = "";
}
stack.pop();
}
else {
var dir = opts[Math.floor(Math.random() * opts.length)];
this.setPath(row, col, dir, this.openOrDoor());
stack.push([
row + (dir == this.DIR_TYPE.North ? -1 : (dir == this.DIR_TYPE.South ? 1 : 0)),
col + (dir == this.DIR_TYPE.West ? -1 : (dir == this.DIR_TYPE.East ? 1 : 0))
]);
}
if (!this.maze.cancelTravel) {
var self = this;
if (stack.length > 0) {
if (this.settings.debug) {
setTimeout(function() {
self.travelMaze.call(self).then(() => resolve())
}, this.settings.travelSpeed);
}
else {
this.travelMaze().then(() => resolve());
}
}
else {
resolve();
}
}
});
},
removeWalls() {
return new Promise((resolve, reject) => {
if (this.settings.removeWallProb > 0 && this.settings.removeWallThreshold < 4) {
var chamberPaths = this.maze.chamberPaths;
for(let r=1; r<7; r++) {
for(let c=1; c<7; c++) {
var opts = [];
for(dir=0; dir<4; dir++) {
if (chamberPaths[r][c][dir] == this.PATH_TYPE.Wall) opts.push(dir);
}
if (opts.length >= this.settings.removeWallThreshold && Math.random() <= this.settings.removeWallProb) {
var removeDir = Math.floor(Math.random() * opts.length);
var type = this.openOrDoor();
this.setPath(r, c, opts[removeDir], type);
}
}
}
}
resolve();
});
},
initMaze() {
return new Promise((resolve, reject) => {
this.maze.stack = [[Math.floor(Math.random() * 8), Math.floor(Math.random() * 8)]];
this.maze.visited = {};
this.maze.lastChamber = null;
this.maze.cancelTravel = false;
let self = this;
this.travelMaze().then(() => setTimeout(function() {
self.removeWalls.call(self).then(() => resolve());
}, this.settings.travelSpeed));
});
},
startGame() {
this.game.level = 1
this.game.numberOfWarriors = 1;
this.game.warrior[0] = {
lives: this.settings.maxLives,
secretRoom: null,
position: null,
moves: null
};
this.game.warrior[1] = {
lives: this.settings.maxLives,
secretRoom: null,
position: null,
moves: null
};
this.game.treasure = {
room: null,
warrior: -1,
visible: false
}
this.game.dragon = {
position: null,
state: this.DRAGON_STATE.Asleep,
visible: false
};
this.selectWarriorRoom(0);
},
start() {
document.body.className += " play";
if (!this.game.init) {
this.reset();
this.game.init = true;
}
},
toggleSwitch() {
this.game.switchButtonOn = !this.game.switchButtonOn;
this.elements.buttons[0].className = this.elements.buttons[0].className.replace(" selected", "");
if (this.game.switchButtonOn) this.elements.buttons[0].className += " selected";
},
toggleSwitchOff() {
if (this.game.switchButtonOn) {
this.elements.buttons[0].className = this.elements.buttons[0].className.replace(" selected", "");
this.game.switchButtonOn = false;
}
},
toggleLevel() {
this.game.level = this.game.level == 1 ? 2 : 1;
let sound = this.game.level == 1 ? this.audio.levelOne : this.audio.levelTwo;
this.help("Switched to game level " + (this.game.level == 1 ? "one (no doors)" : "two (with doors)"));
this.playAudio(sound);
},
playAudioClick(index, secondary) {
if (!secondary) secondary = this.game.switchButtonOn ? true : false;
this.toggleSwitchOff();
let message = "Playing sound \"" + this.audioNameIndex[secondary ? 1 : 0][index] + "\"";
this.help(message, true, true);
var timestamp = (new Date()).getTime();
this.playAudio(this.audioIndex[secondary ? 1 : 0][index]).then(() => {
let duration = (new Date()).getTime() - timestamp;
if (duration < 1000) {
var self = this;
setTimeout(function() { self.resetHelpMessage.call(self); }, 1000 - duration);
}
else {
this.resetHelpMessage();
}
});
},
numToText(warriorNumber, oneText, twoText) {
return warriorNumber == 0 ? (oneText ? oneText : "one") : (twoText ? twoText : "two");
},
selectWarriorRoom(warriorNumber) {
this.game.state = warriorNumber == 0 ? this.GAME_STATE.WarriorOneSelectRoom : this.GAME_STATE.WarriorTwoSelectRoom;
this.playAudio(warriorNumber == 0 ? this.audio.warriorOne : this.audio.warriorTwo);
this.help("Pick a secret room for warrior " + this.numToText(warriorNumber, "one", "two or tap \"Next Turn\" for a one player game"))
this.log("Select secret room for warrior " + this.numToText(warriorNumber));
},
illegalWarriorRoom(warriorNumber) {
this.log("No room selected for warrior " + this.numToText(warriorNumber));
this.help("Invalid secret room", true);
this.playAudio(this.audio.illegalMove);
},
nextTurn() {
if (this.game.switchButtonOn) {
this.toggleSwitchOff();
this.toggleLevel();
}
else {
if (this.game.state == this.GAME_STATE.WarriorOneSelectRoom) {
if (!this.game.warrior[0].secretRoom) {
this.illegalWarriorRoom(0);
}
else {
this.selectWarriorRoom(1);
}
} else if (this.game.state == this.GAME_STATE.WarriorTwoSelectRoom) {
this.startTurns();
} else if (this.game.state == this.GAME_STATE.WarriorOneTurn) {
this.finishWarriorTurn(0);
} else if (this.game.state == this.GAME_STATE.WarriorTwoTurn) {
this.finishWarriorTurn(1);
}
}
},
startTurns() {
document.body.className += " gameStarted";
this.game.numberOfWarriors = this.game.warrior[1].secretRoom ? 2 : 1;
this.log("Number of players: " + this.game.numberOfWarriors);
this.setTreasureRoom();
this.log("Treasure room at: " + this.game.treasure.room);
this.warriorTurn(0);
},
warriorTurn(warriorNumber) {
this.game.state = warriorNumber == 0 ? this.GAME_STATE.WarriorOneTurn : this.GAME_STATE.WarriorTwoTurn;
let moves = this.resetWarriorMoves(warriorNumber)
let message = "Warrior " + this.numToText(warriorNumber) + "'s turn with " + moves + " move" + (moves == 1 ? "" : "s");
this.help(message);
this.log(message);
this.playAudio(warriorNumber == 0 ? this.audio.warriorOne : this.audio.warriorTwo);
},
illegalMove(warriorNumber, chamber) {
this.help("Illegal move for warrior " + this.numToText(warriorNumber), true);
this.log("Illegal move to " + chamber + " for warrior " + this.numToText(warriorNumber));
this.playAudio(this.audio.illegalMove);
},
chamberSelect(chamber, event) {
if (!this.game.delayTimeout) {
var self = this;
this.game.delayTimeout = setTimeout(function() { self.game.delayTimeout = null; }, 250);
if (!this.audioPlayTimeout) {
if (this.game.state == this.GAME_STATE.WarriorOneSelectRoom) {
this.setWarriorRoom(0, chamber);
} else if (this.game.state == this.GAME_STATE.WarriorTwoSelectRoom) {
if (chamber[0] == this.game.warrior[0].secretRoom[0] && chamber[1] == this.game.warrior[0].secretRoom[1]) {
this.playAudio(this.audio.illegalMove);
}
else {
this.setWarriorRoom(1, chamber);
}
} else if (this.game.state == this.GAME_STATE.WarriorOneTurn || this.game.state == this.GAME_STATE.WarriorTwoTurn) {
let warriorNumber = this.game.state == this.GAME_STATE.WarriorOneTurn ? 0 : 1;
this.warriorMoving(warriorNumber, chamber);
} else if (this.game.state == this.GAME_STATE.GameOver) {
this.log("Ignoring selection since game is over");
}
}
}
},
warriorMoving(warriorNumber, chamber) {
if (this.settings.debug) {
document.querySelectorAll(".treasureOptionDebug").forEach(function(optionChamber){
optionChamber.className = optionChamber.className.replace(" treasureOptionDebug", "");
});
document.querySelectorAll(".treasureDebug").forEach(function(optionChamber){
optionChamber.className = optionChamber.className.replace(" treasureDebug", "");
});
}
let position = this.game.warrior[warriorNumber].position;
let dir = this.getMoveDirection(this.game.warrior[warriorNumber].position, chamber);
if (dir == null) {
this.illegalMove(warriorNumber, chamber);
}
else {
this.game.warrior[warriorNumber].moves--;
let type = this.getPathType(position, dir);
let warriorName = this.numToText(warriorNumber);
if (type == this.PATH_TYPE.Wall) {
let message = "Warrior " + warriorName + " moves into wall";
this.help(message, true);
this.log(message);
this.renderWall(position, dir);
this.playAudio(this.audio.wall).then(() => {
this.finishWarriorTurn(warriorNumber);
});
} else if (this.game.level == 2 && type == this.PATH_TYPE.Door) {
if (Math.random() <= this.settings.doorClosedProb) {
let message = "Warrior " + warriorName + " moves into closed door";
this.help(message);
this.log(message);
this.playAudio(this.audio.door).then(() => {
this.finishWarriorTurn(warriorNumber);
});
}
else {
this.log("Warrior " + warriorName + " moves successfully thru door");
this.warriorMoved(warriorNumber, chamber);
}
} else {
this.log("Warrior " + warriorName + " moves successfully");
this.warriorMoved(warriorNumber, chamber);
}
}
},
warriorMoved(warriorNumber, chamber) {
this.playAudio(this.audio.warriorMoves).then(() => {
this.game.warrior[warriorNumber].position = chamber;
this.renderWarriorPosition(warriorNumber);
this.checkTreasureFound(warriorNumber).then(() => {
if (this.game.treasure.warrior == warriorNumber && this.getDistance(chamber, this.game.warrior[warriorNumber].secretRoom) == 0) {
this.help("Warrior " + this.numToText(warriorNumber) + " wins!!");
this.log("Warrior " + this.numToText(warriorNumber) + " wins");
this.trackEvent("Game Won");
this.game.warrior[warriorNumber].moves = 0;
this.playAudio(this.audio.winner).then(() => {
this.game.state = this.GAME_STATE.GameOver;
});
}
else {
this.checkWarriorBattle().then(() => {
this.checkDragonWakes(warriorNumber).then(() => {
this.checkDragonAttacks().then((attacked) => {
if (attacked) {
this.game.warrior[warriorNumber].moves = 0;
}
let remainingMoves = this.game.warrior[warriorNumber].moves;
if (remainingMoves == 0) {
this.finishWarriorTurn(warriorNumber)
}
else {
let message = "Warrior " + this.numToText(warriorNumber) + " has " + remainingMoves + " move" + (remainingMoves == 1 ? "" : "s") + " remaining";
this.help(message);
this.log(message);
};
});
});
});
}
});
});
},
checkWarriorBattle() {
return new Promise((resolve, reject) => {
let bothWarriorsLive = this.game.numberOfWarriors == 2 && this.game.warrior[0].lives > 0 && this.game.warrior[1].lives > 0;
if (bothWarriorsLive && this.game.treasure.warrior >=0 && this.getDistance(this.game.warrior[0].position, this.game.warrior[1].position) == 0) {
this.log("Warriors battle for the treasure");
let winner = Math.floor(Math.random() * 2);
let message = "Warrior " + (winner == 0 ? "one" : "two") + " " + (winner == this.game.treasure.warrior ? "keeps" : "steals") + " the treasure";
this.game.treasure.warrior = winner;
this.help(message);
this.log(message);
this.playAudio(winner == 0 ? this.audio.warriorOne : this.audio.warriorTwo).then(() =>{
this.playAudio(this.audio.treasure).then(() =>{
resolve();
})
})
}
else {
resolve();
}
});
},
renderWall(position, dir) {
if (this.getPathType(position, dir) == this.PATH_TYPE.Wall) {
let wall = null;
if (dir == this.DIR_TYPE.North && position[0] != 0) {
wall = this.elements.chambers[position[0] - 1][position[1]].querySelector(".southWall");
} else if (dir == this.DIR_TYPE.West && position[1] != 0) {
wall = this.elements.chambers[position[0]][position[1] - 1].querySelector(".eastWall");
} else if (dir == this.DIR_TYPE.South && position[0] != 7) {
wall = this.elements.chambers[position[0]][position[1]].querySelector(".southWall");
} else if (dir == this.DIR_TYPE.East && position[1] != 7) {
wall = this.elements.chambers[position[0]][position[1]].querySelector(".eastWall");
}
if (wall) {
wall.style.visibility = "visible";
}
}
},
finishWarriorTurn(currentWarrior) {
if (this.game.state != this.GAME_STATE.GameOver) {
if (currentWarrior == 0 && this.game.numberOfWarriors == 2 && this.game.warrior[1].lives > 0) {
this.warriorTurn(1);
}
else {
this.moveDragon().then(() => {
this.warriorTurn(this.game.warrior[0].lives > 0 ? 0 : 1);
});
}
}
},
checkDragonWakes(warriorNumber) {
return new Promise((resolve, reject) => {
if (this.game.dragon.state == this.DRAGON_STATE.Asleep) {
if (this.getDistance(this.game.warrior[warriorNumber].position, this.game.warrior[warriorNumber].secretRoom) != 0) {
let dragonDistance = this.getDistance(this.game.warrior[warriorNumber].position, this.game.dragon.position);
if (dragonDistance <= this.settings.dragonWakeDistance) {
this.help("Dragon awakes!");
this.log("Dragon awakes");
this.trackEvent("Dragon Awakes");
this.playAudio(this.audio.dragonWakes).then(() => {
this.help("");
this.game.dragon.state = this.DRAGON_STATE.Awake;
resolve();
});
}
else {
resolve();
}
}
else {
resolve();
}
}
else {
resolve();
}
});
},
unsafeWarriors: function() {
let warriors = [];
if (this.game.warrior[0].lives > 0 && !this.warriorSafe(0)) warriors.push(0);
if (this.game.numberOfWarriors == 2 && this.game.warrior[1].lives > 0 && !this.warriorSafe(1)) warriors.push(1);
return warriors;
},
moveDragon() {
return new Promise((resolve, reject) => {
if (this.game.dragon.state == this.DRAGON_STATE.Awake) {
let dragonFlies = true;
let moveTowards = null;
let followWarrior = -1;
if (this.game.treasure.warrior < 0) {
let warriors = this.unsafeWarriors();
if (warriors.length == 2) {
let warriorOneDistance = this.getDistance(this.game.dragon.position, this.game.warrior[0].position);
let warriorTwoDistance = this.getDistance(this.game.dragon.position, this.game.warrior[1].position);
followWarrior = warriorOneDistance < warriorTwoDistance ? 0 : (warriorOneDistance > warriorTwoDistance ? 1 : Math.floor(Math.random() * 2));
}
else {
followWarrior = warriors[0];
}
}
if (this.game.treasure.warrior >= 0) {
followWarrior = this.game.treasure.warrior;
let message = "Dragon follows treasure with warrior " + (followWarrior == 0 ? "one" : "two");
this.help(message, true);
this.log(message);
moveTowards = this.game.warrior[followWarrior].position;
} else if (followWarrior >= 0) {
let message = "Dragon follows warrior " + (followWarrior == 0 ? "one" : "two");
if (this.game.dragon.visible) {
this.help(message, true);
}
else {
let warriorText = "closest warrior";
if (this.game.numberOfWarriors == 1 || this.game.warrior[1].lives == 0) {
warriorText = "warrior one";
}
else if (this.game.numberOfWarriors == 2 && this.game.warrior[0].lives == 0) {
warriorText = "warrior two";
}
this.help("Dragon follows " + warriorText, true);
}
this.log(message);
moveTowards = this.game.warrior[followWarrior].position;
}
else if (this.getDistance(this.game.dragon.position, this.game.treasure.room) > 0) {
this.log("Dragon returns towards treasure");
if (!this.game.treasure.visible) {
this.help("Dragon moves to an unknown location towards the treasure room", true);
this.log("Hiding dragon since the treasure room location is unknown");
this.game.dragon.visible = false;
}
else {
this.help("Dragon moves back towards the treasure room", true);
}
moveTowards = this.game.treasure.room;
} else {
this.log("Dragon stays in treasure room");
if (!this.game.treasure.visible) {
this.game.dragon.visible = false;
}
dragonFlies = false;
}
if (dragonFlies) {
if (moveTowards) {
let lastPosition = this.game.dragon.position;
let moveY = Math.sign(moveTowards[0] - lastPosition[0]);
let moveX = Math.sign(moveTowards[1] - lastPosition[1]);
let newPosition = [
lastPosition[0] + moveY,
lastPosition[1] + moveX
];
this.game.dragon.position = newPosition;
this.renderDragonPosition();
}
this.playAudio(this.audio.dragonFlying).then(() => {
this.checkDragonAttacks().then(() => {
resolve();
});
});
}
else {
resolve();
}
}
else {
resolve();
}
});
},
checkDragonAttacks() {
return new Promise((resolve, reject) => {
let warriors = this.unsafeWarriors();
let self = this;
let threatenedWarriors = [];
warriors.forEach(function(warrior) {
if (self.getDistance(self.game.dragon.position, self.game.warrior[warrior].position) == 0) threatenedWarriors.push(warrior);
});
let attackWarrior = threatenedWarriors.length == 1 ? threatenedWarriors[0] : -1;
let multipleWarriors = threatenedWarriors.length == 2;
if (multipleWarriors) {
if (this.game.treasure.warrior >= 0) {
attackWarrior = this.game.treasure.warrior;
}
else {
attackWarrior = Math.floor(Math.random() * 2);
}
}
if (attackWarrior >= 0 ) {
this.dragonAttack(attackWarrior).then((attacked) => {
resolve(true);
});
}
else {
resolve(false);
}
});
},
dragonAttack(warriorNumber) {
return new Promise((resolve, reject) => {
let message = "Dragon attacks warrior " + this.numToText(warriorNumber);
this.help(message);
this.log(message);
if (!this.game.dragon.visible) {
this.game.dragon.visible = true;
this.renderDragonPosition();
}
this.playAudio(warriorNumber == 0 ? this.audio.warriorOne : this.audio.warriorTwo).then(() => {
this.completeAttack(warriorNumber).then(() => {
resolve();
});
});
});
},
completeAttack(warriorNumber) {
return new Promise((resolve, reject) => {
if (this.game.treasure.warrior == warriorNumber) this.game.treasure.warrior = -1;
this.playAudio(this.audio.dragonAttacks).then(() => {
this.help("");
this.renderTreasurePosition();
this.removeLife(warriorNumber).then(() => {
resolve();
})
});
});
},
removeLife(warriorNumber) {
return new Promise((resolve, reject) => {
this.game.warrior[warriorNumber].lives--;
let lives = this.game.warrior[warriorNumber].lives;
this.log("Warrior " + this.numToText(warriorNumber) + " has " + lives + " lives");
if (lives < 1) {
this.killWarrior(warriorNumber).then(() => {
resolve();
});
}
else {
this.help("Warrior " + this.numToText(warriorNumber) + " has " + lives + " lives left", true);
var secretRoom = this.game.warrior[warriorNumber].secretRoom;
this.game.warrior[warriorNumber].position = secretRoom;
this.renderWarriorPosition(warriorNumber);
resolve();
}
});
},
killWarrior(warriorNumber) {
return new Promise((resolve, reject) => {
let message = "Warrior " + this.numToText(warriorNumber) + " has been killed!";
if (this.game.warrior[0].lives < 1 && (this.game.numberOfWarriors == 1 || this.game.warrior[1].lives < 1)) {
this.game.state = this.GAME_STATE.GameOver;
this.trackEvent("Game Lost");
}
this.trackEvent("Warrior Killed");
this.help(message, true);
this.log(message);
this.playAudio(this.audio.defeat).then(() => {
this.removeWarrior(warriorNumber);
this.removeSecretRoom(warriorNumber);
resolve();
});
});
},
checkTreasureFound(warriorNumber) {
return new Promise((resolve, reject) => {
let warriorPosition = this.game.warrior[warriorNumber].position;
let treasureRoom = this.game.treasure.room;
if (this.game.treasure.warrior < 0 && this.getDistance(warriorPosition, treasureRoom) == 0) {
let message = "Warrior " + this.numToText(warriorNumber) + " found the treasure!";
this.trackEvent("Treasure Found");
this.help(message, true);
this.log(message);
this.game.warrior[warriorNumber].moves = 0;
this.game.treasure.warrior = warriorNumber;
this.game.treasure.visible = true;
this.renderTreasureMarker();
this.renderTreasurePosition();
this.playAudio(this.audio.treasure).then(() => {
resolve();
})
}
else {
resolve();
}
});
},
renderTreasureMarker() {
let treasureRoom = this.game.treasure.room;
let treasureMarker = document.createElement("div");
treasureMarker.className = "treasureRoom";
let chamberElement = this.elements.chambers[treasureRoom[0]][treasureRoom[1]];
chamberElement.appendChild(treasureMarker);
},
warriorSafe(warriorNumber) {
return this.getDistance(this.game.warrior[warriorNumber].position, this.game.warrior[warriorNumber].secretRoom) == 0;
},
resetWarriorMoves(warriorNumber) {
let moves = this.settings.movesPerLife + (this.game.warrior[warriorNumber].lives * this.settings.movesPerLife);
if (this.game.treasure.warrior == warriorNumber) moves = this.settings.movesWithTreasure;
this.game.warrior[warriorNumber].moves = moves;
return moves;
},
getMoveDirection(startPos, nextPos) {
let dir = null;
let distance = this.getDistance(startPos, nextPos);
if (distance == 1) {
let y = startPos[0] - nextPos[0];
let x = startPos[1] - nextPos[1];
if (y == 1) {
dir = this.DIR_TYPE.North;
} else if (y == -1) {
dir = this.DIR_TYPE.South;
} else if (x == 1) {
dir = this.DIR_TYPE.West;
} else if (x == -1) {
dir = this.DIR_TYPE.East;
}
}
return dir;
},
getPathType(position, dir) {
return this.maze.chamberPaths[position[0]][position[1]][dir];
},
setWarriorRoom(warriorNumber, chamber) {
this.help("Pick a different room or tap \"Next Turn\"");
this.log("Secret room for warrior " + this.numToText(warriorNumber) + " is " + chamber.toString());
if (this.settings.debug) {
document.querySelectorAll(".traveledDebug,.travelingDebug").forEach(function(chamber) {
chamber.className = chamber.className.replace(" traveledDebug", "").replace(" travelingDebug", "");
});
var container = document.querySelector("#container:not(.layoutDebug)");
if (container) container.className += " layoutDebug";
}
this.playAudio(this.audio.warriorMoves);
this.removeSecretRoom(warriorNumber);
let markerClass = "secretRoom" + this.numToText(warriorNumber, "One", "Two");
let warriorMarker = document.createElement("div");
warriorMarker.className = markerClass;
let chamberElement = this.elements.chambers[chamber[0]][chamber[1]];
chamberElement.appendChild(warriorMarker);
this.game.warrior[warriorNumber].secretRoom = chamber;
this.game.warrior[warriorNumber].position = chamber;
this.renderWarriorPosition(warriorNumber);
},
removeSecretRoom(warriorNumber) {
let secretRoom = this.game.warrior[warriorNumber].secretRoom;
let markerClass = "secretRoom" + this.numToText(warriorNumber, "One", "Two");
if (secretRoom) {
let chamberElement = this.elements.chambers[secretRoom[0]][secretRoom[1]];
let warriorMarker = chamberElement.querySelector("." + markerClass);
if (warriorMarker) {
chamberElement.removeChild(warriorMarker);
}
}
},
renderWarriorPosition(warriorNumber) {
this.removeWarrior(warriorNumber);
let name = "warrior" + this.numToText(warriorNumber, "One", "Two");
let position = this.game.warrior[warriorNumber].position;
let warriorMarker = document.createElement("div");
warriorMarker.id = name;
warriorMarker.className = name;
warriorMarker.style.zIndex = (position[0] * 100) + warriorNumber + 1;
let chamberElement = this.elements.chambers[position[0]][position[1]];
chamberElement.appendChild(warriorMarker);
if (this.game.treasure.warrior == warriorNumber) this.renderTreasurePosition();
},
removeWarrior(warriorNumber) {
let name = "warrior" + this.numToText(warriorNumber, "One", "Two");
let warriorElement = document.getElementById(name);
if (warriorElement) {
warriorElement.parentNode.removeChild(warriorElement);
}
},
renderDragonPosition() {
let dragonElement = document.getElementById("dragon");
if (dragonElement) {
dragonElement.parentNode.removeChild(dragonElement);
}
if (this.game.dragon.visible || this.settings.debug) {
let position = this.game.dragon.position;
let dragonMarker = document.createElement("div");
dragonMarker.id = "dragon";
dragonMarker.className = "dragon";
dragonMarker.style.zIndex = (position[0] * 100) + 4;
let chamberElement = this.elements.chambers[position[0]][position[1]];
chamberElement.appendChild(dragonMarker);
}
},
renderTreasurePosition() {
if (this.game.treasure.visible || this.settings.debug) {
let treasureElement = document.getElementById("treasure");
if (treasureElement) {
treasureElement.parentNode.removeChild(treasureElement);
}
let withWarrior = this.game.treasure.warrior >= 0;
let position = this.game.treasure.room;
if (withWarrior) {
position = this.game.warrior[this.game.treasure.warrior].position;
}
let treasureMarker = document.createElement("div");
treasureMarker.id = "treasure";
treasureMarker.className = "treasure" + (withWarrior ? " withWarrior" : "");
treasureMarker.style.zIndex = (position[0] * 100) + 3;
let chamberElement = this.elements.chambers[position[0]][position[1]];
chamberElement.appendChild(treasureMarker);
}
},
setTreasureRoom() {
let opts = [];
for(let row=0; row<8; row++) {
for(let col=0; col<8; col++) {
if (this.getDistance([row,col], this.game.warrior[0].secretRoom) > this.settings.treasureRoomDistance) {
if (this.game.numberOfWarriors == 1 || this.getDistance([row,col], this.game.warrior[1].secretRoom) > this.settings.treasureRoomDistance) {
opts.push([row,col]);
if (this.settings.debug) this.elements.chambers[row][col].className += " treasureOptionDebug";
}
}
}
}
let treasurePos = this.game.treasure.room = opts[Math.floor(Math.random() * opts.length)];
if (this.settings.debug) this.elements.chambers[treasurePos[0]][treasurePos[1]].className += " treasureDebug";
this.game.treasure.room = treasurePos;
this.game.dragon.position = treasurePos;
if (this.settings.debug) {
this.renderDragonPosition();
this.renderTreasurePosition();
}
},
getDistance(posA, posB) {
return Math.abs(posA[0] - posB[0]) + Math.abs(posA[1] - posB[1]);
},
help(message, temporary, skipTimeout) {
if (!temporary) {
this.game.helpMessage = message;
}
if (temporary || !this.game.helpTimeout) {
this.elements.help.innerText = message;
if (temporary && !skipTimeout) {
if (this.game.helpTimeout) clearTimeout(this.game.helpTimeout);
var self = this;
this.game.helpTimeout = setTimeout(function() {
self.game.helpTimeout = null;
self.resetHelpMessage.call(self);
}, 2500);
}
}
},
resetHelpMessage() {
this.elements.help.innerText = this.game.helpMessage;
}
}
window.addEventListener('DOMContentLoaded', (event) => { labyrinth.init() });