Sfoglia il codice sorgente

feat: enhance challenge mode for circleOne game with dynamic circle visibility and equation adjustments

priscila.snl 6 giorni fa
parent
commit
bf8931595b

BIN
assets/img/scene/challenge-card.png


BIN
assets/img/scene/circular-question.png


BIN
assets/img/scene/end-tractor-game.png


BIN
assets/img/scene/question-mark-with-arrow.png


+ 31 - 1
assets/lang/en_US

@@ -95,4 +95,34 @@ s2_diff_3_description=The higher the difficulty, the more subdivisions
 s2_diff_4_description=The higher the difficulty, the more subdivisions
 s2_diff_5_description=The higher the difficulty, the more subdivisions
 label_description=Show fractions next to the figures
-info_description=Learn more
+info_description=Learn more
+s1_challenge_title=Level Challenge!
+s1_challenge_subtitle=Can you figure it out?\nThe tractor will carry these blocks to a hole in the ground...
+s1_challenge_question=What should be\nthe size\nof the hole?
+s1_challenge_question_b=How many blocks\nare needed to fill\nthe hole in the ground?
+s1_challenge_accept=Accept the Challenge
+s1_blocks_label=Blocks to carry:
+s1_explain_title=How do we find the hole size?
+s1_explain_title_b=How many blocks do we need?
+s1_explain_step1=Identify the blocks\nand their sizes
+s1_explain_step2=Add all\nthe values
+s1_explain_step3=The result is\nthe hole size!
+s1_explain_body=Each block has the specified size.\nWe add their values to determine\nthe size of the hole in the ground!
+s1_explain_body_b=Each block has the specified size.\nWe add their values to determine\nthe number of blocks needed to cover the hole!
+s1_explain_body_wrong=The selected size is not correct.
+s1_hole_label=Hole size
+s1_hole_label_b=Number of blocks
+c1_challenge_title=Level Challenge!
+c1_challenge_subtitle=Can you figure it out?\nHelp the boy reach the kite...
+c1_challenge_question=In which position will\nthe boy arrive with this advance?
+c1_challenge_question_b=How many arcs are needed\nto reach the kite?
+c1_challenge_accept=Accept the Challenge
+c1_circles_label=Circles to advance:
+c1_explain_title=How did the boy reach the kite?
+c1_explain_step1=Identify the circles\nand their sizes
+c1_explain_step2=Add all\nthe values
+c1_explain_step3=The result is the\nkite position
+c1_explain_body_prefix=By adding the distance:
+c1_explain_body_prefix_b=By adding the arcs:
+c1_explain_body_suffix=, we reach a new position on the number line.
+c1_explain_body_line2=Fractions help represent parts of a path.

+ 31 - 1
assets/lang/es_ES

@@ -95,4 +95,34 @@ s2_diff_3_description=Cuanto mayor sea la dificultad, más subdivisiones
 s2_diff_4_description=Cuanto mayor sea la dificultad, más subdivisiones
 s2_diff_5_description=Cuanto mayor sea la dificultad, más subdivisiones
 label_description=Mostrar fracciones al lado de las figuras
-info_description=Saber más
+info_description=Saber más
+s1_challenge_title=¡Desafío de la Fase!
+s1_challenge_subtitle=¿Puedes descubrirlo?\nEl tractor llevará estos bloques a un agujero en el suelo...
+s1_challenge_question=¿Cuál debe ser\nel tamaño\ndel agujero?
+s1_challenge_question_b=¿Cuántos bloques son\nnecesarios para cubrir\nel agujero en el suelo?
+s1_challenge_accept=Aceptar el Desafío
+s1_blocks_label=Bloques a cargar:
+s1_explain_title=¿Cómo descubrimos el tamaño del agujero?
+s1_explain_title_b=¿Cómo descubrimos cuántos bloques necesitamos?
+s1_explain_step1=Identifica los bloques\ny sus tamaños
+s1_explain_step2=Suma todos\nlos valores
+s1_explain_step3=¡El resultado es\nel tamaño del agujero!
+s1_explain_body=Cada bloque tiene el tamaño especificado.\nSumamos sus valores para determinar\nel tamaño del agujero en el suelo!
+s1_explain_body_b=Cada bloque tiene el tamaño especificado.\nSumamos sus unidades para determinar\nla cantidad de bloques necesarios para cubrir el agujero.
+s1_explain_body_wrong=El tamaño seleccionado no es correcto.
+s1_hole_label=Tamaño del agujero
+s1_hole_label_b=Cantidad de bloques
+c1_challenge_title=¡Desafío de la Fase!
+c1_challenge_subtitle=¿Puedes descubrirlo?\nAyuda al niño a llegar hasta la cometa...
+c1_challenge_question=¿En qué posición llegará\nel niño con este avance?
+c1_challenge_question_b=¿Cuántos arcos son necesarios\npara llegar hasta la cometa?
+c1_challenge_accept=Aceptar el Desafío
+c1_circles_label=Círculos a avanzar:
+c1_explain_title=¿Cómo llegó el niño hasta la cometa?
+c1_explain_step1=Identifica los círculos\ny sus tamaños
+c1_explain_step2=Suma todos\nlos valores
+c1_explain_step3=El resultado es la\nposición de la cometa
+c1_explain_body_prefix=Sumando la distancia:
+c1_explain_body_prefix_b=Sumando los arcos:
+c1_explain_body_suffix=, llegamos a una nueva posición en la recta numérica.
+c1_explain_body_line2=Las fracciones ayudan a representar partes de un camino.

+ 31 - 1
assets/lang/fr_FR

@@ -95,4 +95,34 @@ s2_diff_3_description=Plus la difficulté est grande, plus il y a de subdivision
 s2_diff_4_description=Plus la difficulté est grande, plus il y a de subdivisions
 s2_diff_5_description=Plus la difficulté est grande, plus il y a de subdivisions
 label_description=Afficher les fractions à côté des figures
-info_description=En savoir plus
+info_description=En savoir plus
+s1_challenge_title=Défi du Niveau !
+s1_challenge_subtitle=Tu peux le découvrir ?\nLe tracteur va transporter ces blocs vers un trou dans le sol...
+s1_challenge_question=Quelle doit être\nla taille\ndu trou ?
+s1_challenge_question_b=Combien de blocs\nsont nécessaires pour couvrir\nle trou dans le sol ?
+s1_challenge_accept=Accepter le Défi
+s1_blocks_label=Blocs à transporter :
+s1_explain_title=Comment trouver la taille du trou ?
+s1_explain_title_b=Combien de blocs nous faut-il ?
+s1_explain_step1=Identifie les blocs\net leurs tailles
+s1_explain_step2=Additionne toutes\nles valeurs
+s1_explain_step3=Le résultat est\nla taille du trou !
+s1_explain_body=Chaque bloc a la taille indiquée.\nNous additionnons leurs valeurs pour déterminer\nla taille du trou dans le sol !
+s1_explain_body_b=Chaque bloc a la taille indiquée.\nNous additionnons leurs unités pour déterminer\nle nombre de blocs nécessaires pour couvrir le trou !
+s1_explain_body_wrong=La taille sélectionnée n'est pas correcte.
+s1_hole_label=Taille du trou
+s1_hole_label_b=Nombre de blocs
+c1_challenge_title=Défi du Niveau !
+c1_challenge_subtitle=Tu peux le découvrir ?\nAide le garçon à atteindre la comète...
+c1_challenge_question=À quelle position le garçon\narrivera-t-il avec cet avancement ?
+c1_challenge_question_b=Combien d'arcs sont nécessaires\npour atteindre la comète ?
+c1_challenge_accept=Accepter le Défi
+c1_circles_label=Cercles à avancer :
+c1_explain_title=Comment le garçon\nest-il arrivé à la comète ?
+c1_explain_step1=Identifie les cercles\net leurs tailles
+c1_explain_step2=Additionne toutes\nles valeurs
+c1_explain_step3=Le résultat est la\nposition de la comète
+c1_explain_body_prefix=En additionnant la distance :
+c1_explain_body_prefix_b=En additionnant les arcs :
+c1_explain_body_suffix=, on atteint une nouvelle position.
+c1_explain_body_line2=Les fractions aident à représenter des parties d'un chemin.

+ 31 - 1
assets/lang/it_IT

@@ -95,4 +95,34 @@ s2_diff_3_description=Maggiore è la difficoltà, più suddivisioni ci sono
 s2_diff_4_description=Maggiore è la difficoltà, più suddivisioni ci sono
 s2_diff_5_description=Maggiore è la difficoltà, più suddivisioni ci sono
 label_description=Mostra le frazioni accanto alle figure
-info_description=Scopri di più
+info_description=Scopri di più
+s1_challenge_title=Sfida del Livello!
+s1_challenge_subtitle=Riesci a scoprirlo?\nIl trattore porterà questi blocchi a un buco nel terreno...
+s1_challenge_question=Quale deve essere\nla dimensione\ndel buco?
+s1_challenge_question_b=Quanti blocchi sono\nnecessari per riempire\nil buco nel terreno?
+s1_challenge_accept=Accetta la Sfida
+s1_blocks_label=Blocchi da trasportare:
+s1_explain_title=Come scopriamo la dimensione del buco?
+s1_explain_title_b=Quanti blocchi sono necessari?
+s1_explain_step1=Identifica i blocchi\ne le loro dimensioni
+s1_explain_step2=Somma tutti\ni valori
+s1_explain_step3=Il risultato è\nla dimensione del buco!
+s1_explain_body=Ogni blocco ha la dimensione specificata.\nSommiamo i loro valori per determinare\nla dimensione del buco nel terreno!
+s1_explain_body_b=Ogni blocco ha la dimensione specificata.\nSommiamo le loro unità per determinare\nla quantità di blocchi necessari per coprire il buco!
+s1_explain_body_wrong=La dimensione selezionata non è corretta.
+s1_hole_label=Dimensione del buco
+s1_hole_label_b=Quantità di blocchi
+c1_challenge_title=Sfida del Livello!
+c1_challenge_subtitle=Riesci a scoprirlo?\nAiuta il bambino ad arrivare all'aquilone...
+c1_challenge_question=In quale posizione arriverà\nil bambino con questo avanzamento?
+c1_challenge_question_b=Quanti archi sono necessari\nper raggiungere l'aquilone?
+c1_challenge_accept=Accetta la Sfida
+c1_circles_label=Cerchi da avanzare:
+c1_explain_title=Come ha fatto il bambino\nad arrivare all'aquilone?
+c1_explain_step1=Identifica i cerchi\ne le loro dimensioni
+c1_explain_step2=Somma tutti\ni valori
+c1_explain_step3=Il risultato è la\nposizione dell'aquilone
+c1_explain_body_prefix=Sommando la distanza:
+c1_explain_body_prefix_b=Sommando gli archi:
+c1_explain_body_suffix=, arriviamo a una nuova posizione sulla retta numerica.
+c1_explain_body_line2=Le frazioni aiutano a rappresentare parti di un percorso.

+ 32 - 1
assets/lang/pt_BR

@@ -94,4 +94,35 @@ s2_diff_3_description=Quanto maior a dificuldade, mais subdivisões
 s2_diff_4_description=Quanto maior a dificuldade, mais subdivisões
 s2_diff_5_description=Quanto maior a dificuldade, mais subdivisões
 label_description=Mostrar frações do lado da figuras
-info_description=Saiba mais
+info_description=Saiba mais
+s2_challenge_accept=Aceitar o Desafio
+s1_challenge_title=Desafio da Fase!
+s1_challenge_subtitle=Você consegue descobrir?\nO trator vai carregar esses blocos para um buraco no chão...
+s1_challenge_question=Qual deve ser\no tamanho\ndo buraco?
+s1_challenge_question_b=Quantos blocos são\nnecessários para cobrir\no buraco no chão?
+s1_challenge_accept=Aceitar o Desafio
+s1_blocks_label=Blocos a carregar:
+s1_explain_title=Como descobrimos o tamanho do buraco?
+s1_explain_title_b=Como descobrimos a quantidade de blocos?
+s1_explain_step1=Identifique os blocos\ne seus tamanhos
+s1_explain_step2=Some todos os\nvalores
+s1_explain_step3=O resultado é o\ntamanho do buraco!
+s1_explain_body=Cada bloco tem o tamanho especificado.\nSomamos seus valores para determinar\no tamanho do buraco no chão!
+s1_explain_body_b=Cada bloco tem o tamanho especificado.\nSomamos suas unidades para determinar\na quantidade de blocos necessários para cobrir o buraco!
+s1_explain_body_wrong=O tamanho selecionado não está correto.
+s1_hole_label=Tamanho do buraco
+s1_hole_label_b=Quantidade de blocos
+c1_challenge_title=Desafio da Fase!
+c1_challenge_subtitle=Você consegue descobrir?\nAjude o menino a chegar até a pipa...
+c1_challenge_question=Em que posição o menino\nvai chegar com esse avanço?
+c1_challenge_question_b=Quantos arcos são necessários\npara chegar até a pipa?
+c1_challenge_accept=Aceitar o Desafio
+c1_circles_label=Círculos a avançar:
+c1_explain_title=Como o menino conseguiu chegar até a pipa?
+c1_explain_step1=Identifique os círculos\ne seus tamanhos
+c1_explain_step2=Some todos\nos valores
+c1_explain_step3=O resultado é a\nposição da pipa
+c1_explain_body_prefix=Somando a distância:
+c1_explain_body_prefix_b=Somando os arcos:
+c1_explain_body_suffix=, chegamos a uma nova posição na reta numérica.
+c1_explain_body_line2=As frações ajudam a representar partes de um caminho.

+ 6 - 4
js/gameMechanics.js

@@ -236,7 +236,7 @@ const game = {
 
       game.lang = {}; // Clear previously loaded language
 
-      fetch(url, { mode: 'same-origin' })
+      fetch(url + '?v=' + Date.now(), { mode: 'same-origin' })
         .then((response) => {
           if (!response.ok)
             throw new Error(
@@ -250,10 +250,12 @@ const game = {
           game.loadHandler.max += lines.length;
           lines.forEach((line) => {
             try {
-              const msg = line.split('=');
-              if (msg.length !== 2)
+              const eqIdx = line.indexOf('=');
+              if (eqIdx < 1)
                 throw Error('Game error: sintax error in i18y file.');
-              game.lang[msg[0].trim()] = msg[1].trim();
+              const key = line.slice(0, eqIdx).trim();
+              const value = line.slice(eqIdx + 1).trim();
+              game.lang[key] = value;
             } catch (error) {
               console.error(error.message);
             }

+ 407 - 17
js/games/circleOne.js

@@ -57,6 +57,7 @@ const circleOne = {
     this.ui = {
       help: undefined,
       message: undefined,
+      challenge: {},
       continue: {
         // modal: undefined,
         button: undefined,
@@ -121,6 +122,8 @@ const circleOne = {
       hasClicked: false, // Checks if user has clicked
       checkAnswer: false, // Check kid on top of kiteline
       isCorrect: false, // Informs answer is correct
+      showChallenge: false,
+      challengeAnsweredYes: null,
       showEndInfo: false,
       endSignX: undefined,
       curWalkedPath: 0,
@@ -165,10 +168,10 @@ const circleOne = {
     this.restart = restart;
 
     this.utils.renderCharacters(validPath, kiteX);
-    this.utils.renderMainUI();
+
+    this.utils.renderChallengeUI();
 
     if (!this.restart) {
-      game.timer.start(); // Set a timer for the current level (used in postScore())
       game.event.add('click', this.events.onInputDown);
       game.event.add('mousemove', this.events.onInputOver);
     }
@@ -597,6 +600,205 @@ const circleOne = {
         self.kite.alpha = 1;
       }
     },
+    renderChallengeUI: function () {
+      // Hide game kite line during challenge
+      self.kite_line.alpha = 0;
+      self.kite.alpha = 0;
+
+      const cx = context.canvas.width / 2;
+      const withNewlines = (s) => (s == null ? '' : String(s).replace(/\\n/g, '\n'));
+      const FRAC_UNICODE = { 1: '1', 2: '\u00BD', 4: '\u00BC' };
+
+      const ribbonScale = 0.87;
+      const ribbonH = Math.round(166 * ribbonScale);
+      const ribbonY = 30;
+
+      self.ui.challenge.image = game.add.image(cx, ribbonY, 'challenge-card', ribbonScale, 1);
+      self.ui.challenge.image.anchor(0.5, 0);
+
+      const ribbonCenterY = ribbonY + ribbonH / 2 + 2;
+      self.ui.challenge.title = game.add.text(
+        cx, ribbonCenterY,
+        withNewlines(game.lang.c1_challenge_title),
+        { ...textStyles.h2_, fill: colors.white, font: 'bold ' + textStyles.h2_.font }
+      );
+      self.ui.challenge.title.anchor(0.5, 0.5);
+
+      const ribbonBottom = ribbonY + ribbonH;
+      const subtitleLines = withNewlines(game.lang.c1_challenge_subtitle).split('\n');
+      self.ui.challenge.subtitleTop = game.add.text(
+        cx, ribbonBottom + 48,
+        subtitleLines[0] || '',
+        { ...textStyles.h4_, fill: colors.blueDark, font: 'bold ' + textStyles.h4_.font }
+      );
+      self.ui.challenge.subtitleTop.anchor(0.5, 0.5);
+      self.ui.challenge.subtitleBottom = game.add.text(
+        cx, ribbonBottom + 100,
+        subtitleLines.slice(1).join('\n'),
+        { ...textStyles.h3_, fill: colors.blue }
+      );
+      self.ui.challenge.subtitleBottom.anchor(0.5, 0.5);
+
+      const topCircleY = self.road.defaultY + 20 - 5 - self.circles.diameter / 2
+        - (self.circles.list.length - 1) * self.circles.diameter;
+      const kidHeadY = self.kid ? self.kid.y - 155 : topCircleY;
+      const labelY = Math.min(topCircleY, kidHeadY) - 30;
+      self.ui.challenge.circlesLabel = game.add.text(
+        self.road.x, labelY,
+        withNewlines(game.lang.c1_circles_label),
+        { ...textStyles.h4_, fill: colors.blueDark, font: 'bold ' + textStyles.h4_.font }
+      );
+      self.ui.challenge.circlesLabel.anchor(0.5, 0.5);
+
+      const cardW = 580; const cardH = 260;
+      const cardX = cx;
+      const cardY = context.canvas.height / 2 - 80;
+      self.ui.challenge.card = game.add.geom.rect(
+        cardX, cardY, cardW, cardH, colors.white, 0.95, colors.blueMenuLine, 4
+      );
+      self.ui.challenge.card.anchor(0.5, 0.5);
+
+      const questionWrapped = withNewlines(
+        gameMode === 'b' ? (game.lang.c1_challenge_question_b || game.lang.c1_challenge_question) : game.lang.c1_challenge_question
+      );
+      const qLines = questionWrapped.split('\n').length;
+      const qFontSize = qLines === 3 ? 30 : qLines > 3 ? 26 : 32;
+      const qLineH   = qLines >= 3 ? 34 : 40;
+      const qOffsetY  = qLines >= 3 ? -80 : -65;
+      self.ui.challenge.question = game.add.text(
+        cardX, cardY + qOffsetY,
+        questionWrapped,
+        { ...textStyles.h3_, fill: colors.blueDark, font: `bold ${qFontSize}px ${font.families.default}` },
+        qLineH
+      );
+      self.ui.challenge.question.anchor(0.5, 0.5);
+
+      // Builds equation string for a slice of circles; globalStart tracks
+      // sign context so the very first circle of the whole equation has no
+      // leading '+' even when called for a sub-range.
+      const buildEqStr = (slice, globalStart) => {
+        return slice.map((circle, i) => {
+          const den = circle.info.fraction.denominator;
+          const nom = circle.info.fraction.nominator; // 1 or -1
+          const frac = FRAC_UNICODE[den] ?? `1/${den}`;
+          if (globalStart + i === 0) return nom < 0 ? '-' + frac : frac;
+          return (nom < 0 ? ' - ' : ' + ') + frac;
+        }).join('');
+      };
+
+      // Mode B: only the first correctIndex circles are the answer
+      const challengeCircles = gameMode === 'b'
+        ? self.circles.list.slice(0, self.control.correctIndex)
+        : self.circles.list;
+
+      // Hide extra circles (beyond answer count) so stack matches equation
+      if (gameMode === 'b') {
+        self.circles.list.forEach((c, i) => {
+          if (i >= self.control.correctIndex) {
+            c.alpha = 0;
+            c.info.fraction.labels.forEach(l => { if (l) l.alpha = 0; });
+          }
+        });
+      }
+
+      const circleScale = 0.20;
+      const circleR = 35;
+      const gap = 10;
+      const eqStyle = { ...textStyles.h3_, fill: colors.blueDark };
+      const maxW = cardW - 80;
+      const n = challengeCircles.length;
+
+      context.save();
+      context.font = '38px Arial, sans-serif';
+
+      const fullStr = buildEqStr(challengeCircles, 0) + ' =';
+      const fullW = context.measureText(fullStr).width;
+
+      if (fullW + gap + circleR * 2 <= maxW) {
+        const eqY = cardY + 42;
+        const textX = cardX - gap / 2 - circleR;
+        const circleX = cardX + fullW / 2 + gap / 2;
+        self.ui.challenge.equation = game.add.text(textX, eqY, fullStr, eqStyle);
+        self.ui.challenge.equationCircle = game.add.image(circleX, eqY - 14, 'circular-question', circleScale, 1);
+        self.ui.challenge.equationCircle.anchor(0.5, 0.5);
+      } else {
+        const mid = Math.ceil(n / 2);
+        const nextNom = challengeCircles[mid].info.fraction.nominator;
+        const connector = nextNom < 0 ? ' -' : ' +';
+        const line1Str = buildEqStr(challengeCircles.slice(0, mid), 0) + connector;
+        const line2Str = challengeCircles.slice(mid).map((circle, i) => {
+          const den = circle.info.fraction.denominator;
+          const nom = circle.info.fraction.nominator;
+          const frac = FRAC_UNICODE[den] ?? `1/${den}`;
+          if (i === 0) return frac; // connector already shown at end of line 1
+          return (nom < 0 ? ' - ' : ' + ') + frac;
+        }).join('') + ' =';
+        const line2W = context.measureText(line2Str).width;
+
+        const line1Y = cardY + 28;
+        const line2Y = cardY + 68;
+
+        self.ui.challenge.equation = game.add.text(cardX, line1Y, line1Str, eqStyle);
+        self.ui.challenge.equationLine2 = game.add.text(cardX, line2Y, line2Str, eqStyle);
+        const line2CircleX = cardX + line2W / 2 + gap + circleR;
+        self.ui.challenge.equationCircle = game.add.image(line2CircleX, line2Y - 14, 'circular-question', circleScale, 1);
+        self.ui.challenge.equationCircle.anchor(0.5, 0.5);
+      }
+
+      context.restore();
+
+      // Decorative kite rendered AFTER the card so it appears in front of it
+      // Positioned to the right of the card, same scale/anchor as the game kite
+      const kiteImg = gameOperation === 'minus' ? 'kite_reverse' : 'kite';
+      // kite_reverse extends leftward from its anchor, so needs more offset to clear the card
+      const kiteOffset = gameOperation === 'minus' ? 250 : 80;
+      const decorX = cardX + cardW / 2 + kiteOffset;
+      const decorLineY = self.road.defaultY - 10;
+      const decorKiteY = self.road.defaultY - 275;
+      self.ui.challenge.kiteLineDecor = game.add.image(decorX, decorLineY, 'kite_line', 2, 1);
+      self.ui.challenge.kiteLineDecor.anchor(0.5, 0);
+      self.ui.challenge.kiteDecor = game.add.image(decorX, decorKiteY, kiteImg, 1.8, 1);
+      // kite_reverse attaches at its right edge; regular kite attaches at left edge
+      self.ui.challenge.kiteDecor.anchor(gameOperation === 'minus' ? 1 : 0, 0.5);
+
+      const btnW = cardW; const btnH = 90;
+      const btnY = cardY + cardH / 2 + 65;
+      self.ui.challenge.button = game.add.geom.rect(cardX, btnY, btnW, btnH, '#e09800', 1);
+      self.ui.challenge.button.anchor(0.5, 0.5);
+      self.ui.challenge.buttonText = game.add.text(
+        cardX, btnY + 14,
+        withNewlines(game.lang.c1_challenge_accept),
+        textStyles.btn
+      );
+      self.ui.challenge.buttonText.anchor(0.5, 0.5);
+
+      self.control.showChallenge = true;
+    },
+
+    acceptChallenge: function () {
+      self.control.challengeAnsweredYes = true;
+      Object.values(self.ui.challenge).forEach(el => {
+        if (el && typeof el.alpha !== 'undefined') el.alpha = 0;
+      });
+      // Restore extra circles hidden during challenge (mode B)
+      if (gameMode === 'b') {
+        self.circles.list.forEach((c, i) => {
+          if (i >= self.control.correctIndex) {
+            c.alpha = 1;
+            if (showFractions) {
+              c.info.fraction.labels.forEach(l => { if (l) l.alpha = 1; });
+            }
+          }
+        });
+      }
+      // Restore game kite and line
+      self.kite_line.alpha = gameMode === 'b' ? 1 : 0.8;
+      self.kite.alpha = gameMode === 'b' ? 1 : 0.5;
+      self.control.showChallenge = false;
+      self.utils.renderMainUI();
+      if (!self.restart) game.timer.start();
+    },
+
     renderMainUI: function () {
       // Help pointer
       self.ui.help = game.add.image(0, 0, 'pointer', 2, 0);
@@ -851,6 +1053,171 @@ const circleOne = {
 
       return endSignX;
     },
+    renderExplanationUI: function () {
+      const cx = context.canvas.width / 2;
+      const cy = context.canvas.height / 2;
+      const withNewlines = (s) => (s == null ? '' : String(s).replace(/\\n/g, '\n'));
+      const FRAC_UNICODE = { 1: '1', 2: '\u00BD', 4: '\u00BC' };
+
+      // Reuse end-tractor-game background image (globally loaded)
+      const naturalW = game.image['end-tractor-game'].width;
+      const naturalH = game.image['end-tractor-game'].height;
+      const imgScale = Math.min((context.canvas.width * 0.92) / naturalW, (context.canvas.height * 0.92) / naturalH);
+      const imgW = naturalW * imgScale;
+      const imgH = naturalH * imgScale;
+      const cardTop    = cy - imgH / 2;
+      const cardBottom = cy + imgH / 2;
+      const cardLeft   = cx - imgW / 2;
+
+      const bgImg = game.add.image(cx, cy, 'end-tractor-game');
+      bgImg.anchor(0.5, 0.5);
+      bgImg.scale = imgScale;
+
+      // Mask over the tractor scene area of the background image
+      const vizMask = game.add.geom.rect(cx, cardTop + imgH * 0.575, imgW * 0.9, imgH * 0.36, colors.white, 1);
+      vizMask.anchor(0.5, 0.5);
+
+      // Title — move up and reduce font when 2 lines to avoid overlapping steps
+      const titleText = withNewlines(game.lang.c1_explain_title);
+      const titleIsMultiline = titleText.includes('\n');
+      const titleFontSize = titleIsMultiline ? 24 : parseInt(textStyles.h3_.font);
+      const titleY = cardTop + imgH * (titleIsMultiline ? 0.04 : 0.07);
+      game.add.text(cx + imgW * 0.04, titleY, titleText,
+        { ...textStyles.h3_, fill: colors.white, font: `bold ${titleFontSize}px ${font.families.default}` }
+      );
+
+      // Step labels (same positions as squareOne explanation)
+      const stepFont = `24px ${font.families.default}`;
+      const stepStyle = { ...textStyles.p_, fill: colors.blueDark, font: stepFont };
+      const stepsLine1Y = cardTop + imgH * 0.245;
+      const stepsLine2Y = stepsLine1Y + 26;
+      const stepCenters = [cardLeft + imgW * 0.28, cardLeft + imgW * 0.50, cardLeft + imgW * 0.75];
+      [game.lang.c1_explain_step1, game.lang.c1_explain_step2, game.lang.c1_explain_step3]
+        .map(withNewlines)
+        .forEach((txt, idx) => {
+          const lines = txt.split('\n');
+          game.add.text(stepCenters[idx], stepsLine1Y, lines[0] || '', stepStyle);
+          if (lines[1]) game.add.text(stepCenters[idx], stepsLine2Y, lines[1], stepStyle);
+        });
+
+      // Mode B: use the circles the player actually selected (not the pre-set correctIndex)
+      const explainCircles = gameMode === 'b'
+        ? self.circles.list.slice(0, self.control.selectedIndex + 1)
+        : self.circles.list;
+
+      // Compute final position
+      let totalDistance = 0;
+      explainCircles.forEach(c => {
+        totalDistance += c.info.fraction.nominator / c.info.fraction.denominator;
+      });
+      const startPos = gameOperation === 'minus' ? 5 : 0;
+      const finalPos = startPos + totalDistance;
+
+      const formatLabel = (n) => {
+        if (n < 0) return '-' + formatLabel(-n);
+        if (Number.isInteger(n)) return String(n);
+        const whole = Math.floor(n);
+        const frac = n - whole;
+        const FM = { 0.25: '\u00BC', 0.5: '\u00BD', 0.75: '\u00BE' };
+        const f = FM[Math.round(frac * 4) / 4];
+        return f ? (whole ? String(whole) + f : f) : n.toFixed(2).replace('.', ',');
+      };
+
+      const buildEqStr = (list) => list.map((c, i) => {
+        const frac = FRAC_UNICODE[c.info.fraction.denominator] ?? `1/${c.info.fraction.denominator}`;
+        if (i === 0) return c.info.fraction.nominator < 0 ? '-' + frac : frac;
+        return (c.info.fraction.nominator < 0 ? ' - ' : ' + ') + frac;
+      }).join('');
+
+      // For minus: equation shows the displacement (totalDistance, negative)
+      // For plus/mixed: equation shows final position
+      const eqResult = gameOperation === 'minus' ? totalDistance : finalPos;
+
+      // Equation in image's equation box area
+      const eqFontSize = explainCircles.length <= 4 ? 38 : 30;
+      game.add.text(cx, cardTop + imgH * 0.355,
+        buildEqStr(explainCircles) + ' = ' + formatLabel(eqResult),
+        { ...textStyles.h3_, fill: colors.blueDark, font: `bold ${eqFontSize}px ${font.families.default}` }
+      );
+
+      // Number line
+      const lineY   = cardTop + imgH * 0.58;
+      const lineLeft  = cardLeft + imgW * 0.08;
+      const lineRight = cardLeft + imgW * 0.92;
+      const lineW   = lineRight - lineLeft;
+      const pointStep = lineW / 5;
+      const barH    = 20;
+
+      // Base bar
+      game.add.geom.rect(lineLeft, lineY - barH / 2, lineW, barH, '#d4c080', 0.5);
+
+      // Colored segment showing the walked path
+      // For minus: 0 is at lineRight, segment goes leftward by |totalDistance|
+      // For plus/mixed: 0 is at lineLeft, segment goes rightward to finalPos
+      const fillColor   = gameOperation === 'minus' ? colors.redLight : colors.greenLight;
+      const borderColor = gameOperation === 'minus' ? colors.red : colors.green;
+      const barStartX = gameOperation === 'minus' ? lineRight : lineLeft + startPos * pointStep;
+      const barEndX   = gameOperation === 'minus' ? lineRight + totalDistance * pointStep : lineLeft + finalPos * pointStep;
+      game.add.geom.rect(
+        Math.min(barStartX, barEndX), lineY - barH / 2,
+        Math.abs(barEndX - barStartX), barH,
+        fillColor, 0.9, borderColor, 2
+      );
+
+      // Integer position markers
+      // For minus: shows -5,-4,-3,-2,-1,0 (0 at right)
+      // For plus/mixed: shows 0,1,2,3,4,5
+      for (let i = 0; i <= 5; i++) {
+        const px = lineLeft + i * pointStep;
+        const markerCX = px + 18;
+        const markerCY = lineY + barH / 2 + 16;
+        game.add.geom.circle(markerCX, markerCY, 44, colors.blueDark, 2, colors.white, 1).anchor(0.5, 0.5);
+        const displayVal = gameOperation === 'minus' ? (i - 5) : i;
+        game.add.text(markerCX - 20, markerCY - 11, String(displayVal),
+          { ...textStyles.p_, fill: colors.blueDark, font: `bold 22px ${font.families.default}` }
+        ).anchor(0.5, 0.5);
+      }
+
+      // Orange label at answer position
+      // For minus: answer is at totalDistance from the right (0)
+      // For plus/mixed: answer is at finalPos from the left (0)
+      const answerX = gameOperation === 'minus' ? lineRight + totalDistance * pointStep : lineLeft + finalPos * pointStep;
+      const marker = game.add.geom.rect(answerX, lineY - barH / 2 - 26, 80, 32, '#e09800', 1);
+      marker.anchor(0.5, 0.5);
+      game.add.text(answerX, lineY - barH / 2 - 17, formatLabel(eqResult),
+        { ...textStyles.p_, fill: colors.white, font: `bold 24px ${font.families.default}` }
+      );
+      game.add.geom.line(answerX, lineY - barH / 2 - 9, answerX, lineY - barH / 2, 2, '#e09800');
+
+      // Body text
+      const bodyY1 = cardTop + imgH * 0.825;
+      const bodyY2 = bodyY1 + 34;
+      const bodyPrefix = gameMode === 'b'
+        ? (game.lang.c1_explain_body_prefix_b || game.lang.c1_explain_body_prefix)
+        : game.lang.c1_explain_body_prefix;
+      game.add.text(cx, bodyY1,
+        withNewlines(bodyPrefix) + ' ' +
+        buildEqStr(explainCircles) +
+        withNewlines(game.lang.c1_explain_body_suffix),
+        { ...textStyles.p_, fill: colors.blueDark }
+      );
+      game.add.text(cx, bodyY2, withNewlines(game.lang.c1_explain_body_line2),
+        { ...textStyles.p_, fill: colors.blue }
+      );
+
+      // Checkmark top-right
+      const checkScale = imgH * 0.13 / 256;
+      const checkImg = game.add.image(cardLeft + imgW - 16, cardTop + 16, 'answer_correct', checkScale);
+      checkImg.anchor(1, 0);
+
+      // Continue button
+      const btnW = imgW * 0.38; const btnH = 62;
+      const btnCY = cardBottom - imgH * 0.07;
+      self.ui.continue.button = game.add.geom.rect(cx, btnCY, btnW, btnH, colors.green);
+      self.ui.continue.button.anchor(0.5, 0.5);
+      self.ui.continue.text = game.add.text(cx, btnCY + 14, game.lang.continue, textStyles.btn);
+    },
+
     renderEndUI: function () {
       let btnColor = colors.green;
       let btnText = game.lang.continue;
@@ -962,11 +1329,8 @@ const circleOne = {
 
       self.control.isCorrect = game.math.isOverlap(self.kite_line, self.kid);
 
-      const x = self.utils.renderOperationUI();
       if (self.control.isCorrect) {
         completedLevels++;
-        // self.kid.curFrame = self.kid.curFrame < 12 ? 24 : 25;
-        // console.log(self.kid);
         self.kid.alpha = 0;
         const kidStanding = game.add.sprite(
           self.kid.x,
@@ -984,23 +1348,26 @@ const circleOne = {
         self.kite.y -= 40;
 
         if (audioStatus) game.audio.okSound.play();
-        game.add
-          .image(x + 50, context.canvas.height / 3, 'answer_correct')
-          .anchor(0.5, 0.5);
         if (isDebugMode) console.log('Completed Levels: ' + completedLevels);
+
+        self.fetch.postScore();
+        self.control.checkAnswer = false;
+        self.animation.counter = 0;
+        self.utils.renderExplanationUI();
+        self.control.showEndInfo = true;
+        canGoToNextMapPosition = true;
       } else {
+        const x = self.utils.renderOperationUI();
         if (audioStatus) game.audio.errorSound.play();
         game.add
           .image(x, context.canvas.height / 3, 'answer_wrong')
           .anchor(0.5, 0.5);
-      }
-
-      self.fetch.postScore();
-
-      self.control.checkAnswer = false;
-      self.animation.counter = 0;
 
-      self.animation.animateKite = true;
+        self.fetch.postScore();
+        self.control.checkAnswer = false;
+        self.animation.counter = 0;
+        self.animation.animateKite = true;
+      }
     },
     animateKiteHandler: function () {
       self.animation.counter++;
@@ -1016,8 +1383,7 @@ const circleOne = {
       if (self.animation.counter === 100) {
         self.utils.renderEndUI();
         self.control.showEndInfo = true;
-        if (self.control.isCorrect) canGoToNextMapPosition = true;
-        else canGoToNextMapPosition = false;
+        canGoToNextMapPosition = false;
       }
     },
     endLevel: function () {
@@ -1183,6 +1549,16 @@ const circleOne = {
       const x = game.math.getMouse(mouseEvent).x;
       const y = game.math.getMouse(mouseEvent).y;
 
+      if (self.control.showChallenge) {
+        if (game.math.isOverIcon(x, y, self.ui.challenge.button)) {
+          if (audioStatus) game.audio.popSound.play();
+          self.utils.acceptChallenge();
+        }
+        navigation.onInputDown(x, y);
+        game.render.all();
+        return;
+      }
+
       // GAME MODE A : click road
       if (gameMode === 'a') {
         const isValidX = self.utils.isOverRoad(
@@ -1236,6 +1612,20 @@ const circleOne = {
       const y = game.math.getMouse(mouseEvent).y;
       let isOverCircle = false;
 
+      if (self.control.showChallenge && self.ui.challenge.button) {
+        if (game.math.isOverIcon(x, y, self.ui.challenge.button)) {
+          self.ui.challenge.button.scale = self.ui.challenge.button.initialScale * 1.1;
+          self.ui.challenge.buttonText.style = textStyles.btnLg;
+          document.body.style.cursor = 'pointer';
+        } else {
+          self.ui.challenge.button.scale = self.ui.challenge.button.initialScale * 1;
+          self.ui.challenge.buttonText.style = textStyles.btn;
+          document.body.style.cursor = 'auto';
+        }
+        game.render.all();
+        return;
+      }
+
       if (gameMode === 'a' && !self.control.hasClicked) {
         const isValidX = self.utils.isOverRoad(
           x,

+ 432 - 77
js/games/squareOne.js

@@ -67,8 +67,8 @@ const squareOne = {
       arrow: undefined,
       help: undefined,
       message: undefined,
-      continue: {
-        // modal: undefined,
+      challenge: {},
+      explanation: {
         button: undefined,
         text: undefined,
       },
@@ -83,6 +83,10 @@ const squareOne = {
       isCorrect: false, // Checks player 'answer'
 
       count: 0, // An 'x' position counter used in the tractor animation
+
+      showChallenge: false, // When true, challenge modal is displayed (blocks interaction gated)
+      showExplanation: false, // When true, explanation modal is displayed at end of level
+      challengeAnsweredYes: null, // null = not yet answered, true = accepted challenge
     };
     this.animation = {
       animateTractor: false, // When true allows game to run 'tractor animation' code in update (turns animation of the moving tractor ON/OFF)
@@ -177,14 +181,11 @@ const squareOne = {
       correctXA
     );
     this.utils.renderCharacters();
-    this.utils.renderMainUI();
-
     this.restart = restart;
-    if (!this.restart) {
-      game.timer.start(); // Set a timer for the current level (used in postScore())
-      game.event.add('click', this.events.onInputDown);
-      game.event.add('mousemove', this.events.onInputOver);
-    }
+    this.utils.renderChallengeUI();
+    // Events added now so challenge button is clickable; game block interaction gated by showChallenge flag
+    game.event.add('click', this.events.onInputDown);
+    game.event.add('mousemove', this.events.onInputOver);
   },
 
   /**
@@ -539,6 +540,183 @@ const squareOne = {
         self.tractor.curFrame = 5;
       }
     },
+    renderChallengeUI: () => {
+      const cx = context.canvas.width / 2;
+      const withNewlines = (s) => (s == null ? '' : String(s).replace(/\\n/g, '\n'));
+      const FRAC_UNICODE = { 1: '1', 2: '\u00BD', 4: '\u00BC' };
+
+      // challenge-card.png is 786×166px; scale to ~680px wide
+      const ribbonScale = 0.87;
+      const ribbonH = Math.round(166 * ribbonScale); // ≈ 144px
+      const ribbonY = 30;
+
+      // Challenge ribbon image at top center
+      self.ui.challenge.image = game.add.image(cx, ribbonY, 'challenge-card', ribbonScale, 1);
+      self.ui.challenge.image.anchor(0.5, 0);
+
+      // Title overlaid on ribbon (nudged down slightly to visual center of ribbon)
+      const ribbonCenterY = ribbonY + ribbonH / 2 + 2;
+      self.ui.challenge.title = game.add.text(
+        cx, ribbonCenterY,
+        withNewlines(game.lang.s1_challenge_title),
+        { ...textStyles.h2_, fill: colors.white, font: 'bold ' + textStyles.h2_.font }
+      );
+      self.ui.challenge.title.anchor(0.5, 0.5);
+
+      // Subtitle lines below the ribbon
+      const ribbonBottom = ribbonY + ribbonH;
+      const subtitleLines = withNewlines(game.lang.s1_challenge_subtitle).split('\n');
+      self.ui.challenge.subtitleTop = game.add.text(
+        cx, ribbonBottom + 48,
+        subtitleLines[0] || '',
+        { ...textStyles.h4_, fill: colors.blueDark, font: 'bold ' + textStyles.h4_.font }
+      );
+      self.ui.challenge.subtitleTop.anchor(0.5, 0.5);
+
+      self.ui.challenge.subtitleBottom = game.add.text(
+        cx, ribbonBottom + 100,
+        subtitleLines.slice(1).join('\n'),
+        { ...textStyles.h3_, fill: colors.blue }
+      );
+      self.ui.challenge.subtitleBottom.anchor(0.5, 0.5);
+
+      // "Blocos a carregar:" label above the stacked blocks
+      const stackTopY = self.default.y0 - self.stack.list.length * self.default.height;
+      // Center label over the block stack (x0 is the edge; blocks extend inward by direc)
+      const labelX = self.default.x0 + self.default.width / 2 * self.control.direc;
+      self.ui.challenge.blocksLabel = game.add.text(
+        labelX, stackTopY - 50,
+        withNewlines(game.lang.s1_blocks_label),
+        { ...textStyles.h4_, fill: colors.blueDark, font: 'bold ' + textStyles.h4_.font }
+      );
+      self.ui.challenge.blocksLabel.anchor(0.5, 0.5);
+
+      // question-mark-with-arrow.png is 1230×864px; scale 0.20 → 246×173px
+      // Placed at the correct floor hole position, bottom of image at floor level
+      const holeIdx = gameMode === 'b' ? self.floor.selectedIndex : self.floor.correctIndex;
+      const correctFloorBlock = self.floor.list[holeIdx];
+      const markerX = correctFloorBlock
+        ? correctFloorBlock.x + (correctFloorBlock.width / 2) * (gameOperation === 'minus' ? -1 : 1)
+        : cx;
+      self.ui.challenge.holeMarker = game.add.image(
+        markerX, self.default.y0 - 10,
+        'question-mark-with-arrow',
+        0.20, 1
+      );
+      self.ui.challenge.holeMarker.anchor(0.5, 1);
+
+      // Question card – right half, moved up from center
+      const cardW = 520; const cardH = 260;
+      const cardX = cx + (gameOperation === 'minus' ? -200 : 200);
+      const cardY = context.canvas.height / 2 - 80;
+      self.ui.challenge.card = game.add.geom.rect(
+        cardX, cardY, cardW, cardH, colors.white, 0.95, colors.blueMenuLine, 4
+      );
+      self.ui.challenge.card.anchor(0.5, 0.5);
+
+      // Question text: use \n from lang file directly
+      const questionWrapped = withNewlines(
+        gameMode === 'a' ? (game.lang.s1_challenge_question_b || game.lang.s1_challenge_question) : game.lang.s1_challenge_question
+      );
+      const qLines = questionWrapped.split('\n').length;
+      const qFontSize = qLines === 3 ? 30 : qLines > 3 ? 26 : 32;
+      const qLineH   = qLines >= 3 ? 34 : 40;
+      const qOffsetY  = qLines >= 3 ? -80 : -65;
+      self.ui.challenge.question = game.add.text(
+        cardX, cardY + qOffsetY,
+        questionWrapped,
+        { ...textStyles.h3_, fill: colors.blueDark, font: `bold ${qFontSize}px ${font.families.default}` },
+        qLineH
+      );
+      self.ui.challenge.question.anchor(0.5, 0.5);
+
+      // Fraction equation: "1 + ½ + ... =" as text, then circle — group centered at cardX
+      // Show only the blocks that will fill the hole (0 → stack.correctIndex) in both modes
+      const stackEndIdx = self.stack.correctIndex ?? (self.stack.list.length - 1);
+      const equationParts = self.stack.list.slice(0, stackEndIdx + 1).map(block => {
+        const den = block.fraction.denominator;
+        return FRAC_UNICODE[den] ?? `1/${den}`;
+      });
+      const circleScale = 0.20;
+      const circleR = 35;
+      const gap = 10;
+      const eqStyle = { ...textStyles.h3_, fill: colors.blueDark };
+      const maxW = cardW - 80; // max group width before wrapping
+
+      context.save();
+      context.font = '38px Arial, sans-serif';
+
+      const isMinus = gameOperation === 'minus';
+      const eqSep   = isMinus ? ' - ' : ' + ';
+      const eqFirst = isMinus ? '-' + equationParts[0] : equationParts[0];
+      const eqRest  = equationParts.slice(1).join(eqSep);
+      const eqBody  = eqFirst + (eqRest ? eqSep + eqRest : '');
+      const fullStr = eqBody + ' =';
+      const fullW = context.measureText(fullStr).width;
+
+      if (fullW + gap + circleR * 2 <= maxW) {
+        // Single line — group centered at cardX
+        const eqY = cardY + 42;
+        const textX = cardX - gap / 2 - circleR;
+        const circleX = cardX + fullW / 2 + gap / 2;
+        self.ui.challenge.equation = game.add.text(textX, eqY, fullStr, eqStyle);
+        self.ui.challenge.equationCircle = game.add.image(circleX, eqY - 14, 'circular-question', circleScale, 1);
+        self.ui.challenge.equationCircle.anchor(0.5, 0.5);
+      } else {
+        // Two lines — split parts roughly in half
+        const mid = Math.ceil(equationParts.length / 2);
+        const line1Parts = isMinus
+          ? '-' + equationParts[0] + (mid > 1 ? eqSep + equationParts.slice(1, mid).join(eqSep) : '')
+          : equationParts.slice(0, mid).join(eqSep);
+        const line1Str = line1Parts + eqSep.trimEnd();
+        const line2Str = equationParts.slice(mid).join(eqSep) + ' =';
+        const line2W = context.measureText(line2Str).width;
+
+        const line1Y = cardY + 28;
+        const line2Y = cardY + 68;
+
+        // Both lines centered at cardX; circle goes right of line 2 text
+        self.ui.challenge.equation = game.add.text(cardX, line1Y, line1Str, eqStyle);
+        self.ui.challenge.equationLine2 = game.add.text(cardX, line2Y, line2Str, eqStyle);
+        const line2CircleX = cardX + line2W / 2 + gap + circleR;
+        self.ui.challenge.equationCircle = game.add.image(line2CircleX, line2Y - 14, 'circular-question', circleScale, 1);
+        self.ui.challenge.equationCircle.anchor(0.5, 0.5);
+      }
+
+      context.restore();
+
+      // Accept button – directly below the card
+      const btnW = cardW; const btnH = 90;
+      const btnY = cardY + cardH / 2 + 65;
+      self.ui.challenge.button = game.add.geom.rect(cardX, btnY, btnW, btnH, '#e09800', 1);
+      self.ui.challenge.button.anchor(0.5, 0.5);
+      self.ui.challenge.buttonText = game.add.text(
+        cardX, btnY + 14,
+        withNewlines(game.lang.s1_challenge_accept),
+        textStyles.btn
+      );
+      self.ui.challenge.buttonText.anchor(0.5, 0.5);
+
+      self.control.showChallenge = true;
+    },
+
+    acceptChallenge: () => {
+      self.control.challengeAnsweredYes = true;
+      // Hide all challenge overlay elements
+      Object.values(self.ui.challenge).forEach(el => {
+        if (el && typeof el.alpha !== 'undefined') el.alpha = 0;
+      });
+      self.control.showChallenge = false;
+
+      // Show main game UI (intro message, selection arrow)
+      self.utils.renderMainUI();
+            console.log('Starting game with config:');
+      // Start timer now that challenge has been accepted
+      if (!self.restart) {
+        game.timer.start();
+      }
+    },
+
     renderMainUI: () => {
       // Help pointer
       self.ui.help = game.add.image(0, 0, 'pointer', 1.7, 0);
@@ -819,30 +997,179 @@ const squareOne = {
 
       return endSignX;
     },
+    renderExplanationUI: () => {
+      const cx = context.canvas.width / 2;
+      const cy = context.canvas.height / 2;
+      const withNewlines = (s) => (s == null ? '' : String(s).replace(/\\n/g, '\n'));
+      const FRAC_UNICODE = { 1: '1', 2: '\u00BD', 4: '\u00BC' };
+      const divisor = gameDifficulty == 3 ? 4 : gameDifficulty;
+
+      // CORRECT ANSWER: full explanation card — positions based on actual image dimensions
+      const naturalW = game.image['end-tractor-game'].width;
+      const naturalH = game.image['end-tractor-game'].height;
+      const canvasW  = context.canvas.width;
+      const canvasH  = context.canvas.height;
+      const imgScale = Math.min((canvasW * 0.92) / naturalW, (canvasH * 0.92) / naturalH);
+      const imgW = naturalW * imgScale;
+      const imgH = naturalH * imgScale;
+      const cardTop    = cy - imgH / 2;
+      const cardBottom = cy + imgH / 2;
+      const cardLeft   = cx - imgW / 2;
+
+      // Background image — scaled to fit canvas
+      const bgImg = game.add.image(cx, cy, 'end-tractor-game');
+      bgImg.anchor(0.5, 0.5);
+      bgImg.scale = imgScale;
+
+      // Title overlaid on the image's top blue bar (no emoji — image already has magnifying glass)
+      const titleText = withNewlines(
+        gameMode === 'a' ? (game.lang.s1_explain_title_b || game.lang.s1_explain_title) : game.lang.s1_explain_title
+      );
+      game.add.text(cx + imgW * 0.04, cardTop + imgH * 0.07, titleText, {
+        ...textStyles.h3_, fill: colors.white, font: 'bold ' + textStyles.h3_.font,
+      });
+
+      // Step labels below the image's built-in circles (image already draws 1 → 2 → 3)
+      const stepTexts = [
+        withNewlines(game.lang.s1_explain_step1),
+        withNewlines(game.lang.s1_explain_step2),
+        withNewlines(game.lang.s1_explain_step3),
+      ];
+      const stepFont = `24px ${font.families.default}`;
+      const stepStyle = { ...textStyles.p_, fill: colors.blueDark, font: stepFont };
+      const stepsLine1Y = cardTop + imgH * 0.245;
+      const stepsLine2Y = stepsLine1Y + 26;
+      // Positions centred under each circle in the background image
+      const stepCenters = [
+        cardLeft + imgW * 0.28,
+        cardLeft + imgW * 0.50,
+        cardLeft + imgW * 0.75,
+      ];
+      stepCenters.forEach((sx, idx) => {
+        const lines = stepTexts[idx].split('\n');
+        game.add.text(sx, stepsLine1Y, lines[0] || '', stepStyle);
+        if (lines[1]) game.add.text(sx, stepsLine2Y, lines[1], stepStyle);
+      });
+
+      // Fraction equation — placed in the image's equation box area
+      let holeNumerator = 0;
+      const lastIdx = self.stack.correctIndex ?? (self.stack.list.length - 1);
+      for (let i = 0; i <= lastIdx; i++) {
+        holeNumerator += divisor / self.stack.list[i].fraction.denominator;
+      }
+      const holeSize = holeNumerator / divisor;
+      const formatNum = (n) => {
+        if (Number.isInteger(n)) return String(n);
+        const whole = Math.floor(n);
+        const frac = n - whole;
+        const FRAC_MAP = { 0.25: '\u00BC', 0.5: '\u00BD', 0.75: '\u00BE' };
+        return (whole ? whole + ' + ' : '') + (FRAC_MAP[Math.round(frac * 4) / 4] ?? n.toFixed(2).replace(/0+$/, ''));
+      };
+      const equationParts = self.stack.list.slice(0, lastIdx + 1).map(block => {
+        return FRAC_UNICODE[block.fraction.denominator] ?? `1/${block.fraction.denominator}`;
+      });
+      const isMinus = gameOperation === 'minus';
+      const eqSeparator = isMinus ? ' - ' : ' + ';
+      const eqTerms = isMinus
+        ? '-' + equationParts[0] + (equationParts.length > 1 ? eqSeparator + equationParts.slice(1).join(eqSeparator) : '')
+        : equationParts.join(eqSeparator);
+      // For minus: "-3 + ¾" → "-3 - ¾" so the sign stays consistent
+      const resultStr = isMinus
+        ? '-' + formatNum(holeSize).replace(' + ', ' e ')
+        : formatNum(holeSize);
+      const equationStr = eqTerms + ' = ' + resultStr;
+      game.add.text(cx, cardTop + imgH * 0.355, equationStr, {
+        ...textStyles.h3_, fill: colors.blueDark, font: 'bold ' + textStyles.h3_.font,
+      });
+
+      // Block bars — stacked upward from ground level, height proportional to fraction
+      const groundY     = cardTop + imgH * 0.72;   // ground line where tractor sits
+      const stackTopMin = cardTop + imgH * 0.44;   // never go above equation box
+      const maxStackH   = groundY - stackTopMin;   // available vertical space
+      const barW        = imgW * 0.055;
+      const barsStartX  = cardLeft + imgW * 0.30;
+      const totalBlocks = lastIdx + 1;
+      const fillColor   = gameOperation === 'minus' ? colors.redLight : '#79d2a1';
+      const borderColor = gameOperation === 'minus' ? colors.red      : colors.green;
+
+      // Calculate proportional heights, then scale down if they exceed available space
+      const blocks = self.stack.list.slice(0, totalBlocks);
+      const rawUnitH = Math.max(48, Math.min(80, maxStackH / holeSize));
+      const rawHeights = blocks.map(b => Math.max(20, rawUnitH / b.fraction.denominator));
+      const rawTotal   = rawHeights.reduce((s, h) => s + h + 4, -4);
+      const scale      = rawTotal > maxStackH ? maxStackH / rawTotal : 1;
+      const blockHeights = rawHeights.map(h => Math.max(10, h * scale));
+      const totalStackH = blockHeights.reduce((s, h) => s + h + 4, -4);
+      let curY = groundY - totalStackH;
+      blocks.forEach((block, i) => {
+        const bh = blockHeights[i];
+        game.add.geom.rect(barsStartX, curY, barW, bh, fillColor, 0.8, borderColor, 2);
+        const fracBase = FRAC_UNICODE[block.fraction.denominator] ?? `1/${block.fraction.denominator}`;
+        const fracLabel = (isMinus ? '-' : '') + fracBase;
+        game.add.text(barsStartX + barW + 20, curY + bh / 2 + 4, fracLabel, {
+          ...textStyles.p_, fill: colors.blueDark, font: `20px ${font.families.default}`,
+        });
+        curY += bh + 4;
+      });
+
+      // Hole label — text only, centred over the image's built-in blue rounded rectangle
+      const holeLabelCX = cardLeft + imgW * 0.675;
+      const holeLabelCY = cardTop  + imgH * 0.588;
+      const holeLabelKey = gameMode === 'a' ? (game.lang.s1_hole_label_b || game.lang.s1_hole_label) : game.lang.s1_hole_label;
+      const holeWords = (holeLabelKey || 'Hole of size').split(' ');
+      const holeMid = Math.ceil(holeWords.length / 2);
+      const holeLabelLine1 = holeWords.slice(0, holeMid).join(' ');
+      const holeLabelLine2 = holeWords.slice(holeMid).join(' ');
+      const holeLabelLine3 = (isMinus ? '-' : '') + formatNum(holeSize).replace(' + ', ' e ');
+      const holeFont = `bold 24px ${font.families.default}`;
+      const holeLineH = 24;
+      game.add.text(holeLabelCX, holeLabelCY - holeLineH,  holeLabelLine1, { ...textStyles.p_, fill: colors.white, font: holeFont });
+      game.add.text(holeLabelCX, holeLabelCY,               holeLabelLine2, { ...textStyles.p_, fill: colors.white, font: holeFont });
+      game.add.text(holeLabelCX, holeLabelCY + holeLineH,  holeLabelLine3, { ...textStyles.p_, fill: colors.white, font: holeFont });
+
+      // Body text (up to 3 lines, compact spacing)
+      const bodyKey = gameMode === 'a' ? (game.lang.s1_explain_body_b || game.lang.s1_explain_body) : game.lang.s1_explain_body;
+      const bodyLines = withNewlines(bodyKey).split('\n');
+      const bodyLineH = 30;
+      const bodyGap = 14; // extra gap after first line
+      const bodyStyle = { ...textStyles.p_, fill: colors.blueDark };
+      const bodyMidY = cardBottom - imgH * 0.175;
+      const totalH = (bodyLines.length - 1) * bodyLineH + (bodyLines.length > 1 ? bodyGap : 0);
+      const bodyStartY = bodyMidY - totalH / 2;
+      bodyLines.forEach((line, i) => {
+        const y = bodyStartY + (i === 0 ? 0 : bodyGap + i * bodyLineH);
+        game.add.text(cx, y, line, bodyStyle);
+      });
+
+      // Checkmark in the top-right corner of the card (~90px, scale = 90/256)
+      const checkScale = imgH * 0.13 / 256;
+      const checkImg = game.add.image(cardLeft + imgW - 16, cardTop + 16, 'answer_correct', checkScale);
+      checkImg.anchor(1, 0);
+
+      // Continue button (only reached on correct answer)
+      const btnW = imgW * 0.38; const btnH = 62; const btnCY = cardBottom - imgH * 0.07;
+      self.ui.explanation.button = game.add.geom.rect(cx, btnCY, btnW, btnH, colors.green);
+      self.ui.explanation.button.anchor(0.5, 0.5);
+      self.ui.explanation.text = game.add.text(cx, btnCY + 14, game.lang.continue, textStyles.btn);
+      self.control.showExplanation = true;
+    },
     renderEndUI: () => {
+      const cx = context.canvas.width / 2;
+      const cy = context.canvas.height / 2;
       let btnColor = colors.green;
       let btnText = game.lang.continue;
-
       if (!self.control.isCorrect) {
         btnColor = colors.red;
-        btnText = game.lang.retry;
+        btnText = game.lang.retry || 'Retry';
       }
-
-      // continue button
-      self.ui.continue.button = game.add.geom.rect(
-        context.canvas.width / 2,
-        context.canvas.height / 2 + 100,
-        450,
-        100,
-        btnColor
+      self.ui.explanation.button = game.add.geom.rect(
+        cx, cy + 100, 450, 100, btnColor
       );
-      self.ui.continue.button.anchor(0.5, 0.5);
-      self.ui.continue.text = game.add.text(
-        context.canvas.width / 2,
-        context.canvas.height / 2 + 16 + 100,
-        btnText,
-        textStyles.btn
+      self.ui.explanation.button.anchor(0.5, 0.5);
+      self.ui.explanation.text = game.add.text(
+        cx, cy + 16 + 100, btnText, textStyles.btn
       );
+      self.control.showExplanation = true;
     },
 
     // UPDATE HANDLERS
@@ -914,22 +1241,18 @@ const squareOne = {
           ? self.floor.selectedIndex === self.floor.correctIndex
           : self.stack.selectedIndex === self.stack.correctIndex;
 
-      const x = self.utils.renderOperationUI();
+      const feedbackX = context.canvas.width * 0.75;
+      const feedbackY = context.canvas.height / 3;
 
       // Give feedback to player and turns on sprite animation
       if (self.control.isCorrect) {
         completedLevels++; // Increases number os finished levels
         if (audioStatus) game.audio.okSound.play();
         game.animation.play(self.tractor.animation[0]);
-        game.add
-          .image(x + 50, context.canvas.height / 3, 'answer_correct')
-          .anchor(0.5, 0.5);
         if (isDebugMode) console.log('Completed Levels: ' + completedLevels);
       } else {
         if (audioStatus) game.audio.errorSound.play();
-        game.add
-          .image(x, context.canvas.height / 3, 'answer_wrong')
-          .anchor(0.5, 0.5);
+        game.add.image(feedbackX, feedbackY, 'answer_wrong').anchor(0.5, 0.5);
       }
 
       self.fetch.postScore();
@@ -944,12 +1267,15 @@ const squareOne = {
       // If CORRECT ANSWER runs final tractor animation (else tractor desn't move, just wait)
       if (self.control.isCorrect) self.tractor.x += self.animation.speed;
 
-      if (self.control.count === 100) {
+      if (!self.control.isCorrect && self.control.count === 1) {
+        self.utils.renderOperationUI();
         self.utils.renderEndUI();
-        self.control.showEndInfo = true;
+        canGoToNextMapPosition = false;
+      }
 
-        if (self.control.isCorrect) canGoToNextMapPosition = true;
-        else canGoToNextMapPosition = false;
+      if (self.control.isCorrect && self.control.count === 100) {
+        self.utils.renderExplanationUI();
+        canGoToNextMapPosition = true;
       }
     },
     endLevel: () => {
@@ -984,7 +1310,7 @@ const squareOne = {
      * Function called by self.events.onInputDown() when player clicks on a valid rectangle.
      */
     clickHandler: (clickedIndex, curSet) => {
-      if (!self.control.hasClicked && !self.animation.animateEnding) {
+      if (!self.control.hasClicked && !self.animation.animateEnding && !self.control.showChallenge) {
         document.body.style.cursor = 'auto';
         // Play beep sound
         if (audioStatus) game.audio.popSound.play();
@@ -1091,6 +1417,22 @@ const squareOne = {
       const x = game.math.getMouse(mouseEvent).x;
       const y = game.math.getMouse(mouseEvent).y;
 
+      // Challenge modal: block all game interaction until accepted
+      if (self.control.showChallenge) {
+        if (game.math.isOverIcon(x, y, self.ui.challenge.button)) {
+          if (audioStatus) game.audio.popSound.play();
+          self.utils.acceptChallenge();
+        }
+        navigation.onInputDown(x, y);
+        game.render.all();
+        return;
+      }
+
+      // Hide intro message on first click after challenge acceptance
+      if (!self.control.hasClicked && self.ui.message && self.ui.message[0]) {
+        self.ui.message[0].alpha = 0;
+      }
+
       // click blocks
       const curSet = gameMode == 'a' ? self.floor : self.stack;
       for (let i in curSet.list) {
@@ -1100,9 +1442,9 @@ const squareOne = {
         }
       }
 
-      // Continue button
-      if (self.control.showEndInfo) {
-        if (game.math.isOverIcon(x, y, self.ui.continue.button)) {
+      // Explanation button
+      if (self.control.showExplanation) {
+        if (game.math.isOverIcon(x, y, self.ui.explanation.button)) {
           if (audioStatus) game.audio.popSound.play();
           self.utils.endLevel();
         }
@@ -1124,51 +1466,64 @@ const squareOne = {
       let isOverFloor = false;
       let isOverStack = false;
 
-      if (gameMode === 'a') {
-        self.floor.list.forEach((cur) => {
-          // hover floor blocks
-          if (game.math.isOverIcon(x, y, cur)) {
-            isOverFloor = true;
-            self.utils.overHandler(cur);
-          }
-          // move arrow
-          if (
-            !self.control.hasClicked &&
-            !self.animation.animateEnding &&
-            game.math.isOverIcon(x, self.default.y0, cur)
-          ) {
-            self.ui.arrow.x = x;
-          }
-        });
+      if (!self.control.showChallenge) {
+        if (gameMode === 'a') {
+          self.floor.list.forEach((cur) => {
+            // hover floor blocks
+            if (game.math.isOverIcon(x, y, cur)) {
+              isOverFloor = true;
+              self.utils.overHandler(cur);
+            }
+            // move arrow
+            if (
+              !self.control.hasClicked &&
+              !self.animation.animateEnding &&
+              game.math.isOverIcon(x, self.default.y0, cur)
+            ) {
+              self.ui.arrow.x = x;
+            }
+          });
+
+          if (!isOverFloor) self.utils.outHandler('a');
+        }
 
-        if (!isOverFloor) self.utils.outHandler('a');
+        if (gameMode === 'b') {
+          // hover stack blocks
+          self.stack.list.forEach((cur) => {
+            if (game.math.isOverIcon(x, y, cur)) {
+              isOverStack = true;
+              self.utils.overHandler(cur);
+            }
+          });
+          if (!isOverStack) self.utils.outHandler('b');
+        }
       }
 
-      if (gameMode === 'b') {
-        // hover stack blocks
-        self.stack.list.forEach((cur) => {
-          if (game.math.isOverIcon(x, y, cur)) {
-            isOverStack = true;
-            self.utils.overHandler(cur);
-          }
-        });
-        if (!isOverStack) self.utils.outHandler('b');
+      // Challenge accept button hover
+      if (self.control.showChallenge && self.ui.challenge.button) {
+        if (game.math.isOverIcon(x, y, self.ui.challenge.button)) {
+          document.body.style.cursor = 'pointer';
+          self.ui.challenge.button.scale = self.ui.challenge.button.initialScale * 1.1;
+          self.ui.challenge.buttonText.style = textStyles.btnLg;
+        } else {
+          document.body.style.cursor = 'auto';
+          self.ui.challenge.button.scale = self.ui.challenge.button.initialScale * 1;
+          self.ui.challenge.buttonText.style = textStyles.btn;
+        }
       }
 
-      // Continue button
-      if (self.control.showEndInfo) {
-        if (game.math.isOverIcon(x, y, self.ui.continue.button)) {
-          // If pointer is over icon
+      // Explanation button hover
+      if (self.control.showExplanation) {
+        if (game.math.isOverIcon(x, y, self.ui.explanation.button)) {
           document.body.style.cursor = 'pointer';
-          self.ui.continue.button.scale =
-            self.ui.continue.button.initialScale * 1.1;
-          self.ui.continue.text.style = textStyles.btnLg;
+          self.ui.explanation.button.scale =
+            self.ui.explanation.button.initialScale * 1.05;
+          self.ui.explanation.text.style = textStyles.btnLg;
         } else {
-          // If pointer is not over icon
           document.body.style.cursor = 'auto';
-          self.ui.continue.button.scale =
-            self.ui.continue.button.initialScale * 1;
-          self.ui.continue.text.style = textStyles.btn;
+          self.ui.explanation.button.scale =
+            self.ui.explanation.button.initialScale * 1;
+          self.ui.explanation.text.style = textStyles.btn;
         }
       }
 

+ 5 - 0
js/globals/globals_tokens.js

@@ -163,6 +163,11 @@ const url = {
       // ['close', baseUrl + 'icons_interactive/close.png'],
       ['info', baseUrl + 'icons_interactive/info.png'],
       ['pointer', baseUrl + 'icons_interactive/pointer.png'],
+      // squareOne game UI images (loaded at boot so they are always in cache)
+      ['challenge-card', baseUrl + 'scene/challenge-card.png'],
+      ['circular-question', baseUrl + 'scene/circular-question.png'],
+      ['question-mark-with-arrow', baseUrl + 'scene/question-mark-with-arrow.png'],
+      ['end-tractor-game', baseUrl + 'scene/end-tractor-game.png'],
       // Menu icons - Games
       ['game_0', baseUrl + 'icons_menu/squareOne.png'], // Square I
       ['game_1', baseUrl + 'icons_menu/circleOne.png'], // Circle I