# I confess that this world model heavily borrows from INSTEAD engine. - A.Y. require('./salet.coffee') obj = require('./obj.coffee') markdown = require('./markdown.coffee') assert = (msg, assertion) -> console.assert assertion, msg Function.prototype.fcall = Function.prototype.call Boolean.prototype.fcall = () -> return this.valueOf() 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) -> @visited = 0 @title = "Room" @objects = {} @canView = true @canChoose = true @priority = 1 @displayOrder = 1 @tags = [] @choices = "" @optionText = "Choice" # room illustration image, VN-style. Can be a GIF or WEBM. Can be a function. @pic = false @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() else system.view.removeTransient() if f != @name and f? @visited++ if system.rooms[f].exit? system.rooms[f].exit system, @name if @enter @enter system, f if not @extendSection classes = if @classes then ' ' + @classes.join(' ') else '' room = document.getElementById('current-room') if room? room.removeAttribute('id') system.view.append "
" if f != @name and @before? system.view.write markdown(@before.fcall(this, system, f)) system.view.write @look system, f if f != @name and @after? system.view.write markdown(@after.fcall(this, system, f)) if @beforeChoices? @beforeChoices.fcall(this, system, f) if @choices system.view.writeChoices(system, system.getSituationIdChoices(@choices, @maxChoices)) if @afterChoices? @afterChoices.fcall(this, system, f) if system.autosave system.saveGame() ### 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 and @dsc != "" dsc = @dsc.fcall(this, system, f).toString() retval += markdown(dsc) objDescriptions = [] for thing in @objects if thing.name and typeof(thing.look) == "function" and thing.level == 0 and thing.look(system, f) objDescriptions.push ({ order: thing.order, content: thing.look(system, f) }) objDescriptions.sort((a, b) -> return a.order - b.order ) for description in objDescriptions retval += description.content return retval ### Puts an object in this room. ### @take = (thing) => @objects.push(thing) @drop = (name) => for thing in @objects if thing.name == name index = @objects.indexOf(thing) @objects.splice(index, 1) ### 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 thing in @objects if thing.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.take(thing) @drop name system.view.clearContent() @entering.fcall(this, system, @name) return system.view.write(thing.take.fcall(thing, system).toString()) if thing.act system.view.changeLevel(thing.level) return system.view.write( system.view.wrapLevel( thing.act.fcall(thing, system).toString(), thing.level ) ) # the loop is done but no return came - match not found console.error("Could not find #{link[2]} 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) => responses = @cycle if typeof responses == "function" responses = responses() cycleIndex = window.localStorage.getItem("cycleIndex") cycleIndex ?= 0 response = responses[cycleIndex] cycleIndex++ if cycleIndex == responses.length cycleIndex = 0 window.localStorage.setItem("cycleIndex", cycleIndex) return salet.view.cycleLink(response) } for index, value of spec this[index] = value return this room = (name, salet, spec) -> spec ?= {} spec.name = name return new SaletRoom(spec).register(salet) module.exports = room