
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

/**
 * Spacegame app javascript for Poodll minilesson
 *
 * @package    mod_minilesson
 * @copyright  2024 Justin Hunt (poodllsupport@gmail.com)
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

define(['jquery', 'core/notification', 'mod_minilesson/definitions', 'core/log', 'core/templates', 'core/ajax', 'core/str'],
    function($, notification, def, log, templates, Ajax, str) {

  "use strict"; // jshint ;_;

  /*
  This file is to manage the Space Game item type.
   */

  log.debug('MiniLesson Space Game: initialising');

    class Rectangle {
        /**
         * Constructor for storing information about a rectangle shape.
         * @param {int} left
         * @param {int} top
         * @param {int} width
         * @param {int} height
         */
        constructor(left, top, width, height) {
            this.left = left || 0;
            this.top = top || 0;
            this.width = width || 0;
            this.height = height || 0;
        }

        right () {
            return this.left + this.width;
        };

        bottom() {
            return this.top + this.height;
        };

        Contains (point) {
            return point.x > this.left &&
                point.x < this.right() &&
                point.y > this.top &&
                point.y < this.bottom();
        };

        Intersect(rectangle) {
            var retval = !(rectangle.left > this.right() ||
                rectangle.right() < this.left ||
                rectangle.top > this.bottom() ||
                rectangle.bottom() < this.top);
            return retval;
        };
    }
    class GameObject {
        /**
         * Generate game object.
         * @param {text} src
         * @param {int} x
         * @param {int} y
         */
        constructor(src, x, y) {
            if (src !== null) {
                this.image = this.loadImage(src);
            }
            this.x = x;
            this.y = y;
            this.velocity = {x: 0, y: 0};
            this.direction = {x: 0, y: 0};
            this.movespeed = {x: 5, y: 3};
            this.alive = true;
            this.decay = 0.7;
        }

        loadImage(src) {
            if (!this.image) {    
                this.image = new Image();
            }
            // we dont use the pre-loaded images here, we should, but we need diff images in matching
            this.image.src = M.cfg.wwwroot +'/mod/minilesson/' + src;
            return this.image;
        }

        update() {
            this.velocity.x += this.direction.x * this.movespeed.x;
            this.velocity.y += this.direction.y * this.movespeed.y;
            this.x += this.velocity.x;
            this.y += this.velocity.y;
            this.velocity.y *= this.decay;
            this.velocity.x *= this.decay;
        };

        draw (context) {
            context.drawImage(this.image, this.x, this.y, this.image.width, this.image.height);
        };

        getRect() {
            return new Rectangle(this.x, this.y, this.image.width, this.image.height);
        };

        die() {
            this.alive = false;
        };
    }

    /**
     * Represents a player in the game, handling movement, shooting, and collision detection.
     */
    class Player extends GameObject {
        /**
         * Constructor for Player class, all the information about the player
         * @param {string} src
         * @param {int} x
         * @param {int} y
         */
        constructor(src, x, y) {
            super( src, x, y);
            this.mouse = {x: 0, y: 0};
            this.movespeed = {x: 6, y: 4};
            this.lives = 3;
            this.lastScore = 0;
        }

        update(bounds) {
            if (app.mouseDown || app.touchDown) {
                if (this.x < this.mouse.x - (this.image.width)) {
                    app.player.direction.x = 1;
                } else if (this.x > this.mouse.x) {
                    app.player.direction.x = -1;
                } else {
                    app.player.direction.x = 0;
                }
                if (this.y < this.mouse.y - (this.image.height)) {
                    app.player.direction.y = 1;
                } else if (this.y > this.mouse.y) {
                    app.player.direction.y = -1;
                } else {
                    app.player.direction.y = 0;
                }
            }
            super.update(bounds);

            if (this.x < bounds.x - this.image.width) {
                this.x = bounds.width;
            } else if (this.x > bounds.width) {
                this.x = bounds.x - this.image.width;
            }
            if (this.y < bounds.y) {
                this.y = bounds.y;
            } else if (this.y > bounds.height - this.image.height) {
                this.y = bounds.height - this.image.height;
            }
        };

        Shoot() {
            app.playSound("laser");
            var shooter = this;
            app.gameObjects.unshift(new Laser( app.player.x, app.player.y,shooter, true, 24));
            app.canShoot = false;
        }

        die() {
            super.die();
            app.playSound("explosion");
            app.spray(this.x + this.image.width / 2, this.y + this.image.height / 2, 200, "#FFCC00");
            this.lastScore = app.score;
            app.endGame();
        }

        gotShot(shot) {
            if (shot.alive) {
                if (this.lives <= 1) {
                    this.die();
                } else {
                    this.lives--;
                    app.spray(this.x + this.image.width / 2, this.y + this.image.height / 2, 100, "#FFCC00");
                }
            }
        }
    }

    // Represents a planet in the game, serving as a background object.
    class Planet extends GameObject{
        /**
         * Constructor for Planet (background objects) extends GameObject
         * @param {string} src
         * @param {int} x
         * @param {int} y
         */
        constructor(src, x, y ) {
            super(src,x,y);
        }

        update(bounds) {
            this.image.width = app.displayRect.width;
            this.image.height = app.displayRect.height;
            super.update();
        }

    }

    // Represents an enemy in the game. Adds enemy-specific behavior like shooting and movement patterns.
    class Enemy extends GameObject {
        constructor(src, x, y, text, itempoints, termid) {
            super(src,x,y);
            this.xspeed = app.enemySpeed;
            this.yspeed = app.enemySpeed * (2 + Math.random()) / 4;
            this.movespeed.x = 0;
            this.movespeed.y = 0;
            this.direction.y = 1;
            this.text = text;
            this.itempoints = itempoints;
            this.movementClock = 0;
            this.shotFrequency = 80;
            this.shotClock = (1 + Math.random()) * this.shotFrequency;
            this.level = app.level;
            this.termid = termid;

        }

        update(bounds) {
            if (this.y < bounds.height / 10 || this.y > bounds.height * 9 / 10) {
                this.movespeed.x = this.xspeed * 1;
                this.movespeed.y = this.yspeed * 5;
            } else {
                this.movespeed.x = this.xspeed;
                this.movespeed.y = this.yspeed;
            }

            super.update(bounds);

            this.movementClock--;
            this.movementClock--;

            if (this.movementClock <= 0) {
                this.direction.x = Math.floor(Math.random() * 3) - 1;
                this.movementClock = (2 + Math.random()) * 30;
            }

            this.shotClock -= app.enemySpeed;

            if (this.shotClock <= 0) {
                if (this.y < bounds.height * 0.6) {
                    app.playSound("enemylaser");
                    var shooter=this;
                    var laser = new Laser(this.x, this.y, shooter);
                    laser.direction.y = 1;
                    laser.friendly = false;
                    app.gameObjects.unshift(laser);
                    this.shotClock = (1 + Math.random()) * this.shotFrequency;
                }
            }

            if (this.x < bounds.x - this.image.width) {
                this.x = bounds.width;
            } else if (this.x > bounds.width) {
                this.x = bounds.x - this.image.width;
            }
            if (this.y > bounds.height + this.image.height && this.alive) {
                this.alive = false;
                if (this.itempoints > 0) {
                    app.currentPointsLeft -= this.itempoints;
                    app.score -= 1000 * this.itempoints;
                }

                app.shipReachedEnd.call(this);
            }
        }

        draw(context){

            super.draw(context);
            context.fillStyle = '#FFFFFF';
            context.font = "20px Audiowide";
            context.textAlign = 'center';

            app.wrapText(context, this.text, true, 17, app.displayRect.width * 0.2, this.x + this.image.width / 2, this.y - 5);
        }

        die() {
            super.die();
            app.spray(this.x + this.image.width, this.y + this.image.height, 50 + (this.itempoints * 150), "#FF0000");

            // Adjust Score.
            app.score += this.itempoints * 1000;

            //report the positive association
            app.reportSuccess(this.termid);

            // Kill off the ship.
            app.playSound("explosion");
        }

        gotShot(shot) {
            // Default behaviour, to be overridden.
            shot.die();
            this.die();

        }
    }

    // Represents a laser in the game, capable of moving across the screen and interacting with other game objects.
    class Laser extends GameObject {
        constructor(x, y, shooter, friendly, laserSpeed) {

            super(friendly ? "pix/spacegame/laser.png" : "pix/spacegame/enemylaser.png", x, y);
            this.x = this.x + ((shooter.image.width - this.image.width) / 2);
            this.direction.y = -1;
            this.friendly = friendly ? 1 : 0;
            this.laserSpeed = laserSpeed || 12;
        }

        update(bounds) {
            super.update(bounds);
            if (this.x < bounds.x - this.image.width ||
                this.x > bounds.width ||
                this.y < bounds.y - this.image.height ||
                this.y > bounds.height) {
                this.alive = false;
            }
            this.velocity.y = this.laserSpeed * this.direction.y;
        };

        deflect() {
            this.image = this.loadImage("pix/spacegame/enemylaser.png");
            this.direction.y *= -1;
            this.friendly = !this.friendly;
            app.playSound("deflect");
        };
    }

    class Particle extends GameObject {
        constructor(x, y, velocity, colour) {
            super(null, x, y);
            this.width = 2;
            this.height = 2;
            this.velocity.x = velocity.x;
            this.velocity.y = velocity.y;
            this.aliveTime = 0;
            this.colour = colour;
            this.decay = 1;
        }

        update(bounds){
            super.update(bounds);
            if (this.x < bounds.x - this.width ||
                this.x > bounds.width ||
                this.y < bounds.y - this.height ||
                this.y > bounds.height) {
                this.alive = false;
            }
            this.aliveTime++;
            if (this.aliveTime > (Math.random() * 15) + 5) {
                this.alive = false;
            }
        }

        getRect(){
            return new Rectangle(this.x, this.y, this.width, this.height);
        }

        draw(context){
            context.fillStyle = this.colour;
            context.fillRect(this.x, this.y, this.width, this.height);
            context.stroke();
        }
    }

    // Represents a star in the background of the game, moving downwards to create a parallax effect.
    class Star extends GameObject {
        constructor(bounds) {
            super( null, Math.random() * bounds.width, 0);
            this.width = 2;
            this.height = 2;
            this.direction.y = 1;
            this.movespeed.y = 0.2 + (Math.random() / 2);
            this.aliveTime = 0;
        }

        update(bounds){
            super.update(bounds);
            if (this.y > bounds.height) {
                this.alive = false;
            }
        }

        draw(context){
            context.fillStyle = '#9999AA';
            context.fillRect(this.x, this.y, this.width, this.height);
            context.stroke();
        }
    }

    /**
     * MultiEnemy extends the Enemy class to represent a more complex enemy type in the game.
     *
     * @param {int} x
     * @param {int} y
     * @param {string} text
     * @param {float} itempoints
     * @param {boolean} single
     */
    class MultiEnemy extends Enemy {
        constructor(x, y, text, itempoints, single,termid) {
            super("pix/spacegame/ship-enemy-yellow-" + app.shipsize + ".png", x, y, text, itempoints, termid);
            this.single = single;
        }

        die() {
            super.die();
            if (this.itempoints > 0) {
                app.currentPointsLeft -= this.itempoints;
            }
            // Store the result for later processing.
            app.storeResult(this.termid, this.itempoints);
            if ((this.single && this.itempoints === 1) && this.itempoints >= 1 || (this.itempoints > 0 && app.currentPointsLeft <= 0)) {
                app.killAllAlive();
                app.nextLevel();
            }
        }

        gotShot(shot) {
            if (this.itempoints > 0) {
                shot.die();
                this.die();

                // Report the positive association.
                app.reportSuccess(this.termid);

            } else {
                app.score += (this.itempoints - 0.5) * 600;

                // Record negative association for later processing.
                app.storeResult(this.termid, 0);

                shot.deflect();
            }
        }
    }

    // MatchEnemy extends the Enemy class to represent enemies that can be matched/paired in the game.
    class MatchEnemy extends Enemy {
        constructor(x, y, text, itempoints, pairid, stem, termid  ) {

            if (stem) {
                super("pix/spacegame/ship-enemy-green-" + app.shipsize + ".png", x, y, text, itempoints, termid);
            } else {
               // super("pix/spacegame/ship-enemy-purple-64.png", x, y, text, itempoints, termid);
                super("pix/spacegame/ship-enemy-green-" + app.shipsize + ".png", x, y, text, itempoints, termid);
            }
            this.stem = stem ? true : false;
            this.pairid = pairid;
            this.shotFrequency = 160;
            this.hightlighted = false;
        }

        die() {
            app.currentPointsLeft -= this.itempoints;
            // Sets the itempoints as 0 to stop it adding to the score in #die()
            this.itempoints = 0;
            super.die();

        }

        gotShot(shot) {
            if (shot.alive && this.alive) {
                if (app.lastShot == -this.pairid) {

                    // Increasing the score here instead of in #die(), due to rounding issues being a few numbers off.
                    // This must be done before because when #die is invoked, as it sets the itempoints as 0.
                    app.score += this.itempoints * 1000 * 2;

                    //store the result for later processing
                    app.storeResult(this.termid, this.itempoints);

                    //report the positive association
                    app.reportSuccess(this.termid);


                    shot.die();
                    this.die();
                    var alives = 0;
                    app.currentTeam.forEach(function (match) {
                        if (match.pairid == app.lastShot) {
                            match.die();
                        }
                        if (match.alive) {
                            alives++;
                        }
                    });

                    if (alives <= 0) {
                        app.nextLevel();
                    }
                } else {
                    if (app.lastShot == this.pairid) {
                        shot.deflect();
                    } else {
                        shot.die();
                        this.hightlight();
                        app.lastShot = this.pairid;
                    }
                }
            }
        }

        hightlight() {
            app.currentTeam.forEach(function (match) {
                match.unhightlight();
            });
            if (this.stem) {
                this.loadImage("pix/spacegame/ship-enemy-blue-" + app.shipsize + ".png");
            } else {
                this.loadImage("pix/spacegame/ship-enemy-blue-" + app.shipsize + ".png");
            }
            this.hightlighted = true;
        };

        unhightlight() {
            if (this.hightlighted) {
                if (this.stem) {
                    this.loadImage("pix/spacegame/ship-enemy-green-" + app.shipsize + ".png");
                } else {
                    this.loadImage("pix/spacegame/ship-enemy-green-" + app.shipsize + ".png");
                }
            }
            this.hightlighted = false;
        }
    }

    // Start of app declaration.
var app = {
    isFreeMode:  false,
    strings: {"shootthepairs": "Shoot the Pairs"},
    termAsAlien: "0",
    questions: [],
    quizgame: null,
    stage: null,
    score: 0,
    particles: [],
    gameObjects: [],
    shipsize: "48", //TO DO: make this a setting and refactor image loading to be not clunky
    images: [
        'pix/spacegame/icon.gif',
        'pix/spacegame/planet.png',
        'pix/spacegame/ship.png',
        'pix/spacegame/ship-poodll-64.png',
        'pix/spacegame/ship-enemy-green-64.png',
        'pix/spacegame/ship-enemy-yellow-64.png',
        'pix/spacegame/ship-enemy-blue-64.png',
        'pix/spacegame/ship-poodll-48.png',
        'pix/spacegame/ship-enemy-green-48.png',
        'pix/spacegame/ship-enemy-yellow-48.png',
        'pix/spacegame/ship-enemy-blue-48.png',
        'pix/spacegame/space-bckg.png',
        'pix/spacegame/enemy.png',
        'pix/spacegame/enemystem.png',
        'pix/spacegame/enemychoice.png',
        'pix/spacegame/enemystemselected.png',
        'pix/spacegame/enemychoiceselected.png',
        'pix/spacegame/laser.png',
        'pix/spacegame/enemylaser.png'
    ],
    imagesLoaded:  0,
    loadedImages:[],
    loaded: false,
    player: null,
    planet: null,
    level: -1,
    displayRect: {x: 0, y: 0, width: 0, height: 0},
    question: "",
    interval: null,
    enemySpeed: null,
    touchDown: false,
    mouseDown: false,
    currentTeam: [],
    lastShot: 0,
    currentPointsLeft:  0,
    context: null,
    inFullscreen: false,
    canShoot: true,
    dryRun: false,
    ttslanguage: 'en-US',
    distractors: [],
    controls: {},
    results: [],
    timer: null,



    registerSpaceGameEvents: function(){

        document.onkeyup = this.keyup;
        document.onkeydown = this.keydown;
        document.onmouseup = this.mouseup;
        document.onmousedown = this.mousedown;
        document.onmousemove = this.mousemove;
        document.ontouchstart = this.touchstart;
        document.ontouchend = this.touchend;
        document.addEventListener('touchmove',this.touchmove, {passive: false});
        window.onresize = this.orientationChange;

        document.addEventListener("gesturestart", this.cancelled, false);
        document.addEventListener("gesturechange", this.cancelled, false);
        document.addEventListener("gestureend", this.cancelled, false);
    }  ,

    /**
     * Play sound effect.
     * @param {string} soundName
     */
    playSound: function(soundName) {
        if (document.getElementById("mod_minilesson_spacegame_sound_on").checked) {
            var soundElement = document.getElementById("mod_minilesson_sound_" + soundName);
            soundElement.currentTime = 0;
            soundElement.play();
        }
    },

    /**
     * Adjust for small screens.
     */
    smallscreen: function() {
        app.inFullscreen = false;

        if (document.fullscreenElement) {
            if (document.exitFullscreen) {
                let exitPromise = document.exitFullscreen();
                if (exitPromise instanceof Promise) {
                    exitPromise.then(() => console.log("Document Exited from Full screen mode"))
                               .catch((err) => console.error(err));
                } else {
                    console.log("Document Exited from Full screen mode");
                }
            } else if (document.webkitExitFullscreen) { /* Safari */
                document.webkitExitFullscreen();
                console.log("Document Exited from Full screen mode (webkit)");
            } else if (document.msExitFullscreen) { /* IE11 */
                document.msExitFullscreen();
                console.log("Document Exited from Full screen mode (ms)");
            } else {
                console.error("Fullscreen exit is not supported by this browser");
            }
        } else {
            console.log("Document is not in fullscreen mode");
        }

        app.stage.removeAttribute("width");
        app.stage.removeAttribute("height");
        app.stage.removeAttribute("style");

        app.stage.classList.remove("floating-game-canvas");
        $("#button_container").removeClass("floating-button-container fixed-bottom");

        app.displayRect.width = app.stage.clientWidth;
        app.displayRect.height = app.stage.clientHeight;
        app.stage.style.width = app.displayRect.width;
        app.stage.style.height = app.displayRect.height;
        app.sizeScreen(app.stage);
    },

    /**
     * Adjust screen size (switch between modes).
     */
    fschange: function() {
        if (this.inFullscreen) {
            app.smallscreen();
        }
    },

    /**
     * Expand to full screen.
     */
    fullscreen: function() {
        var landscape = window.matchMedia("(orientation: landscape)").matches;
        try {
            if (app.stage.requestFullscreen) {
                app.stage.requestFullscreen();
            } else if (app.stage.msRequestFullscreen) {
                app.stage.msRequestFullscreen();
            } else if (app.stage.mozRequestFullScreen) {
                app.stage.mozRequestFullScreen();
            } else {
                console.error("Fullscreen API is not supported by this browser");
            }
        } catch (e) {
            log.debug(e, "Fullscreen error: ");
        }
        // The stage.webkitRequestFullscreen() method was removed, due to very easily exiting of full screen in iOS,
        // along with browser messages asking if you are typing in fullscreen.

        app.inFullscreen = true;
        var buttonContainer = $("#button_container");

        var width = window.innerWidth;

        // The window.innerHeight returns an offset value on iOS devices in safari only
        // while in portrait mode for some reason.
        var height = $(window).height();

        // Switch width and height
        if (landscape && width < height) {
            height = [width, width = height][0];
        }

        // Gets the actual button container height, then adds 16px; 8px on the
        // top and 8px on the bottom for the page margin.
        height -= buttonContainer.height() + 16;

        app.displayRect.width = width;
        app.displayRect.height = height;

        app.stage.style.width = width + "px";
        app.stage.style.height = height + "px";

        // Makes the canvas float.
        app.stage.classList.add("floating-game-canvas");

        // This makes the button container float below the game canvas.
        buttonContainer.addClass("floating-button-container fixed-bottom");

        $("#mod_minilesson_spacegame_fullscreen_button").blur(); // The button pressed was still focused, so a blur is necessary.

        app.sizeScreen(app.stage);
    },

    /**
     * Adjust screen size based on browser window.
     * @param {object} stage
     */
    sizeScreen: function(stage) {

        stage.width = app.displayRect.width;
        stage.height = app.displayRect.height;
        app.context.imageSmoothingEnabled = false;
    },

    /**
     * Helper function for when the screen size chages due to rotating on mobile.
     */
    orientationChange: function() {
        if (app.inFullscreen) {
            app.fullscreen();
        } else {
            app.smallscreen();
        }
    },

    /**
     * Helper function to clear all events.
     */
    clearEvents: function() {
        document.onkeydown = null;
        document.onkeyup = null;
        document.onmousedown = null;
        document.onmouseup = null;
        document.onmousemove = null;
        document.ontouchstart = null;
        document.ontouchend = null;
        document.ontouchmove = null;
        window.onresize = null;
    },

    storeResult: function(termid, points){
        var theterm = false;
        app.terms.forEach(function(aterm){
            if(aterm.id == termid){
                theterm = aterm
            }
        });
        //if we found the term, store the result
        if (theterm) {
            var result = {
                question: app.strip_html(theterm['definition']),
                selected: '',
                correct: theterm['term'],
                points: points,
                id: theterm.id
            };
            for (var i = 0; i < app.results.length; i++) {
                if (app.results[i].id == theterm.id) {
                    //if we have a duplicate and the earlier one is a fail, but this is a pass, replace it
                    if (points > 0 && app.results[i].points == 0) {
                        app.results.splice(i, 1);
                    //otherwise we just keep the previous result
                    } else {
                        return;
                    }
                }
            }
            //storing result
            app.results.push(result);
        }
    },

    reportSuccess: function(termid) {
        if (app.dryRun) {
            return;
        }
        //In wordcards we report the success of the association, but not here
        /*
        Ajax.call([{
            methodname: 'mod_minilesson_report_successful_association',
            args: {
                termid: termid,
                isfreemode: app.isFreeMode
            }
        }]);
        */
    },

    reportFailure: function(term1id, term2id) {
        if (app.dryRun) {
            return;
        }
        //In wordcards we report the success of the association, but not here
        /*
        Ajax.call([{
            methodname: 'mod_minilesson_report_failed_association',
            args: {
                term1id: term1id,
                term2id: term2id,
                isfreemode: app.isFreeMode
            }
        }]);
        */
    },

    /**
     * Helper function to handle JS Events.
     */
    menuEvents: function() {
        app.clearEvents();
        document.onkeydown = app.menukeydown;
        document.onmouseup = app.menumousedown;
        document.ontouchend = app.menutouchend;
        window.onresize = app.orientationChange;
    },

    /**
     * Helper function to load game objects
     */
    loadGame: function() {
        // We already do this elsewhere.
        app.shuffle(app.questions);

        if (!app.loaded) {
            app.images.forEach(function (src) {
                var absolute_src = M.cfg.wwwroot +'/mod/minilesson/' + src;

                var image = new Image();
                image.src = absolute_src;
                image.onload = function () {
                    app.loadedImages[src] = image;
                    app.imagesLoaded++;
                    console.log(app.imagesLoaded);
                    if (app.imagesLoaded >= app.images.length) {
                        app.gameLoaded();
                    }
                };
            });
            app.loaded = true;
        } else {
            app.startGame();
        }
    },

    /**
     * Helper function process game-over.
     */
    endGame: function() {

        // Clear full screen.
        if (app.inFullscreen) {
            log.debug("quitting full screen");
            app.inFullscreen = false;
            app.smallscreen();
        }

        clearInterval(app.timer.interval);
        $("#minilesson-gameboard, #minilesson-start-button").hide();
        $("#minilesson-results").show();

        // Template data.
        var tdata = [];
        tdata['allowretry'] = app.allowretry;
        tdata['nexturl'] = app.nexturl;
        tdata['results'] = app.results;
        tdata['total'] = app.questions.length;
        var totalcorrect = app.calc_total_points(app.results);
        //remove any decimal (leave one decimal place)
        tdata['totalcorrect'] = Math.round(totalcorrect * 10) / 10;
        tdata['gamescore'] = Math.round(app.score);
        var total_time = app.timer.count;
         if (total_time == 0) {
             tdata['prettytime'] = '00:00';
         } else {
             tdata['prettytime'] = app.pretty_print_secs(total_time);
         }

        templates.render('mod_minilesson/feedback', tdata).then(
            function(html, js) {
                $("#results-inner").html(html);
            }
        );

        var data = {
            results: app.results,
            activity: "spacegame"
        };
/*
        if (!app.isFreeMode) {
            Ajax.call([{
                methodname: 'mod_minilesson_report_step_grade',
                args: {
                    modid: app.modid,
                    correct: tdata['totalcorrect']
                }
            }]);
        }
            */

        // ajax.call([{
        //     methodname: 'mod_wordcards_update_score',
        //     args: {quizgameid: quizgame, score: Math.trunc(score)},
        //     fail: notification.exception
        // }]);

        // We use wordcards end screen so we dont do menuevents.
        // We just clear events so that the game doesnt restart and kill any ships left on stage
        //this.menuEvents();
        this.killAllAlive();
        this.clearEvents();
    },

    /**
     * Helper function process game ready.
     */
    gameLoaded: function() {

        clearInterval(app.interval);

        app.interval = setInterval(function () {
            app.draw(app.context, app.displayRect, app.gameObjects, app.particles, app.question);
            app.update(app.displayRect, app.gameObjects, app.particles);
        }, 40);

        app.startGame();
    },

    /**
     * Helper function process game start.
     */
    startGame: function() {
        app.score = 0;
        app.gameObjects = [];
        app.particles = [];
        app.level = -1;
        app.enemySpeed = 0.5;
        app.touchDown = false;
        app.mouseDown = false;
        app.results=[];

        // Queue & trigger the game_started event.
        /*
                ajax.call([{
                    methodname: 'mod_wordcards_start_game',
                    args: {quizgameid: quizgame},
                    fail: notification.exception
                }]);
        */
        app.player = new Player("pix/spacegame/ship-poodll-" + app.shipsize + ".png", 0, 0);
        app.player.x = app.displayRect.width / 2;
        app.player.y = app.displayRect.height / 2;
        app.gameObjects.push(app.player);

        app.planet = new Planet("pix/spacegame/planet.png", 0, 0);
        app.planet.image.width = app.displayRect.width;
        app.planet.image.height = app.displayRect.height;
        app.planet.direction.y = 1;
        app.planet.movespeed.y = 0.7;
        app.particles.push(app.planet);
        // Set up the timer.
        app.timer = {
            interval: setInterval(function() {
                app.timer.update();
            }, 1000),
            count: 0,
            update: function() {
                app.timer.count++;
                $("#minilesson-time-counter").text(app.timer.count);
            }
        }

        app.nextLevel();

        app.registerSpaceGameEvents();
    },

    /**
     * Helper function process next level (question).
     */
    nextLevel: function() {
        app.level++;

        // If we've run out of questions
        if (app.level >= app.questions.length) {
            //kill the player and end the game
            app.player.die();

            //previously we raised the speed, but we don't want to do that anymore
            //we might go back on it though.. so its still here
            /*
            app.level = 0;
            app.enemySpeed *= 1.3;
            app.question = app.runLevel(app.questions, app.level, app.displayRect);
            */

        } else {
            app.question = app.runLevel(app.questions, app.level, app.displayRect);
        }
    },

    /**
     * Helper function process current level.
     * @param {array} questions
     * @param {object} level
     * @param {object} bounds
     * @returns {string}
     */
    runLevel: function(questions, level, bounds) {
        app.currentTeam = [];
        app.lastShot = 0;
        app.currentPointsLeft = 0;

        switch(questions[level].type){
            case 'matching':
                var i = 0;
                //so in the case of 3 pairs, we have 6 items, so each item is worth 1/6, one pair will require two shots = 2/6
                var itempoints = 1 / (questions[level].stems.length * 2);
                app.currentPointsLeft += 1;
                questions[level].stems.forEach(function (stem) {
                    i++;
                    var question = new MatchEnemy(Math.random() * bounds.width, -Math.random() * bounds.height / 2,
                        stem.question, itempoints, -i, true,stem.termid);
                    var answer = new MatchEnemy(Math.random() * bounds.width, -Math.random() * bounds.height / 2,
                        stem.answer, itempoints, i,false,stem.termid);
                    app.currentTeam.push(question);
                    app.currentTeam.push(answer);
                    app.gameObjects.push(question);
                    app.gameObjects.push(answer);
                });
                break;

            case 'multichoice':
                questions[level].answers.forEach(function(answer) {
                    var enemy = new MultiEnemy(Math.random() * bounds.width, -Math.random() * bounds.height / 2,
                        answer.text, answer.itempoints, questions[level].single,  questions[level].termid);

                    if (answer.itempoints < 1) {
                        app.currentTeam.push(enemy);
                        if (answer.itempoints > 0) {
                            app.currentPointsLeft += answer.itempoints;
                        }
                    }
                    app.gameObjects.push(enemy);
                });
        }
        return questions[level].question;
    },

    /**
     * Helper function to place text on screen
     * @param {object} context
     * @param {object} displayRect
     * @param {objectc} objects
     * @param {object} particles
     * @param {string} question
     */
    draw: function(context, displayRect, objects, particles, question) {
        context.clearRect(0, 0, displayRect.width, displayRect.height);
        //draw the background
        app.drawSpaceBackground(context,displayRect);

        //draw particles
        var i = 0;
        for (i = 0; i < particles.length; i++) {
            particles[i].draw(context);
        }

        //draw objects
        for (i = 0; i < objects.length; i++) {
            objects[i].draw(context);
        }

        if (app.player.alive) {
            context.fillStyle = '#FFFFFF';
            context.font = "18px Audiowide";
            context.textAlign = 'left';
            context.fillText(app.strings.score + ' ' + Math.round(app.score) +  '  ' + app.strings.lives + ' ' +  app.player.lives,
                5, displayRect.height - 20);
            context.textAlign = 'center';

            app.wrapText(context, question, false, 20, displayRect.width * 0.9, displayRect.width / 2, 20);
        } else {
            context.fillStyle = '#FFFFFF';
            context.font = "18px Audiowide";
            context.textAlign = 'center';
            context.fillText(app.strings.endofgame + ' ' + Math.round(app.player.lastScore),
                displayRect.width / 2, displayRect.height / 2);
        }
    },

    /**
     * Helper function main game logic: process movements and behaviours of game objects
     * @param {object} bounds
     * @param {object} objects
     * @param {object} particles
     */
    update: function(bounds, objects, particles) {
        var i = 0;
        for (i = 0; i < 3; i++) {
            particles.push(new Star(bounds));
        }
        for (i = 0; i < particles.length; i++) {
            particles[i].update(bounds);
            if (!particles[i].alive) {
                particles.splice(i, 1);
                i--;
            }
        }
        for (i = 0; i < objects.length; i++) {
            objects[i].update(bounds);
            for (var j = i + 1; j < objects.length; j++) {
                app.collide(objects[i], objects[j]);
            }
            if (!objects[i].alive) {
                objects.splice(i, 1);
                i--;
            }
        }
    },

    /**
     * Helper function to remove any stray ships on level advance
     */
    killAllAlive: function() {
        app.currentTeam.forEach(function (enemy) {
            if (enemy.alive) {
                // Make the itempoints 0 so it won't count as anything and make a new level.
                enemy.itempoints = 0;
                enemy.die();
            }
        });
        app.currentTeam = [];
    },

    /**
     * Helper function to handle collisions between gameobjects.
     * @param {object} object1
     * @param {object} object2
     * @return {boolean}
     */
    collide: function(object1, object2) {
        return object1.alive && object2.alive && (this.collideOrdered(object1, object2) || this.collideOrdered(object2, object1));
    },

    /**
     * Helper funcction to handle collisions.
     * @param {object} object1
     * @param {object} object2
     * @returns {boolean}
     */
    collideOrdered: function(object1, object2) {
        if (object1 instanceof Laser && object2 instanceof Player) {
            if (!object1.friendly && app.objectsIntersect(object1, object2)) {
                object2.gotShot(object1);
                object1.die();
                return true;
            }
        }
        if (object1 instanceof Laser && object2 instanceof Enemy) {
            if (object1.friendly && app.objectsIntersect(object1, object2)) {
                object2.gotShot(object1);
                return true;
            }
        }
        if (object1 instanceof Player && object2 instanceof Enemy) {
            if (app.objectsIntersect(object1, object2)) {
                object1.die();
                return true;
            }
        }
        return false;
    },

    /**
     * Helper function to handle intersections between GameObjects.
     * @param {object} object1
     * @param {object} object2
     * @return {boolean}
     */
    objectsIntersect: function(object1, object2) {
        var rect1 = object1.getRect();
        var rect2 = object2.getRect();
        return rect1.Intersect(rect2);
    },

    /**
     * Helper function for spraying particle (explosion) effects
     * @param {int} x
     * @param {int} y
     * @param {int} num
     * @param {string} colour
     */
    spray: function(x, y, num, colour) {
        for (var i = 0; i < num; i++) {
            app.particles.push(new Particle(x, y, {
                x: (Math.random() - 0.5) * 16,
                y: ((Math.random() - 0.5) * 16) + 3
            }, colour));
        }
    },

    /**
     * Helper function to display answers.
     * @param {object} context
     * @param {string} input
     * @param {bool} wrapUpwards
     * @param {int} textHeight
     * @param {int} maxLineWidth
     * @param {int} x
     * @param {int} y
     */
    wrapText: function(context, input, wrapUpwards, textHeight, maxLineWidth, x, y) {
        var drawLines = [];
        var originalY = y;
        var words = input.split(' ');
        var line = '';
        var maxTextWidth = 0;
        // Loops through the words, and preprocesses each line with the correct string value and y location.
        words.forEach(function (word) {
            var tempLine ='';
            if(line===''){
                tempLine = line + ' ' + word;
            }else{
                tempLine = line + ' ' + word;
            }

            var metrics = context.measureText(tempLine);
            var textWidth = metrics.width;
            maxTextWidth = Math.max(maxTextWidth, textWidth);

            // If the line with the new word is too long, then push the current line without the new word to drawLines.
            if (textWidth > maxLineWidth) {
                drawLines.push({
                    text: line,
                    y: y += textHeight
                });

                line = word;
            } else {
                // If it's shorter than the limit, just add the word to the line and move on.
                line = tempLine;
            }
        });

        // Push the last line, if it exists.
        drawLines.push({
            text: line,
            y: y += textHeight
        });

        // The offset the text was created.
        var yOffset = y - originalY;

        //box mods
        var boxPadding = 20;
        var boxmodifier = wrapUpwards ? -(yOffset + textHeight + boxPadding / 2) : -(textHeight + boxPadding / 2);

        var borderColor = wrapUpwards ?'transparent':'white';
        app.drawRoundRect(context, borderColor,maxTextWidth + boxPadding, textHeight * drawLines.length + boxPadding, x - (maxTextWidth + boxPadding) / 2, y + boxmodifier);

        context.fillStyle="white";
        drawLines.forEach(function (drawLine) {
            // If it is suppose to wrap upwards (i.e. for enemy ships) it shifts all questions upwards the amount the
            // questions go down.
            var modifier = wrapUpwards ? -yOffset : 0;

            context.fillText(drawLine.text, x, drawLine.y + modifier);
        });
    },

    drawRoundRect: function(context, borderColor, width, height , x , y) {
        var cornerRadius = 4;
        var borderWidth = 4;

        var fillColor = 'rgba(75, 71, 77, 0.5)';
        //var borderColor = 'red';
        if(borderColor !== 'white') {
                borderColor = fillColor;
        }

        // Draw the box with border
        context.fillStyle = fillColor; // Box background color
        context.fillRect(x,y, width, height); // Draw the box
        context.strokeStyle = borderColor; // Border color
        context.lineWidth = borderWidth; // Border width
        context.strokeRect(x - borderWidth / 2, y - borderWidth  / 2, width + borderWidth, height + borderWidth); // Draw the border
    },

    drawSpaceBackground: function(context,displayRect) {

        // Set the source of the image (URL of the background image)
        var img = app.loadedImages['pix/spacegame/space-bckg.png'];

        // Calculate the number of tiles needed to cover the canvas
        var tilesX = Math.ceil(displayRect.width / img.width);
        var tilesY = Math.ceil(displayRect.height / img.height);

        // Loop through and draw the image tiles to cover the canvas
        for (var i = 0; i < tilesX; i++) {
            for (var j = 0; j < tilesY; j++) {
                context.drawImage(img, i * img.width, j * img.height, img.width, img.height);
            }
        }
    },

    /**
     * Helper function for end of level.
     */
    shipReachedEnd: function() {

        var amountLeft = app.currentTeam.filter(function (enemy) {
            return enemy.alive;
        }).length;

        if (amountLeft === 0
            // && (app.currentPointsLeft < app.itempoints || app.currentPointsLeft <= 0)
            && app.player.alive) {
            app.nextLevel();
        }
    },

    /**
     * Helper function for game menu from keyboard.
     * @param {object} e
     */
    menukeydown: function(e) {
        if ([32, 37, 38, 39, 40].indexOf(e.keyCode) !== -1) {
            e.preventDefault();
            if (e.keyCode === 32) {
                app.loadGame();
            }
        }
    },

    /**
     * Helper function for game menu on mobile.
     * @param {object} e
     */
    menumousedown: function(e) {
        if (e.target === app.stage) {
            app.loadGame();
        }
    },

    /**
     * Helper function for game menu on mobile.
     * @param {object} e
     */
    menutouchend: function(e) {
        if (e.target === app.stage) {
            app.loadGame();
        }
    },

    /**
     * Helper function for keyboard movement.
     * @param {object} e
     */
    keydown: function(e) {
        if ([32, 37, 38, 39, 40].indexOf(e.keyCode) !== -1) {
            e.preventDefault();
            if (e.keyCode === 32 && app.player.alive && app.canShoot) {
                app.player.Shoot();
            } else if (e.keyCode === 37) {
                app.player.direction.x = -1;
            } else if (e.keyCode === 38) {
                app.player.direction.y = -1;
            } else if (e.keyCode === 39) {
                app.player.direction.x = 1;
            } else if (e.keyCode === 40) {
                app.player.direction.y = 1;
            }
        }
    },

    /**
     * Helper function for keyboard movement.
     * @param {object} e
     */
    keyup: function(e) {
        if (e.keyCode === 32) {
            app.canShoot = true;
        } else if ([37, 39].indexOf(e.keyCode) !== -1) {
            app.player.direction.x = 0;
        } else if ([38, 40].indexOf(e.keyCode) !== -1) {
            app.player.direction.y = 0;
        }
    },

    /**
     * Helper function for mouse click (starts the player shooting if you clicked on them, moving if you didn't).
     * @param {object} e
     */
    mousedown: function(e) {
        if (e.target === app.stage) {
            var playerWasClicked = app.player.getRect().Contains({x: e.offsetX, y: e.offsetY});
            if (playerWasClicked && app.player.alive) {
                app.player.Shoot();
            }
            if (!app.mouseDown) {
                app.player.mouse.x = e.offsetX;
                app.player.mouse.y = e.offsetY;
                app.mouseDown = true;
            }
        }
    },

    /**
     * Helper function for mouse release. (stops the Player) for mouse mode
     */
    mouseup: function() {
        app.player.direction.x = 0;
        app.player.direction.y = 0;
        app.mouseDown = false;
    },

    /**
     * Helper function for mouse movement.
     * @param {object} e
     */
    mousemove: function(e) {
        app.player.mouse.x = e.offsetX;
        app.player.mouse.y = e.offsetY;
    },

    /**
     * Helper function for cancelled event.
     * @param {object} event
     */
    cancelled: function(event) {
        if (event.target === app.stage) {
            event.preventDefault();
        }
    },

    /**
     * Helper function for movement on mobile touch devices.
     * @param {object} e
     */
    touchstart: function(e) {
        if (e.target === app.stage) {
            if (app.player.alive && e.touches.length > 1) {
                app.player.Shoot();
            } else {
                app.touchDown = true;
                app.touchmove(e);
            }

            e.preventDefault();
        }
    },

    /**
     * Helper function for movement on mobile touch devices.
     * @param {object} e
     */
    touchend: function(e) {
        if (e.touches.length === 0) {
            app.touchDown = false;
        }
        app.player.direction.x = 0;
        app.player.direction.y = 0;

        if (e.target === app.stage) {
            e.preventDefault();
        }
    },

    /**
     * Helper function for movement on mobile touch devices.
     * @param {object} e
     */
    touchmove: function(e) {
        var rect = e.target.getBoundingClientRect();
        // Required for getting the stage's relative touch position, due to a previous significant offset.
        var x = e.touches[0].pageX - rect.left;
        var y = e.touches[0].clientY - rect.top;

        window.stage = app.stage;
        app.player.mouse.x = x;
        app.player.mouse.y = y - (2 * app.player.image.height);

        if (e.target === app.stage) {
            e.preventDefault();
        }
    },

    /**
     * Helper function to shuffle levels.
     * @param {array} array
     * @return {array}
     */
    shuffle: function(array) {
        var currentIndex = array.length;
        var temporaryValue;
        var randomIndex;

        while (0 !== currentIndex) {

            randomIndex = Math.floor(Math.random() * currentIndex);
            currentIndex -= 1;

            temporaryValue = array[currentIndex];
            array[currentIndex] = array[randomIndex];
            array[randomIndex] = temporaryValue;
        }

        return array;
    },

    strip_html: function(htmlstring) {
        return htmlstring.replace(/<\/?[^>]+(>|$)/g, "");
    },

    clone: function () {
        return $.extend(true, {}, this);
    },

    split_array: function(array, chunkSize) {
        if(chunkSize == null){ chunkSize = 4};
        var result = [];
        var currentIndex = 0;

        while (currentIndex < array.length) {
            const chunk = array.slice(currentIndex, currentIndex + chunkSize);
            if (chunk.length < chunkSize) {
                const remaining = chunkSize - chunk.length;
                // Reuse elements from previous chunks
                for (var i = 0; i < remaining; i++) {
                    chunk.push(array[i]);
                }
            }
            result.push(chunk);
            currentIndex += chunkSize;
        }

        return result;
    },

    shuffle_array: function(array) {
        for (let i = array.length - 1; i > 0; i--) {
            const j = Math.floor(Math.random() * (i + 1));
            [array[i], array[j]] = [array[j], array[i]];
        }
    },

    calc_total_points: function(results) {
        var total = 0;
        $.each(results, function(i, o) {
          if (o.points != undefined) {
            total += o.points;
          }
        });
        return total;
      },

      pretty_print_secs: function(time) {
        var minutes = Math.floor(time / 60);
        var seconds = time - minutes * 60;
        return app.str_pad_left(minutes, '0', 2) + ':' + app.str_pad_left(seconds, '0', 2);
      },

      str_pad_left: function(string, pad, length) {
        return (new Array(length + 1).join(pad) + string).slice(-length);
      },

      init_strings: function(){
       
        str.get_strings([
            { "key": "shootthepairs", "component": 'mod_minilesson'},
            { "key": "emptyquiz", "component": 'mod_minilesson'},
            { "key": "score", "component": 'mod_minilesson'},
            { "key": "lives", "component": 'mod_minilesson'},
            { "key": "spacetostart", "component": 'mod_minilesson'},

        ]).done(function (s) {
            var i = 0;
            app.strings.shootthepairs = s[i++];
            app.strings.emptyquiz = s[i++];
            app.strings.score = s[i++];
            app.strings.lives = s[i++];
            app.strings.spacetostart = s[i++];
        });
    },  

    reset: function() {
        clearInterval(app.interval);
        clearInterval(app.timer.interval);
        //clear event handlers (they will be added again in start)
        // The full screen toggle listeners
        if (document.removeEventListener) {
            document.removeEventListener('fullscreenchange', app.fschange, false);
            document.removeEventListener('MSFullscreenChange', app.fschange, false);
            document.removeEventListener('mozfullscreenchange', app.fschange, false);
            document.removeEventListener('webkitfullscreenchange', app.fschange, false);
        }
        // Full screen toggle button handler.
        $('#mod_minilesson_spacegame_fullscreen_button').off('click');


        $("#results-inner").html('');
        app.questions = [];
        app.terms = [];
        app.results = [];
        app.score = 0;
        app.gameObjects = [];
        app.particles = [];
        app.player = null;
        app.planet = null;
        app.loaded = false;
        app.process();
    },

    process: function() {
        var terms = app.itemdata.spacegameitems.map(function(item,index) {
            var theitem = JSON.parse(item);
            theitem.id = index;
            return theitem;
        });
        var multichoice_alien_chunksize = app.itemdata.aliencountmultichoice;
        var matching_alien_chunksize = app.itemdata.aliencountmatching;
        var include_matching_questions = app.itemdata.includematching;
        app.terms = terms;
        app.allowretry = app.itemdata.allowretry;
        app.shuffle_array(terms);

        // Multichoice questions.
        // Split into groups(ie "levels") of 5 terms (a single of mc of more than 5 terms is too much).
        var chunkSize = multichoice_alien_chunksize;
        if(terms.length < chunkSize) {chunkSize = terms.length;}
        var mc_levels = app.split_array(terms, chunkSize);
        // For each level build a set of chunksize questions with 1 correct and chunksize -1  distractors.
        for (var thelevel = 0; thelevel < mc_levels.length; thelevel++) {
            var level = mc_levels[thelevel];
            // Multiple choice questions.
            for (var i = 0; i < level.length; i++) {
                var answers = [];
                for (var j = 0; j < level.length; j++) {
                    var answertext = app.strip_html(level[j].definition);

                    if(app.sgoptions === app.termAsAlien){
                        answertext = level[j].term;
                    }
                    answers.push({"text": answertext,"itempoints": (i === j ? 1 : 0)});
                }
                var questiontext = level[i].term;
                if(app.sgoptions === app.termAsAlien){
                    questiontext = app.strip_html(level[i].definition);
                }
                app.questions.push({
                    "question": questiontext,
                    "termid": level[i].id,
                    "answers": answers,
                    "type": "multichoice",
                    "single": true
                });
            }
        }

        // Matching questions.
        if(include_matching_questions == 1){
            // Split into groups(ie "levels") of 3 terms (even 4 terms = 8 items to shoot, its quite hard).
            chunkSize=matching_alien_chunksize;
            if(terms.length < chunkSize) {chunkSize = terms.length;}
            var matching_levels = app.split_array(terms, chunkSize);
            for (var thelevel = 0; thelevel < matching_levels.length; thelevel++) {
                var level = matching_levels[thelevel];
                var subquestions = [];
                for (var i = 0; i < level.length; i++) {
                    subquestions.push({question: level[i].term, answer: app.strip_html(level[i].definition),"termid": level[i].id});
                }
                // Show a --- in place of a real question, so the user knows its a matching question.
                app.questions.push({"question": app.strings.shootthepairs, "stems": subquestions, "type": "matching"});
            }
        }
        console.log("After process:", app.questions);
    },

    init_controls: function() {
        app.controls.gameboard = $("#minilesson-gameboard");
        app.controls.time_counter = $("#minilesson-time-counter");
    },

    /**
     * Helper function to display game start screen
     */
    showMenu: function() {

        app.context.clearRect(0, 0, app.displayRect.width, app.displayRect.height);

        app.context.fillStyle = '#FFFFFF';
        app.context.font = "18px Audiowide";
        app.context.textAlign = 'center';

        if (app.questions !== null && app.questions.length > 0) {
            app.context.fillText(app.strings.spacetostart, app.displayRect.width / 2, app.displayRect.height / 2);
            app.menuEvents();
        } else {
            app.context.fillText(app.strings.emptyquiz, app.displayRect.width / 2, app.displayRect.height / 2);
        }
        // If we are height 0 then we are in a collapsed state
        // We call our orientation change function to fix this
        if(app.stage.height == 0){
            app.orientationChange();
        }
    },

    start: function() {
        // Hide the intro screen with word cards.
        app.results = [];
        app.controls.gameboard.show();
        app.controls.time_counter.text("00:00");

        app.timer = {
            interval: setInterval(function() {
                app.timer.update();
            }, 1000),
            count: 0,
            update: function() {
                app.timer.count++;
               // app.controls.time_counter.text('zerozero');
            }
        }

        // Begin the game screen.
        if (document.addEventListener) {
            document.addEventListener('fullscreenchange', app.fschange, false);
            document.addEventListener('MSFullscreenChange', app.fschange, false);
            document.addEventListener('mozfullscreenchange', app.fschange, false);
            document.addEventListener('webkitfullscreenchange', app.fschange, false);
        }
        app.stage = document.getElementById("mod_minilesson_spacegame");
        app.context = app.stage.getContext("2d");
        app.smallscreen();
        app.interval = setInterval(function () {
            if (app.questions && app.questions.length > 0) {
                app.showMenu();
            } else {
                console.log("Questions are not ready or empty.");
            }
        }, 500);

        // Full screen toggle button handler.
        $('#mod_minilesson_spacegame_fullscreen_button').on('click', function () {
            if (app.inFullscreen) {
                app.inFullscreen = false;
                app.smallscreen();
            } else {
                app.fullscreen();
            }
        });
    },

    next_question: function() {
        var self = this;
        var stepdata = {};
        stepdata.index = self.index;
        stepdata.hasgrade = true;
        stepdata.totalitems = app.questions.length;
        var totalcorrect = app.calc_total_points(app.results);
        //remove any decimal (leave one decimal place)
        stepdata.correctitems = Math.round(totalcorrect * 10) / 10;
        stepdata.grade = Math.round(100 * (stepdata.correctitems / stepdata.totalitems));
        self.quizhelper.do_next(stepdata);
    },

    register_events: function(index, itemdata, quizhelper) {
        app.index = index;
        app.quizhelper = quizhelper;
        var nextbutton = $("#" + itemdata.uniqueid + "_container .minilesson_nextbutton");
        var clickbutton = $("#" + itemdata.uniqueid + "_container .ml_sg_clickclick");

        nextbutton.on('click', function(e) {
            app.next_question(0);
        });

        clickbutton.on('click', function(e) {
            app.start();
        });

        $('body').on('click', "#minilesson-try-again", function() {
            //location.reload();
            app.reset();
            app.start();
        });

        $('body').on('click', "#minilesson-close-results", function() {
            var total_time = app.timer.count;
            var url = app.nexturl.replace(/&amp;/g, '&') + "&localscattertime=" + total_time
            window.location.replace(url);
        });
    },

    /**
     * Initialization of the game.
     * @param {array} props
     */
    init: function(index, itemdata, quizhelper) {
        log.debug('MiniLesson Space Game: init function');
        app.itemdata = itemdata;
        app.init_strings();
        app.register_events(index, itemdata, quizhelper);
        app.init_controls();
        app.process();
        app.start();
    },
}; // End of app declaration.

    return app;
});