Skip to content
Snippets Groups Projects
spniCore.js 51.4 KiB
Newer Older
/********************************************************************************
 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');
FarawayVision's avatar
FarawayVision committed
$helpModal = $('#help-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.tags = this.baseTags = [];
    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;
        this.baseTags = appearance.tags.slice();
        this.labelOverridden = false;
		/* 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.stageChangeUpdate();
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() { }
/* Compute the Player's tags list from their baseTags list. */
Player.prototype.updateTags = function () {
    var tags = [this.id];
    var stage = this.stage || 0;
    
    if (this.alt_costume && this.alt_costume.id) {
        tags.push(this.alt_costume.id);
    }
    
    this.baseTags.forEach(function (tag_desc) {
        if (typeof(tag_desc) === 'string') {
            tags.push(tag_desc);
            return;
        }
        
        if (!tag_desc.tag) return;
        
        var tag = tag_desc.tag;
        var from = parseInt(tag_desc.from, 10);
        var to = parseInt(tag_desc.to, 10);
        
        if (isNaN(to))   to = Number.POSITIVE_INFINITY;
        if (isNaN(from)) from = 0;
        
        if (stage >= from && stage <= to) {
            tags.push(tag);
        }
    });
    
    this.tags = expandTagsList(tags);
}

Player.prototype.stageChangeUpdate = function () {
    this.updateLabel();
    this.updateFolder();
    this.updateTags();
}

Player.prototype.addTag = function(tag) {
    this.baseTags.push(canonicalizeTag(tag));
}

Player.prototype.removeTag = function(tag) {
    tag = canonicalizeTag(tag);
    
    this.baseTags = this.baseTags.filter(function (t) {
        if (typeof(t) === 'string') { return t !== tag };
        if (!t.tag) return false;
        return t.tag !== tag;
    });
}

Player.prototype.hasTag = function(tag) {
    return tag && this.tags && this.tags.indexOf(canonicalizeTag(tag)) >= 0;
};

/*****************************************************************************
 * 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.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 = {};
    
    /* baseTags stores tags that will be later used in resetState to build the
     * opponent's true tags list. It does not store implied tags.
     *
     * The tags list stores the fully-expanded list of tags for the opponent,
     * including implied tags.
     */
    this.baseTags = $metaXml.find('tags').children().map(function() { return canonicalizeTag($(this).text()); }).get();
    /* 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) {
        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: [],
                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;
            var costumeTags = this.default_costume.tags.slice();
            var tagMods = $xml.find('tags');
            if (tagMods) {
                tagMods.find('tag').each(function (idx, elem) {
                    var $elem = $(elem);
                    var tag = canonicalizeTag(tag);
                    var removed = $elem.attr('remove') || '';
                    var fromStage = $elem.attr('from');
                    var toStage = $elem.attr('to');
                    // Remove previous declarations for this tag
                    costumeTags = costumeTags.filter(function (t) { return t.tag !== tag; });

                    if (removed.toLowerCase() !== 'true') {
                        newTags.push({'tag': tag, 'from': fromStage, 'to': toStage});
                
                Array.prototype.push.apply(costumeTags, newTags);
            }

            this.alt_costume.tags = costumeTags;

        }.bind(this),
        error: function () {
            console.error("Failed to load alternate costume: "+this.selected_costume);
        },
    })
}

Opponent.prototype.unloadAlternateCostume = function () {
    if (!this.alt_costume) {
        return;
    }
    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');
            if (typeof tags !== typeof undefined && tags !== false) {
                tagsArray = $(tags).find('tag').map(function () {
                    return {
                        'tag': canonicalizeTag($(this).text()),
                        'from': $(this).attr('from'),
                        'to': $(this).attr('to'),
                    }
                }).get();
            this.default_costume.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 = canonicalizeTag($(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.hasTag(filter); })))
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 loadVersionInfo () {
    $('.substitute-version').text('Unknown Version');
    
    return $.ajax({
        type: "GET",
		url: "version-info.xml",
		dataType: "text",
		success: function(xml) {
            versionInfo = $(xml);
            CURRENT_VERSION = versionInfo.find('current').attr('version');
            
            $('.substitute-version').text('v'+CURRENT_VERSION);
            console.log("Running SPNATI version "+CURRENT_VERSION);
        }
    });
}

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() {