squareTwo.js 46 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451
  1. /******************************
  2. * This file holds game states.
  3. ******************************/
  4. /** [GAME STATE]
  5. *
  6. * .squareTwo. = gameName
  7. * .../...\...
  8. * ..a.....b.. = gameMode
  9. * ....\./....
  10. * .....|.....
  11. * ..equals... = gameOperation
  12. * .....|.....
  13. * .1,2,3,4,5. = gameDifficulty
  14. *
  15. * Character : kid
  16. * Theme : (not themed)
  17. * Concept : player select equivalent dividends for fractions with different divisors
  18. * Represent fractions as : subdivided rectangles
  19. *
  20. * Game modes can be :
  21. *
  22. * a : equivalence of fractions
  23. * top has more subdivisions
  24. * b : equivalence of fractions
  25. * bottom has more subdivisions
  26. *
  27. * Operations :
  28. *
  29. * equals : Player selects equivalent fractions of both blocks
  30. *
  31. * @namespace
  32. */
  33. const squareTwo = {
  34. ui: undefined,
  35. control: undefined,
  36. blocks: undefined,
  37. /**
  38. * Main code
  39. */
  40. create: function () {
  41. this.ui = {
  42. message: undefined,
  43. startChoice: {
  44. message: undefined,
  45. card: undefined,
  46. image: undefined,
  47. title: undefined,
  48. subtitle: undefined,
  49. accept: {
  50. button: undefined,
  51. text: undefined,
  52. },
  53. yes: {
  54. button: undefined,
  55. text: undefined,
  56. },
  57. no: {
  58. button: undefined,
  59. text: undefined,
  60. },
  61. },
  62. continue: {
  63. // modal: undefined,
  64. button: undefined,
  65. text: undefined,
  66. },
  67. explanation: {
  68. button: undefined,
  69. text: undefined,
  70. },
  71. };
  72. this.control = {
  73. blockWidth: 600,
  74. blockHeight: 75,
  75. isCorrect: false,
  76. showEndInfo: false,
  77. showExplanation: false,
  78. animationDelay: 0,
  79. startDelay: false,
  80. startEndAnimation: false,
  81. started: false,
  82. blockConfig: undefined,
  83. challengePreview: undefined,
  84. challengeAnsweredYes: undefined,
  85. };
  86. this.blocks = {
  87. top: {
  88. list: [], // List of block objects
  89. auxBlocks: [], // List of shadow under selection blocks
  90. fractions: [], // Fractions
  91. selectedAmount: 0,
  92. hasClicked: false, // Check if player clicked blocks from (a)
  93. animate: false, // Animate blocks from (a)
  94. warningText: undefined,
  95. label: undefined,
  96. },
  97. bottom: {
  98. list: [],
  99. auxBlocks: [],
  100. fractions: [],
  101. selectedAmount: 0,
  102. hasClicked: false,
  103. animate: false,
  104. warningText: undefined,
  105. label: undefined,
  106. },
  107. };
  108. renderBackground();
  109. // Calls function that loads navigation icons
  110. // FOR MOODLE
  111. if (moodle) {
  112. navigation.add.right(['audio']);
  113. } else {
  114. navigation.add.left(['back', 'menu'], 'customMenu');
  115. navigation.add.right(['audio']);
  116. }
  117. // Pre-generate the level configuration so the challenge modal can preview real fractions
  118. this.control.blockConfig = this.utils.generateBlockConfig();
  119. this.control.challengePreview = this.utils.generateChallengePreview(
  120. this.control.blockConfig
  121. );
  122. // Wait for user confirmation before starting the level
  123. this.utils.renderStartChoiceUI();
  124. game.event.add('click', this.events.onInputDown);
  125. game.event.add('mousemove', this.events.onInputOver);
  126. },
  127. /**
  128. * Game loop
  129. */
  130. update: function () {
  131. // Animate blocks
  132. if (self.blocks.top.animate || self.blocks.bottom.animate) {
  133. self.utils.moveBlocks();
  134. }
  135. // If (a) and (b) are already clicked
  136. if (
  137. !self.control.startDelay &&
  138. !self.control.startEndAnimation &&
  139. self.blocks.top.hasClicked &&
  140. self.blocks.bottom.hasClicked
  141. ) {
  142. self.control.startDelay = true;
  143. }
  144. if (self.control.startDelay && !self.control.startEndAnimation) {
  145. self.utils.startDelayHandler();
  146. }
  147. // Wait a bit and go to map state
  148. if (self.control.startEndAnimation) {
  149. self.utils.runEndAnimation();
  150. }
  151. game.render.all();
  152. },
  153. utils: {
  154. // RENDERS
  155. generateBlockConfig: function () {
  156. // Coordinates for (a) and (b)
  157. let xA, xB, yA, yB;
  158. if (gameMode != 'b') {
  159. // Has more subdivisions on (a)
  160. xA = context.canvas.width / 2 - self.control.blockWidth / 2;
  161. yA = getFrameInfo().y + 100;
  162. xB = xA;
  163. yB = yA + 3 * self.control.blockHeight + 30;
  164. } else {
  165. // Has more subdivisions on (b)
  166. xB = context.canvas.width / 2 - self.control.blockWidth / 2;
  167. yB = getFrameInfo().y + 100;
  168. xA = xB;
  169. yA = yB + 3 * self.control.blockHeight + 30;
  170. }
  171. // Possible subdivisionList for (a)
  172. const subdivisionList = [2, 4, 6, 8, 9, 10, 12, 14, 15, 16, 18, 20];
  173. const minIndex = (gameDifficulty - 1) * 2 + 1;
  174. const maxIndex = (gameDifficulty - 1) * 2 + 3;
  175. // Build a shuffled pool of all (A, B) pairs for this difficulty and
  176. // iterate through it so the same combination never repeats until all
  177. // possibilities have been exhausted, then reshuffle and start again.
  178. if (
  179. !squareTwo._configPool ||
  180. squareTwo._configPoolDifficulty !== gameDifficulty ||
  181. squareTwo._configIndex >= squareTwo._configPool.length
  182. ) {
  183. const allPairs = [];
  184. for (let i = minIndex; i <= maxIndex; i++) {
  185. const a = subdivisionList[i];
  186. for (let b = 2; b < a; b++) {
  187. if (a % b === 0) allPairs.push({ a, b });
  188. }
  189. }
  190. // Fisher-Yates shuffle
  191. for (let i = allPairs.length - 1; i > 0; i--) {
  192. const j = Math.floor(Math.random() * (i + 1));
  193. [allPairs[i], allPairs[j]] = [allPairs[j], allPairs[i]];
  194. }
  195. squareTwo._configPool = allPairs;
  196. squareTwo._configPoolDifficulty = gameDifficulty;
  197. squareTwo._configIndex = 0;
  198. }
  199. // Number of subdivisions of (a) and (b) (blocks)
  200. const { a: totalBlocksA, b: totalBlocksB } =
  201. squareTwo._configPool[squareTwo._configIndex++];
  202. const blockWidthA = self.control.blockWidth / totalBlocksA;
  203. const blockWidthB = self.control.blockWidth / totalBlocksB;
  204. if (isDebugMode) {
  205. console.log(
  206. '------------------------------' +
  207. '\nGame Map Position: ' +
  208. curMapPosition +
  209. '\n------------------------ setup' +
  210. '\narray: [2, 4, 6, 8, 9, 10, 12, 14, 15, 16, 18, 20]' +
  211. '\nMin index ((gameDifficulty - 1) * 2 + 1): ' +
  212. ((gameDifficulty - 1) * 2 + 1) +
  213. '\nMax index ((gameDifficulty - 1) * 2 + 3): ' +
  214. ((gameDifficulty - 1) * 2 + 3) +
  215. '\n------------------------ this' +
  216. '\ntotalBlocksA: ' +
  217. totalBlocksA +
  218. '\ntotalBlocksB: ' +
  219. totalBlocksB +
  220. '\npool index: ' + (squareTwo._configIndex - 1) + '/' + squareTwo._configPool.length +
  221. '\n------------------------------'
  222. );
  223. }
  224. return {
  225. xA,
  226. yA,
  227. xB,
  228. yB,
  229. totalBlocksA,
  230. totalBlocksB,
  231. blockWidthA,
  232. blockWidthB,
  233. };
  234. },
  235. generateChallengePreview: function (blockConfig) {
  236. if (!blockConfig) return undefined;
  237. const { totalBlocksA, totalBlocksB } = blockConfig;
  238. if (!totalBlocksA || !totalBlocksB) return undefined;
  239. const gcd = (a, b) => {
  240. let x = Math.abs(a);
  241. let y = Math.abs(b);
  242. while (y !== 0) {
  243. const t = x % y;
  244. x = y;
  245. y = t;
  246. }
  247. return x;
  248. };
  249. // Always produce an equivalent integer pair using the gcd
  250. const g = gcd(totalBlocksA, totalBlocksB);
  251. const leftUnit = totalBlocksA / g;
  252. const rightUnit = totalBlocksB / g;
  253. // Deterministic preview (prevents changing values between renders)
  254. const selectedA = leftUnit;
  255. const selectedB = rightUnit;
  256. // Show as left/right fractions in the modal (keep order consistent with blocks)
  257. return {
  258. left: { num: selectedA, den: totalBlocksA },
  259. right: { num: selectedB, den: totalBlocksB },
  260. };
  261. },
  262. renderStartChoiceUI: () => {
  263. const centerX = context.canvas.width / 2;
  264. const centerY = context.canvas.height / 2;
  265. const yesLabel = game.lang.yes;
  266. const noLabel = game.lang.no;
  267. const withNewlines = (s) => (s == null ? '' : String(s).replace(/\\n/g, '\n'));
  268. const title = withNewlines(game.lang.s2_challenge_title);
  269. const subtitle = withNewlines(game.lang.s2_challenge_subtitle);
  270. const question = withNewlines(game.lang.s2_challenge_question);
  271. // Layout reference (no white panel/card)
  272. const cardH = 560;
  273. const panelTop = centerY - cardH / 2;
  274. // Challenge image (ribbon + question mark)
  275. const imageScale = 0.7;
  276. const imageY = panelTop - 150;
  277. self.ui.startChoice.image = game.add.image(
  278. centerX,
  279. imageY,
  280. 'challenge',
  281. imageScale,
  282. 1
  283. );
  284. self.ui.startChoice.image.anchor(0.5, 0);
  285. // Title + subtitle
  286. self.ui.startChoice.title = game.add.text(
  287. centerX,
  288. imageY + 65,
  289. title,
  290. { ...textStyles.h2_, fill: colors.white, font: 'bold ' + textStyles.h2_.font }
  291. );
  292. self.ui.startChoice.title.anchor(0.5, 0.5);
  293. // Subtitle split in two lines (top emphasized)
  294. const subtitleLines = String(subtitle || '').split('\n');
  295. const subtitleTop = subtitleLines[0] || '';
  296. const subtitleBottom = subtitleLines.slice(1).join('\n');
  297. self.ui.startChoice.subtitle = {};
  298. self.ui.startChoice.subtitle.top = game.add.text(
  299. centerX,
  300. panelTop + 10,
  301. subtitleTop,
  302. {
  303. ...textStyles.h4_,
  304. font: 'bold ' + textStyles.h4_.font,
  305. fill: colors.blueDark,
  306. }
  307. );
  308. self.ui.startChoice.subtitle.top.anchor(0.5, 0.5);
  309. self.ui.startChoice.subtitle.bottom = game.add.text(
  310. centerX,
  311. panelTop + 55,
  312. subtitleBottom,
  313. { ...textStyles.h3_, fill: colors.blue }
  314. );
  315. self.ui.startChoice.subtitle.bottom.anchor(0.5, 0.5);
  316. // Fractions preview (left and right) - based on the real generated blockConfig
  317. const preview =
  318. self.control.challengePreview ||
  319. self.utils.generateChallengePreview(self.control.blockConfig);
  320. self.control.challengePreview = preview;
  321. if (preview && preview.left && preview.right) {
  322. const fracFont = { ...textStyles.fraction, fill: colors.blueDark, align: 'center', font: '76px monospace' };
  323. const fracY = panelTop + 255;
  324. const offsetX = 260;
  325. const leftX = centerX - offsetX;
  326. const rightX = centerX + offsetX;
  327. const leftText = game.add.text(
  328. leftX,
  329. fracY,
  330. preview.left.num + '\n' + preview.left.den,
  331. fracFont,
  332. 70
  333. );
  334. leftText.anchor(0.5, 0.5);
  335. const leftLine = game.add.geom.rect(
  336. leftX,
  337. fracY + 10,
  338. 120,
  339. 4,
  340. colors.blueDark,
  341. 1,
  342. colors.blueDark,
  343. 4
  344. );
  345. leftLine.anchor(0.5, 0);
  346. const rightText = game.add.text(
  347. rightX,
  348. fracY,
  349. preview.right.num + '\n' + preview.right.den,
  350. fracFont,
  351. 70
  352. );
  353. rightText.anchor(0.5, 0.5);
  354. const rightLine = game.add.geom.rect(
  355. rightX,
  356. fracY + 10,
  357. 120,
  358. 4,
  359. colors.blueDark,
  360. 1,
  361. colors.blueDark,
  362. 4
  363. );
  364. rightLine.anchor(0.5, 0);
  365. self.ui.startChoice.fractions = {
  366. leftText,
  367. leftLine,
  368. rightText,
  369. rightLine,
  370. };
  371. }
  372. // Prompt
  373. self.ui.startChoice.message = game.add.text(
  374. centerX,
  375. panelTop + 415,
  376. question,
  377. {
  378. ...textStyles.h3_,
  379. font: 'bold ' + textStyles.h3_.font,
  380. fill: colors.blueDark,
  381. }
  382. );
  383. self.ui.startChoice.message.anchor(0.5, 0.5);
  384. // Buttons
  385. const buttonW = 340;
  386. const buttonH = 90;
  387. const gap = 90;
  388. const buttonsY = panelTop + 600;
  389. const choiceTextStyle = { ...textStyles.h3_, fill: colors.blueDark, font: 'bold ' + textStyles.h3_.font };
  390. const borderColor = colors.blueMenuLine;
  391. const fillColor = colors.white;
  392. const fillAlpha = 0.9;
  393. const selectedFillColor = colors.blueLight;
  394. const createPillButton = (x, y, label) => {
  395. // Invisible hit target used for hover/click detection
  396. const hit = game.add.geom.rect(
  397. x,
  398. y,
  399. buttonW,
  400. buttonH,
  401. colors.white,
  402. 0,
  403. colors.white,
  404. 0
  405. );
  406. hit.anchor(0.5, 0.5);
  407. const outerW = buttonW;
  408. const outerH = buttonH;
  409. const innerInset = 10;
  410. const innerW = outerW - innerInset;
  411. const innerH = outerH - innerInset;
  412. const outerRect = game.add.geom.rect(
  413. x,
  414. y,
  415. outerW,
  416. outerH,
  417. borderColor,
  418. 1,
  419. borderColor,
  420. 0
  421. );
  422. outerRect.anchor(0.5, 0.5);
  423. outerRect.shadow = true;
  424. outerRect.shadowColor = colors.gray;
  425. outerRect.shadowBlur = 8;
  426. const innerRect = game.add.geom.rect(
  427. x,
  428. y,
  429. innerW,
  430. innerH,
  431. fillColor,
  432. fillAlpha,
  433. fillColor,
  434. 0
  435. );
  436. innerRect.anchor(0.5, 0.5);
  437. const text = game.add.text(x, y + 10, label, choiceTextStyle);
  438. return {
  439. hit,
  440. text,
  441. outerParts: [outerRect],
  442. innerParts: [innerRect],
  443. setSelected: (isSelected) => {
  444. const c = isSelected ? selectedFillColor : fillColor;
  445. [innerRect].forEach((p) => {
  446. p.fillColor = c;
  447. p.lineColor = c;
  448. });
  449. },
  450. };
  451. };
  452. const yesX = centerX - (buttonW / 2 + gap / 2);
  453. const noX = centerX + (buttonW / 2 + gap / 2);
  454. const yes = createPillButton(yesX, buttonsY, yesLabel);
  455. const no = createPillButton(noX, buttonsY, noLabel);
  456. // keep existing structure expected by event handlers
  457. self.ui.startChoice.yes.button = yes.hit;
  458. self.ui.startChoice.yes.text = yes.text;
  459. self.ui.startChoice.yes.visual = yes;
  460. self.ui.startChoice.no.button = no.hit;
  461. self.ui.startChoice.no.text = no.text;
  462. self.ui.startChoice.no.visual = no;
  463. game.render.all();
  464. },
  465. updateChallengeChoiceUI: () => {
  466. const selected = self.control.challengeAnsweredYes;
  467. const yesBtn = self.ui.startChoice?.yes?.button;
  468. const noBtn = self.ui.startChoice?.no?.button;
  469. const yesVisual = self.ui.startChoice?.yes?.visual;
  470. const noVisual = self.ui.startChoice?.no?.visual;
  471. if (yesBtn) {
  472. if (yesVisual && yesVisual.setSelected) {
  473. yesVisual.setSelected(selected === true);
  474. } else {
  475. yesBtn.fillColor = selected === true ? colors.blueLight : colors.white;
  476. }
  477. }
  478. if (noBtn) {
  479. if (noVisual && noVisual.setSelected) {
  480. noVisual.setSelected(selected === false);
  481. } else {
  482. noBtn.fillColor = selected === false ? colors.blueLight : colors.white;
  483. }
  484. }
  485. game.render.all();
  486. },
  487. startGame: () => {
  488. if (self.control.started) return;
  489. self.control.started = true;
  490. // Hide pre-start UI
  491. if (self.ui.startChoice && self.ui.startChoice.message) {
  492. self.ui.startChoice.message.alpha = 0;
  493. }
  494. if (self.ui.startChoice && self.ui.startChoice.title) {
  495. self.ui.startChoice.title.alpha = 0;
  496. }
  497. if (self.ui.startChoice && self.ui.startChoice.subtitle) {
  498. if (self.ui.startChoice.subtitle.alpha != null) {
  499. self.ui.startChoice.subtitle.alpha = 0;
  500. } else {
  501. const s = self.ui.startChoice.subtitle;
  502. if (s.top) s.top.alpha = 0;
  503. if (s.bottom) s.bottom.alpha = 0;
  504. }
  505. }
  506. if (self.ui.startChoice && self.ui.startChoice.image) {
  507. self.ui.startChoice.image.alpha = 0;
  508. }
  509. if (self.ui.startChoice && self.ui.startChoice.fractions) {
  510. Object.values(self.ui.startChoice.fractions).forEach((obj) => {
  511. if (obj) obj.alpha = 0;
  512. });
  513. }
  514. if (self.ui.startChoice && self.ui.startChoice.card) {
  515. self.ui.startChoice.card.alpha = 0;
  516. }
  517. if (self.ui.startChoice && self.ui.startChoice.yes) {
  518. if (self.ui.startChoice.yes.button) self.ui.startChoice.yes.button.alpha = 0;
  519. if (self.ui.startChoice.yes.text) self.ui.startChoice.yes.text.alpha = 0;
  520. if (self.ui.startChoice.yes.visual) {
  521. const v = self.ui.startChoice.yes.visual;
  522. if (v.outerParts) v.outerParts.forEach((p) => (p.alpha = 0));
  523. if (v.innerParts) v.innerParts.forEach((p) => (p.alpha = 0));
  524. }
  525. }
  526. if (self.ui.startChoice && self.ui.startChoice.no) {
  527. if (self.ui.startChoice.no.button) self.ui.startChoice.no.button.alpha = 0;
  528. if (self.ui.startChoice.no.text) self.ui.startChoice.no.text.alpha = 0;
  529. if (self.ui.startChoice.no.visual) {
  530. const v = self.ui.startChoice.no.visual;
  531. if (v.outerParts) v.outerParts.forEach((p) => (p.alpha = 0));
  532. if (v.innerParts) v.innerParts.forEach((p) => (p.alpha = 0));
  533. }
  534. }
  535. if (self.ui.startChoice && self.ui.startChoice.accept) {
  536. if (self.ui.startChoice.accept.button)
  537. self.ui.startChoice.accept.button.alpha = 0;
  538. if (self.ui.startChoice.accept.text)
  539. self.ui.startChoice.accept.text.alpha = 0;
  540. }
  541. document.body.style.cursor = 'auto';
  542. // Add kid + blocks + UI (original flow)
  543. self.utils.renderCharacters();
  544. self.utils.renderBlockSetup(self.control.blockConfig);
  545. self.utils.renderMainUI();
  546. game.timer.start(); // Set a timer for the current level (used in postScore)
  547. game.render.all();
  548. },
  549. renderBlockSetup: function (blockConfig) {
  550. const cfg = blockConfig || self.utils.generateBlockConfig();
  551. if (!cfg) return;
  552. const {
  553. xA,
  554. yA,
  555. xB,
  556. yB,
  557. totalBlocksA,
  558. totalBlocksB,
  559. blockWidthA,
  560. blockWidthB,
  561. } = cfg;
  562. // (a)
  563. self.utils.renderBlocks(
  564. self.blocks.top,
  565. 'top',
  566. totalBlocksA,
  567. blockWidthA,
  568. colors.blueDark,
  569. colors.blueLight,
  570. xA,
  571. yA
  572. );
  573. // (b)
  574. self.utils.renderBlocks(
  575. self.blocks.bottom,
  576. 'bottom',
  577. totalBlocksB,
  578. blockWidthB,
  579. colors.greenDark,
  580. colors.greenLight,
  581. xB,
  582. yB
  583. );
  584. },
  585. renderBlocks: function (
  586. blocks,
  587. blockType,
  588. totalBlocks,
  589. blockWidth,
  590. lineColor,
  591. fillColor,
  592. x0,
  593. y0
  594. ) {
  595. for (let i = 0; i < totalBlocks; i++) {
  596. // Blocks
  597. const curX = x0 + i * blockWidth;
  598. const curBlock = game.add.geom.rect(
  599. curX,
  600. y0,
  601. blockWidth,
  602. self.control.blockHeight,
  603. fillColor,
  604. 0.5,
  605. lineColor,
  606. 4
  607. );
  608. curBlock.position = blockType;
  609. curBlock.index = i;
  610. curBlock.finalX = x0;
  611. blocks.list.push(curBlock);
  612. // Auxiliar blocks (lower alpha)
  613. const alpha = 0.2;
  614. const curYAux = y0 + self.control.blockHeight + 10;
  615. const curAuxBlock = game.add.geom.rect(
  616. curX,
  617. curYAux,
  618. blockWidth,
  619. self.control.blockHeight,
  620. fillColor,
  621. alpha,
  622. lineColor,
  623. 1
  624. );
  625. blocks.auxBlocks.push(curAuxBlock);
  626. }
  627. // Label - number of blocks (on the right)
  628. let yLabel = y0 + self.control.blockHeight / 2 + 10;
  629. const xLabel = x0 + self.control.blockWidth + 35;
  630. const font = {
  631. ...textStyles.h4_,
  632. font: 'bold ' + textStyles.h4_.font,
  633. fill: lineColor,
  634. };
  635. blocks.label = game.add.text(xLabel, yLabel, blocks.list.length, font);
  636. blocks.label.alpha = showFractions ? 1 : 0;
  637. // 'selected blocks/fraction' label for (a) : at the bottom of (a)
  638. yLabel = y0 + self.control.blockHeight + 40;
  639. blocks.fractions[0] = game.add.text(xLabel, yLabel, '', font);
  640. blocks.fractions[1] = game.add.geom.line(
  641. xLabel,
  642. yLabel + 10,
  643. xLabel + 50,
  644. yLabel + 10,
  645. 2,
  646. lineColor
  647. );
  648. blocks.fractions[1].anchor(0.5, 0);
  649. blocks.fractions[0].alpha = 0;
  650. blocks.fractions[1].alpha = 0;
  651. // Invalid selection text
  652. blocks.warningText = game.add.text(
  653. context.canvas.width / 2,
  654. y0 - 20,
  655. game.lang.s2_error_msg,
  656. { ...font, font: textStyles.h4_.font }
  657. );
  658. blocks.warningText.alpha = 0;
  659. },
  660. renderCharacters: function () {
  661. self.kidAnimation = game.add.sprite(
  662. 100,
  663. context.canvas.height - 128 * 1.5,
  664. 'kid_standing',
  665. 0,
  666. 1.2
  667. );
  668. self.kidAnimation.anchor(0.5, 0.7);
  669. self.kidAnimation.curFrame = 3;
  670. },
  671. renderMainUI: () => {
  672. // Intro text
  673. const treatedMessage = game.lang.squareTwo_intro.split('\\n');
  674. const font = textStyles.h1_;
  675. self.ui.message = [];
  676. self.ui.message.push(
  677. game.add.text(
  678. context.canvas.width / 2,
  679. 170,
  680. treatedMessage[0] + '\n' + treatedMessage[1],
  681. font
  682. )
  683. );
  684. },
  685. renderOperationUI: () => {
  686. const uiList = [
  687. ...self.blocks.top.list,
  688. ...self.blocks.bottom.list,
  689. ...self.blocks.top.fractions,
  690. ...self.blocks.bottom.fractions,
  691. ];
  692. moveList(uiList, -400, 0);
  693. const font = textStyles.fraction;
  694. font.fill = colors.black;
  695. font.align = 'center';
  696. const nominators = [
  697. self.blocks.top.selectedAmount,
  698. self.blocks.bottom.selectedAmount,
  699. ];
  700. const denominators = [
  701. self.blocks.top.list.length,
  702. self.blocks.bottom.list.length,
  703. ];
  704. if (gameMode === 'b') {
  705. const leftNom = nominators[0];
  706. const leftDenom = denominators[0];
  707. nominators[0] = nominators[1];
  708. denominators[0] = denominators[1];
  709. nominators[1] = leftNom;
  710. denominators[1] = leftDenom;
  711. }
  712. const renderList = [];
  713. const padding = 100;
  714. const offsetX = 100;
  715. const cardHeight = 400;
  716. const x0 = padding + 400;
  717. const y0 = context.canvas.height / 2;
  718. let nextX = x0;
  719. const cardX = x0 - padding;
  720. const cardY = y0;
  721. // Card
  722. const card = game.add.geom.rect(
  723. cardX,
  724. cardY,
  725. 0,
  726. cardHeight,
  727. colors.blueLight,
  728. 0.5,
  729. colors.blueDark,
  730. 8
  731. );
  732. card.id = 'card';
  733. card.anchor(0, 0.5);
  734. renderList.push(card);
  735. renderList.push(
  736. game.add.text(
  737. nextX,
  738. y0,
  739. nominators[0] + '\n' + denominators[0],
  740. font,
  741. 70
  742. )
  743. );
  744. const topFractionLine = game.add.geom.rect(
  745. nextX,
  746. y0 + 10,
  747. 100,
  748. 4,
  749. colors.black,
  750. 4
  751. );
  752. topFractionLine.anchor(0.5, 0);
  753. renderList.push(topFractionLine);
  754. font.fill = self.control.isCorrect ? colors.green : colors.red;
  755. nextX += offsetX;
  756. renderList.push(
  757. game.add.text(nextX, y0 + 35, self.control.isCorrect ? '=' : '≠', font)
  758. );
  759. font.fill = colors.black;
  760. nextX += offsetX;
  761. renderList.push(
  762. game.add.text(
  763. nextX,
  764. y0,
  765. nominators[1] + '\n' + denominators[1],
  766. font,
  767. 70
  768. )
  769. );
  770. const bottomFractionLine = game.add.geom.rect(
  771. nextX,
  772. y0 + 10,
  773. 100,
  774. 4,
  775. colors.black,
  776. 4
  777. );
  778. bottomFractionLine.anchor(0.5, 0);
  779. renderList.push(bottomFractionLine);
  780. //let resultWidth = ''.length * widthOfChar;
  781. const cardWidth = nextX - x0 + padding * 2;
  782. card.width = cardWidth;
  783. const endSignX =
  784. (context.canvas.width - cardWidth) / 2 + cardWidth + 400 + 50;
  785. // Center Card
  786. moveList(renderList, (context.canvas.width - cardWidth) / 2, 0);
  787. self.fractionOperationUI = renderList;
  788. return endSignX;
  789. },
  790. renderExplanationUI: () => {
  791. const cx = context.canvas.width / 2;
  792. const cy = context.canvas.height / 2;
  793. const withNewlines = (s) =>
  794. s == null ? '' : String(s).replace(/\\n/g, '\n');
  795. // WRONG ANSWER: simple small card (no equation/bars)
  796. if (!self.control.isCorrect) {
  797. const errW = 680; const errH = 220;
  798. const errLeft = cx - errW / 2; const errTop = cy - errH / 2;
  799. const errBottom = errTop + errH;
  800. game.add.geom.rect(errLeft, errTop, errW, errH, colors.white, 0.97, colors.red, 4);
  801. game.add.text(cx, cy - 12,
  802. game.lang.s2_explain_body_wrong,
  803. { ...textStyles.h3_, fill: colors.red }
  804. );
  805. const btnW = 360; const btnH = 70; const btnCY = errBottom - 52;
  806. self.ui.explanation.button = game.add.geom.rect(
  807. cx - btnW / 2, btnCY - btnH / 2, btnW, btnH, colors.red
  808. );
  809. self.ui.explanation.text = game.add.text(cx, btnCY + 16, game.lang.retry, textStyles.btn);
  810. self.control.showExplanation = true;
  811. return;
  812. }
  813. // CORRECT ANSWER: full explanation card
  814. // Image is 1298×816 — all positions are relative to image coordinates
  815. const imgW = 1298; const imgH = 816;
  816. const cardLeft = cx - imgW / 2; // 311
  817. const cardTop = cy - imgH / 2; // 132
  818. const cardBottom = cardTop + imgH; // 948
  819. // 1. Background image + title text (image has the 🔍 icon, we add the text)
  820. game.add.image(cx, cy, gameMode === 'b' ? 'result-bg-multiply' : 'result-bg').anchor(0.5, 0.5);
  821. const titleText = withNewlines(game.lang.s2_explain_title);
  822. const titleY = cardTop + (titleText.includes('\n') ? 42 : 62);
  823. game.add.text(cx, titleY, titleText, {
  824. ...textStyles.h3_, fill: colors.blueDark, font: 'bold ' + textStyles.h3_.font,
  825. });
  826. // Fraction values
  827. let bigNum, bigDen, bigLineColor, bigFillColor;
  828. let smallNum, smallDen, smallLineColor, smallFillColor;
  829. if (gameMode !== 'b') {
  830. bigNum = self.blocks.top.selectedAmount; bigDen = self.blocks.top.list.length;
  831. bigLineColor = colors.blueDark; bigFillColor = colors.blue;
  832. smallNum = self.blocks.bottom.selectedAmount; smallDen = self.blocks.bottom.list.length;
  833. smallLineColor = colors.greenDark; smallFillColor = colors.green;
  834. } else {
  835. bigNum = self.blocks.bottom.selectedAmount; bigDen = self.blocks.bottom.list.length;
  836. bigLineColor = colors.greenDark; bigFillColor = colors.green;
  837. smallNum = self.blocks.top.selectedAmount; smallDen = self.blocks.top.list.length;
  838. smallLineColor = colors.blueDark; smallFillColor = colors.blue;
  839. }
  840. const factorRaw = (gameMode === 'b') ? smallDen / bigDen : bigDen / smallDen;
  841. const factor = Number.isInteger(factorRaw) ? factorRaw : parseFloat(factorRaw.toFixed(1));
  842. const fracStyle = { ...textStyles.fraction, fill: colors.blueDark, align: 'center' };
  843. // 2. Fraction numbers — placed over the image's fraction bar template
  844. // Image fraction bar LINE is at ~y=250 from image top; numY+32 should align with it
  845. // x centres measured from image: left≈24%, mid≈47%, right≈75%
  846. const numY = cardTop + 238; // numerator baseline
  847. const c1 = cardLeft + 315; // left fraction (≈ 626)
  848. const c3 = cardLeft + (gameMode === 'b' ? 635 : 610); // centre fraction (≈ 921)
  849. const c5 = cardLeft + 975; // right fraction (≈ 1286)
  850. game.add.text(c1, numY, bigNum + '\n' + bigDen, fracStyle, 65);
  851. game.add.text(c3, numY, factor + '\n' + factor, fracStyle, 65);
  852. game.add.text(c5, numY, smallNum + '\n' + smallDen, fracStyle, 65);
  853. // 3. Block bars + badge — centred around the image's star area
  854. // Star centre is ~49% of image width, ~51% of image height
  855. const badgeCx = cx; // star centre aligned to screen centre
  856. const barsTop = cardTop + 390; // ≈ 522
  857. const barH = 72; const barW = 270; const badgeW = 210; const barGap = 95;
  858. const leftBarLeft = badgeCx - badgeW / 2 - barGap - barW;
  859. const rightBarLeft = badgeCx + badgeW / 2 + barGap;
  860. const bwBig = barW / bigDen;
  861. for (let i = 0; i < bigDen; i++) {
  862. game.add.geom.rect(leftBarLeft + i * bwBig, barsTop, bwBig, barH,
  863. i < bigNum ? bigFillColor : colors.blueLight,
  864. i < bigNum ? 0.75 : 0.25, bigLineColor, 2);
  865. }
  866. const bwSmall = barW / smallDen;
  867. for (let i = 0; i < smallDen; i++) {
  868. game.add.geom.rect(rightBarLeft + i * bwSmall, barsTop, bwSmall, barH,
  869. i < smallNum ? smallFillColor : colors.greenLight,
  870. i < smallNum ? 0.75 : 0.25, smallLineColor, 2);
  871. }
  872. // Badge — image already draws the star, just overlay the text
  873. const badgeCenterY = barsTop + barH / 2;
  874. const badgeFontSize = Number.isInteger(factorRaw) ? 40 : 30;
  875. game.add.text(cx, badgeCenterY - 8, factor + '/' + factor + ' = 1', {
  876. ...textStyles.h4_, fill: colors.blueDark, font: `bold ${badgeFontSize}px Arial, sans-serif`,
  877. });
  878. // 4. Body text — below the star area, image body zone starts ~560px from image top
  879. const bodyKey = (gameMode === 'b') ? 's2_explain_body_correct_multiply' : 's2_explain_body_correct';
  880. const bodyParts = withNewlines(game.lang[bodyKey]).split('\n');
  881. const line1Raw = bodyParts[0];
  882. const wrapAt = 42;
  883. const wrapPos = line1Raw.lastIndexOf(' ', wrapAt);
  884. const line1 = (wrapPos > 0 && line1Raw.length > wrapAt)
  885. ? line1Raw.slice(0, wrapPos) + '\n' + line1Raw.slice(wrapPos + 1)
  886. : line1Raw;
  887. const line2 = (game.lang.s2_explain_body_factor_prefix) +
  888. ' ' + factor + '/' + factor + ' ' +
  889. (game.lang.s2_explain_body_factor_suffix);
  890. const line3 = bodyParts[1];
  891. const bodyStyle = { ...textStyles.p_, fill: colors.blueDark, font: 'bold 33px Arial, sans-serif' };
  892. const bodyStyleNormal = { ...textStyles.p_, fill: colors.blueDark, font: '33px Arial, sans-serif' };
  893. const bodyY = cardTop + 550;
  894. game.add.text(cx, bodyY, line1, bodyStyle, 38);
  895. game.add.text(cx, bodyY + 84, line2, bodyStyleNormal);
  896. game.add.text(cx, bodyY + 122, line3, bodyStyleNormal);
  897. // 5. Checkmark top-right
  898. const checkScale = imgH * 0.13 / 256;
  899. const checkImg = game.add.image(cardLeft + imgW - 16, cardTop + 16, 'answer_correct', checkScale);
  900. checkImg.anchor(1, 0);
  901. // 6. Continue button
  902. const btnW = 400; const btnH = 75; const btnCenterY = cardBottom - 62;
  903. self.ui.explanation.button = game.add.geom.rect(
  904. cx - btnW / 2, btnCenterY - btnH / 2, btnW, btnH, colors.green
  905. );
  906. self.ui.explanation.text = game.add.text(cx, btnCenterY + 16, game.lang.continue, textStyles.btn);
  907. self.control.showExplanation = true;
  908. },
  909. renderEndUI: () => {
  910. let btnColor = colors.green;
  911. let btnText = game.lang.continue;
  912. if (!self.control.isCorrect) {
  913. btnColor = colors.red;
  914. btnText = game.lang.retry;
  915. }
  916. // continue button
  917. self.ui.continue.button = game.add.geom.rect(
  918. context.canvas.width / 2 + 400,
  919. context.canvas.height / 2 + 280,
  920. 450,
  921. 100,
  922. btnColor
  923. );
  924. self.ui.continue.button.anchor(0.5, 0.5);
  925. self.ui.continue.text = game.add.text(
  926. context.canvas.width / 2 + 400,
  927. context.canvas.height / 2 + 16 + 280,
  928. btnText,
  929. textStyles.btn
  930. );
  931. },
  932. startDelayHandler: () => {
  933. game.timer.stop();
  934. self.control.animationDelay++;
  935. if (self.control.animationDelay === 50) {
  936. self.ui.message[0].alpha = 0;
  937. self.utils.checkAnswer();
  938. self.control.animationDelay = 0;
  939. self.control.startEndAnimation = true;
  940. }
  941. },
  942. // UPDATE
  943. moveBlocks: function () {
  944. ['top', 'bottom'].forEach((cur) => {
  945. if (self.blocks[cur].animate) {
  946. // Lower selected blocks
  947. for (let i = 0; i < self.blocks[cur].selectedAmount; i++) {
  948. self.blocks[cur].list[i].y += 2;
  949. }
  950. // After fully lowering blocks, set fraction value
  951. if (self.blocks[cur].list[0].y >= self.blocks[cur].auxBlocks[0].y) {
  952. self.blocks[cur].fractions[0].name =
  953. self.blocks[cur].selectedAmount +
  954. '\n' +
  955. self.blocks[cur].list.length;
  956. self.blocks[cur].animate = false;
  957. }
  958. }
  959. });
  960. },
  961. checkAnswer: function () {
  962. self.control.isCorrect =
  963. self.blocks.top.selectedAmount / self.blocks.top.list.length ==
  964. self.blocks.bottom.selectedAmount / self.blocks.bottom.list.length;
  965. if (self.control.isCorrect) {
  966. // Correct: hide blocks to make room for explanation card
  967. ['top', 'bottom'].forEach((s) => {
  968. self.blocks[s].list.forEach((b) => (b.alpha = 0));
  969. self.blocks[s].auxBlocks.forEach((b) => (b.alpha = 0));
  970. self.blocks[s].fractions.forEach((b) => { if (b) b.alpha = 0; });
  971. if (self.blocks[s].label) self.blocks[s].label.alpha = 0;
  972. if (self.blocks[s].warningText) self.blocks[s].warningText.alpha = 0;
  973. });
  974. if (audioStatus) game.audio.okSound.play();
  975. completedLevels++;
  976. if (isDebugMode) console.log('Completed Levels: ' + completedLevels);
  977. } else {
  978. // Wrong: keep main blocks visible but hide aux blocks (they don't shift with renderOperationUI)
  979. ['top', 'bottom'].forEach((s) => {
  980. self.blocks[s].auxBlocks.forEach((b) => (b.alpha = 0));
  981. });
  982. if (audioStatus) game.audio.errorSound.play();
  983. }
  984. self.fetch.postScore();
  985. },
  986. runEndAnimation: function () {
  987. self.control.animationDelay++;
  988. if (self.control.animationDelay === 1) {
  989. if (self.control.isCorrect) {
  990. self.utils.renderExplanationUI();
  991. } else {
  992. const x = self.utils.renderOperationUI();
  993. game.add.image(x, context.canvas.height / 3, 'answer_wrong').anchor(0.5, 0.5);
  994. self.utils.renderEndUI();
  995. self.control.showEndInfo = true;
  996. }
  997. }
  998. },
  999. endLevel: function () {
  1000. game.state.start('map');
  1001. },
  1002. // HANDLERS
  1003. /**
  1004. * Function called by self.onInputDown() when player clicked a valid rectangle.
  1005. *
  1006. * @param {object} curBlock clicked rectangle : can be self.blocks.top.list[i] or self.blocks.bottom.list[i]
  1007. */
  1008. clickSquareHandler: function (curBlock) {
  1009. const curSet = curBlock.position;
  1010. if (
  1011. !self.blocks[curSet].hasClicked &&
  1012. curBlock.index != self.blocks[curSet].list.length - 1
  1013. ) {
  1014. document.body.style.cursor = 'auto';
  1015. // Turn auxiliar blocks invisible
  1016. for (let i in self.blocks[curSet].list) {
  1017. if (i > curBlock.index) self.blocks[curSet].auxBlocks[i].alpha = 0;
  1018. }
  1019. // Turn value label invisible
  1020. self.blocks[curSet].label.alpha = 0;
  1021. if (audioStatus) game.audio.popSound.play();
  1022. // Save number of selected blocks
  1023. self.blocks[curSet].selectedAmount = curBlock.index + 1;
  1024. // Set fraction x position
  1025. const newX =
  1026. curBlock.finalX +
  1027. self.blocks[curSet].selectedAmount *
  1028. (self.control.blockWidth / self.blocks[curSet].list.length) +
  1029. 40;
  1030. self.blocks[curSet].fractions[0].x = newX;
  1031. self.blocks[curSet].fractions[1].x = newX;
  1032. self.blocks[curSet].fractions[0].name = `${curBlock.index + 1}\n${
  1033. self.blocks[curSet].list.length
  1034. }`;
  1035. // End fraction line
  1036. self.blocks[curSet].fractions[1].alpha = showFractions ? 1 : 0;
  1037. self.blocks[curSet].hasClicked = true; // Inform player have clicked in current block set
  1038. self.blocks[curSet].animate = true; // Let it initiate animation
  1039. }
  1040. game.render.all();
  1041. },
  1042. /**
  1043. * Function called by self.onInputOver() when cursor is over a valid rectangle.
  1044. *
  1045. * @param {object} curBlock rectangle the cursor is over : can be self.blocks.top.list[i] or self.blocks.bottom.list[i]
  1046. */
  1047. overSquareHandler: function (curBlock) {
  1048. const curSet = curBlock.position;
  1049. if (!self.blocks[curSet].hasClicked) {
  1050. // self.blocks.top.hasClicked || self.blocks.bottom.hasClicked
  1051. // If over fraction 'n/n' shows warning message not allowing it
  1052. if (curBlock.index == self.blocks[curSet].list.length - 1) {
  1053. const otherSet = curSet == 'top' ? 'bottom' : 'top';
  1054. self.blocks[curSet].warningText.alpha = 1;
  1055. self.blocks[otherSet].warningText.alpha = 0;
  1056. self.utils.outSquareHandler(curSet);
  1057. } else {
  1058. document.body.style.cursor = 'pointer';
  1059. self.blocks.top.warningText.alpha = 0;
  1060. self.blocks.bottom.warningText.alpha = 0;
  1061. // Selected blocks become fully visible
  1062. for (let i in self.blocks[curSet].list) {
  1063. self.blocks[curSet].list[i].alpha = i <= curBlock.index ? 1 : 0.5;
  1064. }
  1065. self.blocks[curSet].fractions[0].name = curBlock.index + 1; // Nominator : selected blocks
  1066. const newX =
  1067. curBlock.finalX +
  1068. (curBlock.index + 1) *
  1069. (self.control.blockWidth / self.blocks[curSet].list.length) +
  1070. 25;
  1071. self.blocks[curSet].fractions[0].x = newX;
  1072. self.blocks[curSet].fractions[1].x = newX;
  1073. // End fraction nominator and denominator
  1074. self.blocks[curSet].fractions[0].alpha = showFractions ? 1 : 0;
  1075. }
  1076. }
  1077. },
  1078. /**
  1079. * Function called (by self.onInputOver() and self.utils.overSquareHandler()) when cursor is out of a valid rectangle.
  1080. *
  1081. * @param {object} curSet set of rectangles : can be top (self.blocks.top) or bottom (self.blocks.bottom)
  1082. */
  1083. outSquareHandler: function (curSet) {
  1084. if (!self.blocks[curSet].hasClicked) {
  1085. self.blocks[curSet].fractions[0].alpha = 0;
  1086. self.blocks[curSet].fractions[1].alpha = 0;
  1087. self.blocks[curSet].list.forEach((cur) => {
  1088. cur.alpha = 0.5;
  1089. });
  1090. }
  1091. },
  1092. },
  1093. events: {
  1094. /**
  1095. * Called by mouse click event
  1096. *
  1097. * @param {object} mouseEvent contains the mouse click coordinates
  1098. */
  1099. onInputDown: function (mouseEvent) {
  1100. const x = game.math.getMouse(mouseEvent).x;
  1101. const y = game.math.getMouse(mouseEvent).y;
  1102. // Pre-start screen: only handle SIM/NÃO + navigation
  1103. if (!self.control.started) {
  1104. if (
  1105. self.ui.startChoice &&
  1106. self.ui.startChoice.yes &&
  1107. self.ui.startChoice.yes.button &&
  1108. game.math.isOverIcon(x, y, self.ui.startChoice.yes.button)
  1109. ) {
  1110. if (audioStatus) game.audio.popSound.play();
  1111. self.control.challengeAnsweredYes = true;
  1112. self.utils.updateChallengeChoiceUI();
  1113. self.utils.startGame();
  1114. return;
  1115. }
  1116. if (
  1117. self.ui.startChoice &&
  1118. self.ui.startChoice.no &&
  1119. self.ui.startChoice.no.button &&
  1120. game.math.isOverIcon(x, y, self.ui.startChoice.no.button)
  1121. ) {
  1122. if (audioStatus) game.audio.popSound.play();
  1123. self.control.challengeAnsweredYes = false;
  1124. self.utils.updateChallengeChoiceUI();
  1125. self.utils.startGame();
  1126. return;
  1127. }
  1128. navigation.onInputDown(x, y);
  1129. game.render.all();
  1130. return;
  1131. }
  1132. // Click block in (a)
  1133. self.blocks.top.list.forEach((cur) => {
  1134. if (game.math.isOverIcon(x, y, cur)) self.utils.clickSquareHandler(cur);
  1135. });
  1136. // Click block in (b)
  1137. self.blocks.bottom.list.forEach((cur) => {
  1138. if (game.math.isOverIcon(x, y, cur)) self.utils.clickSquareHandler(cur);
  1139. });
  1140. // Explanation screen continue/retry button
  1141. if (self.control.showExplanation) {
  1142. if (game.math.isOverIcon(x, y, self.ui.explanation.button)) {
  1143. if (audioStatus) game.audio.popSound.play();
  1144. canGoToNextMapPosition = self.control.isCorrect;
  1145. self.utils.endLevel();
  1146. }
  1147. }
  1148. // Continue button
  1149. if (self.control.showEndInfo) {
  1150. if (game.math.isOverIcon(x, y, self.ui.continue.button)) {
  1151. if (audioStatus) game.audio.popSound.play();
  1152. self.utils.endLevel();
  1153. }
  1154. }
  1155. // Click navigation icons
  1156. navigation.onInputDown(x, y);
  1157. game.render.all();
  1158. },
  1159. /**
  1160. * Called by mouse move event
  1161. *
  1162. * @param {object} mouseEvent contains the mouse move coordinates
  1163. */
  1164. onInputOver: function (mouseEvent) {
  1165. const x = game.math.getMouse(mouseEvent).x;
  1166. const y = game.math.getMouse(mouseEvent).y;
  1167. // Pre-start screen hover: SIM/NÃO + navigation
  1168. if (!self.control.started) {
  1169. let isOverChoice = false;
  1170. const choiceTextStyle = { ...textStyles.h3_, fill: colors.blueDark, font: 'bold ' + textStyles.h3_.font };
  1171. const yesBtn = self.ui.startChoice && self.ui.startChoice.yes && self.ui.startChoice.yes.button;
  1172. const noBtn = self.ui.startChoice && self.ui.startChoice.no && self.ui.startChoice.no.button;
  1173. const yesVisual = self.ui.startChoice && self.ui.startChoice.yes && self.ui.startChoice.yes.visual;
  1174. const noVisual = self.ui.startChoice && self.ui.startChoice.no && self.ui.startChoice.no.visual;
  1175. if (yesBtn && game.math.isOverIcon(x, y, yesBtn)) {
  1176. isOverChoice = true;
  1177. document.body.style.cursor = 'pointer';
  1178. if (self.ui.startChoice.yes.text) self.ui.startChoice.yes.text.style = choiceTextStyle;
  1179. if (yesVisual && yesVisual.outerParts && yesVisual.innerParts) {
  1180. yesVisual.outerParts.forEach((p) => { p.shadowBlur = 10; });
  1181. yesVisual.innerParts.forEach((p) => { p.fillColor = '#e8f0fc'; p.lineColor = '#e8f0fc'; });
  1182. }
  1183. } else if (yesBtn) {
  1184. if (self.ui.startChoice.yes.text) self.ui.startChoice.yes.text.style = choiceTextStyle;
  1185. if (yesVisual && yesVisual.outerParts && yesVisual.innerParts) {
  1186. yesVisual.outerParts.forEach((p) => { p.shadowBlur = 8; });
  1187. yesVisual.innerParts.forEach((p) => { p.fillColor = colors.white; p.lineColor = colors.white; });
  1188. }
  1189. }
  1190. if (noBtn && game.math.isOverIcon(x, y, noBtn)) {
  1191. isOverChoice = true;
  1192. document.body.style.cursor = 'pointer';
  1193. if (self.ui.startChoice.no.text) self.ui.startChoice.no.text.style = choiceTextStyle;
  1194. if (noVisual && noVisual.outerParts && noVisual.innerParts) {
  1195. noVisual.outerParts.forEach((p) => { p.shadowBlur = 10; });
  1196. noVisual.innerParts.forEach((p) => { p.fillColor = '#e8f0fc'; p.lineColor = '#e8f0fc'; });
  1197. }
  1198. } else if (noBtn) {
  1199. if (self.ui.startChoice.no.text) self.ui.startChoice.no.text.style = choiceTextStyle;
  1200. if (noVisual && noVisual.outerParts && noVisual.innerParts) {
  1201. noVisual.outerParts.forEach((p) => { p.shadowBlur = 8; });
  1202. noVisual.innerParts.forEach((p) => { p.fillColor = colors.white; p.lineColor = colors.white; });
  1203. }
  1204. }
  1205. if (!isOverChoice) document.body.style.cursor = 'auto';
  1206. navigation.onInputOver(x, y);
  1207. game.render.all();
  1208. return;
  1209. }
  1210. let flagA = false;
  1211. let flagB = false;
  1212. // Mouse over (a) : show fraction
  1213. self.blocks.top.list.forEach((cur) => {
  1214. if (game.math.isOverIcon(x, y, cur)) {
  1215. flagA = true;
  1216. self.utils.overSquareHandler(cur);
  1217. }
  1218. });
  1219. if (!flagA) self.utils.outSquareHandler('top');
  1220. // Mouse over (b) : show fraction
  1221. self.blocks.bottom.list.forEach((cur) => {
  1222. if (game.math.isOverIcon(x, y, cur)) {
  1223. flagB = true;
  1224. self.utils.overSquareHandler(cur);
  1225. }
  1226. });
  1227. if (!flagB) self.utils.outSquareHandler('bottom');
  1228. if (!flagA && !flagB) document.body.style.cursor = 'auto';
  1229. // Explanation button hover
  1230. if (self.control.showExplanation && self.ui.explanation.button) {
  1231. if (game.math.isOverIcon(x, y, self.ui.explanation.button)) {
  1232. document.body.style.cursor = 'pointer';
  1233. self.ui.explanation.button.scale = self.ui.explanation.button.initialScale * 1.1;
  1234. self.ui.explanation.text.style = textStyles.btnLg;
  1235. } else {
  1236. self.ui.explanation.button.scale = self.ui.explanation.button.initialScale * 1;
  1237. self.ui.explanation.text.style = textStyles.btn;
  1238. }
  1239. }
  1240. // Continue button
  1241. if (self.control.showEndInfo) {
  1242. if (game.math.isOverIcon(x, y, self.ui.continue.button)) {
  1243. // If pointer is over icon
  1244. document.body.style.cursor = 'pointer';
  1245. self.ui.continue.button.scale =
  1246. self.ui.continue.button.initialScale * 1.1;
  1247. self.ui.continue.text.style = textStyles.btnLg;
  1248. } else {
  1249. // If pointer is not over icon
  1250. document.body.style.cursor = 'auto';
  1251. self.ui.continue.button.scale =
  1252. self.ui.continue.button.initialScale * 1;
  1253. self.ui.continue.text.style = textStyles.btn;
  1254. }
  1255. }
  1256. // Mouse over navigation icons : show name
  1257. navigation.onInputOver(x, y);
  1258. game.render.all();
  1259. },
  1260. },
  1261. fetch: {
  1262. /**
  1263. * Saves players data after level ends - to be sent to database. <br>
  1264. *
  1265. * Attention: the 'line_' prefix data table must be compatible to data table fields (MySQL server)
  1266. *
  1267. * @see /php/save.php
  1268. */
  1269. postScore: function () {
  1270. // Creates string that is going to be sent to db
  1271. const data =
  1272. '&line_game=' +
  1273. gameShape +
  1274. '&line_mode=' +
  1275. gameMode +
  1276. '&line_oper=equal' +
  1277. '&line_leve=' +
  1278. gameDifficulty +
  1279. '&line_posi=' +
  1280. curMapPosition +
  1281. '&line_resu=' +
  1282. self.control.isCorrect +
  1283. '&line_time=' +
  1284. game.timer.elapsed +
  1285. '&line_deta=' +
  1286. 'numBlocksA: ' +
  1287. self.blocks.top.list.length +
  1288. ', valueA: ' +
  1289. self.blocks.top.selectedAmount +
  1290. ', numBlocksB: ' +
  1291. self.blocks.bottom.list.length +
  1292. ', valueB: ' +
  1293. self.blocks.bottom.selectedAmount +
  1294. '&challenge_answered_yes=' +
  1295. self.control.challengeAnsweredYes;
  1296. // FOR MOODLE
  1297. sendToDatabase(data);
  1298. },
  1299. },
  1300. };