Did a crude MVC structure.
This commit is contained in:
parent
c48c1ff11e
commit
5f7f142028
|
@ -4,7 +4,7 @@ obj = require('../../lib/obj.coffee')
|
|||
dialogue = require('../../lib/dialogue.coffee')
|
||||
oneOf = require('../../lib/oneOf.coffee')
|
||||
require('../../lib/interface.coffee')
|
||||
undum = require('../../lib/undum.js')
|
||||
salet = require('../../lib/salet.coffee')
|
||||
|
||||
undum.game.id = "your-game-id-here"
|
||||
undum.game.version = "1.0"
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
###
|
||||
Salet interface configuration.
|
||||
###
|
||||
$(document).ready(() ->
|
||||
$("#ways").on("click", "a", (event) ->
|
||||
event.preventDefault()
|
||||
undum.processClick($(this).attr("href"))
|
||||
)
|
||||
$("#inventory").on("click", "a", (event) ->
|
||||
event.preventDefault()
|
||||
)
|
||||
$("#load").on("click", "a", (event) ->
|
||||
window.location.reload()
|
||||
)
|
||||
)
|
|
@ -1,5 +1,5 @@
|
|||
markdown = require('./markdown.coffee')
|
||||
undum = require('./undum.js')
|
||||
salet = require('./salet.coffee')
|
||||
objlink = (content, ref) ->
|
||||
return "<a href='./_act_#{ref}' class='once'>#{content}</a>"
|
||||
|
||||
|
@ -25,7 +25,7 @@ class SaletObj
|
|||
level: 0
|
||||
look: (character, system, f) =>
|
||||
if @dsc
|
||||
text = markdown(@dsc.fcall(this, character, system, f))
|
||||
text = markdown(@dsc.fcall(this, character, system, f))
|
||||
text = "<span class='look lvl#{@level}'>" + text + "</span>"
|
||||
# replace braces {{}} with link to _act_
|
||||
return parsedsc(text, @name)
|
||||
|
|
|
@ -6,19 +6,6 @@ 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
|
||||
|
||||
|
@ -40,16 +27,14 @@ cls = (system) ->
|
|||
update_ways = (ways, name) ->
|
||||
content = ""
|
||||
distances = []
|
||||
if ways
|
||||
document.querySelector(".ways h2").style.display = "block"
|
||||
for way in ways
|
||||
if undum.game.situations[way]?
|
||||
title = undum.game.situations[way].title.fcall(this, name)
|
||||
content += way_to(title, way)
|
||||
distances.push({
|
||||
key: way
|
||||
distance: undum.game.situations[way].distance
|
||||
})
|
||||
if ways then for way in ways
|
||||
if undum.game.situations[way]?
|
||||
title = undum.game.situations[way].title.fcall(this, name)
|
||||
content += way_to(title, way)
|
||||
distances.push({
|
||||
key: way
|
||||
distance: undum.game.situations[way].distance
|
||||
})
|
||||
else
|
||||
document.querySelector(".ways h2").style.display = "none"
|
||||
document.getElementById("ways").innerHTML = content
|
||||
|
@ -85,7 +70,7 @@ class SaletRoom extends undum.Situation
|
|||
visited: 0
|
||||
title: "Room"
|
||||
objects: {}
|
||||
|
||||
|
||||
# room illustration image, VN-style. Can be a GIF or WEBM. Can be a function.
|
||||
pic: false
|
||||
|
||||
|
@ -103,7 +88,7 @@ class SaletRoom extends undum.Situation
|
|||
distance: Infinity # distance to the destination
|
||||
clear: true # clear the screen on entering the room?
|
||||
|
||||
entering: (character, system, from) =>
|
||||
entering: (character, system, from) =>
|
||||
|
||||
###
|
||||
I call SaletRoom.exit every time the player exits to another room.
|
||||
|
@ -148,13 +133,13 @@ class SaletRoom extends undum.Situation
|
|||
current_situation = ""
|
||||
if not @extendSection
|
||||
classes = if @classes then ' ' + @classes.join(' ') else ''
|
||||
situation = document.getElementById('current-situation')
|
||||
situation = document.getElementById('current-room')
|
||||
if situation?
|
||||
situation.removeAttribute('id')
|
||||
# Javascript DOM manipulation functions like jQuery's append() or document.createElement
|
||||
# don't work like a typical printLn - they create *DOM nodes*.
|
||||
# You can't leave an unclosed tag just like that. So we have to buffer the output.
|
||||
current_situation = "<section id='current-situation' data-situation='#{@name}' class='situation-#{@name}#{classes}'>"
|
||||
current_situation = "<section id='current-room' data-situation='#{@name}' class='situation-#{@name}#{classes}'>"
|
||||
|
||||
if f != @name and @before?
|
||||
current_situation += markdown(@before.fcall(this, character, system, f))
|
||||
|
@ -236,7 +221,7 @@ class SaletRoom extends undum.Situation
|
|||
writer: (ref) ->
|
||||
content = that.writers[ref].fcall(that, character, system, action)
|
||||
output = markdown(content)
|
||||
system.writeInto(output, '#current-situation')
|
||||
system.writeInto(output, '#current-room')
|
||||
replacer: (ref) ->
|
||||
content = that.writers[ref].fcall(that, character, system, action)
|
||||
output = "<span>"+content+"</span>" # <p> tags are usually bad for replacers
|
||||
|
|
287
lib/salet.coffee
287
lib/salet.coffee
|
@ -1,4 +1,5 @@
|
|||
markdown = require('./markdown.coffee')
|
||||
view = require('./view.coffee')
|
||||
|
||||
###
|
||||
fcall() (by analogy with fmap) is added to the prototypes of both String and
|
||||
|
@ -7,14 +8,15 @@ 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; }
|
||||
String.prototype.fcall = () ->
|
||||
return this
|
||||
|
||||
# Utility functions
|
||||
|
||||
parseFn = (str) ->
|
||||
unless str?
|
||||
return str
|
||||
|
||||
|
||||
fstr = """
|
||||
(function(character, system, situation) {
|
||||
#{str}
|
||||
|
@ -22,17 +24,16 @@ parseFn = (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());
|
||||
# Feature detection
|
||||
hasLocalStorage = () ->
|
||||
hasStorage = false
|
||||
try
|
||||
hasStorage = ('localStorage' in window) &&
|
||||
window.localStorage != null &&
|
||||
window.localStorage != undefined;
|
||||
catch err
|
||||
hasStorage = false
|
||||
return hasStorage
|
||||
|
||||
# Regular expression to catch every link action.
|
||||
# Salet's default is a general URL-safe expression.
|
||||
|
@ -77,8 +78,6 @@ Salet = {
|
|||
# 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
|
||||
|
@ -93,21 +92,21 @@ Salet = {
|
|||
###
|
||||
This function is called before entering any new
|
||||
situation. It is called before the corresponding situation
|
||||
has its `enter` method called.
|
||||
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
|
||||
|
@ -129,88 +128,10 @@ Salet = {
|
|||
###
|
||||
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
|
||||
|
@ -229,7 +150,7 @@ Salet = {
|
|||
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
|
||||
|
@ -240,7 +161,7 @@ Salet = {
|
|||
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
|
||||
|
@ -258,7 +179,7 @@ Salet = {
|
|||
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.
|
||||
###
|
||||
|
@ -272,27 +193,26 @@ Salet = {
|
|||
|
||||
# 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
|
||||
for tagOrId in listOfOrOneIdsOrTags
|
||||
if (tagOrId.substr(0, 1) == '#')
|
||||
ids = getSituationIdsWithTag(tagOrId.substr(1))
|
||||
for id in ids
|
||||
allIds[id] = 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}))
|
||||
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)
|
||||
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) ->
|
||||
|
@ -303,26 +223,25 @@ Salet = {
|
|||
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);
|
||||
for datum in viewableSituationData
|
||||
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)
|
||||
if (maxChoices == undefined || maxChoices >= totalChoices)
|
||||
# We can use all the choices.
|
||||
committed.push.apply(committed, candidatesAtLastPriority)
|
||||
else if (maxChoices >= committed.length)
|
||||
|
@ -331,8 +250,7 @@ Salet = {
|
|||
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];
|
||||
for datum in candidatesAtLastPriority
|
||||
datum._frequencyValue = this.rnd.random() / datum.frequency;
|
||||
candidatesToInclude.sort((a, b) ->
|
||||
return a.frequencyValue - b.frequencyValue;
|
||||
|
@ -347,8 +265,8 @@ Salet = {
|
|||
|
||||
# And return as a list of ids only.
|
||||
result = []
|
||||
for (i = 0; i < committed.length; ++i)
|
||||
result.push(committed[i].id)
|
||||
for i in committed
|
||||
result.push(i.id)
|
||||
return result
|
||||
|
||||
# This is the data on the player's progress that gets saved.
|
||||
|
@ -387,49 +305,15 @@ Salet = {
|
|||
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)
|
||||
if @linkStack != null
|
||||
@linkStack.push(code)
|
||||
return
|
||||
|
||||
$('.new').removeClass('new')
|
||||
view.mark_all_links_old
|
||||
|
||||
# We're processing, so make the stack available.
|
||||
@linkStack = []
|
||||
|
@ -449,18 +333,13 @@ Salet = {
|
|||
# We're able to save, if we weren't already.
|
||||
@enableSaving()
|
||||
|
||||
disableSaving: () ->
|
||||
$("#save").addClass('disabled');
|
||||
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.
|
||||
###
|
||||
processOneLink: = (code) ->
|
||||
processOneLink: (code) ->
|
||||
match = code.match(linkRe)
|
||||
assert(match, "link_not_valid".l({link:code}))
|
||||
|
||||
|
@ -468,8 +347,8 @@ Salet = {
|
|||
action = match[3]
|
||||
|
||||
# Change the situation
|
||||
if (situation !== '.')
|
||||
if (situation !== current)
|
||||
if situation != '.'
|
||||
if situation != current
|
||||
doTransitionTo(situation)
|
||||
else
|
||||
# We should have an action if we have no situation change.
|
||||
|
@ -490,7 +369,7 @@ Salet = {
|
|||
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)
|
||||
|
||||
|
@ -515,23 +394,7 @@ Salet = {
|
|||
game.exit(character, system, oldSituationId, newSituationId);
|
||||
|
||||
# Remove links and transient sections.
|
||||
$('#content a').each((index, element) ->
|
||||
a = $(element);
|
||||
if (a.hasClass('sticky') || a.attr("href").match(/[?&]sticky[=&]?/))
|
||||
return;
|
||||
a.replaceWith($("<span>").addClass("ex_link").html(a.html()))
|
||||
)
|
||||
contentToHide = $('#content .transient, #content ul.options')
|
||||
contentToHide.add($("#content a").filter(() ->
|
||||
return $(this).attr("href").match(/[?&]transient[=&]?/)
|
||||
))
|
||||
if (interactive)
|
||||
contentToHide.animate({opacity: 0}, 350).
|
||||
slideUp(500, () ->
|
||||
$(this).remove()
|
||||
)
|
||||
else
|
||||
contentToHide.remove()
|
||||
view.remove_transient(@interactive)
|
||||
|
||||
# Move the character.
|
||||
current = newSituationId
|
||||
|
@ -550,7 +413,7 @@ Salet = {
|
|||
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. */
|
||||
no longer has to be the end-all be-all repository of game state.
|
||||
###
|
||||
erase_save: (force = false) =>
|
||||
save_id = @getSaveId() # save slot
|
||||
|
@ -561,9 +424,9 @@ Salet = {
|
|||
# Find and return a list of ids for all situations with the given tag.
|
||||
getSituationIdsWithTag: (tag) =>
|
||||
result = []
|
||||
for (situationId, situation of @situations)
|
||||
for (i = 0; i < situation.tags.length; ++i)
|
||||
if (situation.tags[i] == tag)
|
||||
for situationId, situation of @situations
|
||||
for i in situation.tags
|
||||
if (i == tag)
|
||||
result.push(situationId)
|
||||
break
|
||||
return result
|
||||
|
@ -579,20 +442,18 @@ Salet = {
|
|||
localStorage.setItem(getSaveId(), JSON.stringify(progress))
|
||||
|
||||
# Switch the button highlights.
|
||||
$("#erase").removeClass('disabled')
|
||||
$("#load").removeClass('disabled')
|
||||
@disableSaving()
|
||||
view.disableSaving()
|
||||
view.enableErasing()
|
||||
view.enableLoading()
|
||||
|
||||
# Loads the game from the given data
|
||||
loadGame = (characterData) ->
|
||||
loadGame: (characterData) ->
|
||||
progress = characterData
|
||||
|
||||
character = new Character()
|
||||
system.rnd = new Random(progress.seed)
|
||||
|
||||
# Empty the display
|
||||
$("#content").empty()
|
||||
$("#intro").empty()
|
||||
view.clearContent()
|
||||
|
||||
# Now play through the actions so far:
|
||||
if (game.init)
|
||||
|
@ -600,8 +461,7 @@ Salet = {
|
|||
|
||||
# Run through all the player's history.
|
||||
interactive = false
|
||||
for (i = 0; i < progress.sequence.length; i++)
|
||||
step = progress.sequence[i]
|
||||
for step in progress.sequence
|
||||
# The action must be done at the recorded time.
|
||||
system.time = step.when
|
||||
processLink(step.link)
|
||||
|
@ -620,9 +480,9 @@ Salet = {
|
|||
if (storedCharacter)
|
||||
try
|
||||
@loadGame(JSON.parse(storedCharacter))
|
||||
@disableSaving()
|
||||
$("#erase").removeClass('disabled')
|
||||
catch(err)
|
||||
view.disableSaving()
|
||||
view.enableErasing()
|
||||
catch err
|
||||
@erase_save(true)
|
||||
else
|
||||
progress.seed = new Date().toString()
|
||||
|
@ -631,8 +491,7 @@ Salet = {
|
|||
system.rnd = new Random(progress.seed)
|
||||
progress.sequence = [{link:game.start, when:0}]
|
||||
|
||||
# Empty the display
|
||||
$("#content").empty()
|
||||
view.clearContent()
|
||||
|
||||
# Start the game
|
||||
startTime = new Date().getTime() * 0.001
|
||||
|
|
184
lib/view.coffee
Normal file
184
lib/view.coffee
Normal file
|
@ -0,0 +1,184 @@
|
|||
###
|
||||
Salet interface configuration.
|
||||
In a typical MVC structure, this is the View.
|
||||
Only it knows about the DOM structure.
|
||||
Other modules just use its API or prepare the HTML for insertion.
|
||||
###
|
||||
|
||||
class SaletView
|
||||
init: () ->
|
||||
$("#ways").on("click", "a", (event) ->
|
||||
event.preventDefault()
|
||||
undum.processClick($(this).attr("href"))
|
||||
)
|
||||
$("#inventory").on("click", "a", (event) ->
|
||||
event.preventDefault()
|
||||
)
|
||||
$("#load").on("click", "a", (event) ->
|
||||
window.location.reload()
|
||||
)
|
||||
|
||||
disableSaving: () ->
|
||||
$("#save").addClass('disabled')
|
||||
enableSaving: () ->
|
||||
$("#save").removeClass('disabled')
|
||||
enableErasing: () ->
|
||||
$("#erase").removeClass('disabled')
|
||||
disableErasing: () ->
|
||||
$("#erase").addClass('disabled')
|
||||
enableLoading: () ->
|
||||
$("#load").removeClass('disabled')
|
||||
disableLoading: () ->
|
||||
$("#load").addClass('disabled')
|
||||
|
||||
# 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());
|
||||
|
||||
###
|
||||
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 @write.
|
||||
###
|
||||
clearContent: (elementSelector = "#content") ->
|
||||
if (elementSelector == "#content") # empty the intro with the content
|
||||
document.findElementById("intro").innerHTML = ""
|
||||
document.querySelector(elementSelector).innerHTML = ""
|
||||
|
||||
# Write content to current room
|
||||
write: (content) ->
|
||||
if content == ""
|
||||
return
|
||||
if typeof content == "function"
|
||||
content = content()
|
||||
block = document.getElementById("current-room")
|
||||
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) ->
|
||||
for a in $("a[href='" + code + "']")
|
||||
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) ->
|
||||
if (listOfIds.length == 0)
|
||||
return
|
||||
|
||||
currentSituation = getCurrentSituation();
|
||||
$options = $("<ul>").addClass("options");
|
||||
for situationId in listOfIds
|
||||
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)
|
||||
|
||||
# Marks all links as old. This gets called in a `processLink` function.
|
||||
mark_all_links_old: () ->
|
||||
$('.new').removeClass('new')
|
||||
|
||||
# Removes links and transient sections.
|
||||
# Arguments:
|
||||
# interactive - if we're working in interactive mode (or we're loading a save)
|
||||
remove_transient: (interactive = false) ->
|
||||
for a in $('#content a')
|
||||
if (a.hasClass('sticky') || a.attr("href").match(/[?&]sticky[=&]?/))
|
||||
return;
|
||||
a.replaceWith($("<span>").addClass("ex_link").html(a.html()))
|
||||
contentToHide = $('#content .transient, #content ul.options')
|
||||
contentToHide.add($("#content a").filter(() ->
|
||||
return $(this).attr("href").match(/[?&]transient[=&]?/)
|
||||
))
|
||||
if (interactive)
|
||||
contentToHide.animate({opacity: 0}, 350).
|
||||
slideUp(500, () ->
|
||||
$(this).remove()
|
||||
)
|
||||
else
|
||||
contentToHide.remove()
|
||||
|
||||
# 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);
|
||||
|
||||
view = new SaletView
|
||||
$(document).ready(() ->
|
||||
view.init()
|
||||
)
|
||||
|
||||
module.exports = view
|
Loading…
Reference in a new issue