فهرست منبع

feat: add challenge modal and explanation UI for squareTwo game

priscila.snl 1 هفته پیش
والد
کامیت
2f3f2b733d

BIN
assets/img/scene/background-result.png


BIN
assets/img/scene/challenge.png


+ 11 - 0
assets/lang/en_US

@@ -14,6 +14,17 @@ difficulties=Difficulties
 difficulty=Difficulty
 empty_name=You forgot to type your name
 equals=Equality
+yes=Yes
+no=No
+s2_challenge_title=Level Challenge!
+s2_challenge_subtitle=Can you figure it out?\nSomething curious happens with these fractions...
+s2_challenge_question=Do these fractions represent\nthe same quantity?
+s2_explain_title=How are these fractions equal?
+s2_explain_body_correct=We divide the numerator and denominator by the same number.\nWe only change the way we represent it.
+s2_explain_body_wrong=These fractions are not equivalent.
+s2_explain_body_factor_prefix=Since
+s2_explain_body_factor_suffix=equals 1, the value of the fraction does not change.
+s2_challenge_accept=Accept the Challenge
 error_must_select_game=You must select at least one game!
 game=Game
 game_mode=Game Mode

+ 11 - 0
assets/lang/es_ES

@@ -14,6 +14,17 @@ difficulties=Dificultades
 difficulty=Dificultade
 empty_name=Usted ha olvidado de escribir su nombre
 equals=Igualdad
+yes=Sí
+no=No
+s2_challenge_title=¡Desafío de la Fase!
+s2_challenge_subtitle=¿Puedes descubrirlo?\nAlgo curioso sucede con estas fracciones...
+s2_challenge_question=¿Estas fracciones representan\nla misma cantidad?
+s2_explain_title=¿Cómo son iguales estas fracciones?
+s2_explain_body_correct=Dividimos el numerador y el denominador por el mismo número.\nSolo cambiamos la forma de representarlo.
+s2_explain_body_wrong=Estas fracciones no son equivalentes.
+s2_explain_body_factor_prefix=Como
+s2_explain_body_factor_suffix=es igual a 1, el valor de la fracción no cambia.
+s2_challenge_accept=Aceptar el Desafío
 error_must_select_game=Debes seleccionar al menos un juego! 
 game=Juego
 game_mode=Modo de Juego

+ 11 - 0
assets/lang/fr_FR

@@ -14,6 +14,17 @@ difficulties=Difficultés
 difficulty=Difficulté
 empty_name=Vous avez oublié de taper votre nom
 equals=Égalité
+yes=Oui
+no=Non
+s2_challenge_title=Défi du Niveau !
+s2_challenge_subtitle=Peux-tu le découvrir ?\nQuelque chose d'étrange arrive avec ces fractions...
+s2_challenge_question=Ces fractions représentent-elles\nla même quantité ?
+s2_explain_title=Comment ces fractions\nsont-elles égales ?
+s2_explain_body_correct=On divise le numérateur et le dénominateur par le même nombre.\nOn change seulement la façon de le représenter.
+s2_explain_body_wrong=Ces fractions ne sont pas équivalentes.
+s2_explain_body_factor_prefix=Comme
+s2_explain_body_factor_suffix=est égal à 1, la valeur de la fraction ne change pas.
+s2_challenge_accept=Accepter le Défi
 error_must_select_game=vous devez sélectionner au moins un jeu !
 game=Jeu
 game_mode=Mode de Jeu

+ 11 - 0
assets/lang/it_IT

@@ -14,6 +14,17 @@ difficulties=Difficoltà
 difficulty=Difficoltà
 empty_name=Ti sei dimenticato di digitare il tuo nome
 equals=Uguaglianza
+yes=Sì
+no=No
+s2_challenge_title=Sfida del Livello!
+s2_challenge_subtitle=Riesci a scoprirlo?\nSuccede qualcosa di curioso con queste frazioni...
+s2_challenge_question=Queste frazioni rappresentano\nla stessa quantità?
+s2_explain_title=Come fanno queste\nfrazioni a essere uguali?
+s2_explain_body_correct=Dividiamo numeratore e denominatore per lo stesso numero.\nCambiamo solo il modo di rappresentarla.
+s2_explain_body_wrong=Queste frazioni non sono equivalenti.
+s2_explain_body_factor_prefix=Poiché
+s2_explain_body_factor_suffix=è uguale a 1, il valore della frazione non cambia.
+s2_challenge_accept=Accetta la Sfida
 error_must_select_game=Devi selezionare almeno un gioco!
 game=Gioco
 game_mode=Modalità di Gioco

+ 10 - 0
assets/lang/pt_BR

@@ -14,6 +14,16 @@ difficulties=Dificuldades
 difficulty=Dificuldade
 empty_name=Você esqueceu de digitar seu nome
 equals=Igualdade
+yes=Sim
+no=Não
+s2_challenge_title=Desafio da Fase!
+s2_challenge_subtitle=Você consegue descobrir?\nAlgo curioso acontece com essas frações...
+s2_challenge_question=Essas frações representam\na mesma quantidade?
+s2_explain_title=Como essas frações são iguais?
+s2_explain_body_correct=Dividimos numerador e denominador pelo mesmo número.\nMudamos apenas a forma de representar.
+s2_explain_body_wrong=Essas frações não são equivalentes.
+s2_explain_body_factor_prefix=Como
+s2_explain_body_factor_suffix=é igual a 1, o valor da fração não muda.
 error_must_select_game=Você precisa selecionar pelo menos um jogo!
 game=Jogo
 game_mode=Modo de Jogo

+ 666 - 31
js/games/squareTwo.js

@@ -43,20 +43,48 @@ const squareTwo = {
   create: function () {
     this.ui = {
       message: undefined,
+      startChoice: {
+        message: undefined,
+        card: undefined,
+        image: undefined,
+        title: undefined,
+        subtitle: undefined,
+        accept: {
+          button: undefined,
+          text: undefined,
+        },
+        yes: {
+          button: undefined,
+          text: undefined,
+        },
+        no: {
+          button: undefined,
+          text: undefined,
+        },
+      },
       continue: {
         // modal: undefined,
         button: undefined,
         text: undefined,
       },
+      explanation: {
+        button: undefined,
+        text: undefined,
+      },
     };
     this.control = {
       blockWidth: 600,
       blockHeight: 75,
       isCorrect: false,
       showEndInfo: false,
+      showExplanation: false,
       animationDelay: 0,
       startDelay: false,
       startEndAnimation: false,
+      started: false,
+      blockConfig: undefined,
+      challengePreview: undefined,
+      challengeAnsweredYes: undefined,
     };
     this.blocks = {
       top: {
@@ -93,12 +121,15 @@ const squareTwo = {
       navigation.add.right(['audio']);
     }
 
-    // Add kid
-    this.utils.renderCharacters();
-    this.utils.renderBlockSetup();
-    this.utils.renderMainUI();
+    // Pre-generate the level configuration so the challenge modal can preview real fractions
+    this.control.blockConfig = this.utils.generateBlockConfig();
+    this.control.challengePreview = this.utils.generateChallengePreview(
+      this.control.blockConfig
+    );
+
+    // Wait for user confirmation before starting the level
+    this.utils.renderStartChoiceUI();
 
-    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);
   },
@@ -136,18 +167,18 @@ const squareTwo = {
 
   utils: {
     // RENDERS
-    renderBlockSetup: function () {
+    generateBlockConfig: function () {
       // Coordinates for (a) and (b)
       let xA, xB, yA, yB;
 
       if (gameMode != 'b') {
-        // Has more subdivisions on (b)
+        // Has more subdivisions on (a)
         xA = context.canvas.width / 2 - self.control.blockWidth / 2;
         yA = getFrameInfo().y + 100;
         xB = xA;
         yB = yA + 3 * self.control.blockHeight + 30;
       } else {
-        // Has more subdivisions on (a)
+        // Has more subdivisions on (b)
         xB = context.canvas.width / 2 - self.control.blockWidth / 2;
         yB = getFrameInfo().y + 100;
         xA = xB;
@@ -192,6 +223,387 @@ const squareTwo = {
         );
       }
 
+      return {
+        xA,
+        yA,
+        xB,
+        yB,
+        totalBlocksA,
+        totalBlocksB,
+        blockWidthA,
+        blockWidthB,
+      };
+    },
+    generateChallengePreview: function (blockConfig) {
+      if (!blockConfig) return undefined;
+
+      const { totalBlocksA, totalBlocksB } = blockConfig;
+      if (!totalBlocksA || !totalBlocksB) return undefined;
+
+      const gcd = (a, b) => {
+        let x = Math.abs(a);
+        let y = Math.abs(b);
+        while (y !== 0) {
+          const t = x % y;
+          x = y;
+          y = t;
+        }
+        return x;
+      };
+
+      // Always produce an equivalent integer pair using the gcd
+      const g = gcd(totalBlocksA, totalBlocksB);
+      const leftUnit = totalBlocksA / g;
+      const rightUnit = totalBlocksB / g;
+
+      // Deterministic preview (prevents changing values between renders)
+      const selectedA = leftUnit;
+      const selectedB = rightUnit;
+
+      // Show as left/right fractions in the modal (keep order consistent with blocks)
+      return {
+        left: { num: selectedA, den: totalBlocksA },
+        right: { num: selectedB, den: totalBlocksB },
+      };
+    },
+    renderStartChoiceUI: () => {
+      const centerX = context.canvas.width / 2;
+      const centerY = context.canvas.height / 2;
+
+      const yesLabel = game.lang.yes;
+      const noLabel = game.lang.no;
+      const withNewlines = (s) => (s == null ? '' : String(s).replace(/\\n/g, '\n'));
+      const title = withNewlines(game.lang.s2_challenge_title);
+      const subtitle = withNewlines(game.lang.s2_challenge_subtitle);
+      const question = withNewlines(game.lang.s2_challenge_question);
+
+      // Layout reference (no white panel/card)
+      const cardH = 560;
+
+      const panelTop = centerY - cardH / 2;
+
+      // Challenge image (ribbon + question mark)
+      const imageScale = 0.7;
+      const imageY = panelTop - 150;
+      self.ui.startChoice.image = game.add.image(
+        centerX,
+        imageY,
+        'challenge',
+        imageScale,
+        1
+      );
+
+      self.ui.startChoice.image.anchor(0.5, 0);
+
+      // Title + subtitle
+      self.ui.startChoice.title = game.add.text(
+        centerX,
+        imageY + 65,
+        title,
+        { ...textStyles.h2_, fill: colors.white, font: 'bold ' + textStyles.h2_.font }
+      );
+      self.ui.startChoice.title.anchor(0.5, 0.5);
+
+      // Subtitle split in two lines (top emphasized)
+      const subtitleLines = String(subtitle || '').split('\n');
+      const subtitleTop = subtitleLines[0] || '';
+      const subtitleBottom = subtitleLines.slice(1).join('\n');
+
+      self.ui.startChoice.subtitle = {};
+
+      self.ui.startChoice.subtitle.top = game.add.text(
+        centerX,
+        panelTop + 10,
+        subtitleTop,
+        {
+          ...textStyles.h4_,
+          font: 'bold ' + textStyles.h4_.font,
+          fill: colors.blueDark,
+        }
+      );
+      self.ui.startChoice.subtitle.top.anchor(0.5, 0.5);
+
+      self.ui.startChoice.subtitle.bottom = game.add.text(
+        centerX,
+        panelTop + 55,
+        subtitleBottom,
+        { ...textStyles.h3_, fill: colors.blue }
+      );
+      self.ui.startChoice.subtitle.bottom.anchor(0.5, 0.5);
+
+      // Fractions preview (left and right) - based on the real generated blockConfig
+      const preview =
+        self.control.challengePreview ||
+        self.utils.generateChallengePreview(self.control.blockConfig);
+      self.control.challengePreview = preview;
+      if (preview && preview.left && preview.right) {
+        const fracFont = { ...textStyles.fraction, fill: colors.blueDark, align: 'center', font: '76px monospace' };
+        const fracY = panelTop + 255;
+        const offsetX = 260;
+
+        const leftX = centerX - offsetX;
+        const rightX = centerX + offsetX;
+
+        const leftText = game.add.text(
+          leftX,
+          fracY,
+          preview.left.num + '\n' + preview.left.den,
+          fracFont,
+          70
+        );
+        leftText.anchor(0.5, 0.5);
+        const leftLine = game.add.geom.rect(
+          leftX,
+          fracY + 10,
+          120,
+          4,
+          colors.blueDark,
+          1,
+          colors.blueDark,
+          4
+        );
+        leftLine.anchor(0.5, 0);
+
+        const rightText = game.add.text(
+          rightX,
+          fracY,
+          preview.right.num + '\n' + preview.right.den,
+          fracFont,
+          70
+        );
+        rightText.anchor(0.5, 0.5);
+        const rightLine = game.add.geom.rect(
+          rightX,
+          fracY + 10,
+          120,
+          4,
+          colors.blueDark,
+          1,
+          colors.blueDark,
+          4
+        );
+        rightLine.anchor(0.5, 0);
+
+        self.ui.startChoice.fractions = {
+          leftText,
+          leftLine,
+          rightText,
+          rightLine,
+        };
+      }
+
+      // Prompt
+      self.ui.startChoice.message = game.add.text(
+        centerX,
+        panelTop + 415,
+        question,
+        {
+          ...textStyles.h3_,
+          font: 'bold ' + textStyles.h3_.font,
+          fill: colors.blueDark,
+        }
+      );
+      self.ui.startChoice.message.anchor(0.5, 0.5);
+
+      // Buttons
+      const buttonW = 340;
+      const buttonH = 90;
+      const gap = 90;
+      const buttonsY = panelTop + 600;
+
+      const choiceTextStyle = { ...textStyles.h3_, fill: colors.blueDark, font: 'bold ' + textStyles.h3_.font };
+      const borderColor = colors.blueMenuLine;
+      const fillColor = colors.white;
+      const fillAlpha = 0.9;
+      const selectedFillColor = colors.blueLight;
+
+      const createPillButton = (x, y, label) => {
+        // Invisible hit target used for hover/click detection
+        const hit = game.add.geom.rect(
+          x,
+          y,
+          buttonW,
+          buttonH,
+          colors.white,
+          0,
+          colors.white,
+          0
+        );
+        hit.anchor(0.5, 0.5);
+
+        const outerW = buttonW;
+        const outerH = buttonH;
+
+        const innerInset = 10;
+        const innerW = outerW - innerInset;
+        const innerH = outerH - innerInset;
+
+        const outerRect = game.add.geom.rect(
+          x,
+          y,
+          outerW,
+          outerH,
+          borderColor,
+          1,
+          borderColor,
+          0
+        );
+        outerRect.anchor(0.5, 0.5);
+        outerRect.shadow = true;
+        outerRect.shadowColor = colors.gray;
+        outerRect.shadowBlur = 8;
+
+        const innerRect = game.add.geom.rect(
+          x,
+          y,
+          innerW,
+          innerH,
+          fillColor,
+          fillAlpha,
+          fillColor,
+          0
+        );
+        innerRect.anchor(0.5, 0.5);
+
+        const text = game.add.text(x, y + 10, label, choiceTextStyle);
+
+        return {
+          hit,
+          text,
+          outerParts: [outerRect],
+          innerParts: [innerRect],
+          setSelected: (isSelected) => {
+            const c = isSelected ? selectedFillColor : fillColor;
+            [innerRect].forEach((p) => {
+              p.fillColor = c;
+              p.lineColor = c;
+            });
+          },
+        };
+      };
+
+      const yesX = centerX - (buttonW / 2 + gap / 2);
+      const noX = centerX + (buttonW / 2 + gap / 2);
+
+      const yes = createPillButton(yesX, buttonsY, yesLabel);
+      const no = createPillButton(noX, buttonsY, noLabel);
+
+      // keep existing structure expected by event handlers
+      self.ui.startChoice.yes.button = yes.hit;
+      self.ui.startChoice.yes.text = yes.text;
+      self.ui.startChoice.yes.visual = yes;
+
+      self.ui.startChoice.no.button = no.hit;
+      self.ui.startChoice.no.text = no.text;
+      self.ui.startChoice.no.visual = no;
+
+      game.render.all();
+    },
+    updateChallengeChoiceUI: () => {
+      const selected = self.control.challengeAnsweredYes;
+      const yesBtn = self.ui.startChoice?.yes?.button;
+      const noBtn = self.ui.startChoice?.no?.button;
+
+      const yesVisual = self.ui.startChoice?.yes?.visual;
+      const noVisual = self.ui.startChoice?.no?.visual;
+
+      if (yesBtn) {
+        if (yesVisual && yesVisual.setSelected) {
+          yesVisual.setSelected(selected === true);
+        } else {
+          yesBtn.fillColor = selected === true ? colors.blueLight : colors.white;
+        }
+      }
+      if (noBtn) {
+        if (noVisual && noVisual.setSelected) {
+          noVisual.setSelected(selected === false);
+        } else {
+          noBtn.fillColor = selected === false ? colors.blueLight : colors.white;
+        }
+      }
+
+      game.render.all();
+    },
+    startGame: () => {
+      if (self.control.started) return;
+      self.control.started = true;
+
+      // Hide pre-start UI
+      if (self.ui.startChoice && self.ui.startChoice.message) {
+        self.ui.startChoice.message.alpha = 0;
+      }
+      if (self.ui.startChoice && self.ui.startChoice.title) {
+        self.ui.startChoice.title.alpha = 0;
+      }
+      if (self.ui.startChoice && self.ui.startChoice.subtitle) {
+        if (self.ui.startChoice.subtitle.alpha != null) {
+          self.ui.startChoice.subtitle.alpha = 0;
+        } else {
+          const s = self.ui.startChoice.subtitle;
+          if (s.top) s.top.alpha = 0;
+          if (s.bottom) s.bottom.alpha = 0;
+        }
+      }
+      if (self.ui.startChoice && self.ui.startChoice.image) {
+        self.ui.startChoice.image.alpha = 0;
+      }
+      if (self.ui.startChoice && self.ui.startChoice.fractions) {
+        Object.values(self.ui.startChoice.fractions).forEach((obj) => {
+          if (obj) obj.alpha = 0;
+        });
+      }
+      if (self.ui.startChoice && self.ui.startChoice.card) {
+        self.ui.startChoice.card.alpha = 0;
+      }
+      if (self.ui.startChoice && self.ui.startChoice.yes) {
+        if (self.ui.startChoice.yes.button) self.ui.startChoice.yes.button.alpha = 0;
+        if (self.ui.startChoice.yes.text) self.ui.startChoice.yes.text.alpha = 0;
+        if (self.ui.startChoice.yes.visual) {
+          const v = self.ui.startChoice.yes.visual;
+          if (v.outerParts) v.outerParts.forEach((p) => (p.alpha = 0));
+          if (v.innerParts) v.innerParts.forEach((p) => (p.alpha = 0));
+        }
+      }
+      if (self.ui.startChoice && self.ui.startChoice.no) {
+        if (self.ui.startChoice.no.button) self.ui.startChoice.no.button.alpha = 0;
+        if (self.ui.startChoice.no.text) self.ui.startChoice.no.text.alpha = 0;
+        if (self.ui.startChoice.no.visual) {
+          const v = self.ui.startChoice.no.visual;
+          if (v.outerParts) v.outerParts.forEach((p) => (p.alpha = 0));
+          if (v.innerParts) v.innerParts.forEach((p) => (p.alpha = 0));
+        }
+      }
+      if (self.ui.startChoice && self.ui.startChoice.accept) {
+        if (self.ui.startChoice.accept.button)
+          self.ui.startChoice.accept.button.alpha = 0;
+        if (self.ui.startChoice.accept.text)
+          self.ui.startChoice.accept.text.alpha = 0;
+      }
+      document.body.style.cursor = 'auto';
+
+      // Add kid + blocks + UI (original flow)
+      self.utils.renderCharacters();
+      self.utils.renderBlockSetup(self.control.blockConfig);
+      self.utils.renderMainUI();
+
+      game.timer.start(); // Set a timer for the current level (used in postScore)
+      game.render.all();
+    },
+    renderBlockSetup: function (blockConfig) {
+      const cfg = blockConfig || self.utils.generateBlockConfig();
+      if (!cfg) return;
+
+      const {
+        xA,
+        yA,
+        xB,
+        yB,
+        totalBlocksA,
+        totalBlocksB,
+        blockWidthA,
+        blockWidthB,
+      } = cfg;
+
       // (a)
       self.utils.renderBlocks(
         self.blocks.top,
@@ -443,6 +855,127 @@ const squareTwo = {
 
       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');
+
+      // WRONG ANSWER: simple small card (no equation/bars)
+      if (!self.control.isCorrect) {
+        const errW = 680; const errH = 220;
+        const errLeft = cx - errW / 2; const errTop = cy - errH / 2;
+        const errBottom = errTop + errH;
+        game.add.geom.rect(errLeft, errTop, errW, errH, colors.white, 0.97, colors.red, 4);
+        game.add.text(cx, cy - 12,
+          game.lang.s2_explain_body_wrong,
+          { ...textStyles.h3_, fill: colors.red }
+        );
+        const btnW = 360; const btnH = 70; const btnCY = errBottom - 52;
+        self.ui.explanation.button = game.add.geom.rect(
+          cx - btnW / 2, btnCY - btnH / 2, btnW, btnH, colors.red
+        );
+        self.ui.explanation.text = game.add.text(cx, btnCY + 16, game.lang.retry, textStyles.btn);
+        self.control.showExplanation = true;
+        return;
+      }
+
+      // CORRECT ANSWER: full explanation card
+      // Image is 1298×816 — all positions are relative to image coordinates
+      const imgW = 1298; const imgH = 816;
+      const cardLeft = cx - imgW / 2;  // 311
+      const cardTop  = cy - imgH / 2;  // 132
+      const cardBottom = cardTop + imgH; // 948
+
+      // 1. Background image + title text (image has the 🔍 icon, we add the text)
+      game.add.image(cx, cy, 'result-bg').anchor(0.5, 0.5);
+      const titleText = withNewlines(game.lang.s2_explain_title);
+      const titleY = cardTop + (titleText.includes('\n') ? 42 : 62);
+      game.add.text(cx, titleY, titleText, {
+        ...textStyles.h3_, fill: colors.blueDark, font: 'bold ' + textStyles.h3_.font,
+      });
+
+      // Fraction values
+      let bigNum, bigDen, bigLineColor, bigFillColor;
+      let smallNum, smallDen, smallLineColor, smallFillColor;
+      if (gameMode !== 'b') {
+        bigNum = self.blocks.top.selectedAmount; bigDen = self.blocks.top.list.length;
+        bigLineColor = colors.blueDark; bigFillColor = colors.blue;
+        smallNum = self.blocks.bottom.selectedAmount; smallDen = self.blocks.bottom.list.length;
+        smallLineColor = colors.greenDark; smallFillColor = colors.green;
+      } else {
+        bigNum = self.blocks.bottom.selectedAmount; bigDen = self.blocks.bottom.list.length;
+        bigLineColor = colors.greenDark; bigFillColor = colors.green;
+        smallNum = self.blocks.top.selectedAmount; smallDen = self.blocks.top.list.length;
+        smallLineColor = colors.blueDark; smallFillColor = colors.blue;
+      }
+      const factor = bigDen / smallDen;
+      const fracStyle = { ...textStyles.fraction, fill: colors.blueDark, align: 'center' };
+
+      // 2. Fraction numbers — placed over the image's fraction bar template
+      // Image fraction bar LINE is at ~y=250 from image top; numY+32 should align with it
+      // x centres measured from image: left≈24%, mid≈47%, right≈75%
+      const numY = cardTop + 238; // numerator baseline
+      const c1 = cardLeft + 315;  // left fraction  (≈ 626)
+      const c3 = cardLeft + 610;  // centre fraction (≈ 921)
+      const c5 = cardLeft + 975;  // right fraction  (≈ 1286)
+      game.add.text(c1, numY, bigNum + '\n' + bigDen, fracStyle, 65);
+      game.add.text(c3, numY, factor + '\n' + factor, fracStyle, 65);
+      game.add.text(c5, numY, smallNum + '\n' + smallDen, fracStyle, 65);
+
+      // 3. Block bars + badge — centred around the image's star area
+      // Star centre is ~49% of image width, ~51% of image height
+      const badgeCx = cx; // star centre aligned to screen centre
+      const barsTop = cardTop + 390; // ≈ 522
+      const barH = 72; const barW = 270; const badgeW = 210; const barGap = 95;
+      const leftBarLeft  = badgeCx - badgeW / 2 - barGap - barW;
+      const rightBarLeft = badgeCx + badgeW / 2 + barGap;
+      const bwBig = barW / bigDen;
+      for (let i = 0; i < bigDen; i++) {
+        game.add.geom.rect(leftBarLeft + i * bwBig, barsTop, bwBig, barH,
+          i < bigNum ? bigFillColor : colors.blueLight,
+          i < bigNum ? 0.75 : 0.25, bigLineColor, 2);
+      }
+      const bwSmall = barW / smallDen;
+      for (let i = 0; i < smallDen; i++) {
+        game.add.geom.rect(rightBarLeft + i * bwSmall, barsTop, bwSmall, barH,
+          i < smallNum ? smallFillColor : colors.greenLight,
+          i < smallNum ? 0.75 : 0.25, smallLineColor, 2);
+      }
+      // Badge — image already draws the star, just overlay the text
+      const badgeCenterY = barsTop + barH / 2;
+      game.add.text(cx, badgeCenterY - 8, factor + '/' + factor + ' = 1', {
+        ...textStyles.h4_, fill: colors.blueDark, font: 'bold 40px Arial, sans-serif',
+      });
+
+      // 4. Body text — below the star area, image body zone starts ~560px from image top
+      const bodyParts = withNewlines(game.lang.s2_explain_body_correct).split('\n');
+      const line1Raw = bodyParts[0];
+      const wrapAt = 42;
+      const wrapPos = line1Raw.lastIndexOf(' ', wrapAt);
+      const line1 = (wrapPos > 0 && line1Raw.length > wrapAt)
+        ? line1Raw.slice(0, wrapPos) + '\n' + line1Raw.slice(wrapPos + 1)
+        : line1Raw;
+      const line2 = (game.lang.s2_explain_body_factor_prefix) +
+        ' ' + factor + '/' + factor + ' ' +
+        (game.lang.s2_explain_body_factor_suffix);
+      const line3 = bodyParts[1];
+
+      const bodyStyle = { ...textStyles.p_, fill: colors.blueDark, font: 'bold 33px Arial, sans-serif' };
+      const bodyStyleNormal = { ...textStyles.p_, fill: colors.blueDark, font: '33px Arial, sans-serif' };
+      const bodyY = cardTop + 550;
+      game.add.text(cx, bodyY, line1, bodyStyle, 38);
+      game.add.text(cx, bodyY + 84, line2, bodyStyleNormal);
+      game.add.text(cx, bodyY + 122, line3, bodyStyleNormal);
+
+      // 5. Continue button
+      const btnW = 400; const btnH = 75; const btnCenterY = cardBottom - 62;
+      self.ui.explanation.button = game.add.geom.rect(
+        cx - btnW / 2, btnCenterY - btnH / 2, btnW, btnH, colors.green
+      );
+      self.ui.explanation.text = game.add.text(cx, btnCenterY + 16, game.lang.continue, textStyles.btn);
+      self.control.showExplanation = true;
+    },
     renderEndUI: () => {
       let btnColor = colors.green;
       let btnText = game.lang.continue;
@@ -502,33 +1035,28 @@ const squareTwo = {
       });
     },
     checkAnswer: function () {
-      for (let block of self.blocks.top.auxBlocks) {
-        block.alpha = 0;
-      }
-      for (let block of self.blocks.bottom.auxBlocks) {
-        block.alpha = 0;
-      }
-
-      // After delay is over, check result
-      //if (self.control.animationDelay > 50) {
       self.control.isCorrect =
         self.blocks.top.selectedAmount / self.blocks.top.list.length ==
         self.blocks.bottom.selectedAmount / self.blocks.bottom.list.length;
 
-      const x = self.utils.renderOperationUI();
-
       if (self.control.isCorrect) {
+        // Correct: hide blocks to make room for explanation card
+        ['top', 'bottom'].forEach((s) => {
+          self.blocks[s].list.forEach((b) => (b.alpha = 0));
+          self.blocks[s].auxBlocks.forEach((b) => (b.alpha = 0));
+          self.blocks[s].fractions.forEach((b) => { if (b) b.alpha = 0; });
+          if (self.blocks[s].label) self.blocks[s].label.alpha = 0;
+          if (self.blocks[s].warningText) self.blocks[s].warningText.alpha = 0;
+        });
         if (audioStatus) game.audio.okSound.play();
-        game.add
-          .image(x + 50, context.canvas.height / 2, 'answer_correct')
-          .anchor(0.5, 0.5);
         completedLevels++;
         if (isDebugMode) console.log('Completed Levels: ' + completedLevels);
       } else {
+        // Wrong: keep main blocks visible but hide aux blocks (they don't shift with renderOperationUI)
+        ['top', 'bottom'].forEach((s) => {
+          self.blocks[s].auxBlocks.forEach((b) => (b.alpha = 0));
+        });
         if (audioStatus) game.audio.errorSound.play();
-        game.add
-          .image(x, context.canvas.height / 2, 'answer_wrong')
-          .anchor(0.5, 0.5);
       }
 
       self.fetch.postScore();
@@ -537,11 +1065,14 @@ const squareTwo = {
       self.control.animationDelay++;
 
       if (self.control.animationDelay === 100) {
-        self.utils.renderEndUI();
-        self.control.showEndInfo = true;
-
-        if (self.control.isCorrect) canGoToNextMapPosition = true;
-        else canGoToNextMapPosition = false;
+        if (self.control.isCorrect) {
+          self.utils.renderExplanationUI();
+        } else {
+          const x = self.utils.renderOperationUI();
+          game.add.image(x, context.canvas.height / 3, 'answer_wrong').anchor(0.5, 0.5);
+          self.utils.renderEndUI();
+          self.control.showEndInfo = true;
+        }
       }
     },
     endLevel: function () {
@@ -669,6 +1200,38 @@ const squareTwo = {
       const x = game.math.getMouse(mouseEvent).x;
       const y = game.math.getMouse(mouseEvent).y;
 
+      // Pre-start screen: only handle SIM/NÃO + navigation
+      if (!self.control.started) {
+        if (
+          self.ui.startChoice &&
+          self.ui.startChoice.yes &&
+          self.ui.startChoice.yes.button &&
+          game.math.isOverIcon(x, y, self.ui.startChoice.yes.button)
+        ) {
+          if (audioStatus) game.audio.popSound.play();
+          self.control.challengeAnsweredYes = true;
+          self.utils.updateChallengeChoiceUI();
+          self.utils.startGame();
+          return;
+        }
+        if (
+          self.ui.startChoice &&
+          self.ui.startChoice.no &&
+          self.ui.startChoice.no.button &&
+          game.math.isOverIcon(x, y, self.ui.startChoice.no.button)
+        ) {
+          if (audioStatus) game.audio.popSound.play();
+          self.control.challengeAnsweredYes = false;
+          self.utils.updateChallengeChoiceUI();
+          self.utils.startGame();
+          return;
+        }
+
+        navigation.onInputDown(x, y);
+        game.render.all();
+        return;
+      }
+
       // Click block in (a)
       self.blocks.top.list.forEach((cur) => {
         if (game.math.isOverIcon(x, y, cur)) self.utils.clickSquareHandler(cur);
@@ -679,6 +1242,15 @@ const squareTwo = {
         if (game.math.isOverIcon(x, y, cur)) self.utils.clickSquareHandler(cur);
       });
 
+      // Explanation screen continue/retry button
+      if (self.control.showExplanation) {
+        if (game.math.isOverIcon(x, y, self.ui.explanation.button)) {
+          if (audioStatus) game.audio.popSound.play();
+          canGoToNextMapPosition = self.control.isCorrect;
+          self.utils.endLevel();
+        }
+      }
+
       // Continue button
       if (self.control.showEndInfo) {
         if (game.math.isOverIcon(x, y, self.ui.continue.button)) {
@@ -701,6 +1273,55 @@ const squareTwo = {
     onInputOver: function (mouseEvent) {
       const x = game.math.getMouse(mouseEvent).x;
       const y = game.math.getMouse(mouseEvent).y;
+
+      // Pre-start screen hover: SIM/NÃO + navigation
+      if (!self.control.started) {
+        let isOverChoice = false;
+
+        const choiceTextStyle = { ...textStyles.h3_, fill: colors.blueDark, font: 'bold ' + textStyles.h3_.font };
+
+        const yesBtn = self.ui.startChoice && self.ui.startChoice.yes && self.ui.startChoice.yes.button;
+        const noBtn = self.ui.startChoice && self.ui.startChoice.no && self.ui.startChoice.no.button;
+        const yesVisual = self.ui.startChoice && self.ui.startChoice.yes && self.ui.startChoice.yes.visual;
+        const noVisual = self.ui.startChoice && self.ui.startChoice.no && self.ui.startChoice.no.visual;
+
+        if (yesBtn && game.math.isOverIcon(x, y, yesBtn)) {
+          isOverChoice = true;
+          document.body.style.cursor = 'pointer';
+          if (self.ui.startChoice.yes.text) self.ui.startChoice.yes.text.style = choiceTextStyle;
+          if (yesVisual && yesVisual.outerParts && yesVisual.innerParts) {
+            yesVisual.outerParts.forEach((p) => { p.shadowBlur = 10; });
+            yesVisual.innerParts.forEach((p) => { p.fillColor = '#e8f0fc'; p.lineColor = '#e8f0fc'; });
+          }
+        } else if (yesBtn) {
+          if (self.ui.startChoice.yes.text) self.ui.startChoice.yes.text.style = choiceTextStyle;
+          if (yesVisual && yesVisual.outerParts && yesVisual.innerParts) {
+            yesVisual.outerParts.forEach((p) => { p.shadowBlur = 8; });
+            yesVisual.innerParts.forEach((p) => { p.fillColor = colors.white; p.lineColor = colors.white; });
+          }
+        }
+
+        if (noBtn && game.math.isOverIcon(x, y, noBtn)) {
+          isOverChoice = true;
+          document.body.style.cursor = 'pointer';
+          if (self.ui.startChoice.no.text) self.ui.startChoice.no.text.style = choiceTextStyle;
+          if (noVisual && noVisual.outerParts && noVisual.innerParts) {
+            noVisual.outerParts.forEach((p) => { p.shadowBlur = 10; });
+            noVisual.innerParts.forEach((p) => { p.fillColor = '#e8f0fc'; p.lineColor = '#e8f0fc'; });
+          }
+        } else if (noBtn) {
+          if (self.ui.startChoice.no.text) self.ui.startChoice.no.text.style = choiceTextStyle;
+          if (noVisual && noVisual.outerParts && noVisual.innerParts) {
+            noVisual.outerParts.forEach((p) => { p.shadowBlur = 8; });
+            noVisual.innerParts.forEach((p) => { p.fillColor = colors.white; p.lineColor = colors.white; });
+          }
+        }
+
+        if (!isOverChoice) document.body.style.cursor = 'auto';
+        navigation.onInputOver(x, y);
+        game.render.all();
+        return;
+      }
       let flagA = false;
       let flagB = false;
 
@@ -724,6 +1345,18 @@ const squareTwo = {
 
       if (!flagA && !flagB) document.body.style.cursor = 'auto';
 
+      // Explanation button hover
+      if (self.control.showExplanation && self.ui.explanation.button) {
+        if (game.math.isOverIcon(x, y, self.ui.explanation.button)) {
+          document.body.style.cursor = 'pointer';
+          self.ui.explanation.button.scale = self.ui.explanation.button.initialScale * 1.1;
+          self.ui.explanation.text.style = textStyles.btnLg;
+        } else {
+          self.ui.explanation.button.scale = self.ui.explanation.button.initialScale * 1;
+          self.ui.explanation.text.style = textStyles.btn;
+        }
+      }
+
       // Continue button
       if (self.control.showEndInfo) {
         if (game.math.isOverIcon(x, y, self.ui.continue.button)) {
@@ -779,7 +1412,9 @@ const squareTwo = {
         ', numBlocksB: ' +
         self.blocks.bottom.list.length +
         ', valueB: ' +
-        self.blocks.bottom.selectedAmount;
+        self.blocks.bottom.selectedAmount +
+        '&challenge_answered_yes=' +
+        self.control.challengeAnsweredYes;
 
       // FOR MOODLE
       sendToDatabase(data);

+ 3 - 0
js/globals/globals_tokens.js

@@ -243,6 +243,9 @@ const url = {
       // Map buildings
       ['house', baseUrl + 'scene/building_house.png'],
       ['school', baseUrl + 'scene/building_school.png'],
+      // UI images
+      ['challenge', baseUrl + 'scene/challenge.png'],
+      ['result-bg', baseUrl + 'scene/background-result.png'],
     ],
     sprite: [
       // Game sprites

+ 3 - 0
php/create_ifractions_data_base.sql

@@ -41,7 +41,10 @@ CREATE TABLE `ifractions` (
   `line_mappos` int(5) NOT NULL,
   `line_result` varchar(6) CHARACTER SET latin1 COLLATE latin1_general_ci NOT NULL,
   `line_time` varchar(20) CHARACTER SET latin1 COLLATE latin1_general_ci NOT NULL,
+  -- squareOne: numBlocks, valBlocks, blockIndex, floorIndex
+  -- squareTwo: numBlocksA, valueA, numBlocksB, valueB
   `line_details` varchar(120) CHARACTER SET latin1 COLLATE latin1_general_ci NOT NULL,
+  `challenge_answered_yes` tinyint(1) DEFAULT NULL COMMENT 'squareTwo only: 1=yes, 0=no, NULL=not applicable',
   PRIMARY KEY (`id`)
 ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
 /*!40101 SET character_set_client = @saved_cs_client */;

+ 4 - 2
php/save.php

@@ -51,6 +51,7 @@ $ip = clientIP();
 
 // /js/globals/globals_functions.js: data = line_ip=120.0.0.1&line_name=name&line_lang=pt_BR
 // /js/games/squareOne.js: data += &line_game=square&line_mode=a&line_oper=plus&line_leve=1&line_posi=1&line_resu=true&line_time=3&line_deta=numBlocks:3, valBlocks: 1,1,1, blockIndex: 2, floorIndex: 2;url=php/save.php
+// /js/games/squareTwo.js: data += &line_game=square&line_mode=a&line_oper=equal&line_leve=1&line_posi=1&line_resu=true&line_time=3&line_deta=numBlocksA: 8, valueA: 4, numBlocksB: 2, valueB: 1&challenge_answered_yes=true
 $name = $_REQUEST["line_name"];
 $date = date("Y-m-d H:i:s");
 $lang = $_REQUEST["line_lang"];
@@ -62,6 +63,7 @@ $posi = $_REQUEST["line_posi"];
 $resu = $_REQUEST["line_resu"];
 $time = $_REQUEST["line_time"];
 $deta = $_REQUEST["line_deta"];
+$challenge_answered_yes = isset($_REQUEST["challenge_answered_yes"]) ? (int)($_REQUEST["challenge_answered_yes"] === 'true') : null;
 
 $nameUnchanged = $name; // /js/preMenu.js: playerName
 
@@ -72,9 +74,9 @@ if (is_object($lang))
 
 // Table 'ifractions': line_id line_hostip line_playername line_datetime line_lang line_game line_mode line_operator line_level line_mappos line_result line_time line_details
 $sql = "INSERT INTO $tablename
-(line_hostip, line_playername, line_datetime, line_lang, line_game, line_mode, line_operator, line_level, line_mappos, line_result, line_time, line_details)
+(line_hostip, line_playername, line_datetime, line_lang, line_game, line_mode, line_operator, line_level, line_mappos, line_result, line_time, line_details, challenge_answered_yes)
 VALUES
-('$ip', '$name', '$date', '$lang', '$game', '$mode', '$oper', $leve, $posi, '$resu', $time, '$deta')";
+('$ip', '$name', '$date', '$lang', '$game', '$mode', '$oper', $leve, $posi, '$resu', $time, '$deta', " . ($challenge_answered_yes === null ? "NULL" : $challenge_answered_yes) . ")";
 
 // Register in database
 if ($conn->query($sql) === TRUE) {