mirror of
https://gitlab.com/Oreolek/salet.git
synced 2024-07-07 01:04:25 +03:00
Salet is now passed as an argument.
Also, no frequency filters on choice list.
This commit is contained in:
parent
566c8b27a2
commit
5e7f4087aa
|
@ -3,7 +3,15 @@ room = require("../../lib/room.coffee")
|
||||||
obj = require('../../lib/obj.coffee')
|
obj = require('../../lib/obj.coffee')
|
||||||
dialogue = require('../../lib/dialogue.coffee')
|
dialogue = require('../../lib/dialogue.coffee')
|
||||||
oneOf = require('../../lib/oneOf.coffee')
|
oneOf = require('../../lib/oneOf.coffee')
|
||||||
require('../../lib/salet.coffee')
|
Salet = require('../../lib/salet.coffee')
|
||||||
|
|
||||||
|
salet = new Salet
|
||||||
|
salet.view.init(salet)
|
||||||
|
salet.game_id = "your-game-id-here"
|
||||||
|
salet.game_version = "1.0"
|
||||||
|
$(document).ready(() ->
|
||||||
|
salet.beginGame()
|
||||||
|
)
|
||||||
|
|
||||||
###
|
###
|
||||||
Element helpers. There is no real need to build monsters like a().id("hello")
|
Element helpers. There is no real need to build monsters like a().id("hello")
|
||||||
|
@ -25,7 +33,7 @@ cyclelink = (content) ->
|
||||||
|
|
||||||
# The first room of the game.
|
# The first room of the game.
|
||||||
# For accessibility reasons the text is provided in HTML, not here.
|
# For accessibility reasons the text is provided in HTML, not here.
|
||||||
room "start",
|
room "start", salet,
|
||||||
dsc: """
|
dsc: """
|
||||||
""",
|
""",
|
||||||
choices: "#start"
|
choices: "#start"
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
# This is where you initialize your game.
|
|
||||||
|
|
||||||
$(document).ready(() ->
|
|
||||||
salet.game_id = "your-game-id-here"
|
|
||||||
salet.game_version = "1.0"
|
|
||||||
salet.beginGame()
|
|
||||||
)
|
|
|
@ -1,4 +1,4 @@
|
||||||
room "world",
|
room "world", salet,
|
||||||
tags: ["start"],
|
tags: ["start"],
|
||||||
optionText: "Enter the world",
|
optionText: "Enter the world",
|
||||||
ways: ["plaza"]
|
ways: ["plaza"]
|
||||||
|
@ -13,7 +13,7 @@ room "world",
|
||||||
dsc: "A steep narrow {{well}} proceeds upward."
|
dsc: "A steep narrow {{well}} proceeds upward."
|
||||||
act: "There is only one passage out. See the „Other rooms“ block popped up? Click it."
|
act: "There is only one passage out. See the „Other rooms“ block popped up? Click it."
|
||||||
|
|
||||||
room "plaza",
|
room "plaza", salet,
|
||||||
title: (from) ->
|
title: (from) ->
|
||||||
if from == "world"
|
if from == "world"
|
||||||
return "Upwards"
|
return "Upwards"
|
||||||
|
@ -44,7 +44,7 @@ room "plaza",
|
||||||
dsc: "There are {{people shouting}} nearby."
|
dsc: "There are {{people shouting}} nearby."
|
||||||
act: 'Just some weirdos shouting "Viva la Cthulhu!". Typical.'
|
act: 'Just some weirdos shouting "Viva la Cthulhu!". Typical.'
|
||||||
|
|
||||||
room "shop",
|
room "shop", salet,
|
||||||
title: "The Shop"
|
title: "The Shop"
|
||||||
#pic: "http://loremflickr.com/640/300/room,shop"
|
#pic: "http://loremflickr.com/640/300/room,shop"
|
||||||
ways: ["plaza", "shop-inside", "lair"]
|
ways: ["plaza", "shop-inside", "lair"]
|
||||||
|
@ -55,7 +55,7 @@ room "shop",
|
||||||
You are standing in front of a picturesque sign. It's cold here.
|
You are standing in front of a picturesque sign. It's cold here.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
room "lair",
|
room "lair", salet,
|
||||||
title: "The Lair"
|
title: "The Lair"
|
||||||
before: "Finding The Lair is easy. Leaving it is impossible. Your game ends here."
|
before: "Finding The Lair is easy. Leaving it is impossible. Your game ends here."
|
||||||
dsc: """
|
dsc: """
|
||||||
|
@ -69,11 +69,11 @@ room "lair",
|
||||||
here().drop(@name)
|
here().drop(@name)
|
||||||
return "You eat the bugg mass. Delicious and raw. Perhaps it's a good lair to live in."
|
return "You eat the bugg mass. Delicious and raw. Perhaps it's a good lair to live in."
|
||||||
|
|
||||||
dialogue "Yes", "merchant", "merchant", """
|
dialogue "Yes", salet, "merchant", "merchant", """
|
||||||
Yes.
|
Yes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
room "shop-inside",
|
room "shop-inside", salet,
|
||||||
ways: ["shop"]
|
ways: ["shop"]
|
||||||
tags: ["merchant"]
|
tags: ["merchant"]
|
||||||
optionText: "End the conversation"
|
optionText: "End the conversation"
|
||||||
|
@ -100,7 +100,7 @@ lamp = obj "lamp",
|
||||||
lamp.put("shop-inside")
|
lamp.put("shop-inside")
|
||||||
###
|
###
|
||||||
|
|
||||||
room "merchdialogue",
|
room "merchdialogue", salet,
|
||||||
choices: "#merchant",
|
choices: "#merchant",
|
||||||
dsc: """
|
dsc: """
|
||||||
Nice day, isn't it?
|
Nice day, isn't it?
|
||||||
|
|
|
@ -15,8 +15,8 @@ Usage:
|
||||||
Point out a thing in her purse (mildly)
|
Point out a thing in her purse (mildly)
|
||||||
""", "character.sandbox.mild = true"
|
""", "character.sandbox.mild = true"
|
||||||
###
|
###
|
||||||
dialogue = (title, startTag, endTag, text, effect) ->
|
dialogue = (title, salet, startTag, endTag, text, effect) ->
|
||||||
retval = room(randomid(), {
|
retval = room(randomid(), salet, {
|
||||||
optionText: title
|
optionText: title
|
||||||
dsc: text
|
dsc: text
|
||||||
clear: false # backlog is useful in dialogues
|
clear: false # backlog is useful in dialogues
|
||||||
|
|
|
@ -4,7 +4,7 @@ Implies that you don't mix up your tabs and spaces.
|
||||||
Copyright 2015 Bruno Dias
|
Copyright 2015 Bruno Dias
|
||||||
###
|
###
|
||||||
normaliseTabs = (text) ->
|
normaliseTabs = (text) ->
|
||||||
unless text?
|
unless text? and typeof(text) == "string"
|
||||||
return ""
|
return ""
|
||||||
lines = text.split('\n');
|
lines = text.split('\n');
|
||||||
indents = lines
|
indents = lines
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
# 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.
|
||||||
|
|
||||||
|
require('./salet.coffee')
|
||||||
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')
|
|
||||||
require('./salet.coffee')
|
|
||||||
|
|
||||||
# Assertion
|
# Assertion
|
||||||
assert = console.assert
|
assert = (msg, assertion) -> console.assert assertion, msg
|
||||||
|
|
||||||
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>"
|
||||||
|
@ -21,43 +19,6 @@ addClass = (element, className) ->
|
||||||
else
|
else
|
||||||
element.className += ' ' + className
|
element.className += ' ' + className
|
||||||
|
|
||||||
update_ways = (ways, name) ->
|
|
||||||
content = ""
|
|
||||||
distances = []
|
|
||||||
if ways then for way in ways
|
|
||||||
if salet.rooms[way]?
|
|
||||||
title = salet.rooms[way].title.fcall(this, name)
|
|
||||||
content += way_to(title, way)
|
|
||||||
distances.push({
|
|
||||||
key: way
|
|
||||||
distance: salet.rooms[way].distance
|
|
||||||
})
|
|
||||||
else
|
|
||||||
document.querySelector(".ways h2").style.display = "none"
|
|
||||||
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
|
|
||||||
addClass(document.getElementById("waylink-#{node}"), "destination")
|
|
||||||
|
|
||||||
picture_tag = (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'>"
|
|
||||||
|
|
||||||
class SaletRoom
|
class SaletRoom
|
||||||
constructor: (spec) ->
|
constructor: (spec) ->
|
||||||
for index, value of spec
|
for index, value of spec
|
||||||
|
@ -73,7 +34,6 @@ class SaletRoom
|
||||||
canView: true
|
canView: true
|
||||||
canChoose: true
|
canChoose: true
|
||||||
priority: 1
|
priority: 1
|
||||||
frequency: 1
|
|
||||||
displayOrder: 1
|
displayOrder: 1
|
||||||
tags: []
|
tags: []
|
||||||
choices: ""
|
choices: ""
|
||||||
|
@ -84,14 +44,14 @@ class SaletRoom
|
||||||
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) =>
|
entering: (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.
|
||||||
It's a styling difference.
|
It's a styling difference.
|
||||||
###
|
###
|
||||||
exit: (character, system, to) =>
|
exit: (system, to) =>
|
||||||
return true
|
return true
|
||||||
|
|
||||||
###
|
###
|
||||||
|
@ -102,7 +62,7 @@ class SaletRoom
|
||||||
The upstream Undum version does not allow you to redefine @enter function easily but allows custom @exit one.
|
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.
|
It was renamed as @entering to achieve API consistency.
|
||||||
###
|
###
|
||||||
enter: (character, system, from) =>
|
enter: (system, from) =>
|
||||||
return true
|
return true
|
||||||
|
|
||||||
###
|
###
|
||||||
|
@ -148,25 +108,25 @@ class SaletRoom
|
||||||
if not @extendSection
|
if not @extendSection
|
||||||
room_content += "</section>"
|
room_content += "</section>"
|
||||||
|
|
||||||
system.write(room_content)
|
system.view.write(room_content)
|
||||||
|
|
||||||
if @choices
|
if @choices
|
||||||
system.writeChoices(system.getSituationIdChoices(@choices, @minChoices, @maxChoices))
|
system.view.writeChoices(system, system.getSituationIdChoices(@choices, @maxChoices))
|
||||||
|
|
||||||
###
|
###
|
||||||
An internal function to get the room's description and the descriptions of
|
An internal function to get the room's description and the descriptions of
|
||||||
every object in this room.
|
every object in this room.
|
||||||
###
|
###
|
||||||
look: (character, system, f) =>
|
look: (system, f) =>
|
||||||
update_ways(@ways, @name)
|
system.view.updateWays(system, @ways, @name)
|
||||||
retval = ""
|
retval = ""
|
||||||
|
|
||||||
if @pic
|
if @pic
|
||||||
retval += '<div class="pic">'+picture_tag(@pic.fcall(this, character, system, f))+'</div>'
|
retval += '<div class="pic">'+system.view.pictureTag(@pic.fcall(this, system, f))+'</div>'
|
||||||
|
|
||||||
# Print the room description
|
# Print the room description
|
||||||
if @dsc
|
if @dsc
|
||||||
retval += markdown(@dsc.fcall(this, character, system, f))
|
retval += markdown(@dsc.fcall(this, system, f))
|
||||||
|
|
||||||
for name, thing of @objects
|
for name, thing of @objects
|
||||||
retval += thing.look()
|
retval += thing.look()
|
||||||
|
@ -189,7 +149,7 @@ class SaletRoom
|
||||||
Object action. A function or a string which comes when you click on the object link.
|
Object action. A function or a string which comes when you click on the object link.
|
||||||
You could interpret this as an EXAMINE verb or USE one, it's your call.
|
You could interpret this as an EXAMINE verb or USE one, it's your call.
|
||||||
###
|
###
|
||||||
act: (character, system, action) =>
|
act: (system, action) =>
|
||||||
if (link = action.match(/^_(act|cycle)_(.+)$/)) #object action
|
if (link = action.match(/^_(act|cycle)_(.+)$/)) #object action
|
||||||
for name, thing of @objects
|
for name, thing of @objects
|
||||||
if name == link[2]
|
if name == link[2]
|
||||||
|
@ -215,15 +175,15 @@ class SaletRoom
|
||||||
|
|
||||||
responses = {
|
responses = {
|
||||||
writer: (ref) ->
|
writer: (ref) ->
|
||||||
content = that.writers[ref].fcall(that, character, system, action)
|
content = that.writers[ref].fcall(that, system, action)
|
||||||
output = markdown(content)
|
output = markdown(content)
|
||||||
system.writeInto(output, '#current-room')
|
system.writeInto(output, '#current-room')
|
||||||
replacer: (ref) ->
|
replacer: (ref) ->
|
||||||
content = that.writers[ref].fcall(that, character, system, action)
|
content = that.writers[ref].fcall(that, system, action)
|
||||||
output = "<span>"+content+"</span>" # <p> tags are usually bad for replacers
|
output = "<span>"+content+"</span>" # <p> tags are usually bad for replacers
|
||||||
system.replaceWith(output, '#'+ref)
|
system.replaceWith(output, '#'+ref)
|
||||||
inserter: (ref) ->
|
inserter: (ref) ->
|
||||||
content = that.writers[ref].fcall(that, character, system, action)
|
content = that.writers[ref].fcall(that, system, action)
|
||||||
output = markdown(content)
|
output = markdown(content)
|
||||||
system.writeInto(output, '#'+ref)
|
system.writeInto(output, '#'+ref)
|
||||||
}
|
}
|
||||||
|
@ -236,7 +196,7 @@ class SaletRoom
|
||||||
throw new Error("Tried to call undefined writer: #{action}");
|
throw new Error("Tried to call undefined writer: #{action}");
|
||||||
responses[responder](ref);
|
responses[responder](ref);
|
||||||
else if (@actions.hasOwnProperty(action))
|
else if (@actions.hasOwnProperty(action))
|
||||||
@actions[action].call(this, character, system, action);
|
@actions[action].call(this, system, action);
|
||||||
else
|
else
|
||||||
throw new Error("Tried to call undefined action: #{action}");
|
throw new Error("Tried to call undefined action: #{action}");
|
||||||
|
|
||||||
|
@ -253,7 +213,7 @@ class SaletRoom
|
||||||
node.distance = current_room.distance + 1
|
node.distance = current_room.distance + 1
|
||||||
candidates.push(node)
|
candidates.push(node)
|
||||||
|
|
||||||
register: () =>
|
register: (salet) =>
|
||||||
if not @name?
|
if not @name?
|
||||||
console.error("Situation has no name")
|
console.error("Situation has no name")
|
||||||
return this
|
return this
|
||||||
|
@ -264,17 +224,9 @@ class SaletRoom
|
||||||
cyclewriter: (character) ->
|
cyclewriter: (character) ->
|
||||||
cycle(this.cycle, this.name, character)
|
cycle(this.cycle, this.name, character)
|
||||||
|
|
||||||
room = (name, spec) ->
|
room = (name, salet, spec) ->
|
||||||
spec ?= {}
|
spec ?= {}
|
||||||
spec.name = name
|
spec.name = name
|
||||||
retval = new SaletRoom(spec)
|
return new SaletRoom(spec).register(salet)
|
||||||
$(document).ready(() ->
|
|
||||||
if salet
|
|
||||||
retval.register()
|
|
||||||
else
|
|
||||||
sleep(1000)
|
|
||||||
retval.register()
|
|
||||||
)
|
|
||||||
return retval
|
|
||||||
|
|
||||||
module.exports = room
|
module.exports = room
|
||||||
|
|
166
lib/salet.coffee
166
lib/salet.coffee
|
@ -1,6 +1,7 @@
|
||||||
markdown = require('./markdown.coffee')
|
markdown = require('./markdown.coffee')
|
||||||
SaletView = require('./view.coffee')
|
SaletView = require('./view.coffee')
|
||||||
Random = require('./random.js')
|
Random = require('./random.js')
|
||||||
|
languages = require('./localize.coffee')
|
||||||
|
|
||||||
###
|
###
|
||||||
fcall() (by analogy with fmap) is added to the prototypes of both String and
|
fcall() (by analogy with fmap) is added to the prototypes of both String and
|
||||||
|
@ -9,6 +10,8 @@ when called on a String, it only returns the string itself, discarding any input
|
||||||
###
|
###
|
||||||
|
|
||||||
Function.prototype.fcall = Function.prototype.call;
|
Function.prototype.fcall = Function.prototype.call;
|
||||||
|
Boolean.prototype.fcall = () ->
|
||||||
|
return this
|
||||||
String.prototype.fcall = () ->
|
String.prototype.fcall = () ->
|
||||||
return this
|
return this
|
||||||
|
|
||||||
|
@ -33,31 +36,6 @@ parseFn = (str) ->
|
||||||
# Salet's default is a general URL-safe expression.
|
# Salet's default is a general URL-safe expression.
|
||||||
linkRe = /^([0-9A-Za-z_-]+|\.)(\/([0-9A-Za-z_-]+))?$/
|
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[=&]?/))
|
|
||||||
@view.clearLinks(href)
|
|
||||||
|
|
||||||
processClick(href)
|
|
||||||
return false
|
|
||||||
)
|
|
||||||
else
|
|
||||||
a.addClass("raw")
|
|
||||||
)
|
|
||||||
return output
|
|
||||||
|
|
||||||
###
|
###
|
||||||
This is the control structure, it has minimal amount of data and
|
This is the control structure, it has minimal amount of data and
|
||||||
this data is volatile anyway (as in, it won't get saved).
|
this data is volatile anyway (as in, it won't get saved).
|
||||||
|
@ -149,17 +127,6 @@ class Salet
|
||||||
situations that force the player to go to one destination if
|
situations that force the player to go to one destination if
|
||||||
the player is out of money, for example.
|
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
|
If a maxChoices value is given, then the function will not
|
||||||
return any more than the given number of results. If there are
|
return any more than the given number of results. If there are
|
||||||
more than this number of results possible, then the highest
|
more than this number of results possible, then the highest
|
||||||
|
@ -181,90 +148,65 @@ class Salet
|
||||||
Before this function returns its result, it sorts the
|
Before this function returns its result, it sorts the
|
||||||
situations in increasing order of their displayOrder values.
|
situations in increasing order of their displayOrder values.
|
||||||
###
|
###
|
||||||
getSituationIdChoices: (listOfOrOneIdsOrTags, minChoices, maxChoices) ->
|
getSituationIdChoices: (listOfOrOneIdsOrTags, maxChoices) ->
|
||||||
datum = null
|
datum = null
|
||||||
i = 0
|
i = 0
|
||||||
|
|
||||||
# First check if we have a single string for the id or tag.
|
# First check if we have a single string for the id or tag.
|
||||||
if ($.type(listOfOrOneIdsOrTags) == 'string')
|
if (typeof(listOfOrOneIdsOrTags) == 'string')
|
||||||
listOfOrOneIdsOrTags = [listOfOrOneIdsOrTags]
|
listOfOrOneIdsOrTags = [listOfOrOneIdsOrTags]
|
||||||
|
|
||||||
# First we build a list of all candidate ids.
|
# First we build a list of all candidate ids.
|
||||||
allIds = {}
|
allIds = []
|
||||||
for tagOrId in listOfOrOneIdsOrTags
|
for tagOrId in listOfOrOneIdsOrTags
|
||||||
if (tagOrId.substr(0, 1) == '#')
|
if (tagOrId.substr(0, 1) == '#')
|
||||||
ids = getSituationIdsWithTag(tagOrId.substr(1))
|
ids = @getRoomsTagged(tagOrId.substr(1))
|
||||||
for id in ids
|
for id in ids
|
||||||
allIds[id] = true
|
allIds.push(id)
|
||||||
else
|
else #it's an id, not a tag
|
||||||
allIds[tagOrId] = true
|
allIds.push(tagOrId)
|
||||||
|
|
||||||
#Filter out anything that can't be viewed right now.
|
#Filter out anything that can't be viewed right now.
|
||||||
currentSituation = @getCurrentRoom()
|
currentRoom = @getCurrentRoom()
|
||||||
viewableSituationData = []
|
viewableRoomData = []
|
||||||
for situationId in allIds
|
for roomId in allIds
|
||||||
situation = @rooms[situationId]
|
room = @rooms[roomId]
|
||||||
assert(situation, "unknown_situation".l({id:situationId}))
|
assert(room, "unknown_situation".l({id:roomId}))
|
||||||
|
|
||||||
if (situation.canView(character, salet, currentSituation))
|
if (room.canView.fcall(this, currentRoom))
|
||||||
#While we're here, get the selection data.
|
viewableRoomData.push({
|
||||||
viewableSituationDatum = situation.choiceData(character, salet, currentSituation)
|
priority: room.priority
|
||||||
viewableSituationDatum.id = situationId
|
id: roomId
|
||||||
viewableSituationData.push(viewableSituationDatum)
|
displayOrder: room.displayOrder
|
||||||
|
})
|
||||||
|
|
||||||
# Then we sort in descending priority order.
|
# Then we sort in descending priority order.
|
||||||
viewableSituationData.sort((a, b) ->
|
viewableRoomData.sort((a, b) ->
|
||||||
return b.priority - a.priority
|
return b.priority - a.priority
|
||||||
)
|
)
|
||||||
|
|
||||||
committed = []
|
committed = []
|
||||||
candidatesAtLastPriority = []
|
|
||||||
lastPriority
|
|
||||||
# In descending priority order.
|
|
||||||
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
|
# if we need to filter out the results
|
||||||
# them we wouldn't hit our minimum. But those in
|
if (maxChoices? && viewableRoomData.length > maxChoices)
|
||||||
# candidatesAtLastPriority might take us over our maximum, so
|
viewableRoomData = viewableRoomData[-maxChoices..]
|
||||||
# figure out how many we should choose.
|
|
||||||
totalChoices = committed.length + candidatesAtLastPriority.length
|
for candidateRoom in viewableRoomData
|
||||||
if (maxChoices == undefined || maxChoices >= totalChoices)
|
committed.push({
|
||||||
# We can use all the choices.
|
id: candidateRoom.id
|
||||||
committed.push.apply(committed, candidatesAtLastPriority)
|
displayOrder: candidateRoom.displayOrder
|
||||||
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 datum in candidatesAtLastPriority
|
|
||||||
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.
|
# Now sort in ascending display order.
|
||||||
committed.sort((a, b) ->
|
committed.sort((a, b) ->
|
||||||
return a.displayOrder - b.displayOrder
|
return a.displayOrder - b.displayOrder
|
||||||
)
|
)
|
||||||
|
|
||||||
# And return as a list of ids only.
|
# And return as a list of ids only.
|
||||||
result = []
|
result = []
|
||||||
for i in committed
|
for i in committed
|
||||||
result.push(i.id)
|
result.push(i.id)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
# This is the data on the player's progress that gets saved.
|
# This is the data on the player's progress that gets saved.
|
||||||
|
@ -313,7 +255,7 @@ class Salet
|
||||||
@linkStack = []
|
@linkStack = []
|
||||||
|
|
||||||
# Handle each link in turn.
|
# Handle each link in turn.
|
||||||
processOneLink(code);
|
@processOneLink(code);
|
||||||
while (@linkStack.length > 0)
|
while (@linkStack.length > 0)
|
||||||
code = linkStack.shift()
|
code = linkStack.shift()
|
||||||
processOneLink(code)
|
processOneLink(code)
|
||||||
|
@ -322,10 +264,10 @@ class Salet
|
||||||
@linkStack = null;
|
@linkStack = null;
|
||||||
|
|
||||||
# Scroll to the top of the new content.
|
# Scroll to the top of the new content.
|
||||||
@endOutputTransaction()
|
@view.endOutputTransaction()
|
||||||
|
|
||||||
# We're able to save, if we weren't already.
|
# We're able to save, if we weren't already.
|
||||||
@enableSaving()
|
@view.enableSaving()
|
||||||
|
|
||||||
###
|
###
|
||||||
This gets called to actually do the work of processing a code.
|
This gets called to actually do the work of processing a code.
|
||||||
|
@ -341,9 +283,8 @@ class Salet
|
||||||
action = match[3]
|
action = match[3]
|
||||||
|
|
||||||
# Change the situation
|
# Change the situation
|
||||||
if situation != '.'
|
if situation != '.' and situation != @current_room
|
||||||
if situation != current
|
@doTransitionTo(situation)
|
||||||
@doTransitionTo(situation)
|
|
||||||
else
|
else
|
||||||
# We should have an action if we have no situation change.
|
# We should have an action if we have no situation change.
|
||||||
assert(action, "link_no_action".l())
|
assert(action, "link_no_action".l())
|
||||||
|
@ -370,9 +311,9 @@ class Salet
|
||||||
# This gets called when the user clicks a link to carry out an action.
|
# This gets called when the user clicks a link to carry out an action.
|
||||||
processClick: (code) ->
|
processClick: (code) ->
|
||||||
now = (new Date()).getTime() * 0.001
|
now = (new Date()).getTime() * 0.001
|
||||||
@time = now - startTime
|
@time = now - @startTime
|
||||||
@progress.sequence.push({link:code, when:@time})
|
@progress.sequence.push({link:code, when:@time})
|
||||||
processLink(code)
|
@processLink(code)
|
||||||
|
|
||||||
# Transitions between situations.
|
# Transitions between situations.
|
||||||
doTransitionTo: (newRoomId) ->
|
doTransitionTo: (newRoomId) ->
|
||||||
|
@ -385,11 +326,11 @@ class Salet
|
||||||
# We might not have an old situation if this is the start of the game.
|
# We might not have an old situation if this is the start of the game.
|
||||||
if (oldRoom and @exit)
|
if (oldRoom and @exit)
|
||||||
@exit(oldRoomId, newRoomId)
|
@exit(oldRoomId, newRoomId)
|
||||||
|
|
||||||
@current = newRoomId
|
@current = newRoomId
|
||||||
|
|
||||||
# Remove links and transient sections.
|
# Remove links and transient sections.
|
||||||
@view.remove_transient(@interactive)
|
@view.removeTransient(@interactive)
|
||||||
|
|
||||||
# Notify the incoming situation.
|
# Notify the incoming situation.
|
||||||
if (@enter)
|
if (@enter)
|
||||||
|
@ -407,19 +348,19 @@ class Salet
|
||||||
game state across save/erase cycles, meaning that character.sandbox
|
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) =>
|
eraseSave: (force = false) =>
|
||||||
save_id = @getSaveId() # save slot
|
save_id = @getSaveId() # save slot
|
||||||
if (localStorage.getItem(saveId) and (force or confirm("erase_message".l())))
|
if (localStorage.getItem(saveId) and (force or confirm("erase_message".l())))
|
||||||
localStorage.removeItem(saveId)
|
localStorage.removeItem(saveId)
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
|
|
||||||
# Find and return a list of ids for all situations with the given tag.
|
# Find and return a list of ids for all situations with the given tag.
|
||||||
getSituationIdsWithTag: (tag) =>
|
getRoomsTagged: (tag) =>
|
||||||
result = []
|
result = []
|
||||||
for situationId, situation of @situations
|
for id, room of @rooms
|
||||||
for i in situation.tags
|
for i in room.tags
|
||||||
if (i == tag)
|
if (i == tag)
|
||||||
result.push(situationId)
|
result.push(id)
|
||||||
break
|
break
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@ -477,7 +418,7 @@ class Salet
|
||||||
@view.disableSaving()
|
@view.disableSaving()
|
||||||
@view.enableErasing()
|
@view.enableErasing()
|
||||||
catch err
|
catch err
|
||||||
@erase_save(true)
|
@eraseSave(true)
|
||||||
else
|
else
|
||||||
@progress.seed = new Date().toString()
|
@progress.seed = new Date().toString()
|
||||||
|
|
||||||
|
@ -485,8 +426,6 @@ class Salet
|
||||||
@rnd = new Random(@progress.seed)
|
@rnd = new Random(@progress.seed)
|
||||||
@progress.sequence = [{link:@start, when:0}]
|
@progress.sequence = [{link:@start, when:0}]
|
||||||
|
|
||||||
@view.clearContent()
|
|
||||||
|
|
||||||
# Start the game
|
# Start the game
|
||||||
@startTime = new Date().getTime() * 0.001
|
@startTime = new Date().getTime() * 0.001
|
||||||
if (@init)
|
if (@init)
|
||||||
|
@ -515,11 +454,4 @@ class Salet
|
||||||
return Boolean place.visited
|
return Boolean place.visited
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
# Set up the game when everything is loaded.
|
module.exports = Salet
|
||||||
$(document).ready(() ->
|
|
||||||
salet = new Salet
|
|
||||||
salet.view.init()
|
|
||||||
if (salet.view.hasLocalStorage())
|
|
||||||
$("#erase").click(salet.erase_save) # is Salet defined here?
|
|
||||||
$("#save").click(salet.saveGame)
|
|
||||||
)
|
|
||||||
|
|
102
lib/view.coffee
102
lib/view.coffee
|
@ -1,3 +1,4 @@
|
||||||
|
markdown = require('./markdown.coffee')
|
||||||
###
|
###
|
||||||
Salet interface configuration.
|
Salet interface configuration.
|
||||||
In a typical MVC structure, this is the View.
|
In a typical MVC structure, this is the View.
|
||||||
|
@ -10,8 +11,13 @@ 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.)
|
but they could be something else entirely. (That's why IDs are hardcoded.)
|
||||||
###
|
###
|
||||||
|
|
||||||
|
assert = (msg, assertion) -> console.assert assertion, msg
|
||||||
|
|
||||||
|
way_to = (content, ref) ->
|
||||||
|
return "<a href='#{ref}' class='way'>#{content}</a>"
|
||||||
|
|
||||||
class SaletView
|
class SaletView
|
||||||
init: () ->
|
init: (salet) ->
|
||||||
$("#content, #ways").on("click", "a", (event) ->
|
$("#content, #ways").on("click", "a", (event) ->
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
salet.processClick($(this).attr("href"))
|
salet.processClick($(this).attr("href"))
|
||||||
|
@ -23,6 +29,9 @@ class SaletView
|
||||||
$("#load").on("click", "a", (event) ->
|
$("#load").on("click", "a", (event) ->
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
)
|
)
|
||||||
|
if (@hasLocalStorage())
|
||||||
|
$("#erase").click(salet.erase_save) # is Salet defined here?
|
||||||
|
$("#save").click(salet.saveGame)
|
||||||
|
|
||||||
disableSaving: () ->
|
disableSaving: () ->
|
||||||
$("#save").addClass('disabled')
|
$("#save").addClass('disabled')
|
||||||
|
@ -67,11 +76,15 @@ class SaletView
|
||||||
return
|
return
|
||||||
if typeof content == "function"
|
if typeof content == "function"
|
||||||
content = content()
|
content = content()
|
||||||
|
if content instanceof jQuery
|
||||||
|
content = content[0].outerHTML
|
||||||
block = document.getElementById("current-room")
|
block = document.getElementById("current-room")
|
||||||
if block
|
if block
|
||||||
block.innerHTML = block.innerHTML + markdown(content)
|
block.innerHTML = block.innerHTML + markdown(content)
|
||||||
else
|
else
|
||||||
console.error("No current situation found.")
|
# most likely this is the starting room
|
||||||
|
block = document.getElementById("content")
|
||||||
|
block.innerHTML = content
|
||||||
|
|
||||||
###
|
###
|
||||||
Turns any links that target the given href into plain
|
Turns any links that target the given href into plain
|
||||||
|
@ -98,25 +111,25 @@ class SaletView
|
||||||
manually, ot else use the `getSituationIdChoices` method to
|
manually, ot else use the `getSituationIdChoices` method to
|
||||||
return an ordered list of valid viewable situation ids.
|
return an ordered list of valid viewable situation ids.
|
||||||
###
|
###
|
||||||
writeChoices: (listOfIds) ->
|
writeChoices: (salet, listOfIds) ->
|
||||||
if (listOfIds.length == 0)
|
if (not listOfIds? or listOfIds.length == 0)
|
||||||
return
|
return
|
||||||
|
|
||||||
currentSituation = getCurrentSituation();
|
currentRoom = salet.getCurrentRoom();
|
||||||
$options = $("<ul>").addClass("options");
|
$options = $("<ul>").addClass("options");
|
||||||
for situationId in listOfIds
|
for roomId in listOfIds
|
||||||
situation = game.situations[situationId]
|
room = salet.rooms[roomId]
|
||||||
assert(situation, "unknown_situation".l({id:situationId}))
|
assert(room, "unknown_situation".l({id:roomId}))
|
||||||
if (situation == currentSituation)
|
if (room == currentRoom)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
optionText = situation.optionText.fcall(this, character, this, currentSituation)
|
optionText = room.optionText.fcall(salet, currentRoom)
|
||||||
if (!optionText)
|
if (!optionText)
|
||||||
optionText = "choice".l({number:i+1})
|
optionText = "choice".l({number:i+1})
|
||||||
$option = $("<li>")
|
$option = $("<li>")
|
||||||
$a = $("<span>")
|
$a = $("<span>")
|
||||||
if (situation.canChoose(character, this, currentSituation))
|
if (room.canChoose.fcall(this, salet, currentRoom))
|
||||||
$a = $("<a>").attr({href: situationId})
|
$a = $("<a>").attr({href: roomId})
|
||||||
$a.html(optionText)
|
$a.html(optionText)
|
||||||
$option.html($a)
|
$option.html($a)
|
||||||
$options.append($option)
|
$options.append($option)
|
||||||
|
@ -129,8 +142,9 @@ class SaletView
|
||||||
# Removes links and transient sections.
|
# Removes links and transient sections.
|
||||||
# Arguments:
|
# Arguments:
|
||||||
# interactive - if we're working in interactive mode (or we're loading a save)
|
# interactive - if we're working in interactive mode (or we're loading a save)
|
||||||
remove_transient: (interactive = false) ->
|
removeTransient: (interactive = false) ->
|
||||||
for a in $('#content a')
|
for a in $('#content').find('a')
|
||||||
|
a = $(a)
|
||||||
if (a.hasClass('sticky') || a.attr("href").match(/[?&]sticky[=&]?/))
|
if (a.hasClass('sticky') || a.attr("href").match(/[?&]sticky[=&]?/))
|
||||||
return;
|
return;
|
||||||
a.replaceWith($("<span>").addClass("ex_link").html(a.html()))
|
a.replaceWith($("<span>").addClass("ex_link").html(a.html()))
|
||||||
|
@ -193,4 +207,64 @@ class SaletView
|
||||||
hasStorage = false
|
hasStorage = false
|
||||||
return hasStorage
|
return hasStorage
|
||||||
|
|
||||||
|
updateWays: (salet, ways, name) ->
|
||||||
|
content = ""
|
||||||
|
distances = []
|
||||||
|
if ways then for way in ways
|
||||||
|
if salet.rooms[way]?
|
||||||
|
title = salet.rooms[way].title.fcall(this, name)
|
||||||
|
content += way_to(title, way)
|
||||||
|
distances.push({
|
||||||
|
key: way
|
||||||
|
distance: salet.rooms[way].distance
|
||||||
|
})
|
||||||
|
else
|
||||||
|
document.querySelector(".ways h2").style.display = "none"
|
||||||
|
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
|
||||||
|
addClass(document.getElementById("waylink-#{node}"), "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'>"
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
for index, element in output.find("a")
|
||||||
|
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[=&]?/))
|
||||||
|
@view.clearLinks(href)
|
||||||
|
|
||||||
|
processClick(href)
|
||||||
|
return false
|
||||||
|
)
|
||||||
|
else
|
||||||
|
a.addClass("raw")
|
||||||
|
return output
|
||||||
module.exports = SaletView
|
module.exports = SaletView
|
||||||
|
|
Loading…
Reference in a new issue