From 5e7f4087aa167e97c92dff9be20c005a6f8ab55b Mon Sep 17 00:00:00 2001 From: Alexander Yakovlev Date: Mon, 1 Feb 2016 14:22:23 +0700 Subject: [PATCH] Salet is now passed as an argument. Also, no frequency filters on choice list. --- game/begin.coffee | 12 +++- game/init.coffee | 7 -- game/story.coffee | 14 ++-- lib/dialogue.coffee | 4 +- lib/markdown.coffee | 2 +- lib/room.coffee | 86 +++++------------------ lib/salet.coffee | 166 +++++++++++++------------------------------- lib/view.coffee | 102 +++++++++++++++++++++++---- 8 files changed, 176 insertions(+), 217 deletions(-) delete mode 100644 game/init.coffee diff --git a/game/begin.coffee b/game/begin.coffee index 328164c..b3b6dd3 100644 --- a/game/begin.coffee +++ b/game/begin.coffee @@ -3,7 +3,15 @@ room = require("../../lib/room.coffee") obj = require('../../lib/obj.coffee') dialogue = require('../../lib/dialogue.coffee') oneOf = require('../../lib/oneOf.coffee') -require('../../lib/salet.coffee') +Salet = require('../../lib/salet.coffee') + +salet = new Salet +salet.view.init(salet) +salet.game_id = "your-game-id-here" +salet.game_version = "1.0" +$(document).ready(() -> + salet.beginGame() +) ### Element helpers. There is no real need to build monsters like a().id("hello") @@ -25,7 +33,7 @@ cyclelink = (content) -> # The first room of the game. # For accessibility reasons the text is provided in HTML, not here. -room "start", +room "start", salet, dsc: """ """, choices: "#start" diff --git a/game/init.coffee b/game/init.coffee deleted file mode 100644 index 3cd1dc7..0000000 --- a/game/init.coffee +++ /dev/null @@ -1,7 +0,0 @@ -# This is where you initialize your game. - -$(document).ready(() -> - salet.game_id = "your-game-id-here" - salet.game_version = "1.0" - salet.beginGame() -) diff --git a/game/story.coffee b/game/story.coffee index bcc8278..3d54a10 100644 --- a/game/story.coffee +++ b/game/story.coffee @@ -1,4 +1,4 @@ -room "world", +room "world", salet, tags: ["start"], optionText: "Enter the world", ways: ["plaza"] @@ -13,7 +13,7 @@ room "world", 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", +room "plaza", salet, title: (from) -> if from == "world" return "Upwards" @@ -44,7 +44,7 @@ room "plaza", dsc: "There are {{people shouting}} nearby." act: 'Just some weirdos shouting "Viva la Cthulhu!". Typical.' -room "shop", +room "shop", salet, title: "The Shop" #pic: "http://loremflickr.com/640/300/room,shop" ways: ["plaza", "shop-inside", "lair"] @@ -55,7 +55,7 @@ room "shop", You are standing in front of a picturesque sign. It's cold here. """ -room "lair", +room "lair", salet, title: "The Lair" before: "Finding The Lair is easy. Leaving it is impossible. Your game ends here." dsc: """ @@ -69,11 +69,11 @@ room "lair", here().drop(@name) return "You eat the bugg mass. Delicious and raw. Perhaps it's a good lair to live in." -dialogue "Yes", "merchant", "merchant", """ +dialogue "Yes", salet, "merchant", "merchant", """ Yes. """ -room "shop-inside", +room "shop-inside", salet, ways: ["shop"] tags: ["merchant"] optionText: "End the conversation" @@ -100,7 +100,7 @@ lamp = obj "lamp", lamp.put("shop-inside") ### -room "merchdialogue", +room "merchdialogue", salet, choices: "#merchant", dsc: """ Nice day, isn't it? diff --git a/lib/dialogue.coffee b/lib/dialogue.coffee index bce17b7..240a7c9 100644 --- a/lib/dialogue.coffee +++ b/lib/dialogue.coffee @@ -15,8 +15,8 @@ Usage: Point out a thing in her purse (mildly) """, "character.sandbox.mild = true" ### -dialogue = (title, startTag, endTag, text, effect) -> - retval = room(randomid(), { +dialogue = (title, salet, startTag, endTag, text, effect) -> + retval = room(randomid(), salet, { optionText: title dsc: text clear: false # backlog is useful in dialogues diff --git a/lib/markdown.coffee b/lib/markdown.coffee index 8cfdea8..0277668 100644 --- a/lib/markdown.coffee +++ b/lib/markdown.coffee @@ -4,7 +4,7 @@ Implies that you don't mix up your tabs and spaces. Copyright 2015 Bruno Dias ### normaliseTabs = (text) -> - unless text? + unless text? and typeof(text) == "string" return "" lines = text.split('\n'); indents = lines diff --git a/lib/room.coffee b/lib/room.coffee index a679a8c..a84fac6 100644 --- a/lib/room.coffee +++ b/lib/room.coffee @@ -1,14 +1,12 @@ # 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') -Random = require('./random.js') -languages = require('./localize.coffee') -require('./salet.coffee') # Assertion -assert = console.assert +assert = (msg, assertion) -> console.assert assertion, msg way_to = (content, ref) -> return "#{content}" @@ -21,43 +19,6 @@ addClass = (element, className) -> else element.className += ' ' + className -update_ways = (ways, name) -> - content = "" - distances = [] - if ways then for way in ways - if salet.rooms[way]? - title = salet.rooms[way].title.fcall(this, name) - content += way_to(title, way) - distances.push({ - key: way - distance: salet.rooms[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 constructor: (spec) -> for index, value of spec @@ -73,7 +34,6 @@ class SaletRoom canView: true canChoose: true priority: 1 - frequency: 1 displayOrder: 1 tags: [] choices: "" @@ -84,14 +44,14 @@ class SaletRoom distance: Infinity # distance to the destination clear: true # clear the screen on entering the room? - entering: (character, system, from) => + 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: (character, system, to) => + exit: (system, to) => return true ### @@ -102,7 +62,7 @@ class SaletRoom 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) => + enter: (system, from) => return true ### @@ -148,25 +108,25 @@ class SaletRoom if not @extendSection room_content += "" - system.write(room_content) + system.view.write(room_content) if @choices - system.writeChoices(system.getSituationIdChoices(@choices, @minChoices, @maxChoices)) + 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: (character, system, f) => - update_ways(@ways, @name) + look: (system, f) => + system.view.updateWays(system, @ways, @name) retval = "" if @pic - retval += '
'+picture_tag(@pic.fcall(this, character, system, f))+'
' + retval += '
'+system.view.pictureTag(@pic.fcall(this, system, f))+'
' # Print the room description if @dsc - retval += markdown(@dsc.fcall(this, character, system, f)) + retval += markdown(@dsc.fcall(this, system, f)) for name, thing of @objects retval += thing.look() @@ -189,7 +149,7 @@ class SaletRoom 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) => + act: (system, action) => if (link = action.match(/^_(act|cycle)_(.+)$/)) #object action for name, thing of @objects if name == link[2] @@ -215,15 +175,15 @@ class SaletRoom responses = { writer: (ref) -> - content = that.writers[ref].fcall(that, character, system, action) + content = that.writers[ref].fcall(that, system, action) output = markdown(content) system.writeInto(output, '#current-room') replacer: (ref) -> - content = that.writers[ref].fcall(that, character, system, action) + content = that.writers[ref].fcall(that, 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) + content = that.writers[ref].fcall(that, system, action) output = markdown(content) system.writeInto(output, '#'+ref) } @@ -236,7 +196,7 @@ class SaletRoom throw new Error("Tried to call undefined writer: #{action}"); responses[responder](ref); else if (@actions.hasOwnProperty(action)) - @actions[action].call(this, character, system, action); + @actions[action].call(this, system, action); else throw new Error("Tried to call undefined action: #{action}"); @@ -253,7 +213,7 @@ class SaletRoom node.distance = current_room.distance + 1 candidates.push(node) - register: () => + register: (salet) => if not @name? console.error("Situation has no name") return this @@ -264,17 +224,9 @@ class SaletRoom cyclewriter: (character) -> cycle(this.cycle, this.name, character) -room = (name, spec) -> +room = (name, salet, spec) -> spec ?= {} spec.name = name - retval = new SaletRoom(spec) - $(document).ready(() -> - if salet - retval.register() - else - sleep(1000) - retval.register() - ) - return retval + return new SaletRoom(spec).register(salet) module.exports = room diff --git a/lib/salet.coffee b/lib/salet.coffee index 04e0d53..7fe80c7 100644 --- a/lib/salet.coffee +++ b/lib/salet.coffee @@ -1,6 +1,7 @@ markdown = require('./markdown.coffee') SaletView = require('./view.coffee') Random = require('./random.js') +languages = require('./localize.coffee') ### fcall() (by analogy with fmap) is added to the prototypes of both String and @@ -9,6 +10,8 @@ when called on a String, it only returns the string itself, discarding any input ### Function.prototype.fcall = Function.prototype.call; +Boolean.prototype.fcall = () -> + return this String.prototype.fcall = () -> return this @@ -33,31 +36,6 @@ parseFn = (str) -> # Salet's default is a general URL-safe expression. linkRe = /^([0-9A-Za-z_-]+|\.)(\/([0-9A-Za-z_-]+))?$/ -# Returns HTML from the given content with the non-raw links wired up. -augmentLinks = (content) -> - output = $(content) - - # Wire up the links for regular tags. - output.find("a").each((index, element) -> - a = $(element) - href = a.attr('href') - if (!a.hasClass("raw")|| href.match(/[?&]raw[=&]?/)) - if (href.match(linkRe)) - a.click((event) -> - event.preventDefault() - - # If we're a once-click, remove all matching links. - if (a.hasClass("once") || href.match(/[?&]once[=&]?/)) - @view.clearLinks(href) - - processClick(href) - return false - ) - else - a.addClass("raw") - ) - return output - ### This is the control structure, it has minimal amount of data and this data is volatile anyway (as in, it won't get saved). @@ -149,17 +127,6 @@ class Salet situations that force the player to go to one destination if the player is out of money, for example. - If a minChoices value is given, then the function will attempt - to return at least that many results. If not enough results are - available at the highest priority, then lower priorities will - be considered in turn, until enough situations are found. In - the example above, if we had a minChoices of three, then all - three situations would be returned, even though they have - different priorities. If you need to return all valid - situations, regardless of their priorities, set minChoices to a - large number, such as `Number.MAX_VALUE`, and leave maxChoices - undefined. - If a maxChoices value is given, then the function will not return any more than the given number of results. If there are more than this number of results possible, then the highest @@ -181,90 +148,65 @@ class Salet Before this function returns its result, it sorts the situations in increasing order of their displayOrder values. ### - getSituationIdChoices: (listOfOrOneIdsOrTags, minChoices, maxChoices) -> + getSituationIdChoices: (listOfOrOneIdsOrTags, maxChoices) -> datum = null i = 0 # First check if we have a single string for the id or tag. - if ($.type(listOfOrOneIdsOrTags) == 'string') + if (typeof(listOfOrOneIdsOrTags) == 'string') listOfOrOneIdsOrTags = [listOfOrOneIdsOrTags] # First we build a list of all candidate ids. - allIds = {} + allIds = [] for tagOrId in listOfOrOneIdsOrTags if (tagOrId.substr(0, 1) == '#') - ids = getSituationIdsWithTag(tagOrId.substr(1)) + ids = @getRoomsTagged(tagOrId.substr(1)) for id in ids - allIds[id] = true - else - allIds[tagOrId] = true + allIds.push(id) + else #it's an id, not a tag + allIds.push(tagOrId) #Filter out anything that can't be viewed right now. - currentSituation = @getCurrentRoom() - viewableSituationData = [] - for situationId in allIds - situation = @rooms[situationId] - assert(situation, "unknown_situation".l({id:situationId})) + currentRoom = @getCurrentRoom() + viewableRoomData = [] + for roomId in allIds + room = @rooms[roomId] + assert(room, "unknown_situation".l({id:roomId})) - if (situation.canView(character, salet, currentSituation)) - #While we're here, get the selection data. - viewableSituationDatum = situation.choiceData(character, salet, currentSituation) - viewableSituationDatum.id = situationId - viewableSituationData.push(viewableSituationDatum) + if (room.canView.fcall(this, currentRoom)) + viewableRoomData.push({ + priority: room.priority + id: roomId + displayOrder: room.displayOrder + }) # Then we sort in descending priority order. - viewableSituationData.sort((a, b) -> + viewableRoomData.sort((a, b) -> return b.priority - a.priority ) committed = [] - candidatesAtLastPriority = [] - lastPriority - # In descending priority order. - for datum in viewableSituationData - if (datum.priority != lastPriority) - if (lastPriority != undefined) - # We've dropped a priority group, see if we have enough - # situations so far, and stop if we do. - if (minChoices == undefined || i >= minChoices) - break - # Continue to acccumulate more options. - committed.push.apply(committed, candidatesAtLastPriority); - candidatesAtLastPriority = []; - lastPriority = datum.priority; - candidatesAtLastPriority.push(datum); - # So the values in committed we're committed to, because without - # them we wouldn't hit our minimum. But those in - # candidatesAtLastPriority might take us over our maximum, so - # figure out how many we should choose. - totalChoices = committed.length + candidatesAtLastPriority.length - if (maxChoices == undefined || maxChoices >= totalChoices) - # We can use all the choices. - committed.push.apply(committed, candidatesAtLastPriority) - else if (maxChoices >= committed.length) - # We can only use the commited ones. - # NO-OP - else - # We have to sample the candidates, using their relative frequency. - candidatesToInclude = maxChoices - committed.length; - for datum in candidatesAtLastPriority - datum._frequencyValue = this.rnd.random() / datum.frequency; - candidatesToInclude.sort((a, b) -> - return a.frequencyValue - b.frequencyValue; - ) - chosen = candidatesToInclude.slice(0, candidatesToInclude) - committed.push.apply(committed, chosen) + # if we need to filter out the results + if (maxChoices? && viewableRoomData.length > maxChoices) + viewableRoomData = viewableRoomData[-maxChoices..] + + for candidateRoom in viewableRoomData + committed.push({ + id: candidateRoom.id + displayOrder: candidateRoom.displayOrder + }) # Now sort in ascending display order. committed.sort((a, b) -> - return a.displayOrder - b.displayOrder + return a.displayOrder - b.displayOrder ) # And return as a list of ids only. result = [] for i in committed result.push(i.id) + return result # This is the data on the player's progress that gets saved. @@ -313,7 +255,7 @@ class Salet @linkStack = [] # Handle each link in turn. - processOneLink(code); + @processOneLink(code); while (@linkStack.length > 0) code = linkStack.shift() processOneLink(code) @@ -322,10 +264,10 @@ class Salet @linkStack = null; # Scroll to the top of the new content. - @endOutputTransaction() + @view.endOutputTransaction() # We're able to save, if we weren't already. - @enableSaving() + @view.enableSaving() ### This gets called to actually do the work of processing a code. @@ -341,9 +283,8 @@ class Salet action = match[3] # Change the situation - if situation != '.' - if situation != current - @doTransitionTo(situation) + if situation != '.' and situation != @current_room + @doTransitionTo(situation) else # We should have an action if we have no situation change. assert(action, "link_no_action".l()) @@ -370,9 +311,9 @@ class Salet # This gets called when the user clicks a link to carry out an action. processClick: (code) -> now = (new Date()).getTime() * 0.001 - @time = now - startTime + @time = now - @startTime @progress.sequence.push({link:code, when:@time}) - processLink(code) + @processLink(code) # Transitions between situations. doTransitionTo: (newRoomId) -> @@ -385,11 +326,11 @@ class Salet # We might not have an old situation if this is the start of the game. if (oldRoom and @exit) @exit(oldRoomId, newRoomId) - + @current = newRoomId # Remove links and transient sections. - @view.remove_transient(@interactive) + @view.removeTransient(@interactive) # Notify the incoming situation. if (@enter) @@ -407,19 +348,19 @@ class Salet game state across save/erase cycles, meaning that character.sandbox no longer has to be the end-all be-all repository of game state. ### - erase_save: (force = false) => + eraseSave: (force = false) => save_id = @getSaveId() # save slot if (localStorage.getItem(saveId) and (force or confirm("erase_message".l()))) localStorage.removeItem(saveId) window.location.reload() # Find and return a list of ids for all situations with the given tag. - getSituationIdsWithTag: (tag) => + getRoomsTagged: (tag) => result = [] - for situationId, situation of @situations - for i in situation.tags + for id, room of @rooms + for i in room.tags if (i == tag) - result.push(situationId) + result.push(id) break return result @@ -477,7 +418,7 @@ class Salet @view.disableSaving() @view.enableErasing() catch err - @erase_save(true) + @eraseSave(true) else @progress.seed = new Date().toString() @@ -485,8 +426,6 @@ class Salet @rnd = new Random(@progress.seed) @progress.sequence = [{link:@start, when:0}] - @view.clearContent() - # Start the game @startTime = new Date().getTime() * 0.001 if (@init) @@ -515,11 +454,4 @@ class Salet return Boolean place.visited return 0 -# Set up the game when everything is loaded. -$(document).ready(() -> - salet = new Salet - salet.view.init() - if (salet.view.hasLocalStorage()) - $("#erase").click(salet.erase_save) # is Salet defined here? - $("#save").click(salet.saveGame) -) +module.exports = Salet diff --git a/lib/view.coffee b/lib/view.coffee index 36f453c..24f9f6d 100644 --- a/lib/view.coffee +++ b/lib/view.coffee @@ -1,3 +1,4 @@ +markdown = require('./markdown.coffee') ### Salet interface configuration. In a typical MVC structure, this is the View. @@ -10,8 +11,13 @@ 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.) ### +assert = (msg, assertion) -> console.assert assertion, msg + +way_to = (content, ref) -> + return "#{content}" + class SaletView - init: () -> + init: (salet) -> $("#content, #ways").on("click", "a", (event) -> event.preventDefault() salet.processClick($(this).attr("href")) @@ -23,6 +29,9 @@ class SaletView $("#load").on("click", "a", (event) -> window.location.reload() ) + if (@hasLocalStorage()) + $("#erase").click(salet.erase_save) # is Salet defined here? + $("#save").click(salet.saveGame) disableSaving: () -> $("#save").addClass('disabled') @@ -67,11 +76,15 @@ class SaletView return if typeof content == "function" content = content() + if content instanceof jQuery + content = content[0].outerHTML block = document.getElementById("current-room") if block block.innerHTML = block.innerHTML + markdown(content) else - console.error("No current situation found.") + # most likely this is the starting room + block = document.getElementById("content") + block.innerHTML = content ### Turns any links that target the given href into plain @@ -98,25 +111,25 @@ class SaletView manually, ot else use the `getSituationIdChoices` method to return an ordered list of valid viewable situation ids. ### - writeChoices: (listOfIds) -> - if (listOfIds.length == 0) + writeChoices: (salet, listOfIds) -> + if (not listOfIds? or listOfIds.length == 0) return - currentSituation = getCurrentSituation(); + currentRoom = salet.getCurrentRoom(); $options = $("