diff --git a/controllers/game.php b/controllers/game.php new file mode 100644 index 0000000..82a554a --- /dev/null +++ b/controllers/game.php @@ -0,0 +1,3 @@ + 'controllers/contact.php', '/learn' => 'controllers/learn.php', '/mathe' => 'controllers/mathe.php', + '/game' => 'controllers/game.php', '/note' => 'controllers/notes/show.php', '/notes' => 'controllers/notes/index.php', '/addition' => 'controllers/addition.php', - '/notes/create' => 'controllers/notes/create.php' -]; \ No newline at end of file + '/notes/create' => 'controllers/notes/create.php', + '/login' => 'controllers/login.php' +]; diff --git a/scripts/game.js b/scripts/game.js new file mode 100644 index 0000000..5fca6b9 --- /dev/null +++ b/scripts/game.js @@ -0,0 +1,947 @@ +(function (w) { + 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 = "colored_tilemap_packed.png"; + + const tileOptions = { + layout: "tile", + bg: "transparent", + tileWidth: 8, + tileHeight: 8, + tileSet: tileSet, + tileMap: { + "@": [40, 0], // player + ".": [32, 32], // floor + "M": [88, 0], // monster + "*": [72, 24], // treasure chest + "g": [64, 40], // gold + "x": [56, 32], // axe + "p": [56, 64], // potion + "a": [40, 32], // tree 1 + "b": [32, 40], // tree 2 + "c": [40, 40], // tree 3 + "d": [48, 40], // tree 4 + "e": [56, 40], // tree 5 + "T": [72, 56], // tombstone + "╔": [0, 72], // room corner + "╗": [24, 72], // room corner + "╝": [72, 72], // room corner + "╚": [48, 72], // room corner + "═": [8, 72], // room edge + "║": [32, 72], // room edge + "o": [40, 72], // room corner + "D": [16, 16], //Door to win + "s": [32, 24], //stairs to next Stage + }, + width: 25, + height: 40, + }; + + const usePointer = true; + const useArrows = true; + const touchOffsetY = -20; // move the center by this much + const scaleMobile = 4; // scale mobile screens by this much + const scaleMonitor = 6; // 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); + generateScenery(game.map, zeroCells); + generateRooms(game.map, digger); + + game.player = createBeing(makePlayer, freeCells); + game.monsters = [] + for ( var i= 0; i<= stage; i++) { + 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 = key; + game.items[key] = "s"; + } else { + game.door = key; + game.items[key] = "D"; + } + } else { + game.items[key] = ROT.RNG.getItem(["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 generateScenery(map, freeCells) { + for (let i = 0; i < 100; i++) { + if (freeCells.length) { + const key = takeFreeCell(freeCells); + map[key] = ROT.RNG.getItem("abcde"); + } + } + } + + 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) { + 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: "you", + // 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) { + //combat(p, 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) { + return { + // monster position + _x: x, + _y: y, + character: "M", + name: "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); + 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, hitter, receiver) { + console.log("Click: " + solution + " Antwort: " + answer); + if (solution == answer) { + hitter.stats.hp -= 1; + sfx["hit"].play(); + if (checkDeath(hitter)) { + Game.player.stats.xp += 1; + showScreen("game"); + Game.playerAllowedToMove = true; + Game.engine.unlock(); + } else { + combat(hitter, receiver); + } + checkDeath(hitter); + } else { + sfx["miss"].play(); + //showScreen("game"); + //Game.playerAllowedToMove = true; + //Game.engine.unlock(); + } + } + + function setupButtons(answerValue, hitter, receiver) { + const randomValue = (min, max) => + Math.floor(Math.random() * (max - min)) + min; + let randomVar = randomValue(1, 4); + if (randomVar == 1) { + document.getElementById("answer1").innerHTML = `${answerValue}`; + document.getElementById("answer2").innerHTML = `${ + answerValue + randomValue(1, 4) + }`; + document.getElementById("answer3").innerHTML = `${ + answerValue - randomValue(1, 4) + }`; + } else if (randomVar == 2) { + document.getElementById("answer1").innerHTML = `${ + answerValue + randomValue(1, 4) + }`; + document.getElementById("answer2").innerHTML = `${answerValue}`; + document.getElementById("answer3").innerHTML = `${ + answerValue - randomValue(1, 4) + }`; + } else { + document.getElementById("answer1").innerHTML = `${ + answerValue - randomValue(1, 4) + }`; + document.getElementById("answer2").innerHTML = `${ + answerValue + randomValue(1, 4) + }`; + document.getElementById("answer3").innerHTML = `${answerValue}`; + } + document.getElementById("answer1").addEventListener("click", async() => { + checkSolution(document.getElementById("answer1").innerText, answerValue, hitter, receiver); + }, {once: true}); + document.getElementById("answer2").addEventListener("click", async() => { + checkSolution(document.getElementById("answer2").innerText, answerValue, hitter, receiver); + }, {once: true}); + document.getElementById("answer3").addEventListener("click", async() => { + checkSolution(document.getElementById("answer3").innerText, answerValue, hitter, receiver); + }, {once: true}); + } + + function combat(hitter, receiver) { + const randomValue = (min, max) => + Math.floor(Math.random() * (max - min)) + min; + let [num1, num2] = [randomValue(1, 10), randomValue(1, 10)]; + const answerValue = eval(`${num1} * ${num2}`); + document.getElementById("question").innerHTML = `${num1} * ${num2} = ? `; + setupButtons(answerValue, hitter, receiver); + showScreen("combat"); + checkDeath(receiver); + renderStats(Game.player.stats); + } + + // this gets called when the player wins the game + function win() { + Game.engine.lock(); + 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); + const ghost = createGhost([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 = !!("ontouchstart" in window) ? "touchstart" : "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; + if (useArrows) { + document.ontouchend = arrowStop; + } + showScreen("game"); + } + + function rescale(x, y, game) { + const c = $("canvas"); + const scale = window.innerWidth < 600 ? scaleMobile : 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)); + } + + // 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]])); + } + } + + // creates the ghost sprite when the player dies + function createGhost(pos) { + const tw = tileOptions.tileWidth; + const th = tileOptions.tileHeight; + const left = "left:" + pos[0] * tw + "px;"; + const top = "top:" + pos[1] * th + "px;"; + const ghost = el("div", { + className: "sprite ghost free float-up", + style: left + top, + }); + ghost.onanimationend = function () { + rmel(ghost); + }; + return attach($("#canvas"), ghost); + } + + 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(); + console.log("Full screen pressed."); + return; + } + if (code == 73) { + toggleInventory(ev, true); + 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); diff --git a/styles/game.css b/styles/game.css new file mode 100644 index 0000000..0e3addb --- /dev/null +++ b/styles/game.css @@ -0,0 +1,476 @@ +@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P'); + +.game-title-text { + font-size: 32px; +} + +* { + box-sizing: border-box; + touch-action: manipulation; + user-select: none; + -webkit-user-select: none; + -webkit-touch-callout: none; +} + +html { + height: 100%; + overflow: hidden; +} + +body { + font-family: 'Press Start 2P', cursive; + width: 100%; + height: 100%; + margin: 0px auto; + font-size: 1.5em; + background-color: #222323; + color: #eee; +} + +@media (max-width: 700px), (max-height: 820px) { + body { + font-size: 0.75em; + } +} + +canvas { + image-rendering: optimizeSpeed; + image-rendering: crisp-edges; + image-rendering: -moz-crisp-edges; + image-rendering: -o-crisp-edges; + image-rendering: -webkit-optimize-contrast; + -ms-interpolation-mode: nearest-neighbor; + image-rendering: pixelated; +} + +a { + color: #e77; +} + +a:hover { + color: #faa; + text-decoration: none; +} + +/*** NES.css overrides ***/ + +.nes-container.is-rounded.is-dark { + border-image-slice: 9 9 9 9 fill; + border-image-source: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAphgAAKYYBIuzfjAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAABgSURBVEiJY2AYBQQAIzGK/v///x+rZkZGgvqZSHURqYDmFrDgkkAOFikpZYJqcAXX0A8iFG8REyy4wLNndxGGIgXX0A+iUQsIApxlCTEpClfKQQbDOIiQwcgurkcBQQAARlMedugABy8AAAAASUVORK5CYII='); + background-color: transparent; + border-image-repeat: stretch; +} + +.nes-container.is-fake-rounded.is-dark::after { + background: none; +} + +/*** screens ***/ + +.screen { + height: 100%; + width: 100%; + display: none; + flex-direction: column; + position: absolute; + justify-content: center; + align-items: center; +} + +.modal { + position: absolute; + top: 0px; + left: 0px; + bottom: 0px; + right: 0px; + width: 100%; + height: 100%; + background-color: #222323; +} + +#title { + background-image: url(bg.png); + background-size: cover; + animation: 20s para infinite ease; +} + +@keyframes para { + 0% { + background-position: 0px 0%; + } + 50% { + background-position: 0px 80px; + } + 100% { + background-position: 0px 0px; + } +} + +#plate { + display: flex; + animation: 2s plate-fade; + opacity: 0; +} + +#plate > div { + display: flex; + justify-content: center; + align-items: center; + text-align: left; + padding: 40px; + border-radius: 5px; +} + +@keyframes plate-fade { + 0% { + opacity: 0; + } + 25% { + opacity: 0; + } + 50% { + opacity: 1; + } + 75% { + opacity: 1; + } + 100% { + opacity: 0; + } +} + +#plate > div > * + * { + margin-left: 32px; + margin-right: 0px; +} + +@media (max-width: 700px), (max-height: 820px) { + #plate > div { + flex-direction: column; + text-align: center; + } + + #plate > div > * + * { + margin-left: 0px; + margin-right: 0px; + margin-top: 16px; + } +} + +#game-title { + margin-bottom: 0px; + width: 900px; + max-width: 98%; +} + +@media (min-width: 700px) and (max-height: 820px) { + #game-title { + width: 900px; + max-width: 98%; + max-height: 35vh; + } +} + +@media (max-height: 600px) { + #game-title { + width: 500px; + } +} + +.game-title-animation { + animation: 2s zoomInDown; +} + +/* https://github.com/animate-css/animate.css/blob/master/animate.css */ +@keyframes zoomInDown { + 0% { + opacity: 0; + transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -1000px, 0); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + + 60% { + opacity: 1; + transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} + +#options { + text-align: center; + justify-content: center; + max-width: 90%; +} + +#logo { + width: 100px; +} + +#menu { + width: 400px; + max-width: 100%; + margin-bottom: 64px; + padding: 32px; +} + +#menu label { + margin-left: -1em; + padding-top: 0.5em; + padding-bottom: 0.5em; +} + +.modal > * { + max-width: 90%; + width: 400px; + margin: 50px; + text-align: center; +} + +@media (max-width: 700px), (max-height: 820px) { + .modal > * { + margin: 10px; + } +} + +#instructions div > p { + text-align: left; +} + +#settings div > p { + text-align: left; +} + +#credits ul { + list-style: "> "; + padding-left: 2em; + text-align: left; +} + +#credits ul li { + margin: 0.5em 0px; +} + +.sprite { + display: block; + width: 8px; + height: 8px; + image-rendering: optimizeSpeed; + image-rendering: crisp-edges; + image-rendering: -moz-crisp-edges; + image-rendering: -o-crisp-edges; + image-rendering: -webkit-optimize-contrast; + -ms-interpolation-mode: nearest-neighbor; + image-rendering: pixelated; + transform: scale(8); + background-image: url("colored_tilemap_packed.png"); + margin: 80px auto; +} + +.free { + position: absolute; + transform: none; +} + + +.tomb { + background-position: -72px -56px; +} + +.ghost { + background-position: -72px -8px; + margin: 0px; +} + +.empty { + background-position: -8px -48px; +} + +.float-up { + animation: float-up 2s linear forwards; +} + +@keyframes float-up { + from { + transform: scale(1) translate(0px, 0px); + opacity: 1; + } + to { + transform: scale(3) translate(0px, -20px); + opacity: 0; + } +} + +.grow-fade { + animation: grow-fade 2s linear; +} + +@keyframes grow-fade { + from { + transform: translate(0px, 0px) scale(8); + opacity: 0.5; + } + to { + transform: translate(0px, 0px) scale(16); + opacity: 0; + } +} + +#play { + width: 400px; + max-width: 90%; +} + +#win { + background: url(01coin.gif); + background-size: 20%; +} + +/*** HUD ***/ + +#hud { + position: absolute; + bottom: 0px; + width: 600px; + max-width: 100%; + display: flex; + justify-content: space-evenly; + padding: 24px; +} + +#message { + position: absolute; + top: 24px; + flex-direction: column; +} + +#message .hit { + color: #C01256; +} + +#message .miss { + color: #FFB570; +} + +#inventory { + position: absolute; + bottom: 0px; + left: 0px; +} + +#inventory .sprite { + transform: scale(3); + display: inline-block; + margin: 1em 2em 1em 1em; + vertical-align: middle; +} + +#inventory li { + margin: 1em 0px; +} + +#inventory ul { + list-style-type: none; + margin: 0px; + padding: 0px; +} + +#inventory > div { + display: none; +} + +@media (max-width: 750px) { + #inventory { + bottom: 72px; + } + + #hud { + width: 100%; + } +} + +#arrows { + display: none; + position: absolute; + right: 0px; + bottom: 0px; +} + +#arrows > * { + float: left; + font-size: 16px; + bottom: 0px; + width: 60px; + height: 60px; + display: flex; + align-items: center; + justify-content: center; +} + +#arrows > * > span { + pointer-events: none; +} + +#btn-left { + position: absolute; + right: 7em; +} + +#btn-right { + position: absolute; + right: 0em; +} + +#btn-up { + transform: rotate(90deg); + position: absolute; + right: 3.5em; + margin-bottom: 3.75em; +} + +#btn-down { + transform: rotate(90deg); + position: absolute; + right: 3.5em; +} + +#btn-skip { + position: absolute; + right: 0em; + margin-bottom: 3.75em; + padding: 0px; +} + +@media (max-width: 1024px) { + #arrows > * { + bottom: 72px; + } +} + +/*** CSS animations ***/ + +.fade-in { + animation: fade-in 0.8s; + display: flex; +} + +@keyframes fade-in { + from{opacity:0} to{opacity:1} +} + +.hide { + display: none; +} + +.show { + display: flex; +} + +.fade-out { + display: flex; + opacity: 1; + animation: fade-out 3s forwards; +} + +@keyframes fade-out { + from{opacity:1; display: flex;} 50%{opacity:1; display: flex;} to{opacity:0; display: none;} +} diff --git a/views/game.css b/views/game.css new file mode 100644 index 0000000..0e3addb --- /dev/null +++ b/views/game.css @@ -0,0 +1,476 @@ +@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P'); + +.game-title-text { + font-size: 32px; +} + +* { + box-sizing: border-box; + touch-action: manipulation; + user-select: none; + -webkit-user-select: none; + -webkit-touch-callout: none; +} + +html { + height: 100%; + overflow: hidden; +} + +body { + font-family: 'Press Start 2P', cursive; + width: 100%; + height: 100%; + margin: 0px auto; + font-size: 1.5em; + background-color: #222323; + color: #eee; +} + +@media (max-width: 700px), (max-height: 820px) { + body { + font-size: 0.75em; + } +} + +canvas { + image-rendering: optimizeSpeed; + image-rendering: crisp-edges; + image-rendering: -moz-crisp-edges; + image-rendering: -o-crisp-edges; + image-rendering: -webkit-optimize-contrast; + -ms-interpolation-mode: nearest-neighbor; + image-rendering: pixelated; +} + +a { + color: #e77; +} + +a:hover { + color: #faa; + text-decoration: none; +} + +/*** NES.css overrides ***/ + +.nes-container.is-rounded.is-dark { + border-image-slice: 9 9 9 9 fill; + border-image-source: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAphgAAKYYBIuzfjAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAABgSURBVEiJY2AYBQQAIzGK/v///x+rZkZGgvqZSHURqYDmFrDgkkAOFikpZYJqcAXX0A8iFG8REyy4wLNndxGGIgXX0A+iUQsIApxlCTEpClfKQQbDOIiQwcgurkcBQQAARlMedugABy8AAAAASUVORK5CYII='); + background-color: transparent; + border-image-repeat: stretch; +} + +.nes-container.is-fake-rounded.is-dark::after { + background: none; +} + +/*** screens ***/ + +.screen { + height: 100%; + width: 100%; + display: none; + flex-direction: column; + position: absolute; + justify-content: center; + align-items: center; +} + +.modal { + position: absolute; + top: 0px; + left: 0px; + bottom: 0px; + right: 0px; + width: 100%; + height: 100%; + background-color: #222323; +} + +#title { + background-image: url(bg.png); + background-size: cover; + animation: 20s para infinite ease; +} + +@keyframes para { + 0% { + background-position: 0px 0%; + } + 50% { + background-position: 0px 80px; + } + 100% { + background-position: 0px 0px; + } +} + +#plate { + display: flex; + animation: 2s plate-fade; + opacity: 0; +} + +#plate > div { + display: flex; + justify-content: center; + align-items: center; + text-align: left; + padding: 40px; + border-radius: 5px; +} + +@keyframes plate-fade { + 0% { + opacity: 0; + } + 25% { + opacity: 0; + } + 50% { + opacity: 1; + } + 75% { + opacity: 1; + } + 100% { + opacity: 0; + } +} + +#plate > div > * + * { + margin-left: 32px; + margin-right: 0px; +} + +@media (max-width: 700px), (max-height: 820px) { + #plate > div { + flex-direction: column; + text-align: center; + } + + #plate > div > * + * { + margin-left: 0px; + margin-right: 0px; + margin-top: 16px; + } +} + +#game-title { + margin-bottom: 0px; + width: 900px; + max-width: 98%; +} + +@media (min-width: 700px) and (max-height: 820px) { + #game-title { + width: 900px; + max-width: 98%; + max-height: 35vh; + } +} + +@media (max-height: 600px) { + #game-title { + width: 500px; + } +} + +.game-title-animation { + animation: 2s zoomInDown; +} + +/* https://github.com/animate-css/animate.css/blob/master/animate.css */ +@keyframes zoomInDown { + 0% { + opacity: 0; + transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -1000px, 0); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + + 60% { + opacity: 1; + transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} + +#options { + text-align: center; + justify-content: center; + max-width: 90%; +} + +#logo { + width: 100px; +} + +#menu { + width: 400px; + max-width: 100%; + margin-bottom: 64px; + padding: 32px; +} + +#menu label { + margin-left: -1em; + padding-top: 0.5em; + padding-bottom: 0.5em; +} + +.modal > * { + max-width: 90%; + width: 400px; + margin: 50px; + text-align: center; +} + +@media (max-width: 700px), (max-height: 820px) { + .modal > * { + margin: 10px; + } +} + +#instructions div > p { + text-align: left; +} + +#settings div > p { + text-align: left; +} + +#credits ul { + list-style: "> "; + padding-left: 2em; + text-align: left; +} + +#credits ul li { + margin: 0.5em 0px; +} + +.sprite { + display: block; + width: 8px; + height: 8px; + image-rendering: optimizeSpeed; + image-rendering: crisp-edges; + image-rendering: -moz-crisp-edges; + image-rendering: -o-crisp-edges; + image-rendering: -webkit-optimize-contrast; + -ms-interpolation-mode: nearest-neighbor; + image-rendering: pixelated; + transform: scale(8); + background-image: url("colored_tilemap_packed.png"); + margin: 80px auto; +} + +.free { + position: absolute; + transform: none; +} + + +.tomb { + background-position: -72px -56px; +} + +.ghost { + background-position: -72px -8px; + margin: 0px; +} + +.empty { + background-position: -8px -48px; +} + +.float-up { + animation: float-up 2s linear forwards; +} + +@keyframes float-up { + from { + transform: scale(1) translate(0px, 0px); + opacity: 1; + } + to { + transform: scale(3) translate(0px, -20px); + opacity: 0; + } +} + +.grow-fade { + animation: grow-fade 2s linear; +} + +@keyframes grow-fade { + from { + transform: translate(0px, 0px) scale(8); + opacity: 0.5; + } + to { + transform: translate(0px, 0px) scale(16); + opacity: 0; + } +} + +#play { + width: 400px; + max-width: 90%; +} + +#win { + background: url(01coin.gif); + background-size: 20%; +} + +/*** HUD ***/ + +#hud { + position: absolute; + bottom: 0px; + width: 600px; + max-width: 100%; + display: flex; + justify-content: space-evenly; + padding: 24px; +} + +#message { + position: absolute; + top: 24px; + flex-direction: column; +} + +#message .hit { + color: #C01256; +} + +#message .miss { + color: #FFB570; +} + +#inventory { + position: absolute; + bottom: 0px; + left: 0px; +} + +#inventory .sprite { + transform: scale(3); + display: inline-block; + margin: 1em 2em 1em 1em; + vertical-align: middle; +} + +#inventory li { + margin: 1em 0px; +} + +#inventory ul { + list-style-type: none; + margin: 0px; + padding: 0px; +} + +#inventory > div { + display: none; +} + +@media (max-width: 750px) { + #inventory { + bottom: 72px; + } + + #hud { + width: 100%; + } +} + +#arrows { + display: none; + position: absolute; + right: 0px; + bottom: 0px; +} + +#arrows > * { + float: left; + font-size: 16px; + bottom: 0px; + width: 60px; + height: 60px; + display: flex; + align-items: center; + justify-content: center; +} + +#arrows > * > span { + pointer-events: none; +} + +#btn-left { + position: absolute; + right: 7em; +} + +#btn-right { + position: absolute; + right: 0em; +} + +#btn-up { + transform: rotate(90deg); + position: absolute; + right: 3.5em; + margin-bottom: 3.75em; +} + +#btn-down { + transform: rotate(90deg); + position: absolute; + right: 3.5em; +} + +#btn-skip { + position: absolute; + right: 0em; + margin-bottom: 3.75em; + padding: 0px; +} + +@media (max-width: 1024px) { + #arrows > * { + bottom: 72px; + } +} + +/*** CSS animations ***/ + +.fade-in { + animation: fade-in 0.8s; + display: flex; +} + +@keyframes fade-in { + from{opacity:0} to{opacity:1} +} + +.hide { + display: none; +} + +.show { + display: flex; +} + +.fade-out { + display: flex; + opacity: 1; + animation: fade-out 3s forwards; +} + +@keyframes fade-out { + from{opacity:1; display: flex;} 50%{opacity:1; display: flex;} to{opacity:0; display: none;} +} diff --git a/views/game.js b/views/game.js new file mode 100644 index 0000000..5fca6b9 --- /dev/null +++ b/views/game.js @@ -0,0 +1,947 @@ +(function (w) { + 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 = "colored_tilemap_packed.png"; + + const tileOptions = { + layout: "tile", + bg: "transparent", + tileWidth: 8, + tileHeight: 8, + tileSet: tileSet, + tileMap: { + "@": [40, 0], // player + ".": [32, 32], // floor + "M": [88, 0], // monster + "*": [72, 24], // treasure chest + "g": [64, 40], // gold + "x": [56, 32], // axe + "p": [56, 64], // potion + "a": [40, 32], // tree 1 + "b": [32, 40], // tree 2 + "c": [40, 40], // tree 3 + "d": [48, 40], // tree 4 + "e": [56, 40], // tree 5 + "T": [72, 56], // tombstone + "╔": [0, 72], // room corner + "╗": [24, 72], // room corner + "╝": [72, 72], // room corner + "╚": [48, 72], // room corner + "═": [8, 72], // room edge + "║": [32, 72], // room edge + "o": [40, 72], // room corner + "D": [16, 16], //Door to win + "s": [32, 24], //stairs to next Stage + }, + width: 25, + height: 40, + }; + + const usePointer = true; + const useArrows = true; + const touchOffsetY = -20; // move the center by this much + const scaleMobile = 4; // scale mobile screens by this much + const scaleMonitor = 6; // 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); + generateScenery(game.map, zeroCells); + generateRooms(game.map, digger); + + game.player = createBeing(makePlayer, freeCells); + game.monsters = [] + for ( var i= 0; i<= stage; i++) { + 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 = key; + game.items[key] = "s"; + } else { + game.door = key; + game.items[key] = "D"; + } + } else { + game.items[key] = ROT.RNG.getItem(["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 generateScenery(map, freeCells) { + for (let i = 0; i < 100; i++) { + if (freeCells.length) { + const key = takeFreeCell(freeCells); + map[key] = ROT.RNG.getItem("abcde"); + } + } + } + + 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) { + 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: "you", + // 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) { + //combat(p, 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) { + return { + // monster position + _x: x, + _y: y, + character: "M", + name: "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); + 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, hitter, receiver) { + console.log("Click: " + solution + " Antwort: " + answer); + if (solution == answer) { + hitter.stats.hp -= 1; + sfx["hit"].play(); + if (checkDeath(hitter)) { + Game.player.stats.xp += 1; + showScreen("game"); + Game.playerAllowedToMove = true; + Game.engine.unlock(); + } else { + combat(hitter, receiver); + } + checkDeath(hitter); + } else { + sfx["miss"].play(); + //showScreen("game"); + //Game.playerAllowedToMove = true; + //Game.engine.unlock(); + } + } + + function setupButtons(answerValue, hitter, receiver) { + const randomValue = (min, max) => + Math.floor(Math.random() * (max - min)) + min; + let randomVar = randomValue(1, 4); + if (randomVar == 1) { + document.getElementById("answer1").innerHTML = `${answerValue}`; + document.getElementById("answer2").innerHTML = `${ + answerValue + randomValue(1, 4) + }`; + document.getElementById("answer3").innerHTML = `${ + answerValue - randomValue(1, 4) + }`; + } else if (randomVar == 2) { + document.getElementById("answer1").innerHTML = `${ + answerValue + randomValue(1, 4) + }`; + document.getElementById("answer2").innerHTML = `${answerValue}`; + document.getElementById("answer3").innerHTML = `${ + answerValue - randomValue(1, 4) + }`; + } else { + document.getElementById("answer1").innerHTML = `${ + answerValue - randomValue(1, 4) + }`; + document.getElementById("answer2").innerHTML = `${ + answerValue + randomValue(1, 4) + }`; + document.getElementById("answer3").innerHTML = `${answerValue}`; + } + document.getElementById("answer1").addEventListener("click", async() => { + checkSolution(document.getElementById("answer1").innerText, answerValue, hitter, receiver); + }, {once: true}); + document.getElementById("answer2").addEventListener("click", async() => { + checkSolution(document.getElementById("answer2").innerText, answerValue, hitter, receiver); + }, {once: true}); + document.getElementById("answer3").addEventListener("click", async() => { + checkSolution(document.getElementById("answer3").innerText, answerValue, hitter, receiver); + }, {once: true}); + } + + function combat(hitter, receiver) { + const randomValue = (min, max) => + Math.floor(Math.random() * (max - min)) + min; + let [num1, num2] = [randomValue(1, 10), randomValue(1, 10)]; + const answerValue = eval(`${num1} * ${num2}`); + document.getElementById("question").innerHTML = `${num1} * ${num2} = ? `; + setupButtons(answerValue, hitter, receiver); + showScreen("combat"); + checkDeath(receiver); + renderStats(Game.player.stats); + } + + // this gets called when the player wins the game + function win() { + Game.engine.lock(); + 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); + const ghost = createGhost([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 = !!("ontouchstart" in window) ? "touchstart" : "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; + if (useArrows) { + document.ontouchend = arrowStop; + } + showScreen("game"); + } + + function rescale(x, y, game) { + const c = $("canvas"); + const scale = window.innerWidth < 600 ? scaleMobile : 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)); + } + + // 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]])); + } + } + + // creates the ghost sprite when the player dies + function createGhost(pos) { + const tw = tileOptions.tileWidth; + const th = tileOptions.tileHeight; + const left = "left:" + pos[0] * tw + "px;"; + const top = "top:" + pos[1] * th + "px;"; + const ghost = el("div", { + className: "sprite ghost free float-up", + style: left + top, + }); + ghost.onanimationend = function () { + rmel(ghost); + }; + return attach($("#canvas"), ghost); + } + + 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(); + console.log("Full screen pressed."); + return; + } + if (code == 73) { + toggleInventory(ev, true); + 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); diff --git a/views/game.view.php b/views/game.view.php new file mode 100644 index 0000000..e477403 --- /dev/null +++ b/views/game.view.php @@ -0,0 +1,1583 @@ + + + + The Math Wizard + + + + + + + + + + + + + + + +
+
+ +

The
Math
Wizard

+
+
+ + +
+
+ + + + + + + + Roguelike + + + + + Roguelike + + + +
+
+ +
+ +
+ + + + + + + + + + + + + + + + + +
+
+
+
+
+
<
+
<
+
>
+
>
+
.
+
+
+ + + + diff --git a/views/index.view.php b/views/index.view.php index 0b2b674..ab7091f 100644 --- a/views/index.view.php +++ b/views/index.view.php @@ -1,14 +1,96 @@ - -
-
- -

Hello and Welcome.

-
-
+
+
+
+
+
+ +
+

Automatisiere spielerisch das 1x1

+

Zeige was du kannst und kämpfe dich durch denn Dungeon.

+ +
+
+ + + + + + + + + +
+
- +
+
+ +
+
+
+
+

Der bessere Weg das 1x1 zu automatisieren.

+ +
+
+
+
+ + +
+
+

Immer neue Level

+

Durch zufällig generierte Level wird dir nie langweilig.

+
+
+ +
+
+ + +
+
+

Angepasste Schwierigkeit

+

Die Aufgabe passen sich deinen aktuellen Fähigkeiten an.

+
+
+ +
+
+
+
+
+
+
+
+

+ Bereit los zu legen? + Fange noch heute an zu lernen. +

+ +
+
+
- \ No newline at end of file +
+ + diff --git a/views/login.view.php b/views/login.view.php new file mode 100644 index 0000000..0ade09e --- /dev/null +++ b/views/login.view.php @@ -0,0 +1,69 @@ + + + + +
+
+
+

Melde dich mit deinem Konto an.

+

+ Oder + registriere dich noch Heute +

+
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+ + +
+ +
+ +
+
+
+
+ + diff --git a/views/partials/head.php b/views/partials/head.php index 3692e6b..a6e014d 100644 --- a/views/partials/head.php +++ b/views/partials/head.php @@ -4,8 +4,12 @@ - - Demo + + +The Math Wizard + -
\ No newline at end of file +
diff --git a/views/partials/nav.php b/views/partials/nav.php index 6740088..e120d9c 100644 --- a/views/partials/nav.php +++ b/views/partials/nav.php @@ -1,132 +1,93 @@ - \ No newline at end of file