1
0
Fork 0
mirror of https://gitlab.com/Oreolek/salet.git synced 2024-07-07 01:04:25 +03:00

Trying to sort out the API

This commit is contained in:
Alexander Yakovlev 2016-01-24 14:53:03 +07:00
parent 5f7f142028
commit 63dbeb49f4
6 changed files with 105 additions and 95 deletions

View file

@ -3,11 +3,10 @@ room = require("../../lib/room.coffee")
obj = require('../../lib/obj.coffee') obj = require('../../lib/obj.coffee')
dialogue = require('../../lib/dialogue.coffee') dialogue = require('../../lib/dialogue.coffee')
oneOf = require('../../lib/oneOf.coffee') oneOf = require('../../lib/oneOf.coffee')
require('../../lib/interface.coffee')
salet = require('../../lib/salet.coffee') salet = require('../../lib/salet.coffee')
undum.game.id = "your-game-id-here" salet.game_id = "your-game-id-here"
undum.game.version = "1.0" salet.game_version = "1.0"
### ###
Element helpers. There is no real need to build monsters like a().id("hello") Element helpers. There is no real need to build monsters like a().id("hello")

View file

@ -1,6 +1,4 @@
# This is where you initialize your game. # This is where you initialize your game.
# All code in this file comes last, so the game is almost ready by this point. # All code in this file comes last, so the game is almost ready by this point.
undum.game.init = (character, system) -> salet.init = (character, system) ->
window.onload = undum.begin

View file

@ -37,11 +37,13 @@ class SaletObj
location: "" location: ""
put: (location) => put: (location) =>
@level = 0 # this is scenery @level = 0 # this is scenery
if undum.game.situations[location]? if salet.rooms[location]?
undum.game.situations[location].take(this) salet.rooms[location].take(this)
@location = location @location = location
delete: () => delete: (location = false) =>
undum.game.situations[@location].objects.remove(this) if location == false
location = @location
salet.rooms[location].drop(this)
obj = (name, spec) -> obj = (name, spec) ->
spec ?= {} spec ?= {}

View file

@ -5,6 +5,7 @@ markdown = require('./markdown.coffee')
cycle = require('./cycle.coffee') cycle = require('./cycle.coffee')
Random = require('./random.js') Random = require('./random.js')
languages = require('./localize.coffee') languages = require('./localize.coffee')
salet = require('./salet.coffee')
# Assertion # Assertion
assert = console.assert assert = console.assert
@ -28,12 +29,12 @@ update_ways = (ways, name) ->
content = "" content = ""
distances = [] distances = []
if ways then for way in ways if ways then for way in ways
if undum.game.situations[way]? if salet.rooms[way]?
title = undum.game.situations[way].title.fcall(this, name) title = salet.rooms[way].title.fcall(this, name)
content += way_to(title, way) content += way_to(title, way)
distances.push({ distances.push({
key: way key: way
distance: undum.game.situations[way].distance distance: salet.rooms[way].distance
}) })
else else
document.querySelector(".ways h2").style.display = "none" document.querySelector(".ways h2").style.display = "none"
@ -61,9 +62,8 @@ picture_tag = (picture) ->
""" """
return "<img class='img-responsive' src='#{picture}' alt='Room illustration'>" return "<img class='img-responsive' src='#{picture}' alt='Room illustration'>"
class SaletRoom extends undum.Situation class SaletRoom
constructor: (spec) -> constructor: (spec) ->
undum.Situation.call(this, spec)
for index, value of spec for index, value of spec
this[index] = value this[index] = value
return this return this
@ -124,8 +124,8 @@ class SaletRoom extends undum.Situation
if f != @name and f? if f != @name and f?
@visited++ @visited++
if undum.game.situations[f].exit? if salet.rooms[f].exit?
undum.game.situations[f].exit(character, system, @name) salet.rooms[f].exit(character, system, @name)
if @enter if @enter
@enter character, system, f @enter character, system, f
@ -184,7 +184,7 @@ class SaletRoom extends undum.Situation
@objects[thing.name] = thing @objects[thing.name] = thing
# BUG: for some really weird reason if the call is made in init function or # 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. # during the initialization, this ALSO puts the thing in the start room.
undum.game.situations["start"].objects = {} salet.rooms["start"].objects = {}
drop: (name) => drop: (name) =>
delete @objects[name] delete @objects[name]
@ -261,7 +261,7 @@ class SaletRoom extends undum.Situation
if not @name? if not @name?
console.error("Situation has no name") console.error("Situation has no name")
return this return this
undum.game.situations[@name] = this salet.rooms[@name] = this
return this return this
writers: writers:

View file

@ -1,5 +1,6 @@
markdown = require('./markdown.coffee') markdown = require('./markdown.coffee')
view = require('./view.coffee') view = require('./view.coffee')
Random = require('./random.js')
### ###
fcall() (by analogy with fmap) is added to the prototypes of both String and 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 = () -> String.prototype.fcall = () ->
return this return this
assert = (msg, assertion) -> console.assert assertion, msg
class Character
# Utility functions # Utility functions
parseFn = (str) -> parseFn = (str) ->
@ -18,23 +23,12 @@ parseFn = (str) ->
return str return str
fstr = """ fstr = """
(function(character, system, situation) { (function(character, situation) {
#{str} #{str}
#}) #})
""" """
return eval(fstr) 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. # Regular expression to catch every link action.
# Salet's default is a general URL-safe expression. # Salet's default is a general URL-safe expression.
linkRe = /^([0-9A-Za-z_-]+|\.)(\/([0-9A-Za-z_-]+))?$/ 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 we're a once-click, remove all matching links.
if (a.hasClass("once") || href.match(/[?&]once[=&]?/)) if (a.hasClass("once") || href.match(/[?&]once[=&]?/))
system.clearLinks(href) view.clearLinks(href)
processClick(href) processClick(href)
return false return false
@ -68,7 +62,11 @@ augmentLinks = (content) ->
This is the control structure, it has minimal amount of data and This is the control structure, it has minimal amount of data and
this data is volatile anyway (as in, it won't get saved). 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 rnd: null
time: 0 time: 0
@ -84,23 +82,23 @@ Salet = {
(setting initial quality values, setting the (setting initial quality values, setting the
character-text. This is optional, however, as set-up character-text. This is optional, however, as set-up
processing could also be done by the first situation's processing could also be done by the first situation's
enter function. If this function is given it should have enter function.
the signature function(character, system).
### ###
init: null init: (character) ->
@character = character
### ###
This function is called before entering any new This function is called before entering any new
situation. It is called before the corresponding situation situation. It is called before the corresponding situation
has its `enter` method called. has its `enter` method called.
### ###
entering: (character, system, oldSituationId, newSituationId) -> entering: (character, oldSituationId, newSituationId) ->
### ###
Hook for when the situation has already been carried out Hook for when the situation has already been carried out
and printed. and printed.
### ###
afterEnter: (character, system, oldSituationId, newSituationId) -> afterEnter: (character, oldSituationId, newSituationId) ->
### ###
This function is called before carrying out any action in 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 on to the situation. Note that this is the only one of
these global handlers that can consume the event. 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 This function is called after carrying out any action in
any situation. It is called after the corresponding any situation. It is called after the corresponding
situation has its `act` method called. 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 This function is called after leaving any situation. It is
called after the corresponding situation has its `exit` called after the corresponding situation has its `exit`
method called. method called.
### ###
exit: (character, system, oldSituationId, newSituationId) -> exit: (character, oldSituationId, newSituationId) ->
### ###
Returns a list of situation ids to choose from, given a set of Returns a list of situation ids to choose from, given a set of
@ -202,15 +200,15 @@ Salet = {
allIds[tagOrId] = true allIds[tagOrId] = true
#Filter out anything that can't be viewed right now. #Filter out anything that can't be viewed right now.
currentSituation = getCurrentSituation() currentSituation = @getCurrentRoom()
viewableSituationData = [] viewableSituationData = []
for situationId in allIds for situationId in allIds
situation = game.situations[situationId] situation = @rooms[situationId]
assert(situation, "unknown_situation".l({id: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. #While we're here, get the selection data.
viewableSituationDatum = situation.choiceData(character, system, currentSituation) viewableSituationDatum = situation.choiceData(character, salet, currentSituation)
viewableSituationDatum.id = situationId viewableSituationDatum.id = situationId
viewableSituationData.push(viewableSituationDatum) viewableSituationData.push(viewableSituationDatum)
@ -283,10 +281,6 @@ Salet = {
# The Id of the current situation the player is in. # The Id of the current situation the player is in.
current: null; 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. # Tracks whether we're in interactive mode or batch mode.
interactive: true interactive: true
@ -296,14 +290,14 @@ Salet = {
# The stack of links, resulting from the last action, still be to resolved. # The stack of links, resulting from the last action, still be to resolved.
linkStack: null linkStack: null
getCurrentSituation: () => getCurrentRoom: () =>
if (@current) if (@current)
return @rooms[@current] return @rooms[@current]
return null return null
# Gets the unique id used to identify saved games. # Gets the unique id used to identify saved games.
getSaveId: (slot = "") -> 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 # This gets called when a link needs to be followed, regardless
# of whether it was user action that initiated it. # of whether it was user action that initiated it.
@ -349,49 +343,49 @@ Salet = {
# Change the situation # Change the situation
if situation != '.' if situation != '.'
if situation != current if situation != current
doTransitionTo(situation) @doTransitionTo(situation)
else else
# We should have an action if we have no situation change. # We should have an action if we have no situation change.
assert(action, "link_no_action".l()) assert(action, "link_no_action".l())
# Carry out the action # Carry out the action
if (action) if (action)
situation = getCurrentSituation() situation = @getCurrentRoom()
if (situation) if (situation)
if (game.beforeAction) if (@beforeAction)
# Try the global act handler, and see if we need # Try the global act handler, and see if we need
# to notify the situation. # to notify the situation.
consumed = game.beforeAction( consumed = @beforeAction(
character, system, current, action character, current, action
) )
if (consumed != true) if (consumed != true)
situation.act(character, system, action) situation.act(character, action)
else else
# We have no global act handler, always notify the situation. # We have no global act handler, always notify the situation.
situation.act(character, system, action) situation.act(character, action)
if (game.afterAction) if (@afterAction)
game.afterAction(character, system, current, action) @afterAction(character, current, action)
# This gets called when the user clicks a link to carry out an action. # This gets called when the user clicks a link to carry out an action.
processClick: (code) -> processClick: (code) ->
now = (new Date()).getTime() * 0.001 now = (new Date()).getTime() * 0.001
system.time = now - startTime @time = now - startTime
progress.sequence.push({link:code, when:system.time}) @progress.sequence.push({link:code, when:@time})
processLink(code) processLink(code)
# Transitions between situations. # Transitions between situations.
doTransitionTo: (newSituationId) -> doTransitionTo: (newSituationId) ->
oldSituationId = current oldSituationId = current
oldSituation = getCurrentSituation() oldSituation = @getCurrentRoom()
newSituation = game.situations[newSituationId] newSituation = @rooms[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. # We might not have an old situation if this is the start of the game.
if (oldSituation) if (oldSituation)
if (game.exit) if (@exit)
game.exit(character, system, oldSituationId, newSituationId); @exit(@character, oldSituationId, newSituationId);
# Remove links and transient sections. # Remove links and transient sections.
view.remove_transient(@interactive) view.remove_transient(@interactive)
@ -400,13 +394,13 @@ Salet = {
current = newSituationId current = newSituationId
# Notify the incoming situation. # Notify the incoming situation.
if (game.enter) if (@enter)
game.enter(character, system, oldSituationId, newSituationId) @enter(@character, this, oldSituationId, newSituationId)
newSituation.entering(character, system, oldSituationId) newSituation.entering(@character, oldSituationId)
# additional hook for when the situation text has already been printed # additional hook for when the situation text has already been printed
if (game.afterEnter) if (@afterEnter)
game.afterEnter(character, system, oldSituationId, newSituationId) @afterEnter(@character, oldSituationId, newSituationId)
### ###
Erases the character in local storage. This is permanent! 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 # Store when we're saving the game, to avoid exploits where a
# player loads their file to gain extra time. # player loads their file to gain extra time.
now = (new Date()).getTime() * 0.001 now = (new Date()).getTime() * 0.001
progress.saveTime = now - startTime @progress.saveTime = now - startTime
# Save the game. # Save the game.
localStorage.setItem(getSaveId(), JSON.stringify(progress)) localStorage.setItem(getSaveId(), JSON.stringify(@progress))
# Switch the button highlights. # Switch the button highlights.
view.disableSaving() view.disableSaving()
@ -448,33 +442,33 @@ Salet = {
# Loads the game from the given data # Loads the game from the given data
loadGame: (characterData) -> loadGame: (characterData) ->
progress = characterData @progress = characterData
character = new Character() character = new Character()
system.rnd = new Random(progress.seed) @rnd = new Random(@progress.seed)
view.clearContent() view.clearContent()
# Now play through the actions so far: # Now play through the actions so far:
if (game.init) if (@init)
game.init(character, system) @init(character)
# Run through all the player's history. # Run through all the player's history.
interactive = false interactive = false
for step in progress.sequence for step in @progress.sequence
# The action must be done at the recorded time. # The action must be done at the recorded time.
system.time = step.when @time = step.when
processLink(step.link) processLink(step.link)
interactive = true interactive = true
# Reverse engineer the start time. # Reverse engineer the start time.
now = new Date().getTime() * 0.001 now = new Date().getTime() * 0.001
startTime = now - progress.saveTime startTime = now - @progress.saveTime
game_begin: () -> beginGame: () ->
# Handle storage. # Handle storage.
storedCharacter = false storedCharacter = false
if (@hasLocalStorage()) if (view.hasLocalStorage())
storedCharacter = localStorage.getItem(@getSaveId()) storedCharacter = localStorage.getItem(@getSaveId())
if (storedCharacter) if (storedCharacter)
@ -485,22 +479,22 @@ Salet = {
catch err catch err
@erase_save(true) @erase_save(true)
else else
progress.seed = new Date().toString() @progress.seed = new Date().toString()
character = new Character() character = new Character()
system.rnd = new Random(progress.seed) @rnd = new Random(@progress.seed)
progress.sequence = [{link:game.start, when:0}] @progress.sequence = [{link:@start, when:0}]
view.clearContent() view.clearContent()
# Start the game # Start the game
startTime = new Date().getTime() * 0.001 startTime = new Date().getTime() * 0.001
system.time = 0 @time = 0
if (game.init) if (@init)
game.init(character, system) @init(character)
# Do the first state. # Do the first state.
doTransitionTo(game.start); @doTransitionTo(@start);
# Any point that an option list appears, its options are its first links. # Any point that an option list appears, its options are its first links.
$("body").on('click', "ul.options li, #menu li", (event) -> $("body").on('click', "ul.options li, #menu li", (event) ->
@ -510,14 +504,15 @@ Salet = {
$(link.get(0)).click() $(link.get(0)).click()
); );
} salet = new Salet
# Set up the game when everything is loaded. # Set up the game when everything is loaded.
$(document).ready(() -> $(document).ready(() ->
salet = new Salet if (view.hasLocalStorage())
if (salet.hasLocalStorage())
$("#erase").click(salet.erase_save) # is Salet defined here? $("#erase").click(salet.erase_save) # is Salet defined here?
$("#save").click(salet.saveGame) $("#save").click(salet.saveGame)
salet.game_begin() salet.beginGame()
) )
module.exports = salet

View file

@ -3,13 +3,18 @@ Salet interface configuration.
In a typical MVC structure, this is the View. In a typical MVC structure, this is the View.
Only it knows about the DOM structure. Only it knows about the DOM structure.
Other modules just use its API or prepare the HTML for insertion. 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 class SaletView
init: () -> init: () ->
$("#ways").on("click", "a", (event) -> $("#ways").on("click", "a", (event) ->
event.preventDefault() event.preventDefault()
undum.processClick($(this).attr("href")) salet.processClick($(this).attr("href"))
) )
$("#inventory").on("click", "a", (event) -> $("#inventory").on("click", "a", (event) ->
event.preventDefault() event.preventDefault()
@ -52,7 +57,7 @@ class SaletView
### ###
clearContent: (elementSelector = "#content") -> clearContent: (elementSelector = "#content") ->
if (elementSelector == "#content") # empty the intro with the content if (elementSelector == "#content") # empty the intro with the content
document.findElementById("intro").innerHTML = "" document.getElementById("intro").innerHTML = ""
document.querySelector(elementSelector).innerHTML = "" document.querySelector(elementSelector).innerHTML = ""
# Write content to current room # Write content to current room
@ -176,6 +181,17 @@ class SaletView
# it somewhere near the bottom. # it somewhere near the bottom.
scrollBottomTo(newBottom+100 - optionHeight); 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 view = new SaletView
$(document).ready(() -> $(document).ready(() ->
view.init() view.init()