1
0
Fork 0
mirror of https://gitlab.com/Oreolek/salet.git synced 2024-07-04 07:45:03 +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')
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")

View file

@ -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) ->

View file

@ -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 ?= {}

View file

@ -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 "<img class='img-responsive' src='#{picture}' alt='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:

View file

@ -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

View file

@ -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()