Path: blob/main/public/games/files/pacman/pacman-canvas.js
1036 views
/*-------------------------------------------------------------------12___________ ____ _____ _____ ____3\____ \__ \ _/ ___\ / \\__ \ / \4| |_> > __ \\ \___| Y Y \/ __ \| | \5| __(____ /\___ >__|_| (____ /___| /6|__| \/ \/ \/ \/ \/ .platzh1rsch.ch78author: platzh1rsch (www.platzh1rsch.ch)910-------------------------------------------------------------------*/1112"use strict";1314// global enums15const GHOSTS = {16INKY: 'inky',17BLINKY: 'blinky',18PINKY: 'pinky',19CLYDE: 'clyde'20}2122// global constants23const FINAL_LEVEL = 10;24const PILL_POINTS = 10;25const POWERPILL_POINTS = 50;26const GHOST_POINTS = 100;27const HIGHSCORE_ENABLED = true;282930function geronimo() {31/* ----- Global Variables ---------------------------------------- */32var canvas;33var context;34var game;35var canvas_walls, context_walls;36var inky, blinky, clyde, pinky;3738var mapConfig = "data/map.json";394041/* AJAX stuff */42var getHighscore = () => {43setTimeout(ajax_get, 30);44}45var ajax_get = () => {46var date = new Date().getTime();47$.ajax({48datatype: "json",49type: "GET",50url: "data/db-handler.php",51data: {52timestamp: date,53action: "get"54},55success: function (msg) {56$("#highscore-list").text("");57for (var i = 0; i < msg.length; i++) {58$("#highscore-list").append("<li>" + msg[i]['name'] + "<span id='score'>" + msg[i]['score'] + "</span></li>");59}60}61});62}63var ajax_add = (n, s, l) => {6465$.ajax({66type: 'POST',67url: 'data/db-handler.php',68data: {69action: 'add',70name: n,71score: s,72level: l73},74dataType: 'json',75success: function (data) {76console.log('Highscore added: ' + data);77$('#highscore-form').html('<span class="button" id="show-highscore">View Highscore List</span>');78},79error: function (errorThrown) {80console.log(errorThrown);81}82});83}8485function addHighscore() {86var name = $("input[type=text]").val();87$("#highscore-form").html("Saving highscore...");88ajax_add(name, game.score.score, game.level);89}9091function buildWall(context, gridX, gridY, width, height) {92console.log("BuildWall");93width = width * 2 - 1;94height = height * 2 - 1;95context.fillRect(pacman.radius / 2 + gridX * 2 * pacman.radius, pacman.radius / 2 + gridY * 2 * pacman.radius, width * pacman.radius, height * pacman.radius);96}9798function between(x, min, max) {99return x >= min && x <= max;100}101102// Logger103var logger = function () {104var originalConsoleLog = null;105var originalConsoleDebug = null;106var logger = {};107108logger.enableLogger = function enableLogger() {109if (originalConsoleLog === null)110return;111112window['console']['log'] = originalConsoleLog;113console.log('console.log enabled');114115if (originalConsoleDebug === null)116return;117118window['console']['debug'] = originalConsoleDebug;119console.log('console.debug enabled');120121};122123logger.disableLogger = function disableLogger() {124console.log('console.log disabled');125originalConsoleLog = console.log;126window['console']['log'] = function () {};127originalConsoleDebug = console.debug;128window['console']['debug'] = function () {};129};130131return logger;132}();133134// stop watch to measure the time135function Timer() {136this.time_diff = 0;137this.time_start = 0;138this.time_stop = 0;139this.start = function () {140this.time_start = new Date().getTime();141}142this.stop = function () {143this.time_stop = new Date().getTime();144this.time_diff += this.time_stop - this.time_start;145this.time_stop = 0;146this.time_start = 0;147}148this.reset = function () {149this.time_diff = 0;150this.time_start = 0;151this.time_stop = 0;152}153this.get_time_diff = function () {154return this.time_diff;155}156}157158// Manages the whole game ("God Object")159function Game() {160this.timer = new Timer(); // TODO: implememnt properly, and submit with highscore161this.refreshRate = 33; // speed of the game, will increase in higher levels162163this.started = false; // TODO: what's the purpose of this exactly?164this.pause = true;165this.gameOver = false;166167this.score = new Score();168this.soundfx = 0;169this.map;170this.pillCount; // number of pills171this.monsters;172this.level = 1;173this.refreshLevel = function (h) {174$(h).html("Lvl: " + this.level);175};176this.canvas = $("#myCanvas").get(0);177this.wallColor = "Blue";178this.width = this.canvas.width;179this.height = this.canvas.height;180181// global pill states182this.pillSize = 3;183this.powerpillSizeMin = 2;184this.powerpillSizeMax = 6;185this.powerpillSizeCurrent = this.powerpillSizeMax;186this.powerPillAnimationCounter = 0;187188// TODO: vibrant power pills189this.nextPowerPillSize = function () {190/*if (this.powerPillAnimationCounter === 3) {191this.powerPillAnimationCounter = 0;192this.powerpillSizeCurrent = this.powerpillSizeMin + this.powerpillSizeCurrent % (this.powerpillSizeMax-this.powerpillSizeMin);193} else {194this.powerPillAnimationCounter++;195}*/196return this.powerpillSizeCurrent;197};198199// global ghost states200this.ghostFrightened = false;201this.ghostFrightenedTimer = 240;202this.ghostMode = 0; // 0 = Scatter, 1 = Chase203this.ghostModeTimer = 200; // decrements each animationLoop execution204this.ghostSpeedNormal = (this.level > 4 ? 3 : 2); // global default for ghost speed205this.ghostSpeedDazzled = 2; // global default for ghost speed when dazzled206207/* Game Functions */208this.startGhostFrightened = function () {209console.log("ghost frigthened");210this.ghostFrightened = true;211this.ghostFrightenedTimer = 240;212inky.dazzle();213pinky.dazzle();214blinky.dazzle();215clyde.dazzle();216};217218this.endGhostFrightened = function () {219this.ghostFrightened = false;220inky.undazzle();221pinky.undazzle();222blinky.undazzle();223clyde.undazzle();224};225226227this.checkGhostMode = function () {228if (this.ghostFrightened) {229230this.ghostFrightenedTimer--;231if (this.ghostFrightenedTimer === 0) {232this.endGhostFrightened();233this.ghostFrigthenedTimer = 240;234/*inky.reverseDirection();235pinky.reverseDirection();236clyde.reverseDirection();237blinky.reverseDirection();*/238}239}240// always decrement ghostMode timer241this.ghostModeTimer--;242if (this.ghostModeTimer === 0 && game.level > 1) {243this.ghostMode ^= 1;244this.ghostModeTimer = 200 + this.ghostMode * 450;245console.log("ghostMode=" + this.ghostMode);246247game.buildWalls();248249inky.reverseDirection();250pinky.reverseDirection();251clyde.reverseDirection();252blinky.reverseDirection();253}254};255256this.getMapContent = function (x, y) {257var maxX = game.width / 30 - 1;258var maxY = game.height / 30 - 1;259if (x < 0) x = maxX + x;260if (x > maxX) x = x - maxX;261if (y < 0) y = maxY + y;262if (y > maxY) y = y - maxY;263return this.map.posY[y].posX[x].type;264};265266this.setMapContent = function (x, y, val) {267this.map.posY[y].posX[x].type = val;268};269270this.toggleSound = function () {271this.soundfx === 0 ? this.soundfx = 1 : this.soundfx = 0;272$('#mute').toggle();273};274275// TODO: test276this.reset = function () {277this.score.set(0);278this.score.refresh(".score");279pacman.lives = 3;280game.level = 1;281this.refreshLevel(".level");282283this.pause = false;284this.gameOver = false;285};286287this.newGame = function () {288var r = confirm("Are you sure you want to restart?");289if (r) {290console.log("new Game");291this.init(0);292this.forceResume();293}294};295296this.nextLevel = function () {297console.debug('nextLevel: current, final', this.level, FINAL_LEVEL);298if (this.level === FINAL_LEVEL) {299console.log('next level, ' + FINAL_LEVEL + ', end game');300game.endGame(true);301game.showHighscoreForm();302} else {303this.level++;304console.log("Level " + game.level);305game.pauseAndShowMessage("Level " + game.level, this.getLevelTitle() + "<br/>(Click to continue!)");306game.refreshLevel(".level");307this.init(1);308}309};310311/* UI functions */312this.drawHearts = function (count) {313var html = "";314for (var i = 0; i < count; i++) {315html += " <img src='img/heart.png'>";316}317$(".lives").html("Lives: " + html);318319};320321this.showContent = function (id) {322$('.content').hide();323$('#' + id).show();324};325326this.getLevelTitle = function () {327switch (this.level) {328case 2:329return '"The chase begins"';330// activate chase / scatter switching331case 3:332return '"Inky\s awakening"';333// Inky starts leaving the ghost house334case 4:335return '"Clyde\s awakening"';336// Clyde starts leaving the ghost house337case 5:338return '"need for speed"';339// All the ghosts get faster from now on340case 6:341return '"hunting season 1"';342// TODO: No scatter mood this time343case 7:344return '"the big calm"';345// TODO: Only scatter mood this time346case 8:347return '"hunting season 2"';348// TODO: No scatter mood and all ghosts leave instantly349case 9:350return '"ghosts on speed"';351// TODO: Ghosts get even faster for this level352case FINAL_LEVEL:353return '"The final chase"';354// TODO: Ghosts get even faster for this level355default:356return '"nothing new"';357}358}359360this.showMessage = function (title, text) {361$('#canvas-overlay-container').fadeIn(200);362if ($('.controls').css('display') != "none") $('.controls').slideToggle(200);363$('#canvas-overlay-content #title').text(title);364$('#canvas-overlay-content #text').html(text);365}366367this.pauseAndShowMessage = function (title, text) {368this.timer.stop();369this.pause = true;370this.showMessage(title, text);371};372373this.closeMessage = function () {374$('#canvas-overlay-container').fadeOut(200);375$('.controls').slideToggle(200);376};377378this.validateScoreWithLevel = function () {379const maxLevelPointsPills = 104 * PILL_POINTS;380const maxLevelPointsPowerpills = 4 * POWERPILL_POINTS;381const maxLevelPointsGhosts = 4 * 4 * GHOST_POINTS;382const maxLevelPoints = maxLevelPointsPills + maxLevelPointsPowerpills + maxLevelPointsGhosts;383384const scoreIsValid = this.score.score / this.level <= maxLevelPoints;385console.log('validate score. score: ' + this.score.score + ', level: ' + this.level, scoreIsValid);386return scoreIsValid;387388}389390this.showHighscoreForm = function () {391var scoreIsValid = this.validateScoreWithLevel();392393var inputHTML = scoreIsValid ? `<div id='highscore-form'>394<span id='form-validator'></span>395<input type='text' id='playerName'/>396<span class='button' id='score-submit'>save</span>397</div>` : `<div id='invalid-score'>Your score looks fake, the highscore list is only for honest players ;)</div>`;398this.pauseAndShowMessage("Game over", "Total Score: " + this.score.score + (HIGHSCORE_ENABLED ? inputHTML : ''));399$('#playerName').focus();400}401402/* game controls */403404this.forceStartAnimationLoop = function () {405// start timer406this.timer.start();407408this.pause = false;409this.started = true;410this.closeMessage();411animationLoop();412}413414this.forcePause = function () {415this.timer.stop();416this.pauseAndShowMessage("Pause", "Click to Resume");417}418419this.forceResume = function () {420this.closeMessage();421this.pause = false;422this.timer.start();423}424425this.pauseResume = function () {426if (this.gameOver) {427console.log('Cannot pause / resume. GameOver set to true.');428return;429}430if (!this.started) {431this.forceStartAnimationLoop();432} else if (this.pause) {433this.forceResume();434} else {435this.pauseAndShowMessage("Pause", "Click to Resume");436}437};438439this.loadMapConfig = async () => {440console.log('load map config');441return new Promise((resolve, reject) => {442$.ajax({443url: mapConfig,444beforeSend: function (xhr) {445if (xhr.overrideMimeType) xhr.overrideMimeType("application/json");446},447dataType: "json",448success: (data) => {449console.log('map config loaded');450game.map = data;451resolve(data);452},453error: (response) => {454console.error('error fetching map config');455reject(response);456}457});458})459};460461this.getPillCount = () => {462var temp = 0;463$.each(this.map.posY, function (i, item) {464$.each(this.posX, function () {465if (this.type == "pill") {466temp++;467//console.log("Pill Count++. temp="+temp+". PillCount="+this.pillCount+".");468}469});470});471return temp;472}473474this.init = async (state) => {475476console.log("init game " + state);477478// get Level Map479this.map = await this.loadMapConfig();480481this.pillCount = this.getPillCount();482483// TODO: why are there 2 state checks?484if (state === 0) {485this.timer.reset();486game.reset();487}488pacman.reset();489490game.drawHearts(pacman.lives);491492this.ghostFrightened = false;493this.ghostFrightenedTimer = 240;494this.ghostMode = 0; // 0 = Scatter, 1 = Chase495this.ghostModeTimer = 200; // decrements each animationLoop execution496497// initalize Ghosts, avoid memory flooding498if (pinky === null || pinky === undefined) {499pinky = new Ghost(GHOSTS.PINKY, 7, 5, 'img/pinky.svg', 2, 2);500inky = new Ghost(GHOSTS.INKY, 8, 5, 'img/inky.svg', 13, 11);501blinky = new Ghost(GHOSTS.BLINKY, 9, 5, 'img/blinky.svg', 13, 0);502clyde = new Ghost(GHOSTS.CLYDE, 10, 5, 'img/clyde.svg', 2, 11);503} else {504pinky.reset();505inky.reset();506blinky.reset();507clyde.reset();508}509blinky.start(); // blinky is the first to leave ghostHouse510inky.start();511pinky.start();512clyde.start();513};514515this.checkForLevelUp = function () {516if ((this.pillCount === 0) && game.started) {517this.nextLevel();518}519};520521this.endGame = function (allLevelsCompleted = false) {522console.log('Game Over by ' + (allLevelsCompleted ? 'WIN' : 'LOSS'));523this.pause = true;524this.gameOver = true;525}526527this.toPixelPos = function (gridPos) {528return gridPos * 30;529};530531this.toGridPos = function (pixelPos) {532return ((pixelPos % 30) / 30);533};534535/* ------------ Start Pre-Build Walls ------------ */536this.buildWalls = function () {537if (this.ghostMode === 0) game.wallColor = "Blue";538else game.wallColor = "Red";539canvas_walls = document.createElement('canvas');540canvas_walls.width = game.canvas.width;541canvas_walls.height = game.canvas.height;542context_walls = canvas_walls.getContext("2d");543544context_walls.fillStyle = game.wallColor;545context_walls.strokeStyle = game.wallColor;546547//horizontal outer548buildWall(context_walls, 0, 0, 18, 1);549buildWall(context_walls, 0, 12, 18, 1);550551// vertical outer552buildWall(context_walls, 0, 0, 1, 6);553buildWall(context_walls, 0, 7, 1, 6);554buildWall(context_walls, 17, 0, 1, 6);555buildWall(context_walls, 17, 7, 1, 6);556557// ghost base558buildWall(context_walls, 7, 4, 1, 1);559buildWall(context_walls, 6, 5, 1, 2);560buildWall(context_walls, 10, 4, 1, 1);561buildWall(context_walls, 11, 5, 1, 2);562buildWall(context_walls, 6, 6, 6, 1);563564// ghost base door565context_walls.fillRect(8 * 2 * pacman.radius, pacman.radius / 2 + 4 * 2 * pacman.radius + 5, 4 * pacman.radius, 1);566567// single blocks568buildWall(context_walls, 4, 0, 1, 2);569buildWall(context_walls, 13, 0, 1, 2);570571buildWall(context_walls, 2, 2, 1, 2);572buildWall(context_walls, 6, 2, 2, 1);573buildWall(context_walls, 15, 2, 1, 2);574buildWall(context_walls, 10, 2, 2, 1);575576buildWall(context_walls, 2, 3, 2, 1);577buildWall(context_walls, 14, 3, 2, 1);578buildWall(context_walls, 5, 3, 1, 1);579buildWall(context_walls, 12, 3, 1, 1);580buildWall(context_walls, 3, 3, 1, 3);581buildWall(context_walls, 14, 3, 1, 3);582583buildWall(context_walls, 3, 4, 1, 1);584buildWall(context_walls, 14, 4, 1, 1);585586buildWall(context_walls, 0, 5, 2, 1);587buildWall(context_walls, 3, 5, 2, 1);588buildWall(context_walls, 16, 5, 2, 1);589buildWall(context_walls, 13, 5, 2, 1);590591buildWall(context_walls, 0, 7, 2, 2);592buildWall(context_walls, 16, 7, 2, 2);593buildWall(context_walls, 3, 7, 2, 2);594buildWall(context_walls, 13, 7, 2, 2);595596buildWall(context_walls, 4, 8, 2, 2);597buildWall(context_walls, 12, 8, 2, 2);598buildWall(context_walls, 5, 8, 3, 1);599buildWall(context_walls, 10, 8, 3, 1);600601buildWall(context_walls, 2, 10, 1, 1);602buildWall(context_walls, 15, 10, 1, 1);603buildWall(context_walls, 7, 10, 4, 1);604buildWall(context_walls, 4, 11, 2, 2);605buildWall(context_walls, 12, 11, 2, 2);606/* ------------ End Pre-Build Walls ------------ */607};608609}610611game = new Game();612613614615function Score() {616this.score = 0;617this.set = function (i) {618this.score = i;619};620this.add = function (i) {621this.score += i;622};623this.refresh = function (h) {624$(h).html("Score: " + this.score);625};626627}628629630631// used to play sounds during the game632var Sound = {};633Sound.play = function (sound) {634if (game.soundfx == 1) {635var audio = document.getElementById(sound);636(audio !== null) ? audio.play(): console.log(sound + " not found");637}638};639640641// Direction object in Constructor notation642function Direction(name, angle1, angle2, dirX, dirY) {643this.name = name;644this.angle1 = angle1;645this.angle2 = angle2;646this.dirX = dirX;647this.dirY = dirY;648this.equals = function (dir) {649return JSON.stringify(this) == JSON.stringify(dir);650};651}652653// Direction Objects654var up = new Direction("up", 1.75, 1.25, 0, -1); // UP655var left = new Direction("left", 1.25, 0.75, -1, 0); // LEFT656var down = new Direction("down", 0.75, 0.25, 0, 1); // DOWN657var right = new Direction("right", 0.25, 1.75, 1, 0); // RIGHT658/*var directions = [{},{},{},{}];659directions[0] = up;660directions[1] = down;661directions[2] = right;662directions[3] = left;*/663664665// DirectionWatcher666function directionWatcher() {667this.dir = null;668this.set = function (dir) {669this.dir = dir;670};671this.get = function () {672return this.dir;673};674}675676//var directionWatcher = new directionWatcher();677678// Ghost object in Constructor notation679function Ghost(name, gridPosX, gridPosY, image, gridBaseX, gridBaseY) {680this.name = name;681this.posX = gridPosX * 30;682this.posY = gridPosY * 30;683this.startPosX = gridPosX * 30;684this.startPosY = gridPosY * 30;685this.gridBaseX = gridBaseX;686this.gridBaseY = gridBaseY;687this.speed = game.ghostSpeedNormal;688this.images = JSON.parse(689'{"normal" : {' +690`"${GHOSTS.INKY}" : "0",` +691`"${GHOSTS.PINKY}" : "1",` +692`"${GHOSTS.BLINKY}" : "2",` +693`"${GHOSTS.CLYDE}" : "3"` +694'},' +695'"frightened1" : {' +696'"left" : "", "up": "", "right" : "", "down": ""},' +697'"frightened2" : {' +698'"left" : "", "up": "", "right" : "", "down": ""},' +699'"dead" : {' +700'"left" : "", "up": "", "right" : "", "down": ""}}'701);702this.image = new Image();703this.image.src = image;704this.ghostHouse = true;705this.dazzled = false;706this.dead = false;707this.dazzle = function () {708this.changeSpeed(game.ghostSpeedDazzled);709// ensure ghost doesnt leave grid710if (this.posX > 0) this.posX = this.posX - this.posX % this.speed;711if (this.posY > 0) this.posY = this.posY - this.posY % this.speed;712this.dazzled = true;713}714this.undazzle = function () {715// only change speed if ghost is not "dead"716if (!this.dead) this.changeSpeed(game.ghostSpeedNormal);717// ensure ghost doesnt leave grid718if (this.posX > 0) this.posX = this.posX - this.posX % this.speed;719if (this.posY > 0) this.posY = this.posY - this.posY % this.speed;720this.dazzled = false;721}722this.dazzleImg = new Image();723this.dazzleImg.src = 'img/dazzled.svg';724this.dazzleImg2 = new Image();725this.dazzleImg2.src = 'img/dazzled2.svg';726this.deadImg = new Image();727this.deadImg.src = 'img/dead.svg';728this.direction = right;729this.radius = pacman.radius;730this.draw = function (context) {731if (this.dead) {732context.drawImage(this.deadImg, this.posX, this.posY, 2 * this.radius, 2 * this.radius);733} else if (this.dazzled) {734if (pacman.beastModeTimer < 50 && pacman.beastModeTimer % 8 > 1) {735context.drawImage(this.dazzleImg2, this.posX, this.posY, 2 * this.radius, 2 * this.radius);736} else {737context.drawImage(this.dazzleImg, this.posX, this.posY, 2 * this.radius, 2 * this.radius);738}739} else context.drawImage(this.image, this.posX, this.posY, 2 * this.radius, 2 * this.radius);740}741this.getCenterX = function () {742return this.posX + this.radius;743}744this.getCenterY = function () {745return this.posY + this.radius;746}747748this.reset = function () {749this.dead = false;750this.posX = this.startPosX;751this.posY = this.startPosY;752this.ghostHouse = true;753this.undazzle();754}755756this.die = function () {757if (!this.dead) {758game.score.add(GHOST_POINTS);759//this.reset();760this.dead = true;761this.changeSpeed(game.ghostSpeedNormal);762}763}764this.changeSpeed = function (s) {765// adjust gridPosition to new speed766this.posX = Math.round(this.posX / s) * s;767this.posY = Math.round(this.posY / s) * s;768this.speed = s;769}770771this.move = function () {772773this.checkDirectionChange();774this.checkCollision();775776// leave Ghost House777if (this.ghostHouse == true) {778779// Clyde does not start chasing before 2/3 of all pills are eaten and if level is < 4780if (this.name == GHOSTS.CLYDE) {781if ((game.level < 4) || ((game.pillCount > 104 / 3))) this.stop = true;782else this.stop = false;783}784// Inky starts after 30 pills and only from the third level on785if (this.name == GHOSTS.INKY) {786if ((game.level < 3) || ((game.pillCount > 104 - 30))) this.stop = true;787else this.stop = false;788}789790if ((this.getGridPosY() == 5) && this.inGrid()) {791if ((this.getGridPosX() == 7)) this.setDirection(right);792if ((this.getGridPosX() == 8) || this.getGridPosX() == 9) this.setDirection(up);793if ((this.getGridPosX() == 10)) this.setDirection(left);794}795if ((this.getGridPosY() == 4) && ((this.getGridPosX() == 8) || (this.getGridPosX() == 9)) && this.inGrid()) {796console.log("ghosthouse -> false");797this.ghostHouse = false;798}799}800801if (!this.stop) {802// Move803this.posX += this.speed * this.dirX;804this.posY += this.speed * this.dirY;805806// Check if out of canvas807if (this.posX >= game.width - this.radius) this.posX = this.speed - this.radius;808if (this.posX <= 0 - this.radius) this.posX = game.width - this.speed - this.radius;809if (this.posY >= game.height - this.radius) this.posY = this.speed - this.radius;810if (this.posY <= 0 - this.radius) this.posY = game.height - this.speed - this.radius;811}812}813814this.checkCollision = function () {815816/* Check Back to Home */817if (this.dead && (this.getGridPosX() == this.startPosX / 30) && (this.getGridPosY() == this.startPosY / 30)) this.reset();818else {819820/* Check Ghost / Pacman Collision */821if ((between(pacman.getCenterX(), this.getCenterX() - 10, this.getCenterX() + 10)) &&822(between(pacman.getCenterY(), this.getCenterY() - 10, this.getCenterY() + 10))) {823if ((!this.dazzled) && (!this.dead)) {824pacman.die();825} else {826this.die();827}828}829}830}831832/* Pathfinding */833this.getNextDirection = function () {834// get next field835var pX = this.getGridPosX();836var pY = this.getGridPosY();837game.getMapContent(pX, pY);838var u, d, r, l; // option up, down, right, left839840// get target841if (this.dead) { // go Home842var tX = this.startPosX / 30;843var tY = this.startPosY / 30;844} else if (game.ghostMode == 0) { // Scatter Mode845var tX = this.gridBaseX;846var tY = this.gridBaseY;847} else if (game.ghostMode == 1) { // Chase Mode848849switch (this.name) {850851// target: 4 ahead and 4 left of pacman852case GHOSTS.PINKY:853var pdir = pacman.direction;854var pdirX = pdir.dirX == 0 ? -pdir.dirY : pdir.dirX;855var pdirY = pdir.dirY == 0 ? -pdir.dirX : pdir.dirY;856857var tX = (pacman.getGridPosX() + pdirX * 4) % (game.width / pacman.radius + 1);858var tY = (pacman.getGridPosY() + pdirY * 4) % (game.height / pacman.radius + 1);859break;860861// target: pacman862case GHOSTS.BLINKY:863var tX = pacman.getGridPosX();864var tY = pacman.getGridPosY();865break;866867// target:868case GHOSTS.INKY:869var tX = pacman.getGridPosX() + 2 * pacman.direction.dirX;870var tY = pacman.getGridPosY() + 2 * pacman.direction.dirY;871var vX = tX - blinky.getGridPosX();872var vY = tY - blinky.getGridPosY();873tX = Math.abs(blinky.getGridPosX() + vX * 2);874tY = Math.abs(blinky.getGridPosY() + vY * 2);875break;876877// target: pacman, until pacman is closer than 5 grid fields, then back to scatter878case GHOSTS.CLYDE:879var tX = pacman.getGridPosX();880var tY = pacman.getGridPosY();881var dist = Math.sqrt(Math.pow((pX - tX), 2) + Math.pow((pY - tY), 2));882883if (dist < 5) {884tX = this.gridBaseX;885tY = this.gridBaseY;886}887break;888889}890}891var oppDir = this.getOppositeDirection(); // ghosts are not allowed to change direction 180�892893var dirs = [{}, {}, {}, {}];894dirs[0].field = game.getMapContent(pX, pY - 1);895dirs[0].dir = up;896dirs[0].distance = Math.sqrt(Math.pow((pX - tX), 2) + Math.pow((pY - 1 - tY), 2));897898dirs[1].field = game.getMapContent(pX, pY + 1);899dirs[1].dir = down;900dirs[1].distance = Math.sqrt(Math.pow((pX - tX), 2) + Math.pow((pY + 1 - tY), 2));901902dirs[2].field = game.getMapContent(pX + 1, pY);903dirs[2].dir = right;904dirs[2].distance = Math.sqrt(Math.pow((pX + 1 - tX), 2) + Math.pow((pY - tY), 2));905906dirs[3].field = game.getMapContent(pX - 1, pY);907dirs[3].dir = left;908dirs[3].distance = Math.sqrt(Math.pow((pX - 1 - tX), 2) + Math.pow((pY - tY), 2));909910// Sort possible directions by distance911function compare(a, b) {912if (a.distance < b.distance)913return -1;914if (a.distance > b.distance)915return 1;916return 0;917}918var dirs2 = dirs.sort(compare);919920var r = this.dir;921var j;922923if (this.dead) {924for (var i = dirs2.length - 1; i >= 0; i--) {925if ((dirs2[i].field != "wall") && !(dirs2[i].dir.equals(this.getOppositeDirection()))) {926r = dirs2[i].dir;927}928}929} else {930for (var i = dirs2.length - 1; i >= 0; i--) {931if ((dirs2[i].field != "wall") && (dirs2[i].field != "door") && !(dirs2[i].dir.equals(this.getOppositeDirection()))) {932r = dirs2[i].dir;933}934}935}936this.directionWatcher.set(r);937return r;938}939this.setRandomDirection = function () {940var dir = Math.floor((Math.random() * 10) + 1) % 5;941942switch (dir) {943case 1:944if (this.getOppositeDirection().equals(up)) this.setDirection(down);945else this.setDirection(up);946break;947case 2:948if (this.getOppositeDirection().equals(down)) this.setDirection(up);949else this.setDirection(down);950break;951case 3:952if (this.getOppositeDirection().equals(right)) this.setDirection(left);953else this.setDirection(right);954break;955case 4:956if (this.getOppositeDirection().equals(left)) this.setDirection(right);957else this.setDirection(left);958break;959}960}961this.reverseDirection = function () {962console.log("reverseDirection: " + this.direction.name + " to " + this.getOppositeDirection().name);963this.directionWatcher.set(this.getOppositeDirection());964}965966}967968Ghost.prototype = new Figure();969970// Super Class for Pacman & Ghosts971function Figure() {972this.posX;973this.posY;974this.speed;975this.dirX = right.dirX;976this.dirY = right.dirY;977this.direction;978this.stop = true;979this.directionWatcher = new directionWatcher();980this.getNextDirection = function () {981console.log("Figure getNextDirection");982};983this.checkDirectionChange = function () {984if (this.inGrid() && (this.directionWatcher.get() == null)) this.getNextDirection();985if ((this.directionWatcher.get() != null) && this.inGrid()) {986//console.log("changeDirection to "+this.directionWatcher.get().name);987this.setDirection(this.directionWatcher.get());988this.directionWatcher.set(null);989}990991}992993994this.inGrid = function () {995if ((this.posX % (2 * this.radius) === 0) && (this.posY % (2 * this.radius) === 0)) return true;996return false;997}998this.getOppositeDirection = function () {999if (this.direction.equals(up)) return down;1000else if (this.direction.equals(down)) return up;1001else if (this.direction.equals(right)) return left;1002else if (this.direction.equals(left)) return right;1003}1004this.move = function () {10051006if (!this.stop) {1007this.posX += this.speed * this.dirX;1008this.posY += this.speed * this.dirY;10091010// Check if out of canvas1011if (this.posX >= game.width - this.radius) this.posX = this.speed - this.radius;1012if (this.posX <= 0 - this.radius) this.posX = game.width - this.speed - this.radius;1013if (this.posY >= game.height - this.radius) this.posY = this.speed - this.radius;1014if (this.posY <= 0 - this.radius) this.posY = game.height - this.speed - this.radius;1015}1016}1017this.stop = function () {1018this.stop = true;1019}1020this.start = function () {1021this.stop = false;1022}10231024this.getGridPosX = function () {1025return (this.posX - (this.posX % 30)) / 30;1026}1027this.getGridPosY = function () {1028return (this.posY - (this.posY % 30)) / 30;1029}1030this.setDirection = function (dir) {1031this.dirX = dir.dirX;1032this.dirY = dir.dirY;1033this.angle1 = dir.angle1;1034this.angle2 = dir.angle2;1035this.direction = dir;1036}1037this.setPosition = function (x, y) {1038this.posX = x;1039this.posY = y;1040}1041}10421043function pacman() {1044this.radius = 15;1045this.posX = 0;1046this.posY = 6 * 2 * this.radius;1047this.speed = 5;1048this.angle1 = 0.25;1049this.angle2 = 1.75;1050this.mouth = 1; /* Switches between 1 and -1, depending on mouth closing / opening */1051this.dirX = right.dirX;1052this.dirY = right.dirY;1053this.lives = 3;1054this.stuckX = 0;1055this.stuckY = 0;1056this.frozen = false; // used to play die Animation1057this.freeze = function () {1058this.frozen = true;1059}1060this.unfreeze = function () {1061this.frozen = false;1062}1063this.getCenterX = function () {1064return this.posX + this.radius;1065}1066this.getCenterY = function () {1067return this.posY + this.radius;1068}1069this.directionWatcher = new directionWatcher();10701071this.direction = right;10721073this.beastMode = false;1074this.beastModeTimer = 0;10751076this.checkCollisions = function () {10771078if ((this.stuckX == 0) && (this.stuckY == 0) && this.frozen == false) {10791080// Get the Grid Position of Pac1081var gridX = this.getGridPosX();1082var gridY = this.getGridPosY();1083var gridAheadX = gridX;1084var gridAheadY = gridY;10851086var field = game.getMapContent(gridX, gridY);10871088// get the field 1 ahead to check wall collisions1089if ((this.dirX == 1) && (gridAheadX < 17)) gridAheadX += 1;1090if ((this.dirY == 1) && (gridAheadY < 12)) gridAheadY += 1;1091var fieldAhead = game.getMapContent(gridAheadX, gridAheadY);109210931094/* Check Pill Collision */1095if ((field === "pill") || (field === "powerpill")) {1096//console.log("Pill found at ("+gridX+"/"+gridY+"). Pacman at ("+this.posX+"/"+this.posY+")");1097if (1098((this.dirX == 1) && (between(this.posX, game.toPixelPos(gridX) + this.radius - 5, game.toPixelPos(gridX + 1)))) ||1099((this.dirX == -1) && (between(this.posX, game.toPixelPos(gridX), game.toPixelPos(gridX) + 5))) ||1100((this.dirY == 1) && (between(this.posY, game.toPixelPos(gridY) + this.radius - 5, game.toPixelPos(gridY + 1)))) ||1101((this.dirY == -1) && (between(this.posY, game.toPixelPos(gridY), game.toPixelPos(gridY) + 5))) ||1102(fieldAhead === "wall")1103) {1104var s;1105if (field === "powerpill") {1106Sound.play("powerpill");1107s = POWERPILL_POINTS;1108this.enableBeastMode();1109game.startGhostFrightened();1110} else {1111Sound.play("waka");1112s = PILL_POINTS;1113game.pillCount--;1114}1115game.map.posY[gridY].posX[gridX].type = "null";1116game.score.add(s);1117}1118}11191120/* Check Wall Collision */1121if ((fieldAhead === "wall") || (fieldAhead === "door")) {1122this.stuckX = this.dirX;1123this.stuckY = this.dirY;1124pacman.stop();1125// get out of the wall1126if ((this.stuckX == 1) && ((this.posX % 2 * this.radius) != 0)) this.posX -= 5;1127if ((this.stuckY == 1) && ((this.posY % 2 * this.radius) != 0)) this.posY -= 5;1128if (this.stuckX == -1) this.posX += 5;1129if (this.stuckY == -1) this.posY += 5;1130}11311132}1133}1134this.checkDirectionChange = function () {1135if (this.directionWatcher.get() != null) {1136console.groupCollapsed('checkDirectionChange');1137//console.log("next Direction: "+directionWatcher.get().name);11381139if ((this.stuckX == 1) && this.directionWatcher.get() == right) this.directionWatcher.set(null);1140else {1141// reset stuck events1142this.stuckX = 0;1143this.stuckY = 0;114411451146// only allow direction changes inside the grid1147if ((this.inGrid())) {1148//console.log("changeDirection to "+directionWatcher.get().name);11491150// check if possible to change direction without getting stuck1151console.debug("x: " + this.getGridPosX() + " + " + this.directionWatcher.get().dirX);1152console.debug("y: " + this.getGridPosY() + " + " + this.directionWatcher.get().dirY);1153var x = this.getGridPosX() + this.directionWatcher.get().dirX;1154var y = this.getGridPosY() + this.directionWatcher.get().dirY;1155if (x <= -1) x = game.width / (this.radius * 2) - 1;1156if (x >= game.width / (this.radius * 2)) x = 0;1157if (y <= -1) x = game.height / (this.radius * 2) - 1;1158if (y >= game.heigth / (this.radius * 2)) y = 0;11591160console.debug("x: " + x);1161console.debug("y: " + y);1162var nextTile = game.map.posY[y].posX[x].type;1163console.debug("checkNextTile: " + nextTile);11641165if (nextTile != "wall") {1166this.setDirection(this.directionWatcher.get());1167this.directionWatcher.set(null);1168}1169}1170}1171console.groupEnd();1172}1173}1174this.setDirection = function (dir) {1175if (!this.frozen) {1176this.dirX = dir.dirX;1177this.dirY = dir.dirY;1178this.angle1 = dir.angle1;1179this.angle2 = dir.angle2;1180this.direction = dir;1181}1182}1183this.enableBeastMode = function () {1184this.beastMode = true;1185this.beastModeTimer = 240;1186console.debug("Beast Mode activated!");1187inky.dazzle();1188pinky.dazzle();1189blinky.dazzle();1190clyde.dazzle();1191};1192this.disableBeastMode = function () {1193this.beastMode = false;1194console.debug("Beast Mode is over!");1195inky.undazzle();1196pinky.undazzle();1197blinky.undazzle();1198clyde.undazzle();1199};1200this.move = function () {12011202if (!this.frozen) {1203if (this.beastModeTimer > 0) {1204this.beastModeTimer--;1205//console.log("Beast Mode: "+this.beastModeTimer);1206}1207if ((this.beastModeTimer == 0) && (this.beastMode == true)) this.disableBeastMode();12081209this.posX += this.speed * this.dirX;1210this.posY += this.speed * this.dirY;12111212// Check if out of canvas1213if (this.posX >= game.width - this.radius) this.posX = 5 - this.radius;1214if (this.posX <= 0 - this.radius) this.posX = game.width - 5 - this.radius;1215if (this.posY >= game.height - this.radius) this.posY = 5 - this.radius;1216if (this.posY <= 0 - this.radius) this.posY = game.height - 5 - this.radius;1217} else this.dieAnimation();1218}12191220this.eat = function () {12211222if (!this.frozen) {1223if (this.dirX == this.dirY == 0) {12241225this.angle1 -= this.mouth * 0.07;1226this.angle2 += this.mouth * 0.07;12271228var limitMax1 = this.direction.angle1;1229var limitMax2 = this.direction.angle2;1230var limitMin1 = this.direction.angle1 - 0.21;1231var limitMin2 = this.direction.angle2 + 0.21;12321233if (this.angle1 < limitMin1 || this.angle2 > limitMin2) {1234this.mouth = -1;1235}1236if (this.angle1 >= limitMax1 || this.angle2 <= limitMax2) {1237this.mouth = 1;1238}1239}1240}1241}1242this.stop = function () {1243this.dirX = 0;1244this.dirY = 0;1245}1246this.reset = function () {1247this.unfreeze();1248this.posX = 0;1249this.posY = 6 * 2 * this.radius;1250this.setDirection(right);1251this.stop();1252this.stuckX = 0;1253this.stuckY = 0;1254//console.log("reset pacman");1255}1256this.dieAnimation = function () {1257this.angle1 += 0.05;1258this.angle2 -= 0.05;1259if (this.angle1 >= this.direction.angle1 + 0.7 || this.angle2 <= this.direction.angle2 - 0.7) {1260this.dieFinal();1261}1262}1263this.die = function () {1264Sound.play("die");1265this.freeze();1266this.dieAnimation();1267}1268this.dieFinal = function () {1269this.reset();1270pinky.reset();1271inky.reset();1272blinky.reset();1273clyde.reset();1274this.lives--;1275console.log("pacman died, " + this.lives + " lives left");1276if (this.lives <= 0) {1277game.endGame();1278game.showHighscoreForm();1279}1280game.drawHearts(this.lives);1281}1282this.getGridPosX = function () {1283return (this.posX - (this.posX % 30)) / 30;1284}1285this.getGridPosY = function () {1286return (this.posY - (this.posY % 30)) / 30;1287}1288}1289pacman.prototype = new Figure();1290var pacman = new pacman();1291game.buildWalls();129212931294// Check if a new cache is available on page load.1295function checkAppCache() {1296console.log('check AppCache');1297window.applicationCache.addEventListener('updateready', function (e) {1298console.log("AppCache: updateready");1299if (window.applicationCache.status == window.applicationCache.UPDATEREADY) {13001301// Browser downloaded a new app cache.1302// Swap it in and reload the page to get the new hotness.1303window.applicationCache.swapCache();1304if (confirm('A new version of this site is available. Load it?')) {1305window.location.reload();1306}13071308} else {1309// Manifest didn't change. Nothing new to server.1310}1311}, false);13121313window.applicationCache.addEventListener('cached', function (e) {1314console.log("AppCache: cached");1315}, false);13161317}131813191320// Action starts here:13211322function hideAdressbar() {1323console.log("hide adressbar");1324$("html").scrollTop(1);1325$("body").scrollTop(1);1326}13271328$(document).ready(function () {13291330if (!['localhost', '127.0.0.1'].includes(window.location.hostname)) {1331logger.disableLogger();1332}13331334$.ajaxSetup({1335mimeType: "application/json"1336});13371338$.ajaxSetup({1339beforeSend: function (xhr) {1340if (xhr.overrideMimeType) {1341xhr.overrideMimeType("application/json");1342}1343}1344});13451346// Hide address bar1347hideAdressbar();13481349if (window.applicationCache != null) checkAppCache();13501351/* -------------------- EVENT LISTENERS -------------------------- */13521353// Listen for resize changes1354/*window.addEventListener("resize", function() {1355// Get screen size (inner/outerWidth, inner/outerHeight)1356// deactivated because of problems1357if ((window.outerHeight < window.outerWidth) && (window.outerHeight < 720)) {1358game.pauseAndShowMessage("Rotate Device","Your screen is too small to play in landscape view.");1359console.log("rotate your device to portrait!");1360}1361}, false);*/136213631364// --------------- Controls136513661367// Keyboard1368window.addEventListener('keydown', doKeyDown, true);13691370// pause / resume game on canvas click1371$('#canvas-container').click(function () {1372if (!(game.gameOver === true)) game.pauseResume();1373});13741375// highscore form submit event listener1376$('body').on('click', '#score-submit', function () {1377console.log("submit highscore pressed");1378if ($('#playerName').val() === "" || $('#playerName').val() === undefined) {1379$('#form-validator').html("Please enter a name<br/>");1380} else {1381$('#form-validator').html("");1382addHighscore();1383}1384});13851386$('body').on('click', '#show-highscore', function () {1387game.showContent('highscore-content');1388getHighscore();1389});13901391// Hammerjs Touch Events1392Hammer('.container').on("swiperight", function (event) {1393if ($('#game-content').is(":visible")) {1394event.gesture.preventDefault();1395pacman.directionWatcher.set(right);1396}1397});1398Hammer('.container').on("swipeleft", function (event) {1399if ($('#game-content').is(":visible")) {1400event.gesture.preventDefault();1401pacman.directionWatcher.set(left);1402}1403});1404Hammer('.container').on("swipeup", function (event) {1405if ($('#game-content').is(":visible")) {1406event.gesture.preventDefault();1407pacman.directionWatcher.set(up);1408}1409});1410Hammer('.container').on("swipedown", function (event) {1411if ($('#game-content').is(":visible")) {1412event.gesture.preventDefault();1413pacman.directionWatcher.set(down);1414}1415});14161417// Mobile Control Buttons1418$(document).on('touchend mousedown', '#up', function (event) {1419event.preventDefault();1420pacman.directionWatcher.set(up);1421});1422$(document).on('touchend mousedown', '#down', function (event) {1423event.preventDefault();1424pacman.directionWatcher.set(down);1425});1426$(document).on('touchend mousedown', '#left', function (event) {1427event.preventDefault();1428pacman.directionWatcher.set(left);1429});1430$(document).on('touchend mousedown', '#right', function (event) {1431event.preventDefault();1432pacman.directionWatcher.set(right);1433});14341435// Menu1436$(document).on('click', '.button#newGame', function (event) {1437game.newGame();1438});1439$(document).on('click', '.button#highscore', function (event) {1440game.showContent('highscore-content');1441getHighscore();1442});1443$(document).on('click', '.button#instructions', function (event) {1444game.showContent('instructions-content');1445});1446$(document).on('click', '.button#info', function (event) {1447game.showContent('info-content');1448});1449// back button1450$(document).on('click', '.button#back', function (event) {1451game.showContent('game-content');1452});1453// toggleSound1454$(document).on('click', '.controlSound', function (event) {1455game.toggleSound();1456});1457// get latest1458$(document).on('click', '#updateCode', function (event) {1459console.log('check for new version');1460event.preventDefault();1461window.applicationCache.update();1462});14631464// checkAppCache();14651466canvas = $("#myCanvas").get(0);1467context = canvas.getContext("2d");14681469/* --------------- GAME INITIALISATION ------------------------------------14701471TODO: put this into Game object and change code to accept different setups / levels14721473-------------------------------------------------------------------------- */14741475game.init(0);14761477renderContent();1478});14791480function renderContent() {1481// Refresh Score1482game.score.refresh(".score");14831484// Pills1485context.beginPath();1486context.fillStyle = "White";1487context.strokeStyle = "White";14881489var dotPosY;1490if (game.map && game.map.posY && game.map.posY.length > 0) {1491$.each(game.map.posY, (i, row) => {1492dotPosY = row.row;1493$.each(row.posX, (j, column) => {1494if (column.type == "pill") {1495context.arc(game.toPixelPos(column.col - 1) + pacman.radius, game.toPixelPos(dotPosY - 1) + pacman.radius, game.pillSize, 0 * Math.PI, 2 * Math.PI);1496context.moveTo(game.toPixelPos(column.col - 1), game.toPixelPos(dotPosY - 1));1497} else if (column.type == "powerpill") {1498context.arc(game.toPixelPos(column.col - 1) + pacman.radius, game.toPixelPos(dotPosY - 1) + pacman.radius, game.powerpillSizeCurrent, 0 * Math.PI, 2 * Math.PI);1499context.moveTo(game.toPixelPos(column.col - 1), game.toPixelPos(dotPosY - 1));1500}1501});1502});1503} else {1504console.warn('Map not loaded (yet).')1505}15061507context.fill();15081509// Walls1510context.drawImage(canvas_walls, 0, 0);151115121513if (game.started) {1514// Ghosts1515pinky.draw(context);1516blinky.draw(context);1517inky.draw(context);1518clyde.draw(context);151915201521// Pac Man1522context.beginPath();1523context.fillStyle = "Yellow";1524context.strokeStyle = "Yellow";1525context.arc(pacman.posX + pacman.radius, pacman.posY + pacman.radius, pacman.radius, pacman.angle1 * Math.PI, pacman.angle2 * Math.PI);1526context.lineTo(pacman.posX + pacman.radius, pacman.posY + pacman.radius);1527context.stroke();1528context.fill();1529}15301531}15321533// TODO: only for debugging1534function renderGrid(gridPixelSize, color) {1535context.save();1536context.lineWidth = 0.5;1537context.strokeStyle = color;15381539// horizontal grid lines1540for (var i = 0; i <= canvas.height; i = i + gridPixelSize) {1541context.beginPath();1542context.moveTo(0, i);1543context.lineTo(canvas.width, i);1544context.closePath();1545context.stroke();1546}15471548// vertical grid lines1549for (var i = 0; i <= canvas.width; i = i + gridPixelSize) {1550context.beginPath();1551context.moveTo(i, 0);1552context.lineTo(i, canvas.height);1553context.closePath();1554context.stroke();1555}15561557context.restore();1558}155915601561function animationLoop() {15621563// if (gameOver) return;15641565canvas.width = canvas.width;1566// enable next line to show grid1567// renderGrid(pacman.radius, "red");1568renderContent();15691570if (game.dieAnimation == 1) pacman.dieAnimation();1571if (game.pause !== true) {1572// Make changes before next loop1573pacman.move();1574pacman.eat();1575pacman.checkDirectionChange();1576pacman.checkCollisions(); // has to be the LAST method called on pacman15771578blinky.move();1579inky.move();1580pinky.move();1581clyde.move();15821583game.checkGhostMode();158415851586// All dots collected?1587game.checkForLevelUp();1588}15891590//requestAnimationFrame(animationLoop);1591setTimeout(animationLoop, game.refreshRate);15921593}1594159515961597function doKeyDown(evt) {15981599switch (evt.keyCode) {1600case 38: // UP Arrow Key pressed1601evt.preventDefault();1602case 87: // W pressed1603pacman.directionWatcher.set(up);1604break;1605case 40: // DOWN Arrow Key pressed1606evt.preventDefault();1607case 83: // S pressed1608pacman.directionWatcher.set(down);1609break;1610case 37: // LEFT Arrow Key pressed1611evt.preventDefault();1612case 65: // A pressed1613pacman.directionWatcher.set(left);1614break;1615case 39: // RIGHT Arrow Key pressed1616evt.preventDefault();1617case 68: // D pressed1618pacman.directionWatcher.set(right);1619break;1620case 78: // N pressed1621if (!$('#playerName').is(':focus')) {1622game.pause = 1;1623game.newGame();1624}1625break;1626case 77: // M pressed1627game.toggleSound();1628break;1629case 8: // Backspace pressed -> show Game Content1630case 27: // ESC pressed -> show Game Content1631if (!$('#playerName').is(':focus')) {1632evt.preventDefault();1633game.showContent('game-content');1634}1635break;1636case 32: // SPACE pressed -> pause Game1637evt.preventDefault();1638if (!(game.gameOver == true) &&1639$('#game-content').is(':visible')1640) game.pauseResume();1641break;1642}1643}1644}16451646geronimo();16471648