squareTwo.js 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446
  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. Continue button
  898. const btnW = 400; const btnH = 75; const btnCenterY = cardBottom - 62;
  899. self.ui.explanation.button = game.add.geom.rect(
  900. cx - btnW / 2, btnCenterY - btnH / 2, btnW, btnH, colors.green
  901. );
  902. self.ui.explanation.text = game.add.text(cx, btnCenterY + 16, game.lang.continue, textStyles.btn);
  903. self.control.showExplanation = true;
  904. },
  905. renderEndUI: () => {
  906. let btnColor = colors.green;
  907. let btnText = game.lang.continue;
  908. if (!self.control.isCorrect) {
  909. btnColor = colors.red;
  910. btnText = game.lang.retry;
  911. }
  912. // continue button
  913. self.ui.continue.button = game.add.geom.rect(
  914. context.canvas.width / 2 + 400,
  915. context.canvas.height / 2 + 280,
  916. 450,
  917. 100,
  918. btnColor
  919. );
  920. self.ui.continue.button.anchor(0.5, 0.5);
  921. self.ui.continue.text = game.add.text(
  922. context.canvas.width / 2 + 400,
  923. context.canvas.height / 2 + 16 + 280,
  924. btnText,
  925. textStyles.btn
  926. );
  927. },
  928. startDelayHandler: () => {
  929. game.timer.stop();
  930. self.control.animationDelay++;
  931. if (self.control.animationDelay === 50) {
  932. self.ui.message[0].alpha = 0;
  933. self.utils.checkAnswer();
  934. self.control.animationDelay = 0;
  935. self.control.startEndAnimation = true;
  936. }
  937. },
  938. // UPDATE
  939. moveBlocks: function () {
  940. ['top', 'bottom'].forEach((cur) => {
  941. if (self.blocks[cur].animate) {
  942. // Lower selected blocks
  943. for (let i = 0; i < self.blocks[cur].selectedAmount; i++) {
  944. self.blocks[cur].list[i].y += 2;
  945. }
  946. // After fully lowering blocks, set fraction value
  947. if (self.blocks[cur].list[0].y >= self.blocks[cur].auxBlocks[0].y) {
  948. self.blocks[cur].fractions[0].name =
  949. self.blocks[cur].selectedAmount +
  950. '\n' +
  951. self.blocks[cur].list.length;
  952. self.blocks[cur].animate = false;
  953. }
  954. }
  955. });
  956. },
  957. checkAnswer: function () {
  958. self.control.isCorrect =
  959. self.blocks.top.selectedAmount / self.blocks.top.list.length ==
  960. self.blocks.bottom.selectedAmount / self.blocks.bottom.list.length;
  961. if (self.control.isCorrect) {
  962. // Correct: hide blocks to make room for explanation card
  963. ['top', 'bottom'].forEach((s) => {
  964. self.blocks[s].list.forEach((b) => (b.alpha = 0));
  965. self.blocks[s].auxBlocks.forEach((b) => (b.alpha = 0));
  966. self.blocks[s].fractions.forEach((b) => { if (b) b.alpha = 0; });
  967. if (self.blocks[s].label) self.blocks[s].label.alpha = 0;
  968. if (self.blocks[s].warningText) self.blocks[s].warningText.alpha = 0;
  969. });
  970. if (audioStatus) game.audio.okSound.play();
  971. completedLevels++;
  972. if (isDebugMode) console.log('Completed Levels: ' + completedLevels);
  973. } else {
  974. // Wrong: keep main blocks visible but hide aux blocks (they don't shift with renderOperationUI)
  975. ['top', 'bottom'].forEach((s) => {
  976. self.blocks[s].auxBlocks.forEach((b) => (b.alpha = 0));
  977. });
  978. if (audioStatus) game.audio.errorSound.play();
  979. }
  980. self.fetch.postScore();
  981. },
  982. runEndAnimation: function () {
  983. self.control.animationDelay++;
  984. if (self.control.animationDelay === 1) {
  985. if (self.control.isCorrect) {
  986. self.utils.renderExplanationUI();
  987. } else {
  988. const x = self.utils.renderOperationUI();
  989. game.add.image(x, context.canvas.height / 3, 'answer_wrong').anchor(0.5, 0.5);
  990. self.utils.renderEndUI();
  991. self.control.showEndInfo = true;
  992. }
  993. }
  994. },
  995. endLevel: function () {
  996. game.state.start('map');
  997. },
  998. // HANDLERS
  999. /**
  1000. * Function called by self.onInputDown() when player clicked a valid rectangle.
  1001. *
  1002. * @param {object} curBlock clicked rectangle : can be self.blocks.top.list[i] or self.blocks.bottom.list[i]
  1003. */
  1004. clickSquareHandler: function (curBlock) {
  1005. const curSet = curBlock.position;
  1006. if (
  1007. !self.blocks[curSet].hasClicked &&
  1008. curBlock.index != self.blocks[curSet].list.length - 1
  1009. ) {
  1010. document.body.style.cursor = 'auto';
  1011. // Turn auxiliar blocks invisible
  1012. for (let i in self.blocks[curSet].list) {
  1013. if (i > curBlock.index) self.blocks[curSet].auxBlocks[i].alpha = 0;
  1014. }
  1015. // Turn value label invisible
  1016. self.blocks[curSet].label.alpha = 0;
  1017. if (audioStatus) game.audio.popSound.play();
  1018. // Save number of selected blocks
  1019. self.blocks[curSet].selectedAmount = curBlock.index + 1;
  1020. // Set fraction x position
  1021. const newX =
  1022. curBlock.finalX +
  1023. self.blocks[curSet].selectedAmount *
  1024. (self.control.blockWidth / self.blocks[curSet].list.length) +
  1025. 40;
  1026. self.blocks[curSet].fractions[0].x = newX;
  1027. self.blocks[curSet].fractions[1].x = newX;
  1028. self.blocks[curSet].fractions[0].name = `${curBlock.index + 1}\n${
  1029. self.blocks[curSet].list.length
  1030. }`;
  1031. // End fraction line
  1032. self.blocks[curSet].fractions[1].alpha = showFractions ? 1 : 0;
  1033. self.blocks[curSet].hasClicked = true; // Inform player have clicked in current block set
  1034. self.blocks[curSet].animate = true; // Let it initiate animation
  1035. }
  1036. game.render.all();
  1037. },
  1038. /**
  1039. * Function called by self.onInputOver() when cursor is over a valid rectangle.
  1040. *
  1041. * @param {object} curBlock rectangle the cursor is over : can be self.blocks.top.list[i] or self.blocks.bottom.list[i]
  1042. */
  1043. overSquareHandler: function (curBlock) {
  1044. const curSet = curBlock.position;
  1045. if (!self.blocks[curSet].hasClicked) {
  1046. // self.blocks.top.hasClicked || self.blocks.bottom.hasClicked
  1047. // If over fraction 'n/n' shows warning message not allowing it
  1048. if (curBlock.index == self.blocks[curSet].list.length - 1) {
  1049. const otherSet = curSet == 'top' ? 'bottom' : 'top';
  1050. self.blocks[curSet].warningText.alpha = 1;
  1051. self.blocks[otherSet].warningText.alpha = 0;
  1052. self.utils.outSquareHandler(curSet);
  1053. } else {
  1054. document.body.style.cursor = 'pointer';
  1055. self.blocks.top.warningText.alpha = 0;
  1056. self.blocks.bottom.warningText.alpha = 0;
  1057. // Selected blocks become fully visible
  1058. for (let i in self.blocks[curSet].list) {
  1059. self.blocks[curSet].list[i].alpha = i <= curBlock.index ? 1 : 0.5;
  1060. }
  1061. self.blocks[curSet].fractions[0].name = curBlock.index + 1; // Nominator : selected blocks
  1062. const newX =
  1063. curBlock.finalX +
  1064. (curBlock.index + 1) *
  1065. (self.control.blockWidth / self.blocks[curSet].list.length) +
  1066. 25;
  1067. self.blocks[curSet].fractions[0].x = newX;
  1068. self.blocks[curSet].fractions[1].x = newX;
  1069. // End fraction nominator and denominator
  1070. self.blocks[curSet].fractions[0].alpha = showFractions ? 1 : 0;
  1071. }
  1072. }
  1073. },
  1074. /**
  1075. * Function called (by self.onInputOver() and self.utils.overSquareHandler()) when cursor is out of a valid rectangle.
  1076. *
  1077. * @param {object} curSet set of rectangles : can be top (self.blocks.top) or bottom (self.blocks.bottom)
  1078. */
  1079. outSquareHandler: function (curSet) {
  1080. if (!self.blocks[curSet].hasClicked) {
  1081. self.blocks[curSet].fractions[0].alpha = 0;
  1082. self.blocks[curSet].fractions[1].alpha = 0;
  1083. self.blocks[curSet].list.forEach((cur) => {
  1084. cur.alpha = 0.5;
  1085. });
  1086. }
  1087. },
  1088. },
  1089. events: {
  1090. /**
  1091. * Called by mouse click event
  1092. *
  1093. * @param {object} mouseEvent contains the mouse click coordinates
  1094. */
  1095. onInputDown: function (mouseEvent) {
  1096. const x = game.math.getMouse(mouseEvent).x;
  1097. const y = game.math.getMouse(mouseEvent).y;
  1098. // Pre-start screen: only handle SIM/NÃO + navigation
  1099. if (!self.control.started) {
  1100. if (
  1101. self.ui.startChoice &&
  1102. self.ui.startChoice.yes &&
  1103. self.ui.startChoice.yes.button &&
  1104. game.math.isOverIcon(x, y, self.ui.startChoice.yes.button)
  1105. ) {
  1106. if (audioStatus) game.audio.popSound.play();
  1107. self.control.challengeAnsweredYes = true;
  1108. self.utils.updateChallengeChoiceUI();
  1109. self.utils.startGame();
  1110. return;
  1111. }
  1112. if (
  1113. self.ui.startChoice &&
  1114. self.ui.startChoice.no &&
  1115. self.ui.startChoice.no.button &&
  1116. game.math.isOverIcon(x, y, self.ui.startChoice.no.button)
  1117. ) {
  1118. if (audioStatus) game.audio.popSound.play();
  1119. self.control.challengeAnsweredYes = false;
  1120. self.utils.updateChallengeChoiceUI();
  1121. self.utils.startGame();
  1122. return;
  1123. }
  1124. navigation.onInputDown(x, y);
  1125. game.render.all();
  1126. return;
  1127. }
  1128. // Click block in (a)
  1129. self.blocks.top.list.forEach((cur) => {
  1130. if (game.math.isOverIcon(x, y, cur)) self.utils.clickSquareHandler(cur);
  1131. });
  1132. // Click block in (b)
  1133. self.blocks.bottom.list.forEach((cur) => {
  1134. if (game.math.isOverIcon(x, y, cur)) self.utils.clickSquareHandler(cur);
  1135. });
  1136. // Explanation screen continue/retry button
  1137. if (self.control.showExplanation) {
  1138. if (game.math.isOverIcon(x, y, self.ui.explanation.button)) {
  1139. if (audioStatus) game.audio.popSound.play();
  1140. canGoToNextMapPosition = self.control.isCorrect;
  1141. self.utils.endLevel();
  1142. }
  1143. }
  1144. // Continue button
  1145. if (self.control.showEndInfo) {
  1146. if (game.math.isOverIcon(x, y, self.ui.continue.button)) {
  1147. if (audioStatus) game.audio.popSound.play();
  1148. self.utils.endLevel();
  1149. }
  1150. }
  1151. // Click navigation icons
  1152. navigation.onInputDown(x, y);
  1153. game.render.all();
  1154. },
  1155. /**
  1156. * Called by mouse move event
  1157. *
  1158. * @param {object} mouseEvent contains the mouse move coordinates
  1159. */
  1160. onInputOver: function (mouseEvent) {
  1161. const x = game.math.getMouse(mouseEvent).x;
  1162. const y = game.math.getMouse(mouseEvent).y;
  1163. // Pre-start screen hover: SIM/NÃO + navigation
  1164. if (!self.control.started) {
  1165. let isOverChoice = false;
  1166. const choiceTextStyle = { ...textStyles.h3_, fill: colors.blueDark, font: 'bold ' + textStyles.h3_.font };
  1167. const yesBtn = self.ui.startChoice && self.ui.startChoice.yes && self.ui.startChoice.yes.button;
  1168. const noBtn = self.ui.startChoice && self.ui.startChoice.no && self.ui.startChoice.no.button;
  1169. const yesVisual = self.ui.startChoice && self.ui.startChoice.yes && self.ui.startChoice.yes.visual;
  1170. const noVisual = self.ui.startChoice && self.ui.startChoice.no && self.ui.startChoice.no.visual;
  1171. if (yesBtn && game.math.isOverIcon(x, y, yesBtn)) {
  1172. isOverChoice = true;
  1173. document.body.style.cursor = 'pointer';
  1174. if (self.ui.startChoice.yes.text) self.ui.startChoice.yes.text.style = choiceTextStyle;
  1175. if (yesVisual && yesVisual.outerParts && yesVisual.innerParts) {
  1176. yesVisual.outerParts.forEach((p) => { p.shadowBlur = 10; });
  1177. yesVisual.innerParts.forEach((p) => { p.fillColor = '#e8f0fc'; p.lineColor = '#e8f0fc'; });
  1178. }
  1179. } else if (yesBtn) {
  1180. if (self.ui.startChoice.yes.text) self.ui.startChoice.yes.text.style = choiceTextStyle;
  1181. if (yesVisual && yesVisual.outerParts && yesVisual.innerParts) {
  1182. yesVisual.outerParts.forEach((p) => { p.shadowBlur = 8; });
  1183. yesVisual.innerParts.forEach((p) => { p.fillColor = colors.white; p.lineColor = colors.white; });
  1184. }
  1185. }
  1186. if (noBtn && game.math.isOverIcon(x, y, noBtn)) {
  1187. isOverChoice = true;
  1188. document.body.style.cursor = 'pointer';
  1189. if (self.ui.startChoice.no.text) self.ui.startChoice.no.text.style = choiceTextStyle;
  1190. if (noVisual && noVisual.outerParts && noVisual.innerParts) {
  1191. noVisual.outerParts.forEach((p) => { p.shadowBlur = 10; });
  1192. noVisual.innerParts.forEach((p) => { p.fillColor = '#e8f0fc'; p.lineColor = '#e8f0fc'; });
  1193. }
  1194. } else if (noBtn) {
  1195. if (self.ui.startChoice.no.text) self.ui.startChoice.no.text.style = choiceTextStyle;
  1196. if (noVisual && noVisual.outerParts && noVisual.innerParts) {
  1197. noVisual.outerParts.forEach((p) => { p.shadowBlur = 8; });
  1198. noVisual.innerParts.forEach((p) => { p.fillColor = colors.white; p.lineColor = colors.white; });
  1199. }
  1200. }
  1201. if (!isOverChoice) document.body.style.cursor = 'auto';
  1202. navigation.onInputOver(x, y);
  1203. game.render.all();
  1204. return;
  1205. }
  1206. let flagA = false;
  1207. let flagB = false;
  1208. // Mouse over (a) : show fraction
  1209. self.blocks.top.list.forEach((cur) => {
  1210. if (game.math.isOverIcon(x, y, cur)) {
  1211. flagA = true;
  1212. self.utils.overSquareHandler(cur);
  1213. }
  1214. });
  1215. if (!flagA) self.utils.outSquareHandler('top');
  1216. // Mouse over (b) : show fraction
  1217. self.blocks.bottom.list.forEach((cur) => {
  1218. if (game.math.isOverIcon(x, y, cur)) {
  1219. flagB = true;
  1220. self.utils.overSquareHandler(cur);
  1221. }
  1222. });
  1223. if (!flagB) self.utils.outSquareHandler('bottom');
  1224. if (!flagA && !flagB) document.body.style.cursor = 'auto';
  1225. // Explanation button hover
  1226. if (self.control.showExplanation && self.ui.explanation.button) {
  1227. if (game.math.isOverIcon(x, y, self.ui.explanation.button)) {
  1228. document.body.style.cursor = 'pointer';
  1229. self.ui.explanation.button.scale = self.ui.explanation.button.initialScale * 1.1;
  1230. self.ui.explanation.text.style = textStyles.btnLg;
  1231. } else {
  1232. self.ui.explanation.button.scale = self.ui.explanation.button.initialScale * 1;
  1233. self.ui.explanation.text.style = textStyles.btn;
  1234. }
  1235. }
  1236. // Continue button
  1237. if (self.control.showEndInfo) {
  1238. if (game.math.isOverIcon(x, y, self.ui.continue.button)) {
  1239. // If pointer is over icon
  1240. document.body.style.cursor = 'pointer';
  1241. self.ui.continue.button.scale =
  1242. self.ui.continue.button.initialScale * 1.1;
  1243. self.ui.continue.text.style = textStyles.btnLg;
  1244. } else {
  1245. // If pointer is not over icon
  1246. document.body.style.cursor = 'auto';
  1247. self.ui.continue.button.scale =
  1248. self.ui.continue.button.initialScale * 1;
  1249. self.ui.continue.text.style = textStyles.btn;
  1250. }
  1251. }
  1252. // Mouse over navigation icons : show name
  1253. navigation.onInputOver(x, y);
  1254. game.render.all();
  1255. },
  1256. },
  1257. fetch: {
  1258. /**
  1259. * Saves players data after level ends - to be sent to database. <br>
  1260. *
  1261. * Attention: the 'line_' prefix data table must be compatible to data table fields (MySQL server)
  1262. *
  1263. * @see /php/save.php
  1264. */
  1265. postScore: function () {
  1266. // Creates string that is going to be sent to db
  1267. const data =
  1268. '&line_game=' +
  1269. gameShape +
  1270. '&line_mode=' +
  1271. gameMode +
  1272. '&line_oper=equal' +
  1273. '&line_leve=' +
  1274. gameDifficulty +
  1275. '&line_posi=' +
  1276. curMapPosition +
  1277. '&line_resu=' +
  1278. self.control.isCorrect +
  1279. '&line_time=' +
  1280. game.timer.elapsed +
  1281. '&line_deta=' +
  1282. 'numBlocksA: ' +
  1283. self.blocks.top.list.length +
  1284. ', valueA: ' +
  1285. self.blocks.top.selectedAmount +
  1286. ', numBlocksB: ' +
  1287. self.blocks.bottom.list.length +
  1288. ', valueB: ' +
  1289. self.blocks.bottom.selectedAmount +
  1290. '&challenge_answered_yes=' +
  1291. self.control.challengeAnsweredYes;
  1292. // FOR MOODLE
  1293. sendToDatabase(data);
  1294. },
  1295. },
  1296. };