/***************************************************************
* LInE - Free Education, Private Data - http://www.usp.br/line
*
* This file handles all the game mechanics.
**************************************************************/
/**
* Variable that handles game mechanics.
*
* @namespace
*/
const game = {
image: {}, // [Not directly used] Holds cached reference to media.
sprite: {}, // [Not directly used] Holds cached reference to media.
audio: {}, // Holds cached reference to media - game.audio.<name>.play() plays that audio once.
lang: {}, // Holds language dictionary in a key-value format - game.lang.<key> returns <value>.
loadedCur: 0, // [Not directly used] CURRENT number of cached media (on current state)
loadedMax: 0, // [Not directly used] EXPECTED number of cached media (on current state)
loadManager: { // [Not directly used] <mediaCategory> : [ <isLoading?> , <#CurrentlyCached> ]
lang: [false, 0],
audio: [false, 0],
image: [false, 0],
sprite: [false, 0],
},
/**
* Handles game states. <br>
*
* When a state is associated with an object
* it can accessed using game.state.start('state name'). <br>
* That way the object can use the especial functions: preload(), create() and update();
* that behave according to the following rules: <br>
* - preload() : first function to run when a state is called. Loads media. Runs only once. (is optional) <br>
* - create() : called right after preload(). Where the main code goes. Runs only once. (must exist) <br>
* - update() : called right after create(). Is iteratively called by 'game loop' until going to next state. (is optional)
*
* @namespace
*/
state: {
// [Not directly used] List of game states.
list: [],
// [Not directly used]
name: undefined,
/**
* Create new state. <br>
*
* After a state is created, the object associated with that state
* can be called using game.state.start('state name')
*
* @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.
*
* Will look for the state's preload() to load the files for the current state.
* If there is no preload, will call create().
*
* @param {string} name state name
*/
start: function (name) {
document.body.style.cursor = 'auto'; // Set cursor to default
game.loop.stop(); // Stop last game loop
game.event.clear(); // Clears last event queue
game.animation.clear(); // Clears last animation queue
game.loadedCur = 0; // Clears last state's number of loaded media
game.loadedMax = 0; // Clears last state's expected loaded media
game.state.name = name; // Updates state name
self = game.state.list[name]; // Updates self to current state
if (self.preload) {
game.render.clear(); // Clears render queue
// IF there's media to be loaded, creates progress bar
game.add.geom.rect(0, 0, defaultWidth, defaultHeight, colors.white, 0, colors.blueBckg, 1);
self.progressBar = game.add.geom.rect(defaultWidth / 2, defaultHeight / 2, 20, 20, undefined, 0, colors.white);
self.progressBar.anchor(0.5, 0.5);
// Calls state's preload() to load the state's media
self.preload();
} else {
game.state.create();
}
},
/**
* [Not directly used] Encapsulate create() function in the current state.
*/
create: function () {
game.render.clear(); // Clears render queue, removing 'progress bar' if preload() was called
if (!self.create) {
console.error('Game error: The state called does not have a \'create\' function. Unable to continue.');
} else {
self.create(); // Calls create()
game.render.all(); // After create() ends, renders media on canvas
if (self.restart && self.restart == true) { // If needed, restart state
game.state.start(game.state.name);
} else {
if (self.update) game.loop.start(self); // Calls update() if it exists
}
}
},
},
/**
* Loads media files to cache. <br>
*
* IMPORTANT: Must ONLY be used inside the function preload(),
* as it calls create() after all media is cached.
*
* @see /js/globals.js for the list of media urls (var url)
*
* @namespace
*/
load: {
/**
* Loads language dictionary into the object game.lang using Fetch API.
*
* @param {string} url url for the selected language
*/
lang: function (url) {
game.loadManager.lang[0] = true;
game.loadManager.lang[1] = 0;
game.lang = {}; // Clear previously loaded language
const init = { mode: 'same-origin' };
fetch(url, init)
.then(response => {
return response.text();
})
.then(text => {
let msg = text.split('\n');
game.loadedMax += msg.length - 1;
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.finishedOneMediaElement(msg.length - 1, 'lang');
});
});
},
/**
* Caches audio files using Fetch API.
*
* @param {string[]} urls audio urls for the current state
*/
audio: function (urls) {
game.loadManager.audio[0] = true;
game.loadManager.audio[1] = 0;
urls = this.getUncachedUrls(urls, game.audio);
if (urls.length == 0) {
game.load.finishedOneMediaElement(0, 'audio');
} else {
game.loadedMax += urls.length - 1;
const init = { mode: 'same-origin' };
urls.forEach(cur => {
fetch(cur[1][1], init)
.then(response => response.blob())
.then(myBlob => {
game.audio[cur[0]] = new Audio(URL.createObjectURL(myBlob));
game.load.finishedOneMediaElement(urls.length - 1, 'audio');
});
});
}
},
/**
* Caches image files using HTMLImageElement.
*
* @param {string[]} urls image urls for the current state
*/
image: function (urls) {
game.loadManager.image[0] = true;
game.loadManager.image[1] = 0;
urls = this.getUncachedUrls(urls, game.image);
if (urls.length == 0) {
game.load.finishedOneMediaElement(0, 'image');
} else {
game.loadedMax += urls.length - 1;
urls.forEach(cur => {
const img = new Image();
img.onload = () => {
game.image[cur[0]] = img;
game.load.finishedOneMediaElement(urls.length - 1, 'image');
}
img.src = cur[1];
});
}
},
/**
* Caches spritesheets using HTMLImageElement.
*
* @param {string[]} urls spritesheet urls for the current state
*/
sprite: function (urls) {
game.loadManager.sprite[0] = true;
game.loadManager.sprite[1] = 0;
urls = this.getUncachedUrls(urls, game.sprite);
if (urls.length == 0) {
game.load.finishedOneMediaElement(0, 'sprite');
} else {
game.loadedMax += urls.length - 1;
urls.forEach(cur => {
const img = new Image();
img.onload = () => {
game.sprite[cur[0]] = img;
game.load.finishedOneMediaElement(urls.length - 1, 'sprite');
}
img.src = cur[1];
img.frames = cur[2];
});
}
},
/** [Not directly used] Removes the urls that are already in the cache.
*
* @param {string[]} urls array of urls
* @param {object} media media category
*
* @returns {string[]} array of uncached urls
*/
getUncachedUrls: function (urls, media) {
const newUrls = [];
urls.forEach(cur => {
if (media[cur[0]] == undefined) newUrls.push(cur);
});
return newUrls;
},
/** [Not directly used] Informs ONE media file was loaded to cache. <br>
*
* After ALL FILES of the SAME CATEGORY are cached, calls game.load.finishedOneMediaType()
*
* @param {number} lastIndex last index of the media array (to check if is finished)
* @param {String} mediaType media category (to update the cached files from that category)
*/
finishedOneMediaElement: function (lastIndex, mediaType) {
// Updates progress bar
if (lastIndex != 0) {
self.progressBar.width = (200 / game.loadedMax) * game.loadedCur;
game.render.all();
game.loadedCur++;
}
// If reached last index of current media array
if (lastIndex == game.loadManager[mediaType][1]) {
// Resets load manager
game.loadManager[mediaType][0] = false;
game.loadManager[mediaType][1] = 0;
// Informs
game.load.finishedOneMediaType();
} else {
// Updates
game.loadManager[mediaType][1]++;
}
},
/** [Not directly used] Informs ALL MEDIA files from the SAME CATEGORY are cached. <br>
*
* After ALL CATEGORIES of media are cached, calls create() via game.state. <br>
* ATTENTION: Do not call create() directly.
*/
finishedOneMediaType: function () {
// Checks if finished loading ALL media categories
let endPreload = true;
for (let key in game.loadManager) {
if (game.loadManager[key][0] == true) {
endPreload = false;
break;
}
}
// If flag doesnt change, all media is cached
if (endPreload) {
game.state.create();
}
}
},
/**
* Adds new media to the 'media queue' (game.render.queue). <br>
*
* All queued media will be rendered on canvas when game.render.all() is called.
*
* @namespace
*/
add: {
/**
* Adds image to media queue.
*
* @param {number} x x coordinate for the figure
* @param {number} y y coordinate for the figure
* @param {string} img name of the cached image
* @param {undefined|number} scale scale for the image (default = 1)
* @param {undefined|number} alpha level of transparency, from 0 (invisible) to 1 (100% visible) (default = 1)
*
* @returns {object} a reference to the created image.
*/
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. <br>
* A spritesheet is an image that can be cropped to show only one 'frame' at a time.
*
* @param {number} x x coordinate for the figure
* @param {number} y Y coordinate for the figure
* @param {string} img name of the cached spritesheet
* @param {undefined|number} curFrame current frame (default = 0)
* @param {undefined|number} scale scale for the spritesheet (default = 1)
* @param {undefined|number} alpha level of transparency, from 0 (invisible) to 1 (100% visible) (default = 1)
*
* @returns {object} a reference to the created sprite.
*/
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.
*
* @param {number} x x coordinate for the figure
* @param {number} y y coordinate for the figure
* @param {string} text text to be displayed on screen
* @param {object} style object containing font, color and align for the text
*
* @returns {object} a reference to the created text.
*/
text: function (x, y, text, style) {
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: 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.
*
* @param {number} x x coordinate for top left corner of the rectangle
* @param {number} y y coordinate for top left corner of the rectangle
* @param {number} width rectangle width (default = 50)
* @param {undefined|number} height rectangle height (default = 50)
* @param {undefined|string} lineColor stroke color (default = black)
* @param {undefined|number} lineWidth stroke width (default = 1px)
* @param {undefined|string} fillColor fill color (default = no fill)
* @param {undefined|number} alpha level of transparency, from 0 (invisible) to 1 (100% visible)) (default = 1)
*
* @returns {object} a reference to the created rectangle.
*/
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.
*
* @param {number} x x coordinate for the circle center
* @param {number} y y coordinate for the circle center
* @param {number} diameter circle diameter (default = 50)
* @param {undefined|string} lineColor stroke color (default = black)
* @param {undefined|number} lineWidth stroke width (default = 1px)
* @param {undefined|string} fillColor fill color (default = no fill)
* @param {undefined|number} alpha level of transparency, from 0 (invisible) to 1 (100% visible)) (default = 1)
*
* @returns {object} a reference to the created circle.
*/
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.
*
* @param {number} x x coordinate for the arc center
* @param {number} y 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 {undefined|boolean} anticlockwise if true, arc is created anticlockwise (default = false)
* @param {undefined|string} lineColor stroke color (default = black)
* @param {undefined|number} lineWidth stroke width (default = 1px)
* @param {undefined|string} fillColor fill color (default = no fill)
* @param {undefined|number} alpha level of transparency, from 0 (invisible) to 1 (100% visible)) (default = 1)
*
* @returns {object} a reference to the created arc.
*/
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 values for the media properties.
*/
default: {
// Used in: all types of media.
x: 0,
y: 0,
_xWithAnchor: 0,
_yWithAnchor: 0,
xAnchor: 0,
yAnchor: 0,
shadow: false,
shadowColor: '#0075c5',
shadowBlur: 20,
alpha: 1,
// Used in: image, sprite, square, circle.
scale: 1,
// Used in: text.
font: '14px Arial,sans-serif',
fill: '#000',
align: 'center',
// Used in: 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, // No fill
// Used in: circle.
diameter: 50,
anticlockwise: false,
},
},
/**
* Renders media on current screen. <br<
* It uses properties of html canvas to draw media on screen during game loop.
*
* @namespace
*/
render: {
// [Not directly used] Media queue to be rendered on the current state.
queue: [],
/** [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;
// 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;
// Image
context.drawImage(
game.image[cur.name],
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();
},
/** [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;
// 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;
// Image
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
);
// End
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;
// 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;
// Font style
context.font = cur.font;
context.textAlign = cur.align;
context.fillStyle = cur.fill;
// Text
context.fillText(cur.name, x, y);
// End
context.shadowBlur = 0;
context.globalAlpha = 1;
if (cur.rotate && cur.rotate != 0) context.restore();
},
/** [Not directly used] Renders geometric shapes on canvas.
*
* @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 (arc or circle).
*
* @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 repeatedly by the 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 media queue (used when changing states).
*/
clear: function () {
game.render.queue = [];
}
},
/**
* Math functions.
*
* @namespace
*/
math: {
/**
* Returns a random integer in a range (inclusive for min and max).
*
* @param {number} min smaller integer
* @param {number} max larger integer
*
* @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);
},
/**
* Returns a random divisor for a given number.
*
* @param {number} number number
*
* @returns {number} random divisor for that number
*/
randomDivisor: function (number) {
const validDivisors = [];
// If 'number' can be divided by 'i', add to list of 'validDivisors'
for (let i = 2; i < number; i++) {
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 the center of an icon to mouse/pointer (radius).
*
* @param {number} xMouse mouse x coordinate
* @param {number} xIcon icon x coordinate
* @param {number} yMouse mouse y coordinate
* @param {number} yIcon 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 pointer/mouse is over (rectangular) icon.
*
* @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));
},
/**
* Converts a given time in seconds (number) to the format HH:MM:SS (string)
*
* @param {number} s time in seconds
*
* @returns {string} time in the format HH:MM:SS
*/
convertTime: function (s) {
let h = 0, m = 0;
if (s > 1200) {
h = s / 1200;
s = s % 1200;
}
if (s > 60) {
m = s / 60;
s = s % 60;
}
h = '' + h;
m = '' + m;
s = '' + s;
if (h.length < 2) h = '0' + h;
if (m.length < 2) m = '0' + m;
if (s.length < 2) s = '0' + s;
return h + ':' + m + ':' + s;
}
},
/**
* Timer used to get the time spent to complete a game.
*
* @namespace
*/
timer: {
// [Not directly used] Start time.
_start: 0,
// [Not directly used] End time.
end: 0,
// Elapsed time.
elapsed: 0,
/**
* Reset values and start timer.
*/
start: function () {
game.timer._start = game.timer.end = game.timer.elapsed = 0;
game.timer._start = new Date().getTime();
},
/**
* Stop timer and set elapsed time.
*/
stop: function () {
if (game.timer._start != 0 && game.timer.end == 0) { // If timer has started but not finished
game.timer.end = new Date().getTime();
game.timer.elapsed = Math.floor((game.timer.end - game.timer._start) / 1000);
}
},
},
/**
* Handles pointer events. <br>
*
* @namespace
*/
event: {
// [Not directly used] List of events in current state.
list: [],
/**
* Adds new event to current state.
*
* @param {string} name event name, can be: 'click' or 'mousemove'
* @param {function} func function to be called when event is triggered
*/
add: function (name, func) {
canvas.addEventListener(name, func);
game.event.list.push([name, func]);
},
/** [Not directly used] Clears list of events. Called before moving to new state.
*/
clear: function () {
game.event.list.forEach(cur => {
canvas.removeEventListener(cur[0], cur[1]);
});
game.event.list = [];
},
},
/** [Not directly used] Handles 'game loop'. <br>
*
* After the media queue is filled in create(), the 'game loop' starts.
* It calls update() iteratively, re-rendering the screen every time. <br>
*
* The 'game loop' is stoped by leaving the current state.
*
* @namespace
*/
loop: {
// [Not directly used] Holds animation event.
id: undefined,
// [Not directly used] State that called the loop.
curState: undefined,
// [Not directly used] Loop status, can be: 'on', 'ending' or 'off'.
status: 'off',
// [Not directly used]
waitingToStart: undefined,
// [Not directly used]
startTime: 0,
// [Not directly used] 1000: 1 second | 60: expected frames per second.
duration: 1000 / 60,
/** [Not directly used] Starts 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] Stops game loop.
*/
stop: function () {
if (game.loop.status == 'on') game.loop.status = 'ending';
},
/**
* [Not directly used] Executes game loop.
*
* This code will run on each iteration of the 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) {
// Calls state's update()
game.loop.curState.update();
// Updates state's animation
game.animation.run();
}
game.loop.id = requestAnimationFrame(game.loop.run); // Loop
}
},
/**
* [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; // Clears object that holds animation event
game.loop.curState = undefined; // Clears object that holds current state
game.loop.status = 'off'; // Inform animation must end (read in game.loop.run())
}
if (game.loop.waitingToStart != undefined) {
const temp = game.loop.waitingToStart;
game.loop.waitingToStart = undefined;
game.loop.start(temp);
}
},
},
/**
* Handles spritesheet animation. <br>
* It iterates through the spritesheet frames inside the animation queue.
* Called by game loop.
*
* @namespace
*/
animation: {
// [Not directly used] Animation queue for the current state.
queue: [],
// [Not directly used]
count: 0,
/**
* Plays animation.
*
* @param {string} name animation name (identifier)
*/
play: function (name) {
let newAnimation;
// Gets first object in the 'render queue' with that animation name
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 the 'animation queue'
if (newAnimation != undefined) game.animation.queue.push(newAnimation);
},
/**
* Stops animation.
*
* @param {string} name animation name
*/
stop: function (name) {
// Removes all with that name from the '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] Clears animation queue.
*/
clear: function () {
// Resets animation counter
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 animation queue
game.animation.queue = [];
},
}
};