gameMechanics.js 40 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175
  1. /***************************************************************
  2. * LInE - Free Education, Private Data - http://www.usp.br/line
  3. *
  4. * This file handles all the game mechanics.
  5. **************************************************************/
  6. /**
  7. * Variable that handles game mechanics.
  8. *
  9. * @namespace
  10. */
  11. const game = {
  12. image: {}, // [Not directly used] Holds cached reference to media.
  13. sprite: {}, // [Not directly used] Holds cached reference to media.
  14. audio: {}, // Holds cached reference to media - game.audio.<name>.play() plays that audio once.
  15. lang: {}, // Holds language dictionary in a key-value format - game.lang.<key> returns <value>.
  16. loadedCur: 0, // [Not directly used] CURRENT number of cached media (on current state)
  17. loadedMax: 0, // [Not directly used] EXPECTED number of cached media (on current state)
  18. loadManager: { // [Not directly used] <mediaCategory> : [ <isLoading?> , <#CurrentlyCached> ]
  19. lang: [false, 0],
  20. audio: [false, 0],
  21. image: [false, 0],
  22. sprite: [false, 0],
  23. },
  24. /**
  25. * Handles game states. <br>
  26. *
  27. * When a state is associated with an object
  28. * it can accessed using game.state.start('state name'). <br>
  29. * That way the object can use the especial functions: preload(), create() and update();
  30. * that behave according to the following rules: <br>
  31. * - preload() : first function to run when a state is called. Loads media. Runs only once. (is optional) <br>
  32. * - create() : called right after preload(). Where the main code goes. Runs only once. (must exist) <br>
  33. * - update() : called right after create(). Is iteratively called by 'game loop' until going to next state. (is optional)
  34. *
  35. * @namespace
  36. */
  37. state: {
  38. // [Not directly used] List of game states.
  39. list: [],
  40. // [Not directly used]
  41. name: undefined,
  42. /**
  43. * Create new state. <br>
  44. *
  45. * After a state is created, the object associated with that state
  46. * can be called using game.state.start('state name')
  47. *
  48. * @param {string} name state name
  49. * @param {object} obj object that should be called when accessing the state
  50. */
  51. add: function (name, obj) {
  52. game.state.list[name] = obj;
  53. },
  54. /**
  55. * Start new state.
  56. *
  57. * Will look for the state's preload() to load the files for the current state.
  58. * If there is no preload, will call create().
  59. *
  60. * @param {string} name state name
  61. */
  62. start: function (name) {
  63. document.body.style.cursor = 'auto'; // Set cursor to default
  64. game.loop.stop(); // Stop last game loop
  65. game.event.clear(); // Clears last event queue
  66. game.animation.clear(); // Clears last animation queue
  67. game.loadedCur = 0; // Clears last state's number of loaded media
  68. game.loadedMax = 0; // Clears last state's expected loaded media
  69. game.state.name = name; // Updates state name
  70. self = game.state.list[name]; // Updates self to current state
  71. if (self.preload) {
  72. game.render.clear(); // Clears render queue
  73. // IF there's media to be loaded, creates progress bar
  74. game.add.geom.rect(0, 0, defaultWidth, defaultHeight, colors.white, 0, colors.blueBckg, 1);
  75. self.progressBar = game.add.geom.rect(defaultWidth / 2, defaultHeight / 2, 20, 20, undefined, 0, colors.white);
  76. self.progressBar.anchor(0.5, 0.5);
  77. // Calls state's preload() to load the state's media
  78. self.preload();
  79. } else {
  80. game.state.create();
  81. }
  82. },
  83. /**
  84. * [Not directly used] Encapsulate create() function in the current state.
  85. */
  86. create: function () {
  87. game.render.clear(); // Clears render queue, removing 'progress bar' if preload() was called
  88. if (!self.create) {
  89. console.error('Game error: The state called does not have a \'create\' function. Unable to continue.');
  90. } else {
  91. self.create(); // Calls create()
  92. game.render.all(); // After create() ends, renders media on canvas
  93. if (self.restart && self.restart == true) { // If needed, restart state
  94. game.state.start(game.state.name);
  95. } else {
  96. if (self.update) game.loop.start(self); // Calls update() if it exists
  97. }
  98. }
  99. },
  100. },
  101. /**
  102. * Loads media files to cache. <br>
  103. *
  104. * IMPORTANT: Must ONLY be used inside the function preload(),
  105. * as it calls create() after all media is cached.
  106. *
  107. * @see /js/globals.js for the list of media urls (var url)
  108. *
  109. * @namespace
  110. */
  111. load: {
  112. /**
  113. * Loads language dictionary into the object game.lang using Fetch API.
  114. *
  115. * @param {string} url url for the selected language
  116. */
  117. lang: function (url) {
  118. game.loadManager.lang[0] = true;
  119. game.loadManager.lang[1] = 0;
  120. game.lang = {}; // Clear previously loaded language
  121. const init = { mode: 'same-origin' };
  122. fetch(url, init)
  123. .then(response => {
  124. return response.text();
  125. })
  126. .then(text => {
  127. let msg = text.split('\n');
  128. game.loadedMax += msg.length - 1;
  129. msg.forEach(cur => {
  130. try {
  131. let msg = cur.split('=');
  132. game.lang[msg[0].trim()] = msg[1].trim();
  133. } catch (Error) { if (debugMode) console.log('Sintax error fixed'); }
  134. game.load.finishedOneMediaElement(msg.length - 1, 'lang');
  135. });
  136. });
  137. },
  138. /**
  139. * Caches audio files using Fetch API.
  140. *
  141. * @param {string[]} urls audio urls for the current state
  142. */
  143. audio: function (urls) {
  144. game.loadManager.audio[0] = true;
  145. game.loadManager.audio[1] = 0;
  146. urls = this.getUncachedUrls(urls, game.audio);
  147. if (urls.length == 0) {
  148. game.load.finishedOneMediaElement(0, 'audio');
  149. } else {
  150. game.loadedMax += urls.length - 1;
  151. const init = { mode: 'same-origin' };
  152. urls.forEach(cur => {
  153. fetch(cur[1][1], init)
  154. .then(response => response.blob())
  155. .then(myBlob => {
  156. game.audio[cur[0]] = new Audio(URL.createObjectURL(myBlob));
  157. game.load.finishedOneMediaElement(urls.length - 1, 'audio');
  158. });
  159. });
  160. }
  161. },
  162. /**
  163. * Caches image files using HTMLImageElement.
  164. *
  165. * @param {string[]} urls image urls for the current state
  166. */
  167. image: function (urls) {
  168. game.loadManager.image[0] = true;
  169. game.loadManager.image[1] = 0;
  170. urls = this.getUncachedUrls(urls, game.image);
  171. if (urls.length == 0) {
  172. game.load.finishedOneMediaElement(0, 'image');
  173. } else {
  174. game.loadedMax += urls.length - 1;
  175. urls.forEach(cur => {
  176. const img = new Image();
  177. img.onload = () => {
  178. game.image[cur[0]] = img;
  179. game.load.finishedOneMediaElement(urls.length - 1, 'image');
  180. }
  181. img.src = cur[1];
  182. });
  183. }
  184. },
  185. /**
  186. * Caches spritesheets using HTMLImageElement.
  187. *
  188. * @param {string[]} urls spritesheet urls for the current state
  189. */
  190. sprite: function (urls) {
  191. game.loadManager.sprite[0] = true;
  192. game.loadManager.sprite[1] = 0;
  193. urls = this.getUncachedUrls(urls, game.sprite);
  194. if (urls.length == 0) {
  195. game.load.finishedOneMediaElement(0, 'sprite');
  196. } else {
  197. game.loadedMax += urls.length - 1;
  198. urls.forEach(cur => {
  199. const img = new Image();
  200. img.onload = () => {
  201. game.sprite[cur[0]] = img;
  202. game.load.finishedOneMediaElement(urls.length - 1, 'sprite');
  203. }
  204. img.src = cur[1];
  205. img.frames = cur[2];
  206. });
  207. }
  208. },
  209. /** [Not directly used] Removes the urls that are already in the cache.
  210. *
  211. * @param {string[]} urls array of urls
  212. * @param {object} media media category
  213. *
  214. * @returns {string[]} array of uncached urls
  215. */
  216. getUncachedUrls: function (urls, media) {
  217. const newUrls = [];
  218. urls.forEach(cur => {
  219. if (media[cur[0]] == undefined) newUrls.push(cur);
  220. });
  221. return newUrls;
  222. },
  223. /** [Not directly used] Informs ONE media file was loaded to cache. <br>
  224. *
  225. * After ALL FILES of the SAME CATEGORY are cached, calls game.load.finishedOneMediaType()
  226. *
  227. * @param {number} lastIndex last index of the media array (to check if is finished)
  228. * @param {String} mediaType media category (to update the cached files from that category)
  229. */
  230. finishedOneMediaElement: function (lastIndex, mediaType) {
  231. // Updates progress bar
  232. if (lastIndex != 0) {
  233. self.progressBar.width = (200 / game.loadedMax) * game.loadedCur;
  234. game.render.all();
  235. game.loadedCur++;
  236. }
  237. // If reached last index of current media array
  238. if (lastIndex == game.loadManager[mediaType][1]) {
  239. // Resets load manager
  240. game.loadManager[mediaType][0] = false;
  241. game.loadManager[mediaType][1] = 0;
  242. // Informs
  243. game.load.finishedOneMediaType();
  244. } else {
  245. // Updates
  246. game.loadManager[mediaType][1]++;
  247. }
  248. },
  249. /** [Not directly used] Informs ALL MEDIA files from the SAME CATEGORY are cached. <br>
  250. *
  251. * After ALL CATEGORIES of media are cached, calls create() via game.state. <br>
  252. * ATTENTION: Do not call create() directly.
  253. */
  254. finishedOneMediaType: function () {
  255. // Checks if finished loading ALL media categories
  256. let endPreload = true;
  257. for (let key in game.loadManager) {
  258. if (game.loadManager[key][0] == true) {
  259. endPreload = false;
  260. break;
  261. }
  262. }
  263. // If flag doesnt change, all media is cached
  264. if (endPreload) {
  265. game.state.create();
  266. }
  267. }
  268. },
  269. /**
  270. * Adds new media to the 'media queue' (game.render.queue). <br>
  271. *
  272. * All queued media will be rendered on canvas when game.render.all() is called.
  273. *
  274. * @namespace
  275. */
  276. add: {
  277. /**
  278. * Adds image to media queue.
  279. *
  280. * @param {number} x x coordinate for the figure
  281. * @param {number} y y coordinate for the figure
  282. * @param {string} img name of the cached image
  283. * @param {undefined|number} scale scale for the image (default = 1)
  284. * @param {undefined|number} alpha level of transparency, from 0 (invisible) to 1 (100% visible) (default = 1)
  285. *
  286. * @returns {object} a reference to the created image.
  287. */
  288. image: function (x, y, img, scale, alpha) {
  289. if (x == undefined || y == undefined || img == undefined) console.error('Game error: missing parameters.');
  290. else if (game.image[img] == undefined) console.error('Game error: image not found in cache: ' + img + '.');
  291. else {
  292. const med = {
  293. typeOfMedia: 'image',
  294. name: img,
  295. x: x || game.add.default.x,
  296. y: y || game.add.default.y,
  297. _xWithAnchor: x || game.add.default._xWithAnchor,
  298. _yWithAnchor: y || game.add.default._yWithAnchor,
  299. xAnchor: game.add.default.xAnchor,
  300. yAnchor: game.add.default.yAnchor,
  301. shadow: game.add.default.shadow,
  302. shadowColor: game.add.default.shadowColor,
  303. shadowBlur: game.add.default.shadowBlur,
  304. alpha: (alpha != undefined) ? alpha : game.add.default.alpha,
  305. scale: scale || game.add.default.scale,
  306. width: game.image[img].width,
  307. height: game.image[img].height,
  308. anchor: function (xAnchor, yAnchor) {
  309. this.xAnchor = xAnchor;
  310. this.yAnchor = yAnchor;
  311. },
  312. get xWithAnchor() { return this.x - (this.width * this.scale * this.xAnchor); },
  313. get yWithAnchor() { return this.y - (this.height * this.scale * this.yAnchor); }
  314. };
  315. med.originalScale = med.scale;
  316. game.render.queue.push(med);
  317. return med;
  318. }
  319. },
  320. /**
  321. * Adds spritesheet to media queue. <br>
  322. * A spritesheet is an image that can be cropped to show only one 'frame' at a time.
  323. *
  324. * @param {number} x x coordinate for the figure
  325. * @param {number} y Y coordinate for the figure
  326. * @param {string} img name of the cached spritesheet
  327. * @param {undefined|number} curFrame current frame (default = 0)
  328. * @param {undefined|number} scale scale for the spritesheet (default = 1)
  329. * @param {undefined|number} alpha level of transparency, from 0 (invisible) to 1 (100% visible) (default = 1)
  330. *
  331. * @returns {object} a reference to the created sprite.
  332. */
  333. sprite: function (x, y, img, curFrame, scale, alpha) {
  334. if (x == undefined || y == undefined || img == undefined) console.error('Game error: missing parameters.');
  335. else if (game.sprite[img] == undefined) console.error('Game error: sprite not found in cache: ' + img + '.');
  336. else {
  337. const med = {
  338. typeOfMedia: 'sprite',
  339. name: img,
  340. x: x || game.add.default.x,
  341. y: y || game.add.default.y,
  342. _xWithAnchor: x || game.add.default._xWithAnchor,
  343. _yWithAnchor: y || game.add.default._yWithAnchor,
  344. xAnchor: game.add.default.xAnchor,
  345. yAnchor: game.add.default.yAnchor,
  346. shadow: game.add.default.shadow,
  347. shadowColor: game.add.default.shadowColor,
  348. shadowBlur: game.add.default.shadowBlur,
  349. alpha: (alpha != undefined) ? alpha : game.add.default.alpha,
  350. scale: scale || game.add.default.scale,
  351. width: game.sprite[img].width / game.sprite[img].frames, // Frame width
  352. height: game.sprite[img].height, // Frame height
  353. curFrame: curFrame || 0,
  354. anchor: function (xAnchor, yAnchor) {
  355. this.xAnchor = xAnchor;
  356. this.yAnchor = yAnchor;
  357. },
  358. get xWithAnchor() { return this.x - (this.width * this.scale * this.xAnchor); },
  359. get yWithAnchor() { return this.y - (this.height * this.scale * this.yAnchor); }
  360. };
  361. med.originalScale = med.scale;
  362. game.render.queue.push(med);
  363. return med;
  364. }
  365. },
  366. /**
  367. * Adds text to media queue.
  368. *
  369. * @param {number} x x coordinate for the figure
  370. * @param {number} y y coordinate for the figure
  371. * @param {string} text text to be displayed on screen
  372. * @param {object} style object containing font, color and align for the text
  373. *
  374. * @returns {object} a reference to the created text.
  375. */
  376. text: function (x, y, text, style) {
  377. if (x == undefined || y == undefined || text == undefined || style == undefined) console.error('Game error: missing parameters.');
  378. else {
  379. const med = {
  380. typeOfMedia: 'text',
  381. name: text,
  382. x: x || game.add.default.x,
  383. y: y || game.add.default.y,
  384. _xWithAnchor: x || game.add.default._xWithAnchor,
  385. _yWithAnchor: y || game.add.default._yWithAnchor,
  386. xAnchor: game.add.default.xAnchor,
  387. yAnchor: game.add.default.yAnchor,
  388. shadow: game.add.default.shadow,
  389. shadowColor: game.add.default.shadowColor,
  390. shadowBlur: game.add.default.shadowBlur,
  391. alpha: game.add.default.alpha,
  392. font: style.font || game.add.default.font,
  393. fill: style.fill || game.add.default.fill,
  394. align: style.align || game.add.default.align,
  395. anchor: function () { console.error('Game error: there\'s no anchor for text.'); },
  396. set style(style) {
  397. this.font = style.font;
  398. this.fill = style.fill;
  399. this.align = style.align;
  400. },
  401. get xWithAnchor() { return this.x; },
  402. get yWithAnchor() { return this.y; },
  403. };
  404. game.render.queue.push(med);
  405. return med;
  406. }
  407. },
  408. /**
  409. * Adds geometric shapes.
  410. * @namespace
  411. */
  412. geom: {
  413. /**
  414. * Adds rectangle to media queue.
  415. *
  416. * @param {number} x x coordinate for top left corner of the rectangle
  417. * @param {number} y y coordinate for top left corner of the rectangle
  418. * @param {number} width rectangle width (default = 50)
  419. * @param {undefined|number} height rectangle height (default = 50)
  420. * @param {undefined|string} lineColor stroke color (default = black)
  421. * @param {undefined|number} lineWidth stroke width (default = 1px)
  422. * @param {undefined|string} fillColor fill color (default = no fill)
  423. * @param {undefined|number} alpha level of transparency, from 0 (invisible) to 1 (100% visible)) (default = 1)
  424. *
  425. * @returns {object} a reference to the created rectangle.
  426. */
  427. rect: function (x, y, width, height, lineColor, lineWidth, fillColor, alpha) {
  428. if (x == undefined || y == undefined || width == undefined) console.error('Game error: missing parameters.');
  429. else {
  430. const med = {
  431. typeOfMedia: 'rect',
  432. x: x || game.add.default.x,
  433. y: y || game.add.default.y,
  434. _xWithAnchor: x || game.add.default._xWithAnchor,
  435. _yWithAnchor: y || game.add.default._yWithAnchor,
  436. xAnchor: game.add.default.xAnchor,
  437. yAnchor: game.add.default.yAnchor,
  438. shadow: game.add.default.shadow,
  439. shadowColor: game.add.default.shadowColor,
  440. shadowBlur: game.add.default.shadowBlur,
  441. alpha: (alpha != undefined) ? alpha : game.add.default.alpha,
  442. scale: game.add.default.scale,
  443. width: 0,
  444. height: 0,
  445. lineColor: lineColor || game.add.default.lineColor,
  446. lineWidth: 0,
  447. fillColor: fillColor || game.add.default.fillColor,
  448. anchor: function (xAnchor, yAnchor) {
  449. this.xAnchor = xAnchor;
  450. this.yAnchor = yAnchor;
  451. },
  452. get xWithAnchor() { return this.x - (this.width * this.scale * this.xAnchor); },
  453. get yWithAnchor() { return this.y - (this.height * this.scale * this.yAnchor); }
  454. };
  455. med.originalScale = med.scale;
  456. if (width != 0) { med.width = width || game.add.default.width; }
  457. if (height != 0) { med.height = height || width || game.add.default.height; }
  458. if (lineWidth != 0) { med.lineWidth = lineWidth || game.add.default.lineWidth; }
  459. game.render.queue.push(med);
  460. return med;
  461. }
  462. },
  463. /**
  464. * Adds circle to media queue.
  465. *
  466. * @param {number} x x coordinate for the circle center
  467. * @param {number} y y coordinate for the circle center
  468. * @param {number} diameter circle diameter (default = 50)
  469. * @param {undefined|string} lineColor stroke color (default = black)
  470. * @param {undefined|number} lineWidth stroke width (default = 1px)
  471. * @param {undefined|string} fillColor fill color (default = no fill)
  472. * @param {undefined|number} alpha level of transparency, from 0 (invisible) to 1 (100% visible)) (default = 1)
  473. *
  474. * @returns {object} a reference to the created circle.
  475. */
  476. circle: function (x, y, diameter, lineColor, lineWidth, fillColor, alpha) {
  477. if (x == undefined || y == undefined || diameter == undefined) console.error('Game error: missing parameters.');
  478. else {
  479. const med = {
  480. typeOfMedia: 'arc',
  481. x: x || game.add.default.x,
  482. y: y || game.add.default.y,
  483. _xWithAnchor: x || game.add.default._xWithAnchor,
  484. _yWithAnchor: y || game.add.default._yWithAnchor,
  485. xAnchor: game.add.default.xAnchor,
  486. yAnchor: game.add.default.yAnchor,
  487. shadow: game.add.default.shadow,
  488. shadowColor: game.add.default.shadowColor,
  489. shadowBlur: game.add.default.shadowBlur,
  490. alpha: (alpha != undefined) ? alpha : game.add.default.alpha,
  491. scale: game.add.default.scale,
  492. diameter: 0,
  493. width: 0,
  494. height: 0,
  495. angleStart: 0,
  496. angleEnd: 2 * Math.PI,
  497. anticlockwise: game.add.default.anticlockwise,
  498. lineColor: lineColor || game.add.default.lineColor,
  499. lineWidth: 0,
  500. fillColor: fillColor || game.add.default.fillColor,
  501. anchor: function (xAnchor, yAnchor) {
  502. this.xAnchor = xAnchor;
  503. this.yAnchor = yAnchor;
  504. },
  505. get xWithAnchor() { return this.x - (this.width * this.scale * this.xAnchor); },
  506. get yWithAnchor() { return this.y - (this.height * this.scale * this.yAnchor); }
  507. };
  508. med.originalScale = med.scale;
  509. if (diameter != 0) {
  510. med.diameter = diameter || game.add.default.diameter;
  511. med.width = med.height = med.diameter;
  512. }
  513. if (lineWidth != 0) {
  514. med.lineWidth = lineWidth || game.add.default.lineWidth;
  515. }
  516. game.render.queue.push(med);
  517. return med;
  518. }
  519. },
  520. /**
  521. * Adds arc to media queue.
  522. *
  523. * @param {number} x x coordinate for the arc center
  524. * @param {number} y y coordinate for the arc center
  525. * @param {number} diameter arc diameter
  526. * @param {number} angleStart angle to start the arc
  527. * @param {number} angleEnd angle to end the arc
  528. * @param {undefined|boolean} anticlockwise if true, arc is created anticlockwise (default = false)
  529. * @param {undefined|string} lineColor stroke color (default = black)
  530. * @param {undefined|number} lineWidth stroke width (default = 1px)
  531. * @param {undefined|string} fillColor fill color (default = no fill)
  532. * @param {undefined|number} alpha level of transparency, from 0 (invisible) to 1 (100% visible)) (default = 1)
  533. *
  534. * @returns {object} a reference to the created arc.
  535. */
  536. arc: function (x, y, diameter, angleStart, angleEnd, anticlockwise, lineColor, lineWidth, fillColor, alpha) {
  537. if (x == undefined || y == undefined || diameter == undefined || angleStart == undefined || angleEnd == undefined) console.error('Game error: missing parameters.');
  538. else {
  539. const med = {
  540. typeOfMedia: 'arc',
  541. x: x || game.add.default.x,
  542. y: y || game.add.default.y,
  543. _xWithAnchor: x || game.add.default._xWithAnchor,
  544. _yWithAnchor: y || game.add.default._yWithAnchor,
  545. xAnchor: game.add.default.xAnchor,
  546. yAnchor: game.add.default.yAnchor,
  547. shadow: game.add.default.shadow,
  548. shadowColor: game.add.default.shadowColor,
  549. shadowBlur: game.add.default.shadowBlur,
  550. alpha: (alpha != undefined) ? alpha : game.add.default.alpha,
  551. scale: game.add.default.scale,
  552. diameter: 0,
  553. width: 0,
  554. height: 0,
  555. angleStart: angleStart || 0,
  556. angleEnd: angleEnd || 2 * Math.PI,
  557. anticlockwise: anticlockwise || game.add.default.anticlockwise,
  558. lineColor: lineColor || game.add.default.lineColor,
  559. lineWidth: 0,
  560. fillColor: fillColor || game.add.default.fillColor,
  561. anchor: function (xAnchor, yAnchor) {
  562. this.xAnchor = xAnchor;
  563. this.yAnchor = yAnchor;
  564. },
  565. get xWithAnchor() { return this.x - (this.width * this.scale * this.xAnchor); },
  566. get yWithAnchor() { return this.y - (this.height * this.scale * this.yAnchor); }
  567. };
  568. med.originalScale = med.scale;
  569. if (diameter != 0) {
  570. med.diameter = diameter || game.add.default.diameter;
  571. med.width = med.height = med.diameter;
  572. }
  573. if (lineWidth != 0) { med.lineWidth = lineWidth || game.add.default.lineWidth; }
  574. game.render.queue.push(med);
  575. return med;
  576. }
  577. }
  578. },
  579. /**
  580. * [Not directly used] Default values for the media properties.
  581. */
  582. default: {
  583. // Used in: all types of media.
  584. x: 0,
  585. y: 0,
  586. _xWithAnchor: 0,
  587. _yWithAnchor: 0,
  588. xAnchor: 0,
  589. yAnchor: 0,
  590. shadow: false,
  591. shadowColor: '#0075c5',
  592. shadowBlur: 20,
  593. alpha: 1,
  594. // Used in: image, sprite, square, circle.
  595. scale: 1,
  596. // Used in: text.
  597. font: '14px Arial,sans-serif',
  598. fill: '#000',
  599. align: 'center',
  600. // Used in: square, circle (image and sprite have width and height, but do not have default values).
  601. width: 50,
  602. height: 50,
  603. lineColor: '#000',
  604. lineWidth: 1,
  605. fillColor: 0, // No fill
  606. // Used in: circle.
  607. diameter: 50,
  608. anticlockwise: false,
  609. },
  610. },
  611. /**
  612. * Renders media on current screen. <br<
  613. * It uses properties of html canvas to draw media on screen during game loop.
  614. *
  615. * @namespace
  616. */
  617. render: {
  618. // [Not directly used] Media queue to be rendered on the current state.
  619. queue: [],
  620. /** [Not directly used] Renders image on canvas.
  621. *
  622. * @param {object} cur current media in media queue
  623. */
  624. image: function (cur) {
  625. const x = cur.xWithAnchor, y = cur.yWithAnchor;
  626. // Rotation
  627. if (cur.rotate && cur.rotate != 0) {
  628. context.save();
  629. context.translate(cur.x, cur.y);
  630. context.rotate(cur.rotate * Math.PI / 180);
  631. context.translate(-cur.x, -cur.y);
  632. }
  633. // Alpha
  634. context.globalAlpha = cur.alpha;
  635. // Shadow
  636. context.shadowBlur = (cur.shadow) ? cur.shadowBlur : 0;
  637. context.shadowColor = cur.shadowColor;
  638. // Image
  639. context.drawImage(
  640. game.image[cur.name],
  641. x,
  642. y,
  643. cur.width * cur.scale,
  644. cur.height * cur.scale
  645. );
  646. // End
  647. context.shadowBlur = 0;
  648. context.globalAlpha = 1;
  649. if (cur.rotate && cur.rotate != 0) context.restore();
  650. },
  651. /** [Not directly used] Renders spritesheet on canvas.
  652. *
  653. * @param {object} cur current media in media queue
  654. */
  655. sprite: function (cur) {
  656. const x = cur.xWithAnchor, y = cur.yWithAnchor;
  657. // Rotation
  658. if (cur.rotate && cur.rotate != 0) {
  659. context.save();
  660. context.translate(cur.x, cur.y);
  661. context.rotate(cur.rotate * Math.PI / 180);
  662. context.translate(-cur.x, -cur.y);
  663. }
  664. // Alpha
  665. context.globalAlpha = cur.alpha;
  666. // Shadow
  667. context.shadowBlur = (cur.shadow) ? cur.shadowBlur : 0;
  668. context.shadowColor = cur.shadowColor;
  669. // Image
  670. context.drawImage(
  671. game.sprite[cur.name],
  672. cur.width * cur.curFrame,
  673. 0,
  674. cur.width,
  675. cur.height,
  676. x,
  677. y,
  678. cur.width * cur.scale,
  679. cur.height * cur.scale
  680. );
  681. // End
  682. context.shadowBlur = 0;
  683. context.globalAlpha = 1;
  684. if (cur.rotate && cur.rotate != 0) context.restore();
  685. },
  686. /** [Not directly used] Renders text on canvas.
  687. *
  688. * @param {object} cur current media in media queue
  689. */
  690. text: function (cur) {
  691. const x = cur.xWithAnchor, y = cur.yWithAnchor;
  692. // Rotation
  693. if (cur.rotate && cur.rotate != 0) {
  694. context.save();
  695. context.translate(cur.x, cur.y);
  696. context.rotate(cur.rotate * Math.PI / 180);
  697. context.translate(-cur.x, -cur.y);
  698. }
  699. // Alpha
  700. context.globalAlpha = cur.alpha;
  701. // Shadow
  702. context.shadowBlur = (cur.shadow) ? cur.shadowBlur : 0;
  703. context.shadowColor = cur.shadowColor;
  704. // Font style
  705. context.font = cur.font;
  706. context.textAlign = cur.align;
  707. context.fillStyle = cur.fill;
  708. // Text
  709. context.fillText(cur.name, x, y);
  710. // End
  711. context.shadowBlur = 0;
  712. context.globalAlpha = 1;
  713. if (cur.rotate && cur.rotate != 0) context.restore();
  714. },
  715. /** [Not directly used] Renders geometric shapes on canvas.
  716. *
  717. * @namespace
  718. */
  719. geom: {
  720. /**
  721. * Renders rectangle on canvas.
  722. *
  723. * @param {object} cur current media in media queue
  724. */
  725. rect: function (cur) {
  726. const x = cur.xWithAnchor, y = cur.yWithAnchor;
  727. // Rotation
  728. if (cur.rotate && cur.rotate != 0) {
  729. context.save();
  730. context.translate(cur.x, cur.y);
  731. context.rotate(cur.rotate * Math.PI / 180);
  732. context.translate(-cur.x, -cur.y);
  733. }
  734. // Alpha
  735. context.globalAlpha = cur.alpha;
  736. // Shadow
  737. context.shadowBlur = (cur.shadow) ? cur.shadowBlur : 0;
  738. context.shadowColor = cur.shadowColor;
  739. // Fill
  740. if (cur.fillColor != 0) {
  741. context.fillStyle = cur.fillColor;
  742. context.fillRect(x, y, cur.width * cur.scale, cur.height * cur.scale);
  743. }
  744. // Stroke
  745. if (cur.lineWidth != 0) {
  746. context.strokeStyle = cur.lineColor;
  747. context.lineWidth = cur.lineWidth;
  748. context.strokeRect(x, y, cur.width * cur.scale, cur.height * cur.scale);
  749. }
  750. // End
  751. context.shadowBlur = 0;
  752. context.globalAlpha = 1;
  753. if (cur.rotate && cur.rotate != 0) context.restore();
  754. },
  755. /**
  756. * Renders arc on canvas (arc or circle).
  757. *
  758. * @param {object} cur current media in media queue
  759. */
  760. arc: function (cur) {
  761. const x = cur.xWithAnchor, y = cur.yWithAnchor;
  762. // Rotation
  763. if (cur.rotate && cur.rotate != 0) {
  764. context.save();
  765. context.translate(cur.x, cur.y);
  766. context.rotate(cur.rotate * Math.PI / 180);
  767. context.translate(-cur.x, -cur.y);
  768. }
  769. // Alpha
  770. context.globalAlpha = cur.alpha;
  771. // Shadow
  772. context.shadowBlur = (cur.shadow) ? cur.shadowBlur : 0;
  773. context.shadowColor = cur.shadowColor;
  774. // Fill info
  775. if (cur.fillColor != 0) context.fillStyle = cur.fillColor;
  776. // Stroke info
  777. if (cur.lineWidth != 0) {
  778. context.strokeStyle = cur.lineColor;
  779. context.lineWidth = cur.lineWidth;
  780. }
  781. // Path
  782. context.beginPath();
  783. if (cur.angleEnd != 2 * Math.PI) context.lineTo(x, y);
  784. context.arc(x, y, (cur.diameter / 2) * cur.scale, cur.angleStart, cur.angleEnd, cur.anticlockwise);
  785. if (cur.angleEnd != 2 * Math.PI) context.lineTo(x, y);
  786. // End
  787. if (cur.fillColor != 0) context.fill();
  788. if (cur.lineWidth != 0) context.stroke();
  789. context.shadowBlur = 0;
  790. context.globalAlpha = 1;
  791. if (cur.rotate && cur.rotate != 0) context.restore();
  792. },
  793. },
  794. /**
  795. * Renders all queued media on screen. Called repeatedly by the game loop.
  796. */
  797. all: function () {
  798. game.render.queue.forEach(cur => {
  799. switch (cur.typeOfMedia) {
  800. case 'image': this.image(cur); break;
  801. case 'sprite': this.sprite(cur); break;
  802. case 'text': this.text(cur); break;
  803. case 'rect': this.geom.rect(cur); break;
  804. case 'arc': this.geom.arc(cur); break;
  805. }
  806. });
  807. },
  808. /**
  809. * Clears media queue (used when changing states).
  810. */
  811. clear: function () {
  812. game.render.queue = [];
  813. }
  814. },
  815. /**
  816. * Math functions.
  817. *
  818. * @namespace
  819. */
  820. math: {
  821. /**
  822. * Returns a random integer in a range (inclusive for min and max).
  823. *
  824. * @param {number} min smaller integer
  825. * @param {number} max larger integer
  826. *
  827. * @returns {number} random integer in range
  828. */
  829. randomInRange: function (min, max) {
  830. min = Math.ceil(min);
  831. max = Math.floor(max);
  832. return Math.floor(Math.random() * (max - min + 1) + min);
  833. },
  834. /**
  835. * Returns a random divisor for a given number.
  836. *
  837. * @param {number} number number
  838. *
  839. * @returns {number} random divisor for that number
  840. */
  841. randomDivisor: function (number) {
  842. const validDivisors = [];
  843. // If 'number' can be divided by 'i', add to list of 'validDivisors'
  844. for (let i = 2; i < number; i++) {
  845. if (number % i == 0) validDivisors.push(i);
  846. }
  847. const randIndex = game.math.randomInRange(0, validDivisors.length - 1);
  848. return validDivisors[randIndex];
  849. },
  850. /**
  851. * Converts degree to radian.
  852. *
  853. * @param {number} degree number in degrees
  854. *
  855. * @returns {number} its radian equivalent
  856. */
  857. degreeToRad: function (degree) {
  858. return degree * Math.PI / 180;
  859. },
  860. /**
  861. * Returns distance from the center of an icon to mouse/pointer (radius).
  862. *
  863. * @param {number} xMouse mouse x coordinate
  864. * @param {number} xIcon icon x coordinate
  865. * @param {number} yMouse mouse y coordinate
  866. * @param {number} yIcon icon y coordinate
  867. *
  868. * @returns {number} distance between the two icons
  869. */
  870. distanceToPointer: function (xMouse, xIcon, yMouse, yIcon) {
  871. const a = Math.max(xMouse, xIcon) - Math.min(xMouse, xIcon);
  872. const b = Math.max(yMouse, yIcon) - Math.min(yMouse, yIcon);
  873. return Math.sqrt(a * a + b * b);
  874. },
  875. /**
  876. * Checks if pointer/mouse is over (rectangular) icon.
  877. *
  878. * @param {number} xMouse contains the mouse x coordinate
  879. * @param {number} yMouse contains the mouse y coordinate
  880. * @param {object} icon icon
  881. *
  882. * @returns {boolean} true if cursor is over icon
  883. */
  884. isOverIcon: function (xMouse, yMouse, icon) {
  885. const x = xMouse, y = yMouse, cur = icon;
  886. return y >= cur.yWithAnchor && y <= (cur.yWithAnchor + cur.height * cur.scale) &&
  887. (x >= cur.xWithAnchor && x <= (cur.xWithAnchor + cur.width * cur.scale));
  888. },
  889. /**
  890. * Converts a given time in seconds (number) to the format HH:MM:SS (string)
  891. *
  892. * @param {number} s time in seconds
  893. *
  894. * @returns {string} time in the format HH:MM:SS
  895. */
  896. convertTime: function (s) {
  897. let h = 0, m = 0;
  898. if (s > 1200) {
  899. h = s / 1200;
  900. s = s % 1200;
  901. }
  902. if (s > 60) {
  903. m = s / 60;
  904. s = s % 60;
  905. }
  906. h = '' + h;
  907. m = '' + m;
  908. s = '' + s;
  909. if (h.length < 2) h = '0' + h;
  910. if (m.length < 2) m = '0' + m;
  911. if (s.length < 2) s = '0' + s;
  912. return h + ':' + m + ':' + s;
  913. }
  914. },
  915. /**
  916. * Timer used to get the time spent to complete a game.
  917. *
  918. * @namespace
  919. */
  920. timer: {
  921. // [Not directly used] Start time.
  922. _start: 0,
  923. // [Not directly used] End time.
  924. end: 0,
  925. // Elapsed time.
  926. elapsed: 0,
  927. /**
  928. * Reset values and start timer.
  929. */
  930. start: function () {
  931. game.timer._start = game.timer.end = game.timer.elapsed = 0;
  932. game.timer._start = new Date().getTime();
  933. },
  934. /**
  935. * Stop timer and set elapsed time.
  936. */
  937. stop: function () {
  938. if (game.timer._start != 0 && game.timer.end == 0) { // If timer has started but not finished
  939. game.timer.end = new Date().getTime();
  940. game.timer.elapsed = Math.floor((game.timer.end - game.timer._start) / 1000);
  941. }
  942. },
  943. },
  944. /**
  945. * Handles pointer events. <br>
  946. *
  947. * @namespace
  948. */
  949. event: {
  950. // [Not directly used] List of events in current state.
  951. list: [],
  952. /**
  953. * Adds new event to current state.
  954. *
  955. * @param {string} name event name, can be: 'click' or 'mousemove'
  956. * @param {function} func function to be called when event is triggered
  957. */
  958. add: function (name, func) {
  959. canvas.addEventListener(name, func);
  960. game.event.list.push([name, func]);
  961. },
  962. /** [Not directly used] Clears list of events. Called before moving to new state.
  963. */
  964. clear: function () {
  965. game.event.list.forEach(cur => {
  966. canvas.removeEventListener(cur[0], cur[1]);
  967. });
  968. game.event.list = [];
  969. },
  970. },
  971. /** [Not directly used] Handles 'game loop'. <br>
  972. *
  973. * After the media queue is filled in create(), the 'game loop' starts.
  974. * It calls update() iteratively, re-rendering the screen every time. <br>
  975. *
  976. * The 'game loop' is stoped by leaving the current state.
  977. *
  978. * @namespace
  979. */
  980. loop: {
  981. // [Not directly used] Holds animation event.
  982. id: undefined,
  983. // [Not directly used] State that called the loop.
  984. curState: undefined,
  985. // [Not directly used] Loop status, can be: 'on', 'ending' or 'off'.
  986. status: 'off',
  987. // [Not directly used]
  988. waitingToStart: undefined,
  989. // [Not directly used]
  990. startTime: 0,
  991. // [Not directly used] 1000: 1 second | 60: expected frames per second.
  992. duration: 1000 / 60,
  993. /** [Not directly used] Starts game loop.
  994. *
  995. * @param {object} state current state
  996. */
  997. start: function (state) {
  998. if (game.loop.status == 'off') {
  999. game.loop.curState = state;
  1000. game.loop.startTime = new Date().getTime();
  1001. game.loop.status = 'on';
  1002. game.loop.id = requestAnimationFrame(game.loop.run);
  1003. } else { // If 'game.loop.status' is either 'on' or 'ending'
  1004. game.loop.waitingToStart = state;
  1005. if (game.loop.status == 'on') game.loop.stop();
  1006. }
  1007. },
  1008. /**
  1009. * [Not directly used] Stops game loop.
  1010. */
  1011. stop: function () {
  1012. if (game.loop.status == 'on') game.loop.status = 'ending';
  1013. },
  1014. /**
  1015. * [Not directly used] Executes game loop.
  1016. *
  1017. * This code will run on each iteration of the game loop.
  1018. */
  1019. run: function () {
  1020. if (game.loop.status != 'on') {
  1021. game.loop.clear();
  1022. } else {
  1023. const timestamp = new Date().getTime();
  1024. const runtime = timestamp - game.loop.startTime;
  1025. if (runtime >= game.loop.duration) {
  1026. // Calls state's update()
  1027. game.loop.curState.update();
  1028. // Updates state's animation
  1029. game.animation.run();
  1030. }
  1031. game.loop.id = requestAnimationFrame(game.loop.run); // Loop
  1032. }
  1033. },
  1034. /**
  1035. * [Not directly used] Resets game loop values.
  1036. */
  1037. clear: function () {
  1038. if (game.loop.id != undefined) {
  1039. cancelAnimationFrame(game.loop.id); // Cancel animation event
  1040. game.loop.id = undefined; // Clears object that holds animation event
  1041. game.loop.curState = undefined; // Clears object that holds current state
  1042. game.loop.status = 'off'; // Inform animation must end (read in game.loop.run())
  1043. }
  1044. if (game.loop.waitingToStart != undefined) {
  1045. const temp = game.loop.waitingToStart;
  1046. game.loop.waitingToStart = undefined;
  1047. game.loop.start(temp);
  1048. }
  1049. },
  1050. },
  1051. /**
  1052. * Handles spritesheet animation. <br>
  1053. * It iterates through the spritesheet frames inside the animation queue.
  1054. * Called by game loop.
  1055. *
  1056. * @namespace
  1057. */
  1058. animation: {
  1059. // [Not directly used] Animation queue for the current state.
  1060. queue: [],
  1061. // [Not directly used]
  1062. count: 0,
  1063. /**
  1064. * Plays animation.
  1065. *
  1066. * @param {string} name animation name (identifier)
  1067. */
  1068. play: function (name) {
  1069. let newAnimation;
  1070. // Gets first object in the 'render queue' with that animation name
  1071. for (let i in game.render.queue) {
  1072. if (game.render.queue[i].animation != undefined && game.render.queue[i].animation[0] == name) {
  1073. newAnimation = game.render.queue[i];
  1074. break;
  1075. }
  1076. }
  1077. // If found, saves object in the 'animation queue'
  1078. if (newAnimation != undefined) game.animation.queue.push(newAnimation);
  1079. },
  1080. /**
  1081. * Stops animation.
  1082. *
  1083. * @param {string} name animation name
  1084. */
  1085. stop: function (name) {
  1086. // Removes all with that name from the 'animation queue'
  1087. game.animation.queue.forEach(cur => {
  1088. if (cur.animation[0] == name) {
  1089. game.animation.queue.splice(cur, 1);
  1090. }
  1091. });
  1092. },
  1093. /**
  1094. * [Not directly used] Executes animation.
  1095. */
  1096. run: function () {
  1097. game.animation.queue.forEach(character => {
  1098. if (!character.animation[2] || game.animation.count % character.animation[2] == 0) {
  1099. const i = character.animation[1].indexOf(character.curFrame);
  1100. if (i == -1) { // Frame not found
  1101. if (debugMode) console.error('Game error: animation frame not found.');
  1102. } else if (i < character.animation[1].length - 1) { // Go to next frame
  1103. character.curFrame = character.animation[1][i + 1];
  1104. } else {
  1105. character.curFrame = character.animation[1][0]; // If last frame, restart
  1106. }
  1107. }
  1108. });
  1109. game.animation.count++;
  1110. },
  1111. /**
  1112. * [Not directly used] Clears animation queue.
  1113. */
  1114. clear: function () {
  1115. // Resets animation counter
  1116. game.animation.count = 0;
  1117. // Clears property 'animation' from objects in game.render.queue
  1118. game.render.queue.forEach(cur => {
  1119. if (cur.animation != undefined) {
  1120. delete cur.animation;
  1121. }
  1122. });
  1123. // Clears animation queue
  1124. game.animation.queue = [];
  1125. },
  1126. }
  1127. };