# I confess that this world model heavily borrows from INSTEAD engine. - A.Y. obj = require('./obj.coffee') markdown = require('./markdown.coffee') cycle = require('./cycle.coffee') Random = require('./random.js') languages = require('./localize.coffee') # Feature detection hasLocalStorage = () -> hasStorage = false try { hasStorage = ('localStorage' in window) && window.localStorage !== null && window.localStorage !== undefined; } catch (err) { hasStorage = false } return hasStorage # Assertion assert = console.assert way_to = (content, ref) -> return "#{content}" Array::remove = (e) -> @[t..t] = [] if (t = @indexOf(e)) > -1 addClass = (element, className) -> if (element.classList) element.classList.add(className) else element.className += ' ' + className cls = (system) -> system.clearContent() system.clearContent("#intro") update_ways = (ways, name) -> content = "" distances = [] if ways document.querySelector(".ways h2").style.display = "block" for way in ways if undum.game.situations[way]? title = undum.game.situations[way].title.fcall(this, name) content += way_to(title, way) distances.push({ key: way distance: undum.game.situations[way].distance }) 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 """ """ return "Room illustration" class SaletRoom extends undum.Situation constructor: (spec) -> undum.Situation.call(this, spec) for index, value of spec this[index] = value return this visited: 0 title: "Room" objects: {} # room illustration image, VN-style. Can be a GIF or WEBM. Can be a function. pic: false canView: true canChoose: true priority: 1 frequency: 1 displayOrder: 1 tags: [] choices: "" optionText: "Choice" dsc: false # room description extendSection: false distance: Infinity # distance to the destination clear: true # clear the screen on entering the room? entering: (character, 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) => 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: (character, 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: (character, system, f) => if @clear and f? cls(system) if f != @name and f? @visited++ if undum.game.situations[f].exit? undum.game.situations[f].exit(character, system, @name) if @enter @enter character, system, f current_situation = "" if not @extendSection classes = if @classes then ' ' + @classes.join(' ') else '' situation = document.getElementById('current-situation') if situation? situation.removeAttribute('id') # Javascript DOM manipulation functions like jQuery's append() or document.createElement # don't work like a typical printLn - they create *DOM nodes*. # You can't leave an unclosed tag just like that. So we have to buffer the output. current_situation = "
" if f != @name and @before? current_situation += markdown(@before.fcall(this, character, system, f)) current_situation += @look character, system, f if f != @name and @after? current_situation += markdown(@after.fcall(this, character, system, f)) if not @extendSection current_situation += "
" system.write(current_situation) if @choices system.writeChoices(system.getSituationIdChoices(@choices, @minChoices, @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) retval = "" if @pic retval += '
'+picture_tag(@pic.fcall(this, character, system, f))+'
' # Print the room description if @dsc retval += markdown(@dsc.fcall(this, character, system, f)) for name, thing of @objects retval += thing.look() return retval ### 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 = {} drop: (name) => delete @objects[name] ### 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) => if (link = action.match(/^_(act|cycle)_(.+)$/)) #object action for name, thing of @objects 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 # the loop is done but no return came - match not found console.error("Could not find #{link[1]} in current room.") # 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 = ""+content+"" #

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}"); # 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: () => 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) room = (name, spec) -> spec ?= {} spec.name = name retval = new SaletRoom(spec) retval.register() return retval module.exports = room