diff --git a/game/begin.coffee b/game/begin.coffee index e3cd81d..dfd7b5c 100644 --- a/game/begin.coffee +++ b/game/begin.coffee @@ -3,11 +3,10 @@ room = require("../../lib/room.coffee") obj = require('../../lib/obj.coffee') dialogue = require('../../lib/dialogue.coffee') oneOf = require('../../lib/oneOf.coffee') -require('../../lib/interface.coffee') salet = require('../../lib/salet.coffee') -undum.game.id = "your-game-id-here" -undum.game.version = "1.0" +salet.game_id = "your-game-id-here" +salet.game_version = "1.0" ### Element helpers. There is no real need to build monsters like a().id("hello") diff --git a/game/init.coffee b/game/init.coffee index 658e2ad..f7afeab 100644 --- a/game/init.coffee +++ b/game/init.coffee @@ -1,6 +1,4 @@ # This is where you initialize your game. # All code in this file comes last, so the game is almost ready by this point. -undum.game.init = (character, system) -> - -window.onload = undum.begin +salet.init = (character, system) -> diff --git a/lib/obj.coffee b/lib/obj.coffee index 49b42ae..0aa4bd0 100644 --- a/lib/obj.coffee +++ b/lib/obj.coffee @@ -37,11 +37,13 @@ class SaletObj location: "" put: (location) => @level = 0 # this is scenery - if undum.game.situations[location]? - undum.game.situations[location].take(this) + if salet.rooms[location]? + salet.rooms[location].take(this) @location = location - delete: () => - undum.game.situations[@location].objects.remove(this) + delete: (location = false) => + if location == false + location = @location + salet.rooms[location].drop(this) obj = (name, spec) -> spec ?= {} diff --git a/lib/room.coffee b/lib/room.coffee index 33b994b..5a21665 100644 --- a/lib/room.coffee +++ b/lib/room.coffee @@ -5,6 +5,7 @@ markdown = require('./markdown.coffee') cycle = require('./cycle.coffee') Random = require('./random.js') languages = require('./localize.coffee') +salet = require('./salet.coffee') # Assertion assert = console.assert @@ -28,12 +29,12 @@ update_ways = (ways, name) -> content = "" distances = [] if ways then for way in ways - if undum.game.situations[way]? - title = undum.game.situations[way].title.fcall(this, name) + if salet.rooms[way]? + title = salet.rooms[way].title.fcall(this, name) content += way_to(title, way) distances.push({ key: way - distance: undum.game.situations[way].distance + distance: salet.rooms[way].distance }) else document.querySelector(".ways h2").style.display = "none" @@ -61,9 +62,8 @@ picture_tag = (picture) -> """ return "Room illustration" -class SaletRoom extends undum.Situation +class SaletRoom constructor: (spec) -> - undum.Situation.call(this, spec) for index, value of spec this[index] = value return this @@ -124,8 +124,8 @@ class SaletRoom extends undum.Situation if f != @name and f? @visited++ - if undum.game.situations[f].exit? - undum.game.situations[f].exit(character, system, @name) + if salet.rooms[f].exit? + salet.rooms[f].exit(character, system, @name) if @enter @enter character, system, f @@ -184,7 +184,7 @@ class SaletRoom extends undum.Situation @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 = {} + salet.rooms["start"].objects = {} drop: (name) => delete @objects[name] @@ -261,7 +261,7 @@ class SaletRoom extends undum.Situation if not @name? console.error("Situation has no name") return this - undum.game.situations[@name] = this + salet.rooms[@name] = this return this writers: diff --git a/lib/salet.coffee b/lib/salet.coffee index 9cd4325..941a402 100644 --- a/lib/salet.coffee +++ b/lib/salet.coffee @@ -1,5 +1,6 @@ markdown = require('./markdown.coffee') view = require('./view.coffee') +Random = require('./random.js') ### fcall() (by analogy with fmap) is added to the prototypes of both String and @@ -11,6 +12,10 @@ Function.prototype.fcall = Function.prototype.call; String.prototype.fcall = () -> return this +assert = (msg, assertion) -> console.assert assertion, msg + +class Character + # Utility functions parseFn = (str) -> @@ -18,23 +23,12 @@ parseFn = (str) -> return str fstr = """ -(function(character, system, situation) { +(function(character, situation) { #{str} #}) """ return eval(fstr) -# Feature detection -hasLocalStorage = () -> - hasStorage = false - try - hasStorage = ('localStorage' in window) && - window.localStorage != null && - window.localStorage != undefined; - catch err - hasStorage = false - return hasStorage - # Regular expression to catch every link action. # Salet's default is a general URL-safe expression. linkRe = /^([0-9A-Za-z_-]+|\.)(\/([0-9A-Za-z_-]+))?$/ @@ -54,7 +48,7 @@ augmentLinks = (content) -> # If we're a once-click, remove all matching links. if (a.hasClass("once") || href.match(/[?&]once[=&]?/)) - system.clearLinks(href) + view.clearLinks(href) processClick(href) return false @@ -68,7 +62,11 @@ augmentLinks = (content) -> This is the control structure, it has minimal amount of data and this data is volatile anyway (as in, it won't get saved). ### -Salet = { +class Salet + # REDEFINE THIS IN YOUR GAME + game_id: null + game_version: "1.0" + rnd: null time: 0 @@ -84,23 +82,23 @@ Salet = { (setting initial quality values, setting the character-text. This is optional, however, as set-up processing could also be done by the first situation's - enter function. If this function is given it should have - the signature function(character, system). + enter function. ### - init: null + init: (character) -> + @character = character ### This function is called before entering any new situation. It is called before the corresponding situation has its `enter` method called. ### - entering: (character, system, oldSituationId, newSituationId) -> + entering: (character, oldSituationId, newSituationId) -> ### Hook for when the situation has already been carried out and printed. ### - afterEnter: (character, system, oldSituationId, newSituationId) -> + afterEnter: (character, oldSituationId, newSituationId) -> ### This function is called before carrying out any action in @@ -112,21 +110,21 @@ Salet = { on to the situation. Note that this is the only one of these global handlers that can consume the event. ### - beforeAction: (character, system, situationId, actionId) -> + beforeAction: (character, situationId, actionId) -> ### This function is called after carrying out any action in any situation. It is called after the corresponding situation has its `act` method called. ### - afterAction: (character, system, situationId, actionId) -> + afterAction: (character, situationId, actionId) -> ### This function is called after leaving any situation. It is called after the corresponding situation has its `exit` method called. ### - exit: (character, system, oldSituationId, newSituationId) -> + exit: (character, oldSituationId, newSituationId) -> ### Returns a list of situation ids to choose from, given a set of @@ -202,15 +200,15 @@ Salet = { allIds[tagOrId] = true #Filter out anything that can't be viewed right now. - currentSituation = getCurrentSituation() + currentSituation = @getCurrentRoom() viewableSituationData = [] for situationId in allIds - situation = game.situations[situationId] + situation = @rooms[situationId] assert(situation, "unknown_situation".l({id:situationId})) - if (situation.canView(character, system, currentSituation)) + if (situation.canView(character, salet, currentSituation)) #While we're here, get the selection data. - viewableSituationDatum = situation.choiceData(character, system, currentSituation) + viewableSituationDatum = situation.choiceData(character, salet, currentSituation) viewableSituationDatum.id = situationId viewableSituationData.push(viewableSituationDatum) @@ -283,10 +281,6 @@ Salet = { # The Id of the current situation the player is in. current: null; - # This is the current character. It should be reconstructable - # from the above progress data. - character: {}; - # Tracks whether we're in interactive mode or batch mode. interactive: true @@ -296,14 +290,14 @@ Salet = { # The stack of links, resulting from the last action, still be to resolved. linkStack: null - getCurrentSituation: () => + getCurrentRoom: () => if (@current) return @rooms[@current] return null # Gets the unique id used to identify saved games. getSaveId: (slot = "") -> - return 'salet_'+game.id+'_'+game.version+'_'+slot; + return 'salet_'+@game_id+'_'+@game_version+'_'+slot; # This gets called when a link needs to be followed, regardless # of whether it was user action that initiated it. @@ -349,49 +343,49 @@ Salet = { # Change the situation if situation != '.' if situation != current - doTransitionTo(situation) + @doTransitionTo(situation) else # We should have an action if we have no situation change. assert(action, "link_no_action".l()) # Carry out the action if (action) - situation = getCurrentSituation() + situation = @getCurrentRoom() if (situation) - if (game.beforeAction) + if (@beforeAction) # Try the global act handler, and see if we need # to notify the situation. - consumed = game.beforeAction( - character, system, current, action + consumed = @beforeAction( + character, current, action ) if (consumed != true) - situation.act(character, system, action) + situation.act(character, action) else # We have no global act handler, always notify the situation. - situation.act(character, system, action) + situation.act(character, action) - if (game.afterAction) - game.afterAction(character, system, current, action) + if (@afterAction) + @afterAction(character, current, action) # This gets called when the user clicks a link to carry out an action. processClick: (code) -> now = (new Date()).getTime() * 0.001 - system.time = now - startTime - progress.sequence.push({link:code, when:system.time}) + @time = now - startTime + @progress.sequence.push({link:code, when:@time}) processLink(code) # Transitions between situations. doTransitionTo: (newSituationId) -> oldSituationId = current - oldSituation = getCurrentSituation() - newSituation = game.situations[newSituationId] + oldSituation = @getCurrentRoom() + newSituation = @rooms[newSituationId] assert(newSituation, "unknown_situation".l({id:newSituationId})) # We might not have an old situation if this is the start of the game. if (oldSituation) - if (game.exit) - game.exit(character, system, oldSituationId, newSituationId); + if (@exit) + @exit(@character, oldSituationId, newSituationId); # Remove links and transient sections. view.remove_transient(@interactive) @@ -400,13 +394,13 @@ Salet = { current = newSituationId # Notify the incoming situation. - if (game.enter) - game.enter(character, system, oldSituationId, newSituationId) - newSituation.entering(character, system, oldSituationId) + if (@enter) + @enter(@character, this, oldSituationId, newSituationId) + newSituation.entering(@character, oldSituationId) # additional hook for when the situation text has already been printed - if (game.afterEnter) - game.afterEnter(character, system, oldSituationId, newSituationId) + if (@afterEnter) + @afterEnter(@character, oldSituationId, newSituationId) ### Erases the character in local storage. This is permanent! @@ -436,10 +430,10 @@ Salet = { # Store when we're saving the game, to avoid exploits where a # player loads their file to gain extra time. now = (new Date()).getTime() * 0.001 - progress.saveTime = now - startTime + @progress.saveTime = now - startTime # Save the game. - localStorage.setItem(getSaveId(), JSON.stringify(progress)) + localStorage.setItem(getSaveId(), JSON.stringify(@progress)) # Switch the button highlights. view.disableSaving() @@ -448,33 +442,33 @@ Salet = { # Loads the game from the given data loadGame: (characterData) -> - progress = characterData + @progress = characterData character = new Character() - system.rnd = new Random(progress.seed) + @rnd = new Random(@progress.seed) view.clearContent() # Now play through the actions so far: - if (game.init) - game.init(character, system) + if (@init) + @init(character) # Run through all the player's history. interactive = false - for step in progress.sequence + for step in @progress.sequence # The action must be done at the recorded time. - system.time = step.when + @time = step.when processLink(step.link) interactive = true # Reverse engineer the start time. now = new Date().getTime() * 0.001 - startTime = now - progress.saveTime + startTime = now - @progress.saveTime - game_begin: () -> + beginGame: () -> # Handle storage. storedCharacter = false - if (@hasLocalStorage()) + if (view.hasLocalStorage()) storedCharacter = localStorage.getItem(@getSaveId()) if (storedCharacter) @@ -485,22 +479,22 @@ Salet = { catch err @erase_save(true) else - progress.seed = new Date().toString() + @progress.seed = new Date().toString() character = new Character() - system.rnd = new Random(progress.seed) - progress.sequence = [{link:game.start, when:0}] + @rnd = new Random(@progress.seed) + @progress.sequence = [{link:@start, when:0}] view.clearContent() # Start the game startTime = new Date().getTime() * 0.001 - system.time = 0 - if (game.init) - game.init(character, system) + @time = 0 + if (@init) + @init(character) # Do the first state. - doTransitionTo(game.start); + @doTransitionTo(@start); # Any point that an option list appears, its options are its first links. $("body").on('click', "ul.options li, #menu li", (event) -> @@ -510,14 +504,15 @@ Salet = { $(link.get(0)).click() ); -} +salet = new Salet # Set up the game when everything is loaded. $(document).ready(() -> - salet = new Salet - if (salet.hasLocalStorage()) + if (view.hasLocalStorage()) $("#erase").click(salet.erase_save) # is Salet defined here? $("#save").click(salet.saveGame) - salet.game_begin() + salet.beginGame() ) + +module.exports = salet diff --git a/lib/view.coffee b/lib/view.coffee index ea80dc9..d616f76 100644 --- a/lib/view.coffee +++ b/lib/view.coffee @@ -3,13 +3,18 @@ Salet interface configuration. In a typical MVC structure, this is the View. Only it knows about the DOM structure. Other modules just use its API or prepare the HTML for insertion. +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.) ### class SaletView init: () -> $("#ways").on("click", "a", (event) -> event.preventDefault() - undum.processClick($(this).attr("href")) + salet.processClick($(this).attr("href")) ) $("#inventory").on("click", "a", (event) -> event.preventDefault() @@ -52,7 +57,7 @@ class SaletView ### clearContent: (elementSelector = "#content") -> if (elementSelector == "#content") # empty the intro with the content - document.findElementById("intro").innerHTML = "" + document.getElementById("intro").innerHTML = "" document.querySelector(elementSelector).innerHTML = "" # Write content to current room @@ -176,6 +181,17 @@ class SaletView # it somewhere near the bottom. scrollBottomTo(newBottom+100 - optionHeight); + # Feature detection + hasLocalStorage: () -> + hasStorage = false + try + hasStorage = ('localStorage' in window) && + window.localStorage != null && + window.localStorage != undefined; + catch err + hasStorage = false + return hasStorage + view = new SaletView $(document).ready(() -> view.init()