2016-01-15 03:06:03 +02:00
|
|
|
# I confess that this world model heavily borrows from INSTEAD engine. - A.Y.
|
|
|
|
|
|
|
|
obj = require('./obj.coffee')
|
|
|
|
markdown = require('./markdown.coffee')
|
2016-01-19 05:03:01 +02:00
|
|
|
cycle = require('./cycle.coffee')
|
2016-01-22 17:07:34 +02:00
|
|
|
Random = require('./random.js')
|
|
|
|
languages = require('./localize.coffee')
|
|
|
|
|
|
|
|
# Feature detection
|
|
|
|
hasLocalStorage = () ->
|
|
|
|
hasStorage = false
|
|
|
|
try {
|
|
|
|
hasStorage = ('localStorage' in window) &&
|
|
|
|
window.localStorage !== null &&
|
|
|
|
window.localStorage !== undefined;
|
|
|
|
}
|
|
|
|
catch (err) {
|
|
|
|
hasStorage = false
|
|
|
|
}
|
|
|
|
return hasStorage
|
|
|
|
|
|
|
|
# Assertion
|
|
|
|
assert = console.assert
|
2016-01-15 03:06:03 +02:00
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
addClass = (element, className) ->
|
|
|
|
if (element.classList)
|
|
|
|
element.classList.add(className)
|
|
|
|
else
|
|
|
|
element.className += ' ' + className
|
|
|
|
|
2016-01-16 18:24:23 +02:00
|
|
|
cls = (system) ->
|
|
|
|
system.clearContent()
|
|
|
|
system.clearContent("#intro")
|
|
|
|
|
2016-01-15 17:29:15 +02:00
|
|
|
update_ways = (ways, name) ->
|
2016-01-15 03:06:03 +02:00
|
|
|
content = ""
|
|
|
|
distances = []
|
|
|
|
if ways
|
2016-01-15 10:10:12 +02:00
|
|
|
document.querySelector(".ways h2").style.display = "block"
|
2016-01-15 03:06:03 +02:00
|
|
|
for way in ways
|
|
|
|
if undum.game.situations[way]?
|
2016-01-15 17:29:15 +02:00
|
|
|
title = undum.game.situations[way].title.fcall(this, name)
|
2016-01-15 03:06:03 +02:00
|
|
|
content += way_to(title, way)
|
|
|
|
distances.push({
|
|
|
|
key: way
|
|
|
|
distance: undum.game.situations[way].distance
|
|
|
|
})
|
2016-01-15 10:10:12 +02:00
|
|
|
else
|
|
|
|
document.querySelector(".ways h2").style.display = "none"
|
2016-01-15 03:06:03 +02:00
|
|
|
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")
|
|
|
|
|
2016-01-20 01:56:10 +02:00
|
|
|
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'>"
|
|
|
|
|
2016-01-19 05:03:01 +02:00
|
|
|
class SaletRoom extends undum.Situation
|
2016-01-15 03:06:03 +02:00
|
|
|
constructor: (spec) ->
|
2016-01-19 05:03:01 +02:00
|
|
|
undum.Situation.call(this, spec)
|
2016-01-16 18:24:23 +02:00
|
|
|
for index, value of spec
|
|
|
|
this[index] = value
|
2016-01-15 03:06:03 +02:00
|
|
|
return this
|
2016-01-19 05:03:01 +02:00
|
|
|
visited: 0
|
2016-01-15 17:29:15 +02:00
|
|
|
title: "Room"
|
2016-01-16 18:24:23 +02:00
|
|
|
objects: {}
|
2016-01-20 01:56:10 +02:00
|
|
|
|
|
|
|
# room illustration image, VN-style. Can be a GIF or WEBM. Can be a function.
|
|
|
|
pic: false
|
|
|
|
|
2016-01-22 17:07:34 +02:00
|
|
|
canView: true
|
|
|
|
canChoose: true
|
|
|
|
priority: 1
|
|
|
|
frequency: 1
|
|
|
|
displayOrder: 1
|
|
|
|
tags: []
|
|
|
|
choices: ""
|
|
|
|
optionText: "Choice"
|
|
|
|
|
2016-01-20 01:56:10 +02:00
|
|
|
dsc: false # room description
|
2016-01-15 16:04:09 +02:00
|
|
|
extendSection: false
|
2016-01-15 03:06:03 +02:00
|
|
|
distance: Infinity # distance to the destination
|
2016-01-15 10:10:12 +02:00
|
|
|
clear: true # clear the screen on entering the room?
|
2016-01-15 03:06:03 +02:00
|
|
|
|
2016-01-22 17:07:34 +02:00
|
|
|
entering: (character, system, from) =>
|
|
|
|
|
2016-01-15 03:06:03 +02:00
|
|
|
###
|
|
|
|
I call SaletRoom.exit every time the player exits to another room.
|
2016-01-15 10:10:12 +02:00
|
|
|
Unlike @after this gets called after the section is closed.
|
|
|
|
It's a styling difference.
|
2016-01-15 03:06:03 +02:00
|
|
|
###
|
2016-01-16 18:24:23 +02:00
|
|
|
exit: (character, system, to) =>
|
2016-01-15 10:10:12 +02:00
|
|
|
return true
|
2016-01-18 12:29:14 +02:00
|
|
|
|
2016-01-15 10:10:12 +02:00
|
|
|
###
|
|
|
|
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.
|
|
|
|
###
|
2016-01-16 18:24:23 +02:00
|
|
|
enter: (character, system, from) =>
|
2016-01-15 10:10:12 +02:00
|
|
|
return true
|
|
|
|
|
2016-01-15 03:06:03 +02:00
|
|
|
###
|
2016-01-15 10:10:12 +02:00
|
|
|
Salet's Undum version calls Situation.entering every time a situation is entered, and
|
2016-01-15 03:06:03 +02:00
|
|
|
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.
|
|
|
|
###
|
2016-01-16 18:24:23 +02:00
|
|
|
entering: (character, system, f) =>
|
2016-01-15 16:04:09 +02:00
|
|
|
if @clear and f?
|
2016-01-16 18:24:23 +02:00
|
|
|
cls(system)
|
2016-01-15 10:10:12 +02:00
|
|
|
|
2016-01-15 03:06:03 +02:00
|
|
|
if f != @name and f?
|
|
|
|
@visited++
|
2016-01-15 10:10:12 +02:00
|
|
|
if undum.game.situations[f].exit?
|
|
|
|
undum.game.situations[f].exit(character, system, @name)
|
2016-01-18 12:29:14 +02:00
|
|
|
|
2016-01-15 10:10:12 +02:00
|
|
|
if @enter
|
|
|
|
@enter character, system, f
|
2016-01-15 03:06:03 +02:00
|
|
|
|
2016-01-15 16:04:09 +02:00
|
|
|
current_situation = ""
|
2016-01-15 03:06:03 +02:00
|
|
|
if not @extendSection
|
|
|
|
classes = if @classes then ' ' + @classes.join(' ') else ''
|
|
|
|
situation = document.getElementById('current-situation')
|
|
|
|
if situation?
|
2016-01-15 16:04:09 +02:00
|
|
|
situation.removeAttribute('id')
|
|
|
|
# Javascript DOM manipulation functions like jQuery's append() or document.createElement
|
|
|
|
# don't work like a typical printLn - they create *DOM nodes*.
|
|
|
|
# You can't leave an unclosed tag just like that. So we have to buffer the output.
|
|
|
|
current_situation = "<section id='current-situation' data-situation='#{@name}' class='situation-#{@name}#{classes}'>"
|
2016-01-15 03:06:03 +02:00
|
|
|
|
|
|
|
if f != @name and @before?
|
2016-01-15 17:29:15 +02:00
|
|
|
current_situation += markdown(@before.fcall(this, character, system, f))
|
2016-01-15 03:06:03 +02:00
|
|
|
|
2016-01-16 18:24:23 +02:00
|
|
|
current_situation += @look character, system, f
|
2016-01-15 03:06:03 +02:00
|
|
|
|
|
|
|
if f != @name and @after?
|
2016-01-15 17:29:15 +02:00
|
|
|
current_situation += markdown(@after.fcall(this, character, system, f))
|
2016-01-15 03:06:03 +02:00
|
|
|
|
|
|
|
if not @extendSection
|
2016-01-15 16:04:09 +02:00
|
|
|
current_situation += "</section>"
|
|
|
|
|
|
|
|
system.write(current_situation)
|
2016-01-15 03:06:03 +02:00
|
|
|
|
|
|
|
if @choices
|
|
|
|
system.writeChoices(system.getSituationIdChoices(@choices, @minChoices, @maxChoices))
|
|
|
|
|
2016-01-16 18:24:23 +02:00
|
|
|
###
|
|
|
|
An internal function to get the room's description and the descriptions of
|
|
|
|
every object in this room.
|
|
|
|
###
|
2016-01-19 05:03:01 +02:00
|
|
|
look: (character, system, f) =>
|
2016-01-15 17:29:15 +02:00
|
|
|
update_ways(@ways, @name)
|
2016-01-15 16:04:09 +02:00
|
|
|
retval = ""
|
2016-01-15 03:06:03 +02:00
|
|
|
|
2016-01-20 01:56:10 +02:00
|
|
|
if @pic
|
|
|
|
retval += '<div class="pic">'+picture_tag(@pic.fcall(this, character, system, f))+'</div>'
|
|
|
|
|
2016-01-15 03:06:03 +02:00
|
|
|
# Print the room description
|
2016-01-18 12:29:14 +02:00
|
|
|
if @dsc
|
|
|
|
retval += markdown(@dsc.fcall(this, character, system, f))
|
2016-01-15 03:06:03 +02:00
|
|
|
|
2016-01-16 18:24:23 +02:00
|
|
|
for name, thing of @objects
|
2016-01-15 16:04:09 +02:00
|
|
|
retval += thing.look()
|
|
|
|
|
|
|
|
return retval
|
2016-01-15 03:06:03 +02:00
|
|
|
|
2016-01-16 18:24:23 +02:00
|
|
|
###
|
|
|
|
Puts an object in this room.
|
|
|
|
###
|
|
|
|
take: (thing) =>
|
|
|
|
@objects[thing.name] = thing
|
|
|
|
# BUG: for some really weird reason if the call is made in init function or
|
|
|
|
# during the initialization, this ALSO puts the thing in the start room.
|
|
|
|
undum.game.situations["start"].objects = {}
|
|
|
|
|
2016-01-16 18:43:20 +02:00
|
|
|
drop: (name) =>
|
|
|
|
delete @objects[name]
|
|
|
|
|
2016-01-15 03:06:03 +02:00
|
|
|
###
|
|
|
|
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.
|
|
|
|
###
|
2016-01-16 18:24:23 +02:00
|
|
|
act: (character, system, action) =>
|
2016-01-19 05:03:01 +02:00
|
|
|
if (link = action.match(/^_(act|cycle)_(.+)$/)) #object action
|
2016-01-16 18:24:23 +02:00
|
|
|
for name, thing of @objects
|
2016-01-19 05:03:01 +02:00
|
|
|
if name == link[2]
|
|
|
|
if link[1] == "act"
|
|
|
|
# If it's takeable, the player can take this object.
|
|
|
|
# If not, we check the "act" function.
|
|
|
|
if thing.takeable
|
|
|
|
character.sandbox.inventory.push thing
|
|
|
|
@drop name
|
|
|
|
cls(system)
|
|
|
|
@entering.fcall(this, character, system, @name)
|
|
|
|
return print(thing.take.fcall(thing, character, system))
|
|
|
|
if thing.act
|
|
|
|
return print(thing.act.fcall(thing, character, system))
|
|
|
|
elseif link[1] == "cycle"
|
|
|
|
# TODO object cyclewriter
|
2016-01-15 03:06:03 +02:00
|
|
|
# the loop is done but no return came - match not found
|
|
|
|
console.error("Could not find #{link[1]} in current room.")
|
2016-01-18 12:29:14 +02:00
|
|
|
|
2016-01-19 05:03:01 +02:00
|
|
|
# we're done with objects, now check the regular actions
|
|
|
|
actionClass = action.match(/^_(\w+)_(.+)$/)
|
|
|
|
that = this
|
|
|
|
|
|
|
|
responses = {
|
|
|
|
writer: (ref) ->
|
|
|
|
content = that.writers[ref].fcall(that, character, system, action)
|
|
|
|
output = markdown(content)
|
|
|
|
system.writeInto(output, '#current-situation')
|
|
|
|
replacer: (ref) ->
|
|
|
|
content = that.writers[ref].fcall(that, character, 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)
|
|
|
|
output = markdown(content)
|
|
|
|
system.writeInto(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, character, system, action);
|
|
|
|
else
|
|
|
|
throw new Error("Tried to call undefined action: #{action}");
|
2016-01-15 03:06:03 +02:00
|
|
|
|
|
|
|
# Marks every room in the game with distance to this room
|
2016-01-16 18:24:23 +02:00
|
|
|
destination: () =>
|
2016-01-15 03:06:03 +02:00
|
|
|
@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)
|
|
|
|
|
2016-01-19 05:03:01 +02:00
|
|
|
register: () =>
|
|
|
|
if not @name?
|
|
|
|
console.error("Situation has no name")
|
|
|
|
return this
|
|
|
|
undum.game.situations[@name] = this
|
|
|
|
return this
|
|
|
|
|
|
|
|
writers:
|
|
|
|
cyclewriter: (character) ->
|
|
|
|
cycle(this.cycle, this.name, character)
|
|
|
|
|
2016-01-15 03:06:03 +02:00
|
|
|
room = (name, spec) ->
|
2016-01-16 18:24:23 +02:00
|
|
|
spec ?= {}
|
|
|
|
spec.name = name
|
|
|
|
retval = new SaletRoom(spec)
|
2016-01-16 19:04:00 +02:00
|
|
|
retval.register()
|
|
|
|
return retval
|
2016-01-15 03:06:03 +02:00
|
|
|
|
|
|
|
module.exports = room
|