Source: Stage.js

// @flow

const Timer = require('./Timer.js');
const Utils     = require('./Utils.js');

/**
 * A stage of an {@link App}.
 */
class Stage {

    /*
     * constructor - description
     *
     * @param  {type} name description
     * @param  {type} app  description
     * @return {type}      description
     */
    // constructor(name: string, app: App) {
    constructor(name, app) {

        /**
         * for display only, should be unique.
         * @type {Name}
         */
        this.name       = name;

        /**
         * The identifier of this stage.
         * @type {String}
         */
        this.id = this.name;

        /**
         * the app of this stage.
         * @type {App}
         */
        this.app        = app;

        /**
         * timeout duration in seconds
         * if <= 0, then no timeout for this stage.
         * @type number
         * @default 0
         */
        this.duration 	= 0;

        this.showTimer = true;

        /**
         * How long clients have before stage is auto-submitted (from client, not from server).
         * if <= 0, then no client timeout for this stage.
         * @type number
         * @default 0
         */
        this.clientDuration = 0;

        /**
         * Wait for all players in group to be 'ready' before calling [Stage.groupStart(group)]{@link stage#groupStart} and [Stage.playerStart(player)]{@link stage#groupStart} are called.
         * @type boolean
         * @default true
         */
        this.waitToStart = app.stageWaitToStart;

        /**
         * Wait for all players in group to be 'finished' or 'done' before calling [Stage.groupEnd(group)]{@link stage#groupEnd} and [Stage.playerStart(player)]{@link stage#groupEnd} are called.
         * @type boolean
         * @default true
         */
        this.waitToEnd = app.stageWaitToEnd;

        /**
         * when starting stage for a player, send 'player' object or not.
         * fields determined by [player.outputFields]{@link player#outputFields}.
         * @type boolean
         * @default true
         */
        this.onPlaySendPlayer = true;

        /**
         * how far up the tree should be sent on update. More data comes at cost of higher latency.
         * possible values: 'player', 'group'
         * @type string
         * @default 'player'
         */
        this.updateObject = 'player';

        /**
         * Wait for all players to finish stage on their own (true), or end anyway (false).
         * @type boolean
         * @default true
         */
        this.waitOnTimerEnd = true;

       /**
        * @type boolean
        * @default true
        */
        this.useAppActiveScreen = true;

        /**
         * @type boolean
         * @default true
         */
        this.useAppWaitingScreen = true;

        /**
         *  Wrap stage playing screens in a <form> tag or not.
         */
        this.wrapPlayingScreenInFormTag = app.stageWrapPlayingScreenInFormTag;

        /**
         * 'outputHide' fields are not included in output.
         * @type Array
         * @default []
         */
        this.outputHide = [];

        this.addOKButtonIfNone = true;

        this.waitingScreen = null;

        /**
         * 'outputHideAuto' fields are not included in output.
         * @type {String[]}
         */
        this.outputHideAuto = ['app', 'outputHide', 'outputHideAuto', 'html', 'htmlFile', 'useIdAsHTMLFileName'];

        /**
         * @default null
         */
        this.activeScreen = null;

        /**
         * @default null
         */
        this.html = null;

        /**
         * @default null
         */
        this.htmlFile = null;

        /**
         * @type boolean
         * @default true
         */
        this.useIdAsHTMLFileName = true;

        /**
         * Child stages. If length > 0, then ...
         * @type Array
         * @default []
         */
        this.stages = [];

        /**
         * The number of times to repeat child stages.
         * @type number
         * @default 1
         */
        this.repetitions = 1;

        this.autoplay = `
            jt.defaultAutoplay();
        `

    }

    /**
     * indexInApp - description
     *
     * @return {type}  description
     */
    indexInApp() {
        for (var i in this.app.stages) {
            if (this.app.stages[i] === this) {
                return parseInt(i);
            }
        }
        return -1;
    }

    /*
     * playerCanGroupProceedToNextStage - description
     *
     * @param  {type} player description
     * @return {type}        description
     */
    playerCanGroupProceedToNextStage(player) {
        // PROCEED GROUP
        var players = player.otherPlayersInGroup();
        for (var p in players) {
            var otherPlyr = players[p];
            if (!otherPlyr.atLeastFinishedStage(this, player.period())) {
                return false;
            }
        }
        return true;
    }

    canGroupStart(group) {
        return this.canGroupStartDefault(group);
    }

    canGroupStartDefault(group) {

        // If already started this stage, return false.
        if (group.stageStartedIndex >= this.indexInApp()) {
            return false;
        }

        // // If not finished a previous stage, return false.
        // if (group.stageEndedIndex < this.indexInApp() - 1) {
        //     return false;
        // }

        if (this.waitToStart) {
            // If any player is 1) not "ready" or 2) not in this stage, then return false.
            for (var p in group.players) {
                var player = group.players[p];
                if (player.stageIndex !== this.indexInApp() || player.status !== 'ready') {
                    return false;
                }
            }
        } 
        
        // Otherwise, return true.
        return true;
    }

    getClientDuration(player) {
        return this.clientDuration;
    };

    newStage(id) {
        var stage = new Stage.new(id, this.app);
        this.stages.push(stage);
        return stage;
    }

    canGroupEnd(group, forcePlayersToEnd) {

        var players = group.players;

        // If already finished, return false.
        if (group.stageEndedIndex >= this.indexInApp()) {
            return false;
        }

        if (this.waitToEnd && !forcePlayersToEnd) {
            // PROCEED ONLY IF ALL PLAYERS ARE "finished" and in this stage.
            for (var p in players) {
                var player = players[p];

                if (player.stageIndex > this.indexInApp()) {
                    // Player is already past this stage?!
                } else {
                    // If other player is not finished, return false.
                    if (player.stageIndex < this.indexInApp() || !player.isFinished()) {
                        return false;
                    }
                }
            }
        } 

        // Otherwise, return true.
        return true;

    }

    /**
     * Overwrite in app.jtt.
     *
     * @param  {type} group description
     * @return {type}       description
     */
    groupEnd(group) {}

    /**
    *  Called before the first player plays this stage.
    * Overwrite in app.jtt
    */
    groupStart(group) {}

    /**
     * Can the given player participate in this stage?
     *
     * @param  {type} player description
     * @return {type}        description
     */
    canPlayerParticipate(player) {
        return true;
    }

    canGroupParticipate(group) {
        return true;
    }

    getActiveScreen() {

        if (this.activeScreen == null) {
            this.activeScreen = Utils.getStageContents(this.app.id, this.id)
        }
        return this.activeScreen;
    }

    /**
     * End this stage for the given player.
     * Overwrite in app.jtt.
     *
     * @param  {type} player description
     */
    playerEnd(player) {}

    /**
     * Called when this player plays this stage.
     * Overwrite in app.jtt.
     **/
    playerStart(player) {}

    /**
     * session - description
     *
     * @return {type}  description
     */
    session() {
        return this.app.session;
    }

    outputFields() {
        var fields = [];
        for (var prop in this) {
            if (
                !Utils.isFunction(this[prop]) &&
                !this.outputHide.includes(prop) &&
                !this.outputHideAuto.includes(prop)
            )
            fields.push(prop);
        }
        return fields;
    }

    /**
     * shellWithParent - description
     *
     * @return {type}  description
     */
    shellWithParent() {
        var out = {};
        var fields = this.outputFields();
        for (var f in fields) {
            var field = fields[f];
            out[field] = this[field];
        }
        out['app.index'] = this.app.indexInSession();
        out.app = this.app.shell();
        return out;
    }

    /**
     * CALLED FROM:
     * - {@link Stage#save}
     * - {@link App#shellWithChildren}
     *
     * @return {type}  description
     */
    shell() {
        var out = {};
        var fields = this.outputFields();
        for (var f in fields) {
            var field = fields[f];
            out[field] = this[field];
        }
        out['app.index'] = this.app.indexInSession();
        out.canPlayerParticipate = this.canPlayerParticipate;

        out.groupStart = this.groupStart.toString();
        out.groupEnd = this.groupEnd.toString();
        out.playerStart = this.playerStart.toString();
        out.playerEnd = this.playerEnd.toString();
        out.getGroupDuration = this.getGroupDuration.toString();

        return out;
    }

    getGroupDuration(group) {
        return this.duration;
    }

    getOutputDir() {
        return this.app.getOutputFN() + '/stages/';
    }

    /**
     * CALLED FROM:
     * - {@link App#saveSelfAndChildren}
     *
     * @return {type}  description
     */
    save() {
        try {
            this.session().jt.log('Stage.save: ' + this.id);
            var toSave = this.shell();
    //        Utils.writeJSON(this.getOutputDir() + this.indexInApp() + '_' + this.id + '.json', toSave);
            this.session().saveDataFS(toSave, 'STAGE');
        } catch (err) {
            console.log('Error saving stage ' + this.id + ': ' + err);
        }
    }

}

var exports = module.exports = {};
exports.new = Stage;
exports.load = Stage.load;