/**
* 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);
}
},
},
};