Newer
Older
/* Epilogue UI elements */
$epilogueScreen = $('#epilogue-screen');
var epilogueContainer = document.getElementById('epilogue-container');
var epilogueContent = document.getElementById('epilogue-content');
/* Epilogue selection modal elements */
$epilogueSelectionModal = $('#epilogue-modal'); //the modal box
$epilogueHeader = $('#epilogue-header-text'); //the header text for the epilogue selection box
$epilogueList = $('#epilogue-list'); //the list of epilogues
$epilogueAcceptButton = $('#epilogue-modal-accept-button'); //click this button to go with the chosen ending
var epilogueSelections = []; //references to the epilogue selection UI elements
var winStr = "You've won the game, and possibly made some friends. Who among these players did you become close with?"; //Winning the game, with endings available
var winStrNone = "You've won the game, and possibly made some friends? Unfortunately, none of your competitors are ready for a friend like you.<br>(None of the characters you played with have an ending written.)"; //Player won the game, but none of the characters have an ending written
var lossStr = "Well you lost. And you didn't manage to make any new friends. But you saw some people strip down and show off, and isn't that what life is all about?<br>(You may only view an ending when you win.)"; //Player lost the game. Currently no support for ending scenes when other players win
// Player won the game, but epilogues are disabled.
var winEpiloguesDisabledStr = "You won... but epilogues have been disabled.";
// Player lost the game with epilogues disabled.
var lossEpiloguesDisabledStr = "You lost... but epilogues have been disabled.";
var epilogues = []; //list of epilogue data objects
var chosenEpilogue = null;
// Attach some event listeners
var previousButton = document.getElementById('epilogue-previous');
var nextButton = document.getElementById('epilogue-next');
previousButton.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
progressEpilogue(-1);
});
nextButton.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
progressEpilogue(1);
});
document.getElementById('epilogue-restart').addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
showRestartModal();
});
document.getElementById('epilogue-buttons').addEventListener('click', function() {
if (!previousButton.disabled) {
progressEpilogue(-1);
}
});
epilogueContent.addEventListener('click', function() {
if (!nextButton.disabled) {
progressEpilogue(1);
}
});
/************************************************************
* Return the numerical part of a string s. E.g. "20%" -> 20
************************************************************/
function getNumericalPart(s){
return parseFloat(s); //apparently I don't actually need to remove the % (or anything else) from the string before I do the conversion
}
/************************************************************
* Return the approriate left: setting so that a text box of the specified width is centred
* Assumes a % width
************************************************************/
function getCenteredPosition(width){
var w = getNumericalPart(width); //numerical value of the width
var left = 50 - (w/2); //centre of the text box is at the 50% position
return left + "%";
}
/************************************************************
* Load the Epilogue data for a character
************************************************************/
function loadEpilogueData(player) {
if (!player || !player.xml) { //return an empty list if a character doesn't have an XML variable. (Most likely because they're the player.)
metodLD
committed
var playerGender = players[HUMAN_PLAYER].gender;
metodLD
committed
//get the XML tree that relates to the epilogue, for the specific player gender
//var epXML = $($.parseXML(xml)).find('epilogue[gender="'+playerGender+'"]'); //use parseXML() so that <image> tags come through properly //IE doesn't like this
metodLD
committed
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
var epilogues = player.xml.find('epilogue').filter(function (index) {
/* Returning true from this function adds the current epilogue to the list of selectable epilogues.
* Conversely, returning false from this function will make the current epilogue not selectable.
*/
/* 'gender' attribute: the epilogue will only be selectable if the player character has the given gender, or if the epilogue is marked for 'any' gender. */
var epilogue_gender = $(this).attr('gender');
if (epilogue_gender && epilogue_gender !== playerGender && epilogue_gender !== 'any') {
// if the gender doesn't match, don't make this epilogue selectable
return false;
}
var alsoPlaying = $(this).attr('alsoPlaying');
if (alsoPlaying !== undefined && !(players.some(function(p) { return p.id == alsoPlaying; }))) {
return false;
}
var playerStartingLayers = parseInterval($(this).attr('playerStartingLayers'));
if (playerStartingLayers !== undefined && !inInterval(players[HUMAN_PLAYER].startingLayers, playerStartingLayers)) {
return false;
}
/* 'markers' attribute: the epilogue will only be selectable if the character has ALL markers listed within the attribute set. */
var all_marker_attr = $(this).attr('markers');
if (all_marker_attr
&& !all_marker_attr.trim().split(/\s+/).every(function(marker) {
return checkMarker(marker, player);
})) {
// not every marker set
return false;
}
/* 'not-markers' attribute: the epilogue will only be selectable if the character has NO markers listed within the attribute set. */
var no_marker_attr = $(this).attr('not-markers');
if (no_marker_attr
&& no_marker_attr.trim().split(/\s+/).some(function(marker) {
return checkMarker(marker, player);
})) {
// some disallowed marker set
return false;
}
/* 'any-markers' attribute: the epilogue will only be selectable if the character has at least ONE of the markers listed within the attribute set. */
var any_marker_attr = $(this).attr('any-markers');
if (any_marker_attr
&& !any_marker_attr.trim().split(/\s+/).some(function(marker) {
return checkMarker(marker, player);
})) {
// none of the markers set
return false;
}
/* 'alsoplaying-markers' attribute: this epilogue will only be selectable if ALL markers within the attribute are set for any OTHER characters in the game. */
var alsoplaying_marker_attr = $(this).attr('alsoplaying-markers');
if (alsoplaying_marker_attr
&& !alsoplaying_marker_attr.trim().split(/\s+/).every(function(marker) {
return players.some(function(p) {
return p !== player && checkMarker(marker, p);
});
})) {
// not every marker set by some other character
return false;
}
/* 'alsoplaying-not-markers' attribute: this epilogue will only be selectable if NO markers within the attribute are set for other characters in the game. */
var alsoplaying_not_marker_attr = $(this).attr('alsoplaying-not-markers');
if (alsoplaying_not_marker_attr
&& alsoplaying_not_marker_attr.trim().split(/\s+/).some(function(marker) {
return players.some(function(p) {
return p !== player && checkMarker(marker, p);
});
})) {
// some disallowed marker set by some other character
return false;
}
/* 'alsoplaying-any-markers' attribute: this epilogue will only be selectable if at least one marker within the attribute are set for any OTHER character in the game. */
var alsoplaying_any_marker_attr = $(this).attr('alsoplaying-any-markers');
if (alsoplaying_any_marker_attr
&& !alsoplaying_any_marker_attr.trim().split(/\s+/).some(function(marker) {
return players.some(function(p) {
return p !== player && checkMarker(marker, p);
});
})) {
// none of the markers set by any other player
return false;
}
// if we made it this far the epilogue must be selectable
return true;
}).map(function (i, e) { return parseEpilogue(player, e); }).get();
metodLD
committed

FarawayVision
committed
function parseEpilogue(player, rawEpilogue) {
//use parseXML() so that <image> tags come through properly
//not using parseXML() because internet explorer doesn't like it
var ratio = [4, 3];
try {
var rawRatio = $(rawEpilogue).attr('ratio');
if (rawRatio) {
rawRatio = rawRatio.split(':');
ratio = [parseFloat(rawRatio[0]), parseFloat(rawRatio[1])];
}
} catch(e) {
console.error('Failed reading epilogue ratio: ', $(rawEpilogue).attr('ratio'))
}
var screens = []; //the list of screens for the epilogue
// Leaving this for backwards compatibility, screens are hereby depreciated
$(rawEpilogue).find("screen").each(function() {
var image = $(this).attr("img").trim(); //get the full path for the screen's image
if (image.length > 0) {
image = player.base_folder + image;
}
//use an attribute rather than a tag because IE doesn't like parsing XML

FarawayVision
committed
var textBoxes = parseSceneContent(player, $(this)).textBoxes;

FarawayVision
committed
screens.push({image, textBoxes}); //add a screen object to the list of screens
});
var backgrounds = [];
$(rawEpilogue).find('background').each(function() {
var image = $(this).attr('img').trim();
if (image.length == 0) {
image = '';
} else {
image = image.charAt(0) === '/' ? image : player.base_folder + image;
}
var scenes = [];
$(this).find('scene').each(function() {
scenes.push(parseSceneContent(player, $(this)));
});
var css = $(this).attr('css');

FarawayVision
committed
backgrounds.push({image, scenes, css});

FarawayVision
committed
var epilogue = {player, title, ratio, screens, backgrounds}; //epilogue object
metodLD
committed
if (!epilogue.backgrounds.length && !epilogue.screens.length) {
return;
}
metodLD
committed
function parseSceneContent(player, scene) {
var images = [];
var textBoxes = [];
var backgroundTransform = [scene.attr('background-position-x') || 0, scene.attr('background-position-y') || 0, scene.attr('background-zoom') || 0];
try {
backgroundTransform[0] = parseFloat(backgroundTransform[0]) * -1;
backgroundTransform[1] = parseFloat(backgroundTransform[1]) * -1;
backgroundTransform[2] = parseFloat(backgroundTransform[2]) / 100 + 1;
} catch(e) {}
backgroundTransform = 'translate(' + backgroundTransform[0] + '%,' + backgroundTransform[1] + '%) scale(' + backgroundTransform[2] + ');';
// Find the image data for this shot
scene.find('sprite').each(function() {
var x = $(this).find("x").html().trim();
var y = $(this).find("y").html().trim();
var width = $(this).find("width").html().trim();
var src = $(this).find('src').html().trim();
AmonKurath
committed
src = src.charAt(0) === '/' ? src : player.base_folder + src;
var css = $(this).attr('css');
metodLD
committed
images.push({x, y, width, src, css});
});
//get the information for all the text boxes
scene.find("text").each(function() {
//the text box's position and width
var x = $(this).find("x").html().trim();
var y = $(this).find("y").html().trim();
var w = $(this).find("width").html();
var a = $(this).find("arrow").html();
//the width component is optional. Use a default of 20%.
if (w) {
w = w.trim();
}
if (!w || (w.length <= 0)) {
w = "20%"; //default to text boxes having a width of 20%
}
metodLD
committed
//dialogue bubble arrow
if (a) {
a = a.trim().toLowerCase();
if (a.length >= 1) {
a = "arrow-" + a; //class name for the different arrows. Only use if the writer specified something.
}
} else {
a = "";
}
//automatically centre the text box, if the writer wants that.
if (x.toLowerCase() == "centered") {
x = getCenteredPosition(w);
}
metodLD
committed
var text = fixupDialogue($(this).find("content").html().trim()); //the actual content of the text box
metodLD
committed
var css = $(this).attr('css');
textBoxes.push({x, y, width:w, arrow:a, text, css}); //add a textBox object to the list of textBoxes
return {images, textBoxes, backgroundTransform};
}
/************************************************************
* Add the epilogue to the Epilogue modal
************************************************************/
function addEpilogueEntry(epilogue){
var num = epilogues.length; //index number of the new epilogue
epilogues.push(epilogue);
var player = epilogue.player
metodLD
committed
var nameStr = player.first+" "+player.last;
if (player.first.length <= 0 || player.last.length <= 0){
nameStr = player.first+player.last; //only use a space if they have both first and last names
}
metodLD
committed
var epilogueTitle = nameStr+": "+epilogue.title;
var idName = 'epilogue-option-'+num;
var clickAction = "selectEpilogue("+num+")";
var unlocked = save.hasEnding(player.id, epilogue.title) ? " unlocked" : "";
metodLD
committed
ReformCopyright
committed
var htmlStr = '<li id="'+idName+'" class="epilogue-entry'+unlocked+'"><button onclick="'+clickAction+'">'+epilogueTitle+'</button></li>';
metodLD
committed
$epilogueList.append(htmlStr);
epilogueSelections.push($('#'+idName));
}
/************************************************************
* Clear the Epilogue modal
************************************************************/
function clearEpilogueList(){
$epilogueHeader.html('');
$epilogueList.html('');
epilogues = [];
epilogueSelections = [];
}
/************************************************************
* The user has clicked on a button to choose a particular Epilogue
************************************************************/
function selectEpilogue(epNumber){
chosenEpilogue = epilogues[epNumber]; //select the chosen epilogues
metodLD
committed
for (var i = 0; i < epilogues.length; i++){
epilogueSelections[i].removeClass("active"); //make sure no other epilogue is selected
}
epilogueSelections[epNumber].addClass("active"); //mark the selected epilogue as selected
$epilogueAcceptButton.prop("disabled", false); //allow the player to accept the epilogue
}
/************************************************************
* Show the modal for the player to choose an Epilogue, or restart the game.
************************************************************/
function doEpilogueModal(){
metodLD
committed
clearEpilogueList(); //remove any already loaded epilogues
chosenEpilogue = null; //reset any currently-chosen epilogue
$epilogueAcceptButton.prop("disabled", true); //don't let the player accept an epilogue until they've chosen one
metodLD
committed
//whether or not the human player won
var playerWon = !players[HUMAN_PLAYER].out;
metodLD
committed
if (EPILOGUES_ENABLED && playerWon) { //all the epilogues are for when the player wins, so don't allow them to choose one if they lost
//load the epilogue data for each player
players.forEach(function(p) {
loadEpilogueData(p).forEach(addEpilogueEntry);
});
metodLD
committed
//are there any epilogues available for the player to see?
var haveEpilogues = (epilogues.length >= 1); //whether or not there are any epilogues available
$epilogueAcceptButton.css("visibility", haveEpilogues ? "visible" : "hidden");
//decide which header string to show the player. This describes the situation.
var headerStr = '';
if (playerWon){
headerStr = winStr; //player won, and there are epilogues available
if (!haveEpilogues){
headerStr = winStrNone; //player won, but none of the NPCs have epilogues
}
} else {
headerStr = lossStr; //player lost
}
} else {
headerStr = winEpiloguesDisabledStr;
} else {
headerStr = lossEpiloguesDisabledStr;
}
}
metodLD
committed
$epilogueHeader.html(headerStr); //set the header string
$epilogueSelectionModal.modal("show");//show the epilogue selection modal
}
/************************************************************
* Start the Epilogue
************************************************************/
function doEpilogue(){
save.addEnding(chosenEpilogue.player.id, chosenEpilogue.title);
if (USAGE_TRACKING) {
var usage_tracking_report = {
'date': (new Date()).toISOString(),
'type': 'epilogue',
'session': sessionID,
'game': gameID,
'userAgent': navigator.userAgent,
'origin': getReportedOrigin(),
'table': {},
'chosen': {
'id': chosenEpilogue.player.id,
'title': chosenEpilogue.title
}
};
for (let i=1;i<5;i++) {
if (players[i]) {
usage_tracking_report.table[i] = players[i].id;
}
}
$.ajax({
url: USAGE_TRACKING_ENDPOINT,
method: 'POST',
data: JSON.stringify(usage_tracking_report),
contentType: 'application/json',
error: function (jqXHR, status, err) {
console.error("Could not send usage tracking report - error "+status+": "+err);
},
});
}
metodLD
committed
//just in case, clear any leftover epilogue elements
$(epilogueContent).children(':not(.epilogue-background)').remove();
epilogueContainer.dataset.background = -1;
metodLD
committed
progressEpilogue(1); //initialise buttons and text boxes
screenTransition($titleScreen, $epilogueScreen); //currently transitioning from title screen, because this is for testing
$epilogueSelectionModal.modal("hide");
}
/************************************************************
* Draw Epilogue element
************************************************************/
function drawEpilogueBox(data) {
if (!data) {
return;
}
var imgOrTxt = data.src ? 'image' : 'text';
var idNum = document.getElementsByClassName('epilogue-' + imgOrTxt).length;
//make new div element
var newEpilogueDiv = $(document.createElement('div')).attr('id', 'epilogue-' + imgOrTxt + '-' + idNum).addClass('epilogue-' + imgOrTxt);
switch (imgOrTxt) {
case 'image':
newEpilogueDiv.html('<img src="' + data.src + '">')
break;
case 'text':
var content = expandDialogue(data.text, null, players[HUMAN_PLAYER]);
newEpilogueDiv.html('<span class="dialogue-bubble ' + data.arrow + '">' + content + '</span>');
break;
}
newEpilogueDiv.attr('style', data.css);
//use css to position the box
newEpilogueDiv.css('position', "absolute");
newEpilogueDiv.css('left', data.x);
newEpilogueDiv.css('top', data.y);
newEpilogueDiv.css('width', data.width);
//attach new div element to the content div
epilogueContent.appendChild(newEpilogueDiv[0]);
}
/************************************************************
* Move the Epilogue forwards and backwards.
************************************************************/
metodLD
committed
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
function progressEpilogue(direction) {
var activeText = document.getElementsByClassName('epilogue-text').length;
var datastore = epilogueContainer.dataset;
if (!epilogueContainer.getAttribute('style')) {
var ratio = chosenEpilogue.ratio;
epilogueContainer.setAttribute('style', 'max-width:' + ratio[0] / ratio[1] * 100 + 'vh; height:' + ratio[1] / ratio[0] * 100 + 'vw;');
}
// default all buttons and disable/enable conditionally later
var $epiloguePrevButton = $('#epilogue-buttons > #epilogue-previous');
var $epilogueNextButton = $('#epilogue-buttons > #epilogue-next');
var $epilogueRestartButton = $('#epilogue-buttons > #epilogue-restart');
$epiloguePrevButton.prop("disabled", false);
$epilogueNextButton.prop("disabled", false);
$epilogueRestartButton.prop("disabled", true);
/////////////////////////////////////////
// This bit is only for backwards compatibility, please remove it once we're rid of all the old epilogues
/////////////////////////////////////////
if (chosenEpilogue.screens.length) {
var currentScreen = chosenEpilogue.screens[datastore.scene];
if (direction > 0) {
if (currentScreen && currentScreen.textBoxes.length > activeText) {
// Forward same screen
drawEpilogueBox(currentScreen.textBoxes[activeText++]);
} else {
// Forward and changing screens
$(epilogueContent).children('.epilogue-text').remove();
datastore.scene++;
currentScreen = chosenEpilogue.screens[datastore.scene];
$(epilogueContent).children('.epilogue-background').attr('src', currentScreen.image)
drawEpilogueBox(currentScreen.textBoxes && currentScreen.textBoxes[0]);
activeText = 1;
}
} else if (direction < 0) {
if (activeText > 1) {
// Backwards same screen
epilogueContent.removeChild(document.getElementById('epilogue-text-' + (--activeText)));
} else {
// Backwards and changing screens
$(epilogueContent).children('.epilogue-text').remove();
datastore.scene--;
currentScreen = chosenEpilogue.screens[datastore.scene];
$(epilogueContent).children('.epilogue-background').attr('src', currentScreen.image);
currentScreen.textBoxes.forEach(drawEpilogueBox);
activeText = document.getElementsByClassName('epilogue-text').length
}
}
metodLD
committed
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
// disable buttons
if (datastore.scene <= 0 && activeText <= 1) {
$epiloguePrevButton.prop('disabled', true);
}
if (datastore.scene >= chosenEpilogue.screens.length - 1 && activeText >= currentScreen.textBoxes.length) {
$epilogueNextButton.prop('disabled', true);
$epilogueRestartButton.prop('disabled', false);
}
} else {
/////////////////////////////////////////
// End backwards compatibility
/////////////////////////////////////////
var currentBackground = chosenEpilogue.backgrounds[datastore.background];
var currentScene = currentBackground && currentBackground.scenes[datastore.scene];
if (direction > 0) {
if (currentScene && currentScene.textBoxes.length > activeText) {
// Forward same scene
drawEpilogueBox(currentScene.textBoxes[activeText++]);
} else {
// Forward new scene
datastore.scene++;
if (!currentBackground || datastore.scene >= currentBackground.scenes.length) {
// New background!
datastore.background++;
currentBackground = chosenEpilogue.backgrounds[datastore.background];
datastore.scene = 0;
currentScene = currentBackground.scenes[datastore.scene];
$(epilogueContent).children('.epilogue-background').attr('src', currentBackground.image).siblings().remove();
$(epilogueContent).children('.epilogue-background').attr('style', currentBackground.css);
currentScene.images.forEach(drawEpilogueBox);
drawEpilogueBox(currentScene.textBoxes[0]);
} else {
// New scene, same background
datastore.scene++;
currentScene = currentBackground.scenes[datastore.scene];
$(epilogueContent).children('.epilogue-background').siblings().remove();
currentScene.images.forEach(drawEpilogueBox);
drawEpilogueBox(currentScene.textBoxes[0]);
}
activeText = 1;
$(epilogueContent).children('.epilogue-background').css('transform', currentScene.backgroundTransform);
}
} else if (direction < 0) {
if (activeText > 1) { // TODO: figure out how to make this more flexible if writers want multiple texts to appear at once or some such
// Backwards same scene
epilogueContent.removeChild(document.getElementById('epilogue-text-' + (--activeText)));
} else {
if (datastore.scene) {
// Backwards new scene same background
datastore.scene--;
currentScene = currentBackground.scenes[datastore.scene];
$(epilogueContent).children('.epilogue-background').siblings().remove();
currentScene.images.forEach(drawEpilogueBox);
currentScene.textBoxes.forEach(drawEpilogueBox);
} else {
// Backwards new background! (old background?)
datastore.background--;
currentBackground = chosenEpilogue.backgrounds[datastore.background];
datastore.scene = currentBackground.scenes.length - 1;
currentScene = currentBackground.scenes[datastore.background];
$(epilogueContent).children('.epilogue-background').attr('src', currentBackground.image).siblings().remove();
$(epilogueContent).children('.epilogue-background').attr('style', currentBackground.css);
currentScene.images.forEach(drawEpilogueBox);
currentScene.textBoxes.forEach(drawEpilogueBox);
}
activeText = document.getElementsByClassName('epilogue-text').length;
metodLD
committed
$(epilogueContent).children('.epilogue-background').css('transform', currentScene.backgroundTransform);
metodLD
committed
if (datastore.background <= 0 && datastore.scene <= 0 && activeText <= 1) {
$epiloguePrevButton.prop('disabled', true);
}
if (datastore.background >= chosenEpilogue.backgrounds.length - 1 &&
datastore.scene >= currentBackground.scenes.length - 1 &&
activeText >= currentScene.textBoxes.length) {
$epilogueNextButton.prop('disabled', true);
$epilogueRestartButton.prop('disabled', false);
}
}