999 lines
28 KiB
JavaScript
999 lines
28 KiB
JavaScript
(function (w) {
|
|
var playerStats = document.getElementById("playerStats").innerHTML;
|
|
|
|
function convertData(stats) {
|
|
var player = {};
|
|
stats = stats.replace("'", "").replace("{","").replace("}","");
|
|
stats = stats.split(",");
|
|
for (let i = 0; i < stats.length; i++) {
|
|
var parts = stats[i].split(":");
|
|
if (isNaN(Number(parts[1].replace("'","").replace('"','').replace('"','')))) {
|
|
player[parts[0].replace("'","").replace('"','').replace('"','')] = parts[1].replace("'","").replace('"','').replace('"','');
|
|
} else {
|
|
player[parts[0].replace("'","").replace('"','').replace('"','')] = Number(parts[1].replace("'","").replace('"','').replace('"',''));
|
|
}
|
|
}
|
|
return player;
|
|
}
|
|
|
|
var statsOfPlayer = convertData(playerStats);
|
|
let count = 1;
|
|
// game title
|
|
const gametitle = "The Math Wizard";
|
|
|
|
/*****************
|
|
*** resources ***
|
|
*****************/
|
|
|
|
// This tileset is from kenney.nl
|
|
// It's the "microrogue" tileset
|
|
|
|
const tileSet = document.createElement("img");
|
|
tileSet.src = "./../images/16x16DungeonTileset.png";
|
|
|
|
const tileOptions = {
|
|
layout: "tile",
|
|
bg: "transparent",
|
|
tileWidth: 16,
|
|
tileHeight: 16,
|
|
tileSet: tileSet,
|
|
tileMap: {
|
|
"@": [144, 224], // player
|
|
".": [32, 48], // floor
|
|
"M": [32, 160], // normal orc
|
|
"a": [0, 160], // small orc
|
|
"b": [80, 176], // Boss
|
|
"g": [272, 144], // gold
|
|
"p": [192, 176], // potion
|
|
"T": [112, 160], // tombstone
|
|
"╔": [336, 160], // room corner
|
|
"╗": [368, 160], // room corner
|
|
"╝": [368, 192], // room corner
|
|
"╚": [336, 192], // room corner
|
|
"═": [352, 160], // room edge
|
|
"║": [336, 176], // room edge
|
|
"o": [40, 72], // room corner
|
|
"D": [416, 176], //Door to win
|
|
"s": [288, 112], //stairs to next Stage
|
|
},
|
|
width: 25,
|
|
height: 40,
|
|
};
|
|
|
|
//const usePointer = true;
|
|
//const useArrows = true;
|
|
const touchOffsetY = -20; // move the center by this much
|
|
const scaleMonitor = 3; // scale computer screens by this much
|
|
const turnLengthMS = 200; // shortest time between turns
|
|
|
|
// these map tiles are walkable
|
|
const walkable = [".", "g", "D", "s"];
|
|
|
|
// these map tiles should not be replaced by room edges
|
|
const noreplace = walkable.concat(["M", "╔", "╗", "╚", "╝", "═", "║"]);
|
|
|
|
// These sound effects are generated using sfxr.me
|
|
|
|
const sfx = {
|
|
rubber:
|
|
"5EoyNVaezhPnpFZjpkcJkF8FNCioPncLoztbSHU4u9wDQ8W3P7puffRWvGMnrLRdHa61kGcwhZK3RdoDRitmtwn4JjrQsZCZBmDQgkP5uGUGk863wbpRi1xdA",
|
|
step: "34T6PkwiBPcxMGrK7aegATo5WTMWoP17BTc6pwXbwqRvndwRjGYXx6rG758rLSU5suu35ZTkRCs1K2NAqyrTZbiJUHQmra9qvbBrSdbBbJ7JvmyBFVDo6eiVD",
|
|
choice:
|
|
"34T6PkzXyyB6jHiwFztCFWEWsogkzrhzAH3FH2d97BCuFhqmZgfuXG3xtz8YYSKMzn95yyX8xZXJyesKmpcjpEL3dPP5h2e8mt5MmhExAksyqZyqgavBgsWMd",
|
|
hide: "34T6PkzXyyB6jHiwFztCFWEniygA1GJtjsQuGxcd38JLDquhRqTB28dQgigseMjQSjSY14Z3aBmAtzz9KWcJZ2o9S1oCcgqQY4dxTAXikS7qCs3QJ3KuWJUyD",
|
|
empty:
|
|
"111112RrwhZ2Q7NGcdAP21KUHHKNQa3AhmK4Xea8mbiXfzkxr9aX41M8XYt5xYaaLeo9iZdUKUVL3u2N6XASue2wPv2wCCDy6W6TeFiUjk3dXSzFcBY7kTAM",
|
|
hit: "34T6Pks4nddGzchAFWpSTRAKitwuQsfX8bfzRpJx5eDR7NSqxeeLMEkLjcuwvTCDS1ve7amXBg4eipzDdgKWoYnJBsQVESZh2X1DFV2GWybY5bAihB2EdHsbd",
|
|
miss: "8R25jogvbp3Qy6A4GTPxRP4aT2SywwsAgoJ2pKmxUFMExgNashjgd311MnmZ2ThwrPQz71LA53QCfFmYQLHaXo6SocUv4zcfNAU5SFocZnoQSDCovnjpioNz3",
|
|
win: "34T6Pkv34QJsqDqEa8aV4iwF2LnASMc3683oFUPKZic6kVUHvwjUQi6rz8qNRUHRs34cu37P5iQzz2AzipW3DHMoG5h4BZgDmZnyLhsXgPKsq2r4Fb2eBFVuR",
|
|
lose: "7BMHBGHKKnn7bgcmGprqiBmpuRaTytcd4JS9eRNDzUTRuQy8BTBzs5g8XzS7rrp4C9cNeSaqAtWR9qdvXvtnWVTmTC8GXgDuCXD2KyHJNXzfUahbZrce8ibuy",
|
|
kill: "7BMHBGKMhg8NZkxKcJxNfTWXKtMPiZVNsLR4aPEAghCSpz5ZxpjS5k4j4ZQpJ65UZnHSr4R2d7ALCHJe41pAS2ZPjauM7SveudhDGAxw2dhXpiNwEhG8xUYkX",
|
|
};
|
|
|
|
for (let s in sfx) {
|
|
sfx[s] = new SoundEffect(sfx[s]).generate().getAudio();
|
|
}
|
|
|
|
const keyMap = {
|
|
38: 0,
|
|
33: 1,
|
|
39: 2,
|
|
34: 3,
|
|
40: 4,
|
|
35: 5,
|
|
37: 6,
|
|
36: 7,
|
|
};
|
|
|
|
/*****************
|
|
*** game code ***
|
|
*****************/
|
|
|
|
// based on the original tutorial by Ondřej Žára
|
|
// www.roguebasin.com/index.php?title=Rot.js_tutorial,_part_1
|
|
|
|
const Game = {
|
|
// this is the ROT.js display handler
|
|
display: null,
|
|
// this is our map data
|
|
map: {},
|
|
// map of all items
|
|
items: {},
|
|
// reference to the ROT.js engine which
|
|
// manages stuff like scheduling
|
|
engine: null,
|
|
// schedules events in the game for ROT.js
|
|
scheduler: null,
|
|
// reference to the player object
|
|
player: null,
|
|
// reference to the game monsters array
|
|
monsters: null,
|
|
door: null,
|
|
// arrow handler
|
|
lastArrow: null, // arrow keys held
|
|
arrowInterval: null, // arrow key repeat
|
|
arrowListener: null, // registered listener for arrow event
|
|
// clean up this game instance
|
|
cleanup: cleanup,
|
|
playerAllowedToMove: true,
|
|
};
|
|
|
|
// this gets called by the menu system
|
|
// to launch the actual game
|
|
function init(game) {
|
|
game.map = {};
|
|
game.items = {};
|
|
// first create a ROT.js display manager
|
|
game.display = new ROT.Display(tileOptions);
|
|
resetCanvas(game.display.getContainer());
|
|
|
|
generateMap(game, count);
|
|
|
|
// let ROT.js schedule the player and monster entities
|
|
game.scheduler = new ROT.Scheduler.Simple();
|
|
game.scheduler.add(game.player, true);
|
|
game.monsters.map((m) => game.scheduler.add(m, true));
|
|
|
|
// render the stats hud at the bottom of the screen
|
|
renderStats(game.player.stats);
|
|
|
|
// kick everything off
|
|
game.engine = new ROT.Engine(game.scheduler);
|
|
game.engine.start();
|
|
count = 1;
|
|
}
|
|
|
|
function nextStage(game, stage, stats) {
|
|
game.map = {};
|
|
game.items = {};
|
|
game.display = new ROT.Display(tileOptions);
|
|
resetCanvas(game.display.getContainer());
|
|
if (game.engine) {
|
|
game.scheduler.clear();
|
|
game.scheduler = null;
|
|
game.monsters = null;
|
|
game.door = null;
|
|
game.stairs = null;
|
|
};
|
|
generateMap(game, stage);
|
|
|
|
// let ROT.js schedule the player and monster entities
|
|
game.scheduler = new ROT.Scheduler.Simple();
|
|
game.scheduler.add(game.player, true);
|
|
game.monsters.map((m) => game.scheduler.add(m, true));
|
|
|
|
// render the stats hud at the bottom of the screen
|
|
game.player.stats = stats;
|
|
renderStats(game.player.stats);
|
|
|
|
// kick everything off
|
|
game.engine = new ROT.Engine(game.scheduler);
|
|
game.engine.start();;
|
|
}
|
|
|
|
// this gets called at the end of the game when we want
|
|
// to exit back out and clean everything up to display
|
|
// the menu and get ready for next round
|
|
function destroy(game) {
|
|
// remove all listening event handlers
|
|
removeListeners(game);
|
|
|
|
// tear everything down
|
|
if (game.engine) {
|
|
game.engine.lock();
|
|
game.display = null;
|
|
game.map = {};
|
|
game.items = {};
|
|
game.engine = null;
|
|
game.scheduler.clear();
|
|
game.scheduler = null;
|
|
game.player = null;
|
|
game.monsters = null;
|
|
game.door = null;
|
|
game.stairs = null;
|
|
}
|
|
|
|
// hide the toast message
|
|
hideToast(true);
|
|
// close out the game screen and show the title
|
|
showScreen("title");
|
|
}
|
|
|
|
// this generates the game map
|
|
function generateMap(game, stage) {
|
|
const digger = new ROT.Map.Digger(tileOptions.width, tileOptions.height);
|
|
// list of floor tiles that can be walked on
|
|
const freeCells = [];
|
|
// list of non-floor tiles that can't be traversed
|
|
const zeroCells = [];
|
|
|
|
const digCallback = function (x, y, value) {
|
|
const key = x + "," + y;
|
|
if (value) {
|
|
zeroCells.push(key);
|
|
} else {
|
|
game.map[key] = ".";
|
|
freeCells.push(key);
|
|
}
|
|
};
|
|
digger.create(digCallback.bind(game));
|
|
|
|
generateItems(game, freeCells);
|
|
generateRooms(game.map, digger);
|
|
|
|
game.player = createBeing(makePlayer, freeCells);
|
|
game.monsters = []
|
|
if (stage <= 4) {
|
|
for ( var i= 0; i<= stage; i++) {
|
|
game.monsters.push(createBeing(makeMonster, freeCells));
|
|
}
|
|
} else {
|
|
game.monsters.push(createBeing(makeMonster, freeCells));
|
|
}
|
|
|
|
// draw the map and items
|
|
for (let key in game.map) {
|
|
drawTile(game, key);
|
|
}
|
|
|
|
rescale(game.player._x, game.player._y, game);
|
|
}
|
|
|
|
function generateItems(game, freeCells) {
|
|
for (let i = 0; i < 15; i++) {
|
|
const key = takeFreeCell(freeCells);
|
|
if (!i) {
|
|
if(count < 5) {
|
|
game.stairs = freeCells[freeCells.length - 1];
|
|
game.items[freeCells[freeCells.length - 1]] = "s";
|
|
} else {
|
|
game.door = freeCells[freeCells.length - 1];
|
|
game.items[freeCells[freeCells.length - 1]] = "D";
|
|
}
|
|
} else {
|
|
game.items[key] = "g";
|
|
}
|
|
}
|
|
}
|
|
|
|
function takeFreeCell(freeCells) {
|
|
const index = Math.floor(ROT.RNG.getUniform() * freeCells.length);
|
|
const key = freeCells.splice(index, 1)[0];
|
|
return key;
|
|
}
|
|
|
|
function posFromKey(key) {
|
|
const parts = key.split(",");
|
|
const x = parseInt(parts[0]);
|
|
const y = parseInt(parts[1]);
|
|
return [x, y];
|
|
}
|
|
|
|
function generateRooms(map, mapgen) {
|
|
const rooms = mapgen.getRooms();
|
|
for (let rm = 0; rm < rooms.length; rm++) {
|
|
const room = rooms[rm];
|
|
|
|
const l = room.getLeft() - 1;
|
|
const r = room.getRight() + 1;
|
|
const t = room.getTop() - 1;
|
|
const b = room.getBottom() + 1;
|
|
|
|
map[l + "," + t] = "╔";
|
|
map[r + "," + t] = "╗";
|
|
map[l + "," + b] = "╚";
|
|
map[r + "," + b] = "╝";
|
|
|
|
for (let i = room.getLeft(); i <= room.getRight(); i++) {
|
|
const j = i + "," + t;
|
|
const k = i + "," + b;
|
|
if (noreplace.indexOf(map[j]) == -1) {
|
|
map[j] = "═";
|
|
}
|
|
if (noreplace.indexOf(map[k]) == -1) {
|
|
map[k] = "═";
|
|
}
|
|
}
|
|
|
|
for (let i = room.getTop(); i <= room.getBottom(); i++) {
|
|
const j = l + "," + i;
|
|
const k = r + "," + i;
|
|
if (noreplace.indexOf(map[j]) == -1) {
|
|
map[j] = "║";
|
|
}
|
|
if (noreplace.indexOf(map[k]) == -1) {
|
|
map[k] = "║";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function drawTile(game, key, ignore) {
|
|
const map = game.map;
|
|
if (map[key]) {
|
|
const parts = posFromKey(key);
|
|
const monster = monsterAt(parts[0], parts[1]);
|
|
const player = playerAt(parts[0], parts[1]);
|
|
const display = game.display;
|
|
const items = game.items;
|
|
const draw = [map[key], items[key]];
|
|
draw.push(monster && monster != ignore ? monster.character : null);
|
|
draw.push(player && player != ignore ? player.character : null);
|
|
display.draw(
|
|
parts[0],
|
|
parts[1],
|
|
draw.filter((i) => i)
|
|
);
|
|
}
|
|
}
|
|
|
|
// both the player and monster initial position is set
|
|
function createBeing(what, freeCells) {
|
|
if (what == makePlayer) {
|
|
const pos = posFromKey(freeCells[0]);
|
|
const being = what(pos[0], pos[1]);
|
|
return being;
|
|
} else {
|
|
const key = takeFreeCell(freeCells);
|
|
const pos = posFromKey(key);
|
|
const being = what(pos[0], pos[1]);
|
|
return being;
|
|
}
|
|
}
|
|
|
|
/******************
|
|
*** the player ***
|
|
******************/
|
|
|
|
// creates a player object with position, and stats
|
|
function makePlayer(x, y) {
|
|
return {
|
|
// player's position
|
|
_x: x,
|
|
_y: y,
|
|
character: "@",
|
|
name: statsOfPlayer['username'].replace('"',''),
|
|
// the player's stats
|
|
stats: { hp: 10, xp: 0, gold: 0 },
|
|
// the ROT.js scheduler calls this method when it is time
|
|
// for the player to act
|
|
act: () => {
|
|
Game.engine.lock();
|
|
if (!Game["arrowListener"]) {
|
|
document.addEventListener("arrow", arrowEventHandler);
|
|
Game.arrowListener = true;
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
// this method gets called by the `movePlayer` function
|
|
function checkItem(entity) {
|
|
const key = entity._x + "," + entity._y;
|
|
if (key == Game.door) {
|
|
if(count < 5) {
|
|
nextStage(Game, ++count, Game.player.stats);
|
|
} else {
|
|
win();
|
|
}
|
|
} else if (key == Game.stairs) {
|
|
nextStage(Game, ++count, Game.player.stats);
|
|
}else if (Game.items[key] == "g") {
|
|
Game.player.stats.gold += 1;
|
|
renderStats(Game.player.stats);
|
|
toast("You found gold!");
|
|
sfx["win"].play();
|
|
delete Game.items[key];
|
|
}
|
|
drawTile(Game, key);
|
|
}
|
|
|
|
function movePlayer(dir) {
|
|
const p = Game.player;
|
|
return movePlayerTo(p._x + dir[0], p._y + dir[1]);
|
|
}
|
|
|
|
function movePlayerTo(x, y) {
|
|
const p = Game.player;
|
|
|
|
const newKey = x + "," + y;
|
|
if (walkable.indexOf(Game.map[newKey]) == -1) {
|
|
return;
|
|
}
|
|
|
|
// check if we've hit the monster
|
|
const hitMonster = monsterAt(x, y);
|
|
if (hitMonster) {
|
|
setTimeout(function () {
|
|
Game.engine.unlock();
|
|
}, 250);
|
|
} else {
|
|
hideToast();
|
|
|
|
drawTile(Game, p._x + "," + p._y, p);
|
|
|
|
// update the player's coordinates
|
|
p._x = x;
|
|
p._y = y;
|
|
|
|
// re-draw the player
|
|
for (let key in Game.map) {
|
|
drawTile(Game, key);
|
|
}
|
|
// re-locate the game screen to center the player
|
|
rescale(x, y, Game);
|
|
window.removeEventListener("arrow", arrowEventHandler);
|
|
Game.engine.unlock();
|
|
sfx["step"].play();
|
|
// check if the player stepped on an item
|
|
checkItem(p);
|
|
}
|
|
}
|
|
|
|
/*******************
|
|
*** The monster ***
|
|
*******************/
|
|
|
|
// basic ROT.js entity with position and stats
|
|
function makeMonster(x, y) {
|
|
if (count === 5) {
|
|
return {
|
|
_x: x,
|
|
_y: y,
|
|
character: "b",
|
|
name: "Necromancer",
|
|
stats: {hp: 10},
|
|
act: monsterAct,
|
|
}
|
|
} else {
|
|
let randomMonster = Math.floor(Math.random() * 2);
|
|
if (randomMonster === 1) {
|
|
return {
|
|
_x: x,
|
|
_y: y,
|
|
character: "M",
|
|
name: "Orc",
|
|
stats: { hp: 4 },
|
|
act: monsterAct,
|
|
};
|
|
} else {
|
|
return {
|
|
// monster position
|
|
_x: x,
|
|
_y: y,
|
|
character: "a",
|
|
name: "Kleiner Orc",
|
|
stats: { hp: 3 },
|
|
// called by the ROT.js scheduler
|
|
act: monsterAct,
|
|
};
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
function monsterAct() {
|
|
const m = this;
|
|
const p = Game.player;
|
|
const map = Game.map;
|
|
const display = Game.display;
|
|
|
|
const passableCallback = function (x, y) {
|
|
return walkable.indexOf(map[x + "," + y]) != -1;
|
|
};
|
|
const astar = new ROT.Path.AStar(p._x, p._y, passableCallback, {
|
|
topology: 4,
|
|
});
|
|
const path = [];
|
|
const pathCallback = function (x, y) {
|
|
path.push([x, y]);
|
|
};
|
|
astar.compute(m._x, m._y, pathCallback);
|
|
|
|
path.shift();
|
|
if (path.length <= 1) {
|
|
Game.playerAllowedToMove = false;
|
|
Game.engine.lock();
|
|
combat(m, p);
|
|
} else {
|
|
drawTile(Game, m._x + "," + m._y, m);
|
|
m._x = path[0][0];
|
|
m._y = path[0][1];
|
|
drawTile(Game, m._x + "," + m._y);
|
|
}
|
|
}
|
|
|
|
function monsterAt(x, y) {
|
|
if (Game.monsters && Game.monsters.length) {
|
|
for (let mi = 0; mi < Game.monsters.length; mi++) {
|
|
const m = Game.monsters[mi];
|
|
if (m && m._x == x && m._y == y) {
|
|
return m;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function playerAt(x, y) {
|
|
return Game.player && Game.player._x == x && Game.player._y == y
|
|
? Game.player
|
|
: null;
|
|
}
|
|
|
|
// if the monster is dead remove it from the game
|
|
function checkDeath(m) {
|
|
if (m.stats.hp <= 0) {
|
|
if (m == Game.player) {
|
|
toast("You died!");
|
|
lose();
|
|
} else {
|
|
const key = m._x + "," + m._y;
|
|
removeMonster(m);
|
|
Game.player.stats.xp += 1;
|
|
sfx["kill"].play();
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// remove a monster from the game
|
|
function removeMonster(m) {
|
|
const key = m._x + "," + m._y;
|
|
Game.scheduler.remove(m);
|
|
Game.monsters = Game.monsters.filter((mx) => mx != m);
|
|
drawTile(Game, key);
|
|
}
|
|
|
|
/******************************
|
|
*** combat/win/lose events ***
|
|
******************************/
|
|
// this is how the player fights a monster
|
|
function checkSolution(solution, answer) {
|
|
if (solution == answer) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function setupButtons(answerValue) {
|
|
let operators = ["+", "-"];
|
|
let randomOperator = operators[Math.floor(Math.random() * operators.length)];
|
|
const randomValue = (min, max) =>
|
|
Math.floor(Math.random() * (max - min)) + min;
|
|
let randomVar = randomValue(1, 4);
|
|
let random1 = eval(`${answerValue}${randomOperator} ${randomValue(1, 4)}`);
|
|
let random2 = eval(`${answerValue}${randomOperator} ${randomValue(1, 4)}`);
|
|
if (random1 === random2){
|
|
random2 += 1;
|
|
if (random2 === answerValue) {
|
|
random2 += 1;
|
|
}
|
|
}
|
|
else if (random1 < 1) {
|
|
random1 = 1;
|
|
}
|
|
else if (random2 < 1) {
|
|
random2 = 1;
|
|
}
|
|
if (randomVar === 1) {
|
|
document.getElementById("answer1").innerHTML = `${answerValue}`;
|
|
document.getElementById("answer2").innerHTML = random1;
|
|
document.getElementById("answer3").innerHTML = random2;
|
|
} else if (randomVar === 2) {
|
|
document.getElementById("answer1").innerHTML = random1;
|
|
document.getElementById("answer2").innerHTML = `${answerValue}`;
|
|
document.getElementById("answer3").innerHTML = random2;
|
|
} else {
|
|
document.getElementById("answer1").innerHTML = random1;
|
|
document.getElementById("answer2").innerHTML = random2;
|
|
document.getElementById("answer3").innerHTML = `${answerValue}`;
|
|
}
|
|
showScreen("combat");
|
|
return new Promise((resolve) => {
|
|
const button1 = document.getElementById("answer1");
|
|
const button2 = document.getElementById("answer2");
|
|
const button3 = document.getElementById("answer3");
|
|
button1.addEventListener('click', () => {
|
|
resolve(button1.innerText);
|
|
});
|
|
button2.addEventListener('click', () => {
|
|
resolve(button2.innerText);
|
|
});
|
|
button3.addEventListener('click', () => {
|
|
resolve(button3.innerText);
|
|
});
|
|
});
|
|
}
|
|
|
|
async function combat(hitter, receiver) {
|
|
let msg = [];
|
|
const randomValue = (min, max) =>
|
|
Math.floor(Math.random() * (max - min)) + min;
|
|
let num1, num2;
|
|
if (statsOfPlayer['level'] > 10) {
|
|
[num1, num2] = [randomValue(1, 10), randomValue(1, 10)];
|
|
} else {
|
|
num1 = statsOfPlayer['level'];
|
|
num2 = randomValue(1, 10);
|
|
}
|
|
const answerValue = eval(`${num1} * ${num2}`);
|
|
document.getElementById("question").innerHTML = `${num1} * ${num2} = ? `;
|
|
const clicked = await setupButtons(answerValue);
|
|
let fight = checkSolution(clicked, answerValue);
|
|
if(fight) {
|
|
msg.push(`${Game.player.name} hit the monster.`);
|
|
hitter.stats.hp -= 1;
|
|
sfx["hit"].play();
|
|
} else {
|
|
sfx["miss"].play();
|
|
msg.push(`The monster hit ${Game.player.name}.`);
|
|
Game.player.stats.hp -= 1;
|
|
}
|
|
if(msg) {
|
|
toast(battleMessage(msg));
|
|
}
|
|
checkDeath(hitter);
|
|
checkDeath(receiver);
|
|
showScreen("game");
|
|
renderStats(Game.player.stats);
|
|
Game.playerAllowedToMove = true;
|
|
Game.engine.unlock();
|
|
}
|
|
|
|
// this gets called when the player wins the game
|
|
function win() {
|
|
Game.engine.lock();
|
|
Game.player.stats.xp += 10;
|
|
for (let i = 0; i < 5; i++) {
|
|
setTimeout(function () {
|
|
sfx["win"].play();
|
|
}, 100 * i);
|
|
}
|
|
// set our stats for the end screen
|
|
setEndScreenValues(Game.player.stats.xp, Game.player.stats.gold);
|
|
// tear down the game
|
|
destroy(Game);
|
|
showScreen("win");
|
|
}
|
|
|
|
// this gets called when the player loses the game
|
|
function lose() {
|
|
Game.engine.lock();
|
|
// change the player into a tombstone tile
|
|
const p = Game.player;
|
|
p.character = "T";
|
|
drawTile(Game, p._x + "," + p._y);
|
|
removeListeners(Game);
|
|
sfx["lose"].play();
|
|
setTimeout(function () {
|
|
setEndScreenValues(Game.player.stats.xp, Game.player.stats.gold);
|
|
// tear down the game
|
|
destroy(Game);
|
|
showScreen("lose");
|
|
}, 2000);
|
|
}
|
|
|
|
/************************************
|
|
*** graphics, UI & browser utils ***
|
|
************************************/
|
|
|
|
const clickevt = "click";
|
|
|
|
const $ = document.querySelector.bind(document);
|
|
const $$ = document.querySelectorAll.bind(document);
|
|
NodeList.prototype.forEach = Array.prototype.forEach;
|
|
|
|
// this code resets the ROT.js display canvas
|
|
function resetCanvas(el) {
|
|
$("#canvas").innerHTML = "";
|
|
$("#canvas").appendChild(el);
|
|
window.onkeydown = keyHandler;
|
|
window.onkeyup = arrowStop;
|
|
showScreen("game");
|
|
}
|
|
|
|
function rescale(x, y, game) {
|
|
const c = $("canvas");
|
|
const scale = scaleMonitor;
|
|
const offset = game.touchScreen ? touchOffsetY : 0;
|
|
const tw =
|
|
x * -tileOptions.tileWidth +
|
|
(tileOptions.width * tileOptions.tileWidth) / 2 +
|
|
-4;
|
|
const th =
|
|
y * -tileOptions.tileHeight +
|
|
(tileOptions.height * tileOptions.tileHeight) / 2 +
|
|
offset;
|
|
if (canvas) {
|
|
canvas.style.transition = "transform 0.5s ease-out 0s";
|
|
if (game.display) {
|
|
game.display
|
|
.getContainer()
|
|
.getContext("2d").imageSmoothingEnabled = false;
|
|
}
|
|
canvas.style.transform =
|
|
"scale(" +
|
|
scale +
|
|
") " +
|
|
"translate3d(" +
|
|
Math.floor(tw) +
|
|
"px," +
|
|
Math.floor(th) +
|
|
"px,0px)";
|
|
}
|
|
}
|
|
|
|
function removeListeners(game) {
|
|
if (game.engine) {
|
|
game.lastArrow = null;
|
|
clearInterval(game.arrowInterval);
|
|
game.arrowInterval = null;
|
|
game.engine.lock();
|
|
game.scheduler.clear();
|
|
window.removeEventListener("arrow", arrowEventHandler);
|
|
game.arrowListener = false;
|
|
window.onkeydown = null;
|
|
window.onkeyup = null;
|
|
}
|
|
}
|
|
|
|
// hides all screens and shows the requested screen
|
|
function showScreen(which, ev) {
|
|
ev && ev.preventDefault();
|
|
const el = $("#" + which);
|
|
const actionbutton = $("#" + which + ">.action");
|
|
document.querySelectorAll(".screen").forEach(function (s) {
|
|
s.classList.remove("show");
|
|
s.classList.add("hide");
|
|
});
|
|
el.classList.remove("hide");
|
|
el.classList.remove("show");
|
|
void el.offsetHeight; // trigger CSS reflow
|
|
el.classList.add("show");
|
|
if (actionbutton) {
|
|
actionbutton.focus();
|
|
}
|
|
}
|
|
|
|
// set the end-screen message
|
|
function setEndScreenValues(xp, gold) {
|
|
$$(".xp-stat").forEach((el) => (el.textContent = Math.floor(xp)));
|
|
$$(".gold-stat").forEach((el) => (el.textContent = gold));
|
|
statsOfPlayer["coins"] += gold;
|
|
statsOfPlayer["username"] = Game.player.name.trim().replace('"','');
|
|
if ((statsOfPlayer['xp'] + xp) > 150) {
|
|
statsOfPlayer["level"] = Number(statsOfPlayer["level"]) + 1;
|
|
statsOfPlayer["xp"] += xp;
|
|
statsOfPlayer["xp"] -= 150;
|
|
} else {
|
|
statsOfPlayer["level"] = Number(statsOfPlayer["level"]);
|
|
statsOfPlayer["xp"] += xp;
|
|
}
|
|
var json = JSON.stringify(statsOfPlayer);
|
|
var xhr = new XMLHttpRequest();
|
|
xhr.open("POST", "/updateData", true);
|
|
xhr.setRequestHeader("Content-Type", "application/json");
|
|
xhr.onreadystatechange = function() {
|
|
if (this.readyState == 4 && this.status == 200) {
|
|
console.log(this.responseText);
|
|
}
|
|
}
|
|
console.log(json);
|
|
xhr.send(json);
|
|
}
|
|
|
|
// updates the stats listed at the bottom of the screen
|
|
function renderStats(stats) {
|
|
const st = $("#hud");
|
|
st.innerHTML = "";
|
|
for (let s in stats) {
|
|
attach(st, el("span", {}, [s.toUpperCase() + ": " + stats[s]]));
|
|
}
|
|
}
|
|
|
|
function battleMessage(messages) {
|
|
const components = messages.reduce(function (msgs, m) {
|
|
return msgs
|
|
.concat(
|
|
m.split(" ").map(function (p) {
|
|
const match = p.match(/hit|miss/);
|
|
return el("span", { className: match ? match[0] : "" }, [p, " "]);
|
|
})
|
|
)
|
|
.concat(el("br", {}));
|
|
}, []);
|
|
return el("span", {}, components);
|
|
}
|
|
|
|
function toast(message) {
|
|
const m = $("#message");
|
|
if (
|
|
Game.scheduler._current == Game.player ||
|
|
m.className.indexOf("show") == -1
|
|
) {
|
|
m.innerHTML = "";
|
|
}
|
|
m.classList.remove("fade-out");
|
|
m.classList.add("show");
|
|
if (typeof message == "string") {
|
|
m.appendChild(el("span", {}, [message]));
|
|
} else {
|
|
m.appendChild(message);
|
|
}
|
|
}
|
|
|
|
function hideToast(instant) {
|
|
const m = $("#message");
|
|
if (instant) {
|
|
m.classList.remove("show");
|
|
m.classList.remove("fade-out");
|
|
m.innerHTML = "";
|
|
} else if (m.className.match("show")) {
|
|
m.classList.remove("show");
|
|
m.classList.add("fade-out");
|
|
m.onanimationend = function () {
|
|
m.classList.remove("fade-out");
|
|
m.innerHTML = "";
|
|
};
|
|
}
|
|
}
|
|
|
|
// create an HTML element
|
|
function el(tag, attrs, children) {
|
|
const node = document.createElement(tag);
|
|
for (a in attrs) {
|
|
node[a] = attrs[a];
|
|
}
|
|
(children || []).forEach(function (c) {
|
|
if (typeof c == "string") {
|
|
node.appendChild(document.createTextNode(c));
|
|
} else {
|
|
attach(node, c);
|
|
}
|
|
});
|
|
return node;
|
|
}
|
|
|
|
// add an HTML element to a parent node
|
|
function attach(node, el) {
|
|
node.appendChild(el);
|
|
return el;
|
|
}
|
|
|
|
// remove an element from the dom
|
|
function rmel(node) {
|
|
node.parentNode.removeChild(node);
|
|
}
|
|
|
|
/*************************
|
|
*** UI event handlers ***
|
|
*************************/
|
|
|
|
function keyHandler(ev) {
|
|
const code = ev.keyCode;
|
|
if (code == 187 || code == 189) {
|
|
ev.preventDefault();
|
|
return;
|
|
}
|
|
if (code == 70 && ev.altKey && ev.ctrlKey && ev.shiftKey) {
|
|
document.body.requestFullscreen();
|
|
return;
|
|
}
|
|
if (code == 190) {
|
|
Game.engine.unlock();
|
|
return;
|
|
} // skip turn
|
|
if (!(code in keyMap)) {
|
|
return;
|
|
}
|
|
const dir = ROT.DIRS[8][keyMap[code]];
|
|
if (Game.display) {
|
|
ev.preventDefault();
|
|
}
|
|
if(Game.playerAllowedToMove) {
|
|
arrowStart(dir);
|
|
}
|
|
}
|
|
|
|
function arrowStart(dir) {
|
|
const last = Game.lastArrow;
|
|
Game.lastArrow = dir;
|
|
if (!last) {
|
|
document.dispatchEvent(new Event("arrow"));
|
|
if (Game.arrowInterval) {
|
|
clearInterval(Game.arrowInterval);
|
|
}
|
|
Game.arrowInterval = setInterval(function () {
|
|
document.dispatchEvent(new Event("arrow"));
|
|
}, turnLengthMS);
|
|
}
|
|
}
|
|
|
|
function arrowStop(ev) {
|
|
clearInterval(Game.arrowInterval);
|
|
Game.arrowInterval = null;
|
|
Game.lastArrow = null;
|
|
}
|
|
|
|
function arrowEventHandler() {
|
|
if (Game.lastArrow) {
|
|
movePlayer(Game.lastArrow);
|
|
} else {
|
|
arrowStop();
|
|
}
|
|
}
|
|
|
|
function startGame(ev) {
|
|
showScreen("game");
|
|
sfx["rubber"].play();
|
|
init(Game);
|
|
}
|
|
|
|
function handleMenuChange(which, ev) {
|
|
ev.preventDefault();
|
|
const choice = which.getAttribute("value");
|
|
showScreen(choice);
|
|
sfx["choice"].play();
|
|
}
|
|
|
|
function hideModal(ev) {
|
|
ev.preventDefault();
|
|
showScreen("title");
|
|
sfx["hide"].play();
|
|
}
|
|
|
|
function cleanup() {
|
|
destroy(Game);
|
|
$("#play").removeEventListener(clickevt, startGame);
|
|
}
|
|
|
|
/***************
|
|
*** Startup ***
|
|
***************/
|
|
|
|
// this code is called at load time and sets the game title
|
|
document.querySelectorAll(".game-title-text").forEach(function (t) {
|
|
t.textContent = gametitle;
|
|
});
|
|
|
|
// listen for the start game button
|
|
$("#play").addEventListener(clickevt, startGame);
|
|
|
|
if (w["rbb"]) {
|
|
w["rbb"].cleanup();
|
|
} else {
|
|
$("#plate").addEventListener(
|
|
"animationend",
|
|
showScreen.bind(null, "title")
|
|
);
|
|
document.querySelectorAll("#options #menu input").forEach(function (el) {
|
|
el.addEventListener("click", handleMenuChange.bind(null, el));
|
|
});
|
|
document.querySelectorAll(".modal button.action").forEach(function (el) {
|
|
el.addEventListener(clickevt, hideModal);
|
|
});
|
|
}
|
|
|
|
w["rbb"] = Game;
|
|
})(window);
|