squareOne.js 54 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575
  1. /******************************
  2. * This file holds game states.
  3. ******************************/
  4. /** [GAME STATE]
  5. *
  6. * ..squareOne... = gameName
  7. * ..../...\.....
  8. * ...a.....b.... = gameMode
  9. * .....\./......
  10. * ......|.......
  11. * ...../.\......
  12. * .plus...minus. = gameOperation
  13. * .....\./......
  14. * ......|.......
  15. * ....1,2,3..... = gameDifficulty
  16. *
  17. * Character : tractor
  18. * Theme : farm
  19. * Concept : Player associates 'blocks carried by the tractor' and 'floor spaces to be filled by them'
  20. * Represent fractions as : blocks/rectangles
  21. *
  22. * Game modes can be :
  23. *
  24. * a : Player can select # of 'floor blocks' (hole in the ground)
  25. * Selects size of hole to be made in the ground (to fill with the blocks in front of the truck)
  26. * b : Player can select # of 'stacked blocks' (in front of the truck)
  27. * Selects number of blocks in front of the truck (to fill the hole on the ground)
  28. *
  29. * Operations can be :
  30. *
  31. * plus : addition of fractions
  32. * Represented by : tractor going to the right (floor positions 0..8)
  33. * minus : subtraction of fractions
  34. * Represented by: tractor going to the left (floor positions 8..0)
  35. *
  36. * @namespace
  37. */
  38. const squareOne = {
  39. control: undefined, // level related data
  40. default: undefined, // level related default values
  41. ui: undefined, // graphic elements
  42. animation: undefined, // animation control
  43. tractor: undefined,
  44. stack: undefined, // stack blocks info
  45. floor: undefined, // floor blocks info
  46. /**
  47. * Main code
  48. */
  49. create: function () {
  50. const truckWidth = 201;
  51. const divisor = gameDifficulty == 3 ? 4 : gameDifficulty; // Make sure valid divisors are 1, 2 and 4 (not 3)
  52. let lineColor = undefined;
  53. let fillColor = undefined;
  54. if (gameOperation === 'minus') {
  55. lineColor = colors.red;
  56. fillColor = colors.redLight;
  57. } else {
  58. lineColor = colors.green;
  59. fillColor = colors.greenLight;
  60. }
  61. this.ui = {
  62. arrow: undefined,
  63. help: undefined,
  64. message: undefined,
  65. challenge: {},
  66. explanation: {
  67. button: undefined,
  68. text: undefined,
  69. },
  70. };
  71. this.control = {
  72. direc: gameOperation == 'minus' ? -1 : 1, // Will be multiplied to values to easily change tractor direction when needed
  73. divisorsList: '', // Hold the divisors for each fraction on stacked blocks (created for postScore())
  74. lineWidth: 3,
  75. hasClicked: false, // Checks if player 'clicked' on a block
  76. checkAnswer: false, // When true allows game to run 'check answer' code in update
  77. isCorrect: false, // Checks player 'answer'
  78. count: 0, // An 'x' position counter used in the tractor animation
  79. showChallenge: false, // When true, challenge modal is displayed (blocks interaction gated)
  80. showExplanation: false, // When true, explanation modal is displayed at end of level
  81. challengeAnsweredYes: null, // null = not yet answered, true = accepted challenge
  82. };
  83. this.animation = {
  84. animateTractor: false, // When true allows game to run 'tractor animation' code in update (turns animation of the moving tractor ON/OFF)
  85. animateEnding: false, // When true allows game to run 'tractor ending animation' code in update (turns 'ending' animation of the moving tractor ON/OFF)
  86. speed: 2 * this.control.direc, // X distance in which the tractor moves in each iteration of the animation
  87. };
  88. this.default = {
  89. width: 172, // Base block width,
  90. height: 70, // Base block height
  91. x0:
  92. gameOperation == 'minus'
  93. ? context.canvas.width - truckWidth
  94. : truckWidth, // Initial 'x' coordinate for the tractor and stacked blocks
  95. y0: context.canvas.height - game.image['floor_grass'].width * 1.5,
  96. };
  97. this.default.arrowX0 =
  98. self.default.x0 + self.default.width * self.control.direc;
  99. this.default.arrowXEnd =
  100. self.default.arrowX0 + self.default.width * self.control.direc * 8;
  101. renderBackground();
  102. // FOR MOODLE
  103. if (moodle) {
  104. navigation.add.right(['audio']);
  105. } else {
  106. navigation.add.left(['back', 'menu', 'show_answer'], 'customMenu');
  107. navigation.add.right(['audio']);
  108. }
  109. this.stack = {
  110. list: [],
  111. /**
  112. * (a) all blocks (fixed) (random)
  113. * (b) (updates) user selection
  114. */
  115. selectedIndex: undefined,
  116. /**
  117. * (a) UNDEFINED
  118. * (b) subsection of blocks (fixed) (random)
  119. */
  120. correctIndex: undefined,
  121. /**
  122. * (a/b) (updates) cur index (for animation/check control)
  123. *
  124. * initial value: 0
  125. */
  126. curIndex: 0,
  127. /**
  128. * (a/b) (updates) end of current stack block x coord
  129. */
  130. curBlockEndX: undefined,
  131. };
  132. this.floor = {
  133. list: [],
  134. /**
  135. * (a) (updates) user selection
  136. * (b) subsection of floor blocks (fixed) (generated)
  137. */
  138. selectedIndex: undefined,
  139. /**
  140. * (a) correct floor index - equiv to all in STACK
  141. * (b) UNDEFINED
  142. */
  143. correctIndex: undefined,
  144. /**
  145. * (a/b) (updates) cur index (for animation/check control)
  146. *
  147. * initial value: -1
  148. */
  149. curIndex: -1,
  150. /**
  151. * (a) correct floor x coord - equiv to all in STACK
  152. * (b) generated floor x coord - equiv to correct in STACK (fixed) (generated)
  153. */
  154. correctX: undefined,
  155. };
  156. let [restart, correctXA] = this.utils.renderStackedBlocks(
  157. this.control.direc,
  158. lineColor,
  159. fillColor,
  160. this.control.lineWidth,
  161. divisor
  162. );
  163. this.utils.renderFloorBlocks(
  164. this.control.direc,
  165. lineColor,
  166. this.control.lineWidth,
  167. divisor,
  168. correctXA
  169. );
  170. this.utils.renderCharacters();
  171. this.restart = restart;
  172. this.utils.renderChallengeUI();
  173. // Events added now so challenge button is clickable; game block interaction gated by showChallenge flag
  174. game.event.add('click', this.events.onInputDown);
  175. game.event.add('mousemove', this.events.onInputOver);
  176. },
  177. /**
  178. * Game loop
  179. */
  180. update: function () {
  181. // Starts tractor animation
  182. if (self.animation.animateTractor) {
  183. self.utils.animateTractorHandler();
  184. }
  185. // Check answer after animation ends
  186. if (self.control.checkAnswer) {
  187. self.utils.checkAnswerHandler();
  188. }
  189. // Starts tractor moving animation
  190. if (self.animation.animateEnding) {
  191. self.utils.animateEndingHandler();
  192. }
  193. game.render.all();
  194. },
  195. utils: {
  196. // RENDER
  197. /**
  198. * Create stacked blocks for the level in create()
  199. */
  200. renderStackedBlocks: (direc, lineColor, fillColor, lineWidth, divisor) => {
  201. let restart = false;
  202. let hasBaseDifficulty = false; // Will be true after next for loop if level has at least one '1/difficulty' fraction (if false, restart)
  203. const max = gameMode == 'b' ? 10 : curMapPosition + 4; // Maximum # of stacked blocks for the level
  204. const total = game.math.randomInRange(curMapPosition + 2, max); // Total # of stacked blocks for the level
  205. const renderLabels = (curDivisor, x, y, i, curBlock) => {
  206. let font, curFractionItems;
  207. const fractionPartsList = [];
  208. if (curDivisor === 1) {
  209. font = textStyles.h2_;
  210. curFractionItems = [
  211. {
  212. x: x,
  213. y: self.default.y0 - i * y - 20,
  214. text: '1',
  215. },
  216. {
  217. x: x - 25,
  218. y: self.default.y0 - i * y - 27,
  219. text: gameOperation === 'minus' ? '-' : '',
  220. },
  221. null,
  222. ];
  223. } else {
  224. font = textStyles.p_;
  225. curFractionItems = [
  226. {
  227. x: x,
  228. y: self.default.y0 - i * y - 40,
  229. text: '1\n' + curDivisor,
  230. },
  231. {
  232. x: x - 25,
  233. y: self.default.y0 - i * y - 27,
  234. text: gameOperation === 'minus' ? '-' : '',
  235. },
  236. {
  237. x0: x,
  238. y: self.default.y0 - i * y - 35,
  239. x1: x + 25,
  240. lineWidth: 2,
  241. color: lineColor,
  242. },
  243. ];
  244. }
  245. font = { ...font, font: 'bold ' + font.font, fill: lineColor };
  246. for (let i = 0; i < 2; i++) {
  247. const item = game.add.text(
  248. curFractionItems[i].x,
  249. curFractionItems[i].y,
  250. curFractionItems[i].text,
  251. font
  252. );
  253. item.lineHeight = 30;
  254. fractionPartsList.push(item);
  255. }
  256. if (curFractionItems[2]) {
  257. const line = game.add.geom.line(
  258. curFractionItems[2].x0,
  259. curFractionItems[2].y,
  260. curFractionItems[2].x1,
  261. curFractionItems[2].y,
  262. curFractionItems[2].lineWidth,
  263. curFractionItems[2].color
  264. );
  265. line.anchor(0.5, 0);
  266. fractionPartsList.push(line);
  267. } else {
  268. fractionPartsList.push(null);
  269. }
  270. curBlock.fraction = {
  271. labels: fractionPartsList,
  272. nominator: direc,
  273. denominator: curDivisor,
  274. };
  275. if (!showFractions) {
  276. curBlock.fraction.labels.forEach((label) => {
  277. if (label) label.alpha = 0;
  278. });
  279. }
  280. };
  281. const shouldRestart = (hasBaseDifficulty, correctXA) => {
  282. if (
  283. !hasBaseDifficulty ||
  284. (gameOperation == 'plus' &&
  285. (correctXA < self.default.x0 + self.default.width ||
  286. correctXA > self.default.x0 + 8 * self.default.width)) ||
  287. (gameOperation == 'minus' &&
  288. (correctXA < self.default.x0 - 8 * self.default.width ||
  289. correctXA > self.default.x0 - self.default.width))
  290. )
  291. return true;
  292. return false;
  293. };
  294. let correctXA = self.default.x0 + self.default.width * direc; // Equivalent x coordinate on the floor
  295. // Render blocks
  296. for (let i = 0; i < total; i++) {
  297. let curDivisor = game.math.randomInRange(1, gameDifficulty); // Set divisor for current fraction
  298. if (curDivisor === gameDifficulty) hasBaseDifficulty = true;
  299. if (curDivisor === 3) curDivisor = 4; // Make sure valid divisors are 1, 2 and 4 (not 3)
  300. const curBlockWidth = self.default.width / curDivisor; // Current width is a fraction of the default
  301. self.control.divisorsList += curDivisor + ','; // Documenting list of divisors (for postScore())
  302. correctXA += curBlockWidth * direc;
  303. const curBlock = game.add.geom.rect(
  304. self.default.x0,
  305. self.default.y0 - i * self.default.height - lineWidth,
  306. curBlockWidth,
  307. self.default.height,
  308. fillColor,
  309. 1,
  310. lineColor,
  311. lineWidth
  312. );
  313. curBlock.anchor(gameOperation === 'minus' ? 1 : 0, 1);
  314. curBlock.blockValue = divisor / curDivisor;
  315. // If game mode is (b), add events to stacked blocks
  316. if (gameMode === 'b') curBlock.blockIndex = i;
  317. // Add to stack blocks list
  318. self.stack.list.push(curBlock);
  319. // If 'show fractions' is turned on, create labels that display the fractions on the side of each block
  320. const x = self.default.x0 + (curBlockWidth + 40) * direc;
  321. const y = self.default.height + 1;
  322. renderLabels(curDivisor, x, y, i, curBlock);
  323. }
  324. // Computer generated correct stack index
  325. if (gameMode === 'a') self.stack.selectedIndex = total - 1;
  326. // Will be used as a counter in update, adding in the width of each stacked block to check if the end matches the floor selected position
  327. // initial value is end of current block
  328. self.stack.curBlockEndX =
  329. self.default.x0 + self.stack.list[0].width * direc;
  330. // Check for errors (level too easy for its difficulty or end position out of bounds)
  331. restart = shouldRestart(hasBaseDifficulty, correctXA);
  332. if (isDebugMode) {
  333. console.log(
  334. `------------------------------
  335. \nGame Map Position: ${curMapPosition}
  336. \n------------------------ setup stack
  337. \nMin blocks (curMapPosition + 2): ${curMapPosition + 2}
  338. ${
  339. gameMode === 'a'
  340. ? `\nMax blocks (A mode) (curMapPosition + 4): ${
  341. curMapPosition + 4
  342. }`
  343. : '\nMax blocks (B mode): 10'
  344. }
  345. \nTotal blocks (random min..max): ${total}
  346. \nPossible divisors (random 1..gameDifficulty / if 3 then 4): 1..${
  347. gameDifficulty == 3 ? 4 : gameDifficulty
  348. }
  349. \n(Checks if has 1/diff or floor x is out of bounds)
  350. `
  351. );
  352. if (restart) console.log('Error during level setup. Retrying...');
  353. else console.log('Successfull level setup!');
  354. }
  355. return [restart, correctXA];
  356. },
  357. /**
  358. * Create floor blocks for the level in create()
  359. */
  360. renderFloorBlocks: (direc, lineColor, lineWidth, divisor, correctXA) => {
  361. let correctXB = 0;
  362. let total = 8 * divisor; // Number of floor blocks
  363. const blockWidth = self.default.width / divisor; // Width of each floor block
  364. const renderLabels = () => {
  365. for (let i = 0; i <= 8; i++) {
  366. const x = self.default.x0 + (i + 1) * self.default.width * direc;
  367. const y = self.default.y0 + self.default.height + 45 - 65;
  368. let numberX = x;
  369. let numberText = i;
  370. let numberCircleSize = 45;
  371. let numberFont = {
  372. ...textStyles.h3_,
  373. fill: lineColor,
  374. font: 'bold ' + textStyles.h3_.font,
  375. };
  376. if (gameOperation === 'minus') {
  377. numberX = i !== 0 ? x - 2 : x;
  378. numberText = -i;
  379. numberCircleSize = 45;
  380. numberFont.font = 'bold ' + textStyles.h4_.font;
  381. }
  382. game.add.geom
  383. .circle(x, y, numberCircleSize, undefined, 0, colors.white, 1)
  384. .anchor(0, 0.25);
  385. game.add.text(numberX, y + 2, numberText, numberFont);
  386. }
  387. };
  388. // If game is type (b), select a random floor x position
  389. if (gameMode === 'b') {
  390. self.stack.correctIndex = game.math.randomInRange(
  391. 0,
  392. self.stack.list.length - 1
  393. ); // Correct stacked index
  394. correctXB = self.default.x0 + self.default.width * direc;
  395. for (let i = 0; i <= self.stack.correctIndex; i++) {
  396. correctXB += self.stack.list[i].width * direc; // Equivalent x position on the floor
  397. }
  398. }
  399. // Render floor blocks
  400. for (let i = 0, shouldUpdateIndex = true; i < total; i++) {
  401. const curX =
  402. self.default.x0 + (self.default.width + i * blockWidth) * direc;
  403. // If game is type (a), iterate until find equivalent floor index
  404. if (gameMode === 'a' && shouldUpdateIndex) {
  405. if (
  406. (gameOperation == 'plus' && curX >= correctXA) ||
  407. (gameOperation == 'minus' && curX <= correctXA)
  408. ) {
  409. self.floor.correctIndex = i - 1; // Set index of correct floor block
  410. shouldUpdateIndex = false;
  411. }
  412. }
  413. // If game is type (b), stop iterating after find equivalent floor block
  414. if (gameMode === 'b') {
  415. if (
  416. (gameOperation == 'plus' && curX >= correctXB) ||
  417. (gameOperation == 'minus' && curX <= correctXB)
  418. ) {
  419. total = i;
  420. break;
  421. }
  422. }
  423. // Render floor block
  424. const curBlock = game.add.geom.rect(
  425. curX,
  426. self.default.y0,
  427. blockWidth,
  428. self.default.height + 10,
  429. colors.blueBgInsideLevel,
  430. 1,
  431. colors.gray,
  432. lineWidth
  433. );
  434. const anchor = gameOperation == 'minus' ? 1 : 0;
  435. curBlock.anchor(anchor, 0);
  436. curBlock.blockValue = 1;
  437. // If game is type (a), add events to floor blocks
  438. if (gameMode === 'a') {
  439. curBlock.fillColor = 'transparent';
  440. curBlock.blockIndex = i;
  441. }
  442. // Add current block to list
  443. self.floor.list.push(curBlock);
  444. }
  445. if (gameMode === 'b') {
  446. self.floor.selectedIndex = total - 1; // Computer generated correct floor index
  447. self.floor.correctX = correctXB;
  448. } else {
  449. self.floor.correctX = correctXA;
  450. }
  451. // Creates labels on the floor to display the numbers
  452. renderLabels();
  453. if (isDebugMode) {
  454. console.log(
  455. `------------------------ setup floor
  456. \nDivisor (gameDifficulty / if 3 then 4): ${divisor}
  457. ${
  458. gameMode === 'a'
  459. ? `\nTotal blocks (A) (divisor * 8): ${divisor} * 8 = ${
  460. divisor * 8
  461. }`
  462. : `\nTotal blocks (B) (((equiv. to stack))): ${self.floor.list.length}`
  463. }
  464. ${
  465. gameMode === 'a'
  466. ? `\nCorrect index (A) (((equival. to stack))): ${self.floor.correctIndex}`
  467. : ''
  468. }
  469. `
  470. );
  471. }
  472. },
  473. renderCharacters: () => {
  474. self.tractor = game.add.sprite(
  475. self.default.x0,
  476. self.default.y0,
  477. 'tractor',
  478. 0,
  479. 1
  480. );
  481. if (gameOperation == 'plus') {
  482. self.tractor.anchor(1, 1);
  483. self.tractor.animation = ['move', [0, 1, 2, 3, 4], 4];
  484. } else {
  485. self.tractor.anchor(0, 1);
  486. self.tractor.animation = ['move', [5, 6, 7, 8, 9], 4];
  487. self.tractor.curFrame = 5;
  488. }
  489. },
  490. renderChallengeUI: () => {
  491. const cx = context.canvas.width / 2;
  492. const withNewlines = (s) => (s == null ? '' : String(s).replace(/\\n/g, '\n'));
  493. const FRAC_UNICODE = { 1: '1', 2: '\u00BD', 4: '\u00BC' };
  494. // challenge-card.png is 786×166px; scale to ~680px wide
  495. const ribbonScale = 0.87;
  496. const ribbonH = Math.round(166 * ribbonScale); // ≈ 144px
  497. const ribbonY = 30;
  498. // Challenge ribbon image at top center
  499. self.ui.challenge.image = game.add.image(cx, ribbonY, 'challenge-card', ribbonScale, 1);
  500. self.ui.challenge.image.anchor(0.5, 0);
  501. // Title overlaid on ribbon (nudged down slightly to visual center of ribbon)
  502. const ribbonCenterY = ribbonY + ribbonH / 2 + 2;
  503. self.ui.challenge.title = game.add.text(
  504. cx, ribbonCenterY,
  505. withNewlines(game.lang.s1_challenge_title),
  506. { ...textStyles.h2_, fill: colors.white, font: 'bold ' + textStyles.h2_.font }
  507. );
  508. self.ui.challenge.title.anchor(0.5, 0.5);
  509. // Subtitle lines below the ribbon
  510. const ribbonBottom = ribbonY + ribbonH;
  511. const subtitleLines = withNewlines(game.lang.s1_challenge_subtitle).split('\n');
  512. self.ui.challenge.subtitleTop = game.add.text(
  513. cx, ribbonBottom + 48,
  514. subtitleLines[0] || '',
  515. { ...textStyles.h4_, fill: colors.blueDark, font: 'bold ' + textStyles.h4_.font }
  516. );
  517. self.ui.challenge.subtitleTop.anchor(0.5, 0.5);
  518. self.ui.challenge.subtitleBottom = game.add.text(
  519. cx, ribbonBottom + 100,
  520. subtitleLines.slice(1).join('\n'),
  521. { ...textStyles.h3_, fill: colors.blue }
  522. );
  523. self.ui.challenge.subtitleBottom.anchor(0.5, 0.5);
  524. // "Blocos a carregar:" label above the stacked blocks
  525. const stackTopY = self.default.y0 - self.stack.list.length * self.default.height;
  526. // Center label over the block stack (x0 is the edge; blocks extend inward by direc)
  527. const labelX = self.default.x0 + self.default.width / 2 * self.control.direc;
  528. self.ui.challenge.blocksLabel = game.add.text(
  529. labelX, stackTopY - 50,
  530. withNewlines(game.lang.s1_blocks_label),
  531. { ...textStyles.h4_, fill: colors.blueDark, font: 'bold ' + textStyles.h4_.font }
  532. );
  533. self.ui.challenge.blocksLabel.anchor(0.5, 0.5);
  534. // question-mark-with-arrow.png is 1230×864px; scale 0.20 → 246×173px
  535. // Placed at the correct floor hole position, bottom of image at floor level
  536. const holeIdx = gameMode === 'b' ? self.floor.selectedIndex : self.floor.correctIndex;
  537. const correctFloorBlock = self.floor.list[holeIdx];
  538. const markerX = correctFloorBlock
  539. ? correctFloorBlock.x + (correctFloorBlock.width / 2) * (gameOperation === 'minus' ? -1 : 1)
  540. : cx;
  541. self.ui.challenge.holeMarker = game.add.image(
  542. markerX, self.default.y0 - 10,
  543. 'question-mark-with-arrow',
  544. 0.20, 1
  545. );
  546. self.ui.challenge.holeMarker.anchor(0.5, 1);
  547. // Question card – right half, moved up from center
  548. const cardW = 520; const cardH = 260;
  549. const cardX = cx + (gameOperation === 'minus' ? -200 : 200);
  550. const cardY = context.canvas.height / 2 - 80;
  551. self.ui.challenge.card = game.add.geom.rect(
  552. cardX, cardY, cardW, cardH, colors.white, 0.95, colors.blueMenuLine, 4
  553. );
  554. self.ui.challenge.card.anchor(0.5, 0.5);
  555. // Question text: use \n from lang file directly
  556. const questionWrapped = withNewlines(
  557. gameMode === 'a' ? (game.lang.s1_challenge_question_b || game.lang.s1_challenge_question) : game.lang.s1_challenge_question
  558. );
  559. const qLines = questionWrapped.split('\n').length;
  560. const qFontSize = qLines === 3 ? 30 : qLines > 3 ? 26 : 32;
  561. const qLineH = qLines >= 3 ? 34 : 40;
  562. const qOffsetY = qLines >= 3 ? -80 : -65;
  563. self.ui.challenge.question = game.add.text(
  564. cardX, cardY + qOffsetY,
  565. questionWrapped,
  566. { ...textStyles.h3_, fill: colors.blueDark, font: `bold ${qFontSize}px ${font.families.default}` },
  567. qLineH
  568. );
  569. self.ui.challenge.question.anchor(0.5, 0.5);
  570. // Fraction equation: "1 + ½ + ... =" as text, then circle — group centered at cardX
  571. // Show only the blocks that will fill the hole (0 → stack.correctIndex) in both modes
  572. const stackEndIdx = self.stack.correctIndex ?? (self.stack.list.length - 1);
  573. const equationParts = self.stack.list.slice(0, stackEndIdx + 1).map(block => {
  574. const den = block.fraction.denominator;
  575. return FRAC_UNICODE[den] ?? `1/${den}`;
  576. });
  577. const circleScale = 0.20;
  578. const circleR = 35;
  579. const gap = 10;
  580. const eqStyle = { ...textStyles.h3_, fill: colors.blueDark };
  581. const maxW = cardW - 80; // max group width before wrapping
  582. context.save();
  583. context.font = '38px Arial, sans-serif';
  584. const isMinus = gameOperation === 'minus';
  585. const eqSep = isMinus ? ' - ' : ' + ';
  586. const eqFirst = isMinus ? '-' + equationParts[0] : equationParts[0];
  587. const eqRest = equationParts.slice(1).join(eqSep);
  588. const eqBody = eqFirst + (eqRest ? eqSep + eqRest : '');
  589. const fullStr = eqBody + ' =';
  590. const fullW = context.measureText(fullStr).width;
  591. if (fullW + gap + circleR * 2 <= maxW) {
  592. // Single line — group centered at cardX
  593. const eqY = cardY + 42;
  594. const textX = cardX - gap / 2 - circleR;
  595. const circleX = cardX + fullW / 2 + gap / 2;
  596. self.ui.challenge.equation = game.add.text(textX, eqY, fullStr, eqStyle);
  597. self.ui.challenge.equationCircle = game.add.image(circleX, eqY - 14, 'circular-question', circleScale, 1);
  598. self.ui.challenge.equationCircle.anchor(0.5, 0.5);
  599. } else {
  600. // Two lines — split parts roughly in half
  601. const mid = Math.ceil(equationParts.length / 2);
  602. const line1Parts = isMinus
  603. ? '-' + equationParts[0] + (mid > 1 ? eqSep + equationParts.slice(1, mid).join(eqSep) : '')
  604. : equationParts.slice(0, mid).join(eqSep);
  605. const line1Str = line1Parts + eqSep.trimEnd();
  606. const line2Str = equationParts.slice(mid).join(eqSep) + ' =';
  607. const line2W = context.measureText(line2Str).width;
  608. const line1Y = cardY + 28;
  609. const line2Y = cardY + 68;
  610. // Both lines centered at cardX; circle goes right of line 2 text
  611. self.ui.challenge.equation = game.add.text(cardX, line1Y, line1Str, eqStyle);
  612. self.ui.challenge.equationLine2 = game.add.text(cardX, line2Y, line2Str, eqStyle);
  613. const line2CircleX = cardX + line2W / 2 + gap + circleR;
  614. self.ui.challenge.equationCircle = game.add.image(line2CircleX, line2Y - 14, 'circular-question', circleScale, 1);
  615. self.ui.challenge.equationCircle.anchor(0.5, 0.5);
  616. }
  617. context.restore();
  618. // Accept button – directly below the card
  619. const btnW = cardW; const btnH = 90;
  620. const btnY = cardY + cardH / 2 + 65;
  621. self.ui.challenge.button = game.add.geom.rect(cardX, btnY, btnW, btnH, '#e09800', 1);
  622. self.ui.challenge.button.anchor(0.5, 0.5);
  623. self.ui.challenge.buttonText = game.add.text(
  624. cardX, btnY + 14,
  625. withNewlines(game.lang.s1_challenge_accept),
  626. textStyles.btn
  627. );
  628. self.ui.challenge.buttonText.anchor(0.5, 0.5);
  629. self.control.showChallenge = true;
  630. },
  631. acceptChallenge: () => {
  632. self.control.challengeAnsweredYes = true;
  633. // Hide all challenge overlay elements
  634. Object.values(self.ui.challenge).forEach(el => {
  635. if (el && typeof el.alpha !== 'undefined') el.alpha = 0;
  636. });
  637. self.control.showChallenge = false;
  638. // Show main game UI (intro message, selection arrow)
  639. self.utils.renderMainUI();
  640. console.log('Starting game with config:');
  641. // Start timer now that challenge has been accepted
  642. if (!self.restart) {
  643. game.timer.start();
  644. }
  645. },
  646. renderMainUI: () => {
  647. // Help pointer
  648. self.ui.help = game.add.image(0, 0, 'pointer', 1.7, 0);
  649. if (gameMode === 'b') self.ui.help.anchor(0.25, 0.7);
  650. else self.ui.help.anchor(0.2, 0);
  651. // Selection Arrow
  652. if (gameMode === 'a') {
  653. self.ui.arrow = game.add.image(
  654. self.default.arrowX0,
  655. self.default.y0,
  656. 'arrow_down',
  657. 1.5
  658. );
  659. self.ui.arrow.anchor(0.5, 1);
  660. self.ui.arrow.alpha = 0.5;
  661. }
  662. // Intro text
  663. const correctMessage =
  664. gameMode === 'a'
  665. ? game.lang.squareOne_intro_a
  666. : game.lang.squareOne_intro_b;
  667. const treatedMessage = correctMessage.split('\\n');
  668. const font = textStyles.h1_;
  669. self.ui.message = [];
  670. self.ui.message.push(
  671. game.add.text(
  672. context.canvas.width / 2,
  673. 170,
  674. treatedMessage[0] + '\n' + treatedMessage[1],
  675. font
  676. )
  677. );
  678. },
  679. renderOperationUI: () => {
  680. /**
  681. *
  682. * if game mode A:
  683. * - left: (1) selected floor position (user selection)
  684. * - right: (2) expected floor position equivalent to the stack of blocks (pre-set)
  685. *
  686. * if game mode B:
  687. * - left: (3) floor position equivalent to the stack of blocks (user selection)
  688. * - right: (4) generated floor position (pre-set)
  689. */
  690. const divisor = gameDifficulty == 3 ? 4 : gameDifficulty;
  691. const renderFloorFractions = (lastIndex, divisor) => {
  692. const operator = gameOperation === 'minus' ? '-' : '+';
  693. const index = lastIndex;
  694. const blocks = index + 1;
  695. const valueReal = blocks / divisor;
  696. const valueFloor = Math.floor(valueReal);
  697. const valueRest = valueReal - valueFloor;
  698. let fracNomin = (fracDenomin = fracLine = '');
  699. // adds sign on the left of the equation
  700. if (gameOperation === 'minus') {
  701. fracNomin += ' ';
  702. fracDenomin += ' ';
  703. fracLine += operator;
  704. }
  705. // 1 _ _
  706. if (valueFloor) {
  707. fracNomin += ' ';
  708. fracDenomin += ' ';
  709. fracLine += valueFloor;
  710. }
  711. // _ + _
  712. if (valueFloor && valueRest) {
  713. fracNomin += ' ';
  714. fracDenomin += ' ';
  715. fracLine += operator;
  716. }
  717. // _ _ 1/5
  718. if (valueRest) {
  719. fracNomin += `${valueRest * divisor}`;
  720. fracDenomin += `${divisor}`;
  721. fracLine += '-';
  722. }
  723. return [fracNomin, fracDenomin, fracLine, valueReal];
  724. };
  725. const renderStackFractions = (lastIndex) => {
  726. const operator = gameOperation === 'minus' ? '-' : '+';
  727. const index = lastIndex;
  728. const blocks = index + 1;
  729. const nominators = [];
  730. const denominators = [];
  731. const values = [];
  732. let valueReal = 0;
  733. let fracNomin = (fracDenomin = fracLine = '');
  734. for (let i = 0; i < blocks; i++) {
  735. const m = self.stack.list[i].fraction.denominator || 1;
  736. const temp = self.stack.list[i].fraction.nominator || 0;
  737. const n = gameOperation === 'minus' ? -temp : +temp;
  738. const nm = n / m;
  739. nominators[i] = n + 0;
  740. denominators[i] = m + 0;
  741. values[i] = nm;
  742. valueReal += nm;
  743. }
  744. for (let i = 0; i < blocks; i++) {
  745. const valueReal = values[i];
  746. const valueFloor = Math.floor(valueReal);
  747. const valueRest = valueReal - valueFloor;
  748. if (i > 0 || gameOperation === 'minus') {
  749. fracNomin += ' ';
  750. fracDenomin += ' ';
  751. fracLine += operator;
  752. }
  753. if (valueFloor && !valueRest) {
  754. fracNomin += ' ';
  755. fracDenomin += ' ';
  756. fracLine += valueFloor;
  757. }
  758. if (valueRest) {
  759. fracNomin += `${nominators[i]}`;
  760. fracDenomin += `${denominators[i]}`;
  761. fracLine += '-';
  762. }
  763. }
  764. return [fracNomin, fracDenomin, fracLine, valueReal];
  765. };
  766. // Initial setup
  767. const font = textStyles.fraction;
  768. font.fill = colors.black;
  769. const padding = 100;
  770. const offsetX = 100;
  771. const widthOfChar = 35;
  772. const x0 = padding;
  773. const y0 = context.canvas.height / 3;
  774. let nextX = x0;
  775. const cardHeight = 400;
  776. const cardX = x0 - padding;
  777. const cardY = y0;
  778. const renderList = [];
  779. // Render Card
  780. const card = game.add.geom.rect(
  781. cardX,
  782. cardY,
  783. 0,
  784. cardHeight,
  785. colors.blueLight,
  786. 0.5,
  787. colors.blueDark,
  788. 8
  789. );
  790. card.id = 'card';
  791. card.anchor(0, 0.5);
  792. renderList.push(card);
  793. // Fraction setup
  794. console.clear();
  795. const [floorNominators, floorDenominators, floorLines, floorValue] =
  796. renderFloorFractions(
  797. self.floor.selectedIndex,
  798. divisor
  799. // 'LEFT SIDE - a fração escolhida no chão é...\n\n'
  800. );
  801. renderFloorFractions(
  802. self.floor.correctIndex,
  803. divisor
  804. // '\n\nRIGHT SIDE 1 - a fração CORRETA no chão é...\n\n'
  805. );
  806. const [stackNominators, stackDenominators, stackLines, stackValue] =
  807. renderStackFractions(
  808. self.stack.selectedIndex
  809. // '\n\nRIGHT SIDE 2 - a fração CORRETA na stack é...\n\n'
  810. );
  811. const renderFloorOperationLine = (x) => {
  812. font.fill = colors.black;
  813. const floorNom = game.add.text(
  814. x + offsetX / 2,
  815. y0,
  816. floorNominators,
  817. font,
  818. 60
  819. );
  820. const floorDenom = game.add.text(
  821. x + offsetX / 2,
  822. y0 + 70,
  823. floorDenominators,
  824. font,
  825. 60
  826. );
  827. const floorLin = game.add.text(
  828. x + offsetX / 2,
  829. y0 + 35,
  830. floorLines,
  831. font,
  832. 60
  833. );
  834. renderList.push(floorNom);
  835. renderList.push(floorDenom);
  836. renderList.push(floorLin);
  837. };
  838. const renderStackOperationLine = (x) => {
  839. font.fill = colors.black;
  840. const stackNom = game.add.text(
  841. x + offsetX / 2,
  842. y0,
  843. stackNominators,
  844. font,
  845. 60
  846. );
  847. const stackDenom = game.add.text(
  848. x + offsetX / 2,
  849. y0 + 70,
  850. stackDenominators,
  851. font,
  852. 60
  853. );
  854. const stackLin = game.add.text(
  855. x + offsetX / 2,
  856. y0 + 35,
  857. stackLines,
  858. font,
  859. 60
  860. );
  861. renderList.push(stackNom);
  862. renderList.push(stackDenom);
  863. renderList.push(stackLin);
  864. };
  865. // Render LEFT part of the operation
  866. if (gameMode === 'a') renderFloorOperationLine(x0);
  867. else renderStackOperationLine(x0);
  868. let curNominators = gameMode === 'a' ? floorNominators : stackNominators;
  869. nextX = x0 + (curNominators.length + 2) * widthOfChar;
  870. // Render middle sign - equal by default
  871. font.fill = colors.green;
  872. let comparisonSign = '=';
  873. // Render middle sign - if not equal
  874. if (floorValue != stackValue) {
  875. font.fill = colors.red;
  876. let leftSideIsLarger = floorValue > stackValue;
  877. if (gameMode === 'b') leftSideIsLarger = !leftSideIsLarger;
  878. if (gameOperation === 'minus') leftSideIsLarger = !leftSideIsLarger;
  879. comparisonSign = leftSideIsLarger ? '>' : '<';
  880. }
  881. renderList.push(game.add.text(nextX, y0 + 35, comparisonSign, font));
  882. // Render RIGHT part of the operation
  883. if (gameMode === 'a') renderStackOperationLine(nextX);
  884. else renderFloorOperationLine(nextX);
  885. curNominators = gameMode === 'a' ? stackNominators : floorNominators;
  886. const resultWidth = (curNominators.length + 2) * widthOfChar;
  887. const cardWidth = nextX - x0 + resultWidth + padding * 2;
  888. card.width = cardWidth;
  889. const endSignX = (context.canvas.width - cardWidth) / 2 + cardWidth;
  890. // Center Card
  891. moveList(renderList, (context.canvas.width - cardWidth) / 2, 0);
  892. self.fractionOperationUI = renderList;
  893. return endSignX;
  894. },
  895. renderExplanationUI: () => {
  896. const cx = context.canvas.width / 2;
  897. const cy = context.canvas.height / 2;
  898. const withNewlines = (s) => (s == null ? '' : String(s).replace(/\\n/g, '\n'));
  899. const FRAC_UNICODE = { 1: '1', 2: '\u00BD', 4: '\u00BC' };
  900. const divisor = gameDifficulty == 3 ? 4 : gameDifficulty;
  901. // CORRECT ANSWER: full explanation card — positions based on actual image dimensions
  902. const naturalW = game.image['end-tractor-game'].width;
  903. const naturalH = game.image['end-tractor-game'].height;
  904. const canvasW = context.canvas.width;
  905. const canvasH = context.canvas.height;
  906. const imgScale = Math.min((canvasW * 0.92) / naturalW, (canvasH * 0.92) / naturalH);
  907. const imgW = naturalW * imgScale;
  908. const imgH = naturalH * imgScale;
  909. const cardTop = cy - imgH / 2;
  910. const cardBottom = cy + imgH / 2;
  911. const cardLeft = cx - imgW / 2;
  912. // Background image — scaled to fit canvas
  913. const bgImg = game.add.image(cx, cy, 'end-tractor-game');
  914. bgImg.anchor(0.5, 0.5);
  915. bgImg.scale = imgScale;
  916. // Title overlaid on the image's top blue bar (no emoji — image already has magnifying glass)
  917. const titleText = withNewlines(
  918. gameMode === 'a' ? (game.lang.s1_explain_title_b || game.lang.s1_explain_title) : game.lang.s1_explain_title
  919. );
  920. game.add.text(cx + imgW * 0.04, cardTop + imgH * 0.07, titleText, {
  921. ...textStyles.h3_, fill: colors.white, font: 'bold ' + textStyles.h3_.font,
  922. });
  923. // Step labels below the image's built-in circles (image already draws 1 → 2 → 3)
  924. const stepTexts = [
  925. withNewlines(game.lang.s1_explain_step1),
  926. withNewlines(game.lang.s1_explain_step2),
  927. withNewlines(game.lang.s1_explain_step3),
  928. ];
  929. const stepFont = `24px ${font.families.default}`;
  930. const stepStyle = { ...textStyles.p_, fill: colors.blueDark, font: stepFont };
  931. const stepsLine1Y = cardTop + imgH * 0.245;
  932. const stepsLine2Y = stepsLine1Y + 26;
  933. // Positions centred under each circle in the background image
  934. const stepCenters = [
  935. cardLeft + imgW * 0.28,
  936. cardLeft + imgW * 0.50,
  937. cardLeft + imgW * 0.75,
  938. ];
  939. stepCenters.forEach((sx, idx) => {
  940. const lines = stepTexts[idx].split('\n');
  941. game.add.text(sx, stepsLine1Y, lines[0] || '', stepStyle);
  942. if (lines[1]) game.add.text(sx, stepsLine2Y, lines[1], stepStyle);
  943. });
  944. // Fraction equation — placed in the image's equation box area
  945. let holeNumerator = 0;
  946. const lastIdx = self.stack.correctIndex ?? (self.stack.list.length - 1);
  947. for (let i = 0; i <= lastIdx; i++) {
  948. holeNumerator += divisor / self.stack.list[i].fraction.denominator;
  949. }
  950. const holeSize = holeNumerator / divisor;
  951. const formatNum = (n) => {
  952. if (Number.isInteger(n)) return String(n);
  953. const whole = Math.floor(n);
  954. const frac = n - whole;
  955. const FRAC_MAP = { 0.25: '\u00BC', 0.5: '\u00BD', 0.75: '\u00BE' };
  956. return (whole ? whole + ' + ' : '') + (FRAC_MAP[Math.round(frac * 4) / 4] ?? n.toFixed(2).replace(/0+$/, ''));
  957. };
  958. const equationParts = self.stack.list.slice(0, lastIdx + 1).map(block => {
  959. return FRAC_UNICODE[block.fraction.denominator] ?? `1/${block.fraction.denominator}`;
  960. });
  961. const isMinus = gameOperation === 'minus';
  962. const eqSeparator = isMinus ? ' - ' : ' + ';
  963. const eqTerms = isMinus
  964. ? '-' + equationParts[0] + (equationParts.length > 1 ? eqSeparator + equationParts.slice(1).join(eqSeparator) : '')
  965. : equationParts.join(eqSeparator);
  966. // For minus: "-3 + ¾" → "-3 - ¾" so the sign stays consistent
  967. const resultStr = isMinus
  968. ? '-' + formatNum(holeSize).replace(' + ', ' e ')
  969. : formatNum(holeSize);
  970. const equationStr = eqTerms + ' = ' + resultStr;
  971. game.add.text(cx, cardTop + imgH * 0.355, equationStr, {
  972. ...textStyles.h3_, fill: colors.blueDark, font: 'bold ' + textStyles.h3_.font,
  973. });
  974. // Block bars — stacked upward from ground level, height proportional to fraction
  975. const groundY = cardTop + imgH * 0.72; // ground line where tractor sits
  976. const stackTopMin = cardTop + imgH * 0.44; // never go above equation box
  977. const maxStackH = groundY - stackTopMin; // available vertical space
  978. const barW = imgW * 0.055;
  979. const barsStartX = cardLeft + imgW * 0.30;
  980. const totalBlocks = lastIdx + 1;
  981. const fillColor = gameOperation === 'minus' ? colors.redLight : '#79d2a1';
  982. const borderColor = gameOperation === 'minus' ? colors.red : colors.green;
  983. // Calculate proportional heights, then scale down if they exceed available space
  984. const blocks = self.stack.list.slice(0, totalBlocks);
  985. const rawUnitH = Math.max(48, Math.min(80, maxStackH / holeSize));
  986. const rawHeights = blocks.map(b => Math.max(20, rawUnitH / b.fraction.denominator));
  987. const rawTotal = rawHeights.reduce((s, h) => s + h + 4, -4);
  988. const scale = rawTotal > maxStackH ? maxStackH / rawTotal : 1;
  989. const blockHeights = rawHeights.map(h => Math.max(10, h * scale));
  990. const totalStackH = blockHeights.reduce((s, h) => s + h + 4, -4);
  991. let curY = groundY - totalStackH;
  992. blocks.forEach((block, i) => {
  993. const bh = blockHeights[i];
  994. game.add.geom.rect(barsStartX, curY, barW, bh, fillColor, 0.8, borderColor, 2);
  995. const fracBase = FRAC_UNICODE[block.fraction.denominator] ?? `1/${block.fraction.denominator}`;
  996. const fracLabel = (isMinus ? '-' : '') + fracBase;
  997. game.add.text(barsStartX + barW + 20, curY + bh / 2 + 4, fracLabel, {
  998. ...textStyles.p_, fill: colors.blueDark, font: `20px ${font.families.default}`,
  999. });
  1000. curY += bh + 4;
  1001. });
  1002. // Hole label — text only, centred over the image's built-in blue rounded rectangle
  1003. const holeLabelCX = cardLeft + imgW * 0.675;
  1004. const holeLabelCY = cardTop + imgH * 0.588;
  1005. const holeLabelKey = gameMode === 'a' ? (game.lang.s1_hole_label_b || game.lang.s1_hole_label) : game.lang.s1_hole_label;
  1006. const holeWords = (holeLabelKey || 'Hole of size').split(' ');
  1007. const holeMid = Math.ceil(holeWords.length / 2);
  1008. const holeLabelLine1 = holeWords.slice(0, holeMid).join(' ');
  1009. const holeLabelLine2 = holeWords.slice(holeMid).join(' ');
  1010. const holeLabelLine3 = (isMinus ? '-' : '') + formatNum(holeSize).replace(' + ', ' e ');
  1011. const holeFont = `bold 24px ${font.families.default}`;
  1012. const holeLineH = 24;
  1013. game.add.text(holeLabelCX, holeLabelCY - holeLineH, holeLabelLine1, { ...textStyles.p_, fill: colors.white, font: holeFont });
  1014. game.add.text(holeLabelCX, holeLabelCY, holeLabelLine2, { ...textStyles.p_, fill: colors.white, font: holeFont });
  1015. game.add.text(holeLabelCX, holeLabelCY + holeLineH, holeLabelLine3, { ...textStyles.p_, fill: colors.white, font: holeFont });
  1016. // Body text (up to 3 lines, compact spacing)
  1017. const bodyKey = gameMode === 'a' ? (game.lang.s1_explain_body_b || game.lang.s1_explain_body) : game.lang.s1_explain_body;
  1018. const bodyLines = withNewlines(bodyKey).split('\n');
  1019. const bodyLineH = 30;
  1020. const bodyGap = 14; // extra gap after first line
  1021. const bodyStyle = { ...textStyles.p_, fill: colors.blueDark };
  1022. const bodyMidY = cardBottom - imgH * 0.175;
  1023. const totalH = (bodyLines.length - 1) * bodyLineH + (bodyLines.length > 1 ? bodyGap : 0);
  1024. const bodyStartY = bodyMidY - totalH / 2;
  1025. bodyLines.forEach((line, i) => {
  1026. const y = bodyStartY + (i === 0 ? 0 : bodyGap + i * bodyLineH);
  1027. game.add.text(cx, y, line, bodyStyle);
  1028. });
  1029. // Checkmark in the top-right corner of the card (~90px, scale = 90/256)
  1030. const checkScale = imgH * 0.13 / 256;
  1031. const checkImg = game.add.image(cardLeft + imgW - 16, cardTop + 16, 'answer_correct', checkScale);
  1032. checkImg.anchor(1, 0);
  1033. // Continue button (only reached on correct answer)
  1034. const btnW = imgW * 0.38; const btnH = 62; const btnCY = cardBottom - imgH * 0.07;
  1035. self.ui.explanation.button = game.add.geom.rect(cx, btnCY, btnW, btnH, colors.green);
  1036. self.ui.explanation.button.anchor(0.5, 0.5);
  1037. self.ui.explanation.text = game.add.text(cx, btnCY + 14, game.lang.continue, textStyles.btn);
  1038. self.control.showExplanation = true;
  1039. },
  1040. renderEndUI: () => {
  1041. const cx = context.canvas.width / 2;
  1042. const cy = context.canvas.height / 2;
  1043. let btnColor = colors.green;
  1044. let btnText = game.lang.continue;
  1045. if (!self.control.isCorrect) {
  1046. btnColor = colors.red;
  1047. btnText = game.lang.retry || 'Retry';
  1048. }
  1049. self.ui.explanation.button = game.add.geom.rect(
  1050. cx, cy + 100, 450, 100, btnColor
  1051. );
  1052. self.ui.explanation.button.anchor(0.5, 0.5);
  1053. self.ui.explanation.text = game.add.text(
  1054. cx, cy + 16 + 100, btnText, textStyles.btn
  1055. );
  1056. self.control.showExplanation = true;
  1057. },
  1058. // UPDATE HANDLERS
  1059. animateTractorHandler: () => {
  1060. // Move
  1061. self.tractor.x += self.animation.speed;
  1062. self.stack.list.forEach((block) => (block.x += self.animation.speed));
  1063. // If the current block is 1/n (not 1/1) we need to consider the
  1064. // extra space the truck needs to pass after the blocks falls but
  1065. // before reaching the next block
  1066. const curBlockExtra =
  1067. (self.default.width - self.stack.list[self.stack.curIndex].width) *
  1068. self.control.direc;
  1069. const canLowerBlocks =
  1070. (gameOperation == 'plus' &&
  1071. self.stack.list[0].x >= self.stack.curBlockEndX + curBlockExtra) ||
  1072. (gameOperation == 'minus' &&
  1073. self.stack.list[0].x <= self.stack.curBlockEndX + curBlockExtra);
  1074. if (canLowerBlocks) {
  1075. const endAnimation = self.utils.lowerBlocksHandler();
  1076. if (!endAnimation) {
  1077. self.animation.animateTractor = false;
  1078. self.control.checkAnswer = true;
  1079. }
  1080. }
  1081. },
  1082. lowerBlocksHandler: () => {
  1083. self.floor.curIndex += self.stack.list[self.stack.curIndex].blockValue;
  1084. const tooManyStackBlocks = self.floor.curIndex > self.floor.selectedIndex;
  1085. if (tooManyStackBlocks) return false;
  1086. // fill floor
  1087. for (let i = 0; i <= self.floor.curIndex; i++) {
  1088. self.floor.list[i].fillColor = 'transparent';
  1089. }
  1090. // lower blocks
  1091. self.stack.list.forEach((block) => {
  1092. block.y += self.default.height;
  1093. });
  1094. // hide current block
  1095. self.stack.list[self.stack.curIndex].alpha = 0;
  1096. const isLastFloorBlock = self.floor.curIndex === self.floor.selectedIndex;
  1097. const notEnoughStackBlocks =
  1098. self.stack.curIndex === self.stack.list.length - 1;
  1099. if (isLastFloorBlock || notEnoughStackBlocks) return false;
  1100. // update stack blocks
  1101. self.stack.curIndex++;
  1102. self.stack.curBlockEndX +=
  1103. self.stack.list[self.stack.curIndex].width * self.control.direc;
  1104. return true;
  1105. },
  1106. checkAnswerHandler: () => {
  1107. game.timer.stop();
  1108. game.animation.stop(self.tractor.animation[0]);
  1109. if (gameMode === 'a') self.ui.arrow.alpha = 0;
  1110. self.control.isCorrect =
  1111. gameMode === 'a'
  1112. ? self.floor.selectedIndex === self.floor.correctIndex
  1113. : self.stack.selectedIndex === self.stack.correctIndex;
  1114. const feedbackX = context.canvas.width * 0.75;
  1115. const feedbackY = context.canvas.height / 3;
  1116. // Give feedback to player and turns on sprite animation
  1117. if (self.control.isCorrect) {
  1118. completedLevels++; // Increases number os finished levels
  1119. if (audioStatus) game.audio.okSound.play();
  1120. game.animation.play(self.tractor.animation[0]);
  1121. if (isDebugMode) console.log('Completed Levels: ' + completedLevels);
  1122. } else {
  1123. if (audioStatus) game.audio.errorSound.play();
  1124. game.add.image(feedbackX, feedbackY, 'answer_wrong').anchor(0.5, 0.5);
  1125. }
  1126. self.fetch.postScore();
  1127. self.control.checkAnswer = false;
  1128. self.animation.animateEnding = true;
  1129. },
  1130. animateEndingHandler: () => {
  1131. // ANIMATE ENDING
  1132. self.control.count++;
  1133. // If CORRECT ANSWER runs final tractor animation (else tractor desn't move, just wait)
  1134. if (self.control.isCorrect) self.tractor.x += self.animation.speed;
  1135. if (!self.control.isCorrect && self.control.count === 1) {
  1136. self.utils.renderOperationUI();
  1137. self.utils.renderEndUI();
  1138. canGoToNextMapPosition = false;
  1139. }
  1140. if (self.control.isCorrect && self.control.count === 100) {
  1141. self.utils.renderExplanationUI();
  1142. canGoToNextMapPosition = true;
  1143. }
  1144. },
  1145. endLevel: () => {
  1146. game.state.start('map');
  1147. },
  1148. // INFORMATION
  1149. /**
  1150. * Display correct answer
  1151. */
  1152. showAnswer: () => {
  1153. if (!self.control.hasClicked) {
  1154. // On gameMode (a)
  1155. if (gameMode === 'a') {
  1156. const aux = self.floor.list[0];
  1157. self.ui.help.x =
  1158. self.floor.correctX - (aux.width / 2) * self.control.direc;
  1159. self.ui.help.y = self.default.y0;
  1160. // On gameMode (b)
  1161. } else {
  1162. const aux = self.stack.list[self.stack.correctIndex];
  1163. self.ui.help.x = aux.x + (aux.width / 2) * self.control.direc;
  1164. self.ui.help.y = aux.y;
  1165. }
  1166. self.ui.help.alpha = 0.7;
  1167. }
  1168. },
  1169. // EVENT HANDLERS
  1170. /**
  1171. * Function called by self.events.onInputDown() when player clicks on a valid rectangle.
  1172. */
  1173. clickHandler: (clickedIndex, curSet) => {
  1174. if (!self.control.hasClicked && !self.animation.animateEnding && !self.control.showChallenge) {
  1175. document.body.style.cursor = 'auto';
  1176. // Play beep sound
  1177. if (audioStatus) game.audio.popSound.play();
  1178. // Disable show answer nav icon
  1179. navigation.disableIcon(navigation.showAnswerIcon);
  1180. // Hide intro message
  1181. self.ui.message[0].alpha = 0;
  1182. // Hide labels
  1183. if (showFractions) {
  1184. self.stack.list.forEach((block) => {
  1185. block.fraction.labels.forEach((lbl) => {
  1186. if (lbl) lbl.alpha = 0;
  1187. });
  1188. });
  1189. }
  1190. // Hide solution pointer
  1191. if (self.ui.help != undefined) self.ui.help.alpha = 0;
  1192. // Hide unselected blocks
  1193. for (let i = curSet.list.length - 1; i > clickedIndex; i--) {
  1194. curSet.list[i].alpha = 0;
  1195. }
  1196. // Save selected index
  1197. curSet.selectedIndex = clickedIndex;
  1198. if (gameMode === 'a') {
  1199. self.ui.arrow.alpha = 0;
  1200. } else {
  1201. // update list size
  1202. self.stack.list.length = curSet.selectedIndex + 1;
  1203. }
  1204. // Turn tractor animation on
  1205. game.animation.play(self.tractor.animation[0]);
  1206. self.animation.animateTractor = true;
  1207. self.control.hasClicked = true;
  1208. }
  1209. },
  1210. /**
  1211. * Function called by self.events.onInputOver() when cursor is over a valid rectangle
  1212. *
  1213. * @param {object} cur rectangle the cursor is over
  1214. */
  1215. overHandler: (cur) => {
  1216. if (!self.control.hasClicked) {
  1217. document.body.style.cursor = 'pointer';
  1218. if (gameMode === 'a') {
  1219. for (let i in self.floor.list) {
  1220. self.floor.list[i].fillColor =
  1221. i <= cur.blockIndex ? colors.blueBgInsideLevel : 'transparent';
  1222. }
  1223. self.floor.selectedIndex = cur.blockIndex;
  1224. } else {
  1225. for (let i in self.stack.list) {
  1226. const alpha = i <= cur.blockIndex ? 1 : 0.4;
  1227. self.stack.list[i].alpha = alpha;
  1228. if (showFractions) {
  1229. self.stack.list[i].fraction.labels.forEach((lbl) => {
  1230. if (lbl) lbl.alpha = alpha;
  1231. });
  1232. }
  1233. }
  1234. self.stack.selectedIndex = cur.blockIndex;
  1235. }
  1236. }
  1237. },
  1238. /**
  1239. * Function called by self.events.onInputOver() when cursos is out of a valid rectangle
  1240. */
  1241. outHandler: () => {
  1242. if (!self.control.hasClicked) {
  1243. document.body.style.cursor = 'auto';
  1244. if (gameMode === 'a') {
  1245. for (let i in self.floor.list) {
  1246. self.floor.list[i].fillColor = 'transparent';
  1247. }
  1248. self.floor.selectedIndex = undefined;
  1249. } else {
  1250. for (let i in self.stack.list) {
  1251. self.stack.list[i].alpha = 1;
  1252. if (showFractions) {
  1253. self.stack.list[i].fraction.labels.forEach((lbl) => {
  1254. if (lbl) lbl.alpha = 1;
  1255. });
  1256. }
  1257. }
  1258. self.stack.selectedIndex = undefined;
  1259. }
  1260. }
  1261. },
  1262. },
  1263. events: {
  1264. /**
  1265. * Called by mouse click event
  1266. *
  1267. * @param {object} mouseEvent contains the mouse click coordinates
  1268. */
  1269. onInputDown: (mouseEvent) => {
  1270. const x = game.math.getMouse(mouseEvent).x;
  1271. const y = game.math.getMouse(mouseEvent).y;
  1272. // Challenge modal: block all game interaction until accepted
  1273. if (self.control.showChallenge) {
  1274. if (game.math.isOverIcon(x, y, self.ui.challenge.button)) {
  1275. if (audioStatus) game.audio.popSound.play();
  1276. self.utils.acceptChallenge();
  1277. }
  1278. navigation.onInputDown(x, y);
  1279. game.render.all();
  1280. return;
  1281. }
  1282. // Hide intro message on first click after challenge acceptance
  1283. if (!self.control.hasClicked && self.ui.message && self.ui.message[0]) {
  1284. self.ui.message[0].alpha = 0;
  1285. }
  1286. // click blocks
  1287. const curSet = gameMode == 'a' ? self.floor : self.stack;
  1288. for (let i in curSet.list) {
  1289. if (game.math.isOverIcon(x, y, curSet.list[i])) {
  1290. self.utils.clickHandler(+i, curSet);
  1291. break;
  1292. }
  1293. }
  1294. // Explanation button
  1295. if (self.control.showExplanation) {
  1296. if (game.math.isOverIcon(x, y, self.ui.explanation.button)) {
  1297. if (audioStatus) game.audio.popSound.play();
  1298. self.utils.endLevel();
  1299. }
  1300. }
  1301. navigation.onInputDown(x, y);
  1302. game.render.all();
  1303. },
  1304. /**
  1305. * Called by mouse move event
  1306. *
  1307. * @param {object} mouseEvent contains the mouse move coordinates
  1308. */
  1309. onInputOver: (mouseEvent) => {
  1310. const x = game.math.getMouse(mouseEvent).x;
  1311. const y = game.math.getMouse(mouseEvent).y;
  1312. let isOverFloor = false;
  1313. let isOverStack = false;
  1314. if (!self.control.showChallenge) {
  1315. if (gameMode === 'a') {
  1316. self.floor.list.forEach((cur) => {
  1317. // hover floor blocks
  1318. if (game.math.isOverIcon(x, y, cur)) {
  1319. isOverFloor = true;
  1320. self.utils.overHandler(cur);
  1321. }
  1322. // move arrow
  1323. if (
  1324. !self.control.hasClicked &&
  1325. !self.animation.animateEnding &&
  1326. game.math.isOverIcon(x, self.default.y0, cur)
  1327. ) {
  1328. self.ui.arrow.x = x;
  1329. }
  1330. });
  1331. if (!isOverFloor) self.utils.outHandler('a');
  1332. }
  1333. if (gameMode === 'b') {
  1334. // hover stack blocks
  1335. self.stack.list.forEach((cur) => {
  1336. if (game.math.isOverIcon(x, y, cur)) {
  1337. isOverStack = true;
  1338. self.utils.overHandler(cur);
  1339. }
  1340. });
  1341. if (!isOverStack) self.utils.outHandler('b');
  1342. }
  1343. }
  1344. // Challenge accept button hover
  1345. if (self.control.showChallenge && self.ui.challenge.button) {
  1346. if (game.math.isOverIcon(x, y, self.ui.challenge.button)) {
  1347. document.body.style.cursor = 'pointer';
  1348. self.ui.challenge.button.scale = self.ui.challenge.button.initialScale * 1.1;
  1349. self.ui.challenge.buttonText.style = textStyles.btnLg;
  1350. } else {
  1351. document.body.style.cursor = 'auto';
  1352. self.ui.challenge.button.scale = self.ui.challenge.button.initialScale * 1;
  1353. self.ui.challenge.buttonText.style = textStyles.btn;
  1354. }
  1355. }
  1356. // Explanation button hover
  1357. if (self.control.showExplanation) {
  1358. if (game.math.isOverIcon(x, y, self.ui.explanation.button)) {
  1359. document.body.style.cursor = 'pointer';
  1360. self.ui.explanation.button.scale =
  1361. self.ui.explanation.button.initialScale * 1.05;
  1362. self.ui.explanation.text.style = textStyles.btnLg;
  1363. } else {
  1364. document.body.style.cursor = 'auto';
  1365. self.ui.explanation.button.scale =
  1366. self.ui.explanation.button.initialScale * 1;
  1367. self.ui.explanation.text.style = textStyles.btn;
  1368. }
  1369. }
  1370. navigation.onInputOver(x, y);
  1371. game.render.all();
  1372. },
  1373. },
  1374. fetch: {
  1375. /**
  1376. * Saves players data after level ends - to be sent to database <br>
  1377. *
  1378. * Attention: the 'line_' prefix data table must be compatible to data table fields (MySQL server)
  1379. *
  1380. * @see /php/save.php
  1381. */
  1382. postScore: () => {
  1383. // Creates string that is going to be sent to db
  1384. const data =
  1385. '&line_game=' +
  1386. gameShape +
  1387. '&line_mode=' +
  1388. gameMode +
  1389. '&line_oper=' +
  1390. gameOperation +
  1391. '&line_leve=' +
  1392. gameDifficulty +
  1393. '&line_posi=' +
  1394. curMapPosition +
  1395. '&line_resu=' +
  1396. self.control.isCorrect +
  1397. '&line_time=' +
  1398. game.timer.elapsed +
  1399. '&line_deta=' +
  1400. 'numBlocks:' +
  1401. self.stack.list.length +
  1402. ', valBlocks: ' +
  1403. self.control.divisorsList + // Ends in ','
  1404. ' blockIndex: ' +
  1405. self.stack.selectedIndex +
  1406. ', floorIndex: ' +
  1407. self.floor.selectedIndex;
  1408. // FOR MOODLE
  1409. sendToDatabase(data);
  1410. },
  1411. },
  1412. };