Newer
Older
/********************************************************************************
metodLD
committed
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 */
var EPILOGUES_ENABLED = true;
var EPILOGUE_BADGES_ENABLED = true;
var ALT_COSTUMES_ENABLED = 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';
/* Game Wide Constants */
var HUMAN_PLAYER = 0;
/* Directory Constants */
var IMG = 'img/';

ReformCopyright
committed
var backgroundImage;
/*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';
var includedOpponentStatuses = {};
/* game table */
var tableOpacity = 1;
$gameTable = $('#game-table');
/* useful variables */
var BLANK_PLAYER_IMAGE = "opponents/blank.png";
/* player array */

ReformCopyright
committed
var players = Array(5);

ReformCopyright
committed
/* 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 = '';
/**********************************************************************
* Screens & Modals
**********************************************************************/
/* Screens */
$titleScreen = $('#title-screen');
$selectScreen = $('#main-select-screen');
$individualSelectScreen = $('#individual-select-screen');
$groupSelectScreen = $('#group-select-screen');
$gameScreen = $('#game-screen');
$epilogueScreen = $('#epilogue-screen');
metodLD
committed
$galleryScreen = $('#gallery-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.dialogue;
playerData.currentImage = players[i].chosenState.image;
}
tableReports[i-1] = playerData;
} else {
tableReports[i-1] = null;
}
}
var circumstances = {
'userAgent': navigator.userAgent,
'origin': getReportedOrigin(),
'currentRound': currentRound,
'currentTurn': currentTurn,
'visibleScreens': []
}
if (gamePhase) {
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(),
'session': sessionID,
'type': bugType,
'character': bugCharacter,
'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.
* timer (integer), time until forfeit is finished.
* 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+'/';

FarawayVision
committed
this.base_folder = 'opponents/'+id+'/';
this.first = '';
this.last = '';
this.labels = undefined;
this.size = eSize.MEDIUM;
this.intelligence = eIntelligence.AVERAGE;
this.gender = eGender.MALE;
this.timer = 20;
this.scale = undefined;
ReformCopyright
committed
this.tags = [id];
this.xml = null;
this.metaXml = null;
this.selected_costume = null;
this.alt_costume = null;
this.default_costume = null;
/*******************************************************************
* (Re)Initialize the player properties that change during a game
*******************************************************************/
this.out = this.finished = this.exposed = false;
this.forfeit = "";
this.stage = this.current = this.consecutiveLosses = 0;
this.timeInStage = -1;
this.markers = {};
ReformCopyright
committed
this.exposed = { upper: false, lower: false };
/* 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.
*/
this.allStates = parseDialogue(this.xml.find('start'), this);
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);
}
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 {
this.tags.push(tag);
}
}.bind(this));
if (appearance.id) {
this.tags.push(appearance.id);
}
/* Find and grab the wardrobe tag */
/* find and create all of their clothing */
$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);
ReformCopyright
committed
this.startingLayers = clothingArr.length;
this.mostlyClothed = checkPlayerStatus(this, STATUS_DECENT);
Player.prototype.getIntelligence = function () {
return this.intelligence; // Opponent uses getByStage()
};

ReformCopyright
committed
/* 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 () { }

ReformCopyright
committed
Player.prototype.updateBehaviour = function() { }
/*****************************************************************************
* Subclass of Player for AI-controlled players.
****************************************************************************/
function Opponent (id, $metaXml, status, releaseNumber) {
this.id = id;
this.folder = 'opponents/'+id+'/';

FarawayVision
committed
this.base_folder = 'opponents/'+id+'/';
this.enabled = $metaXml.find('enabled').text();
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 = $metaXml.find('description').text();
this.ending = $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.alternate_costumes = $metaXml.find('alternates').find('costume').map(function () {
return {

FarawayVision
committed
'folder': $(this).attr('folder'),
'label': $(this).text(),
'image': $(this).attr('img')
};
}).get();
}
Opponent.prototype = Object.create(Player.prototype);
Opponent.prototype.constructor = Opponent;

ReformCopyright
committed
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;
}

ReformCopyright
committed
Opponent.prototype.isLoaded = function() {
return this.xml != undefined;
}
Opponent.prototype.onSelected = function() {
this.resetState();
console.log(this.slot+": "+this);
this.updateBehaviour(SELECTED);
updateSelectionVisuals();
}

ReformCopyright
committed
Opponent.prototype.updateLabel = function () {
if (this.labels) this.label = this.getByStage(this.labels);
}
Opponent.prototype.updateFolder = function () {
if (this.folders) this.folder = this.getByStage(this.folders);
}

ReformCopyright
committed
Opponent.prototype.getImagesForStage = function (stage) {
if(!this.xml) return [];

ReformCopyright
committed
var imageSet = {};
var folder = this.folder;
this.xml.find('stage[id="'+stage+'"] state').each(function () {
imageSet[folder+$(this).attr('img')] = true;
});
return Object.keys(imageSet);
};
Opponent.prototype.getByStage = function (arr) {
if (typeof(arr) === "string") {
return arr;
}
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 <= this.stage) {
bestFit = $(arr[i]).text();
bestFitStage = startStage;
}
}
return bestFit;
};

FarawayVision
committed
Opponent.prototype.selectAlternateCostume = function (folder) {
this.selected_costume = folder;

ReformCopyright
committed
Opponent.prototype.getIntelligence = function () {
return this.getByStage(this.intelligence) || eIntelligence.AVERAGE;
};
Opponent.prototype.loadAlternateCostume = function () {
$.ajax({
type: "GET",

FarawayVision
committed
url: this.selected_costume+'costume.xml',
dataType: "text",
success: function (xml) {
var $xml = $(xml);
this.alt_costume = {

FarawayVision
committed
id: $xml.find('id').text(),
tags: $xml.find('tags'),
folders: $xml.find('folder'),
wardrobe: $xml.find('wardrobe')
};
this.onSelected();
}.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.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.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.
************************************************************/

ReformCopyright
committed
Opponent.prototype.loadBehaviour = function (slot) {
if (this.selected_costume) {
this.loadAlternateCostume();
} else {
this.onSelected();
}
fetchCompressedURL(
'opponents/' + this.id + "/behaviour.xml",
/* Success callback.
* 'this' is bound to the Opponent object.
*/
function(xml) {
this.xml = $xml;
this.size = $xml.find('size').text();
this.timer = Number($xml.find('timer').text());
this.intelligence = $xml.find('intelligence');
labels: $xml.find('label'),
tags: null,
folders: this.folder,
wardrobe: $xml.find('wardrobe')
};
if (typeof tags !== typeof undefined && tags !== false) {
$(tags).find('tag').each(function () {
$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);
});
//var newPlayer = createNewPlayer(opponent.id, first, last, labels, gender, size, intelligence, Number(timer), opponent.scale, tagsArray, $xml);
this.targetedLines = targetedLines;
if (ALT_COSTUMES_ENABLED && this.selected_costume) {
this.loadAlternateCostume();
} else {
this.onSelected();
}
/* Error callback. */
function(err) {
console.log("Failed reading \""+this.id+"\" behaviour.xml");

ReformCopyright
committed
delete players[this.slot];
/**********************************************************************
***** 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);
/* 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);
save.loadCookie();
/* 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);
}
return $.ajax({
type: "GET",
url: "config.xml",
dataType: "text",
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 _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 _alts = $(xml).find('alternate-costumes').text();
if(_alts === "true") {
ALT_COSTUMES_ENABLED = true;
console.log("Alternate costumes enabled");
} 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");
});
function enterTitleScreen() {
$warningScreen.hide();
$titleScreen.show();
}
/************************************************************
* Transitions between two screens.
************************************************************/
function screenTransition (first, second) {
first.hide();
second.show();
}
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
/************************************************************
* 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 () {
for (var i = 0; i < players.length; i++) {

ReformCopyright
committed
timers[i] = 0;
updateAllBehaviours(null, SELECTED);
}
/************************************************************
* Restarts the game.
************************************************************/
function restartGame () {
KEYBINDINGS_ENABLED = false;

ReformCopyright
committed
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();
/* there is only one call to this right now */
$epilogueSelectionModal.hide();
$gameScreen.hide();
$epilogueScreen.hide();

ReformCopyright
committed
loadClothing();
$titleScreen.show();
}
/**********************************************************************
***** Interaction Functions *****
**********************************************************************/
/*
* Bug Report Modal functions
*/
function getBugReportJSON() {
var desc = $('#bug-report-desc').val();
var type = $('#bug-report-type').val();
var character = undefined;
var report = compileBaseErrorReport(desc, type);
return JSON.stringify(report);
}
function updateBugReportSendButton() {
if($('#bug-report-desc').val().trim().length > 0) {
$("#bug-report-modal-send-button").removeAttr('disabled');
} else {
$("#bug-report-modal-send-button").attr('disabled', 'true');
}
}
$('#bug-report-desc').keyup(updateBugReportSendButton);
/* Update the bug report text dump. */
$('#bug-report-output').val(getBugReportJSON());
$('#bug-report-status').text("");
updateBugReportSendButton();
}
function copyBugReportOutput() {
var elem = $('#bug-report-output')[0];
elem.select();
document.execCommand("copy");
}
function sendBugReport() {
if($('#bug-report-desc').val().trim().length == 0) {
$('#bug-report-status').text("Please enter a description first!");
return false;
}
$.ajax({
url: BUG_REPORTING_ENDPOINT,
method: 'POST',
data: getBugReportJSON(),
contentType: 'application/json',
error: function (jqXHR, status, err) {
console.error("Could not send bug report - error "+status+": "+err);
$('#bug-report-status').text("Failed to send bug report (error "+status+")");
success: function () {
$('#bug-report-status').text("Bug report sent!");
$('#bug-report-desc').val("");
closeBugReportModal();
}
$('#bug-report-type').change(updateBugReportOutput);
$('#bug-report-desc').change(updateBugReportOutput);
$('#bug-report-copy-btn').click(copyBugReportOutput);
/************************************************************
* The player clicked a bug-report button. Shows the bug reports modal.
************************************************************/
function showBugReportModal () {
var bugReportTypes = [
['freeze', 'Game Freeze or Crash'],
['display', 'Game Graphical Problem'],
['other', 'Other Game Issue'],
]
for (var i=1;i<5;i++) {
if (players[i]) {
var mixedCaseID = players[i].id.charAt(0).toUpperCase()+players[i].id.substring(1);
bugReportTypes.push(['character-'+players[i].id, 'Character Defect ('+mixedCaseID+')']);
}
}
$('#bug-report-type').empty().append(bugReportTypes.map(function (t) {
return $('<option value="'+t[0]+'">'+t[1]+'</option>');
}));
$('#bug-report-modal span[data-toggle="tooltip"]').tooltip();
updateBugReportOutput();
KEYBINDINGS_ENABLED = false;
$bugReportModal.modal('show');
}
function closeBugReportModal() {
KEYBINDINGS_ENABLED = true;
$bugReportModal.modal('hide');
}
/*
* Show the usage tracking consent modal.
*/
function showUsageTrackingModal() {
$usageTrackingModal.modal('show');
}
function enableUsageTracking() {
save.data.askedUsageTracking = true;
USAGE_TRACKING = true;
save.saveOptions();
}
function disableUsageTracking() {
save.data.askedUsageTracking = true;
USAGE_TRACKING = false;
save.saveOptions();
}
/************************************************************
* The player clicked the credits button. Shows the credits modal.
************************************************************/
function showCreditModal () {
$creditModal.modal('show');
}
/************************************************************
* The player clicked the version button. Shows the version modal.
************************************************************/
function showVersionModal () {
$versionModal.modal('show');
}
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/************************************************************
* The player clicked the player tags button. Shows the player tags modal.
************************************************************/
function showPlayerTagsModal () {
if (document.forms['player-tags'].elements.length <= 6) {
/* maybe move this data to an external file if the hardcoded stuff changes often enough */
var playerOptions = {
'hair_length' : [
{value: 'bald', text: 'Bald - No Hair'},
{value: 'short_hair', text: 'Short Hair - Does Not Pass Jawline'},
{value: 'medium_hair', text: 'Medium Hair - Reaches Between Jawline and Shoulders'},
{value: 'long_hair', text: 'Long Hair - Reaches Beyond Shoulders'},
{value: 'very_long_hair long_hair', text: 'Very Long Hair - Reaches the Thighs or Beyond'}
],
'physical_build' : [
{value: 'chubby'},
{value: 'athletic'},
{value: 'muscular athletic'}
],
'height' : [
{value: 'tall'},
{value: 'average'},
{value: 'short'}
],
'pubic_hair_style' : [
{value: 'shaved'},
{value: 'trimmed'},
{value: 'hairy'}
],
'maleOnly_circumcision' : [
{value: 'circumcised'},
{value: 'uncircumcised'}
]
}
for (var choiceName in playerOptions) {
var parsedName = choiceName.match(/^([^_]+)Only_(.*$)/) || [,'',choiceName];
var choiceElem = '<label class="player-tag-select ' + parsedName[1] + '">Choose a' + ('aeiou'.includes(parsedName[2].charAt(0)) ? 'n ' : ' ');