New syntax with NPM module
This commit is contained in:
parent
bda0b84260
commit
5618f05a8f
|
@ -1,14 +1,10 @@
|
|||
room = require("../../lib/room.coffee")
|
||||
unit = require('../../lib/unit.coffee')
|
||||
dialogue = require('../../lib/util/dialogue.coffee')
|
||||
phrase = require('../../lib/util/phrase.coffee')
|
||||
dialogue = require('../../lib/dialogue.coffee')
|
||||
phrase = require('../../lib/phrase.coffee')
|
||||
oneOf = require('../../lib/oneOf.coffee')
|
||||
Salet = require('../../lib/salet.coffee')
|
||||
salet = require('salet')
|
||||
|
||||
salet = Salet({
|
||||
game_id: "your-game-id-here"
|
||||
game_version: "1.2"
|
||||
})
|
||||
salet.game_id = "your-game-id-here"
|
||||
salet.game_version = "1.3"
|
||||
$(document).ready(() ->
|
||||
window.addEventListener('popstate', (event) ->
|
||||
salet.goBack()
|
||||
|
|
|
@ -76,8 +76,8 @@
|
|||
</div> <!-- End of div.page -->
|
||||
|
||||
<!-- CDN JS Libraries -->
|
||||
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/marked/0.3.5/marked.min.js" integrity="sha384-QXBtGc4014gU26HdCwzgy8TVO+FHSSE4+EvPPiSTpdE9w0KyJy1ocfiIbBl1HLq7" crossorigin="anonymous"></script>
|
||||
<script type="text/javascript" src="//code.jquery.com/jquery-3.0.0.min.js"
|
||||
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/marked/0.3.5/marked.min.js" integrity="sha384-QXBtGc4014gU26HdCwzgy8TVO+FHSSE4+EvPPiSTpdE9w0KyJy1ocfiIbBl1HLq7" crossorigin="anonymous"></script>
|
||||
<script type="text/javascript" src="https://code.jquery.com/jquery-3.0.0.min.js"
|
||||
integrity="sha384-THPy051/pYDQGanwU6poAc/hOdQxjnOEXzbT+OuUAFqNqFjL+4IGLBgCJC3ZOShY" crossorigin="anonymous"></script>
|
||||
|
||||
<script type="text/javascript" src="game/bundle.js"></script>
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
invlink = (content, ref) ->
|
||||
return "<a href='./_inv_#{ref}' class='once'>#{content}</a>"
|
||||
|
||||
class Character
|
||||
constructor: (spec) ->
|
||||
@inventory = []
|
||||
|
||||
@take = (thing) =>
|
||||
@inventory.push thing
|
||||
@drop = (thing) =>
|
||||
for i in @inventory
|
||||
if i.name == thing
|
||||
index = @inventory.indexOf(thing)
|
||||
@inventory.splice(index, 1)
|
||||
|
||||
@has = (thing) =>
|
||||
for i in @inventory
|
||||
if i.name == thing
|
||||
return true
|
||||
return false
|
||||
|
||||
@listinv = (thing) =>
|
||||
for i in @inventory
|
||||
if i.name == thing
|
||||
return invlink(i.display, i.name)
|
||||
|
||||
@inv = (thing) =>
|
||||
for i in @inventory
|
||||
if i.name == thing
|
||||
return i.inv.fcall(i)
|
||||
|
||||
for index, value of spec
|
||||
this[index] = value
|
||||
return this
|
||||
|
||||
character = (spec) ->
|
||||
spec ?= {}
|
||||
return( new Character(spec) )
|
||||
|
||||
module.exports = character
|
|
@ -1,4 +1,3 @@
|
|||
room = require("../room.coffee")
|
||||
###
|
||||
A dialogue shortcut.
|
||||
Usage:
|
|
@ -1,62 +0,0 @@
|
|||
# Internationalization support
|
||||
|
||||
languages = {}
|
||||
|
||||
# Default Messages
|
||||
|
||||
en = {
|
||||
terrible: "terrible",
|
||||
poor: "poor",
|
||||
mediocre: "mediocre",
|
||||
fair: "fair",
|
||||
good: "good",
|
||||
great: "great",
|
||||
superb: "superb",
|
||||
yes: "yes",
|
||||
no: "no",
|
||||
choice: "Choice {number}",
|
||||
no_group_definition: "Couldn't find a group definition for {id}.",
|
||||
link_not_valid: "The link '{link}' doesn't appear to be valid.",
|
||||
link_no_action: "A link with a situation of '.', must have an action.",
|
||||
unknown_situation: "You can't move to an unknown situation: {id}.",
|
||||
existing_situation: "You can't override situation {id} in HTML.",
|
||||
erase_message: "This will permanently delete this character and immediately return you to the start of the game. Are you sure?",
|
||||
no_current_situation: "I can't display, because we don't have a current situation.",
|
||||
no_local_storage: "No local storage available.",
|
||||
random_seed_error: "You must provide a valid random seed.",
|
||||
random_error: "Initialize the Random with a non-empty seed before use.",
|
||||
dice_string_error: "Couldn't interpret your dice string: '{string}'."
|
||||
}
|
||||
|
||||
# Set this data as both the default fallback language, and the english preferred language.
|
||||
languages[""] = en
|
||||
languages["en"] = en
|
||||
|
||||
languageCodes = Object.keys(languages)
|
||||
|
||||
localize = (languageCode, message) ->
|
||||
for thisCode in languageCodes
|
||||
if languages[languageCode]?
|
||||
localized = languages[languageCode][message]
|
||||
if localized
|
||||
return localized
|
||||
return message
|
||||
|
||||
# API
|
||||
String.prototype.l = (args) ->
|
||||
# Get lang attribute from html tag.
|
||||
lang = document.getElementsByTagName("html")[0].getAttribute("lang") || ""
|
||||
|
||||
# Find the localized form.
|
||||
localized = localize(lang, this)
|
||||
if typeof(localized) == "function"
|
||||
localized = localized(args)
|
||||
else # Merge in any replacement content.
|
||||
if args
|
||||
for name in args
|
||||
localized = localized.replace(
|
||||
new RegExp("\\{"+name+"\\}"), args[name]
|
||||
)
|
||||
return localized
|
||||
|
||||
module.exports = languages;
|
|
@ -1,39 +0,0 @@
|
|||
###
|
||||
Indent normalization. Removes tabs AND spaces from every line beginning.
|
||||
Implies that you don't mix up your tabs and spaces.
|
||||
Copyright 2015 Bruno Dias
|
||||
###
|
||||
normaliseTabs = (text) ->
|
||||
if not text? or typeof(text) != "string" or text == ""
|
||||
return ""
|
||||
lines = text.split('\n');
|
||||
indents = lines
|
||||
.filter((l) => l != '')
|
||||
.map((l) => l.match(/^\s+/))
|
||||
.map((m) ->
|
||||
if (m == null)
|
||||
return ''
|
||||
return m[0]
|
||||
)
|
||||
smallestIndent = indents.reduce((max, curr) ->
|
||||
if (curr.length < max.length)
|
||||
return curr
|
||||
return max
|
||||
)
|
||||
if smallestIndent == ""
|
||||
return text
|
||||
return lines.map((l) ->
|
||||
return l.replace(new RegExp('^' + smallestIndent), '')
|
||||
).join('\n')
|
||||
|
||||
markdown = (text) ->
|
||||
unless text?
|
||||
return ""
|
||||
if typeof text is Function
|
||||
text = text()
|
||||
text = text.toString()
|
||||
return marked(normaliseTabs(text), {
|
||||
smartypants: true
|
||||
})
|
||||
|
||||
module.exports = markdown
|
|
@ -169,4 +169,4 @@ Array.prototype.oneOf = () ->
|
|||
String.prototype.oneOf = () ->
|
||||
return this
|
||||
|
||||
module.exports = oneOf;
|
||||
module.exports = oneOf
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
room = require("../room.coffee")
|
||||
###
|
||||
A phrase shortcut.
|
||||
Usage:
|
207
lib/random.js
207
lib/random.js
|
@ -1,207 +0,0 @@
|
|||
// Random Number generation based on seedrandom.js code by David Bau.
|
||||
// Copyright 2010 David Bau, all rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or
|
||||
// without modification, are permitted provided that the following
|
||||
// conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above
|
||||
// copyright notice, this list of conditions and the
|
||||
// following disclaimer.
|
||||
//
|
||||
// 2. Redistributions in binary form must reproduce the above
|
||||
// copyright notice, this list of conditions and the
|
||||
// following disclaimer in the documentation and/or other
|
||||
// materials provided with the distribution.
|
||||
//
|
||||
// 3. Neither the name of this module nor the names of its
|
||||
// contributors may be used to endorse or promote products
|
||||
// derived from this software without specific prior written
|
||||
// permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||
// NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
|
||||
// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
||||
// EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
var width = 256;
|
||||
var chunks = 6;
|
||||
var significanceExponent = 52;
|
||||
var startdenom = Math.pow(width, chunks);
|
||||
var significance = Math.pow(2, significanceExponent);
|
||||
var overflow = significance * 2;
|
||||
|
||||
var Random = (function () {
|
||||
var Random = function(seed) {
|
||||
this.random = null;
|
||||
if (!seed) throw {
|
||||
name: "RandomSeedError",
|
||||
message: "random_seed_error".l()
|
||||
};
|
||||
var key = [];
|
||||
mixkey(seed, key);
|
||||
var arc4 = new ARC4(key);
|
||||
this.random = function() {
|
||||
var n = arc4.g(chunks);
|
||||
var d = startdenom;
|
||||
var x = 0;
|
||||
while (n < significance) {
|
||||
n = (n + x) * width;
|
||||
d *= width;
|
||||
x = arc4.g(1);
|
||||
}
|
||||
while (n >= overflow) {
|
||||
n /= 2;
|
||||
d /= 2;
|
||||
x >>>= 1;
|
||||
}
|
||||
return (n + x) / d;
|
||||
};
|
||||
};
|
||||
// Helper type.
|
||||
var ARC4 = function(key) {
|
||||
var t, u, me = this, keylen = key.length;
|
||||
var i = 0, j = me.i = me.j = me.m = 0;
|
||||
me.S = [];
|
||||
me.c = [];
|
||||
if (!keylen) { key = [keylen++]; }
|
||||
while (i < width) { me.S[i] = i++; }
|
||||
for (i = 0; i < width; i++) {
|
||||
t = me.S[i];
|
||||
j = lowbits(j + t + key[i % keylen]);
|
||||
u = me.S[j];
|
||||
me.S[i] = u;
|
||||
me.S[j] = t;
|
||||
}
|
||||
me.g = function getnext(count) {
|
||||
var s = me.S;
|
||||
var i = lowbits(me.i + 1); var t = s[i];
|
||||
var j = lowbits(me.j + t); var u = s[j];
|
||||
s[i] = u;
|
||||
s[j] = t;
|
||||
var r = s[lowbits(t + u)];
|
||||
while (--count) {
|
||||
i = lowbits(i + 1); t = s[i];
|
||||
j = lowbits(j + t); u = s[j];
|
||||
s[i] = u;
|
||||
s[j] = t;
|
||||
r = r * width + s[lowbits(t + u)];
|
||||
}
|
||||
me.i = i;
|
||||
me.j = j;
|
||||
return r;
|
||||
};
|
||||
me.g(width);
|
||||
};
|
||||
// Helper functions.
|
||||
var mixkey = function(seed, key) {
|
||||
seed += '';
|
||||
var smear = 0;
|
||||
for (var j = 0; j < seed.length; j++) {
|
||||
var lb = lowbits(j);
|
||||
smear ^= key[lb];
|
||||
key[lb] = lowbits(smear*19 + seed.charCodeAt(j));
|
||||
}
|
||||
seed = '';
|
||||
for (j in key) {
|
||||
seed += String.fromCharCode(key[j]);
|
||||
}
|
||||
return seed;
|
||||
};
|
||||
var lowbits = function(n) {
|
||||
return n & (width - 1);
|
||||
};
|
||||
|
||||
return Random;
|
||||
})();
|
||||
|
||||
/* Returns a random floating point number between zero and
|
||||
* one. NB: The prototype implementation below just throws an
|
||||
* error, it will be overridden in each Random object when the
|
||||
* seed has been correctly configured. */
|
||||
Random.prototype.random = function() {
|
||||
throw {
|
||||
name:"RandomError",
|
||||
message: "random_error".l()
|
||||
};
|
||||
};
|
||||
/* Returns an integer between the given min and max values,
|
||||
* inclusive. */
|
||||
Random.prototype.randomInt = function(min, max) {
|
||||
return min + Math.floor((max-min+1)*this.random());
|
||||
};
|
||||
/* Returns the result of rolling n dice with dx sides, and adding
|
||||
* plus. */
|
||||
Random.prototype.dice = function(n, dx, plus) {
|
||||
var result = 0;
|
||||
for (var i = 0; i < n; i++) {
|
||||
result += this.randomInt(1, dx);
|
||||
}
|
||||
if (plus) result += plus;
|
||||
return result;
|
||||
};
|
||||
/* Returns the result of rolling n averaging dice (i.e. 6 sided dice
|
||||
* with sides 2,3,3,4,4,5). And adding plus. */
|
||||
Random.prototype.aveDice = (function() {
|
||||
var mapping = [2,3,3,4,4,5];
|
||||
return function(n, plus) {
|
||||
var result = 0;
|
||||
for (var i = 0; i < n; i++) {
|
||||
result += mapping[this.randomInt(0, 5)];
|
||||
}
|
||||
if (plus) result += plus;
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
/* Returns a dice-roll result from the given string dice
|
||||
* specification. The specification should be of the form xdy+z,
|
||||
* where the x component and z component are optional. This rolls
|
||||
* x dice of with y sides, and adds z to the result, the z
|
||||
* component can also be negative: xdy-z. The y component can be
|
||||
* either a number of sides, or can be the special values 'F', for
|
||||
* a fudge die (with 3 sides, +,0,-), '%' for a 100 sided die, or
|
||||
* 'A' for an averaging die (with sides 2,3,3,4,4,5).
|
||||
*/
|
||||
|
||||
Random.prototype.diceString = (function () {
|
||||
var diceRe = /^([1-9][0-9]*)?d([%FA]|[1-9][0-9]*)([-+][1-9][0-9]*)?$/;
|
||||
return function(def) {
|
||||
var match = def.match(diceRe);
|
||||
if (!match) {
|
||||
throw new Error(
|
||||
"dice_string_error".l({string:def})
|
||||
);
|
||||
}
|
||||
|
||||
var num = match[1]?parseInt(match[1], 10):1;
|
||||
var sides;
|
||||
var bonus = match[3]?parseInt(match[3], 10):0;
|
||||
|
||||
switch (match[2]) {
|
||||
case 'A':
|
||||
return this.aveDice(num, bonus);
|
||||
case 'F':
|
||||
sides = 3;
|
||||
bonus -= num*2;
|
||||
break;
|
||||
case '%':
|
||||
sides = 100;
|
||||
break;
|
||||
default:
|
||||
sides = parseInt(match[2], 10);
|
||||
break;
|
||||
}
|
||||
return this.dice(num, sides, bonus);
|
||||
};
|
||||
})();
|
||||
|
||||
module.exports = Random;
|
259
lib/room.coffee
259
lib/room.coffee
|
@ -1,259 +0,0 @@
|
|||
unit = require('./unit.coffee')
|
||||
markdown = require('./markdown.coffee')
|
||||
|
||||
assert = (msg, assertion) -> console.assert assertion, msg
|
||||
|
||||
Function.prototype.fcall = Function.prototype.call
|
||||
Boolean.prototype.fcall = () ->
|
||||
return this.valueOf()
|
||||
String.prototype.fcall = () ->
|
||||
return this.toString()
|
||||
|
||||
way_to = (content, ref) ->
|
||||
return "<a href='#{ref}' class='way' id='waylink-#{ref}'>#{content}</a>"
|
||||
|
||||
Array::remove = (e) -> @[t..t] = [] if (t = @indexOf(e)) > -1
|
||||
|
||||
class SaletRoom
|
||||
constructor: (spec) ->
|
||||
@visited = 0
|
||||
@title = "Room"
|
||||
@units = {}
|
||||
@canView = true
|
||||
@canChoose = true
|
||||
@priority = 1
|
||||
@displayOrder = 1
|
||||
@canSave = true
|
||||
@canExit = true
|
||||
@tags = []
|
||||
@choices = ""
|
||||
@optionText = "Choice"
|
||||
|
||||
# room illustration image, VN-style. Can be a GIF or WEBM. Can be a function.
|
||||
@pic = false
|
||||
@dsc = false # room description
|
||||
@extendSection = false
|
||||
@distance = Infinity # distance to the destination
|
||||
@clear = true # clear the screen on entering the room?
|
||||
###
|
||||
I call SaletRoom.exit every time the player exits to another room.
|
||||
Unlike @after this gets called after the section is closed.
|
||||
It's a styling difference.
|
||||
###
|
||||
@exit = (system, to) =>
|
||||
return true
|
||||
|
||||
###
|
||||
I call SaletRoom.enter every time the player enters this room but before the section is opened.
|
||||
Unlike @before this gets called before the current section is opened.
|
||||
It's a styling difference.
|
||||
|
||||
The upstream Undum version does not allow you to redefine @enter function easily but allows custom @exit one.
|
||||
It was renamed as @entering to achieve API consistency.
|
||||
###
|
||||
@enter = (system, from) =>
|
||||
return true
|
||||
|
||||
###
|
||||
Salet's Undum version calls Situation.entering every time a situation is entered, and
|
||||
passes it three arguments; The character object, the system object,
|
||||
and a string referencing the previous situation, or null if there is
|
||||
none (ie, for the starting situation).
|
||||
|
||||
My version of `enter` splits the location description from the effects.
|
||||
Also if f == this.name (we're in the same location) the `before` and `after` callbacks are ignored.
|
||||
###
|
||||
@entering = (system, f) =>
|
||||
if (
|
||||
f != @name and
|
||||
f? and
|
||||
system.rooms[f].canExit? and
|
||||
system.rooms[f].canExit == false
|
||||
)
|
||||
return system.goTo(f, f)
|
||||
|
||||
if @clear and f?
|
||||
system.view.clearContent()
|
||||
else
|
||||
system.view.removeTransient()
|
||||
|
||||
if f != @name and f?
|
||||
@visited++
|
||||
if system.rooms[f].exit?
|
||||
system.rooms[f].exit system, @name
|
||||
|
||||
history.pushState(@name, @title)
|
||||
|
||||
if @enter
|
||||
@enter system, f
|
||||
|
||||
if not @extendSection
|
||||
classes = if @classes then ' ' + @classes.join(' ') else ''
|
||||
room = document.getElementById('current-room')
|
||||
if room?
|
||||
room.removeAttribute('id')
|
||||
system.view.append "<section id='current-room' data-room='#{@name}' class='room-#{@name}#{classes}'></section>"
|
||||
|
||||
if f != @name and @before?
|
||||
system.view.write markdown(@before.fcall(this, system, f))
|
||||
|
||||
system.view.write @look system, f
|
||||
|
||||
if f != @name and @after?
|
||||
system.view.write markdown(@after.fcall(this, system, f))
|
||||
|
||||
if @beforeChoices?
|
||||
@beforeChoices.fcall(this, system, f)
|
||||
|
||||
if @choices
|
||||
system.view.writeChoices(system, system.getSituationIdChoices(@choices, @maxChoices))
|
||||
|
||||
if @afterChoices?
|
||||
@afterChoices.fcall(this, system, f)
|
||||
|
||||
if system.autosave and @canSave
|
||||
system.saveGame()
|
||||
|
||||
###
|
||||
An internal function to get the room's description and the descriptions of
|
||||
every unit in this room.
|
||||
###
|
||||
@look = (system, f) =>
|
||||
system.view.updateWays(system, @ways, @name)
|
||||
retval = ""
|
||||
|
||||
if @pic
|
||||
retval += '<div class="pic">'+system.view.pictureTag(@pic.fcall(this, system, f))+'</div>'
|
||||
|
||||
# Print the room description
|
||||
if @dsc and @dsc != ""
|
||||
dsc = @dsc.fcall(this, system, f).toString()
|
||||
retval += markdown(dsc)
|
||||
|
||||
unitDescriptions = []
|
||||
for thing in @units
|
||||
if thing.name and typeof(thing.look) == "function" and thing.look(system, f)
|
||||
unitDescriptions.push ({
|
||||
order: thing.order,
|
||||
content: thing.look(system, f)
|
||||
})
|
||||
|
||||
unitDescriptions.sort((a, b) ->
|
||||
return a.order - b.order
|
||||
)
|
||||
|
||||
for description in unitDescriptions
|
||||
retval += description.content
|
||||
|
||||
return retval
|
||||
|
||||
###
|
||||
Places a unit in this room.
|
||||
###
|
||||
@take = (thing) =>
|
||||
@units.push(thing)
|
||||
|
||||
@drop = (name) =>
|
||||
for thing in @units
|
||||
if thing.name == name
|
||||
@units.splice(@units.indexOf(thing), 1)
|
||||
return @units
|
||||
|
||||
###
|
||||
Unit action. A function or a string which comes when you click on a link in unit description.
|
||||
You could interpret this as an EXAMINE verb or USE one, it's your call.
|
||||
###
|
||||
@act = (system, action) =>
|
||||
if (link = action.match(/^_(act|cycle|inv)_(.+)$/)) #unit action
|
||||
if link[1] == "inv"
|
||||
return system.view.write system.character.inv(link[2])
|
||||
for thing in @units
|
||||
if thing.name == link[2]
|
||||
if link[1] == "act"
|
||||
# If it's takeable, the player can take this unit.
|
||||
# If not, we check the "act" function.
|
||||
if thing.takeable
|
||||
system.character.take(thing)
|
||||
@drop link[2]
|
||||
return system.view.write(thing.take.fcall(thing, system).toString())
|
||||
else if thing.act?
|
||||
return system.view.write thing.act.fcall(thing, system)
|
||||
|
||||
# the loop is done but no return came - match not found
|
||||
console.error("Could not find #{link[2]} in current room.")
|
||||
|
||||
# we're done with units, now check the regular actions
|
||||
actionClass = action.match(/^_(\w+)_(.+)$/)
|
||||
that = this
|
||||
|
||||
responses = {
|
||||
writer: (ref) ->
|
||||
content = that.writers[ref].fcall(that, system, action)
|
||||
output = markdown(content)
|
||||
system.view.write(output)
|
||||
replacer: (ref) ->
|
||||
content = that.writers[ref].fcall(that, system, action)
|
||||
system.view.replace(content, '#'+ref)
|
||||
inserter: (ref) ->
|
||||
content = that.writers[ref].fcall(that, system, action)
|
||||
output = markdown(content)
|
||||
system.view.write(output, '#'+ref)
|
||||
}
|
||||
|
||||
if (actionClass)
|
||||
# Matched a special action class
|
||||
[responder, ref] = [actionClass[1], actionClass[2]]
|
||||
|
||||
if(!@writers.hasOwnProperty(actionClass[2]))
|
||||
throw new Error("Tried to call undefined writer: #{action}");
|
||||
responses[responder](ref);
|
||||
else if (@actions.hasOwnProperty(action))
|
||||
@actions[action].call(this, system, action);
|
||||
else
|
||||
throw new Error("Tried to call undefined action: #{action}");
|
||||
|
||||
# Marks every room in the game with distance to this room
|
||||
@destination = () =>
|
||||
@distance = 0
|
||||
|
||||
candidates = [this]
|
||||
while candidates.length > 0
|
||||
current_room = candidates.shift()
|
||||
if current_room.ways
|
||||
for node in current_room.ways
|
||||
if node.distance == Infinity
|
||||
node.distance = current_room.distance + 1
|
||||
candidates.push(node)
|
||||
|
||||
@register = (salet) =>
|
||||
if not @name?
|
||||
console.error("Situation has no name")
|
||||
return this
|
||||
salet.rooms[@name] = this
|
||||
return this
|
||||
|
||||
@writers = {
|
||||
cyclewriter: (salet) =>
|
||||
responses = @cycle
|
||||
if typeof responses == "function"
|
||||
responses = responses()
|
||||
cycleIndex = window.localStorage.getItem("cycleIndex")
|
||||
cycleIndex ?= 0
|
||||
response = responses[cycleIndex]
|
||||
cycleIndex++
|
||||
if cycleIndex == responses.length
|
||||
cycleIndex = 0
|
||||
window.localStorage.setItem("cycleIndex", cycleIndex)
|
||||
return salet.view.cycleLink(response)
|
||||
}
|
||||
|
||||
for index, value of spec
|
||||
this[index] = value
|
||||
return this
|
||||
|
||||
room = (name, salet, spec) ->
|
||||
spec ?= {}
|
||||
spec.name = name
|
||||
return new SaletRoom(spec).register(salet)
|
||||
|
||||
module.exports = room
|
460
lib/salet.coffee
460
lib/salet.coffee
|
@ -1,460 +0,0 @@
|
|||
markdown = require('./markdown.coffee')
|
||||
SaletView = require('./view.coffee')
|
||||
Random = require('./random.js')
|
||||
Character = require('./character.coffee')
|
||||
require('./localize.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
|
||||
Boolean.prototype.fcall = () ->
|
||||
return this
|
||||
String.prototype.fcall = () ->
|
||||
return this
|
||||
|
||||
assert = (msg, assertion) -> console.assert assertion, msg
|
||||
|
||||
###
|
||||
This is the control structure, it has minimal amount of data and
|
||||
this data is volatile anyway (as in, it won't get saved).
|
||||
|
||||
There is only one instance of this class.
|
||||
###
|
||||
class Salet
|
||||
constructor: (spec) ->
|
||||
@character = new Character
|
||||
|
||||
# REDEFINE THIS IN YOUR GAME
|
||||
@game_id = null
|
||||
@game_version = "1.0"
|
||||
@autosave = true
|
||||
|
||||
@rnd = null
|
||||
@time = 0
|
||||
|
||||
# Corresponding room names to room objects.
|
||||
@rooms = {}
|
||||
|
||||
# The unique id of the starting room.
|
||||
@start = "start"
|
||||
|
||||
# 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_-]+))?$/
|
||||
|
||||
###
|
||||
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.
|
||||
###
|
||||
@init = () ->
|
||||
|
||||
###
|
||||
This function is called before entering any new
|
||||
situation. It is called before the corresponding situation
|
||||
has its `enter` method called.
|
||||
###
|
||||
@enter = (oldSituationId, newSituationId) ->
|
||||
|
||||
###
|
||||
Hook for when the situation has already been carried out
|
||||
and printed.
|
||||
###
|
||||
@afterEnter = (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 = (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 = (situationId, actionId) ->
|
||||
|
||||
###
|
||||
This function is called after leaving any situation. It is
|
||||
called after the corresponding situation has its `exit`
|
||||
method called.
|
||||
###
|
||||
@exit = (oldSituationId, newSituationId) ->
|
||||
|
||||
###
|
||||
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 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.
|
||||
|
||||
Before this function returns its result, it sorts the
|
||||
situations in increasing order of their displayOrder values.
|
||||
###
|
||||
@getSituationIdChoices = (listOfOrOneIdsOrTags, maxChoices) =>
|
||||
datum = null
|
||||
i = 0
|
||||
|
||||
# First check if we have a single string for the id or tag.
|
||||
if (typeof(listOfOrOneIdsOrTags) == 'string')
|
||||
listOfOrOneIdsOrTags = [listOfOrOneIdsOrTags]
|
||||
|
||||
# First we build a list of all candidate ids.
|
||||
allIds = []
|
||||
for tagOrId in listOfOrOneIdsOrTags
|
||||
if (tagOrId.substr(0, 1) == '#')
|
||||
ids = @getRoomsTagged(tagOrId.substr(1))
|
||||
for id in ids
|
||||
allIds.push(id)
|
||||
else #it's an id, not a tag
|
||||
allIds.push(tagOrId)
|
||||
|
||||
#Filter out anything that can't be viewed right now.
|
||||
currentRoom = @getCurrentRoom()
|
||||
viewableRoomData = []
|
||||
for roomId in allIds
|
||||
room = @rooms[roomId]
|
||||
assert(room, "unknown_situation".l({id:roomId}))
|
||||
|
||||
if (room.canView.fcall(this, this, currentRoom, room))
|
||||
viewableRoomData.push({
|
||||
priority: room.priority
|
||||
id: roomId
|
||||
displayOrder: room.displayOrder
|
||||
})
|
||||
|
||||
# Then we sort in descending priority order.
|
||||
viewableRoomData.sort((a, b) ->
|
||||
return b.priority - a.priority
|
||||
)
|
||||
|
||||
committed = []
|
||||
|
||||
# if we need to filter out the results
|
||||
if (maxChoices? && viewableRoomData.length > maxChoices)
|
||||
viewableRoomData = viewableRoomData[-maxChoices..]
|
||||
|
||||
for candidateRoom in viewableRoomData
|
||||
committed.push({
|
||||
id: candidateRoom.id
|
||||
displayOrder: candidateRoom.displayOrder
|
||||
})
|
||||
|
||||
# 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 in committed
|
||||
result.push(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: [],
|
||||
# Keeps track of the rooms visited, for when we want to "go back"
|
||||
path: [],
|
||||
# The time when the progress was saved.
|
||||
saveTime: null
|
||||
}
|
||||
|
||||
# The Id of the current room the player is in.
|
||||
@current = null
|
||||
|
||||
# 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
|
||||
|
||||
@getCurrentRoom = () =>
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
@view.mark_all_links_old
|
||||
|
||||
# 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.
|
||||
@view.endOutputTransaction()
|
||||
|
||||
# We're able to save, if we weren't already.
|
||||
@view.enableSaving()
|
||||
|
||||
@goTo = (roomId) =>
|
||||
return @processClick(roomId)
|
||||
|
||||
###
|
||||
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) =>
|
||||
match = code.match(@linkRe)
|
||||
if not match
|
||||
console.error "link_not_valid".l()
|
||||
console.error code
|
||||
|
||||
situation = match[1]
|
||||
action = match[3]
|
||||
|
||||
# Change the situation
|
||||
if situation != '.' and situation != @current_room
|
||||
@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)
|
||||
room = @getCurrentRoom()
|
||||
if room
|
||||
consumed = false
|
||||
|
||||
if @beforeAction
|
||||
# Try the global act handler
|
||||
consumed = @beforeAction(room, action)
|
||||
|
||||
if consumed != true
|
||||
room.act(this, action)
|
||||
|
||||
if @afterAction
|
||||
@afterAction(room, action)
|
||||
|
||||
# This gets called when the user clicks a link to carry out an action.
|
||||
@processClick = (code) =>
|
||||
now = (new Date()).getTime() * 0.001
|
||||
@time = now - @startTime
|
||||
@progress.sequence.push({link:code, when:@time})
|
||||
if @getRoom(code)? # if it's a room
|
||||
@progress.path.push(code)
|
||||
@processLink(code)
|
||||
|
||||
# Go back N rooms. It's not an UNDO.
|
||||
# Also, steps = 1 is the current room
|
||||
@goBack = (steps = 2) =>
|
||||
window.history.back()
|
||||
if @progress.path.length == 1
|
||||
location = @start
|
||||
else if @progress.path.length > steps
|
||||
location = @progress.path[@progress.path.length - steps]
|
||||
else
|
||||
return @goBack(steps - 1)
|
||||
@processClick(location)
|
||||
|
||||
# Transition between rooms.
|
||||
@doTransitionTo = (newRoomId) =>
|
||||
oldRoomId = @current
|
||||
oldRoom = @getCurrentRoom()
|
||||
newRoom = @rooms[newRoomId]
|
||||
|
||||
assert(newRoom, "unknown_situation".l({id:newRoomId}))
|
||||
|
||||
# We might not have an old situation if this is the start of the game.
|
||||
if (oldRoom and @exit)
|
||||
@exit(oldRoomId, newRoomId)
|
||||
|
||||
@current = newRoomId
|
||||
|
||||
# Remove links and transient sections.
|
||||
@view.removeTransient(@interactive)
|
||||
|
||||
# Notify the incoming situation.
|
||||
if (@enter)
|
||||
@enter(oldRoomId, newRoomId)
|
||||
newRoom.entering(this, oldRoomId)
|
||||
|
||||
# additional hook for when the situation text has already been printed
|
||||
if (@afterEnter)
|
||||
@afterEnter(oldRoomId, newRoomId)
|
||||
|
||||
###
|
||||
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.
|
||||
###
|
||||
@eraseSave = (force = false) =>
|
||||
saveId = @getSaveId() # save slot
|
||||
if (localStorage.getItem(saveId) and (force or confirm("erase_message".l())))
|
||||
localStorage.removeItem(saveId)
|
||||
window.location.reload()
|
||||
|
||||
# Find and return a list of ids for all situations with the given tag.
|
||||
@getRoomsTagged = (tag) =>
|
||||
result = []
|
||||
for id, room of @rooms
|
||||
for i in room.tags
|
||||
if (i == tag)
|
||||
result.push(id)
|
||||
break
|
||||
return result
|
||||
|
||||
# Saves the character and the walking history to local storage.
|
||||
@saveGame = () =>
|
||||
# Store when we're saving the game, to avoid exploits where a
|
||||
# player loads their file to gain extra time.
|
||||
now = (new Date()).getTime() * 0.001
|
||||
@progress.saveTime = now - @startTime
|
||||
|
||||
# Save the game.
|
||||
localStorage.setItem(@getSaveId(), JSON.stringify({
|
||||
progress: @progress
|
||||
}))
|
||||
|
||||
# Switch the button highlights.
|
||||
@view.disableSaving()
|
||||
@view.enableErasing()
|
||||
@view.enableLoading()
|
||||
|
||||
# Loads the game from the given data
|
||||
@loadGame = (saveFile) =>
|
||||
@progress = saveFile.progress
|
||||
@character = new Character
|
||||
|
||||
@rnd = new Random(@progress.seed)
|
||||
|
||||
# Start the game
|
||||
@init()
|
||||
|
||||
# Run through all the player's history.
|
||||
@interactive = false
|
||||
for step in @progress.sequence
|
||||
# The action must be done at the recorded time.
|
||||
@time = step.when
|
||||
@processLink(step.link)
|
||||
@interactive = true
|
||||
|
||||
# Reverse engineer the start time.
|
||||
now = new Date().getTime() * 0.001
|
||||
startTime = now - @progress.saveTime
|
||||
|
||||
@view = new SaletView
|
||||
|
||||
@beginGame = () =>
|
||||
@view.fixClicks()
|
||||
|
||||
# Handle storage.
|
||||
saveFile = false
|
||||
if (@view.hasLocalStorage())
|
||||
saveFile = localStorage.getItem(@getSaveId())
|
||||
|
||||
if (saveFile)
|
||||
try
|
||||
@loadGame(JSON.parse(saveFile))
|
||||
@view.disableSaving()
|
||||
@view.enableErasing()
|
||||
catch err
|
||||
console.log "There was an error loading your save. The save will be overwritten."
|
||||
console.error err
|
||||
else
|
||||
@progress.seed = new Date().toString()
|
||||
|
||||
@rnd = new Random(@progress.seed)
|
||||
@progress.sequence = [{link:@start, when:0}]
|
||||
|
||||
# Start the game
|
||||
@startTime = new Date().getTime() * 0.001
|
||||
@init()
|
||||
|
||||
# Do the first state.
|
||||
@doTransitionTo(@start)
|
||||
|
||||
@getRoom = (name) =>
|
||||
if @rooms[name]?
|
||||
return @rooms[name]
|
||||
return undefined
|
||||
|
||||
# Just an alias for getCurrentRoom
|
||||
@here = () => @getCurrentRoom()
|
||||
|
||||
@isVisited = (name) =>
|
||||
place = @getRoom(name)
|
||||
if place
|
||||
return Boolean place.visited
|
||||
return 0
|
||||
|
||||
for index, value of spec
|
||||
this[index] = value
|
||||
|
||||
return this
|
||||
|
||||
salet = (spec) ->
|
||||
spec ?= {}
|
||||
retval = new Salet(spec)
|
||||
retval.view.init(retval)
|
||||
return retval
|
||||
|
||||
module.exports = salet
|
|
@ -1,56 +0,0 @@
|
|||
markdown = require('./markdown.coffee')
|
||||
#require('./salet.coffee')
|
||||
unitlink = (content, ref) ->
|
||||
return "<a href='./_act_#{ref}' class='once'>#{content}</a>"
|
||||
|
||||
Array::remove = (e) -> @[t..t] = [] if (t = @indexOf(e)) > -1
|
||||
|
||||
parsedsc = (text, name) ->
|
||||
window.unitname = name
|
||||
text = text.replace /\{\{(.+)\}\}/g, (str, p1) ->
|
||||
name = window.unitname
|
||||
window.unitname = undefined
|
||||
return unitlink(p1, name)
|
||||
return text
|
||||
|
||||
# A unit class.
|
||||
# A unit cannot be in several locations at once, you must clone the variable.
|
||||
class SaletUnit
|
||||
constructor: (spec) ->
|
||||
unless spec.name?
|
||||
console.error("Trying to create a unit with no name")
|
||||
return null
|
||||
|
||||
@order = 0 # you can use this to sort the descriptions
|
||||
@visible = true
|
||||
@look = (system, f) =>
|
||||
if @dsc and @dsc != "" and @visible
|
||||
text = markdown(@dsc.fcall(this, system, f).toString())
|
||||
# replace braces {{}} with link to _act_
|
||||
return parsedsc(text, @name)
|
||||
@takeable = false
|
||||
@take = (system) => "You take the #{@name}." # taking to inventory
|
||||
@act = (system) => "You don't find anything extraordinary about the #{@name}." # unit action
|
||||
@dsc = (system) => "You see a {{#{@name}}} here." # unit description
|
||||
@inv = (system) => "It's a #{@name}." # inventory description
|
||||
@location = ""
|
||||
@put = (salet, location) =>
|
||||
@level = 0 # this is scenery
|
||||
if salet.rooms[location]?
|
||||
@location = location
|
||||
salet.rooms[location].take(this)
|
||||
else
|
||||
console.log("Could not find location #{location} for a unit #{@name}")
|
||||
@delete = (salet, location = false) =>
|
||||
if location == false
|
||||
location = @location
|
||||
salet.rooms[location].drop(this)
|
||||
|
||||
for key, value of spec
|
||||
this[key] = value
|
||||
|
||||
unit = (name, spec) ->
|
||||
spec ?= {}
|
||||
spec.name = name
|
||||
return new SaletUnit(spec)
|
||||
module.exports = unit
|
305
lib/view.coffee
305
lib/view.coffee
|
@ -1,305 +0,0 @@
|
|||
markdown = require('./markdown.coffee')
|
||||
###
|
||||
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.
|
||||
You don't need to call this module from the game directly.
|
||||
|
||||
The abstraction goal here is to provide the author with a freedom to style his
|
||||
game as he wants to. The save and erase buttons are not necessary buttons,
|
||||
but they could be something else entirely. (That's why IDs are hardcoded.)
|
||||
|
||||
There is only one instance of this class, and it's stored as `salet.view`.
|
||||
###
|
||||
|
||||
assert = (msg, assertion) -> console.assert assertion, msg
|
||||
|
||||
way_to = (content, ref) ->
|
||||
return "<a href='#{ref}' class='way'>#{content}</a>"
|
||||
|
||||
addClass = (element, className) ->
|
||||
if (element.classList)
|
||||
element.classList.add(className)
|
||||
else
|
||||
element.className += ' ' + className
|
||||
|
||||
class SaletView
|
||||
init: (salet) =>
|
||||
$("#page").on("click", "a", (event) ->
|
||||
event.preventDefault()
|
||||
a = $(this)
|
||||
href = a.attr('href')
|
||||
if a.hasClass("once") || href.match(/[?&]once[=&]?/)
|
||||
salet.view.clearLinks(href)
|
||||
if href.match(salet.linkRe)
|
||||
salet.processClick(href)
|
||||
)
|
||||
$("#load").on("click", "a", (event) ->
|
||||
window.location.reload()
|
||||
)
|
||||
if (@hasLocalStorage())
|
||||
$("#erase").click((event) ->
|
||||
event.preventDefault()
|
||||
return salet.eraseSave()
|
||||
)
|
||||
$("#save").click((event) ->
|
||||
event.preventDefault()
|
||||
return salet.saveGame()
|
||||
)
|
||||
|
||||
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
|
||||
intro = document.getElementById("intro")
|
||||
if intro?
|
||||
intro.innerHTML = ""
|
||||
document.querySelector(elementSelector).innerHTML = ""
|
||||
|
||||
prepareContent: (content) ->
|
||||
if typeof content == "function"
|
||||
content = content()
|
||||
if content instanceof jQuery
|
||||
content = content[0].outerHTML
|
||||
return content.toString()
|
||||
|
||||
# Write content to current room
|
||||
write: (content, elementSelector = "#current-room") =>
|
||||
if not content? or content == ""
|
||||
return
|
||||
content = @prepareContent(content)
|
||||
block = document.querySelector(elementSelector)
|
||||
if block
|
||||
block.innerHTML = block.innerHTML + markdown(content)
|
||||
else
|
||||
# most likely this is the starting room
|
||||
block = document.getElementById("content")
|
||||
block.innerHTML = content
|
||||
|
||||
# Append content to a block. Does not replace the old content.
|
||||
append: (content, elementSelector = "#content") =>
|
||||
if content == ""
|
||||
return
|
||||
content = @prepareContent(content)
|
||||
block = document.querySelector(elementSelector)
|
||||
block.innerHTML = block.innerHTML + markdown(content)
|
||||
|
||||
# Replaces the text in the given block with the given text.
|
||||
# !! Does not call markdown on the provided text. !!
|
||||
replace: (content, elementSelector) =>
|
||||
if content == ""
|
||||
return
|
||||
content = @prepareContent(content)
|
||||
block = document.querySelector(elementSelector)
|
||||
block.innerHTML = content
|
||||
###
|
||||
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 $("#page").find("a[href='" + code + "']")
|
||||
html = a.innerHTML
|
||||
a = $(a)
|
||||
a.replaceWith($("<span>").addClass("ex_link").html(html))
|
||||
return true
|
||||
|
||||
###
|
||||
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: (salet, listOfIds) =>
|
||||
if (not listOfIds? or listOfIds.length == 0)
|
||||
return
|
||||
|
||||
currentRoom = salet.getCurrentRoom()
|
||||
$options = $("<ul>").addClass("options")
|
||||
for roomId in listOfIds
|
||||
room = salet.rooms[roomId]
|
||||
assert(room, "unknown_situation".l({id:roomId}))
|
||||
if (room == currentRoom)
|
||||
continue
|
||||
|
||||
optionText = room.optionText.fcall(salet, currentRoom)
|
||||
if (!optionText)
|
||||
optionText = "choice".l({number:i+1})
|
||||
$option = $("<li>")
|
||||
$a = $("<span>")
|
||||
if (room.canChoose.fcall(this, salet, currentRoom))
|
||||
$a = $("<a>").attr({href: roomId})
|
||||
$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)
|
||||
removeTransient: (interactive = false) ->
|
||||
for a in $('#content').find('a')
|
||||
a = $(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);
|
||||
|
||||
# Feature detection
|
||||
hasLocalStorage: () ->
|
||||
return window.localStorage?
|
||||
|
||||
# Any point that an option list appears, its options are its first links.
|
||||
fixClicks: () ->
|
||||
$("body").on('click', "ul.options li", (event) ->
|
||||
# Make option clicks pass through to their first link.
|
||||
link = $("a", this)
|
||||
if (link.length > 0)
|
||||
$(link.get(0)).click()
|
||||
)
|
||||
|
||||
showBlock: (selector) ->
|
||||
block = document.querySelector(selector)
|
||||
if block
|
||||
block.style.display = "block"
|
||||
hideBlock: (selector) ->
|
||||
block = document.querySelector(selector)
|
||||
if block
|
||||
block.style.display = "none"
|
||||
updateWays: (salet, ways, name) ->
|
||||
if document.getElementById("ways") == null
|
||||
return
|
||||
content = ""
|
||||
distances = []
|
||||
if ways then for way in ways
|
||||
if salet.rooms[way]?
|
||||
title = salet.rooms[way].title.fcall(this, name)
|
||||
content += "<li class='nav-item'><a class='nav-link' href='#{way}'>#{title}</a></li>"
|
||||
distances.push({
|
||||
key: way
|
||||
distance: salet.rooms[way].distance
|
||||
})
|
||||
@showBlock(".ways #ways_hint")
|
||||
else
|
||||
@hideBlock(".ways #ways_hint")
|
||||
document.getElementById("ways").innerHTML = content
|
||||
min = Infinity
|
||||
min_key = []
|
||||
for node in distances
|
||||
if node.distance < min
|
||||
min = node.distance
|
||||
min_key = [node.key]
|
||||
if node.distance == min
|
||||
min_key.push(node.key)
|
||||
if min < Infinity
|
||||
for node in min_key
|
||||
waylink = document.getElementById("waylink-#{node}")
|
||||
if waylink
|
||||
addClass(waylink, "destination")
|
||||
|
||||
pictureTag: (picture) ->
|
||||
extension = picture.substr((~-picture.lastIndexOf(".") >>> 0) + 2)
|
||||
if (extension == "webm")
|
||||
return """
|
||||
<video src="#{picture}" controls>
|
||||
Your browser does not support the video tag for some reason.
|
||||
You won't be able to view this video in this browser.
|
||||
</video>
|
||||
"""
|
||||
return "<img class='img-responsive' src='#{picture}' alt='Room illustration'>"
|
||||
|
||||
cycleLink: (content) ->
|
||||
return "<a href='./_replacer_cyclewriter' class='cycle' id='cyclewriter'>#{content}</a>"
|
||||
|
||||
module.exports = SaletView
|
|
@ -1,5 +1,7 @@
|
|||
{
|
||||
"dependencies": {},
|
||||
"dependencies": {
|
||||
"salet": "^1.3.3"
|
||||
},
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"babelify": "^7.2.0",
|
||||
|
|
Loading…
Reference in a new issue