mirror of
https://gitlab.com/Oreolek/salet-module.git
synced 2024-07-02 23:05:02 +03:00
Rewriting Undum WIP.
This commit is contained in:
parent
15792d556c
commit
e54585cdbc
|
@ -1,7 +1,6 @@
|
||||||
###
|
###
|
||||||
Salet interface configuration.
|
Salet interface configuration.
|
||||||
###
|
###
|
||||||
undum = require('./undum.js')
|
|
||||||
$(document).ready(() ->
|
$(document).ready(() ->
|
||||||
$("#ways").on("click", "a", (event) ->
|
$("#ways").on("click", "a", (event) ->
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
|
@ -1,25 +1,30 @@
|
||||||
# I confess that this world model heavily borrows from INSTEAD engine. - A.Y.
|
# I confess that this world model heavily borrows from INSTEAD engine. - A.Y.
|
||||||
|
|
||||||
undum = require('./undum.js')
|
|
||||||
obj = require('./obj.coffee')
|
obj = require('./obj.coffee')
|
||||||
markdown = require('./markdown.coffee')
|
markdown = require('./markdown.coffee')
|
||||||
cycle = require('./cycle.coffee')
|
cycle = require('./cycle.coffee')
|
||||||
|
Random = require('./random.js')
|
||||||
|
languages = require('./localize.coffee')
|
||||||
|
|
||||||
|
# Feature detection
|
||||||
|
hasLocalStorage = () ->
|
||||||
|
hasStorage = false
|
||||||
|
try {
|
||||||
|
hasStorage = ('localStorage' in window) &&
|
||||||
|
window.localStorage !== null &&
|
||||||
|
window.localStorage !== undefined;
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
hasStorage = false
|
||||||
|
}
|
||||||
|
return hasStorage
|
||||||
|
|
||||||
|
# Assertion
|
||||||
|
assert = console.assert
|
||||||
|
|
||||||
way_to = (content, ref) ->
|
way_to = (content, ref) ->
|
||||||
return "<a href='#{ref}' class='way' id='waylink-#{ref}'>#{content}</a>"
|
return "<a href='#{ref}' class='way' id='waylink-#{ref}'>#{content}</a>"
|
||||||
|
|
||||||
# jQuery was confused by this point where's the context so I did it vanilla-way
|
|
||||||
print = (content) ->
|
|
||||||
if content == ""
|
|
||||||
return
|
|
||||||
if typeof content == "function"
|
|
||||||
content = content()
|
|
||||||
block = document.getElementById("current-situation")
|
|
||||||
if block
|
|
||||||
block.innerHTML = block.innerHTML + markdown(content)
|
|
||||||
else
|
|
||||||
console.error("No current situation found.")
|
|
||||||
|
|
||||||
Array::remove = (e) -> @[t..t] = [] if (t = @indexOf(e)) > -1
|
Array::remove = (e) -> @[t..t] = [] if (t = @indexOf(e)) > -1
|
||||||
|
|
||||||
addClass = (element, className) ->
|
addClass = (element, className) ->
|
||||||
|
@ -84,11 +89,22 @@ class SaletRoom extends undum.Situation
|
||||||
# room illustration image, VN-style. Can be a GIF or WEBM. Can be a function.
|
# room illustration image, VN-style. Can be a GIF or WEBM. Can be a function.
|
||||||
pic: false
|
pic: false
|
||||||
|
|
||||||
|
canView: true
|
||||||
|
canChoose: true
|
||||||
|
priority: 1
|
||||||
|
frequency: 1
|
||||||
|
displayOrder: 1
|
||||||
|
tags: []
|
||||||
|
choices: ""
|
||||||
|
optionText: "Choice"
|
||||||
|
|
||||||
dsc: false # room description
|
dsc: false # room description
|
||||||
extendSection: false
|
extendSection: false
|
||||||
distance: Infinity # distance to the destination
|
distance: Infinity # distance to the destination
|
||||||
clear: true # clear the screen on entering the room?
|
clear: true # clear the screen on entering the room?
|
||||||
|
|
||||||
|
entering: (character, system, from) =>
|
||||||
|
|
||||||
###
|
###
|
||||||
I call SaletRoom.exit every time the player exits to another room.
|
I call SaletRoom.exit every time the player exits to another room.
|
||||||
Unlike @after this gets called after the section is closed.
|
Unlike @after this gets called after the section is closed.
|
||||||
|
|
702
lib/salet.coffee
Normal file
702
lib/salet.coffee
Normal file
|
@ -0,0 +1,702 @@
|
||||||
|
markdown = require('./markdown.coffee')
|
||||||
|
|
||||||
|
###
|
||||||
|
fcall() (by analogy with fmap) is added to the prototypes of both String and
|
||||||
|
Function. When called on a Function, it's an alias for Function#call();
|
||||||
|
when called on a String, it only returns the string itself, discarding any input.
|
||||||
|
###
|
||||||
|
|
||||||
|
Function.prototype.fcall = Function.prototype.call;
|
||||||
|
String.prototype.fcall = function() { return this; }
|
||||||
|
|
||||||
|
# Utility functions
|
||||||
|
|
||||||
|
parseFn = (str) ->
|
||||||
|
unless str?
|
||||||
|
return str
|
||||||
|
|
||||||
|
fstr = """
|
||||||
|
(function(character, system, situation) {
|
||||||
|
#{str}
|
||||||
|
#})
|
||||||
|
"""
|
||||||
|
return eval(fstr);
|
||||||
|
|
||||||
|
# Scrolls the top of the screen to the specified point
|
||||||
|
scrollTopTo = (value) ->
|
||||||
|
$('html,body').animate({scrollTop: value}, 500)
|
||||||
|
|
||||||
|
# Scrolls the bottom of the screen to the specified point
|
||||||
|
scrollBottomTo = (value) ->
|
||||||
|
scrollTopTo(value - $(window).height())
|
||||||
|
|
||||||
|
# Scrolls all the way to the bottom of the screen
|
||||||
|
scrollToBottom = () ->
|
||||||
|
scrollTopTo($('html').height() - $(window).height());
|
||||||
|
|
||||||
|
# Regular expression to catch every link action.
|
||||||
|
# Salet's default is a general URL-safe expression.
|
||||||
|
linkRe = /^([0-9A-Za-z_-]+|\.)(\/([0-9A-Za-z_-]+))?$/
|
||||||
|
|
||||||
|
# Returns HTML from the given content with the non-raw links wired up.
|
||||||
|
augmentLinks = (content) ->
|
||||||
|
output = $(content)
|
||||||
|
|
||||||
|
# Wire up the links for regular <a> tags.
|
||||||
|
output.find("a").each((index, element) ->
|
||||||
|
a = $(element)
|
||||||
|
href = a.attr('href')
|
||||||
|
if (!a.hasClass("raw")|| href.match(/[?&]raw[=&]?/))
|
||||||
|
if (href.match(linkRe))
|
||||||
|
a.click((event) ->
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
# If we're a once-click, remove all matching links.
|
||||||
|
if (a.hasClass("once") || href.match(/[?&]once[=&]?/))
|
||||||
|
system.clearLinks(href)
|
||||||
|
|
||||||
|
processClick(href)
|
||||||
|
return false
|
||||||
|
)
|
||||||
|
else
|
||||||
|
a.addClass("raw")
|
||||||
|
)
|
||||||
|
return output
|
||||||
|
|
||||||
|
/* Erases the character in local storage. This is permanent!
|
||||||
|
/* To restart the game afterwards, we perform a simple page refresh.
|
||||||
|
/* This guarantees authors don't have to care about "tainting" the
|
||||||
|
/* game state across save/erase cycles, meaning that character.sandbox
|
||||||
|
/* no longer has to be the end-all be-all repository of game state. */
|
||||||
|
var doErase = function(force) {
|
||||||
|
var saveId = getSaveId();
|
||||||
|
if (localStorage.getItem(saveId)) {
|
||||||
|
if (force || confirm("erase_message".l())) {
|
||||||
|
localStorage.removeItem(saveId);
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Find and return a list of ids for all situations with the given tag. */
|
||||||
|
var getSituationIdsWithTag = function(tag) {
|
||||||
|
var result = [];
|
||||||
|
for (var situationId in game.situations) {
|
||||||
|
var situation = game.situations[situationId];
|
||||||
|
|
||||||
|
for (var i = 0; i < situation.tags.length; ++i) {
|
||||||
|
if (situation.tags[i] == tag) {
|
||||||
|
result.push(situationId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Clear the current game output and start again. */
|
||||||
|
var startGame = function() {
|
||||||
|
progress.seed = new Date().toString();
|
||||||
|
|
||||||
|
character = new Character();
|
||||||
|
system.rnd = new Random(progress.seed);
|
||||||
|
progress.sequence = [{link:game.start, when:0}];
|
||||||
|
|
||||||
|
// Empty the display
|
||||||
|
$("#content").empty();
|
||||||
|
|
||||||
|
// Start the game
|
||||||
|
startTime = new Date().getTime() * 0.001;
|
||||||
|
system.time = 0;
|
||||||
|
if (game.init) game.init(character, system);
|
||||||
|
|
||||||
|
// Do the first state.
|
||||||
|
doTransitionTo(game.start);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Saves the character to local storage. */
|
||||||
|
var saveGame = function() {
|
||||||
|
// Store when we're saving the game, to avoid exploits where a
|
||||||
|
// player loads their file to gain extra time.
|
||||||
|
var now = (new Date()).getTime() * 0.001;
|
||||||
|
progress.saveTime = now - startTime;
|
||||||
|
|
||||||
|
// Save the game.
|
||||||
|
localStorage.setItem(getSaveId(), JSON.stringify(progress));
|
||||||
|
|
||||||
|
// Switch the button highlights.
|
||||||
|
$("#erase").removeClass('disabled');
|
||||||
|
$("#load").removeClass('disabled');
|
||||||
|
$("#save").addClass('disabled');
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Loads the game from the given data */
|
||||||
|
var loadGame = function(characterData) {
|
||||||
|
progress = characterData;
|
||||||
|
|
||||||
|
character = new Character();
|
||||||
|
system.rnd = new Random(progress.seed);
|
||||||
|
|
||||||
|
// Empty the display
|
||||||
|
$("#content").empty();
|
||||||
|
$("#intro").empty();
|
||||||
|
|
||||||
|
// Now play through the actions so far:
|
||||||
|
if (game.init) game.init(character, system);
|
||||||
|
|
||||||
|
// Run through all the player's history.
|
||||||
|
interactive = false;
|
||||||
|
for (var i = 0; i < progress.sequence.length; i++) {
|
||||||
|
var step = progress.sequence[i];
|
||||||
|
// The action must be done at the recorded time.
|
||||||
|
system.time = step.when;
|
||||||
|
processLink(step.link);
|
||||||
|
}
|
||||||
|
interactive = true;
|
||||||
|
|
||||||
|
// Reverse engineer the start time.
|
||||||
|
var now = new Date().getTime() * 0.001;
|
||||||
|
startTime = now - progress.saveTime;
|
||||||
|
};
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Setup
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
var begin = function () {
|
||||||
|
/* Set up the game when everything is loaded. */
|
||||||
|
|
||||||
|
// Handle storage.
|
||||||
|
if (hasLocalStorage()) {
|
||||||
|
var erase = $("#erase").click(function() {
|
||||||
|
doErase();
|
||||||
|
});
|
||||||
|
var save = $("#save").click(saveGame);
|
||||||
|
|
||||||
|
var storedCharacter = localStorage.getItem(getSaveId());
|
||||||
|
if (storedCharacter) {
|
||||||
|
try {
|
||||||
|
loadGame(JSON.parse(storedCharacter));
|
||||||
|
save.addClass('disabled')
|
||||||
|
erase.removeClass('disabled')
|
||||||
|
} catch(err) {
|
||||||
|
doErase(true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
startGame();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
startGame();
|
||||||
|
}
|
||||||
|
// Any point that an option list appears, its options are its
|
||||||
|
// first links.
|
||||||
|
$("body").on('click', "ul.options li, #menu li", function(event) {
|
||||||
|
// Make option clicks pass through to their first link.
|
||||||
|
var link = $("a", this);
|
||||||
|
if (link.length > 0) {
|
||||||
|
$(link.get(0)).click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
###
|
||||||
|
This is the control structure, it has minimal amount of data and
|
||||||
|
this data is volatile anyway (as in, it won't get saved).
|
||||||
|
###
|
||||||
|
Salet = {
|
||||||
|
rnd: null
|
||||||
|
time: 0
|
||||||
|
|
||||||
|
# Corresponding room names to room objects.
|
||||||
|
rooms: {}
|
||||||
|
|
||||||
|
# The unique id of the starting room.
|
||||||
|
start: "start"
|
||||||
|
|
||||||
|
# --- Hooks ---
|
||||||
|
|
||||||
|
###
|
||||||
|
This function is called at the start of the game. It is
|
||||||
|
normally overridden to provide initial character creation
|
||||||
|
(setting initial quality values, setting the
|
||||||
|
character-text. This is optional, however, as set-up
|
||||||
|
processing could also be done by the first situation's
|
||||||
|
enter function. If this function is given it should have
|
||||||
|
the signature function(character, system).
|
||||||
|
###
|
||||||
|
init: null
|
||||||
|
|
||||||
|
###
|
||||||
|
This function is called before entering any new
|
||||||
|
situation. It is called before the corresponding situation
|
||||||
|
has its `enter` method called.
|
||||||
|
###
|
||||||
|
entering: (character, system, oldSituationId, newSituationId) ->
|
||||||
|
|
||||||
|
###
|
||||||
|
Hook for when the situation has already been carried out
|
||||||
|
and printed.
|
||||||
|
###
|
||||||
|
afterEnter: (character, system, oldSituationId, newSituationId) ->
|
||||||
|
|
||||||
|
###
|
||||||
|
This function is called before carrying out any action in
|
||||||
|
any situation. It is called before the corresponding
|
||||||
|
situation has its `act` method called.
|
||||||
|
|
||||||
|
If the function returns true, then it is indicating that it
|
||||||
|
has consumed the action, and the action will not be passed
|
||||||
|
on to the situation. Note that this is the only one of
|
||||||
|
these global handlers that can consume the event.
|
||||||
|
###
|
||||||
|
beforeAction: (character, system, situationId, actionId) ->
|
||||||
|
|
||||||
|
###
|
||||||
|
This function is called after carrying out any action in
|
||||||
|
any situation. It is called after the corresponding
|
||||||
|
situation has its `act` method called.
|
||||||
|
###
|
||||||
|
afterAction: (character, system, situationId, actionId) ->
|
||||||
|
|
||||||
|
###
|
||||||
|
This function is called after leaving any situation. It is
|
||||||
|
called after the corresponding situation has its `exit`
|
||||||
|
method called.
|
||||||
|
###
|
||||||
|
exit: (character, system, oldSituationId, newSituationId) ->
|
||||||
|
|
||||||
|
###
|
||||||
|
Removes all content from the page, clearing the main content area.
|
||||||
|
|
||||||
|
If an elementSelector is given, then only that selector will be
|
||||||
|
cleared. Note that all content from the cleared element is removed,
|
||||||
|
but the element itself remains, ready to be filled again using
|
||||||
|
System.write.
|
||||||
|
###
|
||||||
|
clearContent: (elementSelector) ->
|
||||||
|
document.querySelector(elementSelector).innerHTML = ""
|
||||||
|
|
||||||
|
# jQuery was confused by this point where's the context so I did it vanilla-way
|
||||||
|
write: (content, elementSelector) ->
|
||||||
|
if content == ""
|
||||||
|
return
|
||||||
|
if typeof content == "function"
|
||||||
|
content = content()
|
||||||
|
block = document.getElementById("current-situation")
|
||||||
|
if block
|
||||||
|
block.innerHTML = block.innerHTML + markdown(content)
|
||||||
|
else
|
||||||
|
console.error("No current situation found.")
|
||||||
|
|
||||||
|
###
|
||||||
|
Turns any links that target the given href into plain
|
||||||
|
text. This can be used to remove action options when an action
|
||||||
|
is no longer available. It is used automatically when you give
|
||||||
|
a link the 'once' class.
|
||||||
|
###
|
||||||
|
clearLinks: (code) ->
|
||||||
|
$("a[href='" + code + "']").each((index, element) ->
|
||||||
|
a = $(element)
|
||||||
|
a.replaceWith($("<span>").addClass("ex_link").html(a.html()))
|
||||||
|
)
|
||||||
|
|
||||||
|
###
|
||||||
|
Given a list of situation ids, this outputs a standard option
|
||||||
|
block with the situation choices in the given order.
|
||||||
|
|
||||||
|
The contents of each choice will be a link to the situation,
|
||||||
|
the text of the link will be given by the situation's
|
||||||
|
outputText property. Note that the canChoose function is
|
||||||
|
called, and if it returns false, then the text will appear, but
|
||||||
|
the link will not be clickable.
|
||||||
|
|
||||||
|
Although canChoose is honored, canView and displayOrder are
|
||||||
|
not. If you need to honor these, you should either do so
|
||||||
|
manually, ot else use the `getSituationIdChoices` method to
|
||||||
|
return an ordered list of valid viewable situation ids.
|
||||||
|
###
|
||||||
|
writeChoices: (listOfIds, elementSelector) ->
|
||||||
|
if (listOfIds.length == 0)
|
||||||
|
return
|
||||||
|
|
||||||
|
currentSituation = getCurrentSituation();
|
||||||
|
$options = $("<ul>").addClass("options");
|
||||||
|
for (i = 0; i < listOfIds.length; ++i) {
|
||||||
|
situationId = listOfIds[i]
|
||||||
|
situation = game.situations[situationId]
|
||||||
|
assert(situation, "unknown_situation".l({id:situationId}))
|
||||||
|
if (situation == currentSituation) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
optionText = situation.optionText.fcall(this, character, this, currentSituation)
|
||||||
|
if (!optionText)
|
||||||
|
optionText = "choice".l({number:i+1})
|
||||||
|
$option = $("<li>")
|
||||||
|
$a = $("<span>")
|
||||||
|
if (situation.canChoose(character, this, currentSituation)) {
|
||||||
|
$a = $("<a>").attr({href: situationId})
|
||||||
|
}
|
||||||
|
$a.html(optionText)
|
||||||
|
$option.html($a)
|
||||||
|
$options.append($option)
|
||||||
|
}
|
||||||
|
@write($options, elementSelector)
|
||||||
|
|
||||||
|
###
|
||||||
|
Returns a list of situation ids to choose from, given a set of
|
||||||
|
specifications.
|
||||||
|
|
||||||
|
This function is a complex and powerful way of compiling
|
||||||
|
implicit situation choices. You give it a list of situation ids
|
||||||
|
and situation tags (if a single id or tag is needed just that
|
||||||
|
string can be given, it doesn't need to be wrapped in a
|
||||||
|
list). Tags should be prefixed with a hash # to differentiate
|
||||||
|
them from situation ids. The function then considers all
|
||||||
|
matching situations in descending priority order, calling their
|
||||||
|
canView functions and filtering out any that should not be
|
||||||
|
shown, given the current state. Without additional parameters
|
||||||
|
the function returns a list of the situation ids at the highest
|
||||||
|
level of priority that has any valid results. So, for example,
|
||||||
|
if a tag #places matches three situations, one with priority 2,
|
||||||
|
and two with priority 3, and all of them can be viewed in the
|
||||||
|
current context, then only the two with priority 3 will be
|
||||||
|
returned. This allows you to have high-priority situations that
|
||||||
|
trump any lower situations when they are valid, such as
|
||||||
|
situations that force the player to go to one destination if
|
||||||
|
the player is out of money, for example.
|
||||||
|
|
||||||
|
If a minChoices value is given, then the function will attempt
|
||||||
|
to return at least that many results. If not enough results are
|
||||||
|
available at the highest priority, then lower priorities will
|
||||||
|
be considered in turn, until enough situations are found. In
|
||||||
|
the example above, if we had a minChoices of three, then all
|
||||||
|
three situations would be returned, even though they have
|
||||||
|
different priorities. If you need to return all valid
|
||||||
|
situations, regardless of their priorities, set minChoices to a
|
||||||
|
large number, such as `Number.MAX_VALUE`, and leave maxChoices
|
||||||
|
undefined.
|
||||||
|
|
||||||
|
If a maxChoices value is given, then the function will not
|
||||||
|
return any more than the given number of results. If there are
|
||||||
|
more than this number of results possible, then the highest
|
||||||
|
priority resuls will be guaranteed to be returned, but the
|
||||||
|
lowest priority group will have to fight it out for the
|
||||||
|
remaining places. In this case, a random sample is chosen,
|
||||||
|
taking into account the frequency of each situation. So a
|
||||||
|
situation with a frequency of 100 will be chosen 100 times more
|
||||||
|
often than a situation with a frequency of 1, if there is one
|
||||||
|
space available. Often these frequencies have to be taken as a
|
||||||
|
guideline, and the actual probabilities will only be
|
||||||
|
approximate. Consider three situations with frequencies of 1,
|
||||||
|
1, 100, competing for two spaces. The 100-frequency situation
|
||||||
|
will be chosen almost every time, but for the other space, one
|
||||||
|
of the 1-frequency situations must be chosen. So the actual
|
||||||
|
probabilities will be roughly 50%, 50%, 100%. When selecting
|
||||||
|
more than one result, frequencies can only be a guide.
|
||||||
|
|
||||||
|
Before this function returns its result, it sorts the
|
||||||
|
situations in increasing order of their displayOrder values.
|
||||||
|
###
|
||||||
|
getSituationIdChoices: (listOfOrOneIdsOrTags, minChoices, maxChoices) ->
|
||||||
|
datum = null
|
||||||
|
i = 0
|
||||||
|
|
||||||
|
# First check if we have a single string for the id or tag.
|
||||||
|
if ($.type(listOfOrOneIdsOrTags) == 'string')
|
||||||
|
listOfOrOneIdsOrTags = [listOfOrOneIdsOrTags]
|
||||||
|
|
||||||
|
# First we build a list of all candidate ids.
|
||||||
|
allIds = {}
|
||||||
|
for (i = 0; i < listOfOrOneIdsOrTags.length; ++i)
|
||||||
|
tagOrId = listOfOrOneIdsOrTags[i]
|
||||||
|
if (tagOrId.substr(0, 1) == '#')
|
||||||
|
ids = getSituationIdsWithTag(tagOrId.substr(1))
|
||||||
|
for (j = 0; j < ids.length; ++j)
|
||||||
|
allIds[ids[j]] = true
|
||||||
|
else
|
||||||
|
allIds[tagOrId] = true
|
||||||
|
|
||||||
|
#Filter out anything that can't be viewed right now.
|
||||||
|
currentSituation = getCurrentSituation()
|
||||||
|
viewableSituationData = []
|
||||||
|
for (situationId in allIds)
|
||||||
|
situation = game.situations[situationId]
|
||||||
|
assert(situation, "unknown_situation".l({id:situationId}))
|
||||||
|
|
||||||
|
if (situation.canView(character, system, currentSituation))
|
||||||
|
#While we're here, get the selection data.
|
||||||
|
viewableSituationDatum = situation.choiceData(character, system, currentSituation)
|
||||||
|
viewableSituationDatum.id = situationId
|
||||||
|
viewableSituationData.push(viewableSituationDatum)
|
||||||
|
|
||||||
|
# Then we sort in descending priority order.
|
||||||
|
viewableSituationData.sort((a, b) ->
|
||||||
|
return b.priority - a.priority
|
||||||
|
)
|
||||||
|
|
||||||
|
committed = []
|
||||||
|
candidatesAtLastPriority = []
|
||||||
|
lastPriority
|
||||||
|
# In descending priority order.
|
||||||
|
for (i = 0; i < viewableSituationData.length; ++i)
|
||||||
|
datum = viewableSituationData[i];
|
||||||
|
if (datum.priority != lastPriority)
|
||||||
|
if (lastPriority !== undefined)
|
||||||
|
# We've dropped a priority group, see if we have enough
|
||||||
|
# situations so far, and stop if we do.
|
||||||
|
if (minChoices === undefined || i >= minChoices)
|
||||||
|
break
|
||||||
|
# Continue to acccumulate more options.
|
||||||
|
committed.push.apply(committed, candidatesAtLastPriority);
|
||||||
|
candidatesAtLastPriority = [];
|
||||||
|
lastPriority = datum.priority;
|
||||||
|
candidatesAtLastPriority.push(datum);
|
||||||
|
|
||||||
|
# So the values in committed we're committed to, because without
|
||||||
|
# them we wouldn't hit our minimum. But those in
|
||||||
|
# candidatesAtLastPriority might take us over our maximum, so
|
||||||
|
# figure out how many we should choose.
|
||||||
|
totalChoices = committed.length + candidatesAtLastPriority.length
|
||||||
|
if (maxChoices === undefined || maxChoices >= totalChoices)
|
||||||
|
# We can use all the choices.
|
||||||
|
committed.push.apply(committed, candidatesAtLastPriority)
|
||||||
|
else if (maxChoices >= committed.length)
|
||||||
|
# We can only use the commited ones.
|
||||||
|
# NO-OP
|
||||||
|
else
|
||||||
|
# We have to sample the candidates, using their relative frequency.
|
||||||
|
candidatesToInclude = maxChoices - committed.length;
|
||||||
|
for (i = 0; i < candidatesAtLastPriority.length; ++i)
|
||||||
|
datum = candidatesAtLastPriority[i];
|
||||||
|
datum._frequencyValue = this.rnd.random() / datum.frequency;
|
||||||
|
candidatesToInclude.sort((a, b) ->
|
||||||
|
return a.frequencyValue - b.frequencyValue;
|
||||||
|
)
|
||||||
|
chosen = candidatesToInclude.slice(0, candidatesToInclude)
|
||||||
|
committed.push.apply(committed, chosen)
|
||||||
|
|
||||||
|
# Now sort in ascending display order.
|
||||||
|
committed.sort((a, b) ->
|
||||||
|
return a.displayOrder - b.displayOrder
|
||||||
|
)
|
||||||
|
|
||||||
|
# And return as a list of ids only.
|
||||||
|
result = []
|
||||||
|
for (i = 0; i < committed.length; ++i)
|
||||||
|
result.push(committed[i].id)
|
||||||
|
return result
|
||||||
|
|
||||||
|
# This is the data on the player's progress that gets saved.
|
||||||
|
progress: {
|
||||||
|
# A random seed string, used internally to make random
|
||||||
|
# sequences predictable.
|
||||||
|
seed: null
|
||||||
|
# Keeps track of the links clicked, and when.
|
||||||
|
sequence: [],
|
||||||
|
# The time when the progress was saved.
|
||||||
|
saveTime: null
|
||||||
|
}
|
||||||
|
|
||||||
|
# The Id of the current situation the player is in.
|
||||||
|
current: null;
|
||||||
|
|
||||||
|
# This is the current character. It should be reconstructable
|
||||||
|
# from the above progress data.
|
||||||
|
character: {};
|
||||||
|
|
||||||
|
# Tracks whether we're in interactive mode or batch mode.
|
||||||
|
interactive: true
|
||||||
|
|
||||||
|
# The system time when the game was initialized.
|
||||||
|
startTime: null
|
||||||
|
|
||||||
|
# The stack of links, resulting from the last action, still be to resolved.
|
||||||
|
linkStack: null
|
||||||
|
|
||||||
|
getCurrentSituation: () =>
|
||||||
|
if (@current)
|
||||||
|
return @rooms[@current]
|
||||||
|
return null
|
||||||
|
|
||||||
|
# Gets the unique id used to identify saved games.
|
||||||
|
getSaveId: (slot = "") ->
|
||||||
|
return 'salet_'+game.id+'_'+game.version+'_'+slot;
|
||||||
|
|
||||||
|
# At last, we scroll the view so that .new objects are in view.
|
||||||
|
endOutputTransaction: () =>
|
||||||
|
if (!@interactive) return; # We're loading a save; do nothing at all.
|
||||||
|
$new = $('.new')
|
||||||
|
viewHeight = $(window).height()
|
||||||
|
newTop, newBottom, newHeight, optionHeight = 0
|
||||||
|
|
||||||
|
if ($new.length === 0) return; # Somehow, there's nothing new.
|
||||||
|
|
||||||
|
newTop = $new.first().offset().top,
|
||||||
|
newBottom = $new.last().offset().top + $new.last().height(),
|
||||||
|
newHeight = newBottom - newTop;
|
||||||
|
|
||||||
|
# We take the options list into account, because we don't want the new
|
||||||
|
# content to scroll offscreen when the list disappears. So we calculate
|
||||||
|
# scroll points as though the option list was already gone.
|
||||||
|
if ($('.options').not('.new').length)
|
||||||
|
optionHeight = $('.options').not('new').height()
|
||||||
|
|
||||||
|
if (newHeight > (viewHeight - optionHeight - 50))
|
||||||
|
# The new content is too long for our viewport, so we scroll the
|
||||||
|
# top of the new content to roughly 75% of the way up the viewport's
|
||||||
|
# height.
|
||||||
|
scrollTopTo(newTop-(viewHeight*0.25) - optionHeight);
|
||||||
|
else
|
||||||
|
if (newTop > $('body').height() - viewHeight)
|
||||||
|
# If we scroll right to the bottom, the new content will be in
|
||||||
|
# view. So we do that.
|
||||||
|
scrollToBottom();
|
||||||
|
else
|
||||||
|
# Our new content is too far up the page. So we scroll to place
|
||||||
|
# it somewhere near the bottom.
|
||||||
|
scrollBottomTo(newBottom+100 - optionHeight);
|
||||||
|
|
||||||
|
# This gets called when a link needs to be followed, regardless
|
||||||
|
# of whether it was user action that initiated it.
|
||||||
|
processLink: (code) =>
|
||||||
|
# Check if we should do this now, or if processing is already underway.
|
||||||
|
if (@linkStack !== null)
|
||||||
|
@linkStack.push(code)
|
||||||
|
return
|
||||||
|
|
||||||
|
$('.new').removeClass('new')
|
||||||
|
|
||||||
|
# We're processing, so make the stack available.
|
||||||
|
@linkStack = []
|
||||||
|
|
||||||
|
# Handle each link in turn.
|
||||||
|
processOneLink(code);
|
||||||
|
while (@linkStack.length > 0)
|
||||||
|
code = linkStack.shift()
|
||||||
|
processOneLink(code)
|
||||||
|
|
||||||
|
# We're done, so remove the stack to prevent future pushes.
|
||||||
|
@linkStack = null;
|
||||||
|
|
||||||
|
# Scroll to the top of the new content.
|
||||||
|
@endOutputTransaction()
|
||||||
|
|
||||||
|
# We're able to save, if we weren't already.
|
||||||
|
@enableSaving()
|
||||||
|
|
||||||
|
disableSaving: () ->
|
||||||
|
enableSaving: () ->
|
||||||
|
$("#save").removeClass('disabled');
|
||||||
|
|
||||||
|
/* This gets called to actually do the work of processing a code.
|
||||||
|
* When one doLink is called (or a link is clicked), this may set call
|
||||||
|
* code that further calls doLink, and so on. This method processes
|
||||||
|
* each one, and processLink manages this.
|
||||||
|
*/
|
||||||
|
var processOneLink = function(code) {
|
||||||
|
var match = code.match(linkRe);
|
||||||
|
assert(match, "link_not_valid".l({link:code}));
|
||||||
|
|
||||||
|
var situation = match[1];
|
||||||
|
var action = match[3];
|
||||||
|
|
||||||
|
// Change the situation
|
||||||
|
if (situation !== '.') {
|
||||||
|
if (situation !== current) {
|
||||||
|
doTransitionTo(situation);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// We should have an action if we have no situation change.
|
||||||
|
assert(action, "link_no_action".l());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Carry out the action
|
||||||
|
if (action) {
|
||||||
|
situation = getCurrentSituation();
|
||||||
|
if (situation) {
|
||||||
|
if (game.beforeAction) {
|
||||||
|
// Try the global act handler, and see if we need
|
||||||
|
// to notify the situation.
|
||||||
|
var consumed = game.beforeAction(
|
||||||
|
character, system, current, action
|
||||||
|
);
|
||||||
|
if (consumed !== true) {
|
||||||
|
situation.act(character, system, action);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// We have no global act handler, always notify
|
||||||
|
// the situation.
|
||||||
|
situation.act(character, system, action);
|
||||||
|
}
|
||||||
|
if (game.afterAction) {
|
||||||
|
game.afterAction(character, system, current, action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/* This gets called when the user clicks a link to carry out an
|
||||||
|
* action. */
|
||||||
|
var processClick = function(code) {
|
||||||
|
var now = (new Date()).getTime() * 0.001;
|
||||||
|
system.time = now - startTime;
|
||||||
|
progress.sequence.push({link:code, when:system.time});
|
||||||
|
processLink(code);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Transitions between situations. */
|
||||||
|
var doTransitionTo = function(newSituationId) {
|
||||||
|
var oldSituationId = current;
|
||||||
|
var oldSituation = getCurrentSituation();
|
||||||
|
var newSituation = game.situations[newSituationId];
|
||||||
|
|
||||||
|
assert(newSituation, "unknown_situation".l({id:newSituationId}));
|
||||||
|
|
||||||
|
// We might not have an old situation if this is the start of
|
||||||
|
// the game.
|
||||||
|
if (oldSituation) {
|
||||||
|
if (game.exit) {
|
||||||
|
game.exit(character, system, oldSituationId, newSituationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove links and transient sections.
|
||||||
|
$('#content a').each(function(index, element) {
|
||||||
|
var a = $(element);
|
||||||
|
if (a.hasClass('sticky') || a.attr("href").match(/[?&]sticky[=&]?/))
|
||||||
|
return;
|
||||||
|
a.replaceWith($("<span>").addClass("ex_link").html(a.html()));
|
||||||
|
});
|
||||||
|
var contentToHide = $('#content .transient, #content ul.options');
|
||||||
|
contentToHide.add($("#content a").filter(function(){
|
||||||
|
return $(this).attr("href").match(/[?&]transient[=&]?/);
|
||||||
|
}));
|
||||||
|
if (interactive) {
|
||||||
|
contentToHide.
|
||||||
|
animate({opacity: 0}, 350).
|
||||||
|
slideUp(500, function() {
|
||||||
|
$(this).remove();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
contentToHide.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move the character.
|
||||||
|
current = newSituationId;
|
||||||
|
|
||||||
|
// Notify the incoming situation.
|
||||||
|
if (game.enter) {
|
||||||
|
game.enter(character, system, oldSituationId, newSituationId);
|
||||||
|
}
|
||||||
|
newSituation.entering(character, system, oldSituationId);
|
||||||
|
|
||||||
|
// additional hook for when the situation text has already been printed
|
||||||
|
if (game.afterEnter) {
|
||||||
|
game.afterEnter(character, system, oldSituationId, newSituationId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
1617
lib/undum.js
1617
lib/undum.js
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue