Skip to content
Snippets Groups Projects
spniCore.js 46.8 KiB
Newer Older
  • Learn to ignore specific revisions
  • /********************************************************************************
    
     This file contains the variables and functions that forms the core of the game.
    
     Anything that is needed game-wide is kept here.
     ********************************************************************************/
    
    /**********************************************************************
     * Game Wide Constants
     **********************************************************************/
    
    /* General Constants */
    
    FarawayVision's avatar
    FarawayVision committed
    var DEBUG = false;
    
    var EPILOGUES_ENABLED = true;
    
    var EPILOGUES_UNLOCKED = false;
    
    var EPILOGUE_BADGES_ENABLED = true;
    
    var ALT_COSTUMES_ENABLED = false;
    
    var USAGE_TRACKING = false;
    
    var BASE_FONT_SIZE = 14;
    var BASE_SCREEN_WIDTH = 100;
    
    
    var USAGE_TRACKING_ENDPOINT = 'https://spnati.faraway-vision.io/usage/report';
    var BUG_REPORTING_ENDPOINT = 'https://spnati.faraway-vision.io/usage/bug_report';
    
    
    var VERSION_COMMIT = undefined;
    
    
    /* Game Wide Constants */
    var HUMAN_PLAYER = 0;
    
    /* Directory Constants */
    var IMG = 'img/';
    
    /*var OPP = 'opponents/';
    #The "OPP" folder abbreviation was used to slightly shorten a few lines in spniSelect that looked for opponents in the opponents folder.
    #Now that opponents can be specified in any folder, this is no longer required.*/
    
    
    /* Gender Images */
    var MALE_SYMBOL = IMG + 'male.png';
    var FEMALE_SYMBOL = IMG + 'female.png';
    
    
    
    
    /* game table */
    var tableOpacity = 1;
    $gameTable = $('#game-table');
    
    /* useful variables */
    var BLANK_PLAYER_IMAGE = "opponents/blank.png";
    
    /* player array */
    
    /* Current timeout ID, so we can cancel it when restarting the game in order to avoid trouble. */
    var timeoutID;
    
    
    /**********************************************************************
     * Game Wide Global Variables
     **********************************************************************/
    
    var table = new Table();
    
    var jsErrors = [];
    var sessionID = '';
    
    var gameID = '';
    
    
    /**********************************************************************
     * Screens & Modals
     **********************************************************************/
    
    /* Screens */
    
    Joseph Kantel's avatar
    Joseph Kantel committed
    $warningScreen = $('#warning-screen');
    
    $titleScreen = $('#title-screen');
    $selectScreen = $('#main-select-screen');
    $individualSelectScreen = $('#individual-select-screen');
    $groupSelectScreen = $('#group-select-screen');
    $gameScreen = $('#game-screen');
    $epilogueScreen = $('#epilogue-screen');
    
    var allScreens = [$warningScreen, $titleScreen, $selectScreen, $individualSelectScreen, $groupSelectScreen, $gameScreen, $epilogueScreen, $galleryScreen];
    
    
    /* Modals */
    $searchModal = $('#search-modal');
    
    $groupSearchModal = $('#group-search-modal');
    
    $creditModal = $('#credit-modal');
    $versionModal = $('#version-modal');
    $gameSettingsModal = $('#game-settings-modal');
    
    $bugReportModal = $('#bug-report-modal');
    $usageTrackingModal = $('#usage-reporting-modal');
    
    $playerTagsModal = $('#player-tags-modal');
    
    
    /* Screen State */
    $previousScreen = null;
    
    
    /* CSS rules for arrow offsets */
    var bubbleArrowOffsetRules;
    
    
    /********************************************************************************
     * Game Wide Utility Functions
     ********************************************************************************/
    
    
    function getReportedOrigin () {
        var origin = window.location.origin;
    
        if (origin.toLowerCase().startsWith('file:')) {
            return '<local filesystem origin>';
        } else {
            return origin;
        }
    }
    
    /* Gathers most of the generic information for an error report. */
    function compileBaseErrorReport(userDesc, bugType) {
        var tableReports = [];
        for (let i=1;i<players.length;i++) {
            if (players[i]) {
                playerData = {
                    'id': players[i].id,
                    'slot': i,
                    'stage': players[i].stage,
                    'timeInStage': players[i].timeInStage,
                    'markers': players[i].markers
                }
    
                if (players[i].chosenState) {
    
                    playerData.currentLine    = players[i].chosenState.rawDialogue;
    
                    playerData.currentImage   = players[i].folder + players[i].chosenState.image;
    
                tableReports[i-1] = playerData;
            } else {
                tableReports[i-1] = null;
            }
        }
    
        var circumstances = {
            'userAgent': navigator.userAgent,
            'origin': getReportedOrigin(),
            'currentRound': currentRound,
            'currentTurn': currentTurn,
    
            'previousLoser': previousLoser,
            'recentLoser': recentLoser,
            'gameOver': gameOver,
    
            'visibleScreens': [],
            'rollback': inRollback()
    
        if (gamePhase) {
    
            if (inRollback()) {
                circumstances.gamePhase = rolledBackGamePhase[0];
            } else {
                circumstances.gamePhase = gamePhase[0];
            }
    
        for (let i=0;i<allScreens.length;i++) {
            if (allScreens[i].css('display') !== 'none') {
                circumstances.visibleScreens.push(allScreens[i].attr('id'));
            }
        }
    
        var bugCharacter = null;
        if (bugType.startsWith('character')) {
    
            bugCharacter = bugType.split(':', 2)[1];
    
            bugType = 'character';
        }
    
        return {
            'date': (new Date()).toISOString(),
    
            'commit': VERSION_COMMIT,
    
            'session': sessionID,
    
            'game': gameID,
    
            'type': bugType,
            'character': bugCharacter,
    
            'description': userDesc,
    
            'circumstances': circumstances,
            'table': tableReports,
            'player': {
                'gender': players[HUMAN_PLAYER].gender,
                'size': players[HUMAN_PLAYER].size,
            },
            'jsErrors': jsErrors,
        };
    }
    
    window.addEventListener('error', function (ev) {
        jsErrors.push({
            'date': (new Date()).toISOString(),
            'type': ev.error.name,
            'message': ev.message,
            'filename': ev.filename,
            'lineno': ev.lineno,
            'stack': ev.error.stack
        });
    
        if (USAGE_TRACKING) {
            var report = compileBaseErrorReport('Automatically generated after Javascript error.', 'auto');
    
            $.ajax({
                url: BUG_REPORTING_ENDPOINT,
                method: 'POST',
                data: JSON.stringify(report),
                contentType: 'application/json',
                error: function (jqXHR, status, err) {
                    console.error("Could not send bug report - error "+status+": "+err);
                },
            });
        }
    });
    
    
    /* Fetch a possibly compressed file.
     * Attempts to fetch a compressed version of the file first,
     * then fetches the uncompressed version of the file if that isn't found.
     */
    function fetchCompressedURL(baseUrl, successCb, errorCb) {
        /*
         * The usual Jquery AJAX request function doesn't play nice with
         * the binary-encoded data we'll get here, so we do the XHR manually.
         * (I would use fetch() were it not for compatibility issues.)
         */
        var req = new XMLHttpRequest();
        req.open('GET', baseUrl+'.gz', true);
        req.responseType = 'arraybuffer';
    
        req.onload = function(ev) {
            if (req.status < 400 && req.response) {
                var data = new Uint8Array(req.response);
                var decompressed = pako.inflate(data, { to: 'string' });
                successCb(decompressed);
            } else if (req.status === 404) {
                $.ajax({
                    type: "GET",
            		url: baseUrl,
            		dataType: "text",
                    success: successCb,
                    error: errorCb,
                });
            } else {
                errorCb();
            }
        }
    
        req.onerror = function(err) {
            $.ajax({
                type: "GET",
                url: baseUrl,
                dataType: "text",
                success: successCb,
                error: errorCb,
            });
        }
    
    /**********************************************************************
     *****                Player Object Specification                 *****
     **********************************************************************/
    
     /************************************************************
      * Creates and returns a new player object based on the
      * supplied information.
      *
      * folder (string), the path to their folder
      * first (string), their first name.
      * last (string), their last name.
      * labels (string or XML element), what's shown on screen and what other players refer to them as.
      *   Can vary by stage.
      * size (string): Their level of endowment
      * intelligence (string or XML element), the name of their AI algorithm.
      *   Can vary by stage.
      * gender (constant), their gender.
      * clothing (array of Clothing objects), their clothing.
    
      * stamina (integer), time until forfeit is finished (initial timer value).
    
      * state (array of PlayerState objects), their sequential states.
      * xml (jQuery object), the player's loaded behaviour.xml file.
      * metaXml (jQuery object), the player's loaded meta.xml file.
      ************************************************************/
    
    function Player (id) {
        this.id = id;
        this.folder = 'opponents/'+id+'/';
    
        this.first = '';
        this.last = '';
    
        this.folders = undefined;
    
        this.size = eSize.MEDIUM;
        this.intelligence = eIntelligence.AVERAGE;
        this.gender = eGender.MALE;
    
        this.stamina = 20;
    
        this.scale = undefined;
    
        this.xml = null;
        this.metaXml = null;
    
        this.resetState();
    
    /*******************************************************************
     * Sets initial values of state variables used by targetStatus,
     * targetStartingLayers etc. adccording to wardrobe.
     *******************************************************************/
    Player.prototype.initClothingStatus = function () {
    	this.startingLayers = this.clothing.length;
    	this.exposed = { upper: true, lower: true };
    	for (var position in this.exposed) {
    		if (this.clothing.some(function(c) {
    			return (c.type == IMPORTANT_ARTICLE || c.type == MAJOR_ARTICLE)
    				&& (c.position == position || c.position == FULL_ARTICLE);
    		})) {
    			this.exposed[position] = false;
    		};
    	}
    	this.mostlyClothed = this.decent = !(this.exposed.upper || this.exposed.lower)
    		&& this.clothing.some(function(c) {
    			return c.type == MAJOR_ARTICLE
    				&& [UPPER_ARTICLE, LOWER_ARTICLE, FULL_ARTICLE].indexOf(c.position) >= 0;
    		});
    }
    
    
    /*******************************************************************
    
    FarawayVision's avatar
    FarawayVision committed
     * (Re)Initialize the player properties that change during a game
    
     *******************************************************************/
    
    Player.prototype.resetState = function () {
    
        this.out = this.finished = false;
    
    	this.forfeit = "";
    	this.stage = this.current = this.consecutiveLosses = 0;
    	this.timeInStage = -1;
    	this.markers = {};
    
    	if (this.xml !== null) {
    
            /* Load in the legacy "start" lines, and also
             * initialize player.chosenState to the first listed line.
             * This may be overridden by later updateBehaviour calls if
             * the player has (new-style) selected or game start case lines.
             */
    
    		var allStates = [];
    
            /* Initialize reaction handling state. */
            this.volatileMatches = [];
            this.bestVolatileMatch = null;
            this.currentTarget = null;
            this.currentPriority = -1;
            this.stateLockCount = 0;
    
            this.stateCommitted = false;
    
            this.xml.children('start').children('state').each(function () {
    
                allStates.push(new State($(this)));
            });
    
            this.allStates = allStates;
    
    		this.chosenState = this.allStates[0];
    
            if (!this.chosenState) {
                /* If the opponent does not have legacy start lines then select
                 * a new-style selected line immediately.
                 * Prevents a crash triggered by selected, unselecting, and re-selecting
                 * an opponent with no legacy starting lines.
                 */
                this.updateBehaviour(SELECTED);
            }
    
            this.commitBehaviourUpdate();
    
            var appearance = this.default_costume;
    
            if (ALT_COSTUMES_ENABLED && this.alt_costume) {
    
                appearance = this.alt_costume;
            }
    
            this.labels = appearance.labels;
            this.folders = appearance.folders;
    
            if (appearance.tags) {
    
                var alt_tags = appearance.tags.find('tag').each(function (idx, elem) {
                    var $elem = $(elem);
                    var tag = $elem.text();
                    var removed = $elem.attr('remove') || '';
                    if (removed.toLowerCase() === 'true') {
    
                        if (this.tags.indexOf(tag) >= 0) {
    
                            this.tags.splice(this.tags.indexOf(tag), 1);
                        }
    
                    } else if (this.tags.indexOf(tag) < 0) {
    
            if (appearance.id && this.tags.indexOf(appearance.id) < 0) {
    
                this.tags.push(appearance.id);
            }
    
    		/* Load the player's wardrobe. */
    
        	/* Find and grab the wardrobe tag */
    
        	$wardrobe = appearance.wardrobe;
    
        	/* find and create all of their clothing */
    
            var clothingArr = [];
    
        	$wardrobe.find('clothing').each(function () {
    
        		var formalName = $(this).attr('formalName');
        		var genericName = $(this).attr('genericName') || $(this).attr('lowercase');
    
        		var type = $(this).attr('type');
        		var position = $(this).attr('position');
        		var plural = ['true', 'yes'].indexOf($(this).attr('plural')) >= 0;
    
        		var newClothing = createNewClothing(formalName, genericName, type, position, null, plural, 0);
    
        		clothingArr.push(newClothing);
    
    FarawayVision's avatar
    FarawayVision committed
            
            this.poses = appearance.poses;
    
            this.clothing = clothingArr;
    
    		this.initClothingStatus();
    
    	this.updateLabel();
    
    	this.updateFolder();
    
    Player.prototype.getIntelligence = function () {
        return this.intelligence; // Opponent uses getByStage()
    };
    
    /* These shouldn't do anything for the human player, but exist as empty functions
       to make it easier to iterate over the entire players[] array. */
    Player.prototype.updateLabel = function () { }
    
    Player.prototype.updateFolder = function () { }
    
    Player.prototype.updateBehaviour = function() { }
    
    
    /*****************************************************************************
     * Subclass of Player for AI-controlled players.
     ****************************************************************************/
    
    function Opponent (id, $metaXml, status, releaseNumber) {
    
        this.id = id;
        this.folder = 'opponents/'+id+'/';
    
        this.metaXml = $metaXml;
    
        this.status = status;
        this.first = $metaXml.find('first').text();
        this.last = $metaXml.find('last').text();
        this.label = $metaXml.find('label').text();
        this.image = $metaXml.find('pic').text();
        this.gender = $metaXml.find('gender').text();
        this.height = $metaXml.find('height').text();
        this.source = $metaXml.find('from').text();
        this.artist = $metaXml.find('artist').text();
        this.writer = $metaXml.find('writer').text();
    
        this.description = fixupDialogue($metaXml.find('description').html());
    
        this.endings = $metaXml.find('epilogue');
        this.ending = this.endings.length > 0 || $metaXml.find('has_ending').text() === "true";
    
        this.layers = parseInt($metaXml.find('layers').text(), 10);
        this.scale = Number($metaXml.find('scale').text()) || 100.0;
        this.tags = $metaXml.find('tags').children().map(function() { return $(this).text(); }).get();
    
        this.release = parseInt(releaseNumber, 10) || Number.POSITIVE_INFINITY;
    
        this.selected_costume = null;
        this.alt_costume = null;
        this.default_costume = null;
    
    FarawayVision's avatar
    FarawayVision committed
        this.poses = {};
    
        /* Attempt to preload this opponent's picture for selection. */
        new Image().src = 'opponents/'+id+'/'+this.image;
    
        this.alternate_costumes = [];
        this.selection_image = this.folder + this.image;
        
        $metaXml.find('alternates').find('costume').each(function (i, elem) {
            var set = $(elem).attr('set') || 'offline';
            
            if (alternateCostumeSets['all'] || alternateCostumeSets[set]) {
                var costume_descriptor = {
                    'folder': $(elem).attr('folder'),
                    'label': $(elem).text(),
                    'image': $(elem).attr('img'),
                    'set': set
                };
                
                if (set === FORCE_ALT_COSTUME) {
                    this.selection_image = costume_descriptor['folder'] + costume_descriptor['image'];
    
                    this.selectAlternateCostume(costume_descriptor);
    
                }
                
                this.alternate_costumes.push(costume_descriptor);
            }
        }.bind(this)).get();
    
    }
    
    Opponent.prototype = Object.create(Player.prototype);
    Opponent.prototype.constructor = Opponent;
    
    
    Opponent.prototype.clone = function() {
    	var clone = Object.create(Opponent.prototype);
    	/* This should be deep enough for our purposes. */
    	for (var prop in this) {
    		if (this[prop] instanceof Array) {
    			clone[prop] = this[prop].slice();
    		} else {
    			clone[prop] = this[prop];
    		}
    	}
    	return clone;
    }
    
    
    Opponent.prototype.isLoaded = function() {
    	return this.xml != undefined;
    }
    
    
    Opponent.prototype.onSelected = function(individual) {
    
        this.preloadStageImages(-1);
    
        if (individual) {
            updateAllBehaviours(this.slot, SELECTED, [[OPPONENT_SELECTED]]);
        } else {
            this.updateBehaviour(SELECTED);
            this.commitBehaviourUpdate();
        }
    
    Opponent.prototype.updateLabel = function () {
    
        if (this.labels && !this.labelOverridden) this.label = this.getByStage(this.labels);
    
    Opponent.prototype.updateFolder = function () {
        if (this.folders) this.folder = this.getByStage(this.folders);
    }
    
    Opponent.prototype.getByStage = function (arr, stage) {
    
        if (stage === undefined) stage = this.stage;
    
        var bestFitStage = -1;
        var bestFit = null;
        for (var i = 0; i < arr.length; i++) {
            var startStage = arr[i].getAttribute('stage');
            startStage = parseInt(startStage, 10) || 0;
    
            if (startStage > bestFitStage && startStage <= stage) {
    
                bestFit = $(arr[i]).text();
                bestFitStage = startStage;
            }
        }
        return bestFit;
    };
    
    
    Opponent.prototype.selectAlternateCostume = function (costumeDesc) {
        if (costumeDesc === undefined) {
            this.selected_costume = null;
            this.selection_image = this.base_folder + this.image;
        } else {
            this.selected_costume = costumeDesc.folder;
            this.selection_image = costumeDesc.folder + costumeDesc.image;
        }
    
    Opponent.prototype.getIntelligence = function () {
        return this.getByStage(this.intelligence) || eIntelligence.AVERAGE;
    };
    
    
    Opponent.prototype.loadAlternateCostume = function (individual) {
    
        if (this.alt_costume) {
            if (this.alt_costume.folder != this.selected_costume) {
                this.unloadAlternateCostume();
            } else {
                return;
            }
        }
    
        console.log("Loading alternate costume: "+this.selected_costume);
    
        $.ajax({
            type: "GET",
    
            dataType: "text",
            success: function (xml) {
                var $xml = $(xml);
    
                this.alt_costume = {
    
                    labels: $xml.find('label'),
    
                    tags: $xml.find('tags'),
    
                    folder: this.selected_costume,
    
                    folders: $xml.find('folder'),
                    wardrobe: $xml.find('wardrobe')
                };
    
    FarawayVision's avatar
    FarawayVision committed
                
                var poses = $xml.find('poses');
                var poseDefs = {};
                $(poses).find('pose').each(function (i, elem) {
                    var def = new PoseDefinition($(elem), this);
                    poseDefs[def.id] = def;
                }.bind(this));
                
                this.alt_costume.poses = poseDefs;
    
            }.bind(this),
            error: function () {
    
                console.error("Failed to load alternate costume: "+this.selected_costume);
    
            },
        })
    }
    
    Opponent.prototype.unloadAlternateCostume = function () {
    
        if (!this.alt_costume) {
            return;
        }
    
        if (this.alt_costume.tags) {
    
            this.alt_costume.tags.find('tag').each(function (idx, elem) {
    
                var $elem = $(elem);
                var tag = $elem.text();
                var removed = $elem.attr('remove') || '';
                if (removed.toLowerCase() === 'true') {
                    this.tags.push(tag);    // tag was previously removed, readd it
                } else {
                    if (this.tags.indexOf(tag) > 0) {
                        // remove added tag
                        this.tags.splice(this.tags.indexOf(tag), 1);
                    }
                }
    
        this.tags.splice(this.tags.indexOf(this.alt_costume.id), 1);
    
        this.alt_costume = null;
    
        this.selectAlternateCostume(null);
    
    /************************************************************
    
     * Loads and parses the start of the behaviour XML file of the
    
     * given opponent.
     *
     * The onLoadFinished parameter must be a function capable of
     * receiving a new player object and a slot number.
     ************************************************************/
    
    Opponent.prototype.loadBehaviour = function (slot, individual) {
    
    ReformCopyright's avatar
    ReformCopyright committed
        this.slot = slot;
        if (this.isLoaded()) {
    
            if (this.selected_costume) {
                this.loadAlternateCostume();
            } else {
    
                this.unloadAlternateCostume();
    
    ReformCopyright's avatar
    ReformCopyright committed
            return;
        }
    
        fetchCompressedURL(
    		'opponents/' + this.id + "/behaviour.xml",
    
    		/* Success callback.
             * 'this' is bound to the Opponent object.
             */
    		function(xml) {
    
                var $xml = $(xml);
    
                this.xml = $xml;
                this.size = $xml.find('size').text();
    
                this.stamina = Number($xml.find('timer').text());
    
                this.intelligence = $xml.find('intelligence');
    
                /* The gender listed in meta.xml and behaviour.xml might differ
                 * (for example with gender-revealing characters)
                 * So assume behaviour.xml holds the 'definitive' starting gender
                 * for the character.
                 */
                var startGender = $xml.find('gender').text();
                if (startGender) {
                    this.gender = startGender;    
                }
    
    
                this.default_costume = {
    
                    labels: $xml.find('label'),
                    tags: null,
                    folders: this.folder,
                    wardrobe: $xml.find('wardrobe')
                };
    
                
                var poses = $xml.find('poses');
                var poseDefs = {};
                $(poses).find('pose').each(function (i, elem) {
    
    FarawayVision's avatar
    FarawayVision committed
                    var def = new PoseDefinition($(elem), this);
    
                    poseDefs[def.id] = def;
                }.bind(this));
                
    
    FarawayVision's avatar
    FarawayVision committed
                this.default_costume.poses = poseDefs;
    
                var tags = $xml.find('tags');
    
                var tagsArray = [this.id];
    
                if (typeof tags !== typeof undefined && tags !== false) {
                    $(tags).find('tag').each(function () {
    
                        tagsArray.push($(this).text());
    
                this.tags = tagsArray;
    
                var targetedLines = {};
    
                $xml.find('case[target]>state, case[alsoPlaying]>state').each(function() {
                    var $case = $(this.parentNode);
                    ['target', 'alsoPlaying'].forEach(function(attr) {
                        var id = $case.attr(attr);
                        if (id) {
                            if (!(id in targetedLines)) { targetedLines[id] = { count: 0, seen: {} }; }
                            if (!(this.textContent in targetedLines[id].seen)) {
                                targetedLines[id].seen[this.textContent] = true;
                                targetedLines[id].count++;
                            }
                        }
                    }, this);
                });
    
                this.targetedLines = targetedLines;
    
                if (this.selected_costume) {
                    return this.loadAlternateCostume();
    
    		}.bind(this),
    
    		/* Error callback. */
            function(err) {
    
                console.log("Failed reading \""+this.id+"\" behaviour.xml");
    
            }.bind(this)
    
    Player.prototype.getImagesForStage = function (stage) {
        if(!this.xml) return [];
    
        var imageSet = {};
    
        var folder = this.folders ? this.getByStage(this.folders, stage) : this.folder;
    
    FarawayVision's avatar
    FarawayVision committed
        var advPoses = this.poses;
    
        var selector = (stage == -1 ? 'start, stage[id=1] case[tag=game_start]'
                        : 'stage[id='+stage+'] case');
    
        this.xml.find(selector).each(function () {
    
            var target = $(this).attr('target'), alsoPlaying = $(this).attr('alsoPlaying'),
                filter = $(this).attr('filter');
            // Skip cases requiring a character that isn't present
            if ((target === undefined || players.some(function(p) { return p.id === target; }))
                && (alsoPlaying === undefined || players.some(function(p) { return p.id === alsoPlaying; }))
                && (filter === undefined || players.some(function(p) { return p.tags.indexOf(filter) >= 0; })))
            {
    
    FarawayVision's avatar
    FarawayVision committed
                $(this).children('state').each(function (i, e) {
                    var poseName = $(e).attr('img');
                    if (!poseName) return;
                    
                    if (poseName.startsWith('custom:')) {
                        var key = poseName.split(':', 2)[1];
                        var pose = advPoses[key];
                        if (pose) pose.getUsedImages().forEach(function (img) {
                            imageSet[img] = true;
                        });
                    } else {
                        imageSet[folder+poseName] = true;
                    }
                });
    
        return Object.keys(imageSet);
    };
    
    Player.prototype.preloadStageImages = function (stage) {
        this.getImagesForStage(stage)
            .forEach(function(fn) { new Image().src = fn; }, this );
    };
    
    /**********************************************************************
     *****              Overarching Game Flow Functions               *****
     **********************************************************************/
    
    /************************************************************
     * Loads the initial content of the game.
     ************************************************************/
    function initialSetup () {
        /* start by creating the human player object */
    
        var humanPlayer = new Player('human'); //createNewPlayer("human", "", "", "", eGender.MALE, eSize.MEDIUM, eIntelligence.AVERAGE, 20, undefined, [], null);
    
        players[HUMAN_PLAYER] = humanPlayer;
    
        players[HUMAN_PLAYER].slot = HUMAN_PLAYER;
    
    	/* enable table opacity */
    	tableOpacity = 1;
    	$gameTable.css({opacity:1});
    
        /* load the all content */
        loadTitleScreen();
        selectTitleCandy();
    
    	/* Make sure that the config file is loaded before processing the
    	   opponent list, so that includedOpponentStatuses is populated. */
        loadConfigFile().always(loadSelectScreen);
    
    	/* show the title screen */
    
    Joseph Kantel's avatar
    Joseph Kantel committed
    	$warningScreen.show();
    
        autoResizeFont();
    
        /* Generate a random session ID. */
    
        sessionID = generateRandomID();
    
    
        /* Construct a CSS rule for every combination of arrow direction, screen, and pseudo-element */
        bubbleArrowOffsetRules = [];
        for (var i = 1; i <= 4; i++) {
            var pair = [];
            [["up", "down"], ["left", "right"]].forEach(function(p) {
                var index = document.styleSheets[2].cssRules.length;
                var rule = p.map(function(d) {
                    return ["select", "game"].map(function(s) {
                        return ["before", "after"].map(function(r) {
                            return '#'+s+'-bubble-'+i+'>.dialogue-bubble.arrow-'+d+'::'+r;
                        }).join(', ');
                    }).join(', ');
                }).join(', ') + ' {}';
                document.styleSheets[2].insertRule(rule, index);
                pair.push(document.styleSheets[2].cssRules[index]);
            });
            bubbleArrowOffsetRules.push(pair);
        }
    
    function loadConfigFile () {
    
            type: "GET",
    		url: "config.xml",
    		dataType: "text",
    
    		success: function(xml) {
    
    			var _epilogues = $(xml).find('epilogues').text();
                if(_epilogues.toLowerCase() === 'false') {
                    EPILOGUES_ENABLED = false;
                    console.log("Epilogues are disabled.");
                    $("#title-gallery-edge").hide();
                } else {
                    console.log("Epilogues are enabled.");
                    EPILOGUES_ENABLED = true;
                }
    
                var _epilogues_unlocked = $(xml).find('epilogues-unlocked').text().trim();
    
                if (_epilogues_unlocked.toLowerCase() === 'true') {
    
                    EPILOGUES_UNLOCKED = true;
                    console.error('All epilogues unlocked in config file. You better be using this for development only and not cheating!');
    
                } else {
                    EPILOGUES_UNLOCKED = false;
    
                var _epilogue_badges = $(xml).find('epilogue_badges').text();
                if(_epilogue_badges.toLowerCase() === 'false') {
                    EPILOGUE_BADGES_ENABLED = false;
                    console.log("Epilogue badges are disabled.");
                } else {
                    console.log("Epilogue badges are enabled.");
                    EPILOGUE_BADGES_ENABLED = true;
                }
    
    			var _debug = $(xml).find('debug').text();
    
                if (_debug === "true") {
                    DEBUG = true;
                    console.log("Debugging is enabled");
                }
                else {
                    DEBUG = false;
                    console.log("Debugging is disabled");
                }
    
                var _game_commit = $(xml).find('commit').text();
                if (_game_commit) {
                    VERSION_COMMIT = _game_commit;
                    console.log("Running SPNATI commit "+VERSION_COMMIT+'.');
                } else {
                    console.log("Could not find currently deployed Git commit!");
                }
    
                var _alts = $(xml).find('alternate-costumes').text();
    
                if(_alts === "true") {
                    ALT_COSTUMES_ENABLED = true;
                    console.log("Alternate costumes enabled");
    
                    
                    FORCE_ALT_COSTUME = $(xml).find('force-alternate-costume').text();
                    if (FORCE_ALT_COSTUME) {
                        console.log("Forcing alternate costume set: "+FORCE_ALT_COSTUME);
                        alternateCostumeSets[FORCE_ALT_COSTUME] = true;
                    } else {
                        $(xml).find('alternate-costume-sets').each(function () {
                            var set = $(this).text();
                            alternateCostumeSets[set] = true;
                            if (set === 'all') {
                                console.log("Including all alternate costume sets");
                            } else {
                                console.log("Including alternate costume set: "+set);
                            }
                        });
                    }
    
                } else {
                    ALT_COSTUMES_ENABLED = false;
                    console.log("Alternate costumes disabled");
                }
    
    			$(xml).find('include-status').each(function() {
    				includedOpponentStatuses[$(this).text()] = true;
    				console.log("Including", $(this).text(), "opponents");
    			});
    
    Joseph Kantel's avatar
    Joseph Kantel committed
    function enterTitleScreen() {
        $warningScreen.hide();
        $titleScreen.show();
    }
    
    
    /************************************************************
     * Transitions between two screens.
     ************************************************************/
    function screenTransition (first, second) {
    	first.hide();
    	second.show();
    }
    
    /************************************************************
     * Switches to the next screen based on the screen provided.
     ************************************************************/
    function advanceToNextScreen (screen) {
        if (screen == $titleScreen) {
            /* advance to the select screen */
    		screenTransition($titleScreen, $selectScreen);
    
        } else if (screen == $selectScreen) {
            /* advance to the main game screen */
            $selectScreen.hide();
    		loadGameScreen();
            $gameScreen.show();
        }
    }
    
    /************************************************************
     * Switches to the last screen based on the screen provided.
     ************************************************************/
    function returnToPreviousScreen (screen) {
        if (screen == $selectScreen) {
            /* return to the title screen */
            $selectScreen.hide();
            $titleScreen.show();
        }
    }
    
    /************************************************************
    
     * Resets the game state so that the game can be restarted.
    
     ************************************************************/
    
    function resetPlayers () {
    
        players.forEach(function(p) {
            p.resetState();
        });
        updateAllBehaviours(null, null, SELECTED);
    
    }
    
    /************************************************************
     * Restarts the game.
     ************************************************************/
    function restartGame () {
        KEYBINDINGS_ENABLED = false;
    
    
    	clearTimeout(timeoutID); // No error if undefined or no longer valid
    	timeoutID = autoForfeitTimeoutID = undefined;
    	stopCardAnimations();
    
    	/* enable table opacity */
    	tableOpacity = 1;
    	$gameTable.css({opacity:1});
        $gamePlayerClothingArea.show();
        $gamePlayerCardArea.show();
    
    	/* trigger screen refreshes */
    	updateSelectionVisuals();
    	updateAllGameVisuals();
        selectTitleCandy();
    
        forceTableVisibility(true);
    
    	/* there is only one call to this right now */
    	$epilogueSelectionModal.hide();
    	$gameScreen.hide();
    	$epilogueScreen.hide();