| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423 |
- /******************************
- * This file holds game states.
- ******************************/
- /** [GAME STATE]
- *
- * .squareTwo. = gameName
- * .../...\...
- * ..a.....b.. = gameMode
- * ....\./....
- * .....|.....
- * ..equals... = gameOperation
- * .....|.....
- * .1,2,3,4,5. = gameDifficulty
- *
- * Character : kid
- * Theme : (not themed)
- * Concept : player select equivalent dividends for fractions with different divisors
- * Represent fractions as : subdivided rectangles
- *
- * Game modes can be :
- *
- * a : equivalence of fractions
- * top has more subdivisions
- * b : equivalence of fractions
- * bottom has more subdivisions
- *
- * Operations :
- *
- * equals : Player selects equivalent fractions of both blocks
- *
- * @namespace
- */
- const squareTwo = {
- ui: undefined,
- control: undefined,
- blocks: undefined,
- /**
- * Main code
- */
- 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: {
- list: [], // List of block objects
- auxBlocks: [], // List of shadow under selection blocks
- fractions: [], // Fractions
- selectedAmount: 0,
- hasClicked: false, // Check if player clicked blocks from (a)
- animate: false, // Animate blocks from (a)
- warningText: undefined,
- label: undefined,
- },
- bottom: {
- list: [],
- auxBlocks: [],
- fractions: [],
- selectedAmount: 0,
- hasClicked: false,
- animate: false,
- warningText: undefined,
- label: undefined,
- },
- };
- renderBackground();
- // Calls function that loads navigation icons
- // FOR MOODLE
- if (moodle) {
- navigation.add.right(['audio']);
- } else {
- navigation.add.left(['back', 'menu'], 'customMenu');
- navigation.add.right(['audio']);
- }
- // 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.event.add('click', this.events.onInputDown);
- game.event.add('mousemove', this.events.onInputOver);
- },
- /**
- * Game loop
- */
- update: function () {
- // Animate blocks
- if (self.blocks.top.animate || self.blocks.bottom.animate) {
- self.utils.moveBlocks();
- }
- // If (a) and (b) are already clicked
- if (
- !self.control.startDelay &&
- !self.control.startEndAnimation &&
- self.blocks.top.hasClicked &&
- self.blocks.bottom.hasClicked
- ) {
- self.control.startDelay = true;
- }
- if (self.control.startDelay && !self.control.startEndAnimation) {
- self.utils.startDelayHandler();
- }
- // Wait a bit and go to map state
- if (self.control.startEndAnimation) {
- self.utils.runEndAnimation();
- }
- game.render.all();
- },
- utils: {
- // RENDERS
- generateBlockConfig: function () {
- // Coordinates for (a) and (b)
- let xA, xB, yA, yB;
- if (gameMode != '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 (b)
- xB = context.canvas.width / 2 - self.control.blockWidth / 2;
- yB = getFrameInfo().y + 100;
- xA = xB;
- yA = yB + 3 * self.control.blockHeight + 30;
- }
- // Possible subdivisionList for (a)
- const subdivisionList = [2, 4, 6, 8, 9, 10, 12, 14, 15, 16, 18, 20];
- // Random index for 'subdivision'
- const randomIndex = game.math.randomInRange(
- (gameDifficulty - 1) * 2 + 1,
- (gameDifficulty - 1) * 2 + 3
- );
- // Number of subdivisions of (a) and (b) (blocks)
- const totalBlocksA = subdivisionList[randomIndex];
- const totalBlocksB = game.math.randomDivisor(totalBlocksA);
- const blockWidthA = self.control.blockWidth / totalBlocksA;
- const blockWidthB = self.control.blockWidth / totalBlocksB;
- if (isDebugMode) {
- console.log(
- '------------------------------' +
- '\nGame Map Position: ' +
- curMapPosition +
- '\n------------------------ setup' +
- '\narray: [2, 4, 6, 8, 9, 10, 12, 14, 15, 16, 18, 20]' +
- '\nMin index ((gameDifficulty - 1) * 2 + 1): ' +
- ((gameDifficulty - 1) * 2 + 1) +
- '\nMax index ((gameDifficulty - 1) * 2 + 3): ' +
- ((gameDifficulty - 1) * 2 + 3) +
- '\n------------------------ this' +
- '\nget random min max for A: array[' +
- randomIndex +
- '] = ' +
- totalBlocksA +
- '\nget random divisor for B: ' +
- totalBlocksB +
- '\n------------------------------'
- );
- }
- 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,
- 'top',
- totalBlocksA,
- blockWidthA,
- colors.blueDark,
- colors.blueLight,
- xA,
- yA
- );
- // (b)
- self.utils.renderBlocks(
- self.blocks.bottom,
- 'bottom',
- totalBlocksB,
- blockWidthB,
- colors.greenDark,
- colors.greenLight,
- xB,
- yB
- );
- },
- renderBlocks: function (
- blocks,
- blockType,
- totalBlocks,
- blockWidth,
- lineColor,
- fillColor,
- x0,
- y0
- ) {
- for (let i = 0; i < totalBlocks; i++) {
- // Blocks
- const curX = x0 + i * blockWidth;
- const curBlock = game.add.geom.rect(
- curX,
- y0,
- blockWidth,
- self.control.blockHeight,
- fillColor,
- 0.5,
- lineColor,
- 4
- );
- curBlock.position = blockType;
- curBlock.index = i;
- curBlock.finalX = x0;
- blocks.list.push(curBlock);
- // Auxiliar blocks (lower alpha)
- const alpha = 0.2;
- const curYAux = y0 + self.control.blockHeight + 10;
- const curAuxBlock = game.add.geom.rect(
- curX,
- curYAux,
- blockWidth,
- self.control.blockHeight,
- fillColor,
- alpha,
- lineColor,
- 1
- );
- blocks.auxBlocks.push(curAuxBlock);
- }
- // Label - number of blocks (on the right)
- let yLabel = y0 + self.control.blockHeight / 2 + 10;
- const xLabel = x0 + self.control.blockWidth + 35;
- const font = {
- ...textStyles.h4_,
- font: 'bold ' + textStyles.h4_.font,
- fill: lineColor,
- };
- blocks.label = game.add.text(xLabel, yLabel, blocks.list.length, font);
- blocks.label.alpha = showFractions ? 1 : 0;
- // 'selected blocks/fraction' label for (a) : at the bottom of (a)
- yLabel = y0 + self.control.blockHeight + 40;
- blocks.fractions[0] = game.add.text(xLabel, yLabel, '', font);
- blocks.fractions[1] = game.add.geom.line(
- xLabel,
- yLabel + 10,
- xLabel + 50,
- yLabel + 10,
- 2,
- lineColor
- );
- blocks.fractions[1].anchor(0.5, 0);
- blocks.fractions[0].alpha = 0;
- blocks.fractions[1].alpha = 0;
- // Invalid selection text
- blocks.warningText = game.add.text(
- context.canvas.width / 2,
- y0 - 20,
- game.lang.s2_error_msg,
- { ...font, font: textStyles.h4_.font }
- );
- blocks.warningText.alpha = 0;
- },
- renderCharacters: function () {
- self.kidAnimation = game.add.sprite(
- 100,
- context.canvas.height - 128 * 1.5,
- 'kid_standing',
- 0,
- 1.2
- );
- self.kidAnimation.anchor(0.5, 0.7);
- self.kidAnimation.curFrame = 3;
- },
- renderMainUI: () => {
- // Intro text
- const treatedMessage = game.lang.squareTwo_intro.split('\\n');
- const font = textStyles.h1_;
- self.ui.message = [];
- self.ui.message.push(
- game.add.text(
- context.canvas.width / 2,
- 170,
- treatedMessage[0] + '\n' + treatedMessage[1],
- font
- )
- );
- },
- renderOperationUI: () => {
- const uiList = [
- ...self.blocks.top.list,
- ...self.blocks.bottom.list,
- ...self.blocks.top.fractions,
- ...self.blocks.bottom.fractions,
- ];
- moveList(uiList, -400, 0);
- const font = textStyles.fraction;
- font.fill = colors.black;
- font.align = 'center';
- const nominators = [
- self.blocks.top.selectedAmount,
- self.blocks.bottom.selectedAmount,
- ];
- const denominators = [
- self.blocks.top.list.length,
- self.blocks.bottom.list.length,
- ];
- if (gameMode === 'b') {
- const leftNom = nominators[0];
- const leftDenom = denominators[0];
- nominators[0] = nominators[1];
- denominators[0] = denominators[1];
- nominators[1] = leftNom;
- denominators[1] = leftDenom;
- }
- const renderList = [];
- const padding = 100;
- const offsetX = 100;
- const cardHeight = 400;
- const x0 = padding + 400;
- const y0 = context.canvas.height / 2;
- let nextX = x0;
- const cardX = x0 - padding;
- const cardY = y0;
- // Card
- const card = game.add.geom.rect(
- cardX,
- cardY,
- 0,
- cardHeight,
- colors.blueLight,
- 0.5,
- colors.blueDark,
- 8
- );
- card.id = 'card';
- card.anchor(0, 0.5);
- renderList.push(card);
- renderList.push(
- game.add.text(
- nextX,
- y0,
- nominators[0] + '\n' + denominators[0],
- font,
- 70
- )
- );
- const topFractionLine = game.add.geom.rect(
- nextX,
- y0 + 10,
- 100,
- 4,
- colors.black,
- 4
- );
- topFractionLine.anchor(0.5, 0);
- renderList.push(topFractionLine);
- font.fill = self.control.isCorrect ? colors.green : colors.red;
- nextX += offsetX;
- renderList.push(
- game.add.text(nextX, y0 + 35, self.control.isCorrect ? '=' : '≠', font)
- );
- font.fill = colors.black;
- nextX += offsetX;
- renderList.push(
- game.add.text(
- nextX,
- y0,
- nominators[1] + '\n' + denominators[1],
- font,
- 70
- )
- );
- const bottomFractionLine = game.add.geom.rect(
- nextX,
- y0 + 10,
- 100,
- 4,
- colors.black,
- 4
- );
- bottomFractionLine.anchor(0.5, 0);
- renderList.push(bottomFractionLine);
- //let resultWidth = ''.length * widthOfChar;
- const cardWidth = nextX - x0 + padding * 2;
- card.width = cardWidth;
- const endSignX =
- (context.canvas.width - cardWidth) / 2 + cardWidth + 400 + 50;
- // Center Card
- moveList(renderList, (context.canvas.width - cardWidth) / 2, 0);
- self.fractionOperationUI = renderList;
- 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;
- if (!self.control.isCorrect) {
- btnColor = colors.red;
- btnText = game.lang.retry;
- }
- // continue button
- self.ui.continue.button = game.add.geom.rect(
- context.canvas.width / 2 + 400,
- context.canvas.height / 2 + 280,
- 450,
- 100,
- btnColor
- );
- self.ui.continue.button.anchor(0.5, 0.5);
- self.ui.continue.text = game.add.text(
- context.canvas.width / 2 + 400,
- context.canvas.height / 2 + 16 + 280,
- btnText,
- textStyles.btn
- );
- },
- startDelayHandler: () => {
- game.timer.stop();
- self.control.animationDelay++;
- if (self.control.animationDelay === 50) {
- self.ui.message[0].alpha = 0;
- self.utils.checkAnswer();
- self.control.animationDelay = 0;
- self.control.startEndAnimation = true;
- }
- },
- // UPDATE
- moveBlocks: function () {
- ['top', 'bottom'].forEach((cur) => {
- if (self.blocks[cur].animate) {
- // Lower selected blocks
- for (let i = 0; i < self.blocks[cur].selectedAmount; i++) {
- self.blocks[cur].list[i].y += 2;
- }
- // After fully lowering blocks, set fraction value
- if (self.blocks[cur].list[0].y >= self.blocks[cur].auxBlocks[0].y) {
- self.blocks[cur].fractions[0].name =
- self.blocks[cur].selectedAmount +
- '\n' +
- self.blocks[cur].list.length;
- self.blocks[cur].animate = false;
- }
- }
- });
- },
- checkAnswer: function () {
- self.control.isCorrect =
- self.blocks.top.selectedAmount / self.blocks.top.list.length ==
- self.blocks.bottom.selectedAmount / self.blocks.bottom.list.length;
- 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();
- 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();
- }
- self.fetch.postScore();
- },
- runEndAnimation: function () {
- self.control.animationDelay++;
- if (self.control.animationDelay === 100) {
- 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 () {
- game.state.start('map');
- },
- // HANDLERS
- /**
- * Function called by self.onInputDown() when player clicked a valid rectangle.
- *
- * @param {object} curBlock clicked rectangle : can be self.blocks.top.list[i] or self.blocks.bottom.list[i]
- */
- clickSquareHandler: function (curBlock) {
- const curSet = curBlock.position;
- if (
- !self.blocks[curSet].hasClicked &&
- curBlock.index != self.blocks[curSet].list.length - 1
- ) {
- document.body.style.cursor = 'auto';
- // Turn auxiliar blocks invisible
- for (let i in self.blocks[curSet].list) {
- if (i > curBlock.index) self.blocks[curSet].auxBlocks[i].alpha = 0;
- }
- // Turn value label invisible
- self.blocks[curSet].label.alpha = 0;
- if (audioStatus) game.audio.popSound.play();
- // Save number of selected blocks
- self.blocks[curSet].selectedAmount = curBlock.index + 1;
- // Set fraction x position
- const newX =
- curBlock.finalX +
- self.blocks[curSet].selectedAmount *
- (self.control.blockWidth / self.blocks[curSet].list.length) +
- 40;
- self.blocks[curSet].fractions[0].x = newX;
- self.blocks[curSet].fractions[1].x = newX;
- self.blocks[curSet].fractions[0].name = `${curBlock.index + 1}\n${
- self.blocks[curSet].list.length
- }`;
- // End fraction line
- self.blocks[curSet].fractions[1].alpha = showFractions ? 1 : 0;
- self.blocks[curSet].hasClicked = true; // Inform player have clicked in current block set
- self.blocks[curSet].animate = true; // Let it initiate animation
- }
- game.render.all();
- },
- /**
- * Function called by self.onInputOver() when cursor is over a valid rectangle.
- *
- * @param {object} curBlock rectangle the cursor is over : can be self.blocks.top.list[i] or self.blocks.bottom.list[i]
- */
- overSquareHandler: function (curBlock) {
- const curSet = curBlock.position;
- if (!self.blocks[curSet].hasClicked) {
- // self.blocks.top.hasClicked || self.blocks.bottom.hasClicked
- // If over fraction 'n/n' shows warning message not allowing it
- if (curBlock.index == self.blocks[curSet].list.length - 1) {
- const otherSet = curSet == 'top' ? 'bottom' : 'top';
- self.blocks[curSet].warningText.alpha = 1;
- self.blocks[otherSet].warningText.alpha = 0;
- self.utils.outSquareHandler(curSet);
- } else {
- document.body.style.cursor = 'pointer';
- self.blocks.top.warningText.alpha = 0;
- self.blocks.bottom.warningText.alpha = 0;
- // Selected blocks become fully visible
- for (let i in self.blocks[curSet].list) {
- self.blocks[curSet].list[i].alpha = i <= curBlock.index ? 1 : 0.5;
- }
- self.blocks[curSet].fractions[0].name = curBlock.index + 1; // Nominator : selected blocks
- const newX =
- curBlock.finalX +
- (curBlock.index + 1) *
- (self.control.blockWidth / self.blocks[curSet].list.length) +
- 25;
- self.blocks[curSet].fractions[0].x = newX;
- self.blocks[curSet].fractions[1].x = newX;
- // End fraction nominator and denominator
- self.blocks[curSet].fractions[0].alpha = showFractions ? 1 : 0;
- }
- }
- },
- /**
- * Function called (by self.onInputOver() and self.utils.overSquareHandler()) when cursor is out of a valid rectangle.
- *
- * @param {object} curSet set of rectangles : can be top (self.blocks.top) or bottom (self.blocks.bottom)
- */
- outSquareHandler: function (curSet) {
- if (!self.blocks[curSet].hasClicked) {
- self.blocks[curSet].fractions[0].alpha = 0;
- self.blocks[curSet].fractions[1].alpha = 0;
- self.blocks[curSet].list.forEach((cur) => {
- cur.alpha = 0.5;
- });
- }
- },
- },
- events: {
- /**
- * Called by mouse click event
- *
- * @param {object} mouseEvent contains the mouse click coordinates
- */
- onInputDown: function (mouseEvent) {
- 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);
- });
- // Click block in (b)
- self.blocks.bottom.list.forEach((cur) => {
- 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)) {
- if (audioStatus) game.audio.popSound.play();
- self.utils.endLevel();
- }
- }
- // Click navigation icons
- navigation.onInputDown(x, y);
- game.render.all();
- },
- /**
- * Called by mouse move event
- *
- * @param {object} mouseEvent contains the mouse move coordinates
- */
- 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;
- // Mouse over (a) : show fraction
- self.blocks.top.list.forEach((cur) => {
- if (game.math.isOverIcon(x, y, cur)) {
- flagA = true;
- self.utils.overSquareHandler(cur);
- }
- });
- if (!flagA) self.utils.outSquareHandler('top');
- // Mouse over (b) : show fraction
- self.blocks.bottom.list.forEach((cur) => {
- if (game.math.isOverIcon(x, y, cur)) {
- flagB = true;
- self.utils.overSquareHandler(cur);
- }
- });
- if (!flagB) self.utils.outSquareHandler('bottom');
- 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)) {
- // If pointer is over icon
- document.body.style.cursor = 'pointer';
- self.ui.continue.button.scale =
- self.ui.continue.button.initialScale * 1.1;
- self.ui.continue.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;
- }
- }
- // Mouse over navigation icons : show name
- navigation.onInputOver(x, y);
- game.render.all();
- },
- },
- fetch: {
- /**
- * Saves players data after level ends - to be sent to database. <br>
- *
- * Attention: the 'line_' prefix data table must be compatible to data table fields (MySQL server)
- *
- * @see /php/save.php
- */
- postScore: function () {
- // Creates string that is going to be sent to db
- const data =
- '&line_game=' +
- gameShape +
- '&line_mode=' +
- gameMode +
- '&line_oper=equal' +
- '&line_leve=' +
- gameDifficulty +
- '&line_posi=' +
- curMapPosition +
- '&line_resu=' +
- self.control.isCorrect +
- '&line_time=' +
- game.timer.elapsed +
- '&line_deta=' +
- 'numBlocksA: ' +
- self.blocks.top.list.length +
- ', valueA: ' +
- self.blocks.top.selectedAmount +
- ', numBlocksB: ' +
- self.blocks.bottom.list.length +
- ', valueB: ' +
- self.blocks.bottom.selectedAmount +
- '&challenge_answered_yes=' +
- self.control.challengeAnsweredYes;
- // FOR MOODLE
- sendToDatabase(data);
- },
- },
- };
|