diff --git a/game/story.coffee b/game/story.coffee index 4e74486..8acaa61 100644 --- a/game/story.coffee +++ b/game/story.coffee @@ -8,10 +8,11 @@ room "world", salet, You're in a large room carved inside a giant milky rock mountain. The floor and walls are littered with signs and signatures of the previous visitors. """ - objects: - well: obj "well", + objects: [ + obj "well", 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", salet, title: (from) -> @@ -29,8 +30,8 @@ room "plaza", salet, """ else "You quickly find the central plaza." - objects: - policeman: obj "policeman", + objects: [ + obj "policeman", dsc: "There is a policeman nearby. You could ask him {{for directions.}}" act: (salet) -> if salet.character.has_mark? @@ -40,9 +41,10 @@ room "plaza", salet, """ “Here, let me mark it on your map.” """ - people: obj "people", + obj "people", dsc: "There are {{people shouting}} nearby." act: 'Just some weirdos shouting "Viva la Cthulhu!". Typical.' + ] room "shop", salet, title: "The Shop" @@ -61,13 +63,15 @@ room "lair", salet, dsc: """ The Lair of Yog-Sothoth is a very *n'gai* cave, full of *buggs-shoggogs* and *n'ghaa ng'aa*. """ - objects: - bugg: obj "bugg", + ways: ["shop"] + objects: [ + obj "bugg", dsc: "You see a particularly beautiful slimy {{bugg.}}" takeable: false act: (salet) => - salet.here().drop(@name) + salet.rooms[salet.current].drop(@name) return "You eat the bugg mass. Delicious and raw. Perhaps it's a good lair to live in." + ] dialogue "Yes", salet, "merchant", "merchant", """ Yes. @@ -81,25 +85,25 @@ room "shop-inside", salet, dsc: """ The insides are painted pastel white, honouring The Great Milk Spill of 1985. """ - objects: + objects: [ merchant: obj "merchant", dsc: "A {{merchant}} eyes you warily." takeable: false act: (system) => salet.processClick("merchdialogue") return "" + ] ### I want to be able to do this but I can't because I'm lost in all the `this` and @objects and `new`. The chain of calls is very weird and this puts an object IN EVERY ROOM for God's sake. I need someone smarter than me to fix this. - -lamp = obj "lamp", - dsc: "You see a {{lamp.}}" - takeable: true -lamp.put("shop-inside") ### +lamp = obj "lamp", salet, + takeable: true +lamp.put(salet, "shop-inside") + room "merchdialogue", salet, choices: "#merchant", dsc: """ diff --git a/lib/obj.coffee b/lib/obj.coffee index 5443135..872da0a 100644 --- a/lib/obj.coffee +++ b/lib/obj.coffee @@ -20,32 +20,34 @@ class SaletObj unless spec.name? console.error("Trying to create an object with no name") return null + + @level = 0 # if > 0 it's hidden + @order = 0 # you can use this to sort the descriptions + @look = (system, f) => + if @dsc and @dsc != "" + text = markdown(@dsc.fcall(this, system, f).toString()) + text = system.view.wrapLevel(text, @level) + # replace braces {{}} with link to _act_ + return parsedsc(text, @name) + @takeable = false + @take = (system) => "You take the #{@name}." # taking to inventory + @act = (system) => "You don't find anything extraordinary about the #{@name}." # object action + @dsc = (system) => "You see a {{#{@name}}} here." # object description + @inv = (system) => "It's a {{#{@name}.}}" # inventory description + @location = "" + @put = (salet, location) => + @level = 0 # this is scenery + if salet.rooms[location]? + @location = location + salet.rooms[location].take(this) + @delete = (salet, location = false) => + if location == false + location = @location + salet.rooms[location].drop(this) + for key, value of spec this[key] = value - level: 0 # if > 0 it's hidden - order: 0 # you can use this to sort the descriptions - look: (system, f) => - if @dsc and @dsc != "" - text = markdown(@dsc.fcall(this, system, f).toString()) - text = system.view.wrapLevel(text, @level) - # replace braces {{}} with link to _act_ - return parsedsc(text, @name) - takeable: false - take: (system) => "You take the #{@name}." # taking to inventory - act: (system) => "You don't find anything extraordinary about the #{@name}." # object action - dsc: (system) => "You see a {{#{@name}}} here." # object description - inv: (system) => "It's a {{#{@name}.}}" # inventory description - location: "" - put: (location) => - @level = 0 # this is scenery - if salet.rooms[location]? - salet.rooms[location].take(this) - @location = location - delete: (location = false) => - if location == false - location = @location - salet.rooms[location].drop(this) - + obj = (name, spec) -> spec ?= {} spec.name = name diff --git a/lib/room.coffee b/lib/room.coffee index 43e75d6..86d3ad1 100644 --- a/lib/room.coffee +++ b/lib/room.coffee @@ -19,239 +19,236 @@ 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() + + 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)) + + 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[thing.name] = 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.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 + 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 - 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)) - - 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 - console.log thing - 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[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 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.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 - 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) - + room = (name, salet, spec) -> spec ?= {} spec.name = name diff --git a/lib/salet.coffee b/lib/salet.coffee index d3fe645..b405533 100644 --- a/lib/salet.coffee +++ b/lib/salet.coffee @@ -23,6 +23,8 @@ class Character ### This is the control structure, it has minimal amount of data and this data is volatile anyway (as in, it won't get saved). + +There is only one instance of this class. ### class Salet # REDEFINE THIS IN YOUR GAME diff --git a/lib/view.coffee b/lib/view.coffee index 952995d..b24dcf2 100644 --- a/lib/view.coffee +++ b/lib/view.coffee @@ -9,6 +9,8 @@ You don't need to call this module from the game directly. The abstraction goal here is to provide the author with a freedom to style his 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.) + +There is only one instance of this class, and it's stored as `salet.view`. ### assert = (msg, assertion) -> console.assert assertion, msg