diff --git a/lib/salet.coffee b/lib/salet.coffee index 3a01d64..9e67365 100644 --- a/lib/salet.coffee +++ b/lib/salet.coffee @@ -20,7 +20,7 @@ parseFn = (str) -> #{str} #}) """ - return eval(fstr); + return eval(fstr) # Scrolls the top of the screen to the specified point scrollTopTo = (value) -> @@ -63,143 +63,6 @@ augmentLinks = (content) -> ) return output -/* Erases the character in local storage. This is permanent! -/* To restart the game afterwards, we perform a simple page refresh. -/* This guarantees authors don't have to care about "tainting" the -/* game state across save/erase cycles, meaning that character.sandbox -/* no longer has to be the end-all be-all repository of game state. */ -var doErase = function(force) { - var saveId = getSaveId(); - if (localStorage.getItem(saveId)) { - if (force || confirm("erase_message".l())) { - localStorage.removeItem(saveId); - window.location.reload(); - } - } -}; - -/* Find and return a list of ids for all situations with the given tag. */ -var getSituationIdsWithTag = function(tag) { - var result = []; - for (var situationId in game.situations) { - var situation = game.situations[situationId]; - - for (var i = 0; i < situation.tags.length; ++i) { - if (situation.tags[i] == tag) { - result.push(situationId); - break; - } - } - } - return result; -}; - -/* Clear the current game output and start again. */ -var startGame = function() { - progress.seed = new Date().toString(); - - character = new Character(); - system.rnd = new Random(progress.seed); - progress.sequence = [{link:game.start, when:0}]; - - // Empty the display - $("#content").empty(); - - // Start the game - startTime = new Date().getTime() * 0.001; - system.time = 0; - if (game.init) game.init(character, system); - - // Do the first state. - doTransitionTo(game.start); -}; - -/* Saves the character to local storage. */ -var saveGame = function() { - // Store when we're saving the game, to avoid exploits where a - // player loads their file to gain extra time. - var now = (new Date()).getTime() * 0.001; - progress.saveTime = now - startTime; - - // Save the game. - localStorage.setItem(getSaveId(), JSON.stringify(progress)); - - // Switch the button highlights. - $("#erase").removeClass('disabled'); - $("#load").removeClass('disabled'); - $("#save").addClass('disabled'); -}; - -/* Loads the game from the given data */ -var loadGame = function(characterData) { - progress = characterData; - - character = new Character(); - system.rnd = new Random(progress.seed); - - // Empty the display - $("#content").empty(); - $("#intro").empty(); - - // Now play through the actions so far: - if (game.init) game.init(character, system); - - // Run through all the player's history. - interactive = false; - for (var i = 0; i < progress.sequence.length; i++) { - var step = progress.sequence[i]; - // The action must be done at the recorded time. - system.time = step.when; - processLink(step.link); - } - interactive = true; - - // Reverse engineer the start time. - var now = new Date().getTime() * 0.001; - startTime = now - progress.saveTime; -}; - -// ----------------------------------------------------------------------- -// Setup -// ----------------------------------------------------------------------- - -var begin = function () { -/* Set up the game when everything is loaded. */ - - // Handle storage. - if (hasLocalStorage()) { - var erase = $("#erase").click(function() { - doErase(); - }); - var save = $("#save").click(saveGame); - - var storedCharacter = localStorage.getItem(getSaveId()); - if (storedCharacter) { - try { - loadGame(JSON.parse(storedCharacter)); - save.addClass('disabled') - erase.removeClass('disabled') - } catch(err) { - doErase(true); - } - } else { - startGame(); - } - } else { - startGame(); - } - // Any point that an option list appears, its options are its - // first links. - $("body").on('click', "ul.options li, #menu li", function(event) { - // Make option clicks pass through to their first link. - var link = $("a", this); - if (link.length > 0) { - $(link.get(0)).click(); - } - }); -}; - - ### This is the control structure, it has minimal amount of data and this data is volatile anyway (as in, it won't get saved). @@ -587,116 +450,215 @@ Salet = { @enableSaving() disableSaving: () -> + $("#save").addClass('disabled'); enableSaving: () -> $("#save").removeClass('disabled'); - /* This gets called to actually do the work of processing a code. - * When one doLink is called (or a link is clicked), this may set call - * code that further calls doLink, and so on. This method processes - * each one, and processLink manages this. - */ -var processOneLink = function(code) { - var match = code.match(linkRe); - assert(match, "link_not_valid".l({link:code})); + ### + This gets called to actually do the work of processing a code. + When one doLink is called (or a link is clicked), this may set call + code that further calls doLink, and so on. This method processes + each one, and processLink manages this. + ### + processOneLink: = (code) -> + match = code.match(linkRe) + assert(match, "link_not_valid".l({link:code})) - var situation = match[1]; - var action = match[3]; + situation = match[1] + action = match[3] - // Change the situation - if (situation !== '.') { - if (situation !== current) { - doTransitionTo(situation); - } - } else { - // We should have an action if we have no situation change. - assert(action, "link_no_action".l()); - } + # Change the situation + if (situation !== '.') + if (situation !== current) + 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(); - if (situation) { - if (game.beforeAction) { - // Try the global act handler, and see if we need - // to notify the situation. - var consumed = game.beforeAction( - character, system, current, action - ); - if (consumed !== true) { - situation.act(character, system, action); - } - } else { - // We have no global act handler, always notify - // the situation. - situation.act(character, system, action); - } - if (game.afterAction) { - game.afterAction(character, system, current, action); - } - } - } -}; + # Carry out the action + if (action) + situation = getCurrentSituation() + if (situation) + if (game.beforeAction) + # Try the global act handler, and see if we need + # to notify the situation. + consumed = game.beforeAction( + character, system, current, action + ) + if (consumed != true) + situation.act(character, system, action) + else + # We have no global act handler, always notify the situation. + situation.act(character, system, action) + + if (game.afterAction) + game.afterAction(character, system, current, action) -/* This gets called when the user clicks a link to carry out an - * action. */ -var processClick = function(code) { - var now = (new Date()).getTime() * 0.001; - system.time = now - startTime; - progress.sequence.push({link:code, when:system.time}); - processLink(code); -}; + # 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}) + processLink(code) -/* Transitions between situations. */ -var doTransitionTo = function(newSituationId) { - var oldSituationId = current; - var oldSituation = getCurrentSituation(); - var newSituation = game.situations[newSituationId]; + # Transitions between situations. + doTransitionTo: (newSituationId) -> + oldSituationId = current + oldSituation = getCurrentSituation() + newSituation = game.situations[newSituationId] - assert(newSituation, "unknown_situation".l({id: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); - } + # 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); - // Remove links and transient sections. - $('#content a').each(function(index, element) { - var a = $(element); - if (a.hasClass('sticky') || a.attr("href").match(/[?&]sticky[=&]?/)) - return; - a.replaceWith($("").addClass("ex_link").html(a.html())); - }); - var contentToHide = $('#content .transient, #content ul.options'); - contentToHide.add($("#content a").filter(function(){ - return $(this).attr("href").match(/[?&]transient[=&]?/); - })); - if (interactive) { - contentToHide. - animate({opacity: 0}, 350). - slideUp(500, function() { - $(this).remove(); - }); - } else { - contentToHide.remove(); - } - } + # Remove links and transient sections. + $('#content a').each((index, element) -> + a = $(element); + if (a.hasClass('sticky') || a.attr("href").match(/[?&]sticky[=&]?/)) + return; + a.replaceWith($("").addClass("ex_link").html(a.html())) + ) + contentToHide = $('#content .transient, #content ul.options') + contentToHide.add($("#content a").filter(() -> + return $(this).attr("href").match(/[?&]transient[=&]?/) + )) + if (interactive) + contentToHide.animate({opacity: 0}, 350). + slideUp(500, () -> + $(this).remove() + ) + else + contentToHide.remove() - // Move the character. - current = newSituationId; + # Move the character. + current = newSituationId - // Notify the incoming situation. - if (game.enter) { - game.enter(character, system, oldSituationId, newSituationId); - } - newSituation.entering(character, system, oldSituationId); + # Notify the incoming situation. + if (game.enter) + game.enter(character, system, oldSituationId, newSituationId) + newSituation.entering(character, system, oldSituationId) - // additional hook for when the situation text has already been printed - if (game.afterEnter) { - game.afterEnter(character, system, oldSituationId, newSituationId); - } -}; + # additional hook for when the situation text has already been printed + if (game.afterEnter) + game.afterEnter(character, system, oldSituationId, newSituationId) + + ### + Erases the character in local storage. This is permanent! + To restart the game afterwards, we perform a simple page refresh. + This guarantees authors don't have to care about "tainting" the + 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) => + 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) => + result = [] + for (situationId, situation of @situations) + for (i = 0; i < situation.tags.length; ++i) + if (situation.tags[i] == tag) + result.push(situationId) + break + return result + + # Saves the character to local storage. + saveGame: () -> + # 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 + + # Save the game. + localStorage.setItem(getSaveId(), JSON.stringify(progress)) + + # Switch the button highlights. + $("#erase").removeClass('disabled') + $("#load").removeClass('disabled') + @disableSaving() + + # Loads the game from the given data + loadGame = (characterData) -> + progress = characterData + + character = new Character() + system.rnd = new Random(progress.seed) + + # Empty the display + $("#content").empty() + $("#intro").empty() + + # Now play through the actions so far: + if (game.init) + game.init(character, system) + + # Run through all the player's history. + interactive = false + for (i = 0; i < progress.sequence.length; i++) + step = progress.sequence[i] + # The action must be done at the recorded time. + system.time = step.when + processLink(step.link) + interactive = true + + # Reverse engineer the start time. + now = new Date().getTime() * 0.001 + startTime = now - progress.saveTime + + game_begin: () -> + # Handle storage. + storedCharacter = false + if (@hasLocalStorage()) + storedCharacter = localStorage.getItem(@getSaveId()) + + if (storedCharacter) + try + @loadGame(JSON.parse(storedCharacter)) + @disableSaving() + $("#erase").removeClass('disabled') + catch(err) + @erase_save(true) + else + progress.seed = new Date().toString() + + character = new Character() + system.rnd = new Random(progress.seed) + progress.sequence = [{link:game.start, when:0}] + + # Empty the display + $("#content").empty() + + # Start the game + startTime = new Date().getTime() * 0.001 + system.time = 0 + if (game.init) + game.init(character, system) + + # Do the first state. + doTransitionTo(game.start); + + # Any point that an option list appears, its options are its first links. + $("body").on('click', "ul.options li, #menu li", (event) -> + # Make option clicks pass through to their first link. + link = $("a", this) + if (link.length > 0) + $(link.get(0)).click() + ); } +# Set up the game when everything is loaded. +$(document).ready(() -> + salet = new Salet + if (salet.hasLocalStorage()) + $("#erase").click(salet.erase_save) # is Salet defined here? + $("#save").click(salet.saveGame) + + salet.game_begin() +)