/** * Variable that handles game mechanics * * @namespace */ const game = { audio: {}, lang: {}, // Holds cache reference to media - used in code to get audio and dicitonary image: {}, sprite: {}, // [Not directly used] Holds cache reference to media mediaTypes: ['lang', 'audio', 'image', 'sprite'], // [Not directly used] loadedMedia: [], // [Not directly used] isLoaded: [], // [Not directly used] /** * Load URLs to cache
* To be used only inside function preload() if a state needs to load media
* URLs are in globals.js * * @namespace */ load: { /** * Loads dictionary to cache * * @param {string} url url for the selected language */ lang: function (url) { game.isLoaded['lang'] = false; game.loadedMedia['lang'] = 0; game.lang = {}; // Clear previously loaded language const init = { mode: 'same-origin' }; fetch(url, init) // Fetch API .then(function (response) { return response.text(); }) .then(function (text) { let msg = text.split('\n'); msg.forEach(cur => { try { let msg = cur.split('='); game.lang[msg[0].trim()] = msg[1].trim(); } catch (Error) { if (debugMode) console.log('Sintax error fixed'); } game.load._informMediaIsLoaded(msg.length - 1, 'lang'); }); }); }, /** * Loads audio files to cache * * @param {array} urls audio urls for the current state */ audio: function (urls) { game.isLoaded['audio'] = false; game.loadedMedia['audio'] = 0; urls = this._getNotLoadedUrls(urls, game.audio); if (urls.length == 0) { game.load._informMediaIsLoaded(0, 'audio'); } else { const init = { mode: 'same-origin' }; urls.forEach(cur => { fetch(cur[1][1], init) // Fetch API .then(response => response.blob()) .then(function (myBlob) { game.audio[cur[0]] = new Audio(URL.createObjectURL(myBlob)); game.load._informMediaIsLoaded(urls.length - 1, 'audio'); }); }); } }, /** * Loads images to cache * * @param {array} urls image urls for the current state */ image: function (urls) { game.isLoaded['image'] = false; game.loadedMedia['image'] = 0; urls = this._getNotLoadedUrls(urls, game.image); if (urls.length == 0) { game.load._informMediaIsLoaded(0, 'image'); } else { urls.forEach(cur => { const img = new Image(); // HTMLImageElement img.onload = () => { game.image[cur[0]] = img; game.load._informMediaIsLoaded(urls.length - 1, 'image'); } img.src = cur[1]; }); } }, /** * Loads spritesheets to cache * * @param {array} urls spritesheet urls for the current state */ sprite: function (urls) { game.isLoaded['sprite'] = false; game.loadedMedia['sprite'] = 0; urls = this._getNotLoadedUrls(urls, game.sprite); if (urls.length == 0) { game.load._informMediaIsLoaded(0, 'sprite'); } else { urls.forEach(cur => { const img = new Image(); // HTMLImageElement img.onload = () => { game.sprite[cur[0]] = img; game.load._informMediaIsLoaded(urls.length - 1, 'sprite'); } img.src = cur[1]; img.frames = cur[2]; }); } }, /** * Updates list of urls to whats not already in cache * * @param {array} urls array of urls * @param {object} media whats in cache for current media type * @returns {array} array of urls without what is already in cache */ _getNotLoadedUrls: function (urls, media) { const newUrls = []; urls.forEach(cur => { if (media[cur[0]] == undefined) newUrls.push(cur); }); return newUrls; }, /** * Informs that all files of current media type are loaded * * @param {number} last last index * @param {String} type media type */ _informMediaIsLoaded: function (last, type) { if (game.loadedMedia[type] == last) { game.isLoaded[type] = true; game.load._isPreloadDone(type); } game.loadedMedia[type]++; }, /** * When all types of media are loaded for the current state, calls function create() through state * * @param {String} type type of media fully loaded for current state */ _isPreloadDone: function (type) { let flag = true; for (let i in game.mediaTypes) { if (game.isLoaded[game.mediaTypes[i]] == false) { // If all media for current state is loaded, flag wont change flag = false; break; } } // If flag doesnt change preload is complete if (flag) { game.isLoaded = []; game.loadedMedia[type] = 0; game.state._create(); } } }, /** * Adds new media to 'media queue'
* All queued media is actually drawn on the canvas using game.render.all() * * @namespace */ add: { /** * Adds image to media queue

* * game.add.image(x, y, img)
* game.add.image(x, y, img, scale)
* game.add.image(x, y, img, scale, alpha) * * @param {number} x default x coordinate for the figure * @param {number} y default x coordinate for the figure * @param {string} img name of the cached image * @param {number} scale scale for the image (default = 1) * @param {number} alpha level of transparency, from 0 (invisible) to 1 (completely visible)) * @returns {object} */ image: function (x, y, img, scale, alpha) { if (x == undefined || y == undefined || img == undefined) console.error('Game error: missing parameters'); else if (game.image[img] == undefined) console.error('Game error: image not found in cache: ' + img); else { const med = { typeOfMedia: 'image', name: img, x: x || game.add._default.x, y: y || game.add._default.y, _xWithAnchor: x || game.add._default._xWithAnchor, _yWithAnchor: y || game.add._default._yWithAnchor, xAnchor: game.add._default.xAnchor, yAnchor: game.add._default.yAnchor, shadow: game.add._default.shadow, shadowColor: game.add._default.shadowColor, shadowBlur: game.add._default.shadowBlur, alpha: (alpha != undefined) ? alpha : game.add._default.alpha, scale: scale || game.add._default.scale, width: game.image[img].width, height: game.image[img].height, anchor: function (xAnchor, yAnchor) { this.xAnchor = xAnchor; this.yAnchor = yAnchor; }, get xWithAnchor() { return this.x - (this.width * this.scale * this.xAnchor); }, get yWithAnchor() { return this.y - (this.height * this.scale * this.yAnchor); } }; med.originalScale = med.scale; game.render.queue.push(med); return med; } }, /** * Adds spritesheet to media queue

* * game.add.sprite(x, y, img)
* game.add.sprite(x, y, img, curFrame)
* game.add.sprite(x, y, img, curFrame, scale)
* game.add.sprite(x, y, img, curFrame, scale, alpha) * * @param {number} x default x coordinate for the figure * @param {number} y default x coordinate for the figure * @param {string} img name of the cached spritesheet * @param {number} curFrame current frame from the spritesheet (default = 0) * @param {number} scale scale for the spritesheet (default = 1) * @param {number} alpha level of transparency, from 0 (invisible) to 1 (completely visible)) * @returns {object} */ sprite: function (x, y, img, curFrame, scale, alpha) { if (x == undefined || y == undefined || img == undefined) console.error('Game error: missing parameters'); else if (game.sprite[img] == undefined) console.error('Game error: sprite not found in cache: ' + img); else { const med = { typeOfMedia: 'sprite', name: img, x: x || game.add._default.x, y: y || game.add._default.y, _xWithAnchor: x || game.add._default._xWithAnchor, _yWithAnchor: y || game.add._default._yWithAnchor, xAnchor: game.add._default.xAnchor, yAnchor: game.add._default.yAnchor, shadow: game.add._default.shadow, shadowColor: game.add._default.shadowColor, shadowBlur: game.add._default.shadowBlur, alpha: (alpha != undefined) ? alpha : game.add._default.alpha, scale: scale || game.add._default.scale, width: game.sprite[img].width / game.sprite[img].frames, // Frame width height: game.sprite[img].height, // Frame height curFrame: curFrame || 0, anchor: function (xAnchor, yAnchor) { this.xAnchor = xAnchor; this.yAnchor = yAnchor; }, get xWithAnchor() { return this.x - (this.width * this.scale * this.xAnchor); }, get yWithAnchor() { return this.y - (this.height * this.scale * this.yAnchor); } }; med.originalScale = med.scale; game.render.queue.push(med); return med; } }, /** * Adds text to media queue

* * game.add.text(x, y, text, style)
* game.add.text(x, y, text, style, align) * * @param {number} x default x coordinate for the figure * @param {number} y default x coordinate for the figure * @param {string} text text to be displayed on screen * @param {object} style object containing font, text color and align for the text * @param {string} align text align on screen [left, center or right] * @returns {object} */ text: function (x, y, text, style, align) { if (x == undefined || y == undefined || text == undefined || style == undefined) console.error('Game error: missing parameters'); else { const med = { typeOfMedia: 'text', name: text, x: x || game.add._default.x, y: y || game.add._default.y, _xWithAnchor: x || game.add._default._xWithAnchor, _yWithAnchor: y || game.add._default._yWithAnchor, xAnchor: game.add._default.xAnchor, yAnchor: game.add._default.yAnchor, shadow: game.add._default.shadow, shadowColor: game.add._default.shadowColor, shadowBlur: game.add._default.shadowBlur, alpha: game.add._default.alpha, font: style.font || game.add._default.font, fill: style.fill || game.add._default.fill, align: align || style.align || game.add._default.align, anchor: function () { console.error('Game error: there\'s no anchor for text'); }, set style(style) { this.font = style.font; this.fill = style.fill; this.align = style.align; }, get xWithAnchor() { return this.x; }, get yWithAnchor() { return this.y; }, }; game.render.queue.push(med); return med; } }, /** * Adds geometric shapes * @namespace */ geom: { /** * Adds rectangle to media queue

* * game.add.geom.rect(x, y, width, height)
* game.add.geom.rect(x, y, width, height, lineColor)
* game.add.geom.rect(x, y, width, height, lineColor, lineWidth)
* game.add.geom.rect(x, y, width, height, lineColor, lineWidth, fillColor)
* game.add.geom.rect(x, y, width, height, lineColor, lineWidth, fillColor, alpha) * * @param {number} x default x coordinate for top left corner of the rectangle * @param {number} y default y coordinate for top left corner of the rectangle * @param {number} width rectangle width * @param {number} height rectangle height * @param {string} lineColor stroke color * @param {number} lineWidth stroke width * @param {string} fillColor fill color * @param {number} alpha level of transparency, from 0 (invisible) to 1 (completely visible)) * @returns {object} */ rect: function (x, y, width, height, lineColor, lineWidth, fillColor, alpha) { if (x == undefined || y == undefined || width == undefined) console.error('Game error: missing parameters'); else { const med = { typeOfMedia: 'rect', x: x || game.add._default.x, y: y || game.add._default.y, _xWithAnchor: x || game.add._default._xWithAnchor, _yWithAnchor: y || game.add._default._yWithAnchor, xAnchor: game.add._default.xAnchor, yAnchor: game.add._default.yAnchor, shadow: game.add._default.shadow, shadowColor: game.add._default.shadowColor, shadowBlur: game.add._default.shadowBlur, alpha: (alpha != undefined) ? alpha : game.add._default.alpha, scale: game.add._default.scale, width: 0, height: 0, lineColor: lineColor || game.add._default.lineColor, lineWidth: 0, fillColor: fillColor || game.add._default.fillColor, anchor: function (xAnchor, yAnchor) { this.xAnchor = xAnchor; this.yAnchor = yAnchor; }, get xWithAnchor() { return this.x - (this.width * this.scale * this.xAnchor); }, get yWithAnchor() { return this.y - (this.height * this.scale * this.yAnchor); } }; med.originalScale = med.scale; if (width != 0) { med.width = width || game.add._default.width; } if (height != 0) { med.height = height || width || game.add._default.height; } if (lineWidth != 0) { med.lineWidth = lineWidth || game.add._default.lineWidth; } game.render.queue.push(med); return med; } }, /** * Adds circle to media queue

* * game.add.geom.circle(x, y, diameter)
* game.add.geom.circle(x, y, diameter, lineColor)
* game.add.geom.circle(x, y, diameter, lineColor, lineWidth)
* game.add.geom.circle(x, y, diameter, lineColor, lineWidth, fillColor)
* game.add.geom.circle(x, y, diameter, lineColor, lineWidth, fillColor, alpha) * * @param {number} x default x coordinate for the circle center * @param {number} y default y coordinate for the circle center * @param {number} diameter circle diameter * @param {string} lineColor stroke color * @param {number} lineWidth stroke width * @param {string} fillColor fill color * @param {number} alpha level of transparency, from 0 (invisible) to 1 (completely visible)) * @returns {object} */ circle: function (x, y, diameter, lineColor, lineWidth, fillColor, alpha) { if (x == undefined || y == undefined || diameter == undefined) console.error('Game error: missing parameters'); else { const med = { typeOfMedia: 'arc', x: x || game.add._default.x, y: y || game.add._default.y, _xWithAnchor: x || game.add._default._xWithAnchor, _yWithAnchor: y || game.add._default._yWithAnchor, xAnchor: game.add._default.xAnchor, yAnchor: game.add._default.yAnchor, shadow: game.add._default.shadow, shadowColor: game.add._default.shadowColor, shadowBlur: game.add._default.shadowBlur, alpha: (alpha != undefined) ? alpha : game.add._default.alpha, scale: game.add._default.scale, diameter: 0, width: 0, height: 0, angleStart: 0, angleEnd: 2 * Math.PI, anticlockwise: game.add._default.anticlockwise, lineColor: lineColor || game.add._default.lineColor, lineWidth: 0, fillColor: fillColor || game.add._default.fillColor, anchor: function (xAnchor, yAnchor) { this.xAnchor = xAnchor; this.yAnchor = yAnchor; }, get xWithAnchor() { return this.x - (this.width * this.scale * this.xAnchor); }, get yWithAnchor() { return this.y - (this.height * this.scale * this.yAnchor); } }; med.originalScale = med.scale; if (diameter != 0) { med.diameter = diameter || game.add._default.diameter; med.width = med.height = med.diameter; } if (lineWidth != 0) { med.lineWidth = lineWidth || game.add._default.lineWidth; } game.render.queue.push(med); return med; } }, /** * Adds arc to media queue

* * game.add.geom.arc(x, y, diameter, angleStart, angleEnd)
* game.add.geom.arc(x, y, diameter, angleStart, angleEnd, anticlockWise)
* game.add.geom.arc(x, y, diameter, angleStart, angleEnd, anticlockWise, lineColor)
* game.add.geom.arc(x, y, diameter, angleStart, angleEnd, anticlockWise, lineColor, lineWidth)
* game.add.geom.arc(x, y, diameter, angleStart, angleEnd, anticlockWise, lineColor, lineWidth, fillColor)
* game.add.geom.arc(x, y, diameter, angleStart, angleEnd, anticlockWise, lineColor, lineWidth, fillColor, alpha) * * @param {number} x default x coordinate for the arc center * @param {number} y default y coordinate for the arc center * @param {number} diameter arc diameter * @param {number} angleStart angle to start the arc * @param {number} angleEnd angle to end the arc * @param {boolean} anticlockwise if true, arc is created anticlockwise [default=false] * @param {string} lineColor stroke color * @param {number} lineWidth stroke width * @param {string} fillColor fill color * @param {number} alpha level of transparency, from 0 (invisible) to 1 (completely visible)) * @returns {object} */ arc: function (x, y, diameter, angleStart, angleEnd, anticlockwise, lineColor, lineWidth, fillColor, alpha) { if (x == undefined || y == undefined || diameter == undefined || angleStart == undefined || angleEnd == undefined) console.error('Game error: missing parameters'); else { const med = { typeOfMedia: 'arc', x: x || game.add._default.x, y: y || game.add._default.y, _xWithAnchor: x || game.add._default._xWithAnchor, _yWithAnchor: y || game.add._default._yWithAnchor, xAnchor: game.add._default.xAnchor, yAnchor: game.add._default.yAnchor, shadow: game.add._default.shadow, shadowColor: game.add._default.shadowColor, shadowBlur: game.add._default.shadowBlur, alpha: (alpha != undefined) ? alpha : game.add._default.alpha, scale: game.add._default.scale, diameter: 0, width: 0, height: 0, angleStart: angleStart || 0, angleEnd: angleEnd || 2 * Math.PI, anticlockwise: anticlockwise || game.add._default.anticlockwise, lineColor: lineColor || game.add._default.lineColor, lineWidth: 0, fillColor: fillColor || game.add._default.fillColor, anchor: function (xAnchor, yAnchor) { this.xAnchor = xAnchor; this.yAnchor = yAnchor; }, get xWithAnchor() { return this.x - (this.width * this.scale * this.xAnchor); }, get yWithAnchor() { return this.y - (this.height * this.scale * this.yAnchor); } }; med.originalScale = med.scale; if (diameter != 0) { med.diameter = diameter || game.add._default.diameter; med.width = med.height = med.diameter; } if (lineWidth != 0) { med.lineWidth = lineWidth || game.add._default.lineWidth; } game.render.queue.push(med); return med; } } }, // [Not directly used] _default: { // All media x: 0, y: 0, _xWithAnchor: 0, _yWithAnchor: 0, xAnchor: 0, yAnchor: 0, shadow: false, shadowColor: '#0075c5', shadowBlur: 20, alpha: 1, // Image, sprite, square, circle scale: 1, // Text font: '14px Arial,sans-serif', fill: '#000', align: 'center', // Square, circle (image and sprite have width and height, but do not have default values) width: 50, height: 50, lineColor: '#000', lineWidth: 1, fillColor: 0, // Default no fill // Circle diameter: 50, anticlockwise: false, }, }, /** * Renders media on current screen
* Uses properties of html canvas to draw media on screen during game loop * * @namespace */ render: { queue: [], // [Not directly used] Media queue to be rendered by the current state /** [Not directly used]
* Renders image on canvas * * @param {object} cur current media in media queue */ _image: function (cur) { const x = cur.xWithAnchor, y = cur.yWithAnchor; if (cur.rotate && cur.rotate != 0) { context.save(); context.translate(cur.x, cur.y); context.rotate(cur.rotate * Math.PI / 180); context.translate(-cur.x, -cur.y); } context.globalAlpha = cur.alpha; context.shadowBlur = (cur.shadow) ? cur.shadowBlur : 0; context.shadowColor = cur.shadowColor; context.drawImage( game.image[cur.name], x, y, cur.width * cur.scale, cur.height * cur.scale ); context.shadowBlur = 0; context.globalAlpha = 1; if (cur.rotate && cur.rotate != 0) context.restore(); }, /** [Not directly used]
* Renders spritesheet on canvas * * @param {object} cur current media in media queue */ _sprite: function (cur) { const x = cur.xWithAnchor, y = cur.yWithAnchor; if (cur.rotate && cur.rotate != 0) { context.save(); context.translate(cur.x, cur.y); context.rotate(cur.rotate * Math.PI / 180); context.translate(-cur.x, -cur.y); } context.globalAlpha = cur.alpha; context.shadowBlur = (cur.shadow) ? cur.shadowBlur : 0; context.shadowColor = cur.shadowColor; context.drawImage( game.sprite[cur.name], cur.width * cur.curFrame, 0, cur.width, cur.height, x, y, cur.width * cur.scale, cur.height * cur.scale ); context.shadowBlur = 0; context.globalAlpha = 1; if (cur.rotate && cur.rotate != 0) context.restore(); }, /** [Not directly used]
* Renders text on canvas * * @param {object} cur current media in media queue */ _text: function (cur) { const x = cur.xWithAnchor, y = cur.yWithAnchor; if (cur.rotate && cur.rotate != 0) { context.save(); context.translate(cur.x, cur.y); context.rotate(cur.rotate * Math.PI / 180); context.translate(-cur.x, -cur.y); } context.globalAlpha = cur.alpha; context.shadowBlur = (cur.shadow) ? cur.shadowBlur : 0; context.shadowColor = cur.shadowColor; context.font = cur.font; context.textAlign = cur.align; context.fillStyle = cur.fill; context.fillText(cur.name, x, y); context.shadowBlur = 0; context.globalAlpha = 1; if (cur.rotate && cur.rotate != 0) context.restore(); }, /**[Not directly used]
* Renders geometric shapes * * @namespace */ _geom: { /** * Renders rectangle on canvas * * @param {object} cur current media in media queue */ _rect: function (cur) { const x = cur.xWithAnchor, y = cur.yWithAnchor; // Rotation if (cur.rotate && cur.rotate != 0) { context.save(); context.translate(cur.x, cur.y); context.rotate(cur.rotate * Math.PI / 180); context.translate(-cur.x, -cur.y); } // Alpha context.globalAlpha = cur.alpha; // Shadow context.shadowBlur = (cur.shadow) ? cur.shadowBlur : 0; context.shadowColor = cur.shadowColor; // Fill if (cur.fillColor != 0) { context.fillStyle = cur.fillColor; context.fillRect(x, y, cur.width * cur.scale, cur.height * cur.scale); } // Stroke if (cur.lineWidth != 0) { context.strokeStyle = cur.lineColor; context.lineWidth = cur.lineWidth; context.strokeRect(x, y, cur.width * cur.scale, cur.height * cur.scale); } // End context.shadowBlur = 0; context.globalAlpha = 1; if (cur.rotate && cur.rotate != 0) context.restore(); }, /** * Renders arc on canvas (circle or incomplete arc) * * @param {object} cur current media in media queue */ _arc: function (cur) { const x = cur.xWithAnchor, y = cur.yWithAnchor; // Rotation if (cur.rotate && cur.rotate != 0) { context.save(); context.translate(cur.x, cur.y); context.rotate(cur.rotate * Math.PI / 180); context.translate(-cur.x, -cur.y); } // Alpha context.globalAlpha = cur.alpha; // Shadow context.shadowBlur = (cur.shadow) ? cur.shadowBlur : 0; context.shadowColor = cur.shadowColor; // Fill info if (cur.fillColor != 0) context.fillStyle = cur.fillColor; // Stroke info if (cur.lineWidth != 0) { context.strokeStyle = cur.lineColor; context.lineWidth = cur.lineWidth; } // Path context.beginPath(); if (cur.angleEnd != 2 * Math.PI) context.lineTo(x, y); context.arc(x, y, (cur.diameter / 2) * cur.scale, cur.angleStart, cur.angleEnd, cur.anticlockwise); if (cur.angleEnd != 2 * Math.PI) context.lineTo(x, y); // End if (cur.fillColor != 0) context.fill(); if (cur.lineWidth != 0) context.stroke(); context.shadowBlur = 0; context.globalAlpha = 1; if (cur.rotate && cur.rotate != 0) context.restore(); }, }, /** * Renders all queued media on screen (called during game loop)

*/ all: function () { game.render.queue.forEach(cur => { switch (cur.typeOfMedia) { case 'image': this._image(cur); break; case 'sprite': this._sprite(cur); break; case 'text': this._text(cur); break; case 'rect': this._geom._rect(cur); break; case 'arc': this._geom._arc(cur); break; } }); }, /** * Clears all queued media (used when changing states)

*/ clear: function () { game.render.queue = []; } }, /** * Math functions * * @namespace */ math: { /** * Returns a random integer in a range * * @param {number} min smaller number * @param {number} max larger number * @returns {number} random integer in range */ randomInRange: function (min, max) { min = Math.ceil(min); max = Math.floor(max); return Math.floor(Math.random() * (max - min + 1) + min); // The maximum is inclusive and the minimum is also inclusive }, /** * Returns a random divisor for a given number * * @param {number} number number * @returns {number} random divisor for number */ randomDivisor: function (number) { const validDivisors = []; // Divisors found for (let i = 2; i < number; i++) { // If 'number' can be divided by 'i', add to list of 'validDivisors' if (number % i == 0) validDivisors.push(i); } const randIndex = game.math.randomInRange(0, validDivisors.length - 1); return validDivisors[randIndex]; }, /** * Converts degree to radian * * @param {number} degree number in degrees * @returns {number} its radian equivalent */ degreeToRad: function (degree) { return degree * Math.PI / 180; }, /** * Returns distance from center of an icon to pointer (radius) * * @param {number} xMouse contains the mouse x coordinate * @param {number} xIcon contains the icon x coordinate * @param {number} yMouse contains the mouse y coordinate * @param {number} yIcon contains the icon y coordinate * @returns {number} distance between the two icons */ distanceToPointer: function (xMouse, xIcon, yMouse, yIcon) { const a = Math.max(xMouse, xIcon) - Math.min(xMouse, xIcon); const b = Math.max(yMouse, yIcon) - Math.min(yMouse, yIcon); return Math.sqrt(a * a + b * b); }, /** * Checks if mouse is over icon (rectangle) * * @param {number} xMouse contains the mouse x coordinate * @param {number} yMouse contains the mouse y coordinate * @param {object} icon icon * @returns {boolean} true if cursor is over icon */ isOverIcon: function (xMouse, yMouse, icon) { const x = xMouse, y = yMouse, cur = icon; return y >= cur.yWithAnchor && y <= (cur.yWithAnchor + cur.height * cur.scale) && (x >= cur.xWithAnchor && x <= (cur.xWithAnchor + cur.width * cur.scale)); } }, /** * Timer used to get the time spent to complete a game * * @namespace */ timer: { _start: 0, // [Not directly used] Start time _end: 0, // [Not directly used] End time elapsed: 0, // [Not directly used] Elapsed time /** * Start timer */ start: function () { game.timer._start = game.timer._end = game.timer._elapsed = 0; // Clear game.timer._start = new Date().getTime(); // Set start time }, /** * Stop timer */ stop: function () { if (game.timer._start != 0 && game.timer._end == 0) { // If timer has started but not finished game.timer._end = new Date().getTime(); // Set end time game.timer._elapsed = Math.floor((game.timer._end - game.timer._start) / 1000); // Set elapsed time } }, get elapsed() { return game.timer._elapsed; } }, /** * Handles (mouse) pointer events * * @namespace */ event: { _list: [], // [Not directly used] List of events in current state /** * Create events * * @param {string} name name of event : 'click' or 'mousemove' * @param {function} func function to be called by triggered event */ add: function (name, func) { canvas.addEventListener(name, func); game.event._list.push([name, func]); }, /** [Not directly used]
* Clear list of events */ clear: function () { game.event._list.forEach(cur => { canvas.removeEventListener(cur[0], cur[1]); }); game.event._list = []; }, }, /** [Not directly used]
* Game loop - Handles repetition of function update() and sprite animation
* After the media queue is filled in create(), the game loop starts
* It calls update() iteratively and rerenders the screen * * @namespace */ loop: { id: undefined, // [Not directly used] Holds animation event curState: undefined, // [Not directly used] State that called the loop status: 'off', // [Not directly used] Loop status can be : 'on', 'ending' or 'off' waitingToStart: undefined, // [Not directly used] startTime: 0, // [Not directly used] duration: 1000 / 60, // [Not directly used] 1000: 1 second | 60: expected frames per second /** [Not directly used]
* Start game loop * * @param {object} state current state */ start: function (state) { if (game.loop.status == 'off') { game.loop.curState = state; game.loop.startTime = new Date().getTime(); game.loop.status = 'on'; game.loop.id = requestAnimationFrame(game.loop._run); } else { // If 'game.loop.status' is either 'on' or 'ending' game.loop.waitingToStart = state; if (game.loop.status == 'on') game.loop.stop(); } }, /** * [Not directly used]
* Stop game loop */ stop: function () { if (game.loop.status == 'on') game.loop.status = 'ending'; }, /** * [Not directly used]
* Execute game loop */ _run: function () { if (game.loop.status != 'on') { game.loop._clear(); } else { const timestamp = new Date().getTime(); const runtime = timestamp - game.loop.startTime; if (runtime >= game.loop.duration) { if (debugMode) { let fps = runtime / 1000; fps = Math.round(1 / fps); displayFps.innerHTML = 'Fps: ' + fps; // Show fps } // Update state game.loop.curState.update(); // Animate state game.animation._run(); } game.loop.id = requestAnimationFrame(game.loop._run); } }, /** * [Not directly used]
* Resets game loop values */ _clear: function () { if (game.loop.id != undefined) { cancelAnimationFrame(game.loop.id); // Cancel animation event game.loop.id = undefined; // Clear object that holds animation event game.loop.curState = undefined; // Clear object that holds current state game.loop.status = 'off'; // Inform animation must end (read in _run()) displayFps.innerHTML = ''; // Stop showing fps } if (game.loop.waitingToStart != undefined) { const temp = game.loop.waitingToStart; game.loop.waitingToStart = undefined; game.loop.start(temp); } }, }, /** * Handles spritesheet animation
* Called by game loop
* Changes through frames in queued spritesheets * * @namespace */ animation: { queue: [], // [Not directly used] Animation queue for current level count: 0, // [Not directly used] /** * Play animation * * @param {string} name animation name */ play: function (name) { let newAnimation; // Gets first object that has that animation name (name) in game.render.queue for (let i in game.render.queue) { if (game.render.queue[i].animation != undefined && game.render.queue[i].animation[0] == name) { newAnimation = game.render.queue[i]; break; } } // If found, saves object in game.animation.queue if (newAnimation != undefined) game.animation.queue.push(newAnimation); }, /** * Stop animation * * @param {string} name animation name */ stop: function (name) { // Remove all with that name is game.animation.queue game.animation.queue.forEach(cur => { if (cur.animation[0] == name) { game.animation.queue.splice(cur, 1); } }); }, /** * [Not directly used]
* Executes animation */ _run: function () { game.animation.queue.forEach(character => { if (!character.animation[2] || game.animation.count % character.animation[2] == 0) { const i = character.animation[1].indexOf(character.curFrame); if (i == -1) { // Frame not found if (debugMode) console.error('Game error: animation frame not found'); } else if (i < character.animation[1].length - 1) { // Go to next frame character.curFrame = character.animation[1][i + 1]; } else { character.curFrame = character.animation[1][0]; // If last frame, restart } } }); game.animation.count++; }, /** * [Not directly used]
* Clear animation queue

* * game.animation.clear() */ clear: function () { // Resets animation count game.animation.count = 0; // Clears property 'animation' from objects in game.render.queue game.render.queue.forEach(cur => { if (cur.animation != undefined) { delete cur.animation; } }); // Clears game.animation.queue game.animation.queue = []; }, }, /** * Handles game states
* When an state is associated with an object, its preload(), create() and update() functions
* will be executed acconrding to these rules:
* * preload() : first function to be called when state is called. Loads media. Runs only once.(optional)
* * create() : called right after preload(). Where the main code goes. Runs only once.
* * update() : called right after create(). Is iteratively called by game loop until end of the state. (optional) * * @namespace */ state: { list: [], // [Not directly used] name: undefined, // [Not directly used] /** * Create new state * * @param {string} name state name * @param {object} obj object that should be called when accessing the state */ add: function (name, obj) { game.state.list[name] = obj; }, /** * Start new state * * @param {string} name state name */ start: function (name) { document.body.style.cursor = 'auto'; game.loop.stop(); game.event.clear(); game.animation.clear(); game.state.name = name; self = game.state.list[name]; if (self.preload) self.preload(); else game.state._create(); }, /** * [Not directly used]
* Encapsulate create() function in the current state */ _create: function () { game.render.clear(); self.create(); game.render.all(); if (self.restart && self.restart == true) { game.state.start(game.state.name); } else { if (self.update) game.loop.start(self); } }, }, };