# 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') assert = (msg, assertion) -> console.assert assertion, msg Function.prototype.fcall = Function.prototype.call; Boolean.prototype.fcall = () -> return this String.prototype.fcall = () -> return this way_to = (content, ref) -> return "#{content}" Array::remove = (e) -> @[t..t] = [] if (t = @indexOf(e)) > -1 class SaletRoom constructor: (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 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: (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: (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 @clear and f? system.view.clearContent() if f != @name and f? @visited++ if system.rooms[f].exit? system.rooms[f].exit system, @name if @enter @enter system, f room_content = "" if not @extendSection classes = if @classes then ' ' + @classes.join(' ') else '' room = document.getElementById('current-room') if room? room.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. room_content = "
" if f != @name and @before? room_content += markdown(@before.fcall(this, system, f)) room_content += @look system, f if f != @name and @after? room_content += markdown(@after.fcall(this, system, f)) if not @extendSection room_content += "
" system.view.write(room_content) if @choices 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: (system, f) => system.view.updateWays(system, @ways, @name) retval = "" if @pic retval += '
'+system.view.pictureTag(@pic.fcall(this, system, f))+'
' # Print the room description if @dsc dsc = @dsc.fcall(this, system, f).toString() retval += markdown(dsc) 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. salet.rooms["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: (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 system.character.inventory.push thing @drop name system.view.clearContent() @entering.fcall(this, system, @name) return system.view.write(thing.take.fcall(thing, system).toString()) if thing.act return system.view.write(thing.act.fcall(thing, system).toString()) 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, 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) -> cycle(this.cycle, this.name, salet.character) room = (name, salet, spec) -> spec ?= {} spec.name = name return new SaletRoom(spec).register(salet) module.exports = room