1
0
Fork 0
mirror of https://gitlab.com/Oreolek/salet.git synced 2024-06-01 07:48:20 +03:00

Salet is now passed as an argument.

Also, no frequency filters on choice list.
This commit is contained in:
Alexander Yakovlev 2016-02-01 14:22:23 +07:00
parent 566c8b27a2
commit 5e7f4087aa
8 changed files with 176 additions and 217 deletions

View file

@ -3,7 +3,15 @@ room = require("../../lib/room.coffee")
obj = require('../../lib/obj.coffee')
dialogue = require('../../lib/dialogue.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")
@ -25,7 +33,7 @@ cyclelink = (content) ->
# The first room of the game.
# For accessibility reasons the text is provided in HTML, not here.
room "start",
room "start", salet,
dsc: """
""",
choices: "#start"

View file

@ -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()
)

View file

@ -1,4 +1,4 @@
room "world",
room "world", salet,
tags: ["start"],
optionText: "Enter the world",
ways: ["plaza"]
@ -13,7 +13,7 @@ room "world",
dsc: "A steep narrow {{well}} proceeds upward."
act: "There is only one passage out. See the „Other rooms“ block popped up? Click it."
room "plaza",
room "plaza", salet,
title: (from) ->
if from == "world"
return "Upwards"
@ -44,7 +44,7 @@ room "plaza",
dsc: "There are {{people shouting}} nearby."
act: 'Just some weirdos shouting "Viva la Cthulhu!". Typical.'
room "shop",
room "shop", salet,
title: "The Shop"
#pic: "http://loremflickr.com/640/300/room,shop"
ways: ["plaza", "shop-inside", "lair"]
@ -55,7 +55,7 @@ room "shop",
You are standing in front of a picturesque sign. It's cold here.
"""
room "lair",
room "lair", salet,
title: "The Lair"
before: "Finding The Lair is easy. Leaving it is impossible. Your game ends here."
dsc: """
@ -69,11 +69,11 @@ room "lair",
here().drop(@name)
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.
"""
room "shop-inside",
room "shop-inside", salet,
ways: ["shop"]
tags: ["merchant"]
optionText: "End the conversation"
@ -100,7 +100,7 @@ lamp = obj "lamp",
lamp.put("shop-inside")
###
room "merchdialogue",
room "merchdialogue", salet,
choices: "#merchant",
dsc: """
Nice day, isn't it?

View file

@ -15,8 +15,8 @@ Usage:
Point out a thing in her purse (mildly)
""", "character.sandbox.mild = true"
###
dialogue = (title, startTag, endTag, text, effect) ->
retval = room(randomid(), {
dialogue = (title, salet, startTag, endTag, text, effect) ->
retval = room(randomid(), salet, {
optionText: title
dsc: text
clear: false # backlog is useful in dialogues

View file

@ -4,7 +4,7 @@ Implies that you don't mix up your tabs and spaces.
Copyright 2015 Bruno Dias
###
normaliseTabs = (text) ->
unless text?
unless text? and typeof(text) == "string"
return ""
lines = text.split('\n');
indents = lines

View file

@ -1,14 +1,12 @@
# I confess that this world model heavily borrows from INSTEAD engine. - A.Y.
require('./salet.coffee')
obj = require('./obj.coffee')
markdown = require('./markdown.coffee')
cycle = require('./cycle.coffee')
Random = require('./random.js')
languages = require('./localize.coffee')
require('./salet.coffee')
# Assertion
assert = console.assert
assert = (msg, assertion) -> console.assert assertion, msg
way_to = (content, ref) ->
return "<a href='#{ref}' class='way' id='waylink-#{ref}'>#{content}</a>"
@ -21,43 +19,6 @@ addClass = (element, className) ->
else
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
constructor: (spec) ->
for index, value of spec
@ -73,7 +34,6 @@ class SaletRoom
canView: true
canChoose: true
priority: 1
frequency: 1
displayOrder: 1
tags: []
choices: ""
@ -84,14 +44,14 @@ class SaletRoom
distance: Infinity # distance to the destination
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.
Unlike @after this gets called after the section is closed.
It's a styling difference.
###
exit: (character, system, to) =>
exit: (system, to) =>
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.
It was renamed as @entering to achieve API consistency.
###
enter: (character, system, from) =>
enter: (system, from) =>
return true
###
@ -148,25 +108,25 @@ class SaletRoom
if not @extendSection
room_content += "</section>"
system.write(room_content)
system.view.write(room_content)
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
every object in this room.
###
look: (character, system, f) =>
update_ways(@ways, @name)
look: (system, f) =>
system.view.updateWays(system, @ways, @name)
retval = ""
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
if @dsc
retval += markdown(@dsc.fcall(this, character, system, f))
retval += markdown(@dsc.fcall(this, system, f))
for name, thing of @objects
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.
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
for name, thing of @objects
if name == link[2]
@ -215,15 +175,15 @@ class SaletRoom
responses = {
writer: (ref) ->
content = that.writers[ref].fcall(that, character, system, action)
content = that.writers[ref].fcall(that, system, action)
output = markdown(content)
system.writeInto(output, '#current-room')
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
system.replaceWith(output, '#'+ref)
inserter: (ref) ->
content = that.writers[ref].fcall(that, character, system, action)
content = that.writers[ref].fcall(that, system, action)
output = markdown(content)
system.writeInto(output, '#'+ref)
}
@ -236,7 +196,7 @@ class SaletRoom
throw new Error("Tried to call undefined writer: #{action}");
responses[responder](ref);
else if (@actions.hasOwnProperty(action))
@actions[action].call(this, character, system, action);
@actions[action].call(this, system, action);
else
throw new Error("Tried to call undefined action: #{action}");
@ -253,7 +213,7 @@ class SaletRoom
node.distance = current_room.distance + 1
candidates.push(node)
register: () =>
register: (salet) =>
if not @name?
console.error("Situation has no name")
return this
@ -264,17 +224,9 @@ class SaletRoom
cyclewriter: (character) ->
cycle(this.cycle, this.name, character)
room = (name, spec) ->
room = (name, salet, spec) ->
spec ?= {}
spec.name = name
retval = new SaletRoom(spec)
$(document).ready(() ->
if salet
retval.register()
else
sleep(1000)
retval.register()
)
return retval
return new SaletRoom(spec).register(salet)
module.exports = room

View file

@ -1,6 +1,7 @@
markdown = require('./markdown.coffee')
SaletView = require('./view.coffee')
Random = require('./random.js')
languages = require('./localize.coffee')
###
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;
Boolean.prototype.fcall = () ->
return this
String.prototype.fcall = () ->
return this
@ -33,31 +36,6 @@ parseFn = (str) ->
# Salet's default is a general URL-safe expression.
linkRe = /^([0-9A-Za-z_-]+|\.)(\/([0-9A-Za-z_-]+))?$/
# Returns HTML from the given content with the non-raw links wired up.
augmentLinks = (content) ->
output = $(content)
# Wire up the links for regular <a> tags.
output.find("a").each((index, element) ->
a = $(element)
href = a.attr('href')
if (!a.hasClass("raw")|| href.match(/[?&]raw[=&]?/))
if (href.match(linkRe))
a.click((event) ->
event.preventDefault()
# If we're a once-click, remove all matching links.
if (a.hasClass("once") || href.match(/[?&]once[=&]?/))
@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 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
the player is out of money, for example.
If a minChoices value is given, then the function will attempt
to return at least that many results. If not enough results are
available at the highest priority, then lower priorities will
be considered in turn, until enough situations are found. In
the example above, if we had a minChoices of three, then all
three situations would be returned, even though they have
different priorities. If you need to return all valid
situations, regardless of their priorities, set minChoices to a
large number, such as `Number.MAX_VALUE`, and leave maxChoices
undefined.
If a maxChoices value is given, then the function will not
return any more than the given number of results. If there are
more than this number of results possible, then the highest
@ -181,90 +148,65 @@ class Salet
Before this function returns its result, it sorts the
situations in increasing order of their displayOrder values.
###
getSituationIdChoices: (listOfOrOneIdsOrTags, minChoices, maxChoices) ->
getSituationIdChoices: (listOfOrOneIdsOrTags, maxChoices) ->
datum = null
i = 0
# First check if we have a single string for the id or tag.
if ($.type(listOfOrOneIdsOrTags) == 'string')
if (typeof(listOfOrOneIdsOrTags) == 'string')
listOfOrOneIdsOrTags = [listOfOrOneIdsOrTags]
# First we build a list of all candidate ids.
allIds = {}
allIds = []
for tagOrId in listOfOrOneIdsOrTags
if (tagOrId.substr(0, 1) == '#')
ids = getSituationIdsWithTag(tagOrId.substr(1))
ids = @getRoomsTagged(tagOrId.substr(1))
for id in ids
allIds[id] = true
else
allIds[tagOrId] = true
allIds.push(id)
else #it's an id, not a tag
allIds.push(tagOrId)
#Filter out anything that can't be viewed right now.
currentSituation = @getCurrentRoom()
viewableSituationData = []
for situationId in allIds
situation = @rooms[situationId]
assert(situation, "unknown_situation".l({id:situationId}))
currentRoom = @getCurrentRoom()
viewableRoomData = []
for roomId in allIds
room = @rooms[roomId]
assert(room, "unknown_situation".l({id:roomId}))
if (situation.canView(character, salet, currentSituation))
#While we're here, get the selection data.
viewableSituationDatum = situation.choiceData(character, salet, currentSituation)
viewableSituationDatum.id = situationId
viewableSituationData.push(viewableSituationDatum)
if (room.canView.fcall(this, currentRoom))
viewableRoomData.push({
priority: room.priority
id: roomId
displayOrder: room.displayOrder
})
# Then we sort in descending priority order.
viewableSituationData.sort((a, b) ->
viewableRoomData.sort((a, b) ->
return b.priority - a.priority
)
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
# them we wouldn't hit our minimum. But those in
# candidatesAtLastPriority might take us over our maximum, so
# figure out how many we should choose.
totalChoices = committed.length + candidatesAtLastPriority.length
if (maxChoices == undefined || maxChoices >= totalChoices)
# We can use all the choices.
committed.push.apply(committed, candidatesAtLastPriority)
else if (maxChoices >= committed.length)
# We can only use the commited ones.
# NO-OP
else
# We have to sample the candidates, using their relative frequency.
candidatesToInclude = maxChoices - committed.length;
for 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)
# 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
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.
@ -313,7 +255,7 @@ class Salet
@linkStack = []
# Handle each link in turn.
processOneLink(code);
@processOneLink(code);
while (@linkStack.length > 0)
code = linkStack.shift()
processOneLink(code)
@ -322,10 +264,10 @@ class Salet
@linkStack = null;
# Scroll to the top of the new content.
@endOutputTransaction()
@view.endOutputTransaction()
# 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.
@ -341,9 +283,8 @@ class Salet
action = match[3]
# Change the situation
if situation != '.'
if situation != current
@doTransitionTo(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())
@ -370,9 +311,9 @@ class Salet
# 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
@time = now - @startTime
@progress.sequence.push({link:code, when:@time})
processLink(code)
@processLink(code)
# Transitions between situations.
doTransitionTo: (newRoomId) ->
@ -385,11 +326,11 @@ class Salet
# 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.remove_transient(@interactive)
@view.removeTransient(@interactive)
# Notify the incoming situation.
if (@enter)
@ -407,19 +348,19 @@ class Salet
game state across save/erase cycles, meaning that character.sandbox
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
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.
getSituationIdsWithTag: (tag) =>
getRoomsTagged: (tag) =>
result = []
for situationId, situation of @situations
for i in situation.tags
for id, room of @rooms
for i in room.tags
if (i == tag)
result.push(situationId)
result.push(id)
break
return result
@ -477,7 +418,7 @@ class Salet
@view.disableSaving()
@view.enableErasing()
catch err
@erase_save(true)
@eraseSave(true)
else
@progress.seed = new Date().toString()
@ -485,8 +426,6 @@ class Salet
@rnd = new Random(@progress.seed)
@progress.sequence = [{link:@start, when:0}]
@view.clearContent()
# Start the game
@startTime = new Date().getTime() * 0.001
if (@init)
@ -515,11 +454,4 @@ class Salet
return Boolean place.visited
return 0
# Set up the game when everything is loaded.
$(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)
)
module.exports = Salet

View file

@ -1,3 +1,4 @@
markdown = require('./markdown.coffee')
###
Salet interface configuration.
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.)
###
assert = (msg, assertion) -> console.assert assertion, msg
way_to = (content, ref) ->
return "<a href='#{ref}' class='way'>#{content}</a>"
class SaletView
init: () ->
init: (salet) ->
$("#content, #ways").on("click", "a", (event) ->
event.preventDefault()
salet.processClick($(this).attr("href"))
@ -23,6 +29,9 @@ class SaletView
$("#load").on("click", "a", (event) ->
window.location.reload()
)
if (@hasLocalStorage())
$("#erase").click(salet.erase_save) # is Salet defined here?
$("#save").click(salet.saveGame)
disableSaving: () ->
$("#save").addClass('disabled')
@ -67,11 +76,15 @@ class SaletView
return
if typeof content == "function"
content = content()
if content instanceof jQuery
content = content[0].outerHTML
block = document.getElementById("current-room")
if block
block.innerHTML = block.innerHTML + markdown(content)
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
@ -98,25 +111,25 @@ class SaletView
manually, ot else use the `getSituationIdChoices` method to
return an ordered list of valid viewable situation ids.
###
writeChoices: (listOfIds) ->
if (listOfIds.length == 0)
writeChoices: (salet, listOfIds) ->
if (not listOfIds? or listOfIds.length == 0)
return
currentSituation = getCurrentSituation();
currentRoom = salet.getCurrentRoom();
$options = $("<ul>").addClass("options");
for situationId in listOfIds
situation = game.situations[situationId]
assert(situation, "unknown_situation".l({id:situationId}))
if (situation == currentSituation)
for roomId in listOfIds
room = salet.rooms[roomId]
assert(room, "unknown_situation".l({id:roomId}))
if (room == currentRoom)
continue
optionText = situation.optionText.fcall(this, character, this, currentSituation)
optionText = room.optionText.fcall(salet, currentRoom)
if (!optionText)
optionText = "choice".l({number:i+1})
$option = $("<li>")
$a = $("<span>")
if (situation.canChoose(character, this, currentSituation))
$a = $("<a>").attr({href: situationId})
if (room.canChoose.fcall(this, salet, currentRoom))
$a = $("<a>").attr({href: roomId})
$a.html(optionText)
$option.html($a)
$options.append($option)
@ -129,8 +142,9 @@ class SaletView
# 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')
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()))
@ -193,4 +207,64 @@ class SaletView
hasStorage = false
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