gameMechanics.js 37 KB

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