gameMechanics.js 40 KB

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